2023-10-13 11:22:50 +00:00
|
|
|
// This file implements operations.Copy
|
|
|
|
//
|
|
|
|
// This is probably the most important operation in rclone.
|
|
|
|
|
2023-10-12 19:28:27 +00:00
|
|
|
package operations
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"path"
|
|
|
|
"strings"
|
|
|
|
"time"
|
2023-10-28 16:12:03 +00:00
|
|
|
"unicode/utf8"
|
2023-10-12 19:28:27 +00:00
|
|
|
|
|
|
|
"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"
|
|
|
|
)
|
|
|
|
|
2023-10-13 11:22:50 +00:00
|
|
|
// 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"
|
|
|
|
}
|
|
|
|
|
2023-10-12 19:28:27 +00:00
|
|
|
// Used to remove a failed copy
|
2023-10-13 11:22:50 +00:00
|
|
|
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)
|
|
|
|
}
|
2023-10-12 19:28:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Used to remove a failed partial copy
|
2023-10-13 11:22:50 +00:00
|
|
|
func (c *copy) removeFailedPartialCopy(ctx context.Context, f fs.Fs, remote string) {
|
|
|
|
o, err := f.NewObject(ctx, remote)
|
2023-10-12 19:28:27 +00:00
|
|
|
if errors.Is(err, fs.ErrorObjectNotFound) {
|
2023-10-13 11:22:50 +00:00
|
|
|
// Assume object has been deleted
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
fs.Infof(remote, "Failed to remove failed partial copy: %s", err)
|
|
|
|
return
|
2023-10-12 19:28:27 +00:00
|
|
|
}
|
2023-10-13 11:22:50 +00:00
|
|
|
c.removeFailedCopy(ctx, o)
|
2023-10-12 19:28:27 +00:00
|
|
|
}
|
|
|
|
|
2023-10-28 16:12:03 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2023-10-13 11:22:50 +00:00
|
|
|
// 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
|
2024-03-28 12:12:48 +00:00
|
|
|
base := path.Base(remoteForCopy)
|
2023-10-13 11:22:50 +00:00
|
|
|
if len(base) > 100 {
|
2024-03-28 12:12:48 +00:00
|
|
|
remoteForCopy = TruncateString(remoteForCopy, len(remoteForCopy)-len(suffix)) + suffix
|
2023-10-13 11:22:50 +00:00
|
|
|
} 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
|
|
|
|
}
|
2023-10-30 16:45:20 +00:00
|
|
|
_ = in.Close()
|
2023-10-13 11:22:50 +00:00
|
|
|
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
|
2023-10-12 19:28:27 +00:00
|
|
|
//
|
2023-10-13 11:22:50 +00:00
|
|
|
// 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)
|
|
|
|
}
|
2023-10-12 19:28:27 +00:00
|
|
|
}
|
|
|
|
|
2023-10-13 11:22:50 +00:00
|
|
|
// NB Rcat closes in0
|
2024-05-15 16:48:05 +00:00
|
|
|
fsrc, ok := c.src.Fs().(fs.Fs)
|
|
|
|
if !ok {
|
|
|
|
fsrc = nil
|
|
|
|
}
|
|
|
|
newDst, err = rcatSrc(ctx, c.f, c.remoteForCopy, in, c.src.ModTime(ctx), meta, fsrc)
|
2023-10-13 11:22:50 +00:00
|
|
|
if c.doUpdate {
|
|
|
|
actionTaken = "Copied (Rcat, replaced existing)"
|
|
|
|
} else {
|
|
|
|
actionTaken = "Copied (Rcat, new)"
|
2023-10-12 19:28:27 +00:00
|
|
|
}
|
2023-10-13 11:22:50 +00:00
|
|
|
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
|
2023-10-12 19:28:27 +00:00
|
|
|
|
2023-10-13 11:22:50 +00:00
|
|
|
// 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
|
2023-10-12 19:28:27 +00:00
|
|
|
}
|
2023-10-13 11:22:50 +00:00
|
|
|
} 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)
|
|
|
|
}
|
2023-10-12 19:28:27 +00:00
|
|
|
|
2023-10-13 11:22:50 +00:00
|
|
|
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) {
|
2024-03-31 05:23:26 +00:00
|
|
|
return fmt.Errorf("corrupted on transfer: sizes differ src(%s) %d vs dst(%s) %d", c.src.Fs(), c.src.Size(), newDst.Fs(), newDst.Size())
|
2023-10-13 11:22:50 +00:00
|
|
|
}
|
|
|
|
// 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 {
|
2024-03-31 05:23:26 +00:00
|
|
|
return fmt.Errorf("corrupted on transfer: %v hashes differ src(%s) %q vs dst(%s) %q", c.hashType, c.src.Fs(), srcSum, newDst.Fs(), dstSum)
|
2023-10-12 19:28:27 +00:00
|
|
|
}
|
|
|
|
}
|
2023-10-13 11:22:50 +00:00
|
|
|
return nil
|
|
|
|
}
|
2023-10-12 19:28:27 +00:00
|
|
|
|
2023-10-13 11:22:50 +00:00
|
|
|
// 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) {
|
2023-10-12 19:28:27 +00:00
|
|
|
var actionTaken string
|
2023-10-13 11:22:50 +00:00
|
|
|
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
|
2023-10-12 19:28:27 +00:00
|
|
|
}
|
2023-10-13 11:22:50 +00:00
|
|
|
|
|
|
|
// Try server side copy
|
|
|
|
actionTaken, newDst, err = c.serverSideCopy(ctx)
|
|
|
|
|
2023-10-12 19:28:27 +00:00
|
|
|
// If can't server-side copy, do it manually
|
|
|
|
if errors.Is(err, fs.ErrorCantCopy) {
|
2023-10-13 11:22:50 +00:00
|
|
|
actionTaken, newDst, err = c.manualCopy(ctx)
|
2023-10-12 19:28:27 +00:00
|
|
|
}
|
2023-10-13 11:22:50 +00:00
|
|
|
|
|
|
|
// End if ctx is in error
|
2023-10-12 19:28:27 +00:00
|
|
|
if fserrors.ContextError(ctx, &err) {
|
|
|
|
break
|
|
|
|
}
|
2023-10-13 11:22:50 +00:00
|
|
|
|
|
|
|
// Retry if err returned a retry error
|
|
|
|
retry = false
|
2023-10-12 19:28:27 +00:00
|
|
|
if fserrors.IsRetryError(err) || fserrors.ShouldRetry(err) {
|
|
|
|
retry = true
|
|
|
|
} else if t, ok := pacer.IsRetryAfter(err); ok {
|
2023-10-13 11:22:50 +00:00
|
|
|
fs.Debugf(c.src, "Sleeping for %v (as indicated by the server) to obey Retry-After error: %v", t, err)
|
2023-10-12 19:28:27 +00:00
|
|
|
time.Sleep(t)
|
|
|
|
retry = true
|
|
|
|
}
|
|
|
|
if retry {
|
2023-10-13 11:22:50 +00:00
|
|
|
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
|
2023-10-12 19:28:27 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
err = fs.CountError(err)
|
2023-10-13 11:22:50 +00:00
|
|
|
fs.Errorf(c.src, "Failed to copy: %v", err)
|
|
|
|
if !c.inplace {
|
|
|
|
c.removeFailedPartialCopy(ctx, c.f, c.remoteForCopy)
|
2023-10-12 19:28:27 +00:00
|
|
|
}
|
|
|
|
return newDst, err
|
|
|
|
}
|
|
|
|
|
2023-10-13 11:22:50 +00:00
|
|
|
// Verify the copy
|
|
|
|
err = c.verify(ctx, newDst)
|
|
|
|
if err != nil {
|
|
|
|
fs.Errorf(newDst, "%v", err)
|
2023-10-12 19:28:27 +00:00
|
|
|
err = fs.CountError(err)
|
2023-10-13 11:22:50 +00:00
|
|
|
c.removeFailedCopy(ctx, newDst)
|
|
|
|
return nil, err
|
2023-10-12 19:28:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Move the copied file to its real destination.
|
2023-10-13 11:22:50 +00:00
|
|
|
if !c.inplace && c.remoteForCopy != c.remote {
|
|
|
|
movedNewDst, err := c.dstFeatures.Move(ctx, newDst, c.remote)
|
|
|
|
if err != nil {
|
2023-10-12 19:28:27 +00:00
|
|
|
fs.Errorf(newDst, "partial file rename failed: %v", err)
|
|
|
|
err = fs.CountError(err)
|
2023-10-13 11:22:50 +00:00
|
|
|
c.removeFailedCopy(ctx, newDst)
|
|
|
|
return nil, err
|
2023-10-12 19:28:27 +00:00
|
|
|
}
|
2023-10-13 11:22:50 +00:00
|
|
|
fs.Debugf(newDst, "renamed to: %s", c.remote)
|
|
|
|
newDst = movedNewDst
|
2023-10-12 19:28:27 +00:00
|
|
|
}
|
|
|
|
|
2023-10-13 11:22:50 +00:00
|
|
|
// Log what we have done
|
|
|
|
if newDst != nil && c.src.String() != newDst.String() {
|
2023-10-12 19:28:27 +00:00
|
|
|
actionTaken = fmt.Sprintf("%s to: %s", actionTaken, newDst.String())
|
|
|
|
}
|
2023-10-13 11:22:50 +00:00
|
|
|
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)
|
2024-01-18 16:44:13 +00:00
|
|
|
tr := accounting.Stats(ctx).NewTransfer(src, f)
|
2023-10-13 11:22:50 +00:00
|
|
|
defer func() {
|
|
|
|
tr.Done(ctx, err)
|
|
|
|
}()
|
|
|
|
if SkipDestructive(ctx, src, "copy") {
|
|
|
|
in := tr.Account(ctx, nil)
|
|
|
|
in.DryRun(src.Size())
|
|
|
|
return newDst, nil
|
|
|
|
}
|
|
|
|
c := ©{
|
|
|
|
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)
|
2023-10-12 19:28:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|