| @@ -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 | |||
| } | |||
| 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 { | |||
| return ctx.Table("Object").Where("PackageID = ? AND Path LIKE ?", oldPkgID, escapeLike("", "%", oldPrefix)). | |||
| Updates(map[string]any{ | |||
| @@ -628,3 +624,36 @@ func (db *ObjectDB) AppendPart(tx SQLContext, block types.ObjectBlock) error { | |||
| 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() | |||
| } | |||
| func (m *Mount) MountPoint() string { | |||
| if m.cfg == nil { | |||
| return "" | |||
| } | |||
| return m.cfg.MountPoint | |||
| } | |||
| func (m *Mount) Dump() MountStatus { | |||
| if m.vfs == nil { | |||
| return MountStatus{} | |||
| @@ -27,6 +27,10 @@ func (m *Mount) Start() *MountEventChan { | |||
| return m.eventChan | |||
| } | |||
| func (m *Mount) MountPoint() string { | |||
| return "" | |||
| } | |||
| func (m *Mount) Stop() { | |||
| m.eventChan.Send(ExitEvent{}) | |||
| } | |||
| @@ -52,6 +52,7 @@ func loadCacheDir(c *Cache, pathComps []string) (*CacheDir, error) { | |||
| } | |||
| return &CacheDir{ | |||
| cache: c, | |||
| pathComps: pathComps, | |||
| name: stat.Name(), | |||
| modTime: stat.ModTime(), | |||
| @@ -75,6 +76,7 @@ func makeCacheDirFromOption(c *Cache, pathComps []string, opt CreateDirOption) ( | |||
| os.Chtimes(dataPath, opt.ModTime, opt.ModTime) | |||
| return &CacheDir{ | |||
| cache: c, | |||
| pathComps: pathComps, | |||
| name: pathComps[len(pathComps)-1], | |||
| modTime: opt.ModTime, | |||
| @@ -99,8 +99,12 @@ func listChildren(vfs *Vfs, ctx context.Context, parent FuseNode) ([]fuse.FsEntr | |||
| } | |||
| 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 { | |||
| 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 | |||
| } | |||
| @@ -223,8 +227,11 @@ func moveChild(vfs *Vfs, ctx context.Context, oldName string, oldParent FuseNode | |||
| } | |||
| 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 | |||
| @@ -273,11 +280,11 @@ func moveRemote(vfs *Vfs, tx db.SQLContext, oldChildPath []string, newParentPath | |||
| 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 { | |||
| return d.Object().MoveByPrefix(tx, | |||
| 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) | |||
| 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 { | |||
| return err | |||