aboutsummaryrefslogtreecommitdiff
path: root/fs/operations/operations.go
diff options
context:
space:
mode:
Diffstat (limited to 'fs/operations/operations.go')
-rw-r--r--fs/operations/operations.go2742
1 files changed, 2742 insertions, 0 deletions
diff --git a/fs/operations/operations.go b/fs/operations/operations.go
new file mode 100644
index 0000000..1ada8f8
--- /dev/null
+++ b/fs/operations/operations.go
@@ -0,0 +1,2742 @@
+// Package operations does generic operations on filesystems and objects
+package operations
+
+import (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "encoding/csv"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "mime"
+ "net/http"
+ "os"
+ "path"
+ "path/filepath"
+ "runtime"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/rclone/rclone/fs"
+ "github.com/rclone/rclone/fs/accounting"
+ "github.com/rclone/rclone/fs/cache"
+ "github.com/rclone/rclone/fs/config"
+ "github.com/rclone/rclone/fs/filter"
+ "github.com/rclone/rclone/fs/fserrors"
+ "github.com/rclone/rclone/fs/fshttp"
+ "github.com/rclone/rclone/fs/hash"
+ "github.com/rclone/rclone/fs/object"
+ "github.com/rclone/rclone/fs/walk"
+ "github.com/rclone/rclone/lib/atexit"
+ "github.com/rclone/rclone/lib/errcount"
+ "github.com/rclone/rclone/lib/pacer"
+ "github.com/rclone/rclone/lib/random"
+ "github.com/rclone/rclone/lib/readers"
+ "github.com/rclone/rclone/lib/transform"
+ "golang.org/x/sync/errgroup"
+ "golang.org/x/text/unicode/norm"
+)
+
+// CheckHashes checks the two files to see if they have common
+// known hash types and compares them
+//
+// Returns.
+//
+// equal - which is equality of the hashes
+//
+// hash - the HashType. This is HashNone if either of the hashes were
+// unset or a compatible hash couldn't be found.
+//
+// err - may return an error which will already have been logged
+//
+// If an error is returned it will return equal as false
+func CheckHashes(ctx context.Context, src fs.ObjectInfo, dst fs.Object) (equal bool, ht hash.Type, err error) {
+ common := src.Fs().Hashes().Overlap(dst.Fs().Hashes())
+ // fs.Debugf(nil, "Shared hashes: %v", common)
+ if common.Count() == 0 {
+ return true, hash.None, nil
+ }
+ equal, ht, _, _, err = checkHashes(ctx, src, dst, common.GetOne())
+ return equal, ht, err
+}
+
+var errNoHash = errors.New("no hash available")
+
+// checkHashes does the work of CheckHashes but takes a hash.Type and
+// returns the effective hash type used.
+func checkHashes(ctx context.Context, src fs.ObjectInfo, dst fs.Object, ht hash.Type) (equal bool, htOut hash.Type, srcHash, dstHash string, err error) {
+ // Calculate hashes in parallel
+ g, ctx := errgroup.WithContext(ctx)
+ var srcErr, dstErr error
+ g.Go(func() (err error) {
+ srcHash, srcErr = src.Hash(ctx, ht)
+ if srcErr != nil {
+ return srcErr
+ }
+ if srcHash == "" {
+ fs.Debugf(src, "Src hash empty - aborting Dst hash check")
+ return errNoHash
+ }
+ return nil
+ })
+ g.Go(func() (err error) {
+ dstHash, dstErr = dst.Hash(ctx, ht)
+ if dstErr != nil {
+ return dstErr
+ }
+ if dstHash == "" {
+ fs.Debugf(dst, "Dst hash empty - aborting Src hash check")
+ return errNoHash
+ }
+ return nil
+ })
+ err = g.Wait()
+ if err == errNoHash {
+ return true, hash.None, srcHash, dstHash, nil
+ }
+ if srcErr != nil {
+ err = fs.CountError(ctx, srcErr)
+ fs.Errorf(src, "Failed to calculate src hash: %v", err)
+ }
+ if dstErr != nil {
+ err = fs.CountError(ctx, dstErr)
+ fs.Errorf(dst, "Failed to calculate dst hash: %v", err)
+ }
+ if err != nil {
+ return false, ht, srcHash, dstHash, err
+ }
+ if srcHash != dstHash {
+ fs.Debugf(src, "%v = %s (%v)", ht, srcHash, src.Fs())
+ fs.Debugf(dst, "%v = %s (%v)", ht, dstHash, dst.Fs())
+ return false, ht, srcHash, dstHash, nil
+ }
+ fs.Debugf(src, "%v = %s OK", ht, srcHash)
+ return true, ht, srcHash, dstHash, nil
+}
+
+// Equal checks to see if the src and dst objects are equal by looking at
+// size, mtime and hash
+//
+// If the src and dst size are different then it is considered to be
+// not equal. If --size-only is in effect then this is the only check
+// that is done. If --ignore-size is in effect then this check is
+// skipped and the files are considered the same size.
+//
+// If the size is the same and the mtime is the same then it is
+// considered to be equal. This check is skipped if using --checksum.
+//
+// If the size is the same and mtime is different, unreadable or
+// --checksum is set and the hash is the same then the file is
+// considered to be equal. In this case the mtime on the dst is
+// updated if --checksum is not set.
+//
+// Otherwise the file is considered to be not equal including if there
+// were errors reading info.
+func Equal(ctx context.Context, src fs.ObjectInfo, dst fs.Object) bool {
+ return equal(ctx, src, dst, defaultEqualOpt(ctx))
+}
+
+// DirsEqual is like Equal but for dirs instead of objects.
+// It returns true if two dirs should be considered "equal" for the purposes of syncCopyMove
+// (in other words, true == "skip updating modtime/metadata for this dir".)
+// Unlike Equal, it does not consider size or checksum, as these do not apply to directories.
+func DirsEqual(ctx context.Context, src, dst fs.Directory, opt DirsEqualOpt) (equal bool) {
+ if dst == nil {
+ return false
+ }
+ ci := fs.GetConfig(ctx)
+ if ci.SizeOnly || ci.Immutable || ci.IgnoreExisting || opt.ModifyWindow == fs.ModTimeNotSupported {
+ return true
+ }
+ if ci.IgnoreTimes {
+ return false
+ }
+ if !(opt.SetDirModtime || opt.SetDirMetadata) {
+ return true
+ }
+ srcModTime, dstModTime := src.ModTime(ctx), dst.ModTime(ctx)
+ if srcModTime.IsZero() || dstModTime.IsZero() {
+ return false
+ }
+ dt := dstModTime.Sub(srcModTime)
+ if dt < opt.ModifyWindow && dt > -opt.ModifyWindow {
+ fs.Debugf(dst, "Directory modification time the same (differ by %s, within tolerance %s)", dt, opt.ModifyWindow)
+ return true
+ }
+ if ci.UpdateOlder && dt >= opt.ModifyWindow {
+ fs.Debugf(dst, "Destination directory is newer than source, skipping")
+ return true
+ }
+ return false
+}
+
+// sizeDiffers compare the size of src and dst taking into account the
+// various ways of ignoring sizes
+func sizeDiffers(ctx context.Context, src, dst fs.ObjectInfo) bool {
+ ci := fs.GetConfig(ctx)
+ if ci.IgnoreSize || src.Size() < 0 || dst.Size() < 0 {
+ return false
+ }
+ if src.Size() == dst.Size() {
+ fs.Debugf(dst, "size = %d OK", dst.Size())
+ return false
+ }
+ fs.Debugf(src, "size = %d (%v)", src.Size(), src.Fs())
+ fs.Debugf(dst, "size = %d (%v)", dst.Size(), dst.Fs())
+ return true
+}
+
+var checksumWarning sync.Once
+
+// options for equal function()
+type equalOpt struct {
+ sizeOnly bool // if set only check size
+ checkSum bool // if set check checksum+size instead of modtime+size
+ updateModTime bool // if set update the modtime if hashes identical and checking with modtime+size
+ forceModTimeMatch bool // if set assume modtimes match
+}
+
+// default set of options for equal()
+func defaultEqualOpt(ctx context.Context) equalOpt {
+ ci := fs.GetConfig(ctx)
+ return equalOpt{
+ sizeOnly: ci.SizeOnly,
+ checkSum: ci.CheckSum,
+ updateModTime: !ci.NoUpdateModTime,
+ forceModTimeMatch: false,
+ }
+}
+
+// DirsEqualOpt represents options for DirsEqual function()
+type DirsEqualOpt struct {
+ ModifyWindow time.Duration // Max time diff to be considered the same
+ SetDirModtime bool // whether to consider dir modtime
+ SetDirMetadata bool // whether to consider dir metadata
+}
+
+var modTimeUploadOnce sync.Once
+
+// emit a log if we are about to upload a file to set its modification time
+func logModTimeUpload(dst fs.Object) {
+ modTimeUploadOnce.Do(func() {
+ fs.Logf(dst.Fs(), "Forced to upload files to set modification times on this backend.")
+ })
+}
+
+// EqualFn allows replacing Equal() with a custom function during NeedTransfer()
+type (
+ EqualFn func(ctx context.Context, src fs.ObjectInfo, dst fs.Object) bool
+ equalFnContextKey struct{}
+)
+
+var equalFnKey = equalFnContextKey{}
+
+// WithEqualFn stores equalFn in ctx and returns a copy of ctx in which equalFnKey = equalFn
+func WithEqualFn(ctx context.Context, equalFn EqualFn) context.Context {
+ return context.WithValue(ctx, equalFnKey, equalFn)
+}
+
+func equal(ctx context.Context, src fs.ObjectInfo, dst fs.Object, opt equalOpt) bool {
+ ci := fs.GetConfig(ctx)
+ logger, _ := GetLogger(ctx)
+ if sizeDiffers(ctx, src, dst) {
+ fs.Debug(src, "Sizes differ")
+ logger(ctx, Differ, src, dst, nil)
+ return false
+ }
+ if opt.sizeOnly {
+ fs.Debugf(src, "Sizes identical")
+ logger(ctx, Match, src, dst, nil)
+ return true
+ }
+
+ // Assert: Size is equal or being ignored
+
+ // If checking checksum and not modtime
+ if opt.checkSum {
+ // Check the hash
+ same, ht, _ := CheckHashes(ctx, src, dst)
+ if !same {
+ fs.Debugf(src, "%v differ", ht)
+ logger(ctx, Differ, src, dst, nil)
+ return false
+ }
+ if ht == hash.None {
+ common := src.Fs().Hashes().Overlap(dst.Fs().Hashes())
+ if common.Count() == 0 {
+ checksumWarning.Do(func() {
+ fs.Logf(dst.Fs(), "--checksum is in use but the source and destination have no hashes in common; falling back to --size-only")
+ })
+ }
+ fs.Debugf(src, "Size of src and dst objects identical")
+ } else {
+ fs.Debugf(src, "Size and %v of src and dst objects identical", ht)
+ }
+ logger(ctx, Match, src, dst, nil)
+ return true
+ }
+
+ srcModTime := src.ModTime(ctx)
+ if !opt.forceModTimeMatch {
+ // Sizes the same so check the mtime
+ modifyWindow := fs.GetModifyWindow(ctx, src.Fs(), dst.Fs())
+ if modifyWindow == fs.ModTimeNotSupported {
+ fs.Debugf(src, "Sizes identical")
+ logger(ctx, Match, src, dst, nil)
+ return true
+ }
+ dstModTime := dst.ModTime(ctx)
+ dt := dstModTime.Sub(srcModTime)
+ if dt < modifyWindow && dt > -modifyWindow {
+ fs.Debugf(src, "Size and modification time the same (differ by %s, within tolerance %s)", dt, modifyWindow)
+ logger(ctx, Match, src, dst, nil)
+ return true
+ }
+
+ fs.Debugf(src, "Modification times differ by %s: %v, %v", dt, srcModTime, dstModTime)
+ }
+
+ // Check if the hashes are the same
+ same, ht, _ := CheckHashes(ctx, src, dst)
+ if !same {
+ fs.Debugf(src, "%v differ", ht)
+ logger(ctx, Differ, src, dst, nil)
+ return false
+ }
+ if ht == hash.None && !ci.RefreshTimes {
+ // if couldn't check hash, return that they differ
+ logger(ctx, Differ, src, dst, nil)
+ return false
+ }
+
+ // mod time differs but hash is the same to reset mod time if required
+ if opt.updateModTime {
+ if !SkipDestructive(ctx, src, "update modification time") {
+ // Size and hash the same but mtime different
+ // Error if objects are treated as immutable
+ if ci.Immutable {
+ fs.Errorf(dst, "Timestamp mismatch between immutable objects")
+ logger(ctx, Differ, src, dst, nil)
+ return false
+ }
+ // Update the mtime of the dst object here
+ err := dst.SetModTime(ctx, srcModTime)
+ if errors.Is(err, fs.ErrorCantSetModTime) {
+ logModTimeUpload(dst)
+ fs.Infof(dst, "src and dst identical but can't set mod time without re-uploading")
+ logger(ctx, Differ, src, dst, nil)
+ return false
+ } else if errors.Is(err, fs.ErrorCantSetModTimeWithoutDelete) {
+ logModTimeUpload(dst)
+ fs.Infof(dst, "src and dst identical but can't set mod time without deleting and re-uploading")
+ // Remove the file if BackupDir isn't set. If BackupDir is set we would rather have the old file
+ // put in the BackupDir than deleted which is what will happen if we don't delete it.
+ if ci.BackupDir == "" {
+ err = dst.Remove(ctx)
+ if err != nil {
+ fs.Errorf(dst, "failed to delete before re-upload: %v", err)
+ }
+ }
+ logger(ctx, Differ, src, dst, nil)
+ return false
+ } else if err != nil {
+ err = fs.CountError(ctx, err)
+ fs.Errorf(dst, "Failed to set modification time: %v", err)
+ } else {
+ fs.Infof(src, "Updated modification time in destination")
+ }
+ }
+ }
+ logger(ctx, Match, src, dst, nil)
+ return true
+}
+
+// CommonHash returns a single hash.Type and a HashOption with that
+// type which is in common between the two fs.Fs.
+func CommonHash(ctx context.Context, fa, fb fs.Info) (hash.Type, *fs.HashesOption) {
+ ci := fs.GetConfig(ctx)
+ // work out which hash to use - limit to 1 hash in common
+ var common hash.Set
+ hashType := hash.None
+ if !ci.IgnoreChecksum {
+ common = fb.Hashes().Overlap(fa.Hashes())
+ if common.Count() > 0 {
+ hashType = common.GetOne()
+ common = hash.Set(hashType)
+ }
+ }
+ return hashType, &fs.HashesOption{Hashes: common}
+}
+
+// SameObject returns true if src and dst could be pointing to the
+// same object.
+func SameObject(src, dst fs.Object) bool {
+ srcFs, dstFs := src.Fs(), dst.Fs()
+ if !SameConfig(srcFs, dstFs) {
+ // If same remote type then check ID of objects if available
+ doSrcID, srcIDOK := src.(fs.IDer)
+ doDstID, dstIDOK := dst.(fs.IDer)
+ if srcIDOK && dstIDOK && SameRemoteType(srcFs, dstFs) {
+ srcID, dstID := doSrcID.ID(), doDstID.ID()
+ if srcID != "" && srcID == dstID {
+ return true
+ }
+ }
+ return false
+ }
+ srcPath := path.Join(srcFs.Root(), src.Remote())
+ dstPath := path.Join(dstFs.Root(), dst.Remote())
+ if srcFs.Features().IsLocal && dstFs.Features().IsLocal && runtime.GOOS == "darwin" {
+ if norm.NFC.String(srcPath) == norm.NFC.String(dstPath) {
+ return true
+ }
+ }
+ if dst.Fs().Features().CaseInsensitive {
+ srcPath = strings.ToLower(srcPath)
+ dstPath = strings.ToLower(dstPath)
+ }
+ return srcPath == dstPath
+}
+
+// Move src object to dst or fdst if nil. If dst is nil then it uses
+// remote as the name of the new object.
+//
+// Note that you must check the destination does not exist before
+// calling this and pass it as dst. If you pass dst=nil and the
+// destination does exist then this may create duplicates or return
+// errors.
+//
+// It returns the destination object if possible. Note that this may
+// be nil.
+//
+// This is accounted as a check.
+func Move(ctx context.Context, fdst fs.Fs, dst fs.Object, remote string, src fs.Object) (newDst fs.Object, err error) {
+ return move(ctx, fdst, dst, remote, src, false)
+}
+
+// MoveTransfer moves src object to dst or fdst if nil. If dst is nil
+// then it uses remote as the name of the new object.
+//
+// This is identical to Move but is accounted as a transfer.
+func MoveTransfer(ctx context.Context, fdst fs.Fs, dst fs.Object, remote string, src fs.Object) (newDst fs.Object, err error) {
+ return move(ctx, fdst, dst, remote, src, true)
+}
+
+// move - see Move for help
+func move(ctx context.Context, fdst fs.Fs, dst fs.Object, remote string, src fs.Object, isTransfer bool) (newDst fs.Object, err error) {
+ origRemote := remote // avoid double-transform on fallback to copy
+ remote = transform.Path(ctx, remote, false)
+ ci := fs.GetConfig(ctx)
+ newDst = dst
+ if ci.DryRun && dst != nil && SameObject(src, dst) && src.Remote() == transform.Path(ctx, dst.Remote(), false) {
+ return // avoid SkipDestructive log for objects that won't really be moved
+ }
+ var tr *accounting.Transfer
+ if isTransfer {
+ tr = accounting.Stats(ctx).NewTransfer(src, fdst)
+ } else {
+ tr = accounting.Stats(ctx).NewCheckingTransfer(src, "moving")
+ }
+ defer func() {
+ if err == nil {
+ accounting.Stats(ctx).Renames(1)
+ }
+ tr.Done(ctx, err)
+ }()
+ action := "move"
+ if remote != src.Remote() {
+ action += " to " + remote
+ }
+ if SkipDestructive(ctx, src, action) {
+ in := tr.Account(ctx, nil)
+ in.DryRun(src.Size())
+ return newDst, nil
+ }
+ // See if we have Move available
+ if doMove := fdst.Features().Move; doMove != nil && (SameConfig(src.Fs(), fdst) || (SameRemoteType(src.Fs(), fdst) && (fdst.Features().ServerSideAcrossConfigs || ci.ServerSideAcrossConfigs))) {
+ // Delete destination if it exists and is not the same file as src (could be same file while seemingly different if the remote is case insensitive)
+ if dst != nil {
+ remote = transform.Path(ctx, dst.Remote(), false)
+ if !SameObject(src, dst) {
+ err = DeleteFile(ctx, dst)
+ if err != nil {
+ return newDst, err
+ }
+ } else if src.Remote() == remote {
+ return newDst, nil
+ } else if needsMoveCaseInsensitive(fdst, fdst, remote, src.Remote(), false) {
+ doMove = func(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
+ return MoveCaseInsensitive(ctx, fdst, fdst, remote, src.Remote(), false, src)
+ }
+ }
+ } else if needsMoveCaseInsensitive(fdst, fdst, remote, src.Remote(), false) {
+ doMove = func(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
+ return MoveCaseInsensitive(ctx, fdst, fdst, remote, src.Remote(), false, src)
+ }
+ }
+ // Move dst <- src
+ in := tr.Account(ctx, nil) // account the transfer
+ in.ServerSideTransferStart()
+ newDst, err = doMove(ctx, src, remote)
+ switch err {
+ case nil:
+ if newDst != nil && src.String() != newDst.String() {
+ fs.Infof(src, "Moved (server-side) to: %s", newDst.String())
+ } else {
+ fs.Infof(src, "Moved (server-side)")
+ }
+ in.ServerSideMoveEnd(newDst.Size()) // account the bytes for the server-side transfer
+ _ = in.Close()
+ return newDst, nil
+ case fs.ErrorCantMove:
+ fs.Debugf(src, "Can't move, switching to copy")
+ _ = in.Close()
+ default:
+ err = fs.CountError(ctx, err)
+ fs.Errorf(src, "Couldn't move: %v", err)
+ _ = in.Close()
+ return newDst, err
+ }
+ }
+ // Move not found or didn't work so copy dst <- src
+ if origRemote != remote {
+ dst = nil
+ }
+ newDst, err = Copy(ctx, fdst, dst, origRemote, src)
+ if err != nil {
+ fs.Errorf(src, "Not deleting source as copy failed: %v", err)
+ return newDst, err
+ }
+ // Delete src if no error on copy
+ return newDst, DeleteFile(ctx, src)
+}
+
+// CanServerSideMove returns true if fdst support server-side moves or
+// server-side copies
+//
+// Some remotes simulate rename by server-side copy and delete, so include
+// remotes that implements either Mover or Copier.
+func CanServerSideMove(fdst fs.Fs) bool {
+ canMove := fdst.Features().Move != nil
+ canCopy := fdst.Features().Copy != nil
+ return canMove || canCopy
+}
+
+// SuffixName adds the current --suffix to the remote, obeying
+// --suffix-keep-extension if set
+func SuffixName(ctx context.Context, remote string) string {
+ ci := fs.GetConfig(ctx)
+ if ci.Suffix == "" {
+ return remote
+ }
+ if ci.SuffixKeepExtension {
+ return transform.SuffixKeepExtension(remote, ci.Suffix)
+ }
+ return remote + ci.Suffix
+}
+
+// DeleteFileWithBackupDir deletes a single file respecting --dry-run
+// and accumulating stats and errors.
+//
+// If backupDir is set then it moves the file to there instead of
+// deleting
+func DeleteFileWithBackupDir(ctx context.Context, dst fs.Object, backupDir fs.Fs) (err error) {
+ tr := accounting.Stats(ctx).NewCheckingTransfer(dst, "deleting")
+ defer func() {
+ tr.Done(ctx, err)
+ }()
+ err = accounting.Stats(ctx).DeleteFile(ctx, dst.Size())
+ if err != nil {
+ return err
+ }
+ action, actioned := "delete", "Deleted"
+ if backupDir != nil {
+ action, actioned = "move into backup dir", "Moved into backup dir"
+ }
+ skip := SkipDestructive(ctx, dst, action)
+ if skip {
+ // do nothing
+ } else if backupDir != nil {
+ err = MoveBackupDir(ctx, backupDir, dst)
+ } else {
+ err = dst.Remove(ctx)
+ }
+ if err != nil {
+ fs.Errorf(dst, "Couldn't %s: %v", action, err)
+ err = fs.CountError(ctx, err)
+ } else if !skip {
+ fs.Infof(dst, "%s", actioned)
+ }
+ return err
+}
+
+// DeleteFile deletes a single file respecting --dry-run and accumulating stats and errors.
+//
+// If useBackupDir is set and --backup-dir is in effect then it moves
+// the file to there instead of deleting
+func DeleteFile(ctx context.Context, dst fs.Object) (err error) {
+ return DeleteFileWithBackupDir(ctx, dst, nil)
+}
+
+// DeleteFilesWithBackupDir removes all the files passed in the
+// channel
+//
+// If backupDir is set the files will be placed into that directory
+// instead of being deleted.
+func DeleteFilesWithBackupDir(ctx context.Context, toBeDeleted fs.ObjectsChan, backupDir fs.Fs) error {
+ var wg sync.WaitGroup
+ ci := fs.GetConfig(ctx)
+ wg.Add(ci.Checkers)
+ var errorCount atomic.Int32
+ var fatalErrorCount atomic.Int32
+
+ for range ci.Checkers {
+ go func() {
+ defer wg.Done()
+ for dst := range toBeDeleted {
+ err := DeleteFileWithBackupDir(ctx, dst, backupDir)
+ if err != nil {
+ errorCount.Add(1)
+ logger, _ := GetLogger(ctx)
+ logger(ctx, TransferError, nil, dst, err)
+ if fserrors.IsFatalError(err) {
+ fs.Errorf(dst, "Got fatal error on delete: %s", err)
+ fatalErrorCount.Add(1)
+ return
+ }
+ }
+ }
+ }()
+ }
+ fs.Debugf(nil, "Waiting for deletions to finish")
+ wg.Wait()
+ if errorCount.Load() > 0 {
+ err := fmt.Errorf("failed to delete %d files", errorCount.Load())
+ if fatalErrorCount.Load() > 0 {
+ return fserrors.FatalError(err)
+ }
+ return err
+ }
+ return nil
+}
+
+// DeleteFiles removes all the files passed in the channel
+func DeleteFiles(ctx context.Context, toBeDeleted fs.ObjectsChan) error {
+ return DeleteFilesWithBackupDir(ctx, toBeDeleted, nil)
+}
+
+// ReadFile reads the object into memory and accounts it
+func ReadFile(ctx context.Context, o fs.Object) (b []byte, err error) {
+ tr := accounting.Stats(ctx).NewTransfer(o, nil)
+ defer func() {
+ tr.Done(ctx, err)
+ }()
+ in0, err := Open(ctx, o)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open %v: %w", o, err)
+ }
+ in := tr.Account(ctx, in0).WithBuffer() // account and buffer the transfer
+ defer fs.CheckClose(in, &err) // closes in0 also
+ b, err = io.ReadAll(in)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read %v: %w", o, err)
+ }
+ return b, nil
+}
+
+// SameRemoteType returns true if fdst and fsrc are the same type
+func SameRemoteType(fdst, fsrc fs.Info) bool {
+ return fmt.Sprintf("%T", fdst) == fmt.Sprintf("%T", fsrc)
+}
+
+// SameConfig returns true if fdst and fsrc are using the same config
+// file entry
+func SameConfig(fdst, fsrc fs.Info) bool {
+ return fdst.Name() == fsrc.Name()
+}
+
+// SameConfigArr returns true if any of []fsrcs has same config file entry with fdst
+func SameConfigArr(fdst fs.Info, fsrcs []fs.Fs) bool {
+ for _, fsrc := range fsrcs {
+ if fdst.Name() == fsrc.Name() {
+ return true
+ }
+ }
+ return false
+}
+
+// Same returns true if fdst and fsrc point to the same underlying Fs
+func Same(fdst, fsrc fs.Info) bool {
+ return SameConfig(fdst, fsrc) && strings.Trim(fdst.Root(), "/") == strings.Trim(fsrc.Root(), "/")
+}
+
+// fixRoot returns the Root with a trailing / if not empty.
+//
+// It returns a case folded version for case insensitive file systems
+func fixRoot(f fs.Info) (s string, folded string) {
+ s = strings.Trim(filepath.ToSlash(f.Root()), "/")
+ if s != "" {
+ s += "/"
+ }
+ folded = s
+ if f.Features().CaseInsensitive {
+ folded = strings.ToLower(s)
+ }
+ return s, folded
+}
+
+// OverlappingFilterCheck returns true if fdst and fsrc point to the same
+// underlying Fs and they overlap without fdst being excluded by any filter rule.
+func OverlappingFilterCheck(ctx context.Context, fdst fs.Fs, fsrc fs.Fs) bool {
+ if !SameConfig(fdst, fsrc) {
+ return false
+ }
+ fdstRoot, fdstRootFolded := fixRoot(fdst)
+ fsrcRoot, fsrcRootFolded := fixRoot(fsrc)
+ if fdstRootFolded == fsrcRootFolded {
+ return true
+ } else if strings.HasPrefix(fdstRootFolded, fsrcRootFolded) {
+ fdstRelative := fdstRoot[len(fsrcRoot):]
+ return filterCheck(ctx, fsrc, fdstRelative)
+ } else if strings.HasPrefix(fsrcRootFolded, fdstRootFolded) {
+ fsrcRelative := fsrcRoot[len(fdstRoot):]
+ return filterCheck(ctx, fdst, fsrcRelative)
+ }
+ return false
+}
+
+// filterCheck checks if dir is included in f
+func filterCheck(ctx context.Context, f fs.Fs, dir string) bool {
+ fi := filter.GetConfig(ctx)
+ includeDirectory := fi.IncludeDirectory(ctx, f)
+ include, err := includeDirectory(dir)
+ if err != nil {
+ fs.Errorf(f, "Failed to discover whether directory is included: %v", err)
+ return true
+ }
+ return include
+}
+
+// SameDir returns true if fdst and fsrc point to the same
+// underlying Fs and they are the same directory.
+func SameDir(fdst, fsrc fs.Info) bool {
+ if !SameConfig(fdst, fsrc) {
+ return false
+ }
+ _, fdstRootFolded := fixRoot(fdst)
+ _, fsrcRootFolded := fixRoot(fsrc)
+ return fdstRootFolded == fsrcRootFolded
+}
+
+// Retry runs fn up to maxTries times if it returns a retriable error
+func Retry(ctx context.Context, o any, maxTries int, fn func() error) (err error) {
+ for tries := 1; tries <= maxTries; tries++ {
+ // Call the function which might error
+ err = fn()
+ if err == nil {
+ break
+ }
+ // End if ctx is in error
+ if fserrors.ContextError(ctx, &err) {
+ break
+ }
+ // Retry if err returned a retry error
+ if fserrors.IsRetryError(err) || fserrors.ShouldRetry(err) {
+ fs.Debugf(o, "Received error: %v - low level retry %d/%d", err, tries, maxTries)
+ continue
+ } else if t, ok := pacer.IsRetryAfter(err); ok {
+ fs.Debugf(o, "Sleeping for %v (as indicated by the server) to obey Retry-After error: %v", t, err)
+ time.Sleep(t)
+ continue
+ }
+ break
+ }
+ return err
+}
+
+// ListFn lists the Fs to the supplied function
+//
+// Lists in parallel which may get them out of order
+func ListFn(ctx context.Context, f fs.Fs, fn func(fs.Object)) error {
+ ci := fs.GetConfig(ctx)
+ return walk.ListR(ctx, f, "", false, ci.MaxDepth, walk.ListObjects, func(entries fs.DirEntries) error {
+ entries.ForObject(fn)
+ return nil
+ })
+}
+
+// StdoutMutex mutex for synchronized output on stdout
+var StdoutMutex sync.Mutex
+
+// SyncPrintf is a global var holding the Printf function so that it
+// can be overridden.
+//
+// This writes to stdout holding the StdoutMutex. If you are going to
+// override it and write to os.Stdout then you should hold the
+// StdoutMutex too.
+var SyncPrintf = func(format string, a ...any) {
+ StdoutMutex.Lock()
+ defer StdoutMutex.Unlock()
+ fmt.Printf(format, a...)
+}
+
+// SyncFprintf - Synchronized fmt.Fprintf
+//
+// Ignores errors from Fprintf.
+//
+// Prints to stdout if w is nil
+func SyncFprintf(w io.Writer, format string, a ...any) {
+ if w == nil || w == os.Stdout {
+ SyncPrintf(format, a...)
+ } else {
+ StdoutMutex.Lock()
+ defer StdoutMutex.Unlock()
+ _, _ = fmt.Fprintf(w, format, a...)
+ }
+}
+
+// SizeString make string representation of size for output
+//
+// Optional human-readable format including a binary suffix
+func SizeString(size int64, humanReadable bool) string {
+ if humanReadable {
+ if size < 0 {
+ return "-" + fs.SizeSuffix(-size).String()
+ }
+ return fs.SizeSuffix(size).String()
+ }
+ return strconv.FormatInt(size, 10)
+}
+
+// SizeStringField make string representation of size for output in fixed width field
+//
+// Optional human-readable format including a binary suffix
+// Argument rawWidth is used to format field with of raw value. When humanReadable
+// option the width is hard coded to 9, since SizeSuffix strings have precision 3
+// and longest value will be "999.999Ei". This way the width can be optimized
+// depending to the humanReadable option. To always use a longer width the return
+// value can always be fed into another format string with a specific field with.
+func SizeStringField(size int64, humanReadable bool, rawWidth int) string {
+ str := SizeString(size, humanReadable)
+ if humanReadable {
+ return fmt.Sprintf("%9s", str)
+ }
+ return fmt.Sprintf("%[2]*[1]s", str, rawWidth)
+}
+
+// CountString make string representation of count for output
+//
+// Optional human-readable format including a decimal suffix
+func CountString(count int64, humanReadable bool) string {
+ if humanReadable {
+ if count < 0 {
+ return "-" + fs.CountSuffix(-count).String()
+ }
+ return fs.CountSuffix(count).String()
+ }
+ return strconv.FormatInt(count, 10)
+}
+
+// CountStringField make string representation of count for output in fixed width field
+//
+// Similar to SizeStringField, but human readable with decimal prefix and field width 8
+// since there is no 'i' in the decimal prefix symbols (e.g. "999.999E")
+func CountStringField(count int64, humanReadable bool, rawWidth int) string {
+ str := CountString(count, humanReadable)
+ if humanReadable {
+ return fmt.Sprintf("%8s", str)
+ }
+ return fmt.Sprintf("%[2]*[1]s", str, rawWidth)
+}
+
+// List the Fs to the supplied writer
+//
+// Shows size and path - obeys includes and excludes.
+//
+// Lists in parallel which may get them out of order
+func List(ctx context.Context, f fs.Fs, w io.Writer) error {
+ ci := fs.GetConfig(ctx)
+ return ListFn(ctx, f, func(o fs.Object) {
+ SyncFprintf(w, "%s %s\n", SizeStringField(o.Size(), ci.HumanReadable, 9), o.Remote())
+ })
+}
+
+// ListLong lists the Fs to the supplied writer
+//
+// Shows size, mod time and path - obeys includes and excludes.
+//
+// Lists in parallel which may get them out of order
+func ListLong(ctx context.Context, f fs.Fs, w io.Writer) error {
+ ci := fs.GetConfig(ctx)
+ return ListFn(ctx, f, func(o fs.Object) {
+ tr := accounting.Stats(ctx).NewCheckingTransfer(o, "listing")
+ defer func() {
+ tr.Done(ctx, nil)
+ }()
+ modTime := o.ModTime(ctx)
+ SyncFprintf(w, "%s %s %s\n", SizeStringField(o.Size(), ci.HumanReadable, 9), modTime.Local().Format("2006-01-02 15:04:05.000000000"), o.Remote())
+ })
+}
+
+// HashSum returns the human-readable hash for ht passed in. This may
+// be UNSUPPORTED or ERROR. If it isn't returning a valid hash it will
+// return an error.
+func HashSum(ctx context.Context, ht hash.Type, base64Encoded bool, downloadFlag bool, o fs.Object) (string, error) {
+ var sum string
+ var err error
+
+ // If downloadFlag is true, download and hash the file.
+ // If downloadFlag is false, call o.Hash asking the remote for the hash
+ if downloadFlag {
+ // Setup: Define accounting, open the file with NewReOpen to provide restarts, account for the transfer, and setup a multi-hasher with the appropriate type
+ // Execution: io.Copy file to hasher, get hash and encode in hex
+
+ tr := accounting.Stats(ctx).NewTransfer(o, nil)
+ defer func() {
+ tr.Done(ctx, err)
+ }()
+
+ // Open with NewReOpen to provide restarts
+ var options []fs.OpenOption
+ for _, option := range fs.GetConfig(ctx).DownloadHeaders {
+ options = append(options, option)
+ }
+ var in io.ReadCloser
+ in, err = Open(ctx, o, options...)
+ if err != nil {
+ return "ERROR", fmt.Errorf("failed to open file %v: %w", o, err)
+ }
+
+ // Account and buffer the transfer
+ in = tr.Account(ctx, in).WithBuffer()
+
+ // Setup hasher
+ hasher, err := hash.NewMultiHasherTypes(hash.NewHashSet(ht))
+ if err != nil {
+ return "UNSUPPORTED", fmt.Errorf("hash unsupported: %w", err)
+ }
+
+ // Copy to hasher, downloading the file and passing directly to hash
+ _, err = io.Copy(hasher, in)
+ if err != nil {
+ return "ERROR", fmt.Errorf("failed to copy file to hasher: %w", err)
+ }
+
+ // Get hash as hex or base64 encoded string
+ sum, err = hasher.SumString(ht, base64Encoded)
+ if err != nil {
+ return "ERROR", fmt.Errorf("hasher returned an error: %w", err)
+ }
+ } else {
+ tr := accounting.Stats(ctx).NewCheckingTransfer(o, "hashing")
+ defer func() {
+ tr.Done(ctx, err)
+ }()
+
+ sum, err = o.Hash(ctx, ht)
+ if base64Encoded {
+ hexBytes, _ := hex.DecodeString(sum)
+ sum = base64.URLEncoding.EncodeToString(hexBytes)
+ }
+ if err == hash.ErrUnsupported {
+ return "", fmt.Errorf("hash unsupported: %w", err)
+ }
+ if err != nil {
+ return "", fmt.Errorf("failed to get hash %v from backend: %w", ht, err)
+ }
+ }
+
+ return sum, nil
+}
+
+// HashLister does an md5sum equivalent for the hash type passed in
+// Updated to handle both standard hex encoding and base64
+// Updated to perform multiple hashes concurrently
+func HashLister(ctx context.Context, ht hash.Type, outputBase64 bool, downloadFlag bool, f fs.Fs, w io.Writer) error {
+ width := hash.Width(ht, outputBase64)
+ // Use --checkers concurrency unless downloading in which case use --transfers
+ concurrency := fs.GetConfig(ctx).Checkers
+ if downloadFlag {
+ concurrency = fs.GetConfig(ctx).Transfers
+ }
+ concurrencyControl := make(chan struct{}, concurrency)
+ var wg sync.WaitGroup
+ err := ListFn(ctx, f, func(o fs.Object) {
+ wg.Add(1)
+ concurrencyControl <- struct{}{}
+ go func() {
+ defer func() {
+ <-concurrencyControl
+ wg.Done()
+ }()
+ sum, err := HashSum(ctx, ht, outputBase64, downloadFlag, o)
+ if err != nil {
+ fs.Errorf(o, "%v", fs.CountError(ctx, err))
+ return
+ }
+ SyncFprintf(w, "%*s %s\n", width, sum, o.Remote())
+ }()
+ })
+ wg.Wait()
+ return err
+}
+
+// HashSumStream outputs a line compatible with md5sum to w based on the
+// input stream in and the hash type ht passed in. If outputBase64 is
+// set then the hash will be base64 instead of hexadecimal.
+func HashSumStream(ht hash.Type, outputBase64 bool, in io.ReadCloser, w io.Writer) error {
+ hasher, err := hash.NewMultiHasherTypes(hash.NewHashSet(ht))
+ if err != nil {
+ return fmt.Errorf("hash unsupported: %w", err)
+ }
+ written, err := io.Copy(hasher, in)
+ fs.Debugf(nil, "Creating %s hash of %d bytes read from input stream", ht, written)
+ if err != nil {
+ return fmt.Errorf("failed to copy input to hasher: %w", err)
+ }
+ sum, err := hasher.SumString(ht, outputBase64)
+ if err != nil {
+ return fmt.Errorf("hasher returned an error: %w", err)
+ }
+ width := hash.Width(ht, outputBase64)
+ SyncFprintf(w, "%*s -\n", width, sum)
+ return nil
+}
+
+// Count counts the objects and their sizes in the Fs
+//
+// Obeys includes and excludes
+func Count(ctx context.Context, f fs.Fs) (objects int64, size int64, sizelessObjects int64, err error) {
+ err = ListFn(ctx, f, func(o fs.Object) {
+ atomic.AddInt64(&objects, 1)
+ objectSize := o.Size()
+ if objectSize < 0 {
+ atomic.AddInt64(&sizelessObjects, 1)
+ } else if objectSize > 0 {
+ atomic.AddInt64(&size, objectSize)
+ }
+ })
+ return
+}
+
+// ConfigMaxDepth returns the depth to use for a recursive or non recursive listing.
+func ConfigMaxDepth(ctx context.Context, recursive bool) int {
+ ci := fs.GetConfig(ctx)
+ depth := ci.MaxDepth
+ if !recursive && depth < 0 {
+ depth = 1
+ }
+ return depth
+}
+
+// ListDir lists the directories/buckets/containers in the Fs to the supplied writer
+func ListDir(ctx context.Context, f fs.Fs, w io.Writer) error {
+ ci := fs.GetConfig(ctx)
+ return walk.ListR(ctx, f, "", false, ConfigMaxDepth(ctx, false), walk.ListDirs, func(entries fs.DirEntries) error {
+ entries.ForDir(func(dir fs.Directory) {
+ if dir != nil {
+ SyncFprintf(w, "%s %13s %s %s\n", SizeStringField(dir.Size(), ci.HumanReadable, 12), dir.ModTime(ctx).Local().Format("2006-01-02 15:04:05"), CountStringField(dir.Items(), ci.HumanReadable, 9), dir.Remote())
+ }
+ })
+ return nil
+ })
+}
+
+// Mkdir makes a destination directory or container
+func Mkdir(ctx context.Context, f fs.Fs, dir string) error {
+ if SkipDestructive(ctx, fs.LogDirName(f, dir), "make directory") {
+ return nil
+ }
+ fs.Infof(fs.LogDirName(f, dir), "Making directory")
+ err := f.Mkdir(ctx, dir)
+ if err != nil {
+ err = fs.CountError(ctx, err)
+ return err
+ }
+ return nil
+}
+
+// MkdirMetadata makes a destination directory or container with metadata
+//
+// If the destination Fs doesn't support this it will fall back to
+// Mkdir and in this case newDst will be nil.
+func MkdirMetadata(ctx context.Context, f fs.Fs, dir string, metadata fs.Metadata) (newDst fs.Directory, err error) {
+ do := f.Features().MkdirMetadata
+ if do == nil {
+ return nil, Mkdir(ctx, f, dir)
+ }
+ logName := fs.LogDirName(f, dir)
+ if SkipDestructive(ctx, logName, "make directory") {
+ return nil, nil
+ }
+ fs.Debugf(fs.LogDirName(f, dir), "Making directory with metadata")
+ newDst, err = do(ctx, dir, metadata)
+ if err != nil {
+ err = fs.CountError(ctx, err)
+ return nil, err
+ }
+ if mtime, ok := metadata["mtime"]; ok {
+ fs.Infof(logName, "Made directory with metadata (mtime=%s)", mtime)
+ } else {
+ fs.Infof(logName, "Made directory with metadata")
+ }
+ return newDst, err
+}
+
+// MkdirModTime makes a destination directory or container with modtime
+//
+// It will try to make the directory with MkdirMetadata and if that
+// succeeds it will return a non-nil newDst. In all other cases newDst
+// will be nil.
+//
+// If the directory was created with MkDir then it will attempt to use
+// Fs.DirSetModTime to update the directory modtime if available.
+func MkdirModTime(ctx context.Context, f fs.Fs, dir string, modTime time.Time) (newDst fs.Directory, err error) {
+ logName := fs.LogDirName(f, dir)
+ if SkipDestructive(ctx, logName, "make directory") {
+ return nil, nil
+ }
+ metadata := fs.Metadata{
+ "mtime": modTime.Format(time.RFC3339Nano),
+ }
+ newDst, err = MkdirMetadata(ctx, f, dir, metadata)
+ if err != nil {
+ return nil, err
+ }
+ if newDst != nil {
+ // The directory was created and we have logged already
+ return newDst, nil
+ }
+ // The directory was created with Mkdir then we should try to set the time
+ if do := f.Features().DirSetModTime; do != nil {
+ err = do(ctx, dir, modTime)
+ }
+ fs.Infof(logName, "Made directory with modification time %v", modTime)
+ return newDst, err
+}
+
+// TryRmdir removes a container but not if not empty. It doesn't
+// count errors but may return one.
+func TryRmdir(ctx context.Context, f fs.Fs, dir string) error {
+ accounting.Stats(ctx).DeletedDirs(1)
+ if SkipDestructive(ctx, fs.LogDirName(f, dir), "remove directory") {
+ return nil
+ }
+ fs.Infof(fs.LogDirName(f, dir), "Removing directory")
+ return f.Rmdir(ctx, dir)
+}
+
+// Rmdir removes a container but not if not empty
+func Rmdir(ctx context.Context, f fs.Fs, dir string) error {
+ err := TryRmdir(ctx, f, dir)
+ if err != nil {
+ err = fs.CountError(ctx, err)
+ return err
+ }
+ return err
+}
+
+// Purge removes a directory and all of its contents
+func Purge(ctx context.Context, f fs.Fs, dir string) (err error) {
+ doFallbackPurge := true
+ if doPurge := f.Features().Purge; doPurge != nil {
+ doFallbackPurge = false
+ accounting.Stats(ctx).DeletedDirs(1)
+ if SkipDestructive(ctx, fs.LogDirName(f, dir), "purge directory") {
+ return nil
+ }
+ err = doPurge(ctx, dir)
+ if errors.Is(err, fs.ErrorCantPurge) {
+ doFallbackPurge = true
+ }
+ }
+ if doFallbackPurge {
+ // DeleteFiles and Rmdir observe --dry-run
+ err = DeleteFiles(ctx, listToChan(ctx, f, dir))
+ if err != nil {
+ return err
+ }
+ err = Rmdirs(ctx, f, dir, false)
+ }
+ if err != nil {
+ err = fs.CountError(ctx, err)
+ return err
+ }
+ return nil
+}
+
+// Delete removes all the contents of a container. Unlike Purge, it
+// obeys includes and excludes.
+func Delete(ctx context.Context, f fs.Fs) error {
+ ci := fs.GetConfig(ctx)
+ delChan := make(fs.ObjectsChan, ci.Checkers)
+ delErr := make(chan error, 1)
+ go func() {
+ delErr <- DeleteFiles(ctx, delChan)
+ }()
+ err := ListFn(ctx, f, func(o fs.Object) {
+ delChan <- o
+ })
+ close(delChan)
+ delError := <-delErr
+ if err == nil {
+ err = delError
+ }
+ return err
+}
+
+// RemoveExisting removes an existing file in a safe way so that it
+// can be restored if the operation fails.
+//
+// This first detects if there is an existing file and renames it to a
+// temporary name if there is.
+//
+// The returned cleanup function should be called on a defer statement
+// with a pointer to the error returned. It will revert the changes if
+// there is an error or delete the existing file if not.
+func RemoveExisting(ctx context.Context, f fs.Fs, remote string, operation string) (cleanup func(*error), err error) {
+ existingObj, err := f.NewObject(ctx, remote)
+ if err != nil {
+ return func(*error) {}, nil
+ }
+ doMove := f.Features().Move
+ if doMove == nil {
+ return nil, fmt.Errorf("%s: destination file exists already and can't rename", operation)
+ }
+
+ // Avoid making the leaf name longer if it's already lengthy to avoid
+ // trouble with file name length limits.
+ suffix := "." + random.String(8)
+ var remoteSaved string
+ if len(path.Base(remote)) > 100 {
+ remoteSaved = TruncateString(remote, len(remote)-len(suffix)) + suffix
+ } else {
+ remoteSaved = remote + suffix
+ }
+
+ fs.Debugf(existingObj, "%s: renaming existing object to %q before starting", operation, remoteSaved)
+ existingObj, err = doMove(ctx, existingObj, remoteSaved)
+ if err != nil {
+ return nil, fmt.Errorf("%s: failed to rename existing file: %w", operation, err)
+ }
+ return func(perr *error) {
+ if *perr == nil {
+ fs.Debugf(existingObj, "%s: removing renamed existing file after operation", operation)
+ err := existingObj.Remove(ctx)
+ if err != nil {
+ *perr = fmt.Errorf("%s: failed to remove renamed existing file: %w", operation, err)
+ }
+ } else {
+ fs.Debugf(existingObj, "%s: renaming existing back after failed operation", operation)
+ _, renameErr := doMove(ctx, existingObj, remote)
+ if renameErr != nil {
+ fs.Errorf(existingObj, "%s: failed to restore existing file after failed operation: %v", operation, renameErr)
+ }
+ }
+ }, nil
+}
+
+// listToChan will transfer all objects in the listing to the output
+//
+// If an error occurs, the error will be logged, and it will close the
+// channel.
+//
+// If the error was ErrorDirNotFound then it will be ignored
+func listToChan(ctx context.Context, f fs.Fs, dir string) fs.ObjectsChan {
+ ci := fs.GetConfig(ctx)
+ o := make(fs.ObjectsChan, ci.Checkers)
+ go func() {
+ defer close(o)
+ err := walk.ListR(ctx, f, dir, true, ci.MaxDepth, walk.ListObjects, func(entries fs.DirEntries) error {
+ entries.ForObject(func(obj fs.Object) {
+ o <- obj
+ })
+ return nil
+ })
+ if err != nil && err != fs.ErrorDirNotFound {
+ err = fmt.Errorf("failed to list: %w", err)
+ err = fs.CountError(ctx, err)
+ fs.Errorf(nil, "%v", err)
+ }
+ }()
+ return o
+}
+
+// CleanUp removes the trash for the Fs
+func CleanUp(ctx context.Context, f fs.Fs) error {
+ doCleanUp := f.Features().CleanUp
+ if doCleanUp == nil {
+ return fmt.Errorf("%v doesn't support cleanup", f)
+ }
+ if SkipDestructive(ctx, f, "clean up old files") {
+ return nil
+ }
+ return doCleanUp(ctx)
+}
+
+// wrap a Reader and a Closer together into a ReadCloser
+type readCloser struct {
+ io.Reader
+ io.Closer
+}
+
+// Cat any files to the io.Writer
+//
+// if offset == 0 it will be ignored
+// if offset > 0 then the file will be seeked to that offset
+// if offset < 0 then the file will be seeked that far from the end
+//
+// if count < 0 then it will be ignored
+// if count >= 0 then only that many characters will be output
+func Cat(ctx context.Context, f fs.Fs, w io.Writer, offset, count int64, sep []byte) error {
+ var mu sync.Mutex
+ ci := fs.GetConfig(ctx)
+ return ListFn(ctx, f, func(o fs.Object) {
+ var err error
+ tr := accounting.Stats(ctx).NewTransfer(o, nil)
+ defer func() {
+ tr.Done(ctx, err)
+ }()
+ opt := fs.RangeOption{Start: offset, End: -1}
+ size := o.Size()
+ if opt.Start < 0 {
+ opt.Start += size
+ }
+ if count >= 0 {
+ opt.End = opt.Start + count - 1
+ }
+ var options []fs.OpenOption
+ if opt.Start > 0 || opt.End >= 0 {
+ options = append(options, &opt)
+ }
+ for _, option := range ci.DownloadHeaders {
+ options = append(options, option)
+ }
+ var in io.ReadCloser
+ in, err = Open(ctx, o, options...)
+ if err != nil {
+ err = fs.CountError(ctx, err)
+ fs.Errorf(o, "Failed to open: %v", err)
+ return
+ }
+ if count >= 0 {
+ in = &readCloser{Reader: &io.LimitedReader{R: in, N: count}, Closer: in}
+ }
+ in = tr.Account(ctx, in).WithBuffer() // account and buffer the transfer
+ // take the lock just before we output stuff, so at the last possible moment
+ mu.Lock()
+ defer mu.Unlock()
+ _, err = io.Copy(w, in)
+ if err != nil {
+ err = fs.CountError(ctx, err)
+ fs.Errorf(o, "Failed to send to output: %v", err)
+ }
+ if len(sep) > 0 {
+ _, err = w.Write(sep)
+ if err != nil {
+ err = fs.CountError(ctx, err)
+ fs.Errorf(o, "Failed to send separator to output: %v", err)
+ }
+ }
+ })
+}
+
+// Rcat reads data from the Reader until EOF and uploads it to a file on remote
+//
+// in is closed at the end of the transfer
+func Rcat(ctx context.Context, fdst fs.Fs, dstFileName string, in io.ReadCloser, modTime time.Time, meta fs.Metadata) (dst fs.Object, err error) {
+ return rcatSrc(ctx, fdst, dstFileName, in, modTime, meta, nil)
+}
+
+// rcatSrc reads data from the Reader until EOF and uploads it to a file on remote
+//
+// in is closed at the end of the transfer
+//
+// Pass in fsrc if known or nil if not
+func rcatSrc(ctx context.Context, fdst fs.Fs, dstFileName string, in io.ReadCloser, modTime time.Time, meta fs.Metadata, fsrc fs.Fs) (dst fs.Object, err error) {
+ if SkipDestructive(ctx, dstFileName, "upload from pipe") {
+ // prevents "broken pipe" errors
+ _, err = io.Copy(io.Discard, in)
+ return nil, err
+ }
+
+ ci := fs.GetConfig(ctx)
+ tr := accounting.Stats(ctx).NewTransferRemoteSize(dstFileName, -1, nil, fdst)
+ defer func() {
+ tr.Done(ctx, err)
+ }()
+ var streamIn io.Reader = tr.Account(ctx, in).WithBuffer()
+
+ readCounter := readers.NewCountingReader(streamIn)
+ var trackingIn io.Reader
+ var hasher *hash.MultiHasher
+ var options []fs.OpenOption
+ if !ci.IgnoreChecksum {
+ hashes := hash.NewHashSet(fdst.Hashes().GetOne()) // just pick one hash
+ hashOption := &fs.HashesOption{Hashes: hashes}
+ options = append(options, hashOption)
+ hasher, err = hash.NewMultiHasherTypes(hashes)
+ if err != nil {
+ return nil, err
+ }
+ trackingIn = io.TeeReader(readCounter, hasher)
+ } else {
+ trackingIn = readCounter
+ }
+ for _, option := range ci.UploadHeaders {
+ options = append(options, option)
+ }
+ if ci.MetadataSet != nil {
+ options = append(options, fs.MetadataOption(ci.MetadataSet))
+ }
+
+ // get the sums from the hasher if in use, or nil
+ getSums := func() (sums map[hash.Type]string) {
+ if hasher != nil {
+ sums = hasher.Sums()
+ }
+ return sums
+ }
+
+ // Read the start of the input and check if it is small enough for direct upload
+ buf := make([]byte, ci.StreamingUploadCutoff)
+ fileIsSmall := false
+ if n, err := io.ReadFull(trackingIn, buf); err == io.EOF || err == io.ErrUnexpectedEOF {
+ fileIsSmall = true
+ buf = buf[:n]
+ }
+
+ // Read the data we have already read in buf and any further unread
+ streamIn = io.MultiReader(bytes.NewReader(buf), trackingIn)
+
+ doPutStream := fdst.Features().PutStream
+
+ // Upload the input
+ if fileIsSmall || doPutStream == nil {
+ var rs io.ReadSeeker
+ if fileIsSmall {
+ fs.Debugf(fdst, "File to upload is small (%d bytes), uploading instead of streaming", len(buf))
+ rs = bytes.NewReader(buf)
+ } else {
+ fs.Debugf(fdst, "Target remote doesn't support streaming uploads, creating temporary local FS to spool file")
+ spool, err := os.CreateTemp("", "rclone-spool")
+ if err != nil {
+ return nil, fmt.Errorf("failed to create temporary spool file: %v", err)
+ }
+ fileName := spool.Name()
+ defer func() {
+ err := spool.Close()
+ if err != nil {
+ fs.Errorf(fileName, "Failed to close temporary spool file: %v", err)
+ }
+ err = os.Remove(fileName)
+ if err != nil {
+ fs.Errorf(fileName, "Failed to delete temporary spool file: %v", err)
+ }
+ }()
+ _, err = io.Copy(spool, streamIn)
+ if err != nil {
+ return nil, fmt.Errorf("failed to copy to temporary spool file: %v", err)
+ }
+ rs = spool
+ }
+ // Upload with Put with retries - since we have downloaded the file we know the size, and the hashes
+ sums := getSums()
+ size := int64(readCounter.BytesRead())
+ objInfo := object.NewStaticObjectInfo(dstFileName, modTime, size, false, sums, fsrc).WithMetadata(meta)
+ err = Retry(ctx, objInfo, ci.LowLevelRetries, func() error {
+ _, err = rs.Seek(0, io.SeekStart)
+ if err != nil {
+ return fmt.Errorf("failed to rewind temporary spool file: %v", err)
+ }
+ dst, err = fdst.Put(ctx, rs, objInfo, options...)
+ return err
+ })
+ } else {
+ // Upload with PutStream with no retries
+ objInfo := object.NewStaticObjectInfo(dstFileName, modTime, -1, false, nil, fsrc).WithMetadata(meta)
+ dst, err = doPutStream(ctx, streamIn, objInfo, options...)
+ }
+ if err != nil {
+ return dst, err
+ }
+
+ // Check transfer
+ sums := getSums()
+ opt := defaultEqualOpt(ctx)
+ if sums != nil {
+ // force --checksum on if we have hashes
+ opt.checkSum = true
+ }
+ src := object.NewStaticObjectInfo(dstFileName, modTime, int64(readCounter.BytesRead()), false, sums, fdst).WithMetadata(meta)
+ if !equal(ctx, src, dst, opt) {
+ err = fmt.Errorf("corrupted on transfer")
+ err = fs.CountError(ctx, err)
+ fs.Errorf(dst, "%v", err)
+ return dst, err
+ }
+ return dst, nil
+}
+
+// PublicLink adds a "readable by anyone with link" permission on the given file or folder.
+func PublicLink(ctx context.Context, f fs.Fs, remote string, expire fs.Duration, unlink bool) (string, error) {
+ doPublicLink := f.Features().PublicLink
+ if doPublicLink == nil {
+ return "", fmt.Errorf("%v doesn't support public links", f)
+ }
+ return doPublicLink(ctx, remote, expire, unlink)
+}
+
+// Rmdirs removes any empty directories (or directories only
+// containing empty directories) under f, including f.
+//
+// Rmdirs obeys the filters
+func Rmdirs(ctx context.Context, f fs.Fs, dir string, leaveRoot bool) error {
+ ci := fs.GetConfig(ctx)
+ fi := filter.GetConfig(ctx)
+ dirEmpty := make(map[string]bool)
+ dirEmpty[dir] = !leaveRoot
+ err := walk.Walk(ctx, f, dir, false, ci.MaxDepth, func(dirPath string, entries fs.DirEntries, err error) error {
+ if err != nil {
+ err = fs.CountError(ctx, err)
+ fs.Errorf(f, "Failed to list %q: %v", dirPath, err)
+ return nil
+ }
+ for _, entry := range entries {
+ switch x := entry.(type) {
+ case fs.Directory:
+ // add a new directory as empty
+ dir := x.Remote()
+ _, found := dirEmpty[dir]
+ if !found {
+ dirEmpty[dir] = true
+ }
+ case fs.Object:
+ // mark the parents of the file as being non-empty
+ dir := x.Remote()
+ for dir != "" {
+ dir = path.Dir(dir)
+ if dir == "." || dir == "/" {
+ dir = ""
+ }
+ empty, found := dirEmpty[dir]
+ // End if we reach a directory which is non-empty
+ if found && !empty {
+ break
+ }
+ dirEmpty[dir] = false
+ }
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("failed to rmdirs: %w", err)
+ }
+
+ // Group directories to delete by level
+ var toDelete [][]string
+ for dir, empty := range dirEmpty {
+ if empty {
+ // If a filter matches the directory then that
+ // directory is a candidate for deletion
+ if fi.IncludeRemote(dir + "/") {
+ level := strings.Count(dir, "/") + 1
+ // The root directory "" is at the top level
+ if dir == "" {
+ level = 0
+ }
+ if len(toDelete) < level+1 {
+ toDelete = append(toDelete, make([][]string, level+1-len(toDelete))...)
+ }
+ toDelete[level] = append(toDelete[level], dir)
+ }
+ }
+ }
+
+ errCount := errcount.New()
+ // Delete all directories at the same level in parallel
+ for level := len(toDelete) - 1; level >= 0; level-- {
+ dirs := toDelete[level]
+ if len(dirs) == 0 {
+ continue
+ }
+ fs.Debugf(nil, "removing %d level %d directories", len(dirs), level)
+ sort.Strings(dirs)
+ g, gCtx := errgroup.WithContext(ctx)
+ g.SetLimit(ci.Checkers)
+ for _, dir := range dirs {
+ // End early if error
+ if gCtx.Err() != nil {
+ break
+ }
+ dir := dir
+ g.Go(func() error {
+ err := TryRmdir(gCtx, f, dir)
+ if err != nil {
+ err = fs.CountError(ctx, err)
+ fs.Errorf(dir, "Failed to rmdir: %v", err)
+ errCount.Add(err)
+ }
+ return nil // don't return errors, just count them
+ })
+ }
+ err := g.Wait()
+ if err != nil {
+ return err
+ }
+ }
+ return errCount.Err("failed to remove directories")
+}
+
+// GetCompareDest sets up --compare-dest
+func GetCompareDest(ctx context.Context) (CompareDest []fs.Fs, err error) {
+ ci := fs.GetConfig(ctx)
+ CompareDest, err = cache.GetArr(ctx, ci.CompareDest)
+ if err != nil {
+ return nil, fserrors.FatalError(fmt.Errorf("failed to make fs for --compare-dest %q: %w", ci.CompareDest, err))
+ }
+ return CompareDest, nil
+}
+
+// compareDest checks --compare-dest to see if src needs to
+// be copied
+//
+// Returns True if src is in --compare-dest
+func compareDest(ctx context.Context, dst, src fs.Object, CompareDest fs.Fs) (NoNeedTransfer bool, err error) {
+ var remote string
+ if dst == nil {
+ remote = src.Remote()
+ } else {
+ remote = dst.Remote()
+ }
+ CompareDestFile, err := CompareDest.NewObject(ctx, remote)
+ switch err {
+ case fs.ErrorObjectNotFound:
+ return false, nil
+ case nil:
+ break
+ default:
+ return false, err
+ }
+ opt := defaultEqualOpt(ctx)
+ opt.updateModTime = false
+ if equal(ctx, src, CompareDestFile, opt) {
+ fs.Debugf(src, "Destination found in --compare-dest, skipping")
+ return true, nil
+ }
+ return false, nil
+}
+
+// GetCopyDest sets up --copy-dest
+func GetCopyDest(ctx context.Context, fdst fs.Fs) (CopyDest []fs.Fs, err error) {
+ ci := fs.GetConfig(ctx)
+ CopyDest, err = cache.GetArr(ctx, ci.CopyDest)
+ if err != nil {
+ return nil, fserrors.FatalError(fmt.Errorf("failed to make fs for --copy-dest %q: %w", ci.CopyDest, err))
+ }
+ if !SameConfigArr(fdst, CopyDest) {
+ return nil, fserrors.FatalError(errors.New("parameter to --copy-dest has to be on the same remote as destination"))
+ }
+ for _, cf := range CopyDest {
+ if cf.Features().Copy == nil {
+ return nil, fserrors.FatalError(errors.New("can't use --copy-dest on a remote which doesn't support server side copy"))
+ }
+ }
+
+ return CopyDest, nil
+}
+
+// copyDest checks --copy-dest to see if src needs to
+// be copied
+//
+// Returns True if src was copied from --copy-dest
+func copyDest(ctx context.Context, fdst fs.Fs, dst, src fs.Object, CopyDest, backupDir fs.Fs) (NoNeedTransfer bool, err error) {
+ var remote string
+ if dst == nil {
+ remote = src.Remote()
+ } else {
+ remote = dst.Remote()
+ }
+ CopyDestFile, err := CopyDest.NewObject(ctx, remote)
+ switch err {
+ case fs.ErrorObjectNotFound:
+ return false, nil
+ case nil:
+ break
+ default:
+ return false, err
+ }
+ opt := defaultEqualOpt(ctx)
+ opt.updateModTime = false
+ if equal(ctx, src, CopyDestFile, opt) {
+ if dst == nil || !Equal(ctx, src, dst) {
+ if dst != nil && backupDir != nil {
+ err = MoveBackupDir(ctx, backupDir, dst)
+ if err != nil {
+ return false, fmt.Errorf("moving to --backup-dir failed: %w", err)
+ }
+ // If successful zero out the dstObj as it is no longer there
+ dst = nil
+ }
+ _, err := Copy(ctx, fdst, dst, remote, CopyDestFile)
+ if err != nil {
+ fs.Errorf(src, "Destination found in --copy-dest, error copying")
+ return false, nil
+ }
+ fs.Debugf(src, "Destination found in --copy-dest, using server-side copy")
+ return true, nil
+ }
+ fs.Debugf(src, "Unchanged skipping")
+ return true, nil
+ }
+ fs.Debugf(src, "Destination not found in --copy-dest")
+ return false, nil
+}
+
+// CompareOrCopyDest checks --compare-dest and --copy-dest to see if src
+// does not need to be copied
+//
+// Returns True if src does not need to be copied
+func CompareOrCopyDest(ctx context.Context, fdst fs.Fs, dst, src fs.Object, CompareOrCopyDest []fs.Fs, backupDir fs.Fs) (NoNeedTransfer bool, err error) {
+ ci := fs.GetConfig(ctx)
+ if len(ci.CompareDest) > 0 {
+ for _, compareF := range CompareOrCopyDest {
+ NoNeedTransfer, err := compareDest(ctx, dst, src, compareF)
+ if NoNeedTransfer || err != nil {
+ return NoNeedTransfer, err
+ }
+ }
+ } else if len(ci.CopyDest) > 0 {
+ for _, copyF := range CompareOrCopyDest {
+ NoNeedTransfer, err := copyDest(ctx, fdst, dst, src, copyF, backupDir)
+ if NoNeedTransfer || err != nil {
+ return NoNeedTransfer, err
+ }
+ }
+ }
+ return false, nil
+}
+
+// NeedTransfer checks to see if src needs to be copied to dst using
+// the current config.
+//
+// Returns a flag which indicates whether the file needs to be
+// transferred or not.
+func NeedTransfer(ctx context.Context, dst, src fs.Object) bool {
+ ci := fs.GetConfig(ctx)
+ logger, _ := GetLogger(ctx)
+ if dst == nil {
+ fs.Debugf(src, "Need to transfer - File not found at Destination")
+ logger(ctx, MissingOnDst, src, nil, nil)
+ return true
+ }
+ // If we should ignore existing files, don't transfer
+ if ci.IgnoreExisting {
+ fs.Debugf(src, "Destination exists, skipping")
+ logger(ctx, Match, src, dst, nil)
+ return false
+ }
+ // If we should upload unconditionally
+ if ci.IgnoreTimes {
+ fs.Debugf(src, "Transferring unconditionally as --ignore-times is in use")
+ logger(ctx, Differ, src, dst, nil)
+ return true
+ }
+ // If UpdateOlder is in effect, skip if dst is newer than src
+ if ci.UpdateOlder {
+ srcModTime := src.ModTime(ctx)
+ dstModTime := dst.ModTime(ctx)
+ dt := dstModTime.Sub(srcModTime)
+ // If have a mutually agreed precision then use that
+ modifyWindow := fs.GetModifyWindow(ctx, dst.Fs(), src.Fs())
+ if modifyWindow == fs.ModTimeNotSupported {
+ // Otherwise use 1 second as a safe default as
+ // the resolution of the time a file was
+ // uploaded.
+ modifyWindow = time.Second
+ }
+ switch {
+ case dt >= modifyWindow:
+ fs.Debugf(src, "Destination is newer than source, skipping")
+ logger(ctx, Match, src, dst, nil)
+ return false
+ case dt <= -modifyWindow:
+ // force --checksum on for the check and do update modtimes by default
+ opt := defaultEqualOpt(ctx)
+ opt.forceModTimeMatch = true
+ if equal(ctx, src, dst, opt) {
+ fs.Debugf(src, "Unchanged skipping")
+ return false
+ }
+ default:
+ // Do a size only compare unless --checksum is set
+ opt := defaultEqualOpt(ctx)
+ opt.sizeOnly = !ci.CheckSum
+ if equal(ctx, src, dst, opt) {
+ fs.Debugf(src, "Destination mod time is within %v of source and files identical, skipping", modifyWindow)
+ return false
+ }
+ fs.Debugf(src, "Destination mod time is within %v of source but files differ, transferring", modifyWindow)
+ }
+ } else {
+ // Check to see if changed or not
+ equalFn, ok := ctx.Value(equalFnKey).(EqualFn)
+ if ok {
+ return !equalFn(ctx, src, dst)
+ }
+ if Equal(ctx, src, dst) && !SameObject(src, dst) {
+ fs.Debugf(src, "Unchanged skipping")
+ return false
+ }
+ }
+ return true
+}
+
+// RcatSize reads data from the Reader until EOF and uploads it to a file on remote.
+// Pass in size >=0 if known, <0 if not known
+func RcatSize(ctx context.Context, fdst fs.Fs, dstFileName string, in io.ReadCloser, size int64, modTime time.Time, meta fs.Metadata) (dst fs.Object, err error) {
+ var obj fs.Object
+
+ if size >= 0 {
+ var err error
+ // Size known use Put
+ tr := accounting.Stats(ctx).NewTransferRemoteSize(dstFileName, size, nil, fdst)
+ defer func() {
+ tr.Done(ctx, err)
+ }()
+ body := io.NopCloser(in) // we let the server close the body
+ in := tr.Account(ctx, body) // account the transfer (no buffering)
+
+ if SkipDestructive(ctx, dstFileName, "upload from pipe") {
+ // prevents "broken pipe" errors
+ _, err = io.Copy(io.Discard, in)
+ return nil, err
+ }
+
+ info := object.NewStaticObjectInfo(dstFileName, modTime, size, true, nil, fdst).WithMetadata(meta)
+ obj, err = fdst.Put(ctx, in, info)
+ if err != nil {
+ fs.Errorf(dstFileName, "Post request put error: %v", err)
+
+ return nil, err
+ }
+ } else {
+ // Size unknown use Rcat
+ obj, err = Rcat(ctx, fdst, dstFileName, in, modTime, meta)
+ if err != nil {
+ fs.Errorf(dstFileName, "Post request rcat error: %v", err)
+
+ return nil, err
+ }
+ }
+
+ return obj, nil
+}
+
+// copyURLFunc is called from CopyURLFn
+type copyURLFunc func(ctx context.Context, dstFileName string, in io.ReadCloser, size int64, modTime time.Time) (err error)
+
+// copyURLFn copies the data from the url to the function supplied
+func copyURLFn(ctx context.Context, dstFileName string, url string, autoFilename, dstFileNameFromHeader bool, fn copyURLFunc) (err error) {
+ client := fshttp.NewClient(ctx)
+ resp, err := client.Get(url)
+ if err != nil {
+ return err
+ }
+ defer fs.CheckClose(resp.Body, &err)
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return fmt.Errorf("CopyURL failed: %s", resp.Status)
+ }
+ modTime, err := http.ParseTime(resp.Header.Get("Last-Modified"))
+ if err != nil {
+ modTime = time.Now()
+ }
+ if autoFilename {
+ if dstFileNameFromHeader {
+ _, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition"))
+ headerFilename := path.Base(strings.ReplaceAll(params["filename"], "\\", "/"))
+ if err != nil || headerFilename == "" {
+ return fmt.Errorf("CopyURL failed: filename not found in the Content-Disposition header")
+ }
+ fs.Debugf(headerFilename, "filename found in Content-Disposition header.")
+ return fn(ctx, headerFilename, resp.Body, resp.ContentLength, modTime)
+ }
+
+ dstFileName = path.Base(resp.Request.URL.Path)
+ if dstFileName == "." || dstFileName == "/" {
+ return fmt.Errorf("CopyURL failed: file name wasn't found in url")
+ }
+ fs.Debugf(dstFileName, "File name found in url")
+ }
+ return fn(ctx, dstFileName, resp.Body, resp.ContentLength, modTime)
+}
+
+// CopyURL copies the data from the url to (fdst, dstFileName)
+func CopyURL(ctx context.Context, fdst fs.Fs, dstFileName string, url string, autoFilename, dstFileNameFromHeader bool, noClobber bool) (dst fs.Object, err error) {
+ err = copyURLFn(ctx, dstFileName, url, autoFilename, dstFileNameFromHeader, func(ctx context.Context, dstFileName string, in io.ReadCloser, size int64, modTime time.Time) (err error) {
+ if noClobber {
+ _, err = fdst.NewObject(ctx, dstFileName)
+ if err == nil {
+ return errors.New("CopyURL failed: file already exist")
+ }
+ }
+ dst, err = RcatSize(ctx, fdst, dstFileName, in, size, modTime, nil)
+ return err
+ })
+ return dst, err
+}
+
+// CopyURLToWriter copies the data from the url to the io.Writer supplied
+func CopyURLToWriter(ctx context.Context, url string, out io.Writer) (err error) {
+ return copyURLFn(ctx, "", url, false, false, func(ctx context.Context, dstFileName string, in io.ReadCloser, size int64, modTime time.Time) (err error) {
+ _, err = io.Copy(out, in)
+ return err
+ })
+}
+
+// BackupDir returns the correctly configured --backup-dir
+func BackupDir(ctx context.Context, fdst fs.Fs, fsrc fs.Fs, srcFileName string) (backupDir fs.Fs, err error) {
+ ci := fs.GetConfig(ctx)
+ if ci.BackupDir != "" {
+ backupDir, err = cache.Get(ctx, ci.BackupDir)
+ if err != nil {
+ return nil, fserrors.FatalError(fmt.Errorf("failed to make fs for --backup-dir %q: %w", ci.BackupDir, err))
+ }
+ if !SameConfig(fdst, backupDir) {
+ return nil, fserrors.FatalError(errors.New("parameter to --backup-dir has to be on the same remote as destination"))
+ }
+ if srcFileName == "" {
+ if OverlappingFilterCheck(ctx, backupDir, fdst) {
+ return nil, fserrors.FatalError(errors.New("destination and parameter to --backup-dir mustn't overlap"))
+ }
+ if OverlappingFilterCheck(ctx, backupDir, fsrc) {
+ return nil, fserrors.FatalError(errors.New("source and parameter to --backup-dir mustn't overlap"))
+ }
+ } else if ci.Suffix == "" {
+ if SameDir(fdst, backupDir) {
+ return nil, fserrors.FatalError(errors.New("destination and parameter to --backup-dir mustn't be the same"))
+ }
+ if SameDir(fsrc, backupDir) {
+ return nil, fserrors.FatalError(errors.New("source and parameter to --backup-dir mustn't be the same"))
+ }
+ }
+ } else if ci.Suffix != "" {
+ // --backup-dir is not set but --suffix is - use the destination as the backupDir
+ backupDir = fdst
+ } else {
+ return nil, fserrors.FatalError(errors.New("internal error: BackupDir called when --backup-dir and --suffix both empty"))
+ }
+ if !CanServerSideMove(backupDir) {
+ return nil, fserrors.FatalError(errors.New("can't use --backup-dir on a remote which doesn't support server-side move or copy"))
+ }
+ return backupDir, nil
+}
+
+// MoveBackupDir moves a file to the backup dir
+func MoveBackupDir(ctx context.Context, backupDir fs.Fs, dst fs.Object) (err error) {
+ remoteWithSuffix := SuffixName(ctx, dst.Remote())
+ overwritten, _ := backupDir.NewObject(ctx, remoteWithSuffix)
+ _, err = Move(ctx, backupDir, overwritten, remoteWithSuffix, dst)
+ return err
+}
+
+// needsMoveCaseInsensitive returns true if moveCaseInsensitive is needed
+func needsMoveCaseInsensitive(fdst fs.Fs, fsrc fs.Fs, dstFileName string, srcFileName string, cp bool) bool {
+ dstFilePath := path.Join(fdst.Root(), dstFileName)
+ srcFilePath := path.Join(fsrc.Root(), srcFileName)
+ if !cp && fdst.Name() == fsrc.Name() && dstFileName != srcFileName && norm.NFC.String(dstFilePath) == norm.NFC.String(srcFilePath) {
+ return true
+ }
+ return !cp && fdst.Name() == fsrc.Name() && fdst.Features().CaseInsensitive && dstFileName != srcFileName && strings.EqualFold(dstFilePath, srcFilePath)
+}
+
+// MoveCaseInsensitive handles changing case of a file on a case insensitive remote.
+// This will move the file to a temporary name then
+// move it back to the intended destination. This is required
+// to avoid issues with certain remotes and avoid file deletion.
+// returns nil, nil if !needsMoveCaseInsensitive.
+// this does not account a transfer -- the caller should do that if desired.
+func MoveCaseInsensitive(ctx context.Context, fdst fs.Fs, fsrc fs.Fs, dstFileName string, srcFileName string, cp bool, srcObj fs.Object) (newDst fs.Object, err error) {
+ logger, _ := GetLogger(ctx)
+
+ // Choose operations
+ Op := MoveTransfer
+ if cp {
+ Op = Copy
+ }
+
+ if SkipDestructive(ctx, srcFileName, "rename to "+dstFileName) {
+ // avoid fatalpanic on --dry-run (trying to access non-existent tmpObj)
+ return nil, nil
+ }
+ // Create random name to temporarily move file to
+ tmpObjName := dstFileName + "-rclone-move-" + random.String(8)
+ tmpObjFail, err := fdst.NewObject(ctx, tmpObjName)
+ if err != fs.ErrorObjectNotFound {
+ if err == nil {
+ logger(ctx, TransferError, nil, tmpObjFail, err)
+ return nil, errors.New("found an already existing file with a randomly generated name. Try the operation again")
+ }
+ logger(ctx, TransferError, nil, tmpObjFail, err)
+ return nil, fmt.Errorf("error while attempting to move file to a temporary location: %w", err)
+ }
+ fs.Debugf(srcObj, "moving to %v", tmpObjName)
+ tmpObj, err := Op(ctx, fdst, nil, tmpObjName, srcObj)
+ if err != nil {
+ logger(ctx, TransferError, srcObj, tmpObj, err)
+ return nil, fmt.Errorf("error while moving file to temporary location: %w", err)
+ }
+ fs.Debugf(srcObj, "moving to %v", dstFileName)
+ newDst, err = Op(ctx, fdst, nil, dstFileName, tmpObj)
+ logger(ctx, MissingOnDst, tmpObj, nil, err)
+ return newDst, err
+}
+
+// moveOrCopyFile moves or copies a single file possibly to a new name
+func moveOrCopyFile(ctx context.Context, fdst fs.Fs, fsrc fs.Fs, dstFileName string, srcFileName string, cp bool, allowOverlap bool) (err error) {
+ ci := fs.GetConfig(ctx)
+ logger, usingLogger := GetLogger(ctx)
+ dstFilePath := path.Join(fdst.Root(), dstFileName)
+ srcFilePath := path.Join(fsrc.Root(), srcFileName)
+ if fdst.Name() == fsrc.Name() && dstFilePath == srcFilePath && !allowOverlap {
+ fs.Debugf(fdst, "don't need to copy/move %s, it is already at target location", dstFileName)
+ if usingLogger {
+ srcObj, _ := fsrc.NewObject(ctx, srcFileName)
+ dstObj, _ := fsrc.NewObject(ctx, dstFileName)
+ logger(ctx, Match, srcObj, dstObj, nil)
+ }
+ return nil
+ }
+
+ // Choose operations
+ Op := MoveTransfer
+ if cp {
+ Op = Copy
+ }
+
+ // Find src object
+ srcObj, err := fsrc.NewObject(ctx, srcFileName)
+ if err != nil {
+ logger(ctx, TransferError, srcObj, nil, err)
+ return err
+ }
+
+ // Find dst object if it exists
+ var dstObj fs.Object
+ if !ci.NoCheckDest {
+ dstObj, err = fdst.NewObject(ctx, dstFileName)
+ if errors.Is(err, fs.ErrorObjectNotFound) {
+ dstObj = nil
+ } else if err != nil {
+ logger(ctx, TransferError, nil, dstObj, err)
+ return err
+ }
+ }
+
+ // Special case for changing case of a file on a case insensitive remote
+ // This will move the file to a temporary name then
+ // move it back to the intended destination. This is required
+ // to avoid issues with certain remotes and avoid file deletion.
+ if needsMoveCaseInsensitive(fdst, fsrc, dstFileName, srcFileName, cp) {
+ tr := accounting.Stats(ctx).NewTransfer(srcObj, fdst)
+ defer func() {
+ tr.Done(ctx, err)
+ }()
+ _, err = MoveCaseInsensitive(ctx, fdst, fsrc, dstFileName, srcFileName, cp, srcObj)
+ return err
+ }
+
+ var backupDir fs.Fs
+ var copyDestDir []fs.Fs
+ if ci.BackupDir != "" || ci.Suffix != "" {
+ backupDir, err = BackupDir(ctx, fdst, fsrc, srcFileName)
+ if err != nil {
+ return fmt.Errorf("creating Fs for --backup-dir failed: %w", err)
+ }
+ }
+ if len(ci.CompareDest) > 0 {
+ copyDestDir, err = GetCompareDest(ctx)
+ if err != nil {
+ return err
+ }
+ } else if len(ci.CopyDest) > 0 {
+ copyDestDir, err = GetCopyDest(ctx, fdst)
+ if err != nil {
+ return err
+ }
+ }
+ needTransfer := NeedTransfer(ctx, dstObj, srcObj)
+ if needTransfer {
+ NoNeedTransfer, err := CompareOrCopyDest(ctx, fdst, dstObj, srcObj, copyDestDir, backupDir)
+ if err != nil {
+ return err
+ }
+ if NoNeedTransfer {
+ needTransfer = false
+ }
+ }
+ if needTransfer {
+ // If destination already exists, then we must move it into --backup-dir if required
+ if dstObj != nil && backupDir != nil {
+ err = MoveBackupDir(ctx, backupDir, dstObj)
+ if err != nil {
+ logger(ctx, TransferError, dstObj, nil, err)
+ return fmt.Errorf("moving to --backup-dir failed: %w", err)
+ }
+ // If successful zero out the dstObj as it is no longer there
+ logger(ctx, MissingOnDst, dstObj, nil, nil)
+ dstObj = nil
+ }
+
+ _, err = Op(ctx, fdst, dstObj, dstFileName, srcObj)
+ } else if !cp {
+ if ci.IgnoreExisting {
+ fs.Debugf(srcObj, "Not removing source file as destination file exists and --ignore-existing is set")
+ logger(ctx, Match, srcObj, dstObj, nil)
+ } else if !SameObject(srcObj, dstObj) {
+ err = DeleteFile(ctx, srcObj)
+ logger(ctx, Differ, srcObj, dstObj, nil)
+ }
+ }
+ return err
+}
+
+// MoveFile moves a single file possibly to a new name
+//
+// This is treated as a transfer.
+func MoveFile(ctx context.Context, fdst fs.Fs, fsrc fs.Fs, dstFileName string, srcFileName string) (err error) {
+ return moveOrCopyFile(ctx, fdst, fsrc, dstFileName, srcFileName, false, false)
+}
+
+// TransformFile transforms a file in place using --name-transform
+//
+// This is treated as a transfer.
+func TransformFile(ctx context.Context, fdst fs.Fs, srcFileName string) (err error) {
+ return moveOrCopyFile(ctx, fdst, fdst, srcFileName, srcFileName, false, true)
+}
+
+// SetTier changes tier of object in remote
+func SetTier(ctx context.Context, fsrc fs.Fs, tier string) error {
+ return ListFn(ctx, fsrc, func(o fs.Object) {
+ objImpl, ok := o.(fs.SetTierer)
+ if !ok {
+ fs.Errorf(fsrc, "Remote object does not implement SetTier")
+ return
+ }
+ err := objImpl.SetTier(tier)
+ if err != nil {
+ fs.Errorf(fsrc, "Failed to do SetTier, %v", err)
+ }
+ })
+}
+
+// SetTierFile changes tier of a single file in remote
+func SetTierFile(ctx context.Context, o fs.Object, tier string) error {
+ do, ok := o.(fs.SetTierer)
+ if !ok {
+ return errors.New("remote object does not implement SetTier")
+ }
+ err := do.SetTier(tier)
+ if err != nil {
+ fs.Errorf(o, "Failed to do SetTier, %v", err)
+ return err
+ }
+ return nil
+}
+
+// TouchDir touches every file in directory with time t
+func TouchDir(ctx context.Context, f fs.Fs, remote string, t time.Time, recursive bool) error {
+ ci := fs.GetConfig(ctx)
+ g, gCtx := errgroup.WithContext(ctx)
+ g.SetLimit(ci.Transfers)
+ err := walk.ListR(ctx, f, remote, false, ConfigMaxDepth(ctx, recursive), walk.ListObjects, func(entries fs.DirEntries) error {
+ entries.ForObject(func(o fs.Object) {
+ if !SkipDestructive(ctx, o, "touch") {
+ g.Go(func() error {
+ fs.Debugf(f, "Touching %q", o.Remote())
+ err := o.SetModTime(gCtx, t)
+ if err != nil {
+ err = fmt.Errorf("failed to touch: %w", err)
+ err = fs.CountError(gCtx, err)
+ fs.Errorf(o, "%v", err)
+ }
+ return nil
+ })
+ }
+ })
+ return nil
+ })
+ _ = g.Wait()
+ return err
+}
+
+// ListFormat defines files information print format
+type ListFormat struct {
+ separator string
+ dirSlash bool
+ absolute bool
+ output []func(entry *ListJSONItem) string
+ csv *csv.Writer
+ buf bytes.Buffer
+}
+
+// SetSeparator changes separator in struct
+func (l *ListFormat) SetSeparator(separator string) {
+ l.separator = separator
+}
+
+// SetDirSlash defines if slash should be printed
+func (l *ListFormat) SetDirSlash(dirSlash bool) {
+ l.dirSlash = dirSlash
+}
+
+// SetAbsolute prints a leading slash in front of path names
+func (l *ListFormat) SetAbsolute(absolute bool) {
+ l.absolute = absolute
+}
+
+// SetCSV defines if the output should be csv
+//
+// Note that you should call SetSeparator before this if you want a
+// custom separator
+func (l *ListFormat) SetCSV(useCSV bool) {
+ if useCSV {
+ l.csv = csv.NewWriter(&l.buf)
+ if l.separator != "" {
+ l.csv.Comma = []rune(l.separator)[0]
+ }
+ } else {
+ l.csv = nil
+ }
+}
+
+// SetOutput sets functions used to create files information
+func (l *ListFormat) SetOutput(output []func(entry *ListJSONItem) string) {
+ l.output = output
+}
+
+// AddModTime adds file's Mod Time to output
+func (l *ListFormat) AddModTime(timeFormat string) {
+ switch timeFormat {
+ case "":
+ l.AppendOutput(func(entry *ListJSONItem) string {
+ return entry.ModTime.When.Local().Format("2006-01-02 15:04:05")
+ })
+ case "unix":
+ l.AppendOutput(func(entry *ListJSONItem) string {
+ return fmt.Sprint(entry.ModTime.When.Unix())
+ })
+ case "unixnano":
+ l.AppendOutput(func(entry *ListJSONItem) string {
+ return fmt.Sprint(entry.ModTime.When.UnixNano())
+ })
+ default:
+ timeFormat = transform.TimeFormat(timeFormat)
+ l.AppendOutput(func(entry *ListJSONItem) string {
+ return entry.ModTime.When.Local().Format(timeFormat)
+ })
+ }
+}
+
+// AddSize adds file's size to output
+func (l *ListFormat) AddSize() {
+ l.AppendOutput(func(entry *ListJSONItem) string {
+ return strconv.FormatInt(entry.Size, 10)
+ })
+}
+
+// normalisePath makes sure the path has the correct slashes for the current mode
+func (l *ListFormat) normalisePath(entry *ListJSONItem, remote string) string {
+ if l.absolute && !strings.HasPrefix(remote, "/") {
+ remote = "/" + remote
+ }
+ if entry.IsDir && l.dirSlash {
+ remote += "/"
+ }
+ return remote
+}
+
+// AddPath adds path to file to output
+func (l *ListFormat) AddPath() {
+ l.AppendOutput(func(entry *ListJSONItem) string {
+ return l.normalisePath(entry, entry.Path)
+ })
+}
+
+// AddEncrypted adds the encrypted path to file to output
+func (l *ListFormat) AddEncrypted() {
+ l.AppendOutput(func(entry *ListJSONItem) string {
+ return l.normalisePath(entry, entry.Encrypted)
+ })
+}
+
+// AddHash adds the hash of the type given to the output
+func (l *ListFormat) AddHash(ht hash.Type) {
+ hashName := ht.String()
+ l.AppendOutput(func(entry *ListJSONItem) string {
+ if entry.IsDir {
+ return ""
+ }
+ return entry.Hashes[hashName]
+ })
+}
+
+// AddID adds file's ID to the output if known
+func (l *ListFormat) AddID() {
+ l.AppendOutput(func(entry *ListJSONItem) string {
+ return entry.ID
+ })
+}
+
+// AddOrigID adds file's Original ID to the output if known
+func (l *ListFormat) AddOrigID() {
+ l.AppendOutput(func(entry *ListJSONItem) string {
+ return entry.OrigID
+ })
+}
+
+// AddTier adds file's Tier to the output if known
+func (l *ListFormat) AddTier() {
+ l.AppendOutput(func(entry *ListJSONItem) string {
+ return entry.Tier
+ })
+}
+
+// AddMimeType adds file's MimeType to the output if known
+func (l *ListFormat) AddMimeType() {
+ l.AppendOutput(func(entry *ListJSONItem) string {
+ return entry.MimeType
+ })
+}
+
+// AddMetadata adds file's Metadata to the output if known
+func (l *ListFormat) AddMetadata() {
+ l.AppendOutput(func(entry *ListJSONItem) string {
+ metadata := entry.Metadata
+ if metadata == nil {
+ metadata = make(fs.Metadata)
+ }
+ out, err := json.Marshal(metadata)
+ if err != nil {
+ return fmt.Sprintf("Failed to read metadata: %v", err.Error())
+ }
+ return string(out)
+ })
+}
+
+// AppendOutput adds string generated by specific function to printed output
+func (l *ListFormat) AppendOutput(functionToAppend func(item *ListJSONItem) string) {
+ l.output = append(l.output, functionToAppend)
+}
+
+// Format prints information about the DirEntry in the format defined
+func (l *ListFormat) Format(entry *ListJSONItem) (result string) {
+ var out []string
+ for _, fun := range l.output {
+ out = append(out, fun(entry))
+ }
+ if l.csv != nil {
+ l.buf.Reset()
+ _ = l.csv.Write(out) // can't fail writing to bytes.Buffer
+ l.csv.Flush()
+ result = strings.TrimRight(l.buf.String(), "\n")
+ } else {
+ result = strings.Join(out, l.separator)
+ }
+ return result
+}
+
+// FormatForLSFPrecision Returns a time format for the given precision
+func FormatForLSFPrecision(precision time.Duration) string {
+ switch {
+ case precision <= time.Nanosecond:
+ return "2006-01-02 15:04:05.000000000"
+ case precision <= 10*time.Nanosecond:
+ return "2006-01-02 15:04:05.00000000"
+ case precision <= 100*time.Nanosecond:
+ return "2006-01-02 15:04:05.0000000"
+ case precision <= time.Microsecond:
+ return "2006-01-02 15:04:05.000000"
+ case precision <= 10*time.Microsecond:
+ return "2006-01-02 15:04:05.00000"
+ case precision <= 100*time.Microsecond:
+ return "2006-01-02 15:04:05.0000"
+ case precision <= time.Millisecond:
+ return "2006-01-02 15:04:05.000"
+ case precision <= 10*time.Millisecond:
+ return "2006-01-02 15:04:05.00"
+ case precision <= 100*time.Millisecond:
+ return "2006-01-02 15:04:05.0"
+ }
+ return "2006-01-02 15:04:05"
+}
+
+// DirMove renames srcRemote to dstRemote
+//
+// It does this by loading the directory tree into memory (using ListR
+// if available) and doing renames in parallel.
+func DirMove(ctx context.Context, f fs.Fs, srcRemote, dstRemote string) (err error) {
+ ci := fs.GetConfig(ctx)
+
+ if SkipDestructive(ctx, srcRemote, "dirMove") {
+ accounting.Stats(ctx).Renames(1)
+ return nil
+ }
+
+ // Use DirMove if possible
+ if doDirMove := f.Features().DirMove; doDirMove != nil {
+ err = doDirMove(ctx, f, srcRemote, dstRemote)
+ if err == nil {
+ accounting.Stats(ctx).Renames(1)
+ }
+ if err != fs.ErrorCantDirMove && err != fs.ErrorDirExists {
+ return err
+ }
+ fs.Infof(f, "Can't DirMove - falling back to file moves: %v", err)
+ }
+
+ // Load the directory tree into memory
+ tree, err := walk.NewDirTree(ctx, f, srcRemote, true, -1)
+ if err != nil {
+ return fmt.Errorf("RenameDir tree walk: %w", err)
+ }
+
+ // Get the directories in sorted order
+ dirs := tree.Dirs()
+
+ // Make the destination directories - must be done in order not in parallel
+ for _, dir := range dirs {
+ dstPath := dstRemote + dir[len(srcRemote):]
+ err := f.Mkdir(ctx, dstPath)
+ if err != nil {
+ return fmt.Errorf("RenameDir mkdir: %w", err)
+ }
+ }
+
+ // Rename the files in parallel
+ type rename struct {
+ o fs.Object
+ newPath string
+ }
+ renames := make(chan rename, ci.Checkers)
+ g, gCtx := errgroup.WithContext(context.Background())
+ for range ci.Checkers {
+ g.Go(func() error {
+ for job := range renames {
+ dstOverwritten, _ := f.NewObject(gCtx, job.newPath)
+ _, err := Move(gCtx, f, dstOverwritten, job.newPath, job.o)
+ if err != nil {
+ return err
+ }
+ select {
+ case <-gCtx.Done():
+ return gCtx.Err()
+ default:
+ }
+
+ }
+ return nil
+ })
+ }
+ for dir, entries := range tree {
+ dstPath := dstRemote + dir[len(srcRemote):]
+ for _, entry := range entries {
+ if o, ok := entry.(fs.Object); ok {
+ renames <- rename{o, path.Join(dstPath, path.Base(o.Remote()))}
+ }
+ }
+ }
+ close(renames)
+ err = g.Wait()
+ if err != nil {
+ return fmt.Errorf("RenameDir renames: %w", err)
+ }
+
+ // Remove the source directories in reverse order
+ for i := len(dirs) - 1; i >= 0; i-- {
+ err := f.Rmdir(ctx, dirs[i])
+ if err != nil {
+ return fmt.Errorf("RenameDir rmdir: %w", err)
+ }
+ }
+
+ return nil
+}
+
+// DirMoveCaseInsensitive does DirMove in two steps (to temp name, then real name)
+// which is necessary for some case-insensitive backends
+func DirMoveCaseInsensitive(ctx context.Context, f fs.Fs, srcRemote, dstRemote string) (err error) {
+ tmpDstRemote := dstRemote + "-rclone-move-" + random.String(8)
+ err = DirMove(ctx, f, srcRemote, tmpDstRemote)
+ if err != nil {
+ return err
+ }
+ return DirMove(ctx, f, tmpDstRemote, dstRemote)
+}
+
+// FsInfo provides information about a remote
+type FsInfo struct {
+ // Name of the remote (as passed into NewFs)
+ Name string
+
+ // Root of the remote (as passed into NewFs)
+ Root string
+
+ // String returns a description of the FS
+ String string
+
+ // Precision of the ModTimes in this Fs in Nanoseconds
+ Precision time.Duration
+
+ // Returns the supported hash types of the filesystem
+ Hashes []string
+
+ // Features returns the optional features of this Fs
+ Features map[string]bool
+
+ // MetadataInfo returns info about the metadata for this backend
+ MetadataInfo *fs.MetadataInfo
+}
+
+// GetFsInfo gets the information (FsInfo) about a given Fs
+func GetFsInfo(f fs.Fs) *FsInfo {
+ features := f.Features()
+ info := &FsInfo{
+ Name: f.Name(),
+ Root: f.Root(),
+ String: f.String(),
+ Precision: f.Precision(),
+ Hashes: make([]string, 0, 4),
+ Features: features.Enabled(),
+ MetadataInfo: nil,
+ }
+ for _, hashType := range f.Hashes().Array() {
+ info.Hashes = append(info.Hashes, hashType.String())
+ }
+ fsInfo, _, _, _, err := fs.ParseRemote(fs.ConfigString(f))
+ if err == nil && fsInfo != nil && fsInfo.MetadataInfo != nil {
+ info.MetadataInfo = fsInfo.MetadataInfo
+ }
+ return info
+}
+
+var (
+ interactiveMu sync.Mutex // protects the following variables
+ skipped = map[string]bool{}
+)
+
+// skipDestructiveChoose asks the user which action to take
+//
+// Call with interactiveMu held
+func skipDestructiveChoose(ctx context.Context, subject any, action string) (skip bool) {
+ // Lock the StdoutMutex - must not call fs.Log anything
+ // otherwise it will deadlock with --interactive --progress
+ StdoutMutex.Lock()
+
+ fmt.Printf("\nrclone: %s \"%v\"?\n", action, subject)
+ i := config.CommandDefault([]string{
+ "yYes, this is OK",
+ "nNo, skip this",
+ fmt.Sprintf("sSkip all %s operations with no more questions", action),
+ fmt.Sprintf("!Do all %s operations with no more questions", action),
+ "qExit rclone now.",
+ }, 0)
+
+ StdoutMutex.Unlock()
+
+ switch i {
+ case 'y':
+ skip = false
+ case 'n':
+ skip = true
+ case 's':
+ skip = true
+ skipped[action] = true
+ fs.Logf(nil, "Skipping all %s operations from now on without asking", action)
+ case '!':
+ skip = false
+ skipped[action] = false
+ fs.Logf(nil, "Doing all %s operations from now on without asking", action)
+ case 'q':
+ fs.Logf(nil, "Quitting rclone now")
+ atexit.Run()
+ os.Exit(0)
+ default:
+ skip = true
+ fs.Errorf(nil, "Bad choice %c", i)
+ }
+ return skip
+}
+
+// SkipDestructive should be called whenever rclone is about to do an destructive operation.
+//
+// It will check the --dry-run flag and it will ask the user if the --interactive flag is set.
+//
+// subject should be the object or directory in use
+//
+// action should be a descriptive word or short phrase
+//
+// Together they should make sense in this sentence: "Rclone is about
+// to action subject".
+func SkipDestructive(ctx context.Context, subject any, action string) (skip bool) {
+ var flag string
+ ci := fs.GetConfig(ctx)
+ switch {
+ case ci.DryRun:
+ flag = "--dry-run"
+ skip = true
+ case ci.Interactive:
+ flag = "--interactive"
+ interactiveMu.Lock()
+ defer interactiveMu.Unlock()
+ var found bool
+ skip, found = skipped[action]
+ if !found {
+ skip = skipDestructiveChoose(ctx, subject, action)
+ }
+ default:
+ return false
+ }
+ if skip {
+ size := int64(-1)
+ if do, ok := subject.(interface{ Size() int64 }); ok {
+ size = do.Size()
+ }
+ if size >= 0 {
+ fs.Logf(subject, "Skipped %s as %s is set (size %v)", fs.LogValue("skipped", action), flag, fs.LogValue("size", fs.SizeSuffix(size)))
+ } else {
+ fs.Logf(subject, "Skipped %s as %s is set", fs.LogValue("skipped", action), flag)
+ }
+ }
+ return skip
+}
+
+// Return the best way of describing the directory for the logs
+func dirName(f fs.Fs, dst fs.Directory, dir string) any {
+ if dst != nil {
+ if dst.Remote() != "" {
+ return dst
+ }
+ // Root is described as the Fs
+ return f
+ }
+ if dir != "" {
+ return dir
+ }
+ // Root is described as the Fs
+ return f
+}
+
+// CopyDirMetadata copies the src directory to dst or f if nil. If dst is nil then it uses
+// dir as the name of the new directory.
+//
+// It returns the destination directory if possible. Note that this may
+// be nil.
+func CopyDirMetadata(ctx context.Context, f fs.Fs, dst fs.Directory, dir string, src fs.Directory) (newDst fs.Directory, err error) {
+ ci := fs.GetConfig(ctx)
+ logName := dirName(f, dst, dir)
+ if SkipDestructive(ctx, logName, "update directory metadata") {
+ return nil, nil
+ }
+
+ // Options for the directory metadata
+ options := []fs.OpenOption{}
+ if ci.MetadataSet != nil {
+ options = append(options, fs.MetadataOption(ci.MetadataSet))
+ }
+
+ // Read metadata from src and add options and use metadata mapper
+ metadata, err := fs.GetMetadataOptions(ctx, f, src, options)
+ if err != nil {
+ return nil, err
+ }
+
+ // Fall back to ModTime if metadata not available
+ if metadata == nil {
+ metadata = fs.Metadata{}
+ }
+ if metadata["mtime"] == "" {
+ metadata["mtime"] = src.ModTime(ctx).Format(time.RFC3339Nano)
+ }
+
+ // Now set the metadata
+ if dst == nil {
+ do := f.Features().MkdirMetadata
+ if do == nil {
+ return nil, fmt.Errorf("internal error: expecting %v to have MkdirMetadata method: %w", f, fs.ErrorNotImplemented)
+ }
+ newDst, err = do(ctx, dir, metadata)
+ } else {
+ do, ok := dst.(fs.SetMetadataer)
+ if !ok {
+ return nil, fmt.Errorf("internal error: expecting directory %s (%T) from %v to have SetMetadata method: %w", logName, dst, f, fs.ErrorNotImplemented)
+ }
+ err = do.SetMetadata(ctx, metadata)
+ newDst = dst
+ }
+ if err != nil {
+ return nil, err
+ }
+ fs.Infof(logName, "Updated directory metadata")
+ return newDst, nil
+}
+
+// SetDirModTime sets the modtime on dst or dir
+//
+// If dst is nil then it uses dir as the name of the directory.
+//
+// It returns the destination directory if possible. Note that this
+// may be nil.
+//
+// It does not create the directory.
+func SetDirModTime(ctx context.Context, f fs.Fs, dst fs.Directory, dir string, modTime time.Time) (newDst fs.Directory, err error) {
+ logName := dirName(f, dst, dir)
+ ci := fs.GetConfig(ctx)
+ if ci.NoUpdateDirModTime {
+ fs.Debugf(logName, "Skipping set directory modification time as --no-update-dir-modtime is set")
+ return nil, nil
+ }
+ if SkipDestructive(ctx, logName, "set directory modification time") {
+ return nil, nil
+ }
+ if dst != nil {
+ dir = dst.Remote()
+ }
+
+ // Try to set the ModTime with the Directory.SetModTime method first as this is the most efficient
+ if dst != nil {
+ if do, ok := dst.(fs.SetModTimer); ok {
+ err := do.SetModTime(ctx, modTime)
+ if errors.Is(err, fs.ErrorNotImplemented) {
+ // Fall through and run the code below if not implemented
+ // This can happen for fs.DirWrapper instances
+ } else if err != nil {
+ return dst, err
+ } else {
+ fs.Infof(logName, "Set directory modification time (using SetModTime)")
+ return dst, nil
+ }
+ }
+ }
+
+ // Next try to set the ModTime with the Fs.DirSetModTime method as this works for non-metadata backends
+ if do := f.Features().DirSetModTime; do != nil {
+ err := do(ctx, dir, modTime)
+ if err != nil {
+ return dst, err
+ }
+ fs.Infof(logName, "Set directory modification time (using DirSetModTime)")
+ return dst, nil
+ }
+
+ // Something should have worked so return an error
+ return nil, fmt.Errorf("no method to set directory modtime found for %v (%T): %w", f, dst, fs.ErrorNotImplemented)
+}