diff --git a/vfs/dir.go b/vfs/dir.go index c45033d0e..cc598a7bb 100644 --- a/vfs/dir.go +++ b/vfs/dir.go @@ -862,6 +862,30 @@ func (d *Dir) Create(name string, flags int) (*File, error) { if d.vfs.Opt.ReadOnly { return nil, EROFS } + // Avoid regular and symlink identical names in same directory + { + isLink := strings.HasSuffix(name, fs.LinkSuffix) + + rname := name + if isLink { + rname = strings.TrimSuffix(rname, fs.LinkSuffix) + } else { + rname += fs.LinkSuffix + } + + _, err = d.stat(rname) + + switch err { + case ENOENT: + // not found, carry on + case nil: + return nil, EEXIST + default: + // a different error - report + fs.Errorf(d, "Dir.Create stat failed: %v", err) + return nil, err + } + } // This gets added to the directory when the file is opened for write return newFile(d, d.Path(), nil, name), nil } @@ -887,6 +911,22 @@ func (d *Dir) Mkdir(name string) (*Dir, error) { fs.Errorf(d, "Dir.Mkdir failed to read directory: %v", err) return nil, err } + // Avoid dir and symlink identical names in same directory + { + rname := name + fs.LinkSuffix + _, err = d.stat(rname) + + switch err { + case ENOENT: + // not found, carry on + case nil: + return nil, EEXIST + default: + // a different error - report + fs.Errorf(d, "Dir.Mkdir failed to read directory: %v", err) + return nil, err + } + } // fs.Debugf(path, "Dir.Mkdir") err = d.f.Mkdir(context.TODO(), path) if err != nil { @@ -984,6 +1024,35 @@ func (d *Dir) Rename(oldName, newName string, destDir *Dir) error { fs.Errorf(oldPath, "Dir.Rename error: %v", err) return err } + // Ensure a link stay a link or a regular file a regular file + if strings.HasSuffix(oldName, fs.LinkSuffix) != strings.HasSuffix(newName, fs.LinkSuffix) { + fs.Errorf(d, "Dir.Rename inconsistent names: %v, %v", oldName, newName) + return EINVAL + } + // Avoid regular and symlink identical names in same directory + { + isLink := strings.HasSuffix(newName, fs.LinkSuffix) + + rnewName := newName + if isLink { + rnewName = strings.TrimSuffix(rnewName, fs.LinkSuffix) + } else { + rnewName += fs.LinkSuffix + } + + _, err = destDir.stat(rnewName) + + switch err { + case ENOENT: + // not found, carry on + case nil: + return EEXIST + default: + // a different error - report + fs.Errorf(d, "Dir.Rename stat failed: %v", err) + return err + } + } switch x := oldNode.DirEntry().(type) { case nil: if oldFile, ok := oldNode.(*File); ok { diff --git a/vfs/file.go b/vfs/file.go index 86f51e252..eba01861e 100644 --- a/vfs/file.go +++ b/vfs/file.go @@ -100,9 +100,14 @@ func (f *File) IsSymlink() bool { func (f *File) Mode() (mode os.FileMode) { f.mu.RLock() defer f.mu.RUnlock() - mode = f.d.vfs.Opt.FilePerms - if f.appendMode { - mode |= os.ModeAppend + if f.IsSymlink() { + mode = f.d.vfs.Opt.LinkPerms + } else { + mode = f.d.vfs.Opt.FilePerms + + if f.appendMode { + mode |= os.ModeAppend + } } return mode } diff --git a/vfs/vfs.go b/vfs/vfs.go index b52ec6196..5c689ceb8 100644 --- a/vfs/vfs.go +++ b/vfs/vfs.go @@ -483,6 +483,15 @@ func decodeOpenFlags(flags int) string { func (vfs *VFS) OpenFile(name string, flags int, perm os.FileMode) (fd Handle, err error) { defer log.Trace(name, "flags=%s, perm=%v", decodeOpenFlags(flags), perm)("fd=%v, err=%v", &fd, &err) + if flags&os.O_CREATE != 0 { + isLink := vfs.IsSymlink(name) + modeIsLink := perm&os.ModeSymlink != 0 + if (isLink && !modeIsLink) || (!isLink && modeIsLink) { + fs.Errorf(nil, "Inconsistent leaf/mode: %v / %v", name, perm) + return nil, EINVAL + } + } + // http://pubs.opengroup.org/onlinepubs/7908799/xsh/open.html // The result of using O_TRUNC with O_RDONLY is undefined. // Linux seems to truncate the file, but we prefer to return EINVAL @@ -512,7 +521,7 @@ func (vfs *VFS) OpenFile(name string, flags int, perm os.FileMode) (fd Handle, e // the returned file can be used for reading; the associated file // descriptor has mode O_RDONLY. func (vfs *VFS) Open(name string) (Handle, error) { - return vfs.OpenFile(name, os.O_RDONLY, 0) + return vfs.OpenFile(name, os.O_RDONLY, vfs.Opt.FilePerms) } // Create creates the named file with mode 0666 (before umask), truncating @@ -520,7 +529,7 @@ func (vfs *VFS) Open(name string) (Handle, error) { // File can be used for I/O; the associated file descriptor has mode // O_RDWR. func (vfs *VFS) Create(name string) (Handle, error) { - return vfs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + return vfs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, vfs.Opt.FilePerms) } // Rename oldName to newName @@ -730,3 +739,53 @@ func (vfs *VFS) TrimSymlink(remote string) (string, bool) { return remote, false } + +// Readlink returns the destination of the named symbolic link. +// If there is an error, it will be of type *PathError. +func (vfs *VFS) Readlink(name string) (s string, err error) { + if !strings.HasSuffix(name, fs.LinkSuffix) { + fs.Errorf(nil, "VFS.Readlink: Invalid symlink suffix: %v", name) + return "", EINVAL + } + + b, err := vfs.ReadFile(name) + if err != nil { + return "", err + } + + return string(b), nil +} + +// Symlink creates newname as a symbolic link to oldname. +// On Windows, a symlink to a non-existent oldname creates a file symlink; +// if oldname is later created as a directory the symlink will not work. +// If there is an error, it will be of type *LinkError. +func (vfs *VFS) Symlink(oldname, newname string) error { + if !strings.HasSuffix(newname, fs.LinkSuffix) { + fs.Errorf(nil, "VFS.Symlink: Invalid symlink suffix: %v", newname) + return EINVAL + } + + osFlags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC + osMode := vfs.Opt.FilePerms + if vfs.Opt.Links { + osMode = vfs.Opt.LinkPerms + } + + fh, err := vfs.OpenFile(newname, osFlags, osMode) + if err != nil { + return err + } + + _, err = fh.Write([]byte(oldname)) + if err != nil { + return err + } + + err = fh.Release() + if err != nil { + return err + } + + return nil +} diff --git a/vfs/vfstest/file.go b/vfs/vfstest/file.go index a4e490f0e..7622be907 100644 --- a/vfs/vfstest/file.go +++ b/vfs/vfstest/file.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/rclone/rclone/fs" "github.com/rclone/rclone/vfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -72,3 +73,277 @@ func TestFileModTimeWithOpenWriters(t *testing.T) { run.rm(t, "cp-archive-test") } + +// TestSymlinks tests all the api of the VFS / Mount symlinks support +func TestSymlinks(t *testing.T) { + run.skipIfNoFUSE(t) + + if runtime.GOOS == "windows" { + t.Skip("Skipping test on Windows") + } + + { + // VFS only implements os.Stat, which return information to target for symlinks, getting symlink information would require os.Lstat implementation. + // We will not bother to add Lstat implemented, but in the test we can just call os.Lstat which return the information needed when !useVFS + + // this is a link to a directory + // ldl, _ := os.Lstat("/tmp/kkk/link_dir") + // ld, _ := os.Stat("/tmp/kkk/link_dir") + + // LINK_DIR: Lrwxrwxrwx, false <-> drwxr-xr-x, true + // fs.Logf(nil, "LINK_DIR: %v, %v <-> %v, %v", ldl.Mode(), ldl.IsDir(), ld.Mode(), ld.IsDir()) + + // This is a link to a regular file + // lfl, _ := os.Lstat("/tmp/kkk/link_file") + // lf, _ := os.Stat("/tmp/kkk/link_file") + + // LINK_FILE: Lrwxrwxrwx, false <-> -rw-r--r--, false + // fs.Logf(nil, "LINK_FILE: %v, %v <-> %v, %v", lfl.Mode(), lfl.IsDir(), lf.Mode(), lf.IsDir()) + } + + if !run.useVFS { + t.Skip("Requires useVFS") + } + + suffix := "" + + if run.useVFS || !run.vfsOpt.Links { + suffix = fs.LinkSuffix + } + + fs.Logf(nil, "Links: %v, useVFS: %v, suffix: %v", run.vfsOpt.Links, run.useVFS, suffix) + + run.mkdir(t, "dir1") + run.mkdir(t, "dir1/sub1dir1") + run.createFile(t, "dir1/file1", "potato") + + run.mkdir(t, "dir2") + run.mkdir(t, "dir2/sub1dir2") + run.createFile(t, "dir2/file1", "chicken") + + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7") + + // Link to a file + run.relativeSymlink(t, "dir1/file1", "dir1file1_link"+suffix) + + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7|dir1file1_link"+suffix+" 10") + + if run.vfsOpt.Links { + if run.useVFS { + run.checkMode(t, "dir1file1_link"+suffix, run.vfsOpt.LinkPerms, run.vfsOpt.LinkPerms) + } else { + run.checkMode(t, "dir1file1_link"+suffix, run.vfsOpt.LinkPerms, run.vfsOpt.FilePerms) + } + } else { + run.checkMode(t, "dir1file1_link"+suffix, run.vfsOpt.FilePerms, run.vfsOpt.FilePerms) + } + + assert.Equal(t, "dir1/file1", run.readlink(t, "dir1file1_link"+suffix)) + + if !run.useVFS && run.vfsOpt.Links { + assert.Equal(t, "potato", run.readFile(t, "dir1file1_link"+suffix)) + + err := writeFile(run.path("dir1file1_link"+suffix), []byte("carrot"), 0600) + require.NoError(t, err) + + assert.Equal(t, "carrot", run.readFile(t, "dir1file1_link"+suffix)) + assert.Equal(t, "carrot", run.readFile(t, "dir1/file1")) + } else { + assert.Equal(t, "dir1/file1", run.readFile(t, "dir1file1_link"+suffix)) + } + + err := run.os.Rename(run.path("dir1file1_link"+suffix), run.path("dir1file1_link")+"_bla"+suffix) + require.NoError(t, err) + + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7|dir1file1_link_bla"+suffix+" 10") + + assert.Equal(t, "dir1/file1", run.readlink(t, "dir1file1_link_bla"+suffix)) + + run.rm(t, "dir1file1_link_bla"+suffix) + + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7") + + // Link to a dir + run.relativeSymlink(t, "dir1", "dir1_link"+suffix) + + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7|dir1_link"+suffix+" 4") + + if run.vfsOpt.Links { + if run.useVFS { + run.checkMode(t, "dir1_link"+suffix, run.vfsOpt.LinkPerms, run.vfsOpt.LinkPerms) + } else { + run.checkMode(t, "dir1_link"+suffix, run.vfsOpt.LinkPerms, run.vfsOpt.DirPerms) + } + } else { + run.checkMode(t, "dir1_link"+suffix, run.vfsOpt.FilePerms, run.vfsOpt.FilePerms) + } + + assert.Equal(t, "dir1", run.readlink(t, "dir1_link"+suffix)) + + fh, err := run.os.OpenFile(run.path("dir1_link"+suffix), os.O_WRONLY, 0600) + + if !run.useVFS && run.vfsOpt.Links { + require.Error(t, err) + + dirLinksEntries := make(dirMap) + run.readLocal(t, dirLinksEntries, "dir1_link"+suffix) + + assert.Equal(t, 2, len(dirLinksEntries)) + + dir1Entries := make(dirMap) + run.readLocal(t, dir1Entries, "dir1") + + assert.Equal(t, 2, len(dir1Entries)) + } else { + require.NoError(t, err) + // Don't care about the result, in some cache mode the file can't be opened for writing, so closing would trigger an err + _ = fh.Close() + + assert.Equal(t, "dir1", run.readFile(t, "dir1_link"+suffix)) + } + + err = run.os.Rename(run.path("dir1_link"+suffix), run.path("dir1_link")+"_bla"+suffix) + require.NoError(t, err) + + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7|dir1_link_bla"+suffix+" 4") + + assert.Equal(t, "dir1", run.readlink(t, "dir1_link_bla"+suffix)) + + run.rm(t, "dir1_link_bla"+suffix) // run.rmdir works fine as well + + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7") + + // Corner case #1 - We do not allow creating regular and symlink files having the same name (ie, test.txt and test.txt.rclonelink) + + // Symlink first, then regular + { + link1Name := "link1.txt" + suffix + + run.relativeSymlink(t, "dir1/file1", link1Name) + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7|link1.txt"+suffix+" 10") + + fh, err = run.os.OpenFile(run.path("link1.txt"), os.O_WRONLY|os.O_CREATE, run.vfsOpt.FilePerms) + + // On real mount with links enabled, that open the symlink target as expected, else that fails to create a new file + if !run.useVFS && run.vfsOpt.Links { + assert.Equal(t, true, err == nil) + // Don't care about the result, in some cache mode the file can't be opened for writing, so closing would trigger an err + _ = fh.Close() + } else { + assert.Equal(t, true, err != nil) + } + + run.rm(t, link1Name) + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7") + } + + // Regular first, then symlink + { + link1Name := "link1.txt" + suffix + + run.createFile(t, "link1.txt", "") + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7|link1.txt 0") + + err = run.os.Symlink(".", run.path(link1Name)) + assert.Equal(t, true, err != nil) + + run.rm(t, "link1.txt") + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7") + } + + // Corner case #2 - We do not allow creating directory and symlink file having the same name (ie, test and test.rclonelink) + + // Symlink first, then directory + { + link1Name := "link1" + suffix + + run.relativeSymlink(t, ".", link1Name) + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7|link1"+suffix+" 1") + + err = run.os.Mkdir(run.path("link1"), run.vfsOpt.DirPerms) + assert.Equal(t, true, err != nil) + + run.rm(t, link1Name) + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7") + } + + // Directory first, then symlink + { + link1Name := "link1" + suffix + + run.mkdir(t, "link1") + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7|link1/") + + err = run.os.Symlink(".", run.path(link1Name)) + assert.Equal(t, true, err != nil) + + run.rm(t, "link1") + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7") + } + + // Corner case #3 - We do not allow moving directory or file having the same name in a target (ie, test and test.rclonelink) + + // Move symlink -> regular file + { + link1Name := "link1.txt" + suffix + + run.relativeSymlink(t, ".", link1Name) + run.createFile(t, "dir1/link1.txt", "") + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7|link1.txt"+suffix+" 1|dir1/link1.txt 0") + + err = run.os.Rename(run.path(link1Name), run.path("dir1/"+link1Name)) + assert.Equal(t, true, err != nil) + + run.rm(t, link1Name) + run.rm(t, "dir1/link1.txt") + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7") + } + + // Move regular file -> symlink + { + link1Name := "link1.txt" + suffix + + run.createFile(t, "link1.txt", "") + run.relativeSymlink(t, ".", "dir1/"+link1Name) + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7|link1.txt 0|dir1/link1.txt"+suffix+" 1") + + err = run.os.Rename(run.path("link1.txt"), run.path("dir1/link1.txt")) + assert.Equal(t, true, err != nil) + + run.rm(t, "link1.txt") + run.rm(t, "dir1/"+link1Name) + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7") + } + + // Move symlink -> directory + { + link1Name := "link1" + suffix + + run.relativeSymlink(t, ".", link1Name) + run.mkdir(t, "dir1/link1") + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7|link1"+suffix+" 1|dir1/link1/") + + err = run.os.Rename(run.path(link1Name), run.path("dir1/"+link1Name)) + assert.Equal(t, true, err != nil) + + run.rm(t, link1Name) + run.rm(t, "dir1/link1") + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7") + } + + // Move directory -> symlink + { + link1Name := "dir1/link1" + suffix + + run.mkdir(t, "link1") + run.relativeSymlink(t, ".", link1Name) + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7|link1/|dir1/link1"+suffix+" 1") + + err = run.os.Rename(run.path("link1"), run.path("dir1/link1")) + assert.Equal(t, true, err != nil) + + run.rm(t, "link1") + run.rm(t, link1Name) + run.checkDir(t, "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7") + } +} diff --git a/vfs/vfstest/fs.go b/vfs/vfstest/fs.go index 6781fbe31..c652a29b9 100644 --- a/vfs/vfstest/fs.go +++ b/vfs/vfstest/fs.go @@ -48,6 +48,10 @@ func RunTests(t *testing.T, useVFS bool, mountFn mountlib.MountFn) { startMount(mountFn, useVFS, *runMount) return } + links := []bool{ + false, + true, + } tests := []struct { cacheMode vfscommon.CacheMode writeBack time.Duration @@ -59,47 +63,52 @@ func RunTests(t *testing.T, useVFS bool, mountFn mountlib.MountFn) { {cacheMode: vfscommon.CacheModeFull, writeBack: 100 * time.Millisecond}, } for _, test := range tests { - vfsOpt := vfsflags.Opt - vfsOpt.CacheMode = test.cacheMode - vfsOpt.WriteBack = test.writeBack - run = newRun(useVFS, &vfsOpt, mountFn) - what := fmt.Sprintf("CacheMode=%v", test.cacheMode) - if test.writeBack > 0 { - what += fmt.Sprintf(",WriteBack=%v", test.writeBack) - } - log.Printf("Starting test run with %s", what) - ok := t.Run(what, 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) - 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) - }) - log.Printf("Finished test run with %s (ok=%v)", what, ok) - run.Finalise() - if !ok { - break + for _, link := range links { + vfsOpt := vfsflags.Opt + vfsOpt.CacheMode = test.cacheMode + vfsOpt.WriteBack = test.writeBack + vfsOpt.Links = link + run = newRun(useVFS, &vfsOpt, mountFn) + what := fmt.Sprintf("CacheMode=%v", test.cacheMode) + if test.writeBack > 0 { + what += fmt.Sprintf(",WriteBack=%v", test.writeBack) + } + what += fmt.Sprintf(",Links=%v", link) + log.Printf("Starting test run with %s", what) + ok := t.Run(what, 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) + 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) + t.Run("TestSymlinks", TestSymlinks) + }) + log.Printf("Finished test run with %s (ok=%v)", what, ok) + run.Finalise() + if !ok { + break + } } } } @@ -210,10 +219,16 @@ func newDirMap(dirString string) (dm dirMap) { } // Returns a dirmap with only the files in -func (dm dirMap) filesOnly() dirMap { +func (dm dirMap) filesOnly(stripLinksSuffix bool) dirMap { newDm := make(dirMap) for name := range dm { if !strings.HasSuffix(name, "/") { + if stripLinksSuffix { + index := strings.LastIndex(name, " ") + if index != -1 { + name = strings.TrimSuffix(name[0:index], fs.LinkSuffix) + name[index:] + } + } newDm[name] = struct{}{} } } @@ -233,7 +248,11 @@ func (r *Run) readLocal(t *testing.T, dir dirMap, filePath string) { assert.Equal(t, r.vfsOpt.DirPerms&os.ModePerm, fi.Mode().Perm()) } else { dir[fmt.Sprintf("%s %d", name, fi.Size())] = struct{}{} - assert.Equal(t, r.vfsOpt.FilePerms&os.ModePerm, fi.Mode().Perm()) + if fi.Mode()&os.ModeSymlink != 0 { + assert.Equal(t, r.vfsOpt.LinkPerms&os.ModePerm, fi.Mode().Perm()) + } else { + assert.Equal(t, r.vfsOpt.FilePerms&os.ModePerm, fi.Mode().Perm()) + } } } } @@ -268,7 +287,7 @@ func (r *Run) checkDir(t *testing.T, dirString string) { remoteDm = make(dirMap) r.readRemote(t, remoteDm, "") // Ignore directories for remote compare - remoteOK = reflect.DeepEqual(dm.filesOnly(), remoteDm.filesOnly()) + remoteOK = reflect.DeepEqual(dm.filesOnly(false), remoteDm.filesOnly(!r.useVFS && r.vfsOpt.Links)) fuseOK = reflect.DeepEqual(dm, localDm) if remoteOK && fuseOK { return @@ -277,7 +296,7 @@ func (r *Run) checkDir(t *testing.T, dirString string) { t.Logf("Sleeping for %v for list eventual consistency: %d/%d", sleep, i, retries) time.Sleep(sleep) } - assert.Equal(t, dm.filesOnly(), remoteDm.filesOnly(), "expected vs remote") + assert.Equal(t, dm.filesOnly(false), remoteDm.filesOnly(!r.useVFS && r.vfsOpt.Links), "expected vs remote") assert.Equal(t, dm, localDm, "expected vs fuse mount") } @@ -350,6 +369,69 @@ func (r *Run) rmdir(t *testing.T, filepath string) { require.NoError(t, err) } +func (r *Run) symlink(t *testing.T, oldname, newname string) { + oldname = r.path(oldname) + newname = r.path(newname) + err := r.os.Symlink(oldname, newname) + // The native code path with Links disabled would check the created file is really a symlink + // In this case ensure the .rclonelink file was created by stating it. + if err != nil && !r.vfsOpt.Links { + _, eerr := r.os.Stat(newname) + + if eerr == nil { + err = nil + } + } + require.NoError(t, err) +} + +func (r *Run) relativeSymlink(t *testing.T, oldname, newname string) { + newname = r.path(newname) + err := r.os.Symlink(oldname, newname) + // The native code path with Links disabled would check the created file is really a symlink + // In this case ensure the .rclonelink file was created by stating it. + if err != nil && !r.vfsOpt.Links { + _, eerr := r.os.Stat(newname) + + if eerr == nil { + err = nil + } + } + require.NoError(t, err) +} + +func (r *Run) checkMode(t *testing.T, name string, lexpected os.FileMode, expected os.FileMode) { + if r.useVFS { + info, err := run.os.Stat(run.path(name)) + require.NoError(t, err) + assert.Equal(t, lexpected, info.Mode()) + assert.Equal(t, expected, info.Mode()) + assert.Equal(t, name, info.Name()) + } else { + info, err := os.Lstat(run.path(name)) + require.NoError(t, err) + assert.Equal(t, lexpected, info.Mode()) + assert.Equal(t, name, info.Name()) + + info, err = run.os.Stat(run.path(name)) + require.NoError(t, err) + assert.Equal(t, expected, info.Mode()) + assert.Equal(t, name, info.Name()) + } +} + +func (r *Run) readlink(t *testing.T, name string) string { + result, err := r.os.Readlink(r.path(name)) + // The native code path with Links disabled would check the file is really a symlink + // In this case read the existing .rclonelink file. + if err != nil && !r.vfsOpt.Links { + result = r.readFile(t, name) + err = nil + } + require.NoError(t, err) + return result +} + // TestMount checks that the Fs is mounted by seeing if the mountpoint // is in the mount output func TestMount(t *testing.T) { diff --git a/vfs/vfstest/os.go b/vfs/vfstest/os.go index d12f55ddb..e80401cc1 100644 --- a/vfs/vfstest/os.go +++ b/vfs/vfstest/os.go @@ -22,6 +22,8 @@ type Oser interface { Remove(name string) error Rename(oldName, newName string) error Stat(path string) (os.FileInfo, error) + Symlink(oldname, newname string) error + Readlink(name string) (s string, err error) } // realOs is an implementation of Oser backed by the "os" package @@ -122,6 +124,16 @@ func (r realOs) Stat(path string) (os.FileInfo, error) { return os.Stat(path) } +// Symlink +func (r realOs) Symlink(oldname, newname string) error { + return os.Symlink(oldname, newname) +} + +// Readlink +func (r realOs) Readlink(name string) (s string, err error) { + return os.Readlink(name) +} + // Check interfaces var _ Oser = &realOs{} var _ vfs.Handle = &realOsFile{}