238 lines
5.2 KiB
Go
238 lines
5.2 KiB
Go
// SiYuan - Build Your Eternal Digital Garden
|
|
// Copyright (c) 2020-present, b3log.org
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
//go:build !android && !ios
|
|
|
|
package filesys
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/88250/flock"
|
|
"github.com/88250/gulu"
|
|
"github.com/siyuan-note/siyuan/kernel/util"
|
|
)
|
|
|
|
var ErrUnableLockFile = errors.New("unable to lock file")
|
|
|
|
var (
|
|
fileLocks = sync.Map{}
|
|
expiration = 5 * time.Minute
|
|
fileReadWriteLock = sync.Mutex{}
|
|
)
|
|
|
|
type LockItem struct {
|
|
fl *flock.Flock
|
|
expired int64
|
|
}
|
|
|
|
func init() {
|
|
go func() {
|
|
// 锁定超时自动解锁
|
|
for range time.Tick(10 * time.Second) {
|
|
fileReadWriteLock.Lock()
|
|
|
|
now := time.Now().UnixNano()
|
|
var expiredKeys []string
|
|
fileLocks.Range(func(k, v interface{}) bool {
|
|
lockItem := v.(*LockItem)
|
|
if now > lockItem.expired {
|
|
expiredKeys = append(expiredKeys, k.(string))
|
|
}
|
|
return true
|
|
})
|
|
|
|
for _, k := range expiredKeys {
|
|
if err := unlockFile0(k); nil != err {
|
|
util.LogErrorf("unlock file [%s] failed: %s", k, err)
|
|
continue
|
|
}
|
|
|
|
//util.LogInfof("released file lock [%s]", k)
|
|
}
|
|
|
|
fileReadWriteLock.Unlock()
|
|
}
|
|
}()
|
|
}
|
|
|
|
func ReleaseFileLocks(localAbsPath string) {
|
|
fileReadWriteLock.Lock()
|
|
defer fileReadWriteLock.Unlock()
|
|
|
|
fileLocks.Range(func(k, v interface{}) bool {
|
|
if strings.HasPrefix(k.(string), localAbsPath) {
|
|
if err := unlockFile0(k.(string)); nil != err {
|
|
util.LogErrorf("unlock file [%s] failed: %s", k, err)
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
func ReleaseAllFileLocks() {
|
|
fileReadWriteLock.Lock()
|
|
defer fileReadWriteLock.Unlock()
|
|
|
|
fileLocks.Range(func(k, v interface{}) bool {
|
|
if err := unlockFile0(k.(string)); nil != err {
|
|
util.LogErrorf("unlock file [%s] failed: %s", k, err)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
func NoLockFileRead(filePath string) (data []byte, err error) {
|
|
fileReadWriteLock.Lock()
|
|
defer fileReadWriteLock.Unlock()
|
|
|
|
v, ok := fileLocks.Load(filePath)
|
|
if !ok {
|
|
return os.ReadFile(filePath)
|
|
}
|
|
lockItem := v.(*LockItem)
|
|
handle := lockItem.fl.Fh()
|
|
if _, err = handle.Seek(0, io.SeekStart); nil != err {
|
|
return
|
|
}
|
|
return io.ReadAll(handle)
|
|
}
|
|
|
|
func LockFileRead(filePath string) (data []byte, err error) {
|
|
fileReadWriteLock.Lock()
|
|
defer fileReadWriteLock.Unlock()
|
|
|
|
if !gulu.File.IsExist(filePath) {
|
|
err = os.ErrNotExist
|
|
return
|
|
}
|
|
|
|
lock, lockErr := lockFile0(filePath)
|
|
if nil != lockErr {
|
|
err = lockErr
|
|
return
|
|
}
|
|
|
|
handle := lock.Fh()
|
|
if _, err = handle.Seek(0, io.SeekStart); nil != err {
|
|
return
|
|
}
|
|
return io.ReadAll(handle)
|
|
}
|
|
|
|
func NoLockFileWrite(filePath string, data []byte) (err error) {
|
|
fileReadWriteLock.Lock()
|
|
defer fileReadWriteLock.Unlock()
|
|
|
|
v, ok := fileLocks.Load(filePath)
|
|
if !ok {
|
|
return os.WriteFile(filePath, data, 0644)
|
|
}
|
|
|
|
lockItem := v.(*LockItem)
|
|
handle := lockItem.fl.Fh()
|
|
err = gulu.File.WriteFileSaferByHandle(handle, data)
|
|
return
|
|
}
|
|
|
|
func LockFileWrite(filePath string, data []byte) (err error) {
|
|
fileReadWriteLock.Lock()
|
|
defer fileReadWriteLock.Unlock()
|
|
|
|
lock, lockErr := lockFile0(filePath)
|
|
if nil != lockErr {
|
|
err = lockErr
|
|
return
|
|
}
|
|
|
|
handle := lock.Fh()
|
|
err = gulu.File.WriteFileSaferByHandle(handle, data)
|
|
return
|
|
}
|
|
|
|
func IsLocked(filePath string) bool {
|
|
v, _ := fileLocks.Load(filePath)
|
|
if nil == v {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func UnlockFile(filePath string) (err error) {
|
|
fileReadWriteLock.Lock()
|
|
defer fileReadWriteLock.Unlock()
|
|
return unlockFile0(filePath)
|
|
}
|
|
|
|
func unlockFile0(filePath string) (err error) {
|
|
v, _ := fileLocks.Load(filePath)
|
|
if nil == v {
|
|
return
|
|
}
|
|
lockItem := v.(*LockItem)
|
|
err = lockItem.fl.Unlock()
|
|
fileLocks.Delete(filePath)
|
|
return
|
|
}
|
|
|
|
func LockFile(filePath string) (err error) {
|
|
fileReadWriteLock.Lock()
|
|
defer fileReadWriteLock.Unlock()
|
|
_, err = lockFile0(filePath)
|
|
return
|
|
}
|
|
|
|
func lockFile0(filePath string) (lock *flock.Flock, err error) {
|
|
lockItemVal, _ := fileLocks.Load(filePath)
|
|
var lockItem *LockItem
|
|
if nil == lockItemVal {
|
|
lock = flock.New(filePath)
|
|
var locked bool
|
|
var lockErr error
|
|
for i := 0; i < 7; i++ {
|
|
locked, lockErr = lock.TryLock()
|
|
if nil != lockErr || !locked {
|
|
time.Sleep(100 * time.Millisecond)
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
if nil != lockErr {
|
|
util.LogErrorf("lock file [%s] failed: %s", filePath, lockErr)
|
|
err = ErrUnableLockFile
|
|
return
|
|
}
|
|
|
|
if !locked {
|
|
util.LogErrorf("unable to lock file [%s]", filePath)
|
|
err = ErrUnableLockFile
|
|
return
|
|
}
|
|
lockItem = &LockItem{fl: lock}
|
|
} else {
|
|
lockItem = lockItemVal.(*LockItem)
|
|
lock = lockItem.fl
|
|
}
|
|
lockItem.expired = time.Now().Add(expiration).UnixNano()
|
|
fileLocks.Store(filePath, lockItem)
|
|
return
|
|
}
|