rclone/fs/operations/copy.go
Nick Craig-Wood 23ab6fa3a0 operations: fix server side copies on partial upload backends after refactor
After the copy refactor:

179f978f75 operations: refactor Copy into methods on an temporary object

There was some confusion in the code about server side copies - should
they or shouldn't they use partials?

This manifested in unit test failures for remotes which supported
server side Copy and PartialUploads. This combination is rare and only
exists in the sftp backend with the --sftp-copy-is-hardlink flag.

This fix makes the choice that backends which set PartialUploads
always use partials even for server side copies.
2023-10-30 16:50:19 +00:00

411 lines
13 KiB
Go

// This file implements operations.Copy
//
// This is probably the most important operation in rclone.
package operations
import (
"context"
"errors"
"fmt"
"io"
"path"
"strings"
"time"
"unicode/utf8"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/accounting"
"github.com/rclone/rclone/fs/fserrors"
"github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/lib/atexit"
"github.com/rclone/rclone/lib/pacer"
"github.com/rclone/rclone/lib/random"
)
// State of the copy
type copy struct {
f fs.Fs // destination fs.Fs
dstFeatures *fs.Features // Features() for fs.Fs
dst fs.Object // destination object to update, may be nil
remote string // destination path, used if dst is nil
src fs.Object // source object
ci *fs.ConfigInfo // current config
maxTries int // max number of tries to do the copy
doUpdate bool // whether we are updating an existing file or not
hashType hash.Type // common hash to use
hashOption *fs.HashesOption // open option for the common hash
tr *accounting.Transfer // accounting for the transfer
inplace bool // set if we are updating inplace and not using a partial name
remoteForCopy string // the name used for the transfer, either remote or remote+".partial"
}
// Used to remove a failed copy
func (c *copy) removeFailedCopy(ctx context.Context, o fs.Object) {
if o == nil {
return
}
fs.Infof(o, "Removing failed copy")
err := o.Remove(ctx)
if err != nil {
fs.Infof(o, "Failed to remove failed copy: %s", err)
}
}
// Used to remove a failed partial copy
func (c *copy) removeFailedPartialCopy(ctx context.Context, f fs.Fs, remote string) {
o, err := f.NewObject(ctx, remote)
if errors.Is(err, fs.ErrorObjectNotFound) {
// Assume object has been deleted
return
}
if err != nil {
fs.Infof(remote, "Failed to remove failed partial copy: %s", err)
return
}
c.removeFailedCopy(ctx, o)
}
// TruncateString s to n bytes.
//
// If s is valid UTF-8 then this may truncate to fewer than n bytes to
// make the returned string also valid UTF-8.
func TruncateString(s string, n int) string {
truncated := s[:n]
if !utf8.ValidString(s) {
// If input string wasn't valid UTF-8 then just return the truncation
return truncated
}
for len(truncated) > 0 {
if utf8.ValidString(truncated) {
return truncated
}
// Remove 1 byte until valid
truncated = truncated[:len(truncated)-1]
}
return truncated
}
// Check to see if we should be using a partial name and return the name for the copy and the inplace flag
func (c *copy) checkPartial() (remoteForCopy string, inplace bool, err error) {
remoteForCopy = c.remote
if c.ci.Inplace || c.dstFeatures.Move == nil || !c.dstFeatures.PartialUploads || strings.HasSuffix(c.remote, ".rclonelink") {
return remoteForCopy, true, nil
}
if len(c.ci.PartialSuffix) > 16 {
return remoteForCopy, true, fmt.Errorf("expecting length of --partial-suffix to be not greater than %d but got %d", 16, len(c.ci.PartialSuffix))
}
// Avoid making the leaf name longer if it's already lengthy to avoid
// trouble with file name length limits.
suffix := "." + random.String(8) + c.ci.PartialSuffix
base := path.Base(c.remoteForCopy)
if len(base) > 100 {
remoteForCopy = TruncateString(c.remoteForCopy, len(c.remoteForCopy)-len(suffix)) + suffix
} else {
remoteForCopy += suffix
}
return remoteForCopy, false, nil
}
// Check to see if we have hit max transfer limits
func (c *copy) checkLimits(ctx context.Context) (err error) {
if c.ci.MaxTransfer < 0 {
return nil
}
var bytesSoFar int64
if c.ci.CutoffMode == fs.CutoffModeCautious {
bytesSoFar = accounting.Stats(ctx).GetBytesWithPending() + c.src.Size()
} else {
bytesSoFar = accounting.Stats(ctx).GetBytes()
}
if bytesSoFar >= int64(c.ci.MaxTransfer) {
if c.ci.CutoffMode == fs.CutoffModeHard {
return accounting.ErrorMaxTransferLimitReachedFatal
}
return accounting.ErrorMaxTransferLimitReachedGraceful
}
return nil
}
// Server side copy c.src to (c.f, c.remoteForCopy) if possible or return fs.ErrorCantCopy if not
func (c *copy) serverSideCopy(ctx context.Context) (actionTaken string, newDst fs.Object, err error) {
doCopy := c.dstFeatures.Copy
serverSideCopyOK := false
if doCopy == nil {
serverSideCopyOK = false
} else if SameConfig(c.src.Fs(), c.f) {
serverSideCopyOK = true
} else if SameRemoteType(c.src.Fs(), c.f) {
serverSideCopyOK = c.dstFeatures.ServerSideAcrossConfigs || c.ci.ServerSideAcrossConfigs
}
if !serverSideCopyOK {
return actionTaken, nil, fs.ErrorCantCopy
}
in := c.tr.Account(ctx, nil) // account the transfer
in.ServerSideTransferStart()
newDst, err = doCopy(ctx, c.src, c.remoteForCopy)
if err == nil {
in.ServerSideCopyEnd(newDst.Size()) // account the bytes for the server-side transfer
}
_ = in.Close()
if errors.Is(err, fs.ErrorCantCopy) {
c.tr.Reset(ctx) // skip incomplete accounting - will be overwritten by the manual copy
}
actionTaken = "Copied (server-side copy)"
return actionTaken, newDst, err
}
// Copy c.src to (c.f, c.remoteForCopy) using multiThreadCopy
func (c *copy) multiThreadCopy(ctx context.Context, uploadOptions []fs.OpenOption) (actionTaken string, newDst fs.Object, err error) {
newDst, err = multiThreadCopy(ctx, c.f, c.remoteForCopy, c.src, c.ci.MultiThreadStreams, c.tr, uploadOptions...)
if c.doUpdate {
actionTaken = "Multi-thread Copied (replaced existing)"
} else {
actionTaken = "Multi-thread Copied (new)"
}
return actionTaken, newDst, err
}
// Copy the stream from in to (c.f, c.remoteForCopy) and close it
//
// Use Rcat to handle both remotes supporting and not supporting PutStream.
func (c *copy) rcat(ctx context.Context, in io.ReadCloser) (actionTaken string, newDst fs.Object, err error) {
// Make any metadata to pass to rcat
var meta fs.Metadata
if c.ci.Metadata {
meta, err = fs.GetMetadata(ctx, c.src)
if err != nil {
fs.Errorf(c.src, "Failed to read metadata: %v", err)
}
}
// NB Rcat closes in0
newDst, err = Rcat(ctx, c.f, c.remoteForCopy, in, c.src.ModTime(ctx), meta)
if c.doUpdate {
actionTaken = "Copied (Rcat, replaced existing)"
} else {
actionTaken = "Copied (Rcat, new)"
}
return actionTaken, newDst, err
}
// Copy the stream from in to (c.f, c.remoteForCopy) and close it
func (c *copy) updateOrPut(ctx context.Context, in io.ReadCloser, uploadOptions []fs.OpenOption) (actionTaken string, newDst fs.Object, err error) {
// account and buffer the transfer
inAcc := c.tr.Account(ctx, in).WithBuffer()
var wrappedSrc fs.ObjectInfo = c.src
// We try to pass the original object if possible
if c.src.Remote() != c.remoteForCopy {
wrappedSrc = fs.NewOverrideRemote(c.src, c.remoteForCopy)
}
if c.doUpdate && c.inplace {
err = c.dst.Update(ctx, inAcc, wrappedSrc, uploadOptions...)
// Make sure newDst is c.dst since we updated it
if err == nil {
newDst = c.dst
}
} else {
newDst, err = c.f.Put(ctx, inAcc, wrappedSrc, uploadOptions...)
}
closeErr := inAcc.Close()
if err == nil {
err = closeErr
}
if c.doUpdate {
actionTaken = "Copied (replaced existing)"
} else {
actionTaken = "Copied (new)"
}
return actionTaken, newDst, err
}
// Do a manual copy by reading the bytes and writing them
func (c *copy) manualCopy(ctx context.Context) (actionTaken string, newDst fs.Object, err error) {
// Remove partial files on premature exit
if !c.inplace {
defer atexit.Unregister(atexit.Register(func() {
ctx := context.Background()
c.removeFailedPartialCopy(ctx, c.f, c.remoteForCopy)
}))
}
// Options for the upload
uploadOptions := []fs.OpenOption{c.hashOption}
for _, option := range c.ci.UploadHeaders {
uploadOptions = append(uploadOptions, option)
}
if c.ci.MetadataSet != nil {
uploadOptions = append(uploadOptions, fs.MetadataOption(c.ci.MetadataSet))
}
// Options for the download
downloadOptions := []fs.OpenOption{c.hashOption}
for _, option := range c.ci.DownloadHeaders {
downloadOptions = append(downloadOptions, option)
}
if doMultiThreadCopy(ctx, c.f, c.src) {
return c.multiThreadCopy(ctx, uploadOptions)
}
var in io.ReadCloser
in, err = Open(ctx, c.src, downloadOptions...)
if err != nil {
return actionTaken, nil, fmt.Errorf("failed to open source object: %w", err)
}
// Note that c.rcat and c.updateOrPut close in
if c.src.Size() == -1 {
return c.rcat(ctx, in)
}
return c.updateOrPut(ctx, in, uploadOptions)
}
// Verify the copy
func (c *copy) verify(ctx context.Context, newDst fs.Object) (err error) {
// Verify sizes are the same after transfer
if sizeDiffers(ctx, c.src, newDst) {
return fmt.Errorf("corrupted on transfer: sizes differ %d vs %d", c.src.Size(), newDst.Size())
}
// Verify hashes are the same after transfer - ignoring blank hashes
if c.hashType != hash.None {
// checkHashes has logs and counts errors
equal, _, srcSum, dstSum, _ := checkHashes(ctx, c.src, newDst, c.hashType)
if !equal {
return fmt.Errorf("corrupted on transfer: %v hash differ %q vs %q", c.hashType, srcSum, dstSum)
}
}
return nil
}
// copy src object to dst or f if nil. If dst is nil then it uses
// remote as the name of the new object.
//
// It returns the destination object if possible. Note that this may
// be nil.
func (c *copy) copy(ctx context.Context) (newDst fs.Object, err error) {
var actionTaken string
retry := true
for tries := 0; retry && tries < c.maxTries; tries++ {
// Check we haven't hit any accounting limits
err = c.checkLimits(ctx)
if err != nil {
return nil, err
}
// Try server side copy
actionTaken, newDst, err = c.serverSideCopy(ctx)
// If can't server-side copy, do it manually
if errors.Is(err, fs.ErrorCantCopy) {
actionTaken, newDst, err = c.manualCopy(ctx)
}
// End if ctx is in error
if fserrors.ContextError(ctx, &err) {
break
}
// Retry if err returned a retry error
retry = false
if fserrors.IsRetryError(err) || fserrors.ShouldRetry(err) {
retry = true
} else if t, ok := pacer.IsRetryAfter(err); ok {
fs.Debugf(c.src, "Sleeping for %v (as indicated by the server) to obey Retry-After error: %v", t, err)
time.Sleep(t)
retry = true
}
if retry {
fs.Debugf(c.src, "Received error: %v - low level retry %d/%d", err, tries, c.maxTries)
c.tr.Reset(ctx) // skip incomplete accounting - will be overwritten by retry
continue
}
}
if err != nil {
err = fs.CountError(err)
fs.Errorf(c.src, "Failed to copy: %v", err)
if !c.inplace {
c.removeFailedPartialCopy(ctx, c.f, c.remoteForCopy)
}
return newDst, err
}
// Verify the copy
err = c.verify(ctx, newDst)
if err != nil {
fs.Errorf(newDst, "%v", err)
err = fs.CountError(err)
c.removeFailedCopy(ctx, newDst)
return nil, err
}
// Move the copied file to its real destination.
if !c.inplace && c.remoteForCopy != c.remote {
movedNewDst, err := c.dstFeatures.Move(ctx, newDst, c.remote)
if err != nil {
fs.Errorf(newDst, "partial file rename failed: %v", err)
err = fs.CountError(err)
c.removeFailedCopy(ctx, newDst)
return nil, err
}
fs.Debugf(newDst, "renamed to: %s", c.remote)
newDst = movedNewDst
}
// Log what we have done
if newDst != nil && c.src.String() != newDst.String() {
actionTaken = fmt.Sprintf("%s to: %s", actionTaken, newDst.String())
}
fs.Infof(c.src, "%s%s", actionTaken, fs.LogValueHide("size", fs.SizeSuffix(c.src.Size())))
return newDst, nil
}
// Copy src object to dst or f if nil. If dst is nil then it uses
// remote as the name of the new object.
//
// It returns the destination object if possible. Note that this may
// be nil.
func Copy(ctx context.Context, f fs.Fs, dst fs.Object, remote string, src fs.Object) (newDst fs.Object, err error) {
ci := fs.GetConfig(ctx)
tr := accounting.Stats(ctx).NewTransfer(src)
defer func() {
tr.Done(ctx, err)
}()
if SkipDestructive(ctx, src, "copy") {
in := tr.Account(ctx, nil)
in.DryRun(src.Size())
return newDst, nil
}
c := &copy{
f: f,
dstFeatures: f.Features(),
dst: dst,
remote: remote,
src: src,
ci: ci,
tr: tr,
maxTries: ci.LowLevelRetries,
doUpdate: dst != nil,
}
c.hashType, c.hashOption = CommonHash(ctx, f, src.Fs())
if c.dst != nil {
c.remote = c.dst.Remote()
}
// Are we using partials?
//
// If so set the flag and update the name we use for the copy
c.remoteForCopy, c.inplace, err = c.checkPartial()
if err != nil {
return nil, err
}
// Do the copy now everything is set up
return c.copy(ctx)
}
// CopyFile moves a single file possibly to a new name
func CopyFile(ctx context.Context, fdst fs.Fs, fsrc fs.Fs, dstFileName string, srcFileName string) (err error) {
return moveOrCopyFile(ctx, fdst, fsrc, dstFileName, srcFileName, true)
}