forked from TrueCloudLab/rclone
Compare commits
8 commits
master
...
pasnox-sym
Author | SHA1 | Date | |
---|---|---|---|
|
19c6081de2 | ||
|
1891b6848b | ||
|
8323727d19 | ||
|
0801342108 | ||
|
5c8bd27c7f | ||
|
46de095e9c | ||
|
3834de5ab1 | ||
|
44c3aebfc3 |
19 changed files with 989 additions and 90 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -15,3 +15,4 @@ fuzz-build.zip
|
|||
*.rej
|
||||
Thumbs.db
|
||||
__pycache__
|
||||
*.kate-swp
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
132
cmd/cmount/fs.go
132
cmd/cmount/fs.go
|
@ -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)
|
||||
|
|
103
cmd/mount/dir.go
103
cmd/mount/dir.go
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
2
fs/fs.go
2
fs/fs.go
|
@ -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
|
||||
|
|
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 {
|
||||
|
|
18
vfs/file.go
18
vfs/file.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
84
vfs/vfs.go
84
vfs/vfs.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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…
Add table
Reference in a new issue