| @@ -0,0 +1,231 @@ | |||||
| package cmdline | |||||
| import ( | |||||
| "fmt" | |||||
| "os" | |||||
| "testing" | |||||
| "time" | |||||
| "github.com/spf13/cobra" | |||||
| "gitlink.org.cn/cloudream/common/pkgs/logger" | |||||
| "gitlink.org.cn/cloudream/storage2/client/internal/accessstat" | |||||
| "gitlink.org.cn/cloudream/storage2/client/internal/config" | |||||
| "gitlink.org.cn/cloudream/storage2/client/internal/db" | |||||
| "gitlink.org.cn/cloudream/storage2/client/internal/downloader" | |||||
| "gitlink.org.cn/cloudream/storage2/client/internal/downloader/strategy" | |||||
| "gitlink.org.cn/cloudream/storage2/client/internal/http" | |||||
| "gitlink.org.cn/cloudream/storage2/client/internal/metacache" | |||||
| "gitlink.org.cn/cloudream/storage2/client/internal/mount" | |||||
| "gitlink.org.cn/cloudream/storage2/client/internal/mount/vfstest" | |||||
| "gitlink.org.cn/cloudream/storage2/client/internal/services" | |||||
| "gitlink.org.cn/cloudream/storage2/client/internal/uploader" | |||||
| stgglb "gitlink.org.cn/cloudream/storage2/common/globals" | |||||
| "gitlink.org.cn/cloudream/storage2/common/models/datamap" | |||||
| "gitlink.org.cn/cloudream/storage2/common/pkgs/connectivity" | |||||
| "gitlink.org.cn/cloudream/storage2/common/pkgs/distlock" | |||||
| "gitlink.org.cn/cloudream/storage2/common/pkgs/storage/pool" | |||||
| "gitlink.org.cn/cloudream/storage2/common/pkgs/sysevent" | |||||
| ) | |||||
| // 初始化函数,将ServeHTTP命令注册到命令列表中。 | |||||
| func init() { | |||||
| var configPath string | |||||
| var opt serveHTTPOptions | |||||
| cmd := cobra.Command{ | |||||
| Use: "vfstest", | |||||
| Short: "test vfs", | |||||
| Run: func(cmd *cobra.Command, args []string) { | |||||
| vfsTest(configPath, opt) | |||||
| }, | |||||
| } | |||||
| cmd.Flags().StringVarP(&configPath, "config", "c", "", "config file path") | |||||
| cmd.Flags().BoolVarP(&opt.DisableHTTP, "no-http", "", false, "disable http server") | |||||
| cmd.Flags().StringVarP(&opt.HTTPListenAddr, "listen", "", "", "http listen address, will override config file") | |||||
| cmd.Flags().BoolVarP(&opt.DisableMount, "no-mount", "", false, "disable mount") | |||||
| cmd.Flags().StringVarP(&opt.MountPoint, "mount", "", "", "mount point, will override config file") | |||||
| RootCmd.AddCommand(&cmd) | |||||
| } | |||||
| func vfsTest(configPath string, opts serveHTTPOptions) { | |||||
| err := config.Init(configPath) | |||||
| if err != nil { | |||||
| fmt.Printf("init config failed, err: %s", err.Error()) | |||||
| os.Exit(1) | |||||
| } | |||||
| err = logger.Init(&config.Cfg().Logger) | |||||
| if err != nil { | |||||
| fmt.Printf("init logger failed, err: %s", err.Error()) | |||||
| os.Exit(1) | |||||
| } | |||||
| stgglb.InitLocal(config.Cfg().Local) | |||||
| stgglb.InitMQPool(config.Cfg().RabbitMQ) | |||||
| stgglb.InitHubRPCPool(&config.Cfg().HubGRPC) | |||||
| // 数据库 | |||||
| db, err := db.NewDB(&config.Cfg().DB) | |||||
| if err != nil { | |||||
| logger.Fatalf("new db failed, err: %s", err.Error()) | |||||
| } | |||||
| // 初始化系统事件发布器 | |||||
| evtPub, err := sysevent.NewPublisher(sysevent.ConfigFromMQConfig(config.Cfg().RabbitMQ), &datamap.SourceClient{ | |||||
| UserID: config.Cfg().Local.UserID, | |||||
| }) | |||||
| if err != nil { | |||||
| logger.Errorf("new sysevent publisher: %v", err) | |||||
| os.Exit(1) | |||||
| } | |||||
| evtPubChan := evtPub.Start() | |||||
| defer evtPub.Stop() | |||||
| // 连接性信息收集 | |||||
| conCol := connectivity.NewCollector(&config.Cfg().Connectivity, nil) | |||||
| conCol.CollectInPlace() | |||||
| // 元数据缓存 | |||||
| metaCacheHost := metacache.NewHost(db) | |||||
| go metaCacheHost.Serve() | |||||
| stgMeta := metaCacheHost.AddStorageMeta() | |||||
| hubMeta := metaCacheHost.AddHubMeta() | |||||
| conMeta := metaCacheHost.AddConnectivity() | |||||
| // 分布式锁 | |||||
| distlockSvc, err := distlock.NewService(&config.Cfg().DistLock) | |||||
| if err != nil { | |||||
| logger.Warnf("new distlock service failed, err: %s", err.Error()) | |||||
| os.Exit(1) | |||||
| } | |||||
| go serveDistLock(distlockSvc) | |||||
| // 访问统计 | |||||
| acStat := accessstat.NewAccessStat(accessstat.Config{ | |||||
| // TODO 考虑放到配置里 | |||||
| ReportInterval: time.Second * 10, | |||||
| }, db) | |||||
| acStatChan := acStat.Start() | |||||
| defer acStat.Stop() | |||||
| // 存储管理器 | |||||
| stgPool := pool.NewPool() | |||||
| // 下载策略 | |||||
| strgSel := strategy.NewSelector(config.Cfg().DownloadStrategy, stgMeta, hubMeta, conMeta) | |||||
| // 下载器 | |||||
| dlder := downloader.NewDownloader(config.Cfg().Downloader, &conCol, stgPool, strgSel, db) | |||||
| // 上传器 | |||||
| uploader := uploader.NewUploader(distlockSvc, &conCol, stgPool, stgMeta, db) | |||||
| // 挂载 | |||||
| mntCfg := config.Cfg().Mount | |||||
| if !opts.DisableMount && mntCfg != nil && mntCfg.Enabled { | |||||
| if opts.MountPoint != "" { | |||||
| mntCfg.MountPoint = opts.MountPoint | |||||
| } | |||||
| } else { | |||||
| mntCfg = nil | |||||
| } | |||||
| mnt := mount.NewMount(mntCfg, db, uploader, dlder) | |||||
| mntChan := mnt.Start() | |||||
| defer mnt.Stop() | |||||
| svc := services.NewService(distlockSvc, dlder, acStat, uploader, strgSel, stgMeta, db, evtPub, mnt) | |||||
| // HTTP接口 | |||||
| httpCfg := config.Cfg().HTTP | |||||
| if !opts.DisableHTTP && httpCfg != nil && httpCfg.Enabled { | |||||
| if opts.HTTPListenAddr != "" { | |||||
| httpCfg.Listen = opts.HTTPListenAddr | |||||
| } | |||||
| } else { | |||||
| httpCfg = nil | |||||
| } | |||||
| httpSvr := http.NewServer(httpCfg, svc) | |||||
| httpChan := httpSvr.Start() | |||||
| defer httpSvr.Stop() | |||||
| go func() { | |||||
| <-time.After(5 * time.Second) | |||||
| testing.Main(nil, []testing.InternalTest{ | |||||
| {"VFS", func(t *testing.T) { | |||||
| vfstest.RunTests(t, mnt) | |||||
| }}, | |||||
| }, nil, nil) | |||||
| }() | |||||
| /// 开始监听各个模块的事件 | |||||
| evtPubEvt := evtPubChan.Receive() | |||||
| acStatEvt := acStatChan.Receive() | |||||
| httpEvt := httpChan.Receive() | |||||
| mntEvt := mntChan.Receive() | |||||
| loop: | |||||
| for { | |||||
| select { | |||||
| case e := <-evtPubEvt.Chan(): | |||||
| if e.Err != nil { | |||||
| logger.Errorf("receive publisher event: %v", err) | |||||
| break loop | |||||
| } | |||||
| switch val := e.Value.(type) { | |||||
| case sysevent.PublishError: | |||||
| logger.Errorf("publishing event: %v", val) | |||||
| case sysevent.PublisherExited: | |||||
| if val.Err != nil { | |||||
| logger.Errorf("publisher exited with error: %v", val.Err) | |||||
| } else { | |||||
| logger.Info("publisher exited") | |||||
| } | |||||
| break loop | |||||
| case sysevent.OtherError: | |||||
| logger.Errorf("sysevent: %v", val) | |||||
| } | |||||
| evtPubEvt = evtPubChan.Receive() | |||||
| case e := <-acStatEvt.Chan(): | |||||
| if e.Err != nil { | |||||
| logger.Errorf("receive access stat event: %v", err) | |||||
| break loop | |||||
| } | |||||
| switch e := e.Value.(type) { | |||||
| case accessstat.ExitEvent: | |||||
| logger.Infof("access stat exited, err: %v", e.Err) | |||||
| break loop | |||||
| } | |||||
| acStatEvt = acStatChan.Receive() | |||||
| case e := <-httpEvt.Chan(): | |||||
| if e.Err != nil { | |||||
| logger.Errorf("receive http event: %v", err) | |||||
| break loop | |||||
| } | |||||
| switch e := e.Value.(type) { | |||||
| case http.ExitEvent: | |||||
| logger.Infof("http server exited, err: %v", e.Err) | |||||
| break loop | |||||
| } | |||||
| httpEvt = httpChan.Receive() | |||||
| case e := <-mntEvt.Chan(): | |||||
| if e.Err != nil { | |||||
| logger.Errorf("receive mount event: %v", e.Err) | |||||
| break loop | |||||
| } | |||||
| switch e := e.Value.(type) { | |||||
| case mount.ExitEvent: | |||||
| logger.Infof("mount exited, err: %v", e.Err) | |||||
| break loop | |||||
| } | |||||
| mntEvt = mntChan.Receive() | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -566,10 +566,6 @@ func (db *ObjectDB) BatchUpdateRedundancy(ctx SQLContext, updates []UpdatingObje | |||||
| return nil | return nil | ||||
| } | } | ||||
| func (db *ObjectDB) DeleteByPath(ctx SQLContext, packageID types.PackageID, path string) error { | |||||
| return ctx.Table("Object").Where("PackageID = ? AND Path = ?", packageID, path).Delete(&types.Object{}).Error | |||||
| } | |||||
| func (db *ObjectDB) MoveByPrefix(ctx SQLContext, oldPkgID types.PackageID, oldPrefix string, newPkgID types.PackageID, newPrefix string) error { | func (db *ObjectDB) MoveByPrefix(ctx SQLContext, oldPkgID types.PackageID, oldPrefix string, newPkgID types.PackageID, newPrefix string) error { | ||||
| return ctx.Table("Object").Where("PackageID = ? AND Path LIKE ?", oldPkgID, escapeLike("", "%", oldPrefix)). | return ctx.Table("Object").Where("PackageID = ? AND Path LIKE ?", oldPkgID, escapeLike("", "%", oldPrefix)). | ||||
| Updates(map[string]any{ | Updates(map[string]any{ | ||||
| @@ -628,3 +624,36 @@ func (db *ObjectDB) AppendPart(tx SQLContext, block types.ObjectBlock) error { | |||||
| return nil | return nil | ||||
| } | } | ||||
| func (db *ObjectDB) BatchDeleteComplete(ctx SQLContext, objectIDs []types.ObjectID) error { | |||||
| err := db.Object().BatchDelete(ctx, objectIDs) | |||||
| if err != nil { | |||||
| return fmt.Errorf("batch deleting objects: %w", err) | |||||
| } | |||||
| err = db.ObjectBlock().BatchDeleteByObjectID(ctx, objectIDs) | |||||
| if err != nil { | |||||
| return fmt.Errorf("batch deleting object blocks: %w", err) | |||||
| } | |||||
| err = db.PinnedObject().BatchDeleteByObjectID(ctx, objectIDs) | |||||
| if err != nil { | |||||
| return fmt.Errorf("batch deleting pinned objects: %w", err) | |||||
| } | |||||
| err = db.ObjectAccessStat().BatchDeleteByObjectID(ctx, objectIDs) | |||||
| if err != nil { | |||||
| return fmt.Errorf("batch deleting object access stats: %w", err) | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func (db *ObjectDB) DeleteCompleteByPath(ctx SQLContext, packageID types.PackageID, path string) error { | |||||
| obj, err := db.Object().GetByPath(ctx, packageID, path) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| return db.BatchDeleteComplete(ctx, []types.ObjectID{obj.ObjectID}) | |||||
| } | |||||
| @@ -77,6 +77,14 @@ func (m *Mount) Stop() { | |||||
| m.fuseServer.Unmount() | m.fuseServer.Unmount() | ||||
| } | } | ||||
| func (m *Mount) MountPoint() string { | |||||
| if m.cfg == nil { | |||||
| return "" | |||||
| } | |||||
| return m.cfg.MountPoint | |||||
| } | |||||
| func (m *Mount) Dump() MountStatus { | func (m *Mount) Dump() MountStatus { | ||||
| if m.vfs == nil { | if m.vfs == nil { | ||||
| return MountStatus{} | return MountStatus{} | ||||
| @@ -27,6 +27,10 @@ func (m *Mount) Start() *MountEventChan { | |||||
| return m.eventChan | return m.eventChan | ||||
| } | } | ||||
| func (m *Mount) MountPoint() string { | |||||
| return "" | |||||
| } | |||||
| func (m *Mount) Stop() { | func (m *Mount) Stop() { | ||||
| m.eventChan.Send(ExitEvent{}) | m.eventChan.Send(ExitEvent{}) | ||||
| } | } | ||||
| @@ -52,6 +52,7 @@ func loadCacheDir(c *Cache, pathComps []string) (*CacheDir, error) { | |||||
| } | } | ||||
| return &CacheDir{ | return &CacheDir{ | ||||
| cache: c, | |||||
| pathComps: pathComps, | pathComps: pathComps, | ||||
| name: stat.Name(), | name: stat.Name(), | ||||
| modTime: stat.ModTime(), | modTime: stat.ModTime(), | ||||
| @@ -75,6 +76,7 @@ func makeCacheDirFromOption(c *Cache, pathComps []string, opt CreateDirOption) ( | |||||
| os.Chtimes(dataPath, opt.ModTime, opt.ModTime) | os.Chtimes(dataPath, opt.ModTime, opt.ModTime) | ||||
| return &CacheDir{ | return &CacheDir{ | ||||
| cache: c, | |||||
| pathComps: pathComps, | pathComps: pathComps, | ||||
| name: pathComps[len(pathComps)-1], | name: pathComps[len(pathComps)-1], | ||||
| modTime: opt.ModTime, | modTime: opt.ModTime, | ||||
| @@ -99,8 +99,12 @@ func listChildren(vfs *Vfs, ctx context.Context, parent FuseNode) ([]fuse.FsEntr | |||||
| } | } | ||||
| objPath := clitypes.JoinObjectPath(myPathComps[2:]...) | objPath := clitypes.JoinObjectPath(myPathComps[2:]...) | ||||
| objPrefix := objPath | |||||
| if objPath != "" { | |||||
| objPrefix += clitypes.ObjectPathSeparator | |||||
| } | |||||
| objs, coms, err := d.Object().GetByPrefixGrouped(tx, pkg.PackageID, objPath+clitypes.ObjectPathSeparator) | |||||
| objs, coms, err := d.Object().GetByPrefixGrouped(tx, pkg.PackageID, objPrefix) | |||||
| if err != nil { | if err != nil { | ||||
| return err | return err | ||||
| } | } | ||||
| @@ -186,8 +190,8 @@ func removeChild(vfs *Vfs, ctx context.Context, name string, parent FuseNode) er | |||||
| } | } | ||||
| // 存储系统不会保存目录结构,所以这里是尝试删除同名文件 | // 存储系统不会保存目录结构,所以这里是尝试删除同名文件 | ||||
| err = d.Object().DeleteByPath(tx, pkg.PackageID, joinedPath) | |||||
| if err != nil { | |||||
| err = d.Object().DeleteCompleteByPath(tx, pkg.PackageID, joinedPath) | |||||
| if err != nil && err != gorm.ErrRecordNotFound { | |||||
| return err | return err | ||||
| } | } | ||||
| @@ -223,8 +227,11 @@ func moveChild(vfs *Vfs, ctx context.Context, oldName string, oldParent FuseNode | |||||
| } | } | ||||
| err2 := vfs.cache.Move(oldChildPath, newChildPath) | err2 := vfs.cache.Move(oldChildPath, newChildPath) | ||||
| if err == fuse.ErrNotExists && err2 == fuse.ErrNotExists { | |||||
| return fuse.ErrNotExists | |||||
| if err2 == fuse.ErrNotExists { | |||||
| if err == fuse.ErrNotExists { | |||||
| return fuse.ErrNotExists | |||||
| } | |||||
| return nil | |||||
| } | } | ||||
| return err2 | return err2 | ||||
| @@ -273,11 +280,11 @@ func moveRemote(vfs *Vfs, tx db.SQLContext, oldChildPath []string, newParentPath | |||||
| return d.Object().BatchUpdate(tx, []clitypes.Object{oldObj}) | return d.Object().BatchUpdate(tx, []clitypes.Object{oldObj}) | ||||
| } | } | ||||
| if err == gorm.ErrRecordNotFound { | |||||
| return fuse.ErrNotExists | |||||
| if err != gorm.ErrRecordNotFound { | |||||
| return err | |||||
| } | } | ||||
| err = d.Object().HasObjectWithPrefix(tx, oldObj.PackageID, oldChildPathJoined+clitypes.ObjectPathSeparator) | |||||
| err = d.Object().HasObjectWithPrefix(tx, oldPkg.PackageID, oldChildPathJoined+clitypes.ObjectPathSeparator) | |||||
| if err == nil { | if err == nil { | ||||
| return d.Object().MoveByPrefix(tx, | return d.Object().MoveByPrefix(tx, | ||||
| oldPkg.PackageID, oldChildPathJoined+clitypes.ObjectPathSeparator, | oldPkg.PackageID, oldChildPathJoined+clitypes.ObjectPathSeparator, | ||||
| @@ -0,0 +1,225 @@ | |||||
| package vfstest | |||||
| import ( | |||||
| "testing" | |||||
| "time" | |||||
| "github.com/stretchr/testify/assert" | |||||
| "github.com/stretchr/testify/require" | |||||
| ) | |||||
| // TestDirLs checks out listing | |||||
| func TestDirLs(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| run.checkDir(t, "") | |||||
| run.mkdir(t, "a directory") | |||||
| run.createFile(t, "a file", "hello") | |||||
| run.checkDir(t, "a directory/|a file 5") | |||||
| run.rmdir(t, "a directory") | |||||
| run.rm(t, "a file") | |||||
| run.checkDir(t, "") | |||||
| } | |||||
| // TestDirCreateAndRemoveDir tests creating and removing a directory | |||||
| func TestDirCreateAndRemoveDir(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| run.mkdir(t, "dir") | |||||
| run.mkdir(t, "dir/subdir") | |||||
| run.checkDir(t, "dir/|dir/subdir/") | |||||
| // Check we can't delete a directory with stuff in | |||||
| err := run.os.Remove(run.path("dir")) | |||||
| assert.Error(t, err, "file exists") | |||||
| // Now delete subdir then dir - should produce no errors | |||||
| run.rmdir(t, "dir/subdir") | |||||
| run.checkDir(t, "dir/") | |||||
| run.rmdir(t, "dir") | |||||
| run.checkDir(t, "") | |||||
| } | |||||
| // TestDirCreateAndRemoveFile tests creating and removing a file | |||||
| func TestDirCreateAndRemoveFile(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| run.mkdir(t, "dir") | |||||
| run.createFile(t, "dir/file", "potato") | |||||
| run.checkDir(t, "dir/|dir/file 6") | |||||
| // Check we can't delete a directory with stuff in | |||||
| err := run.os.Remove(run.path("dir")) | |||||
| assert.Error(t, err, "file exists") | |||||
| // Now delete file | |||||
| run.rm(t, "dir/file") | |||||
| run.checkDir(t, "dir/") | |||||
| run.rmdir(t, "dir") | |||||
| run.checkDir(t, "") | |||||
| } | |||||
| // TestDirRenameFile tests renaming a file | |||||
| func TestDirRenameFile(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| run.mkdir(t, "dir") | |||||
| run.createFile(t, "file", "potato") | |||||
| run.checkDir(t, "dir/|file 6") | |||||
| err := run.os.Rename(run.path("file"), run.path("file2")) | |||||
| require.NoError(t, err) | |||||
| run.checkDir(t, "dir/|file2 6") | |||||
| data := run.readFile(t, "file2") | |||||
| assert.Equal(t, "potato", data) | |||||
| err = run.os.Rename(run.path("file2"), run.path("dir/file3")) | |||||
| require.NoError(t, err) | |||||
| run.checkDir(t, "dir/|dir/file3 6") | |||||
| data = run.readFile(t, "dir/file3") | |||||
| require.NoError(t, err) | |||||
| assert.Equal(t, "potato", data) | |||||
| run.rm(t, "dir/file3") | |||||
| run.rmdir(t, "dir") | |||||
| run.checkDir(t, "") | |||||
| } | |||||
| // TestDirRenameEmptyDir tests renaming and empty directory | |||||
| func TestDirRenameEmptyDir(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| run.mkdir(t, "dir") | |||||
| run.mkdir(t, "dir1") | |||||
| run.checkDir(t, "dir/|dir1/") | |||||
| err := run.os.Rename(run.path("dir1"), run.path("dir/dir2")) | |||||
| require.NoError(t, err) | |||||
| run.checkDir(t, "dir/|dir/dir2/") | |||||
| err = run.os.Rename(run.path("dir/dir2"), run.path("dir/dir3")) | |||||
| require.NoError(t, err) | |||||
| run.checkDir(t, "dir/|dir/dir3/") | |||||
| run.rmdir(t, "dir/dir3") | |||||
| run.rmdir(t, "dir") | |||||
| run.checkDir(t, "") | |||||
| } | |||||
| // TestDirRenameFullDir tests renaming a full directory | |||||
| func TestDirRenameFullDir(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| run.mkdir(t, "dir") | |||||
| run.mkdir(t, "dir1") | |||||
| run.createFile(t, "dir1/potato.txt", "maris piper") | |||||
| run.checkDir(t, "dir/|dir1/|dir1/potato.txt 11") | |||||
| err := run.os.Rename(run.path("dir1"), run.path("dir/dir2")) | |||||
| require.NoError(t, err) | |||||
| run.checkDir(t, "dir/|dir/dir2/|dir/dir2/potato.txt 11") | |||||
| err = run.os.Rename(run.path("dir/dir2"), run.path("dir/dir3")) | |||||
| require.NoError(t, err) | |||||
| run.checkDir(t, "dir/|dir/dir3/|dir/dir3/potato.txt 11") | |||||
| run.rm(t, "dir/dir3/potato.txt") | |||||
| run.rmdir(t, "dir/dir3") | |||||
| run.rmdir(t, "dir") | |||||
| run.checkDir(t, "") | |||||
| } | |||||
| // TestDirModTime tests mod times | |||||
| func TestDirModTime(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| run.mkdir(t, "dir") | |||||
| mtime := time.Date(2012, time.November, 18, 17, 32, 31, 0, time.UTC) | |||||
| err := run.os.Chtimes(run.path("dir"), mtime, mtime) | |||||
| require.NoError(t, err) | |||||
| info, err := run.os.Stat(run.path("dir")) | |||||
| require.NoError(t, err) | |||||
| // avoid errors because of timezone differences | |||||
| assert.Equal(t, info.ModTime().Unix(), mtime.Unix()) | |||||
| run.rmdir(t, "dir") | |||||
| } | |||||
| /* | |||||
| // TestDirCacheFlush tests flushing the dir cache | |||||
| func TestDirCacheFlush(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| run.checkDir(t, "") | |||||
| run.mkdir(t, "dir") | |||||
| run.mkdir(t, "otherdir") | |||||
| run.createFile(t, "dir/file", "1") | |||||
| run.createFile(t, "otherdir/file", "1") | |||||
| dm := newDirMap("otherdir/|otherdir/file 1|dir/|dir/file 1") | |||||
| localDm := make(dirMap) | |||||
| run.readLocal(t, localDm, "") | |||||
| assert.Equal(t, dm, localDm, "expected vs fuse mount") | |||||
| err := run.fremote.Mkdir(context.Background(), "dir/subdir") | |||||
| require.NoError(t, err) | |||||
| // expect newly created "subdir" on remote to not show up | |||||
| run.forget("otherdir") | |||||
| run.readLocal(t, localDm, "") | |||||
| assert.Equal(t, dm, localDm, "expected vs fuse mount") | |||||
| run.forget("dir") | |||||
| dm = newDirMap("otherdir/|otherdir/file 1|dir/|dir/file 1|dir/subdir/") | |||||
| run.readLocal(t, localDm, "") | |||||
| assert.Equal(t, dm, localDm, "expected vs fuse mount") | |||||
| run.rm(t, "otherdir/file") | |||||
| run.rmdir(t, "otherdir") | |||||
| run.rm(t, "dir/file") | |||||
| run.rmdir(t, "dir/subdir") | |||||
| run.rmdir(t, "dir") | |||||
| run.checkDir(t, "") | |||||
| } | |||||
| // TestDirCacheFlushOnDirRename tests flushing the dir cache on rename | |||||
| func TestDirCacheFlushOnDirRename(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| run.mkdir(t, "dir") | |||||
| run.createFile(t, "dir/file", "1") | |||||
| dm := newDirMap("dir/|dir/file 1") | |||||
| localDm := make(dirMap) | |||||
| run.readLocal(t, localDm, "") | |||||
| assert.Equal(t, dm, localDm, "expected vs fuse mount") | |||||
| // expect remotely created directory to not show up | |||||
| err := run.fremote.Mkdir(context.Background(), "dir/subdir") | |||||
| require.NoError(t, err) | |||||
| run.readLocal(t, localDm, "") | |||||
| assert.Equal(t, dm, localDm, "expected vs fuse mount") | |||||
| err = run.os.Rename(run.path("dir"), run.path("rid")) | |||||
| require.NoError(t, err) | |||||
| dm = newDirMap("rid/|rid/subdir/|rid/file 1") | |||||
| localDm = make(dirMap) | |||||
| run.readLocal(t, localDm, "") | |||||
| assert.Equal(t, dm, localDm, "expected vs fuse mount") | |||||
| run.rm(t, "rid/file") | |||||
| run.rmdir(t, "rid/subdir") | |||||
| run.rmdir(t, "rid") | |||||
| run.checkDir(t, "") | |||||
| } | |||||
| */ | |||||
| @@ -0,0 +1,60 @@ | |||||
| package vfstest | |||||
| import ( | |||||
| "runtime" | |||||
| "testing" | |||||
| "github.com/stretchr/testify/require" | |||||
| ) | |||||
| // TestTouchAndDelete checks that writing a zero byte file and immediately | |||||
| // deleting it is not racy. See https://github.com/rclone/rclone/issues/1181 | |||||
| func TestTouchAndDelete(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| run.checkDir(t, "") | |||||
| run.createFile(t, "touched", "") | |||||
| run.rm(t, "touched") | |||||
| run.checkDir(t, "") | |||||
| } | |||||
| // TestRenameOpenHandle checks that a file with open writers is successfully | |||||
| // renamed after all writers close. See https://github.com/rclone/rclone/issues/2130 | |||||
| func TestRenameOpenHandle(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| if runtime.GOOS == "windows" { | |||||
| t.Skip("Skipping test on Windows") | |||||
| } | |||||
| run.checkDir(t, "") | |||||
| // create file | |||||
| example := []byte("Some Data") | |||||
| path := run.path("rename") | |||||
| file, err := osCreate(path) | |||||
| require.NoError(t, err) | |||||
| // write some data | |||||
| _, err = file.Write(example) | |||||
| require.NoError(t, err) | |||||
| err = file.Sync() | |||||
| require.NoError(t, err) | |||||
| // attempt to rename open file | |||||
| err = run.os.Rename(path, path+"bla") | |||||
| require.NoError(t, err) | |||||
| // close open writers to allow rename on remote to go through | |||||
| err = file.Close() | |||||
| require.NoError(t, err) | |||||
| run.waitForWriters() | |||||
| // verify file was renamed properly | |||||
| run.checkDir(t, "renamebla 9") | |||||
| // cleanup | |||||
| run.rm(t, "renamebla") | |||||
| run.checkDir(t, "") | |||||
| } | |||||
| @@ -0,0 +1,73 @@ | |||||
| package vfstest | |||||
| import ( | |||||
| "os" | |||||
| "runtime" | |||||
| "testing" | |||||
| "time" | |||||
| "github.com/stretchr/testify/assert" | |||||
| "github.com/stretchr/testify/require" | |||||
| ) | |||||
| // TestFileModTime tests mod times on files | |||||
| func TestFileModTime(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| run.createFile(t, "file", "123") | |||||
| mtime := time.Date(2012, time.November, 18, 17, 32, 31, 0, time.UTC) | |||||
| err := run.os.Chtimes(run.path("file"), mtime, mtime) | |||||
| require.NoError(t, err) | |||||
| info, err := run.os.Stat(run.path("file")) | |||||
| require.NoError(t, err) | |||||
| // avoid errors because of timezone differences | |||||
| assert.Equal(t, info.ModTime().Unix(), mtime.Unix()) | |||||
| run.rm(t, "file") | |||||
| } | |||||
| // run.os.Create without opening for write too | |||||
| func osCreate(name string) (OsFiler, error) { | |||||
| return run.os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) | |||||
| } | |||||
| // run.os.Create with append | |||||
| func osAppend(name string) (OsFiler, error) { | |||||
| return run.os.OpenFile(name, os.O_WRONLY|os.O_APPEND, 0666) | |||||
| } | |||||
| // TestFileModTimeWithOpenWriters tests mod time on open files | |||||
| func TestFileModTimeWithOpenWriters(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| if runtime.GOOS == "windows" { | |||||
| t.Skip("Skipping test on Windows") | |||||
| } | |||||
| mtime := time.Date(2012, time.November, 18, 17, 32, 31, 0, time.UTC) | |||||
| filepath := run.path("cp-archive-test") | |||||
| f, err := osCreate(filepath) | |||||
| require.NoError(t, err) | |||||
| _, err = f.Write([]byte{104, 105}) | |||||
| require.NoError(t, err) | |||||
| err = run.os.Chtimes(filepath, mtime, mtime) | |||||
| require.NoError(t, err) | |||||
| err = f.Close() | |||||
| require.NoError(t, err) | |||||
| run.waitForWriters() | |||||
| info, err := run.os.Stat(filepath) | |||||
| require.NoError(t, err) | |||||
| // avoid errors because of timezone differences | |||||
| assert.Equal(t, info.ModTime().Unix(), mtime.Unix()) | |||||
| run.rm(t, "cp-archive-test") | |||||
| } | |||||
| @@ -0,0 +1,338 @@ | |||||
| // Test suite for rclonefs | |||||
| package vfstest | |||||
| import ( | |||||
| "fmt" | |||||
| "io" | |||||
| "os" | |||||
| "path" | |||||
| "path/filepath" | |||||
| "reflect" | |||||
| "runtime" | |||||
| "strings" | |||||
| "testing" | |||||
| "time" | |||||
| "github.com/stretchr/testify/assert" | |||||
| "github.com/stretchr/testify/require" | |||||
| "gitlink.org.cn/cloudream/common/pkgs/logger" | |||||
| "gitlink.org.cn/cloudream/storage2/client/internal/mount" | |||||
| ) | |||||
| const ( | |||||
| waitForWritersDelay = 30 * time.Second // time to wait for existing writers | |||||
| testRoot = "b1/p1" | |||||
| ) | |||||
| // RunTests runs all the tests against all the VFS cache modes | |||||
| // | |||||
| // If useVFS is set then it runs the tests against a VFS rather than a | |||||
| // mount | |||||
| // | |||||
| // If useVFS is not set then it runs the mount in a subprocess in | |||||
| // order to avoid kernel deadlocks. | |||||
| func RunTests(t *testing.T, mnt *mount.Mount) { | |||||
| run = &Run{ | |||||
| os: realOs{}, | |||||
| mountPath: mnt.MountPoint(), | |||||
| mnt: mnt, | |||||
| } | |||||
| run.Init() | |||||
| logger.Infof("Starting test run") | |||||
| ok := t.Run("", func(t *testing.T) { | |||||
| // t.Run("TestTouchAndDelete", TestTouchAndDelete) | |||||
| // t.Run("TestRenameOpenHandle", TestRenameOpenHandle) | |||||
| // t.Run("TestDirLs", TestDirLs) | |||||
| // t.Run("TestDirCreateAndRemoveDir", TestDirCreateAndRemoveDir) | |||||
| // t.Run("TestDirCreateAndRemoveFile", TestDirCreateAndRemoveFile) | |||||
| // t.Run("TestDirRenameFile", TestDirRenameFile) | |||||
| // t.Run("TestDirRenameEmptyDir", TestDirRenameEmptyDir) | |||||
| // t.Run("TestDirRenameFullDir", TestDirRenameFullDir) | |||||
| // t.Run("TestDirModTime", TestDirModTime) | |||||
| // if enableCacheTests { | |||||
| // t.Run("TestDirCacheFlush", TestDirCacheFlush) | |||||
| // } | |||||
| // t.Run("TestDirCacheFlushOnDirRename", TestDirCacheFlushOnDirRename) | |||||
| // t.Run("TestFileModTime", TestFileModTime) | |||||
| // t.Run("TestFileModTimeWithOpenWriters", TestFileModTimeWithOpenWriters) | |||||
| // t.Run("TestMount", TestMount) | |||||
| // t.Run("TestRoot", TestRoot) | |||||
| // t.Run("TestReadByByte", TestReadByByte) | |||||
| // t.Run("TestReadChecksum", TestReadChecksum) | |||||
| // t.Run("TestReadFileDoubleClose", TestReadFileDoubleClose) | |||||
| // t.Run("TestReadSeek", TestReadSeek) | |||||
| // t.Run("TestWriteFileNoWrite", TestWriteFileNoWrite) | |||||
| // t.Run("TestWriteFileWrite", TestWriteFileWrite) | |||||
| // t.Run("TestWriteFileOverwrite", TestWriteFileOverwrite) | |||||
| // t.Run("TestWriteFileDoubleClose", TestWriteFileDoubleClose) | |||||
| // t.Run("TestWriteFileFsync", TestWriteFileFsync) | |||||
| // t.Run("TestWriteFileDup", TestWriteFileDup) | |||||
| t.Run("TestWriteFileAppend", TestWriteFileAppend) | |||||
| }) | |||||
| logger.Infof("Finished test run (ok=%v)", ok) | |||||
| run.Finalise() | |||||
| } | |||||
| // Run holds the remotes for a test run | |||||
| type Run struct { | |||||
| os Oser | |||||
| mountPath string | |||||
| skip bool | |||||
| mnt *mount.Mount | |||||
| } | |||||
| // run holds the master Run data | |||||
| var run *Run | |||||
| func (r *Run) skipIfNoFUSE(t *testing.T) { | |||||
| if r.skip { | |||||
| t.Skip("FUSE not found so skipping test") | |||||
| } | |||||
| } | |||||
| func (r *Run) skipIfVFS(t *testing.T) { | |||||
| if r.skip { | |||||
| t.Skip("Not running under VFS") | |||||
| } | |||||
| } | |||||
| func (r *Run) Init() { | |||||
| testRootDir := filepath.Join(r.mountPath, testRoot) | |||||
| err := os.MkdirAll(testRootDir, 0644) | |||||
| if err != nil { | |||||
| logger.Infof("Failed to make test root dir %q: %v", testRootDir, err) | |||||
| } | |||||
| } | |||||
| // Finalise cleans the remote and unmounts | |||||
| func (r *Run) Finalise() { | |||||
| r.mnt.Stop() | |||||
| err := os.RemoveAll(r.mountPath) | |||||
| if err != nil { | |||||
| logger.Infof("Failed to clean mountPath %q: %v", r.mountPath, err) | |||||
| } | |||||
| } | |||||
| // path returns an OS local path for filepath | |||||
| func (r *Run) path(filePath string) string { | |||||
| // return windows drive letter root as E:\ | |||||
| if filePath == "" && runtime.GOOS == "windows" { | |||||
| return r.mountPath + `\` | |||||
| } | |||||
| return filepath.Join(r.mountPath, testRoot, filepath.FromSlash(filePath)) | |||||
| } | |||||
| type dirMap map[string]struct{} | |||||
| // Create a dirMap from a string | |||||
| func newDirMap(dirString string) (dm dirMap) { | |||||
| dm = make(dirMap) | |||||
| for _, entry := range strings.Split(dirString, "|") { | |||||
| if entry != "" { | |||||
| dm[entry] = struct{}{} | |||||
| } | |||||
| } | |||||
| return dm | |||||
| } | |||||
| // Returns a dirmap with only the files in | |||||
| func (dm dirMap) filesOnly() dirMap { | |||||
| newDm := make(dirMap) | |||||
| for name := range dm { | |||||
| if !strings.HasSuffix(name, "/") { | |||||
| newDm[name] = struct{}{} | |||||
| } | |||||
| } | |||||
| return newDm | |||||
| } | |||||
| // reads the local tree into dir | |||||
| func (r *Run) readLocal(t *testing.T, dir dirMap, filePath string) { | |||||
| realPath := r.path(filePath) | |||||
| files, err := r.os.ReadDir(realPath) | |||||
| require.NoError(t, err) | |||||
| for _, fi := range files { | |||||
| name := path.Join(filePath, fi.Name()) | |||||
| if fi.IsDir() { | |||||
| dir[name+"/"] = struct{}{} | |||||
| r.readLocal(t, dir, name) | |||||
| // assert.Equal(t, os.FileMode(r.vfsOpt.DirPerms)&os.ModePerm, fi.Mode().Perm()) | |||||
| } else { | |||||
| dir[fmt.Sprintf("%s %d", name, fi.Size())] = struct{}{} | |||||
| // assert.Equal(t, os.FileMode(r.vfsOpt.FilePerms)&os.ModePerm, fi.Mode().Perm()) | |||||
| } | |||||
| } | |||||
| } | |||||
| // reads the remote tree into dir | |||||
| // func (r *Run) readRemote(t *testing.T, dir dirMap, filepath string) { | |||||
| // // objs, dirs, err := walk.GetAll(context.Background(), r.fremote, filepath, true, 1) | |||||
| // // if err == fs.ErrorDirNotFound { | |||||
| // // return | |||||
| // // } | |||||
| // // require.NoError(t, err) | |||||
| // // for _, obj := range objs { | |||||
| // // dir[fmt.Sprintf("%s %d", obj.Remote(), obj.Size())] = struct{}{} | |||||
| // // } | |||||
| // // for _, d := range dirs { | |||||
| // // name := d.Remote() | |||||
| // // dir[name+"/"] = struct{}{} | |||||
| // // r.readRemote(t, dir, name) | |||||
| // // } | |||||
| // } | |||||
| // checkDir checks the local and remote against the string passed in | |||||
| func (r *Run) checkDir(t *testing.T, dirString string) { | |||||
| var retries = 3 | |||||
| sleep := time.Second / 5 | |||||
| var fuseOK bool | |||||
| var dm, localDm dirMap | |||||
| for i := 1; i <= retries; i++ { | |||||
| dm = newDirMap(dirString) | |||||
| localDm = make(dirMap) | |||||
| r.readLocal(t, localDm, "") | |||||
| fuseOK = reflect.DeepEqual(dm, localDm) | |||||
| if fuseOK { | |||||
| return | |||||
| } | |||||
| sleep *= 2 | |||||
| t.Logf("Sleeping for %v for list eventual consistency: %d/%d", sleep, i, retries) | |||||
| time.Sleep(sleep) | |||||
| } | |||||
| assert.Equal(t, dm, localDm, "expected vs fuse mount") | |||||
| } | |||||
| // writeFile writes data to a file named by filename. | |||||
| // If the file does not exist, WriteFile creates it with permissions perm; | |||||
| // otherwise writeFile truncates it before writing. | |||||
| // If there is an error writing then writeFile | |||||
| // deletes it an existing file and tries again. | |||||
| func writeFile(filename string, data []byte, perm os.FileMode) error { | |||||
| f, err := run.os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) | |||||
| if err != nil { | |||||
| err = run.os.Remove(filename) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| f, err = run.os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, perm) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| } | |||||
| n, err := f.Write(data) | |||||
| if err == nil && n < len(data) { | |||||
| err = io.ErrShortWrite | |||||
| } | |||||
| if err1 := f.Close(); err == nil { | |||||
| err = err1 | |||||
| } | |||||
| return err | |||||
| } | |||||
| func (r *Run) createFile(t *testing.T, filepath string, contents string) { | |||||
| filepath = r.path(filepath) | |||||
| err := writeFile(filepath, []byte(contents), 0644) | |||||
| require.NoError(t, err) | |||||
| r.waitForWriters() | |||||
| } | |||||
| func (r *Run) readFile(t *testing.T, filepath string) string { | |||||
| filepath = r.path(filepath) | |||||
| result, err := r.os.ReadFile(filepath) | |||||
| require.NoError(t, err) | |||||
| return string(result) | |||||
| } | |||||
| func (r *Run) mkdir(t *testing.T, filepath string) { | |||||
| filepath = r.path(filepath) | |||||
| err := r.os.Mkdir(filepath, 0755) | |||||
| require.NoError(t, err) | |||||
| } | |||||
| func (r *Run) rm(t *testing.T, filepath string) { | |||||
| filepath = r.path(filepath) | |||||
| err := r.os.Remove(filepath) | |||||
| require.NoError(t, err) | |||||
| // Wait for file to disappear from listing | |||||
| for i := 0; i < 100; i++ { | |||||
| _, err := r.os.Stat(filepath) | |||||
| if os.IsNotExist(err) { | |||||
| return | |||||
| } | |||||
| time.Sleep(100 * time.Millisecond) | |||||
| } | |||||
| assert.Fail(t, "failed to delete file", filepath) | |||||
| } | |||||
| func (r *Run) rmdir(t *testing.T, filepath string) { | |||||
| filepath = r.path(filepath) | |||||
| err := r.os.Remove(filepath) | |||||
| require.NoError(t, err) | |||||
| } | |||||
| func (r *Run) waitForWriters() { | |||||
| timeout := waitForWritersDelay | |||||
| tickTime := 10 * time.Millisecond | |||||
| deadline := time.NewTimer(timeout) | |||||
| defer deadline.Stop() | |||||
| tick := time.NewTimer(tickTime) | |||||
| defer tick.Stop() | |||||
| tick.Stop() | |||||
| for { | |||||
| writers := 0 | |||||
| cacheInUse := 0 | |||||
| status := r.mnt.Dump() | |||||
| for _, f := range status.Cache.ActiveFiles { | |||||
| if f.RefCount > 0 { | |||||
| writers++ | |||||
| } | |||||
| } | |||||
| cacheInUse = len(status.Cache.ActiveFiles) | |||||
| if writers == 0 && cacheInUse == 0 { | |||||
| return | |||||
| } | |||||
| logger.Debugf("Still %d writers active and %d cache items in use, waiting %v", writers, cacheInUse, tickTime) | |||||
| tick.Reset(tickTime) | |||||
| select { | |||||
| case <-tick.C: | |||||
| case <-deadline.C: | |||||
| logger.Errorf("Exiting even though %d writers active and %d cache items in use after %v\n%v", writers, cacheInUse, timeout, status) | |||||
| return | |||||
| } | |||||
| tickTime *= 2 | |||||
| if tickTime > time.Second { | |||||
| tickTime = time.Second | |||||
| } | |||||
| } | |||||
| } | |||||
| // TestMount checks that the Fs is mounted by seeing if the mountpoint | |||||
| // is in the mount output | |||||
| // func TestMount(t *testing.T) { | |||||
| // run.skipIfVFS(t) | |||||
| // run.skipIfNoFUSE(t) | |||||
| // if runtime.GOOS == "windows" { | |||||
| // t.Skip("not running on windows") | |||||
| // } | |||||
| // out, err := exec.Command("mount").Output() | |||||
| // require.NoError(t, err) | |||||
| // assert.Contains(t, string(out), run.mountPath) | |||||
| // } | |||||
| // TestRoot checks root directory is present and correct | |||||
| func TestRoot(t *testing.T) { | |||||
| run.skipIfVFS(t) | |||||
| run.skipIfNoFUSE(t) | |||||
| fi, err := os.Lstat(run.mountPath) | |||||
| require.NoError(t, err) | |||||
| assert.True(t, fi.IsDir()) | |||||
| // assert.Equal(t, os.FileMode(run.vfsOpt.DirPerms)&os.ModePerm, fi.Mode().Perm()) | |||||
| } | |||||
| @@ -0,0 +1,139 @@ | |||||
| package vfstest | |||||
| import ( | |||||
| "os" | |||||
| "time" | |||||
| ) | |||||
| // Oser defines the things that the "os" package can do | |||||
| // | |||||
| // This covers what the VFS can do also | |||||
| type Oser interface { | |||||
| Chtimes(name string, atime time.Time, mtime time.Time) error | |||||
| Create(name string) (OsFiler, error) | |||||
| Mkdir(name string, perm os.FileMode) error | |||||
| Open(name string) (OsFiler, error) | |||||
| OpenFile(name string, flags int, perm os.FileMode) (fd OsFiler, err error) | |||||
| ReadDir(dirname string) ([]os.FileInfo, error) | |||||
| ReadFile(filename string) (b []byte, err error) | |||||
| Remove(name string) error | |||||
| Rename(oldName, newName string) error | |||||
| Stat(path string) (os.FileInfo, error) | |||||
| } | |||||
| // realOs is an implementation of Oser backed by the "os" package | |||||
| type realOs struct { | |||||
| } | |||||
| // realOsFile is an implementation of vfs.Handle | |||||
| type realOsFile struct { | |||||
| *os.File | |||||
| } | |||||
| // Flush | |||||
| func (f realOsFile) Flush() error { | |||||
| return nil | |||||
| } | |||||
| // Release | |||||
| func (f realOsFile) Release() error { | |||||
| return f.File.Close() | |||||
| } | |||||
| // Chtimes | |||||
| func (r realOs) Chtimes(name string, atime time.Time, mtime time.Time) error { | |||||
| return os.Chtimes(name, atime, mtime) | |||||
| } | |||||
| // Create | |||||
| func (r realOs) Create(name string) (OsFiler, error) { | |||||
| fd, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return realOsFile{File: fd}, err | |||||
| } | |||||
| // Mkdir | |||||
| func (r realOs) Mkdir(name string, perm os.FileMode) error { | |||||
| return os.Mkdir(name, perm) | |||||
| } | |||||
| // Open | |||||
| func (r realOs) Open(name string) (OsFiler, error) { | |||||
| fd, err := os.Open(name) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return realOsFile{File: fd}, err | |||||
| } | |||||
| // OpenFile | |||||
| func (r realOs) OpenFile(name string, flags int, perm os.FileMode) (OsFiler, error) { | |||||
| fd, err := os.OpenFile(name, flags, perm) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return realOsFile{File: fd}, err | |||||
| } | |||||
| // ReadDir | |||||
| func (r realOs) ReadDir(dirname string) ([]os.FileInfo, error) { | |||||
| entries, err := os.ReadDir(dirname) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| infos := make([]os.FileInfo, 0, len(entries)) | |||||
| for _, entry := range entries { | |||||
| info, err := entry.Info() | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| infos = append(infos, info) | |||||
| } | |||||
| return infos, nil | |||||
| } | |||||
| // ReadFile | |||||
| func (r realOs) ReadFile(filename string) (b []byte, err error) { | |||||
| return os.ReadFile(filename) | |||||
| } | |||||
| // Remove | |||||
| func (r realOs) Remove(name string) error { | |||||
| return os.Remove(name) | |||||
| } | |||||
| // Rename | |||||
| func (r realOs) Rename(oldName, newName string) error { | |||||
| return os.Rename(oldName, newName) | |||||
| } | |||||
| // Stat | |||||
| func (r realOs) Stat(path string) (os.FileInfo, error) { | |||||
| return os.Stat(path) | |||||
| } | |||||
| // Check interfaces | |||||
| var _ Oser = &realOs{} | |||||
| // OsFiler is the methods on *os.File | |||||
| type OsFiler interface { | |||||
| Chdir() error | |||||
| Chmod(mode os.FileMode) error | |||||
| Chown(uid, gid int) error | |||||
| Close() error | |||||
| Fd() uintptr | |||||
| Name() string | |||||
| Read(b []byte) (n int, err error) | |||||
| ReadAt(b []byte, off int64) (n int, err error) | |||||
| Readdir(n int) ([]os.FileInfo, error) | |||||
| Readdirnames(n int) (names []string, err error) | |||||
| Seek(offset int64, whence int) (ret int64, err error) | |||||
| Stat() (os.FileInfo, error) | |||||
| Sync() error | |||||
| Truncate(size int64) error | |||||
| Write(b []byte) (n int, err error) | |||||
| WriteAt(b []byte, off int64) (n int, err error) | |||||
| WriteString(s string) (n int, err error) | |||||
| } | |||||
| @@ -0,0 +1,123 @@ | |||||
| package vfstest | |||||
| import ( | |||||
| "io" | |||||
| "testing" | |||||
| "github.com/stretchr/testify/assert" | |||||
| ) | |||||
| // TestReadByByte reads by byte including don't read any bytes | |||||
| func TestReadByByte(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| var data = []byte("hellohello") | |||||
| run.createFile(t, "testfile", string(data)) | |||||
| run.checkDir(t, "testfile 10") | |||||
| for i := 0; i < len(data); i++ { | |||||
| fd, err := run.os.Open(run.path("testfile")) | |||||
| assert.NoError(t, err) | |||||
| for j := 0; j < i; j++ { | |||||
| buf := make([]byte, 1) | |||||
| n, err := io.ReadFull(fd, buf) | |||||
| assert.NoError(t, err) | |||||
| assert.Equal(t, 1, n) | |||||
| assert.Equal(t, buf[0], data[j]) | |||||
| } | |||||
| err = fd.Close() | |||||
| assert.NoError(t, err) | |||||
| } | |||||
| run.rm(t, "testfile") | |||||
| } | |||||
| // TestReadChecksum checks the checksum reading is working | |||||
| func TestReadChecksum(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| // create file big enough so we exceed any single FUSE read | |||||
| // request | |||||
| b := make([]rune, 3*128*1024) | |||||
| for i := range b { | |||||
| b[i] = 'r' | |||||
| } | |||||
| run.createFile(t, "bigfile", string(b)) | |||||
| // The hash comparison would fail in Flush, if we did not | |||||
| // ensure we read the whole file | |||||
| fd, err := run.os.Open(run.path("bigfile")) | |||||
| assert.NoError(t, err) | |||||
| buf := make([]byte, 10) | |||||
| _, err = io.ReadFull(fd, buf) | |||||
| assert.NoError(t, err) | |||||
| err = fd.Close() | |||||
| assert.NoError(t, err) | |||||
| // The hash comparison would fail, because we only read parts | |||||
| // of the file | |||||
| fd, err = run.os.Open(run.path("bigfile")) | |||||
| assert.NoError(t, err) | |||||
| // read at start | |||||
| _, err = io.ReadFull(fd, buf) | |||||
| assert.NoError(t, err) | |||||
| // read at end | |||||
| _, err = fd.Seek(int64(len(b)-len(buf)), io.SeekStart) | |||||
| assert.NoError(t, err) | |||||
| _, err = io.ReadFull(fd, buf) | |||||
| assert.NoError(t, err) | |||||
| // ensure we don't compare hashes | |||||
| err = fd.Close() | |||||
| assert.NoError(t, err) | |||||
| run.rm(t, "bigfile") | |||||
| } | |||||
| // TestReadSeek test seeking | |||||
| func TestReadSeek(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| var data = []byte("helloHELLO") | |||||
| run.createFile(t, "testfile", string(data)) | |||||
| run.checkDir(t, "testfile 10") | |||||
| fd, err := run.os.Open(run.path("testfile")) | |||||
| assert.NoError(t, err) | |||||
| // Seek to half way | |||||
| _, err = fd.Seek(5, io.SeekStart) | |||||
| assert.NoError(t, err) | |||||
| buf, err := io.ReadAll(fd) | |||||
| assert.NoError(t, err) | |||||
| assert.Equal(t, buf, []byte("HELLO")) | |||||
| // Test seeking to the end | |||||
| _, err = fd.Seek(10, io.SeekStart) | |||||
| assert.NoError(t, err) | |||||
| buf, err = io.ReadAll(fd) | |||||
| assert.NoError(t, err) | |||||
| assert.Equal(t, buf, []byte("")) | |||||
| // Test seeking beyond the end | |||||
| _, err = fd.Seek(1000000, io.SeekStart) | |||||
| assert.NoError(t, err) | |||||
| buf, err = io.ReadAll(fd) | |||||
| assert.NoError(t, err) | |||||
| assert.Equal(t, buf, []byte("")) | |||||
| // Now back to the start | |||||
| _, err = fd.Seek(0, io.SeekStart) | |||||
| assert.NoError(t, err) | |||||
| buf, err = io.ReadAll(fd) | |||||
| assert.NoError(t, err) | |||||
| assert.Equal(t, buf, []byte("helloHELLO")) | |||||
| err = fd.Close() | |||||
| assert.NoError(t, err) | |||||
| run.rm(t, "testfile") | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| //go:build !linux && !darwin && !freebsd | |||||
| package vfstest | |||||
| import ( | |||||
| "runtime" | |||||
| "testing" | |||||
| ) | |||||
| // TestReadFileDoubleClose tests double close on read | |||||
| func TestReadFileDoubleClose(t *testing.T) { | |||||
| t.Skip("not supported on " + runtime.GOOS) | |||||
| } | |||||
| @@ -0,0 +1,53 @@ | |||||
| //go:build linux || darwin || freebsd | |||||
| package vfstest | |||||
| import ( | |||||
| "syscall" | |||||
| "testing" | |||||
| "github.com/stretchr/testify/assert" | |||||
| ) | |||||
| // TestReadFileDoubleClose tests double close on read | |||||
| func TestReadFileDoubleClose(t *testing.T) { | |||||
| run.skipIfVFS(t) | |||||
| run.skipIfNoFUSE(t) | |||||
| run.createFile(t, "testdoubleclose", "hello") | |||||
| in, err := run.os.Open(run.path("testdoubleclose")) | |||||
| assert.NoError(t, err) | |||||
| fd := in.Fd() | |||||
| fd1, err := syscall.Dup(int(fd)) | |||||
| assert.NoError(t, err) | |||||
| fd2, err := syscall.Dup(int(fd)) | |||||
| assert.NoError(t, err) | |||||
| // close one of the dups - should produce no error | |||||
| err = syscall.Close(fd1) | |||||
| assert.NoError(t, err) | |||||
| // read from the file | |||||
| buf := make([]byte, 1) | |||||
| _, err = in.Read(buf) | |||||
| assert.NoError(t, err) | |||||
| // close it | |||||
| err = in.Close() | |||||
| assert.NoError(t, err) | |||||
| // read from the other dup - should produce no error as this | |||||
| // file is now buffered | |||||
| n, err := syscall.Read(fd2, buf) | |||||
| assert.NoError(t, err) | |||||
| assert.Equal(t, 1, n) | |||||
| // close the dup - should not produce an error | |||||
| err = syscall.Close(fd2) | |||||
| assert.NoError(t, err, "input/output error") | |||||
| run.rm(t, "testdoubleclose") | |||||
| } | |||||
| @@ -0,0 +1,170 @@ | |||||
| package vfstest | |||||
| import ( | |||||
| "os" | |||||
| "runtime" | |||||
| "testing" | |||||
| "time" | |||||
| "github.com/stretchr/testify/assert" | |||||
| "github.com/stretchr/testify/require" | |||||
| ) | |||||
| // TestWriteFileNoWrite tests writing a file with no write()'s to it | |||||
| func TestWriteFileNoWrite(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| fd, err := osCreate(run.path("testnowrite")) | |||||
| require.NoError(t, err) | |||||
| err = fd.Close() | |||||
| assert.NoError(t, err) | |||||
| run.waitForWriters() | |||||
| run.checkDir(t, "testnowrite 0") | |||||
| run.rm(t, "testnowrite") | |||||
| } | |||||
| // FIXMETestWriteOpenFileInDirListing tests open file in directory listing | |||||
| func FIXMETestWriteOpenFileInDirListing(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| fd, err := osCreate(run.path("testnowrite")) | |||||
| assert.NoError(t, err) | |||||
| run.checkDir(t, "testnowrite 0") | |||||
| err = fd.Close() | |||||
| assert.NoError(t, err) | |||||
| run.waitForWriters() | |||||
| run.rm(t, "testnowrite") | |||||
| } | |||||
| // TestWriteFileWrite tests writing a file and reading it back | |||||
| func TestWriteFileWrite(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| run.createFile(t, "testwrite", "data") | |||||
| run.checkDir(t, "testwrite 4") | |||||
| contents := run.readFile(t, "testwrite") | |||||
| assert.Equal(t, "data", contents) | |||||
| run.rm(t, "testwrite") | |||||
| } | |||||
| // TestWriteFileOverwrite tests overwriting a file | |||||
| func TestWriteFileOverwrite(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| run.createFile(t, "testwrite", "data") | |||||
| run.checkDir(t, "testwrite 4") | |||||
| run.createFile(t, "testwrite", "potato") | |||||
| contents := run.readFile(t, "testwrite") | |||||
| assert.Equal(t, "potato", contents) | |||||
| run.rm(t, "testwrite") | |||||
| } | |||||
| // TestWriteFileFsync tests Fsync | |||||
| // | |||||
| // NB the code for this is in file.go rather than write.go | |||||
| func TestWriteFileFsync(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| filepath := run.path("to be synced") | |||||
| fd, err := osCreate(filepath) | |||||
| require.NoError(t, err) | |||||
| _, err = fd.Write([]byte("hello")) | |||||
| require.NoError(t, err) | |||||
| err = fd.Sync() | |||||
| require.NoError(t, err) | |||||
| err = fd.Close() | |||||
| require.NoError(t, err) | |||||
| run.waitForWriters() | |||||
| run.rm(t, "to be synced") | |||||
| } | |||||
| // TestWriteFileDup tests behavior of mmap() in Python by using dup() on a file handle | |||||
| func TestWriteFileDup(t *testing.T) { | |||||
| run.skipIfVFS(t) | |||||
| run.skipIfNoFUSE(t) | |||||
| filepath := run.path("to be synced") | |||||
| fh, err := osCreate(filepath) | |||||
| require.NoError(t, err) | |||||
| testData := []byte("0123456789") | |||||
| err = fh.Truncate(int64(len(testData) + 2)) | |||||
| require.NoError(t, err) | |||||
| err = fh.Sync() | |||||
| require.NoError(t, err) | |||||
| var dupFd uintptr | |||||
| dupFd, err = writeTestDup(fh.Fd()) | |||||
| require.NoError(t, err) | |||||
| dupFile := os.NewFile(dupFd, fh.Name()) | |||||
| _, err = dupFile.Write(testData) | |||||
| require.NoError(t, err) | |||||
| err = dupFile.Close() | |||||
| require.NoError(t, err) | |||||
| _, err = fh.Seek(int64(len(testData)), 0) | |||||
| require.NoError(t, err) | |||||
| _, err = fh.Write([]byte("10")) | |||||
| require.NoError(t, err) | |||||
| err = fh.Close() | |||||
| require.NoError(t, err) | |||||
| run.waitForWriters() | |||||
| run.rm(t, "to be synced") | |||||
| } | |||||
| // TestWriteFileAppend tests that O_APPEND works on cache backends >= writes | |||||
| func TestWriteFileAppend(t *testing.T) { | |||||
| run.skipIfNoFUSE(t) | |||||
| // TODO: Windows needs the v1.5 release of WinFsp to handle O_APPEND properly. | |||||
| // Until it gets released, skip this test on Windows. | |||||
| if runtime.GOOS == "windows" { | |||||
| t.Skip("currently unsupported on Windows") | |||||
| } | |||||
| filepath := run.path("to be synced") | |||||
| fh, err := osCreate(filepath) | |||||
| require.NoError(t, err) | |||||
| testData := []byte("0123456789") | |||||
| appendData := []byte("10") | |||||
| _, err = fh.Write(testData) | |||||
| require.NoError(t, err) | |||||
| err = fh.Close() | |||||
| require.NoError(t, err) | |||||
| fh, err = osAppend(filepath) | |||||
| require.NoError(t, err) | |||||
| _, err = fh.Write(appendData) | |||||
| require.NoError(t, err) | |||||
| err = fh.Close() | |||||
| require.NoError(t, err) | |||||
| <-time.After(time.Second * 10) | |||||
| info, err := run.os.Stat(filepath) | |||||
| require.NoError(t, err) | |||||
| require.EqualValues(t, len(testData)+len(appendData), info.Size()) | |||||
| run.waitForWriters() | |||||
| run.rm(t, "to be synced") | |||||
| } | |||||
| @@ -0,0 +1,20 @@ | |||||
| //go:build !linux && !darwin && !freebsd && !windows | |||||
| // +build !linux,!darwin,!freebsd,!windows | |||||
| package vfstest | |||||
| import ( | |||||
| "errors" | |||||
| "runtime" | |||||
| "testing" | |||||
| ) | |||||
| // TestWriteFileDoubleClose tests double close on write | |||||
| func TestWriteFileDoubleClose(t *testing.T) { | |||||
| t.Skip("not supported on " + runtime.GOOS) | |||||
| } | |||||
| // writeTestDup performs the platform-specific implementation of the dup() unix | |||||
| func writeTestDup(oldfd uintptr) (uintptr, error) { | |||||
| return 0, errors.New("not supported on " + runtime.GOOS) | |||||
| } | |||||
| @@ -0,0 +1,62 @@ | |||||
| //go:build linux || darwin || freebsd | |||||
| package vfstest | |||||
| import ( | |||||
| "runtime" | |||||
| "testing" | |||||
| "github.com/stretchr/testify/assert" | |||||
| "github.com/stretchr/testify/require" | |||||
| "golang.org/x/sys/unix" | |||||
| ) | |||||
| // TestWriteFileDoubleClose tests double close on write | |||||
| func TestWriteFileDoubleClose(t *testing.T) { | |||||
| run.skipIfVFS(t) | |||||
| run.skipIfNoFUSE(t) | |||||
| if runtime.GOOS == "darwin" { | |||||
| t.Skip("Skipping test on OSX") | |||||
| } | |||||
| out, err := osCreate(run.path("testdoubleclose")) | |||||
| require.NoError(t, err) | |||||
| fd := out.Fd() | |||||
| fd1, err := unix.Dup(int(fd)) | |||||
| assert.NoError(t, err) | |||||
| fd2, err := unix.Dup(int(fd)) | |||||
| assert.NoError(t, err) | |||||
| // close one of the dups - should produce no error | |||||
| err = unix.Close(fd1) | |||||
| assert.NoError(t, err) | |||||
| // write to the file | |||||
| buf := []byte("hello") | |||||
| n, err := out.Write(buf) | |||||
| assert.NoError(t, err) | |||||
| assert.Equal(t, 5, n) | |||||
| // close it | |||||
| err = out.Close() | |||||
| assert.NoError(t, err) | |||||
| // write to the other dup | |||||
| _, err = unix.Write(fd2, buf) | |||||
| assert.NoError(t, err) | |||||
| // close the dup - should not produce an error | |||||
| err = unix.Close(fd2) | |||||
| assert.NoError(t, err) | |||||
| run.waitForWriters() | |||||
| run.rm(t, "testdoubleclose") | |||||
| } | |||||
| // writeTestDup performs the platform-specific implementation of the dup() unix | |||||
| func writeTestDup(oldfd uintptr) (uintptr, error) { | |||||
| newfd, err := unix.Dup(int(oldfd)) | |||||
| return uintptr(newfd), err | |||||
| } | |||||
| @@ -0,0 +1,22 @@ | |||||
| //go:build windows | |||||
| package vfstest | |||||
| import ( | |||||
| "runtime" | |||||
| "testing" | |||||
| "golang.org/x/sys/windows" | |||||
| ) | |||||
| // TestWriteFileDoubleClose tests double close on write | |||||
| func TestWriteFileDoubleClose(t *testing.T) { | |||||
| t.Skip("not supported on " + runtime.GOOS) | |||||
| } | |||||
| // writeTestDup performs the platform-specific implementation of the dup() syscall | |||||
| func writeTestDup(oldfd uintptr) (uintptr, error) { | |||||
| p := windows.CurrentProcess() | |||||
| var h windows.Handle | |||||
| return uintptr(h), windows.DuplicateHandle(p, windows.Handle(oldfd), p, &h, 0, true, windows.DUPLICATE_SAME_ACCESS) | |||||
| } | |||||
| @@ -382,27 +382,7 @@ func (svc *ObjectService) Delete(objectIDs []types.ObjectID) error { | |||||
| } | } | ||||
| sucs = lo.Keys(avaiIDs) | sucs = lo.Keys(avaiIDs) | ||||
| err = svc.DB.Object().BatchDelete(tx, objectIDs) | |||||
| if err != nil { | |||||
| return fmt.Errorf("batch deleting objects: %w", err) | |||||
| } | |||||
| err = svc.DB.ObjectBlock().BatchDeleteByObjectID(tx, objectIDs) | |||||
| if err != nil { | |||||
| return fmt.Errorf("batch deleting object blocks: %w", err) | |||||
| } | |||||
| err = svc.DB.PinnedObject().BatchDeleteByObjectID(tx, objectIDs) | |||||
| if err != nil { | |||||
| return fmt.Errorf("batch deleting pinned objects: %w", err) | |||||
| } | |||||
| err = svc.DB.ObjectAccessStat().BatchDeleteByObjectID(tx, objectIDs) | |||||
| if err != nil { | |||||
| return fmt.Errorf("batch deleting object access stats: %w", err) | |||||
| } | |||||
| return nil | |||||
| return svc.DB.Object().BatchDeleteComplete(tx, sucs) | |||||
| }) | }) | ||||
| if err != nil { | if err != nil { | ||||
| return err | return err | ||||