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
This commit is contained in:
Filipe Azevedo 2022-12-14 22:14:20 +01:00 committed by albertony
parent 1891b6848b
commit 19c6081de2
6 changed files with 373 additions and 33 deletions

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
}
@ -250,13 +255,14 @@ func (ds *dirStream) Next() (de fuse.DirEntry, errno syscall.Errno) {
}, 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
@ -304,6 +310,7 @@ func (n *Node) Readdir(ctx context.Context) (ds fusefs.DirStream, errno syscall.
}
return &dirStream{
nodes: items,
fsys: n.fsys,
}, 0
}
@ -341,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)
@ -416,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

@ -101,10 +101,6 @@ func TestSymlinks(t *testing.T) {
// fs.Logf(nil, "LINK_FILE: %v, %v <-> %v, %v", lfl.Mode(), lfl.IsDir(), lf.Mode(), lf.IsDir())
}
if !run.useVFS {
t.Skip("Requires useVFS")
}
suffix := ""
if run.useVFS || !run.vfsOpt.Links {