mirror of
https://github.com/rclone/rclone.git
synced 2024-11-22 00:13:49 +01:00
sftp: add symlink support - fixes #5011
Add new flag (--sftp-links) for backend sftp to recreate symlink from source to destination.
This commit is contained in:
parent
be448c9e13
commit
773620ca0b
@ -232,6 +232,19 @@ E.g. the second example above should be rewritten as:
|
||||
Help: "Set to skip any symlinks and any other non regular files.",
|
||||
Advanced: true,
|
||||
}, {
|
||||
Name: "links",
|
||||
Default: false,
|
||||
Help: `Copy symlinks instead of following them.
|
||||
|
||||
This permit to recreate the same symlink structure on the destination.
|
||||
|
||||
Only works between two remotes with symlink support. [Currently only supported between two SFTP remotes].
|
||||
|
||||
Symlink is validate if target file on destination and on source have same size and same hash. [Except if target is a directory, size and hash are not checked].
|
||||
`,
|
||||
Advanced: true,
|
||||
}, {
|
||||
|
||||
Name: "subsystem",
|
||||
Default: "sftp",
|
||||
Help: "Specifies the SSH2 subsystem on the remote host.",
|
||||
@ -523,6 +536,7 @@ type Options struct {
|
||||
Md5sumCommand string `config:"md5sum_command"`
|
||||
Sha1sumCommand string `config:"sha1sum_command"`
|
||||
SkipLinks bool `config:"skip_links"`
|
||||
TranslateSymlinks bool `config:"links"`
|
||||
Subsystem string `config:"subsystem"`
|
||||
ServerCommand string `config:"server_command"`
|
||||
UseFstat bool `config:"use_fstat"`
|
||||
@ -575,6 +589,8 @@ type Object struct {
|
||||
mode os.FileMode // mode bits from the file
|
||||
md5sum *string // Cached MD5 checksum
|
||||
sha1sum *string // Cached SHA1 checksum
|
||||
linkTarget string // If object isSymlink, this is the target
|
||||
linkTargetIsDir bool // If object isSymlink, this is true if the target is a directory
|
||||
}
|
||||
|
||||
// conn encapsulates an ssh client and corresponding sftp client
|
||||
@ -852,6 +868,9 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
||||
if len(opt.SSH) != 0 && ((opt.User != currentUser && opt.User != "") || opt.Host != "" || (opt.Port != "22" && opt.Port != "")) {
|
||||
fs.Logf(name, "--sftp-ssh is in use - ignoring user/host/port from config - set in the parameters to --sftp-ssh (remove them from the config to silence this warning)")
|
||||
}
|
||||
if opt.TranslateSymlinks && opt.SkipLinks {
|
||||
return nil, errors.New("can't use --sftp-links and --sftp-skip-links together")
|
||||
}
|
||||
f.tokens = pacer.NewTokenDispenser(opt.Connections)
|
||||
|
||||
if opt.User == "" {
|
||||
@ -1115,6 +1134,12 @@ func NewFsWithConnection(ctx context.Context, f *Fs, name string, root string, m
|
||||
// Disable server side copy unless --sftp-copy-is-hardlink is set
|
||||
f.features.Copy = nil
|
||||
}
|
||||
if opt.TranslateSymlinks {
|
||||
// Enable symlink translation Feature when --sftp-links is set
|
||||
// Not used yet but may be used in the future on shared backend operations.
|
||||
// Maybe to check if src and dst backend support this feature before proceeding with the operation.
|
||||
f.features.TranslateSymlink = true
|
||||
}
|
||||
// Make a connection and pool it to return errors early
|
||||
c, err := f.getSftpConnection(ctx)
|
||||
if err != nil {
|
||||
@ -1331,6 +1356,18 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
||||
remote: remote,
|
||||
}
|
||||
o.setMetadata(info)
|
||||
if o.IsSymlink() {
|
||||
// Read the link target if it is a symlink to get path to real object and its size
|
||||
linkTarget, linkTargetIsDir, sizeTarget, err := f.Readlink(ctx, o.path())
|
||||
if err != nil {
|
||||
// If we can't read the link target, log the error and continue
|
||||
fs.Errorf(remote, "Readlink failed: %v", err)
|
||||
continue
|
||||
}
|
||||
o.size = sizeTarget
|
||||
o.linkTarget = linkTarget
|
||||
o.linkTargetIsDir = linkTargetIsDir
|
||||
}
|
||||
entries = append(entries, o)
|
||||
}
|
||||
}
|
||||
@ -1340,14 +1377,36 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
||||
// Put data from <in> into a new remote sftp file object described by <src.Remote()> and <src.ModTime(ctx)>
|
||||
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
||||
err := f.mkParentDir(ctx, src.Remote())
|
||||
|
||||
// Init two variables to store symlink object and check if source object is a symlink
|
||||
// If TranslateSymlinks is set
|
||||
var srcSymlinkObject fs.Object
|
||||
isSymlink := false
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Put mkParentDir failed: %w", err)
|
||||
}
|
||||
|
||||
if f.opt.TranslateSymlinks {
|
||||
// If TranslateSymlinks is set, we need to check if source object is a symlink
|
||||
if or, ok := src.(*fs.OverrideRemote); ok {
|
||||
srcSymlinkObject = or.UnWrap()
|
||||
isSymlink = srcSymlinkObject.(*Object).IsSymlink()
|
||||
}
|
||||
}
|
||||
// Temporary object under construction
|
||||
o := &Object{
|
||||
fs: f,
|
||||
remote: src.Remote(),
|
||||
}
|
||||
// if source file is a symlink, we need to specify target path to temporary object
|
||||
if isSymlink {
|
||||
o.linkTarget = srcSymlinkObject.(*Object).linkTarget
|
||||
o.size = srcSymlinkObject.(*Object).size
|
||||
o.mode = srcSymlinkObject.(*Object).mode
|
||||
o.linkTargetIsDir = srcSymlinkObject.(*Object).linkTargetIsDir
|
||||
}
|
||||
|
||||
err = o.Update(ctx, in, src, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -1822,7 +1881,9 @@ func (o *Object) Remote() string {
|
||||
// Hash returns the selected checksum of the file
|
||||
// If no checksum is available it returns ""
|
||||
func (o *Object) Hash(ctx context.Context, r hash.Type) (string, error) {
|
||||
if o.fs.opt.DisableHashCheck {
|
||||
// Check if HashCheck is disabled or
|
||||
// if TranslateSymlinks is enabled and the target is a directory
|
||||
if o.fs.opt.DisableHashCheck || (o.fs.opt.TranslateSymlinks && o.linkTargetIsDir) {
|
||||
return "", nil
|
||||
}
|
||||
_ = o.fs.Hashes()
|
||||
@ -1987,6 +2048,11 @@ func (o *Object) setMetadata(info os.FileInfo) {
|
||||
o.mode = info.Mode()
|
||||
}
|
||||
|
||||
// IsSymlink returns true if the remote sftp file is a symlink
|
||||
func (o *Object) IsSymlink() bool {
|
||||
return o.mode&os.ModeSymlink != 0
|
||||
}
|
||||
|
||||
// statRemote stats the file or directory at the remote given
|
||||
func (f *Fs) stat(ctx context.Context, remote string) (info os.FileInfo, err error) {
|
||||
absPath := remote
|
||||
@ -1997,7 +2063,13 @@ func (f *Fs) stat(ctx context.Context, remote string) (info os.FileInfo, err err
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat: %w", err)
|
||||
}
|
||||
if f.opt.TranslateSymlinks {
|
||||
// Lstat is used to get the info of the symlink itself instead of the target
|
||||
// We use Lstat only if the user has requested --sftp-links flag
|
||||
info, err = c.sftpClient.Lstat(absPath)
|
||||
} else {
|
||||
info, err = c.sftpClient.Stat(absPath)
|
||||
}
|
||||
f.putSftpConnection(&c, err)
|
||||
return info, err
|
||||
}
|
||||
@ -2041,8 +2113,13 @@ func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Storable returns whether the remote sftp file is a regular file (not a directory, symbolic link, block device, character device, named pipe, etc.)
|
||||
// Storable returns whether the remote sftp file is a regular file (not a directory, block device, character device, named pipe, etc.)
|
||||
// if TranslateSymlinks is set, symlinks are also allowed
|
||||
func (o *Object) Storable() bool {
|
||||
if o.fs.opt.TranslateSymlinks {
|
||||
// If TranslateSymlinks is set, we also allow symlinks
|
||||
return o.mode.IsRegular() || o.IsSymlink()
|
||||
}
|
||||
return o.mode.IsRegular()
|
||||
}
|
||||
|
||||
@ -2156,12 +2233,6 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
||||
if err != nil {
|
||||
return fmt.Errorf("Update: %w", err)
|
||||
}
|
||||
// Hang on to the connection for the whole upload so it doesn't get reused while we are uploading
|
||||
file, err := c.sftpClient.OpenFile(o.path(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
|
||||
if err != nil {
|
||||
o.fs.putSftpConnection(&c, err)
|
||||
return fmt.Errorf("Update Create failed: %w", err)
|
||||
}
|
||||
// remove the file if upload failed
|
||||
remove := func() {
|
||||
c, removeErr := o.fs.getSftpConnection(ctx)
|
||||
@ -2177,6 +2248,44 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
||||
fs.Debugf(src, "Removed after failed upload: %v", err)
|
||||
}
|
||||
}
|
||||
// Check if TranslateSymlinks flag is set and if temporary object given by Put() is Symlink
|
||||
if o.fs.opt.TranslateSymlinks && o.IsSymlink() {
|
||||
// Create or update symlink
|
||||
err = o.fs.Symlink(ctx, o.linkTarget, o.path())
|
||||
if err != nil {
|
||||
o.fs.putSftpConnection(&c, err)
|
||||
return fmt.Errorf("Update Symlink failed: %w", err)
|
||||
}
|
||||
_, linkTargetIsDir, sizeTarget, err := o.fs.Readlink(ctx, o.path())
|
||||
if o.linkTargetIsDir == linkTargetIsDir {
|
||||
// if symlink target is a directory, in will be closed with ErrorReadIsDirectory
|
||||
// FIXME : Needed to bypass closeErr := inAcc.Close() in updateOrPut function in copy.go <== Need Help here if bad practice
|
||||
err = in.(*accounting.Account).Close()
|
||||
if err == fs.ErrorReadIsDirectory {
|
||||
fs.Debugf(o, "Readlink returned directory, Continue normally")
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
o.fs.putSftpConnection(&c, err)
|
||||
remove()
|
||||
return fmt.Errorf("Update Readlink failed: %w", err)
|
||||
}
|
||||
if sizeTarget != src.Size() {
|
||||
o.fs.putSftpConnection(&c, err)
|
||||
remove()
|
||||
return fmt.Errorf("Update Readlink target's size mismatch: %d != %d", sizeTarget, src.Size())
|
||||
}
|
||||
o.fs.putSftpConnection(&c, err)
|
||||
o.size = sizeTarget
|
||||
return nil
|
||||
}
|
||||
// Hang on to the connection for the whole upload so it doesn't get reused while we are uploading
|
||||
file, err := c.sftpClient.OpenFile(o.path(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
|
||||
if err != nil {
|
||||
o.fs.putSftpConnection(&c, err)
|
||||
return fmt.Errorf("Update Create failed: %w", err)
|
||||
}
|
||||
_, err = file.ReadFrom(&sizeReader{Reader: in, size: src.Size()})
|
||||
if err != nil {
|
||||
o.fs.putSftpConnection(&c, err)
|
||||
@ -2227,6 +2336,33 @@ func (o *Object) Remove(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Symlink creates a symbolic link on remote.
|
||||
func (f *Fs) Symlink(ctx context.Context, targetPath, linkName string) error {
|
||||
c, err := f.getSftpConnection(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Symlink: %w", err)
|
||||
}
|
||||
err = c.sftpClient.Symlink(targetPath, linkName)
|
||||
f.putSftpConnection(&c, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Readlink reads the target of a symbolic link.
|
||||
func (f *Fs) Readlink(ctx context.Context, path string) (string, bool, int64, error) {
|
||||
c, err := f.getSftpConnection(ctx)
|
||||
if err != nil {
|
||||
return "", false, 0, err
|
||||
}
|
||||
target, err := c.sftpClient.Stat(path)
|
||||
if err != nil {
|
||||
f.putSftpConnection(&c, err)
|
||||
return "", false, 0, err
|
||||
}
|
||||
link, err := c.sftpClient.ReadLink(path)
|
||||
f.putSftpConnection(&c, err)
|
||||
return link, target.IsDir(), target.Size(), err
|
||||
}
|
||||
|
||||
// Check the interfaces are satisfied
|
||||
var (
|
||||
_ fs.Fs = &Fs{}
|
||||
|
@ -730,6 +730,8 @@ Properties:
|
||||
|
||||
Set to skip any symlinks and any other non regular files.
|
||||
|
||||
Mutually exclusive with `--sftp-links`.
|
||||
|
||||
Properties:
|
||||
|
||||
- Config: skip_links
|
||||
@ -737,6 +739,19 @@ Properties:
|
||||
- Type: bool
|
||||
- Default: false
|
||||
|
||||
#### --sftp-links
|
||||
|
||||
Set to follow symlinks. Recreate or update symlinks as symlinks and not copy the underlying file/directory.
|
||||
|
||||
Mutually exclusive with `--sftp-skip-links`.
|
||||
|
||||
Properties:
|
||||
|
||||
- Config: links
|
||||
- Env Var: RCLONE_SFTP_LINKS
|
||||
- Type: bool
|
||||
- Default: false
|
||||
|
||||
#### --sftp-subsystem
|
||||
|
||||
Specifies the SSH2 subsystem on the remote host.
|
||||
|
@ -39,6 +39,7 @@ type Features struct {
|
||||
NoMultiThreading bool // set if can't have multiplethreads on one download open
|
||||
Overlay bool // this wraps one or more backends to add functionality
|
||||
ChunkWriterDoesntSeek bool // set if the chunk writer doesn't need to read the data more than once
|
||||
TranslateSymlink bool // set if backend can Read and Write symlinks
|
||||
|
||||
// Purge all files in the directory specified
|
||||
//
|
||||
|
1
fs/fs.go
1
fs/fs.go
@ -48,6 +48,7 @@ var (
|
||||
ErrorNotImplemented = errors.New("optional feature not implemented")
|
||||
ErrorCommandNotFound = errors.New("command not found")
|
||||
ErrorFileNameTooLong = errors.New("file name too long")
|
||||
ErrorReadIsDirectory = errors.New("read is a directory")
|
||||
)
|
||||
|
||||
// CheckClose is a utility function used to check the return from
|
||||
|
Loading…
Reference in New Issue
Block a user