local: fix --copy-links on macOS when cloning

Before this change, --copy-links erroneously behaved like --links when using cloning
on macOS, and cloning was not supported at all when using --links.

After this change, --copy-links does what it's supposed to, and takes advantage of
cloning when possible, by copying the file being linked to instead of the link
itself.

Cloning is now also supported in --links mode for regular files (which benefit
most from cloning). symlinks in --links mode continue to be tossed back to be
handled by rclone's special translation logic.

See https://forum.rclone.org/t/macos-local-to-local-copy-with-copy-links-causes-error/47671/5?u=nielash
This commit is contained in:
nielash 2024-09-16 16:24:19 -04:00 committed by Nick Craig-Wood
parent 0b7b3cacdc
commit 462a1cf491
2 changed files with 46 additions and 6 deletions

View File

@ -6,6 +6,7 @@ package local
import ( import (
"context" "context"
"fmt" "fmt"
"path/filepath"
"runtime" "runtime"
"github.com/go-darwin/apfs" "github.com/go-darwin/apfs"
@ -22,7 +23,7 @@ import (
// //
// If it isn't possible then return fs.ErrorCantCopy // If it isn't possible then return fs.ErrorCantCopy
func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
if runtime.GOOS != "darwin" || f.opt.TranslateSymlinks || f.opt.NoClone { if runtime.GOOS != "darwin" || f.opt.NoClone {
return nil, fs.ErrorCantCopy return nil, fs.ErrorCantCopy
} }
srcObj, ok := src.(*Object) srcObj, ok := src.(*Object)
@ -30,6 +31,9 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
fs.Debugf(src, "Can't clone - not same remote type") fs.Debugf(src, "Can't clone - not same remote type")
return nil, fs.ErrorCantCopy return nil, fs.ErrorCantCopy
} }
if f.opt.TranslateSymlinks && srcObj.translatedLink { // in --links mode, use cloning only for regular files
return nil, fs.ErrorCantCopy
}
// Fetch metadata if --metadata is in use // Fetch metadata if --metadata is in use
meta, err := fs.GetMetadataOptions(ctx, f, src, fs.MetadataAsOpenOptions(ctx)) meta, err := fs.GetMetadataOptions(ctx, f, src, fs.MetadataAsOpenOptions(ctx))
@ -44,11 +48,18 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
return nil, err return nil, err
} }
err = Clone(srcObj.path, f.localPath(remote)) srcPath := srcObj.path
if f.opt.FollowSymlinks { // in --copy-links mode, find the real file being pointed to and pass that in instead
srcPath, err = filepath.EvalSymlinks(srcPath)
if err != nil {
return nil, err
}
}
err = Clone(srcPath, f.localPath(remote))
if err != nil { if err != nil {
return nil, err return nil, err
} }
fs.Debugf(remote, "server-side cloned!")
// Set metadata if --metadata is in use // Set metadata if --metadata is in use
if meta != nil { if meta != nil {

View File

@ -73,7 +73,6 @@ func TestUpdatingCheck(t *testing.T) {
r.WriteFile(filePath, "content updated", time.Now()) r.WriteFile(filePath, "content updated", time.Now())
_, err = in.Read(buf) _, err = in.Read(buf)
require.NoError(t, err) require.NoError(t, err)
} }
// Test corrupted on transfer // Test corrupted on transfer
@ -224,7 +223,7 @@ func TestHashOnUpdate(t *testing.T) {
assert.Equal(t, "9a0364b9e99bb480dd25e1f0284c8555", md5) assert.Equal(t, "9a0364b9e99bb480dd25e1f0284c8555", md5)
// Reupload it with different contents but same size and timestamp // Reupload it with different contents but same size and timestamp
var b = bytes.NewBufferString("CONTENT") b := bytes.NewBufferString("CONTENT")
src := object.NewStaticObjectInfo(filePath, when, int64(b.Len()), true, nil, f) src := object.NewStaticObjectInfo(filePath, when, int64(b.Len()), true, nil, f)
err = o.Update(ctx, b, src) err = o.Update(ctx, b, src)
require.NoError(t, err) require.NoError(t, err)
@ -395,7 +394,6 @@ func TestMetadata(t *testing.T) {
assert.Equal(t, "wedges", m["potato"]) assert.Equal(t, "wedges", m["potato"])
} }
}) })
} }
func TestFilter(t *testing.T) { func TestFilter(t *testing.T) {
@ -572,4 +570,35 @@ func TestCopySymlink(t *testing.T) {
linkContents, err := os.Readlink(dstPath) linkContents, err := os.Readlink(dstPath)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "file.txt", linkContents) assert.Equal(t, "file.txt", linkContents)
// Set fs into "-L/--copy-links" mode
f.opt.FollowSymlinks = true
f.opt.TranslateSymlinks = false
f.lstat = os.Stat
// Create dst
require.NoError(t, f.Mkdir(ctx, "dst2"))
// Do copy from src into dst
src, err = f.NewObject(ctx, "src/link.txt")
require.NoError(t, err)
require.NotNil(t, src)
dst, err = operations.Copy(ctx, f, nil, "dst2/link.txt", src)
require.NoError(t, err)
require.NotNil(t, dst)
// Test that we made a NON-symlink and it has the right contents
dstPath = filepath.Join(r.LocalName, "dst2", "link.txt")
fi, err := os.Lstat(dstPath)
require.NoError(t, err)
assert.True(t, fi.Mode()&os.ModeSymlink == 0)
want := fstest.NewItem("dst2/link.txt", "hello world", when)
fstest.CompareItems(t, []fs.DirEntry{dst}, []fstest.Item{want}, nil, f.precision, "")
// Test that copying a normal file also works
dst, err = operations.Copy(ctx, f, nil, "dst2/file.txt", dst)
require.NoError(t, err)
require.NotNil(t, dst)
want = fstest.NewItem("dst2/file.txt", "hello world", when)
fstest.CompareItems(t, []fs.DirEntry{dst}, []fstest.Item{want}, nil, f.precision, "")
} }