Compare commits

...
Sign in to create a new pull request.

8 commits

Author SHA1 Message Date
Filipe Azevedo
19c6081de2 cmount,mount,mount2: Introduce symlink support
We enable symlink support using the --links command line switch.
When symlink support is enabled, the mount backends will translate
the name of the vfs symlinks files (truncating their rclonelink suffix).
Also, operations like rename, symlink etc does not needs the rclonelink
suffix, it is handled internally to pass it to the underlying low level
VFS.
When symlink support is disabled, Symlink and Readlink functions will
transparently manage ".rclonelink" files as regular files.

Fixes #2975
2023-01-04 21:56:15 +01:00
Filipe Azevedo
1891b6848b 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.
2023-01-04 21:56:15 +01:00
Filipe Azevedo
8323727d19 vfs: Add link permissions 2023-01-04 21:56:15 +01:00
Filipe Azevedo
0801342108 vfs: Add IsSymLink and TrimSymlink helpers 2023-01-04 21:56:14 +01:00
Filipe Azevedo
5c8bd27c7f vfs: Add VFS --links command line switch
This will be used to enable links support for the various mount engines
in a follow up commit.
2023-01-04 21:56:14 +01:00
Filipe Azevedo
46de095e9c fs: Move link suffix to fs 2023-01-04 21:56:14 +01:00
Filipe Azevedo
3834de5ab1 mount2: Fix missing . and .. entries 2023-01-04 21:56:14 +01:00
Filipe Azevedo
44c3aebfc3 Update ignored files 2023-01-04 21:56:14 +01:00
19 changed files with 989 additions and 90 deletions

1
.gitignore vendored
View file

@ -15,3 +15,4 @@ fuzz-build.zip
*.rej
Thumbs.db
__pycache__
*.kate-swp

View file

@ -32,7 +32,6 @@ import (
// Constants
const devUnset = 0xdeadbeefcafebabe // a device id meaning it is unset
const linkSuffix = ".rclonelink" // The suffix added to a translated symbolic link
const useReadDir = (runtime.GOOS == "windows" || runtime.GOOS == "plan9") // these OSes read FileInfos directly
// Register with Fs
@ -72,7 +71,7 @@ supported by all file systems) under the "user.*" prefix.
Advanced: true,
}, {
Name: "links",
Help: "Translate symlinks to/from regular files with a '" + linkSuffix + "' extension.",
Help: "Translate symlinks to/from regular files with a '" + fs.LinkSuffix + "' extension.",
Default: false,
NoPrefix: true,
ShortOpt: "l",
@ -375,8 +374,8 @@ func (f *Fs) caseInsensitive() bool {
//
// for regular files, localPath is returned unchanged
func translateLink(remote, localPath string) (newLocalPath string, isTranslatedLink bool) {
isTranslatedLink = strings.HasSuffix(remote, linkSuffix)
newLocalPath = strings.TrimSuffix(localPath, linkSuffix)
isTranslatedLink = strings.HasSuffix(remote, fs.LinkSuffix)
newLocalPath = strings.TrimSuffix(localPath, fs.LinkSuffix)
return newLocalPath, isTranslatedLink
}
@ -551,7 +550,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
} else {
// Check whether this link should be translated
if f.opt.TranslateSymlinks && fi.Mode()&os.ModeSymlink != 0 {
newRemote += linkSuffix
newRemote += fs.LinkSuffix
}
fso, err := f.newObjectWithInfo(newRemote, fi)
if err != nil {

View file

@ -91,7 +91,7 @@ func TestSymlink(t *testing.T) {
require.NoError(t, lChtimes(symlinkPath, modTime2, modTime2))
// Object viewed as symlink
file2 := fstest.NewItem("symlink.txt"+linkSuffix, "file.txt", modTime2)
file2 := fstest.NewItem("symlink.txt"+fs.LinkSuffix, "file.txt", modTime2)
// Object viewed as destination
file2d := fstest.NewItem("symlink.txt", "hello", modTime1)
@ -120,7 +120,7 @@ func TestSymlink(t *testing.T) {
// Create a symlink
modTime3 := fstest.Time("2002-03-03T04:05:10.123123123Z")
file3 := r.WriteObjectTo(ctx, r.Flocal, "symlink2.txt"+linkSuffix, "file.txt", modTime3, false)
file3 := r.WriteObjectTo(ctx, r.Flocal, "symlink2.txt"+fs.LinkSuffix, "file.txt", modTime3, false)
fstest.CheckListingWithPrecision(t, r.Flocal, []fstest.Item{file1, file2, file3}, nil, fs.ModTimeNotSupported)
if haveLChtimes {
r.CheckLocalItems(t, file1, file2, file3)
@ -136,9 +136,9 @@ func TestSymlink(t *testing.T) {
assert.Equal(t, "file.txt", linkText)
// Check that NewObject gets the correct object
o, err := r.Flocal.NewObject(ctx, "symlink2.txt"+linkSuffix)
o, err := r.Flocal.NewObject(ctx, "symlink2.txt"+fs.LinkSuffix)
require.NoError(t, err)
assert.Equal(t, "symlink2.txt"+linkSuffix, o.Remote())
assert.Equal(t, "symlink2.txt"+fs.LinkSuffix, o.Remote())
assert.Equal(t, int64(8), o.Size())
// Check that NewObject doesn't see the non suffixed version

View file

@ -8,6 +8,7 @@ import (
"io"
"os"
"path"
"strings"
"sync"
"sync/atomic"
"time"
@ -95,8 +96,11 @@ func (fsys *FS) closeHandle(fh uint64) (errc int) {
}
// lookup a Node given a path
func (fsys *FS) lookupNode(path string) (node vfs.Node, errc int) {
func (fsys *FS) lookupNode(path string) (vfs.Node, int) {
node, err := fsys.VFS.Stat(path)
if err == vfs.ENOENT && fsys.VFS.Opt.Links {
node, err = fsys.VFS.Stat(path + fs.LinkSuffix)
}
return node, translateError(err)
}
@ -117,6 +121,13 @@ func (fsys *FS) lookupDir(path string) (dir *vfs.Dir, errc int) {
func (fsys *FS) lookupParentDir(filePath string) (leaf string, dir *vfs.Dir, errc int) {
parentDir, leaf := path.Split(filePath)
dir, errc = fsys.lookupDir(parentDir)
// Try to get real leaf for symlinks
if fsys.VFS.Opt.Links {
node, e := fsys.lookupNode(filePath)
if e == 0 {
leaf = node.Name()
}
}
return leaf, dir, errc
}
@ -153,15 +164,9 @@ func (fsys *FS) stat(node vfs.Node, stat *fuse.Stat_t) (errc int) {
Size := uint64(node.Size())
Blocks := (Size + 511) / 512
modTime := node.ModTime()
Mode := node.Mode().Perm()
if node.IsDir() {
Mode |= fuse.S_IFDIR
} else {
Mode |= fuse.S_IFREG
}
//stat.Dev = 1
stat.Ino = node.Inode() // FIXME do we need to set the inode number?
stat.Mode = uint32(Mode)
stat.Mode = getMode(node)
stat.Nlink = 1
stat.Uid = fsys.VFS.Opt.UID
stat.Gid = fsys.VFS.Opt.GID
@ -252,7 +257,7 @@ func (fsys *FS) Readdir(dirPath string,
fill(".", nil, 0)
fill("..", nil, 0)
for _, node := range nodes {
name := node.Name()
name, _ := fsys.VFS.TrimSymlink(node.Name())
if len(name) > mountlib.MaxLeafSize {
fs.Errorf(dirPath, "Name too long (%d bytes) for FUSE, skipping: %s", len(name), name)
continue
@ -330,13 +335,15 @@ func (fsys *FS) CreateEx(filePath string, mode uint32, fi *fuse.FileInfo_t) (err
if errc != 0 {
return errc
}
file, err := parentDir.Create(leaf, fi.Flags)
// translate the fuse flags to os flags
osFlags := translateOpenFlags(fi.Flags) | os.O_CREATE
// translate the fuse mode to os mode
//osMode := getFileMode(mode)
file, err := parentDir.Create(leaf, osFlags)
if err != nil {
return translateError(err)
}
// translate the fuse flags to os flags
flags := translateOpenFlags(fi.Flags) | os.O_CREATE
handle, err := file.Open(flags)
handle, err := file.Open(osFlags)
if err != nil {
return translateError(err)
}
@ -456,6 +463,18 @@ func (fsys *FS) Rmdir(dirPath string) (errc int) {
// Rename renames a file.
func (fsys *FS) Rename(oldPath string, newPath string) (errc int) {
defer log.Trace(oldPath, "newPath=%q", newPath)("errc=%d", &errc)
if fsys.VFS.Opt.Links {
node, e := fsys.lookupNode(oldPath)
if e == 0 {
if strings.HasSuffix(node.Name(), fs.LinkSuffix) {
oldPath += fs.LinkSuffix
newPath += fs.LinkSuffix
}
}
}
return translateError(fsys.VFS.Rename(oldPath, newPath))
}
@ -505,14 +524,58 @@ func (fsys *FS) Link(oldpath string, newpath string) (errc int) {
// Symlink creates a symbolic link.
func (fsys *FS) Symlink(target string, newpath string) (errc int) {
defer log.Trace(target, "newpath=%q", newpath)("errc=%d", &errc)
return -fuse.ENOSYS
defer log.Trace(fsys, "Requested to symlink newpath=%q, target=%q", newpath, target)("errc=%d", &errc)
if fsys.VFS.Opt.Links {
// The user must NOT provide .rclonelink suffix
if strings.HasSuffix(newpath, fs.LinkSuffix) {
fs.Errorf(nil, "Invalid name suffix provided: %v", newpath)
return translateError(vfs.EINVAL)
}
newpath += fs.LinkSuffix
} else {
// The user must provide .rclonelink suffix
if !strings.HasSuffix(newpath, fs.LinkSuffix) {
fs.Errorf(nil, "Invalid name suffix provided: %v", newpath)
return translateError(vfs.EINVAL)
}
}
// Add target suffix when linking to a link
if !strings.HasSuffix(target, fs.LinkSuffix) {
vnode, err := fsys.lookupNode(target)
if err == 0 && strings.HasSuffix(vnode.Name(), fs.LinkSuffix) {
target += fs.LinkSuffix
}
}
return translateError(fsys.VFS.Symlink(target, newpath))
}
// Readlink reads the target of a symbolic link.
func (fsys *FS) Readlink(path string) (errc int, linkPath string) {
defer log.Trace(path, "")("linkPath=%q, errc=%d", &linkPath, &errc)
return -fuse.ENOSYS, ""
defer log.Trace(fsys, "Requested to read link")("errc=%v, linkPath=%q", &errc, linkPath)
if fsys.VFS.Opt.Links {
// The user must NOT provide .rclonelink suffix
if strings.HasSuffix(path, fs.LinkSuffix) {
fs.Errorf(nil, "Invalid name suffix provided: %v", path)
return translateError(vfs.EINVAL), ""
}
path += fs.LinkSuffix
} else {
// The user must provide .rclonelink suffix
if !strings.HasSuffix(path, fs.LinkSuffix) {
fs.Errorf(nil, "Invalid name suffix provided: %v", path)
return translateError(vfs.EINVAL), ""
}
}
linkPath, err := fsys.VFS.Readlink(path)
linkPath, _ = fsys.VFS.TrimSymlink(linkPath)
return translateError(err), linkPath
}
// Chmod changes the permission bits of a file.
@ -627,6 +690,41 @@ func translateOpenFlags(inFlags int) (outFlags int) {
return outFlags
}
// get the Mode from a vfs Node
func getMode(node os.FileInfo) uint32 {
vfsMode := node.Mode()
Mode := vfsMode.Perm()
if vfsMode&os.ModeDir != 0 {
Mode |= fuse.S_IFDIR
} else if vfsMode&os.ModeSymlink != 0 {
Mode |= fuse.S_IFLNK
} else if vfsMode&os.ModeNamedPipe != 0 {
Mode |= fuse.S_IFIFO
} else {
Mode |= fuse.S_IFREG
}
return uint32(Mode)
}
// convert fuse mode to os.FileMode
// func getFileMode(mode uint32) os.FileMode {
// osMode := os.FileMode(0)
// if mode&fuse.S_IFDIR != 0 {
// mode ^= fuse.S_IFDIR
// osMode |= os.ModeDir
// } else if mode&fuse.S_IFREG != 0 {
// mode ^= fuse.S_IFREG
// } else if mode&fuse.S_IFLNK != 0 {
// mode ^= fuse.S_IFLNK
// osMode |= os.ModeSymlink
// } else if mode&fuse.S_IFIFO != 0 {
// mode ^= fuse.S_IFIFO
// osMode |= os.ModeNamedPipe
// }
// osMode |= os.FileMode(mode)
// return osMode
// }
// Make sure interfaces are satisfied
var (
_ fuse.FileSystemInterface = (*FS)(nil)

View file

@ -8,6 +8,8 @@ import (
"fmt"
"io"
"os"
"path"
"strings"
"syscall"
"time"
@ -28,13 +30,21 @@ type Dir struct {
// Check interface satisfied
var _ fusefs.Node = (*Dir)(nil)
func fallbackStat(dir *vfs.Dir, leaf string) (node vfs.Node, err error) {
node, err = dir.Stat(leaf)
if err == vfs.ENOENT && dir.VFS().Opt.Links {
node, err = dir.Stat(leaf + fs.LinkSuffix)
}
return node, err
}
// Attr updates the attributes of a directory
func (d *Dir) Attr(ctx context.Context, a *fuse.Attr) (err error) {
defer log.Trace(d, "")("attr=%+v, err=%v", a, &err)
a.Valid = d.fsys.opt.AttrTimeout
a.Gid = d.VFS().Opt.GID
a.Uid = d.VFS().Opt.UID
a.Mode = os.ModeDir | d.VFS().Opt.DirPerms
a.Mode = d.Mode()
modTime := d.ModTime()
a.Atime = modTime
a.Mtime = modTime
@ -74,7 +84,7 @@ var _ fusefs.NodeRequestLookuper = (*Dir)(nil)
// Lookup need not to handle the names "." and "..".
func (d *Dir) Lookup(ctx context.Context, req *fuse.LookupRequest, resp *fuse.LookupResponse) (node fusefs.Node, err error) {
defer log.Trace(d, "name=%q", req.Name)("node=%+v, err=%v", &node, &err)
mnode, err := d.Dir.Stat(req.Name)
mnode, err := fallbackStat(d.Dir, req.Name)
if err != nil {
return nil, translateError(err)
}
@ -117,7 +127,7 @@ func (d *Dir) ReadDirAll(ctx context.Context) (dirents []fuse.Dirent, err error)
Name: "..",
})
for _, node := range items {
name := node.Name()
name, isLink := d.VFS().TrimSymlink(node.Name())
if len(name) > mountlib.MaxLeafSize {
fs.Errorf(d, "Name too long (%d bytes) for FUSE, skipping: %s", len(name), name)
continue
@ -127,7 +137,9 @@ func (d *Dir) ReadDirAll(ctx context.Context) (dirents []fuse.Dirent, err error)
Type: fuse.DT_File,
Name: name,
}
if node.IsDir() {
if isLink {
dirent.Type = fuse.DT_Link
} else if node.IsDir() {
dirent.Type = fuse.DT_Dir
}
dirents = append(dirents, dirent)
@ -141,11 +153,13 @@ var _ fusefs.NodeCreater = (*Dir)(nil)
// Create makes a new file
func (d *Dir) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (node fusefs.Node, handle fusefs.Handle, err error) {
defer log.Trace(d, "name=%q", req.Name)("node=%v, handle=%v, err=%v", &node, &handle, &err)
file, err := d.Dir.Create(req.Name, int(req.Flags))
// translate the fuse flags to os flags
osFlags := int(req.Flags) | os.O_CREATE
file, err := d.Dir.Create(req.Name, osFlags)
if err != nil {
return nil, nil, translateError(err)
}
fh, err := file.Open(int(req.Flags) | os.O_CREATE)
fh, err := file.Open(osFlags)
if err != nil {
return nil, nil, translateError(err)
}
@ -175,7 +189,18 @@ var _ fusefs.NodeRemover = (*Dir)(nil)
// may correspond to a file (unlink) or to a directory (rmdir).
func (d *Dir) Remove(ctx context.Context, req *fuse.RemoveRequest) (err error) {
defer log.Trace(d, "name=%q", req.Name)("err=%v", &err)
err = d.Dir.RemoveName(req.Name)
name := req.Name
if d.VFS().Opt.Links {
node, err := fallbackStat(d.Dir, name)
if err == nil {
name = node.Name()
}
}
err = d.Dir.RemoveName(name)
if err != nil {
return translateError(err)
}
@ -202,7 +227,22 @@ func (d *Dir) Rename(ctx context.Context, req *fuse.RenameRequest, newDir fusefs
return fmt.Errorf("unknown Dir type %T", newDir)
}
err = d.Dir.Rename(req.OldName, req.NewName, destDir.Dir)
oldName := req.OldName
newName := req.NewName
if d.VFS().Opt.Links {
node, err := fallbackStat(d.Dir, oldName)
if err == nil {
oldName = node.Name()
if strings.HasSuffix(oldName, fs.LinkSuffix) {
newName += fs.LinkSuffix
}
}
}
err = d.Dir.Rename(oldName, newName, destDir.Dir)
if err != nil {
return translateError(err)
}
@ -240,6 +280,53 @@ func (d *Dir) Link(ctx context.Context, req *fuse.LinkRequest, old fusefs.Node)
return nil, syscall.ENOSYS
}
var _ fusefs.NodeSymlinker = (*Dir)(nil)
// Symlink create a symbolic link.
func (d *Dir) Symlink(ctx context.Context, req *fuse.SymlinkRequest) (node fusefs.Node, err error) {
defer log.Trace(d, "Requested to symlink newname=%v, target=%v", req.NewName, req.Target)("node=%v, err=%v", &node, &err)
newName := path.Join(d.Path(), req.NewName)
target := req.Target
if d.VFS().Opt.Links {
// The user must NOT provide .rclonelink suffix
if strings.HasSuffix(newName, fs.LinkSuffix) {
fs.Errorf(nil, "Invalid name suffix provided: %v", newName)
return nil, vfs.EINVAL
}
newName += fs.LinkSuffix
} else {
// The user must provide .rclonelink suffix
if !strings.HasSuffix(newName, fs.LinkSuffix) {
fs.Errorf(nil, "Invalid name suffix provided: %v", newName)
return nil, vfs.EINVAL
}
}
// Add target suffix when linking to a link
if !strings.HasSuffix(target, fs.LinkSuffix) {
vnode, err := fallbackStat(d.Dir, target)
if err == nil && strings.HasSuffix(vnode.Name(), fs.LinkSuffix) {
target += fs.LinkSuffix
}
}
err = d.VFS().Symlink(target, newName)
if err != nil {
return nil, err
}
n, err := d.Stat(path.Base(newName))
if err != nil {
return nil, err
}
node = &File{n.(*vfs.File), d.fsys}
return node, nil
}
// Check interface satisfied
var _ fusefs.NodeMknoder = (*Dir)(nil)

View file

@ -5,11 +5,14 @@ package mount
import (
"context"
"os"
"strings"
"syscall"
"time"
"bazil.org/fuse"
fusefs "bazil.org/fuse/fs"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/log"
"github.com/rclone/rclone/vfs"
)
@ -32,7 +35,7 @@ func (f *File) Attr(ctx context.Context, a *fuse.Attr) (err error) {
Blocks := (Size + 511) / 512
a.Gid = f.VFS().Opt.GID
a.Uid = f.VFS().Opt.UID
a.Mode = f.VFS().Opt.FilePerms
a.Mode = f.File.Mode() &^ os.ModeAppend
a.Size = Size
a.Atime = modTime
a.Mtime = modTime
@ -126,3 +129,32 @@ func (f *File) Removexattr(ctx context.Context, req *fuse.RemovexattrRequest) er
}
var _ fusefs.NodeRemovexattrer = (*File)(nil)
var _ fusefs.NodeReadlinker = (*File)(nil)
// Readlink read symbolic link target.
func (f *File) Readlink(ctx context.Context, req *fuse.ReadlinkRequest) (ret string, err error) {
defer log.Trace(f, "Requested to read link")("ret=%v, err=%v", &ret, &err)
path := f.Path()
if f.VFS().Opt.Links {
// The user must NOT provide .rclonelink suffix
// if strings.HasSuffix(path, fs.LinkSuffix) {
// fs.Errorf(nil, "Invalid name suffix provided: %v", path)
// return "", vfs.EINVAL
// }
// path += fs.LinkSuffix
} else {
// The user must provide .rclonelink suffix
if !strings.HasSuffix(path, fs.LinkSuffix) {
fs.Errorf(nil, "Invalid name suffix provided: %v", path)
return "", vfs.EINVAL
}
}
ret, err = f.VFS().Readlink(path)
ret, _ = f.VFS().TrimSymlink(ret)
return ret, err
}

View file

@ -51,15 +51,39 @@ func (f *FS) SetDebug(debug bool) {
// get the Mode from a vfs Node
func getMode(node os.FileInfo) uint32 {
Mode := node.Mode().Perm()
if node.IsDir() {
vfsMode := node.Mode()
Mode := vfsMode.Perm()
if vfsMode&os.ModeDir != 0 {
Mode |= fuse.S_IFDIR
} else if vfsMode&os.ModeSymlink != 0 {
Mode |= fuse.S_IFLNK
} else if vfsMode&os.ModeNamedPipe != 0 {
Mode |= fuse.S_IFIFO
} else {
Mode |= fuse.S_IFREG
}
return uint32(Mode)
}
// convert fuse mode to os.FileMode
// func getFileMode(mode uint32) os.FileMode {
// osMode := os.FileMode(0)
// if mode&fuse.S_IFDIR != 0 {
// mode ^= fuse.S_IFDIR
// osMode |= os.ModeDir
// } else if mode&fuse.S_IFREG != 0 {
// mode ^= fuse.S_IFREG
// } else if mode&fuse.S_IFLNK != 0 {
// mode ^= fuse.S_IFLNK
// osMode |= os.ModeSymlink
// } else if mode&fuse.S_IFIFO != 0 {
// mode ^= fuse.S_IFIFO
// osMode |= os.ModeNamedPipe
// }
// osMode |= os.FileMode(mode)
// return osMode
// }
// fill in attr from node
func setAttr(node vfs.Node, attr *fuse.Attr) {
Size := uint64(node.Size())

View file

@ -7,6 +7,7 @@ import (
"context"
"os"
"path"
"strings"
"syscall"
fusefs "github.com/hanwen/go-fuse/v2/fs"
@ -56,6 +57,9 @@ func (n *Node) lookupVfsNodeInDir(leaf string) (vfsNode vfs.Node, errno syscall.
return nil, syscall.ENOTDIR
}
vfsNode, err := dir.Stat(leaf)
if err == vfs.ENOENT && dir.VFS().Opt.Links {
vfsNode, err = dir.Stat(leaf + fs.LinkSuffix)
}
return vfsNode, translateError(err)
}
@ -219,6 +223,7 @@ func (n *Node) Opendir(ctx context.Context) syscall.Errno {
var _ = (fusefs.NodeOpendirer)((*Node)(nil))
type dirStream struct {
fsys *FS
nodes []os.FileInfo
i int
}
@ -226,7 +231,7 @@ type dirStream struct {
// HasNext indicates if there are further entries. HasNext
// might be called on already closed streams.
func (ds *dirStream) HasNext() bool {
return ds.i < len(ds.nodes)
return ds.i < len(ds.nodes)+2
}
// Next retrieves the next entry. It is only called if HasNext
@ -234,14 +239,30 @@ func (ds *dirStream) HasNext() bool {
// indicate I/O errors
func (ds *dirStream) Next() (de fuse.DirEntry, errno syscall.Errno) {
// defer log.Trace(nil, "")("de=%+v, errno=%v", &de, &errno)
fi := ds.nodes[ds.i]
if ds.i == 0 {
ds.i++
return fuse.DirEntry{
Mode: fuse.S_IFDIR,
Name: ".",
Ino: 0, // FIXME
}, 0
} else if ds.i == 1 {
ds.i++
return fuse.DirEntry{
Mode: fuse.S_IFDIR,
Name: "..",
Ino: 0, // FIXME
}, 0
}
fi := ds.nodes[ds.i-2]
name, _ := ds.fsys.VFS.TrimSymlink(path.Base(fi.Name()))
de = fuse.DirEntry{
// Mode is the file's mode. Only the high bits (e.g. S_IFDIR)
// are considered.
Mode: getMode(fi),
// Name is the basename of the file in the directory.
Name: path.Base(fi.Name()),
Name: name,
// Ino is the inode number.
Ino: 0, // FIXME
@ -289,6 +310,7 @@ func (n *Node) Readdir(ctx context.Context) (ds fusefs.DirStream, errno syscall.
}
return &dirStream{
nodes: items,
fsys: n.fsys,
}, 0
}
@ -326,6 +348,8 @@ func (n *Node) Create(ctx context.Context, name string, flags uint32, mode uint3
}
// translate the fuse flags to os flags
osFlags := int(flags) | os.O_CREATE
// translate the fuse mode to os mode
//osMode := getFileMode(mode)
file, err := dir.Create(name, osFlags)
if err != nil {
return nil, nil, 0, translateError(err)
@ -401,7 +425,101 @@ func (n *Node) Rename(ctx context.Context, oldName string, newParent fusefs.Inod
if !ok {
return syscall.ENOTDIR
}
if oldDir.VFS().Opt.Links {
node, err := n.lookupVfsNodeInDir(oldName)
if err == 0 {
oldName = node.Name()
if strings.HasSuffix(oldName, fs.LinkSuffix) {
newName += fs.LinkSuffix
}
}
}
return translateError(oldDir.Rename(oldName, newName, newDir))
}
var _ = (fusefs.NodeRenamer)((*Node)(nil))
var _ fusefs.NodeReadlinker = (*Node)(nil)
// Readlink read symbolic link target.
func (n *Node) Readlink(ctx context.Context) (ret []byte, err syscall.Errno) {
defer log.Trace(n, "Requested to read link")("ret=%v, err=%v", &ret, &err)
path := n.node.Path()
if n.node.VFS().Opt.Links {
// The user must NOT provide .rclonelink suffix
// if strings.HasSuffix(path, fs.LinkSuffix) {
// fs.Errorf(nil, "Invalid name suffix provided: %v", path)
// return nil, translateError(vfs.EINVAL)
// }
// path += fs.LinkSuffix
} else {
// The user must provide .rclonelink suffix
if !strings.HasSuffix(path, fs.LinkSuffix) {
fs.Errorf(nil, "Invalid name suffix provided: %v", path)
return nil, translateError(vfs.EINVAL)
}
}
s, serr := n.node.VFS().Readlink(path)
if serr != nil {
return nil, translateError(serr)
}
s, _ = n.node.VFS().TrimSymlink(s)
return []byte(s), 0
}
var _ fusefs.NodeSymlinker = (*Node)(nil)
// Symlink create symbolic link.
func (n *Node) Symlink(ctx context.Context, target, name string, out *fuse.EntryOut) (node *fusefs.Inode, err syscall.Errno) {
defer log.Trace(n, "Requested to symlink name=%v, target=%v", name, target)("node=%v, err=%v", &node, &err)
name = path.Join(n.node.Path(), name)
if n.node.VFS().Opt.Links {
// The user must NOT provide .rclonelink suffix
if strings.HasSuffix(name, fs.LinkSuffix) {
fs.Errorf(nil, "Invalid name suffix provided: %v", name)
return nil, translateError(vfs.EINVAL)
}
name += fs.LinkSuffix
} else {
// The user must provide .rclonelink suffix
if !strings.HasSuffix(name, fs.LinkSuffix) {
fs.Errorf(nil, "Invalid name suffix provided: %v", name)
return nil, translateError(vfs.EINVAL)
}
}
// Add target suffix when linking to a link
if !strings.HasSuffix(target, fs.LinkSuffix) {
vnode, err := n.lookupVfsNodeInDir(target)
if err == 0 && strings.HasSuffix(vnode.Name(), fs.LinkSuffix) {
target += fs.LinkSuffix
}
}
serr := n.node.VFS().Symlink(target, name)
if serr != nil {
return nil, translateError(serr)
}
// Find the created node
vfsNode, err := n.lookupVfsNodeInDir(path.Base(name))
if err != 0 {
return nil, err
}
n.fsys.setEntryOut(vfsNode, out)
newNode := newNode(n.fsys, vfsNode)
fs.Debugf(nil, "attr=%#v", out.Attr)
newInode := n.NewInode(ctx, newNode, fusefs.StableAttr{Mode: out.Attr.Mode})
return newInode, 0
}

View file

@ -16,6 +16,8 @@ const (
ModTimeNotSupported = 100 * 365 * 24 * time.Hour
// MaxLevel is a sentinel representing an infinite depth for listings
MaxLevel = math.MaxInt32
// The suffix added to a translated symbolic link
LinkSuffix = ".rclonelink"
)
// Globals

View file

@ -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 {

View file

@ -89,13 +89,25 @@ func (f *File) IsDir() bool {
return false
}
// IsSymlink returns true for symlinks when --links is enabled
func (f *File) IsSymlink() bool {
f.mu.RLock()
defer f.mu.RUnlock()
return f.d.vfs.IsSymlink(f.leaf)
}
// Mode bits of the file or directory - satisfies Node interface
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
}

View file

@ -258,6 +258,7 @@ read of the modification time takes a transaction.
--no-modtime Don't read/write the modification time (can speed things up).
--no-seek Don't allow seeking in files.
--read-only Only allow read-only access.
--links Translate symlinks to/from regular files with a '.rclonelink' extension.
Sometimes rclone is delivered reads or writes out of order. Rather
than seeking rclone will wait a short time for the in sequence read or

View file

@ -227,6 +227,11 @@ func New(f fs.Fs, opt *vfscommon.Options) *VFS {
fs.Logf(f, "--vfs-cache-mode writes or full is recommended for this remote as it can't stream")
}
// Warn if we handle symlinks
if vfs.Opt.Links {
fs.Logf(f, "Symlinks support enabled")
}
vfs.SetCacheMode(vfs.Opt.CacheMode)
// Pin the Fs into the cache so that when we use cache.NewFs
@ -478,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
@ -507,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
@ -515,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
@ -709,3 +723,69 @@ func (vfs *VFS) AddVirtual(remote string, size int64, isDir bool) error {
dir.AddVirtual(leaf, size, false)
return nil
}
// IsSymlink returns true if remote ends with fs.LinkSuffix when --links is enabled
func (vfs *VFS) IsSymlink(remote string) bool {
return vfs.Opt.Links && strings.HasSuffix(remote, fs.LinkSuffix)
}
// TrimSymlink returns true if remote ends with fs.LinkSuffix when --links is enabled
// Also, if it's a link, it's trimmed from its fs.LinkSuffix
func (vfs *VFS) TrimSymlink(remote string) (string, bool) {
if vfs.IsSymlink(remote) {
remote := strings.TrimSuffix(remote, fs.LinkSuffix)
return remote, true
}
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

@ -140,6 +140,7 @@ func TestVFSNew(t *testing.T) {
// Check making a VFS with nil options
var defaultOpt = vfscommon.DefaultOpt
defaultOpt.DirPerms |= os.ModeDir
defaultOpt.LinkPerms |= os.ModeSymlink
assert.Equal(t, vfs.Opt, defaultOpt)
assert.Equal(t, vfs.f, r.Fremote)

View file

@ -13,6 +13,7 @@ type Options struct {
NoSeek bool // don't allow seeking if set
NoChecksum bool // don't check checksums if set
ReadOnly bool // if set VFS is read only
Links bool // if set VFS handle symlinks
NoModTime bool // don't read mod times for files
DirCacheTime time.Duration // how long to consider directory listing cache valid
PollInterval time.Duration
@ -21,6 +22,7 @@ type Options struct {
GID uint32
DirPerms os.FileMode
FilePerms os.FileMode
LinkPerms os.FileMode
ChunkSize fs.SizeSuffix // if > 0 read files in chunks
ChunkSizeLimit fs.SizeSuffix // if > ChunkSize double the chunk size after each chunk until reached
CacheMode CacheMode
@ -45,11 +47,13 @@ var DefaultOpt = Options{
DirCacheTime: 5 * 60 * time.Second,
PollInterval: time.Minute,
ReadOnly: false,
Links: false,
Umask: 0,
UID: ^uint32(0), // these values instruct WinFSP-FUSE to use the current user
GID: ^uint32(0), // overridden for non windows in mount_unix.go
DirPerms: os.FileMode(0777),
FilePerms: os.FileMode(0666),
LinkPerms: os.ModePerm,
CacheMode: CacheModeOff,
CacheMaxAge: 3600 * time.Second,
CachePollInterval: 60 * time.Second,
@ -70,8 +74,12 @@ func (opt *Options) Init() {
// Mask the permissions with the umask
opt.DirPerms &= ^os.FileMode(opt.Umask)
opt.FilePerms &= ^os.FileMode(opt.Umask)
opt.LinkPerms &= ^os.FileMode(opt.Umask)
// Make sure directories are returned as directories
opt.DirPerms |= os.ModeDir
// Make sure links are returned as links
opt.LinkPerms |= os.ModeSymlink
}

View file

@ -2,6 +2,7 @@
package vfsflags
import (
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/vfs/vfscommon"
@ -24,6 +25,7 @@ func AddFlags(flagSet *pflag.FlagSet) {
flags.DurationVarP(flagSet, &Opt.DirCacheTime, "dir-cache-time", "", Opt.DirCacheTime, "Time to cache directory entries for")
flags.DurationVarP(flagSet, &Opt.PollInterval, "poll-interval", "", Opt.PollInterval, "Time to wait between polling for changes, must be smaller than dir-cache-time and only on supported remotes (set 0 to disable)")
flags.BoolVarP(flagSet, &Opt.ReadOnly, "read-only", "", Opt.ReadOnly, "Only allow read-only access")
flags.BoolVarP(flagSet, &Opt.Links, "links", "l", Opt.Links, "Translate symlinks to/from regular files with a '"+fs.LinkSuffix+"' extension")
flags.FVarP(flagSet, &Opt.CacheMode, "vfs-cache-mode", "", "Cache mode off|minimal|writes|full")
flags.DurationVarP(flagSet, &Opt.CachePollInterval, "vfs-cache-poll-interval", "", Opt.CachePollInterval, "Interval to poll the cache for stale objects")
flags.DurationVarP(flagSet, &Opt.CacheMaxAge, "vfs-cache-max-age", "", Opt.CacheMaxAge, "Max age of objects in the cache")

View file

@ -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,273 @@ 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())
}
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)
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) {

View file

@ -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{}