462a1cf491
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
93 lines
2.5 KiB
Go
93 lines
2.5 KiB
Go
//go:build darwin && cgo
|
|
|
|
// Package local provides a filesystem interface
|
|
package local
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"github.com/go-darwin/apfs"
|
|
"github.com/rclone/rclone/fs"
|
|
)
|
|
|
|
// Copy src to this remote using server-side copy operations.
|
|
//
|
|
// # This is stored with the remote path given
|
|
//
|
|
// # It returns the destination Object and a possible error
|
|
//
|
|
// Will only be called if src.Fs().Name() == f.Name()
|
|
//
|
|
// If it isn't possible then return fs.ErrorCantCopy
|
|
func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
|
|
if runtime.GOOS != "darwin" || f.opt.NoClone {
|
|
return nil, fs.ErrorCantCopy
|
|
}
|
|
srcObj, ok := src.(*Object)
|
|
if !ok {
|
|
fs.Debugf(src, "Can't clone - not same remote type")
|
|
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
|
|
meta, err := fs.GetMetadataOptions(ctx, f, src, fs.MetadataAsOpenOptions(ctx))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("copy: failed to read metadata: %w", err)
|
|
}
|
|
|
|
// Create destination
|
|
dstObj := f.newObject(remote)
|
|
err = dstObj.mkdirAll()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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 {
|
|
return nil, err
|
|
}
|
|
|
|
// Set metadata if --metadata is in use
|
|
if meta != nil {
|
|
err = dstObj.writeMetadata(meta)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("copy: failed to set metadata: %w", err)
|
|
}
|
|
}
|
|
|
|
return f.NewObject(ctx, remote)
|
|
}
|
|
|
|
// Clone uses APFS cloning if possible, otherwise falls back to copying (with full metadata preservation)
|
|
// note that this is closely related to unix.Clonefile(src, dst, unix.CLONE_NOFOLLOW) but not 100% identical
|
|
// https://opensource.apple.com/source/copyfile/copyfile-173.40.2/copyfile.c.auto.html
|
|
func Clone(src, dst string) error {
|
|
state := apfs.CopyFileStateAlloc()
|
|
defer func() {
|
|
if err := apfs.CopyFileStateFree(state); err != nil {
|
|
fs.Errorf(dst, "free state error: %v", err)
|
|
}
|
|
}()
|
|
cloned, err := apfs.CopyFile(src, dst, state, apfs.COPYFILE_CLONE)
|
|
fs.Debugf(dst, "isCloned: %v, error: %v", cloned, err)
|
|
return err
|
|
}
|
|
|
|
// Check the interfaces are satisfied
|
|
var (
|
|
_ fs.Copier = &Fs{}
|
|
)
|