mirror of
https://github.com/rclone/rclone.git
synced 2025-01-18 12:21:06 +01:00
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:
parent
8323727d19
commit
1891b6848b
69
vfs/dir.go
69
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 {
|
||||
|
@ -100,10 +100,15 @@ func (f *File) IsSymlink() bool {
|
||||
func (f *File) Mode() (mode os.FileMode) {
|
||||
f.mu.RLock()
|
||||
defer f.mu.RUnlock()
|
||||
if f.IsSymlink() {
|
||||
mode = f.d.vfs.Opt.LinkPerms
|
||||
} else {
|
||||
mode = f.d.vfs.Opt.FilePerms
|
||||
|
||||
if f.appendMode {
|
||||
mode |= os.ModeAppend
|
||||
}
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
|
63
vfs/vfs.go
63
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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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,14 +63,17 @@ func RunTests(t *testing.T, useVFS bool, mountFn mountlib.MountFn) {
|
||||
{cacheMode: vfscommon.CacheModeFull, writeBack: 100 * time.Millisecond},
|
||||
}
|
||||
for _, test := range tests {
|
||||
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)
|
||||
@ -95,6 +102,7 @@ func RunTests(t *testing.T, useVFS bool, mountFn mountlib.MountFn) {
|
||||
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()
|
||||
@ -102,6 +110,7 @@ func RunTests(t *testing.T, useVFS bool, mountFn mountlib.MountFn) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
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,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())
|
||||
} else {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reads the remote tree into dir
|
||||
@ -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) {
|
||||
|
@ -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{}
|
||||
|
Loading…
Reference in New Issue
Block a user