vfs: Introduce symlink support

We enable symlink support using the --links command line switch.
On the VFS layer, symlinks always ends with the rclonelink suffix.
This is because it is what we send/get to/from the remote layer.
That mean than any regular operation like rename, remove etc on
symlinks files always need to have their rclonelink suffix.
That way, we don't mess the internal map of items and avoid lots of
troubles.
When symlink support is disabled, Symlink and Readlink functions will
transparently manage ".rclonelink" files as regular files.
This commit is contained in:
Filipe Azevedo 2022-12-14 23:02:10 +01:00 committed by albertony
parent 8323727d19
commit 1891b6848b
6 changed files with 552 additions and 50 deletions

View File

@ -862,6 +862,30 @@ func (d *Dir) Create(name string, flags int) (*File, error) {
if d.vfs.Opt.ReadOnly { if d.vfs.Opt.ReadOnly {
return nil, EROFS 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 // This gets added to the directory when the file is opened for write
return newFile(d, d.Path(), nil, name), nil 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) fs.Errorf(d, "Dir.Mkdir failed to read directory: %v", err)
return nil, 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") // fs.Debugf(path, "Dir.Mkdir")
err = d.f.Mkdir(context.TODO(), path) err = d.f.Mkdir(context.TODO(), path)
if err != nil { 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) fs.Errorf(oldPath, "Dir.Rename error: %v", err)
return 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) { switch x := oldNode.DirEntry().(type) {
case nil: case nil:
if oldFile, ok := oldNode.(*File); ok { if oldFile, ok := oldNode.(*File); ok {

View File

@ -100,10 +100,15 @@ func (f *File) IsSymlink() bool {
func (f *File) Mode() (mode os.FileMode) { func (f *File) Mode() (mode os.FileMode) {
f.mu.RLock() f.mu.RLock()
defer f.mu.RUnlock() defer f.mu.RUnlock()
if f.IsSymlink() {
mode = f.d.vfs.Opt.LinkPerms
} else {
mode = f.d.vfs.Opt.FilePerms mode = f.d.vfs.Opt.FilePerms
if f.appendMode { if f.appendMode {
mode |= os.ModeAppend mode |= os.ModeAppend
} }
}
return mode return mode
} }

View File

@ -483,6 +483,15 @@ func decodeOpenFlags(flags int) string {
func (vfs *VFS) OpenFile(name string, flags int, perm os.FileMode) (fd Handle, err error) { 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) 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 // http://pubs.opengroup.org/onlinepubs/7908799/xsh/open.html
// The result of using O_TRUNC with O_RDONLY is undefined. // The result of using O_TRUNC with O_RDONLY is undefined.
// Linux seems to truncate the file, but we prefer to return EINVAL // 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 // the returned file can be used for reading; the associated file
// descriptor has mode O_RDONLY. // descriptor has mode O_RDONLY.
func (vfs *VFS) Open(name string) (Handle, error) { 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 // 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 // File can be used for I/O; the associated file descriptor has mode
// O_RDWR. // O_RDWR.
func (vfs *VFS) Create(name string) (Handle, error) { 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 // Rename oldName to newName
@ -730,3 +739,53 @@ func (vfs *VFS) TrimSymlink(remote string) (string, bool) {
return remote, false 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
}

View File

@ -6,6 +6,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/vfs" "github.com/rclone/rclone/vfs"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -72,3 +73,277 @@ func TestFileModTimeWithOpenWriters(t *testing.T) {
run.rm(t, "cp-archive-test") 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")
}
}

View File

@ -48,6 +48,10 @@ func RunTests(t *testing.T, useVFS bool, mountFn mountlib.MountFn) {
startMount(mountFn, useVFS, *runMount) startMount(mountFn, useVFS, *runMount)
return return
} }
links := []bool{
false,
true,
}
tests := []struct { tests := []struct {
cacheMode vfscommon.CacheMode cacheMode vfscommon.CacheMode
writeBack time.Duration writeBack time.Duration
@ -59,14 +63,17 @@ func RunTests(t *testing.T, useVFS bool, mountFn mountlib.MountFn) {
{cacheMode: vfscommon.CacheModeFull, writeBack: 100 * time.Millisecond}, {cacheMode: vfscommon.CacheModeFull, writeBack: 100 * time.Millisecond},
} }
for _, test := range tests { for _, test := range tests {
for _, link := range links {
vfsOpt := vfsflags.Opt vfsOpt := vfsflags.Opt
vfsOpt.CacheMode = test.cacheMode vfsOpt.CacheMode = test.cacheMode
vfsOpt.WriteBack = test.writeBack vfsOpt.WriteBack = test.writeBack
vfsOpt.Links = link
run = newRun(useVFS, &vfsOpt, mountFn) run = newRun(useVFS, &vfsOpt, mountFn)
what := fmt.Sprintf("CacheMode=%v", test.cacheMode) what := fmt.Sprintf("CacheMode=%v", test.cacheMode)
if test.writeBack > 0 { if test.writeBack > 0 {
what += fmt.Sprintf(",WriteBack=%v", test.writeBack) what += fmt.Sprintf(",WriteBack=%v", test.writeBack)
} }
what += fmt.Sprintf(",Links=%v", link)
log.Printf("Starting test run with %s", what) log.Printf("Starting test run with %s", what)
ok := t.Run(what, func(t *testing.T) { ok := t.Run(what, func(t *testing.T) {
t.Run("TestTouchAndDelete", TestTouchAndDelete) t.Run("TestTouchAndDelete", TestTouchAndDelete)
@ -95,6 +102,7 @@ func RunTests(t *testing.T, useVFS bool, mountFn mountlib.MountFn) {
t.Run("TestWriteFileFsync", TestWriteFileFsync) t.Run("TestWriteFileFsync", TestWriteFileFsync)
t.Run("TestWriteFileDup", TestWriteFileDup) t.Run("TestWriteFileDup", TestWriteFileDup)
t.Run("TestWriteFileAppend", TestWriteFileAppend) t.Run("TestWriteFileAppend", TestWriteFileAppend)
t.Run("TestSymlinks", TestSymlinks)
}) })
log.Printf("Finished test run with %s (ok=%v)", what, ok) log.Printf("Finished test run with %s (ok=%v)", what, ok)
run.Finalise() run.Finalise()
@ -102,6 +110,7 @@ func RunTests(t *testing.T, useVFS bool, mountFn mountlib.MountFn) {
break break
} }
} }
}
} }
// Run holds the remotes for a test run // Run holds the remotes for a test run
@ -210,10 +219,16 @@ func newDirMap(dirString string) (dm dirMap) {
} }
// Returns a dirmap with only the files in // Returns a dirmap with only the files in
func (dm dirMap) filesOnly() dirMap { func (dm dirMap) filesOnly(stripLinksSuffix bool) dirMap {
newDm := make(dirMap) newDm := make(dirMap)
for name := range dm { for name := range dm {
if !strings.HasSuffix(name, "/") { 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{}{} newDm[name] = struct{}{}
} }
} }
@ -233,9 +248,13 @@ func (r *Run) readLocal(t *testing.T, dir dirMap, filePath string) {
assert.Equal(t, r.vfsOpt.DirPerms&os.ModePerm, fi.Mode().Perm()) assert.Equal(t, r.vfsOpt.DirPerms&os.ModePerm, fi.Mode().Perm())
} else { } else {
dir[fmt.Sprintf("%s %d", name, fi.Size())] = struct{}{} dir[fmt.Sprintf("%s %d", name, fi.Size())] = struct{}{}
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()) assert.Equal(t, r.vfsOpt.FilePerms&os.ModePerm, fi.Mode().Perm())
} }
} }
}
} }
// reads the remote tree into dir // reads the remote tree into dir
@ -268,7 +287,7 @@ func (r *Run) checkDir(t *testing.T, dirString string) {
remoteDm = make(dirMap) remoteDm = make(dirMap)
r.readRemote(t, remoteDm, "") r.readRemote(t, remoteDm, "")
// Ignore directories for remote compare // 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) fuseOK = reflect.DeepEqual(dm, localDm)
if remoteOK && fuseOK { if remoteOK && fuseOK {
return 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) t.Logf("Sleeping for %v for list eventual consistency: %d/%d", sleep, i, retries)
time.Sleep(sleep) 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") 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) 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 // TestMount checks that the Fs is mounted by seeing if the mountpoint
// is in the mount output // is in the mount output
func TestMount(t *testing.T) { func TestMount(t *testing.T) {

View File

@ -22,6 +22,8 @@ type Oser interface {
Remove(name string) error Remove(name string) error
Rename(oldName, newName string) error Rename(oldName, newName string) error
Stat(path string) (os.FileInfo, 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 // 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) 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 // Check interfaces
var _ Oser = &realOs{} var _ Oser = &realOs{}
var _ vfs.Handle = &realOsFile{} var _ vfs.Handle = &realOsFile{}