diff --git a/client2/internal/mount/fuse/fuse.go b/client2/internal/mount/fuse/fuse.go index 78431b6..a69ccd1 100644 --- a/client2/internal/mount/fuse/fuse.go +++ b/client2/internal/mount/fuse/fuse.go @@ -47,6 +47,9 @@ func translateError(err error) syscall.Errno { case ErrIOError: return syscall.EIO + case ErrNotSupported: + return syscall.ENOTSUP + default: return syscall.EIO } diff --git a/client2/internal/mount/fuse/types.go b/client2/internal/mount/fuse/types.go index c579462..50c2b36 100644 --- a/client2/internal/mount/fuse/types.go +++ b/client2/internal/mount/fuse/types.go @@ -8,11 +8,12 @@ import ( ) var ( - ErrNotExists = os.ErrNotExist - ErrExists = os.ErrExist - ErrNotEmpty = errors.New("directory not empty") - ErrPermission = os.ErrPermission - ErrIOError = errors.New("I/O error") + ErrNotExists = os.ErrNotExist + ErrExists = os.ErrExist + ErrNotEmpty = errors.New("directory not empty") + ErrPermission = os.ErrPermission + ErrIOError = errors.New("I/O error") + ErrNotSupported = errors.New("not supported") ) type Fs interface { diff --git a/client2/internal/mount/vfs/cache/cache.go b/client2/internal/mount/vfs/cache/cache.go index c4787e8..8130a68 100644 --- a/client2/internal/mount/vfs/cache/cache.go +++ b/client2/internal/mount/vfs/cache/cache.go @@ -2,7 +2,6 @@ package cache import ( "errors" - "fmt" "os" "path/filepath" "sync" @@ -159,7 +158,7 @@ func (c *Cache) LoadFile(pathComps []string, obj *cdssdk.Object) *CacheFile { return nil } - ch, err = makeCacheFileFromObject(c, pathComps, obj) + ch, err = newCacheFileFromObject(c, pathComps, obj) if err != nil { // TODO 日志记录 logger.Tracef("make cache file from object: %v", err) @@ -299,6 +298,51 @@ func (c *Cache) Remove(pathComps []string) error { // 移动指定路径的缓存文件或目录到新的路径。如果目标路径已经存在,则会报错。 // // 如果移动成功,则返回移动后的缓存文件或目录。如果文件或目录不存在,则返回nil。 -func (c *Cache) Move(pathComps []string, newPathComps []string) (CacheEntry, error) { - return nil, fmt.Errorf("not implemented") +func (c *Cache) Move(pathComps []string, newPathComps []string) error { + c.lock.Lock() + defer c.lock.Unlock() + + _, ok := c.activeCache.WalkEnd(newPathComps) + if ok { + return fuse.ErrExists + } + + newMetaPath := c.GetCacheMetaPath(newPathComps...) + newDataPath := c.GetCacheDataPath(newPathComps...) + + _, err := os.Stat(newMetaPath) + if err == nil { + return fuse.ErrExists + } else if !os.IsNotExist(err) { + return err + } + + metaPath := c.GetCacheMetaPath(pathComps...) + dataPath := c.GetCacheDataPath(pathComps...) + + // 每个缓存文件持有meta文件和data文件的句柄,所以这里移动文件,不影响句柄的使用。 + // 只能忽略这里的错误 + os.Rename(metaPath, newMetaPath) + os.Rename(dataPath, newDataPath) + + // 更新缓存 + oldNode, ok := c.activeCache.WalkEnd(pathComps) + if ok { + newNode := c.activeCache.CreateWords(newPathComps) + newNode.Value = oldNode.Value + newNode.WordNexts = oldNode.WordNexts + oldNode.RemoveSelf(false) + + if newNode.Value != nil { + newNode.Value.Move(newPathComps) + } + newNode.Iterate(func(path []string, node *trie.Node[*CacheFile], isWordNode bool) trie.VisitCtrl { + if node.Value != nil { + node.Value.Move(lo2.AppendNew(newPathComps, path...)) + } + return trie.VisitContinue + }) + } + + return nil } diff --git a/client2/internal/mount/vfs/cache/file.go b/client2/internal/mount/vfs/cache/file.go index 953c44d..b156ae0 100644 --- a/client2/internal/mount/vfs/cache/file.go +++ b/client2/internal/mount/vfs/cache/file.go @@ -2,12 +2,14 @@ package cache import ( "fmt" + "io" "os" "sync" "time" "gitlink.org.cn/cloudream/common/pkgs/logger" cdssdk "gitlink.org.cn/cloudream/common/sdks/storage" + "gitlink.org.cn/cloudream/common/utils/io2" "gitlink.org.cn/cloudream/common/utils/lo2" "gitlink.org.cn/cloudream/common/utils/math2" "gitlink.org.cn/cloudream/common/utils/serder" @@ -88,7 +90,8 @@ type CacheFile struct { savingMetaChan chan any isDeleted bool - localFile *os.File + metaFile *os.File + dataFile *os.File writeLock *sync.RWMutex } @@ -107,14 +110,21 @@ func createNewCacheFile(cache *Cache, pathComps []string) (*CacheFile, error) { return nil, err } - err = os.WriteFile(metaPath, infoData, 0644) + metaFile, err := os.OpenFile(metaPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) if err != nil { - return nil, fmt.Errorf("save cache file: %w", err) + return nil, fmt.Errorf("create cache meta file: %w", err) } - localFile, err := os.OpenFile(dataPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) + err = io2.WriteAll(metaFile, infoData) if err != nil { - return nil, fmt.Errorf("create cache file: %w", err) + metaFile.Close() + return nil, fmt.Errorf("save cache meta file: %w", err) + } + + dataFile, err := os.OpenFile(dataPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) + if err != nil { + metaFile.Close() + return nil, fmt.Errorf("create cache data file: %w", err) } ch := &CacheFile{ @@ -124,7 +134,8 @@ func createNewCacheFile(cache *Cache, pathComps []string) (*CacheFile, error) { info: info, rwLock: &sync.RWMutex{}, savingMetaChan: make(chan any, 1), - localFile: localFile, + metaFile: metaFile, + dataFile: dataFile, writeLock: &sync.RWMutex{}, } @@ -137,20 +148,22 @@ func loadCacheFile(cache *Cache, pathComps []string) (*CacheFile, error) { metaPath := cache.GetCacheMetaPath(pathComps...) dataPath := cache.GetCacheDataPath(pathComps...) - metaData, err := os.ReadFile(metaPath) + metaFile, err := os.OpenFile(metaPath, os.O_CREATE|os.O_RDWR, 0644) if err != nil { - return nil, err + return nil, fmt.Errorf("open cache meta file: %w", err) } info := &FileInfo{} - err = serder.JSONToObject(metaData, info) + err = serder.JSONToObjectStream(metaFile, info) if err != nil { + metaFile.Close() return nil, err } - localFile, err := os.OpenFile(dataPath, os.O_CREATE|os.O_RDWR, 0644) + dataFile, err := os.OpenFile(dataPath, os.O_CREATE|os.O_RDWR, 0644) if err != nil { - return nil, fmt.Errorf("create cache file: %w", err) + metaFile.Close() + return nil, fmt.Errorf("open cache data file: %w", err) } ch := &CacheFile{ @@ -160,7 +173,8 @@ func loadCacheFile(cache *Cache, pathComps []string) (*CacheFile, error) { info: *info, rwLock: &sync.RWMutex{}, savingMetaChan: make(chan any, 1), - localFile: localFile, + metaFile: metaFile, + dataFile: dataFile, writeLock: &sync.RWMutex{}, } @@ -169,7 +183,7 @@ func loadCacheFile(cache *Cache, pathComps []string) (*CacheFile, error) { return ch, nil } -func makeCacheFileFromObject(cache *Cache, pathComps []string, obj *cdssdk.Object) (*CacheFile, error) { +func newCacheFileFromObject(cache *Cache, pathComps []string, obj *cdssdk.Object) (*CacheFile, error) { metaPath := cache.GetCacheMetaPath(pathComps...) dataPath := cache.GetCacheDataPath(pathComps...) @@ -185,13 +199,20 @@ func makeCacheFileFromObject(cache *Cache, pathComps []string, obj *cdssdk.Objec return nil, err } - err = os.WriteFile(metaPath, infoData, 0644) + metaFile, err := os.OpenFile(metaPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) if err != nil { - return nil, fmt.Errorf("save cache file: %w", err) + return nil, fmt.Errorf("create cache meta file: %w", err) } - localFile, err := os.OpenFile(dataPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) + err = io2.WriteAll(metaFile, infoData) if err != nil { + metaFile.Close() + return nil, fmt.Errorf("save cache meta file: %w", err) + } + + dataFile, err := os.OpenFile(dataPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) + if err != nil { + metaFile.Close() return nil, fmt.Errorf("create cache file: %w", err) } @@ -203,7 +224,8 @@ func makeCacheFileFromObject(cache *Cache, pathComps []string, obj *cdssdk.Objec remoteObj: obj, rwLock: &sync.RWMutex{}, savingMetaChan: make(chan any, 1), - localFile: localFile, + metaFile: metaFile, + dataFile: dataFile, writeLock: &sync.RWMutex{}, } @@ -285,6 +307,19 @@ func (f *CacheFile) Delete() { f.letSave() } +func (f *CacheFile) Move(newPathComps []string) { + f.writeLock.Lock() + defer f.writeLock.Unlock() + + f.rwLock.Lock() + defer f.rwLock.Unlock() + + f.pathComps = newPathComps + f.name = newPathComps[len(newPathComps)-1] + + f.letSave() +} + func (f *CacheFile) SetRemoteObject(obj *cdssdk.Object) { f.remoteObj = obj } @@ -342,7 +377,7 @@ func (f *CacheFile) Truncate(size int64) error { f.writeLock.Lock() defer f.writeLock.Unlock() - err := f.localFile.Truncate(size) + err := f.dataFile.Truncate(size) if err != nil { return err } @@ -408,7 +443,19 @@ func (f *CacheFile) savingMeta() { break } - err = os.WriteFile(f.cache.GetCacheMetaPath(f.pathComps...), jsonData, 0644) + err = f.metaFile.Truncate(0) + if err != nil { + // TODO 日志 + break + } + + _, err = f.metaFile.Seek(0, io.SeekStart) + if err != nil { + // TODO 日志 + break + } + + err = io2.WriteAll(f.metaFile, jsonData) if err != nil { // TODO 日志 break @@ -460,7 +507,7 @@ func (h *CacheFileReadWriter) ReadAt(buf []byte, off int64) (int, error) { rngIdx := FirstContainsIndex(h.file.info.Segments, curOff) if rngIdx >= 0 && h.file.info.Segments[rngIdx].End() > curOff { readLen := math2.Min(int64(len(curBuf)), h.file.info.Segments[rngIdx].End()-curOff) - realReadLen, err := h.file.localFile.ReadAt(curBuf[:readLen], curOff) + realReadLen, err := h.file.dataFile.ReadAt(curBuf[:readLen], curOff) totalReadLen += realReadLen h.file.rwLock.RUnlock() @@ -514,7 +561,7 @@ func (h *CacheFileReadWriter) ReadAt(buf []byte, off int64) (int, error) { // 写入到本地缓存文件 writeStart := loadRng.Position - curOff - _, err = h.file.localFile.WriteAt(curBuf[writeStart:writeStart+loadRng.Length], curOff) + _, err = h.file.dataFile.WriteAt(curBuf[writeStart:writeStart+loadRng.Length], curOff) if err != nil { h.file.writeLock.Unlock() @@ -549,7 +596,7 @@ func (h *CacheFileReadWriter) WriteAt(buf []byte, off int64) (int, error) { defer h.file.writeLock.RUnlock() // 写入到本地缓存文件 - writeLen, err := h.file.localFile.WriteAt(buf, off) + writeLen, err := h.file.dataFile.WriteAt(buf, off) if err != nil { return writeLen, fmt.Errorf("save to local file: %w", err) } @@ -569,7 +616,7 @@ func (h *CacheFileReadWriter) WriteAt(buf []byte, off int64) (int, error) { } func (f *CacheFileReadWriter) Sync() error { - return f.file.localFile.Sync() + return f.file.dataFile.Sync() } func (f *CacheFileReadWriter) Close() error { diff --git a/client2/internal/mount/vfs/fuse_bucket.go b/client2/internal/mount/vfs/fuse_bucket.go index 9650458..2c4a4cb 100644 --- a/client2/internal/mount/vfs/fuse_bucket.go +++ b/client2/internal/mount/vfs/fuse_bucket.go @@ -221,16 +221,43 @@ func (r *FuseBucket) RemoveChild(ctx context.Context, name string) error { } func (r *FuseBucket) MoveChild(ctx context.Context, oldName string, newName string, newParent fuse.FsDir) error { - - // TODO 有问题 - newParentNode := newParent.(FuseNode) - _, err := r.vfs.cache.Move([]string{r.bktName, oldName}, append(newParentNode.PathComps(), newName)) - if err != nil { - return err + newParentPath := newParentNode.PathComps() + + // 仅允许在不同桶之间移动 + if len(newParentPath) != 1 { + return fuse.ErrNotSupported } - return nil + d := r.vfs.db + return d.DoTx(func(tx db2.SQLContext) error { + _, err := d.Package().GetUserPackageByName(tx, 1, newParentPath[0], newName) + if err == nil { + // 目标节点已经存在,不能重命名,直接退出 + return fuse.ErrExists + } else if err != gorm.ErrRecordNotFound { + return err + } + + newBkt, err := d.Bucket().GetByName(tx, newParentPath[0]) + if err == nil { + oldPkg, err := d.Package().GetUserPackageByName(tx, 1, r.bktName, oldName) + if err == nil { + err = d.Package().Move(tx, oldPkg.PackageID, newBkt.BucketID, newName) + if err != nil { + return err + } + } else if err != gorm.ErrRecordNotFound { + return err + } + + } else if err != gorm.ErrRecordNotFound { + return err + } + + // 有可能是移动文件,所以如果是源文件夹未找到,也尝试进行移动 + return r.vfs.cache.Move([]string{r.bktName, oldName}, []string{newParentPath[0], newName}) + }) } func (r *FuseBucket) loadCacheDir() *cache.CacheDir { diff --git a/client2/internal/mount/vfs/fuse_dir.go b/client2/internal/mount/vfs/fuse_dir.go index f61ab80..851bbb5 100644 --- a/client2/internal/mount/vfs/fuse_dir.go +++ b/client2/internal/mount/vfs/fuse_dir.go @@ -259,17 +259,74 @@ func (r *FuseDir) RemoveChild(ctx context.Context, name string) error { } func (r *FuseDir) MoveChild(ctx context.Context, oldName string, newName string, newParent fuse.FsDir) error { - - // TODO 有问题 - oldPathComps := append(lo2.ArrayClone(r.pathComps), oldName) - newParentNode := newParent.(FuseNode) - _, err := r.vfs.cache.Move(oldPathComps, append(newParentNode.PathComps(), newName)) - if err != nil { - return err + newParentPath := newParentNode.PathComps() + newChildPath := lo2.AppendNew(newParentPath, newName) + newChildPathJoined := cdssdk.JoinObjectPath(newChildPath[2:]...) + + // 不允许移动任何内容到Package层级以上 + if len(newParentPath) < 2 { + return fuse.ErrNotSupported } - return nil + oldChildPath := lo2.AppendNew(r.PathComps(), oldName) + oldChildPathJoined := cdssdk.JoinObjectPath(oldChildPath[2:]...) + + // 先更新远程,再更新本地,因为远程使用事务更新,可以回滚,而本地不行 + d := r.vfs.db + return r.vfs.db.DoTx(func(tx db2.SQLContext) error { + newPkg, err := d.Package().GetUserPackageByName(tx, 1, newParentPath[0], newParentPath[1]) + if err != nil { + if err == gorm.ErrRecordNotFound { + return fuse.ErrNotExists + } + return err + } + + oldPkg, err := d.Package().GetUserPackageByName(tx, 1, oldChildPath[0], oldChildPath[1]) + if err != nil { + if err == gorm.ErrRecordNotFound { + return fuse.ErrNotExists + } + return err + } + + // 检查目的文件或文件夹是否已经存在 + _, err = d.Object().GetByPath(tx, newPkg.PackageID, newChildPathJoined) + if err == nil { + return fuse.ErrExists + } + + has, err := d.Object().HasObjectWithPrefix(tx, newPkg.PackageID, newChildPathJoined+cdssdk.ObjectPathSeparator) + if err != nil { + return err + } + if has { + return fuse.ErrExists + } + + // 都不存在,就开始移动文件 + oldObj, err := d.Object().GetByPath(tx, oldPkg.PackageID, oldChildPathJoined) + if err == nil { + oldObj.PackageID = newPkg.PackageID + oldObj.Path = newChildPathJoined + + err = d.Object().BatchUpdate(tx, []cdssdk.Object{oldObj}) + if err != nil { + return err + } + } + + err = d.Object().MoveByPrefix(tx, + oldPkg.PackageID, oldChildPathJoined+cdssdk.ObjectPathSeparator, + newPkg.PackageID, newChildPathJoined+cdssdk.ObjectPathSeparator, + ) + if err != nil { + return err + } + + return r.vfs.cache.Move(oldChildPath, newChildPath) + }) } func (r *FuseDir) loadCacheDir() *cache.CacheDir { diff --git a/client2/internal/mount/vfs/fuse_package.go b/client2/internal/mount/vfs/fuse_package.go index e45a069..2fa67f9 100644 --- a/client2/internal/mount/vfs/fuse_package.go +++ b/client2/internal/mount/vfs/fuse_package.go @@ -8,6 +8,7 @@ import ( "time" cdssdk "gitlink.org.cn/cloudream/common/sdks/storage" + "gitlink.org.cn/cloudream/common/utils/lo2" "gitlink.org.cn/cloudream/storage/client2/internal/mount/fuse" "gitlink.org.cn/cloudream/storage/client2/internal/mount/vfs/cache" "gitlink.org.cn/cloudream/storage/common/pkgs/db2" @@ -253,16 +254,74 @@ func (r *FusePackage) RemoveChild(ctx context.Context, name string) error { } func (r *FusePackage) MoveChild(ctx context.Context, oldName string, newName string, newParent fuse.FsDir) error { - - // TODO 有问题 - newParentNode := newParent.(FuseNode) - _, err := r.vfs.cache.Move([]string{r.bktName, r.pkgName, oldName}, append(newParentNode.PathComps(), newName)) - if err != nil { - return err + newParentPath := newParentNode.PathComps() + newChildPath := lo2.AppendNew(newParentPath, newName) + newChildPathJoined := cdssdk.JoinObjectPath(newChildPath[2:]...) + + // 不允许移动任何内容到Package层级以上 + if len(newParentPath) < 2 { + return fuse.ErrNotSupported } - return nil + oldChildPath := lo2.AppendNew(r.PathComps(), oldName) + oldChildPathJoined := cdssdk.JoinObjectPath(oldChildPath[2:]...) + + // 先更新远程,再更新本地,因为远程使用事务更新,可以回滚,而本地不行 + d := r.vfs.db + return r.vfs.db.DoTx(func(tx db2.SQLContext) error { + newPkg, err := d.Package().GetUserPackageByName(tx, 1, newParentPath[0], newParentPath[1]) + if err != nil { + if err == gorm.ErrRecordNotFound { + return fuse.ErrNotExists + } + return err + } + + oldPkg, err := d.Package().GetUserPackageByName(tx, 1, r.bktName, r.pkgName) + if err != nil { + if err == gorm.ErrRecordNotFound { + return fuse.ErrNotExists + } + return err + } + + // 检查目的地是否已经存在 + _, err = d.Object().GetByPath(tx, newPkg.PackageID, newChildPathJoined) + if err == nil { + return fuse.ErrExists + } + + has, err := d.Object().HasObjectWithPrefix(tx, newPkg.PackageID, newChildPathJoined+cdssdk.ObjectPathSeparator) + if err != nil { + return err + } + if has { + return fuse.ErrExists + } + + // 都不存在,就开始移动文件 + oldObj, err := d.Object().GetByPath(tx, oldPkg.PackageID, oldChildPathJoined) + if err == nil { + oldObj.PackageID = newPkg.PackageID + oldObj.Path = newChildPathJoined + + err = d.Object().BatchUpdate(tx, []cdssdk.Object{oldObj}) + if err != nil { + return err + } + } + + err = d.Object().MoveByPrefix(tx, + oldPkg.PackageID, oldChildPathJoined+cdssdk.ObjectPathSeparator, + newPkg.PackageID, newChildPathJoined+cdssdk.ObjectPathSeparator, + ) + if err != nil { + return err + } + + return r.vfs.cache.Move(oldChildPath, newChildPath) + }) } func (r *FusePackage) loadCacheDir() *cache.CacheDir { diff --git a/client2/internal/mount/vfs/fuse_root.go b/client2/internal/mount/vfs/fuse_root.go index bc42b9d..a772f85 100644 --- a/client2/internal/mount/vfs/fuse_root.go +++ b/client2/internal/mount/vfs/fuse_root.go @@ -12,46 +12,46 @@ import ( "gorm.io/gorm" ) -type Root struct { +type FuseRoot struct { vfs *Vfs } -func newRoot(vfs *Vfs) *Root { - return &Root{ +func newRoot(vfs *Vfs) *FuseRoot { + return &FuseRoot{ vfs: vfs, } } -func (r *Root) PathComps() []string { +func (r *FuseRoot) PathComps() []string { return []string{} } -func (r *Root) Name() string { +func (r *FuseRoot) Name() string { return "" } -func (r *Root) Size() int64 { +func (r *FuseRoot) Size() int64 { return 0 } -func (r *Root) Mode() os.FileMode { +func (r *FuseRoot) Mode() os.FileMode { return os.ModeDir | 0755 } -func (r *Root) ModTime() time.Time { +func (r *FuseRoot) ModTime() time.Time { return time.Now() } -func (r *Root) IsDir() bool { +func (r *FuseRoot) IsDir() bool { return true } -func (r *Root) SetModTime(time time.Time) error { +func (r *FuseRoot) SetModTime(time time.Time) error { return nil } // 如果不存在,应该返回ErrNotExists -func (r *Root) Child(ctx context.Context, name string) (fuse.FsEntry, error) { +func (r *FuseRoot) Child(ctx context.Context, name string) (fuse.FsEntry, error) { ca := r.vfs.cache.Stat([]string{name}) if ca == nil { @@ -81,11 +81,11 @@ func (r *Root) Child(ctx context.Context, name string) (fuse.FsEntry, error) { return newFileFromCache(*ca, r.vfs), nil } -func (r *Root) Children(ctx context.Context) ([]fuse.FsEntry, error) { +func (r *FuseRoot) Children(ctx context.Context) ([]fuse.FsEntry, error) { return r.listChildren() } -func (r *Root) ReadChildren() (fuse.DirReader, error) { +func (r *FuseRoot) ReadChildren() (fuse.DirReader, error) { ens, err := r.listChildren() if err != nil { return nil, err @@ -94,7 +94,7 @@ func (r *Root) ReadChildren() (fuse.DirReader, error) { return newFuseDirReader(ens), nil } -func (r *Root) listChildren() ([]fuse.FsEntry, error) { +func (r *FuseRoot) listChildren() ([]fuse.FsEntry, error) { var ens []fuse.FsEntry infos := r.vfs.cache.StatMany([]string{}) @@ -136,7 +136,7 @@ func (r *Root) listChildren() ([]fuse.FsEntry, error) { return ens, nil } -func (r *Root) NewDir(ctx context.Context, name string) (fuse.FsDir, error) { +func (r *FuseRoot) NewDir(ctx context.Context, name string) (fuse.FsDir, error) { cache := r.vfs.cache.CreateDir([]string{name}) if cache == nil { return nil, fuse.ErrPermission @@ -150,7 +150,7 @@ func (r *Root) NewDir(ctx context.Context, name string) (fuse.FsDir, error) { return newBucketFromCache(cache.Info(), r.vfs), nil } -func (r *Root) NewFile(ctx context.Context, name string, flags uint32) (fuse.FileHandle, uint32, error) { +func (r *FuseRoot) NewFile(ctx context.Context, name string, flags uint32) (fuse.FileHandle, uint32, error) { cache := r.vfs.cache.CreateFile([]string{name}) if cache == nil { return nil, 0, fuse.ErrPermission @@ -163,7 +163,7 @@ func (r *Root) NewFile(ctx context.Context, name string, flags uint32) (fuse.Fil return newFileHandle(fileNode, hd), flags, nil } -func (r *Root) RemoveChild(ctx context.Context, name string) error { +func (r *FuseRoot) RemoveChild(ctx context.Context, name string) error { // TODO 生成系统事件 db := r.vfs.db return r.vfs.db.DoTx(func(tx db2.SQLContext) error { @@ -196,51 +196,36 @@ func (r *Root) RemoveChild(ctx context.Context, name string) error { }) } -func (r *Root) MoveChild(ctx context.Context, oldName string, newName string, newParent fuse.FsDir) error { - - // TODO 有问题 - +func (r *FuseRoot) MoveChild(ctx context.Context, oldName string, newName string, newParent fuse.FsDir) error { newParentNode := newParent.(FuseNode) - _, err := r.vfs.cache.Move([]string{oldName}, append(newParentNode.PathComps(), newName)) - if err != nil { - return err + newParentPath := newParentNode.PathComps() + + // 只允许同层级内改名 + if len(newParentPath) != 0 { + return fuse.ErrNotSupported } d := r.vfs.db - // 如果目标节点是根节点,那么就是重命名桶 - if _, ok := newParent.(*Root); ok { - d.DoTx(func(tx db2.SQLContext) error { - _, err := d.Bucket().GetByName(tx, newName) - if err == nil { - // 目标节点已经存在,不能重命名,直接退出 - return err - } - - oldBkt, err := d.Bucket().GetByName(tx, oldName) - if err != nil { - // 源节点不存在,直接退出 - return err - } + return d.DoTx(func(tx db2.SQLContext) error { + _, err := d.Bucket().GetByName(tx, newName) + if err == nil { + // 目标节点已经存在,不能重命名,直接退出 + return fuse.ErrExists + } - // 不关注重命名是否成功,仅尝试一下 - return d.Bucket().Rename(tx, oldBkt.BucketID, newName) - }) - } else { - // TODO 做法存疑 - // 其他情况则删除旧桶 - d.DoTx(func(tx db2.SQLContext) error { - oldBkt, err := d.Bucket().GetByName(tx, oldName) + oldBkt, err := d.Bucket().GetByName(tx, oldName) + if err == nil { + err = d.Bucket().Rename(tx, oldBkt.BucketID, newName) if err != nil { - // 源节点不存在,直接退出 return err } + } else if err != gorm.ErrRecordNotFound { + return err + } - return d.Bucket().DeleteComplete(tx, oldBkt.BucketID) - }) - } - - return nil + return r.vfs.cache.Move([]string{oldName}, []string{newName}) + }) } -var _ fuse.FsDir = (*Root)(nil) -var _ FuseNode = (*Root)(nil) +var _ fuse.FsDir = (*FuseRoot)(nil) +var _ FuseNode = (*FuseRoot)(nil) diff --git a/common/pkgs/db2/object.go b/common/pkgs/db2/object.go index a88b5fc..bb00fae 100644 --- a/common/pkgs/db2/object.go +++ b/common/pkgs/db2/object.go @@ -45,7 +45,7 @@ func (db *ObjectDB) GetByFullPath(ctx SQLContext, bktName string, pkgName string func (db *ObjectDB) GetWithPathPrefix(ctx SQLContext, packageID cdssdk.PackageID, pathPrefix string) ([]cdssdk.Object, error) { var ret []cdssdk.Object - err := ctx.Table("Object").Where("PackageID = ? AND Path LIKE ?", packageID, pathPrefix+"%").Order("ObjectID ASC").Find(&ret).Error + err := ctx.Table("Object").Where("PackageID = ? AND Path LIKE ?", packageID, escapeLike("", "%", pathPrefix)).Order("ObjectID ASC").Find(&ret).Error return ret, err } @@ -58,7 +58,7 @@ func (db *ObjectDB) GetCommonPrefixes(ctx SQLContext, packageID cdssdk.PackageID err := ctx.Table("Object").Select(prefixStatm+" as Prefix"). Where("PackageID = ?", packageID). - Where("Path like ?", pathPrefix+"%"). + Where("Path like ?", escapeLike("", "%", pathPrefix)). Where(prefixStatm + " <> Path"). Group("Prefix").Find(&ret).Error if err != nil { @@ -81,7 +81,7 @@ func (db *ObjectDB) GetDirectChildren(ctx SQLContext, packageID cdssdk.PackageID err := ctx.Table("Object"). Where("PackageID = ?", packageID). - Where("Path like ?", pathPrefix+"%"). + Where("Path like ?", escapeLike("", "%", pathPrefix)). Where(prefixStatm + " = Path"). Find(&ret).Error return ret, err @@ -89,7 +89,7 @@ func (db *ObjectDB) GetDirectChildren(ctx SQLContext, packageID cdssdk.PackageID func (db *ObjectDB) HasObjectWithPrefix(ctx SQLContext, packageID cdssdk.PackageID, pathPrefix string) (bool, error) { var obj cdssdk.Object - err := ctx.Table("Object").Where("PackageID = ? AND Path LIKE ?", packageID, pathPrefix+"%").First(&obj).Error + err := ctx.Table("Object").Where("PackageID = ? AND Path LIKE ?", packageID, escapeLike("", "%", pathPrefix)).First(&obj).Error if err == nil { return true, nil } @@ -455,3 +455,11 @@ func (db *ObjectDB) DeleteInPackage(ctx SQLContext, packageID cdssdk.PackageID) func (db *ObjectDB) DeleteByPath(ctx SQLContext, packageID cdssdk.PackageID, path string) error { return ctx.Table("Object").Where("PackageID = ? AND Path = ?", packageID, path).Delete(&cdssdk.Object{}).Error } + +func (db *ObjectDB) MoveByPrefix(ctx SQLContext, oldPkgID cdssdk.PackageID, oldPrefix string, newPkgID cdssdk.PackageID, newPrefix string) error { + return ctx.Table("Object").Where("PackageID = ? AND Path LIKE ?", oldPkgID, escapeLike("", "%", oldPrefix)). + Updates(map[string]any{ + "PackageID": newPkgID, + "Path": gorm.Expr("concat(?, substring(Path, ?))", newPrefix, len(oldPrefix)+1), + }).Error +} diff --git a/common/pkgs/db2/package.go b/common/pkgs/db2/package.go index c42b4ae..8a9046e 100644 --- a/common/pkgs/db2/package.go +++ b/common/pkgs/db2/package.go @@ -202,3 +202,8 @@ func (*PackageDB) HasPackageIn(ctx SQLContext, bucketID cdssdk.BucketID) (bool, } return true, nil } + +func (*PackageDB) Move(ctx SQLContext, packageID cdssdk.PackageID, newBktID cdssdk.BucketID, newName string) error { + err := ctx.Table("Package").Where("PackageID = ?", packageID).Update("BucketID", newBktID).Update("Name", newName).Error + return err +} diff --git a/common/pkgs/db2/utils.go b/common/pkgs/db2/utils.go index 9ca7dc5..deeda45 100644 --- a/common/pkgs/db2/utils.go +++ b/common/pkgs/db2/utils.go @@ -1,6 +1,8 @@ package db2 import ( + "strings" + "gorm.io/gorm" ) @@ -55,3 +57,25 @@ func min(a, b int) int { } return b } + +func escapeLike(left, right, word string) string { + var n int + for i := range word { + if c := word[i]; c == '%' || c == '_' || c == '\\' { + n++ + } + } + // No characters to escape. + if n == 0 { + return left + word + right + } + var b strings.Builder + b.Grow(len(word) + n) + for _, c := range word { + if c == '%' || c == '_' || c == '\\' { + b.WriteByte('\\') + } + b.WriteRune(c) + } + return left + b.String() + right +}