Implement -L, --copy-links flag to allow rclone to follow symlinks

Fixes #40
This commit is contained in:
Nick Craig-Wood 2017-01-29 13:43:20 +00:00
parent 29c6e22024
commit 94947f2523
2 changed files with 70 additions and 6 deletions

View file

@ -79,6 +79,47 @@ file exceeds 258 characters on z, so only use this option if you have to.
Here are the command line options specific to local storage Here are the command line options specific to local storage
#### --copy-links, -L ####
Normally rclone will ignore symlinks or junction points (which behave
like symlinks under Windows).
If you supply this flag then rclone will follow the symlink and copy
the pointed to file or directory.
This flag applies to all commands.
For example, supposing you have a directory structure like this
```
$ tree /tmp/a
/tmp/a
├── b -> ../b
├── expected -> ../expected
├── one
└── two
└── three
```
Then you can see the difference with and without the flag like this
```
$ rclone ls /tmp/a
6 one
6 two/three
```
and
```
$ rclone -L ls /tmp/a
4174 expected
6 one
6 two/three
6 b/two
6 b/one
```
#### --one-file-system, -x #### #### --one-file-system, -x ####
This tells rclone to stay in the filesystem specified by the root and This tells rclone to stay in the filesystem specified by the root and

View file

@ -21,6 +21,10 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
var (
followSymlinks = fs.BoolP("copy-links", "L", false, "Follow symlinks and copy the pointed to item.")
)
// Constants // Constants
const devUnset = 0xdeadbeefcafebabe // a device id meaning it is unset const devUnset = 0xdeadbeefcafebabe // a device id meaning it is unset
@ -54,6 +58,8 @@ type Fs struct {
wmu sync.Mutex // used for locking access to 'warned'. wmu sync.Mutex // used for locking access to 'warned'.
warned map[string]struct{} // whether we have warned about this string warned map[string]struct{} // whether we have warned about this string
nounc bool // Skip UNC conversion on Windows nounc bool // Skip UNC conversion on Windows
// do os.Lstat or os.Stat
lstat func(name string) (os.FileInfo, error)
} }
// Object represents a local filesystem object // Object represents a local filesystem object
@ -77,12 +83,16 @@ func NewFs(name, root string) (fs.Fs, error) {
warned: make(map[string]struct{}), warned: make(map[string]struct{}),
nounc: nounc == "true", nounc: nounc == "true",
dev: devUnset, dev: devUnset,
lstat: os.Lstat,
} }
f.root = f.cleanPath(root) f.root = f.cleanPath(root)
f.features = (&fs.Features{CaseInsensitive: f.caseInsensitive()}).Fill(f) f.features = (&fs.Features{CaseInsensitive: f.caseInsensitive()}).Fill(f)
if *followSymlinks {
f.lstat = os.Stat
}
// Check to see if this points to a file // Check to see if this points to a file
fi, err := os.Lstat(f.root) fi, err := f.lstat(f.root)
if err == nil { if err == nil {
f.dev = readDevice(fi) f.dev = readDevice(fi)
} }
@ -197,12 +207,22 @@ func (f *Fs) list(out fs.ListOpts, remote string, dirpath string, level int) (su
for _, fi := range fis { for _, fi := range fis {
name := fi.Name() name := fi.Name()
mode := fi.Mode()
newRemote := path.Join(remote, name) newRemote := path.Join(remote, name)
newPath := filepath.Join(dirpath, name) newPath := filepath.Join(dirpath, name)
// Follow symlinks if required
if *followSymlinks && (mode&os.ModeSymlink) != 0 {
fi, err = os.Stat(newPath)
if err != nil {
out.SetError(err)
return nil
}
mode = fi.Mode()
}
if fi.IsDir() { if fi.IsDir() {
// Ignore directories which are symlinks. These are junction points under windows which // Ignore directories which are symlinks. These are junction points under windows which
// are kind of a souped up symlink. Unix doesn't have directories which are symlinks. // are kind of a souped up symlink. Unix doesn't have directories which are symlinks.
if (fi.Mode()&os.ModeSymlink) == 0 && out.IncludeDirectory(newRemote) { if (mode&os.ModeSymlink) == 0 && out.IncludeDirectory(newRemote) {
dir := &fs.Dir{ dir := &fs.Dir{
Name: f.cleanRemote(newRemote), Name: f.cleanRemote(newRemote),
When: fi.ModTime(), When: fi.ModTime(),
@ -321,7 +341,7 @@ func (f *Fs) Mkdir(dir string) error {
return err return err
} }
if dir == "" { if dir == "" {
fi, err := os.Lstat(root) fi, err := f.lstat(root)
if err != nil { if err != nil {
return err return err
} }
@ -404,7 +424,7 @@ func (f *Fs) readPrecision() (precision time.Duration) {
// deleting all the files quicker than just running Remove() on the // deleting all the files quicker than just running Remove() on the
// result of List() // result of List()
func (f *Fs) Purge() error { func (f *Fs) Purge() error {
fi, err := os.Lstat(f.root) fi, err := f.lstat(f.root)
if err != nil { if err != nil {
return err return err
} }
@ -591,7 +611,10 @@ func (o *Object) Storable() bool {
fs.Debug(o, "Clearing symlink bit to allow a file with reparse points to be copied") fs.Debug(o, "Clearing symlink bit to allow a file with reparse points to be copied")
mode &^= os.ModeSymlink mode &^= os.ModeSymlink
} }
if mode&(os.ModeSymlink|os.ModeNamedPipe|os.ModeSocket|os.ModeDevice) != 0 { if mode&os.ModeSymlink != 0 {
fs.Debug(o, "Can't follow symlink without -L/--copy-links")
return false
} else if mode&(os.ModeNamedPipe|os.ModeSocket|os.ModeDevice) != 0 {
fs.Debug(o, "Can't transfer non file/directory") fs.Debug(o, "Can't transfer non file/directory")
return false return false
} else if mode&os.ModeDir != 0 { } else if mode&os.ModeDir != 0 {
@ -713,7 +736,7 @@ func (o *Object) Update(in io.Reader, src fs.ObjectInfo) error {
// Stat a Object into info // Stat a Object into info
func (o *Object) lstat() error { func (o *Object) lstat() error {
info, err := os.Lstat(o.path) info, err := o.fs.lstat(o.path)
o.info = info o.info = info
return err return err
} }