mirror of
https://github.com/rclone/rclone.git
synced 2024-11-26 02:09:55 +08:00
1625 lines
43 KiB
Go
1625 lines
43 KiB
Go
// Generic operations on filesystems and objects
|
|
|
|
package fs
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"mime"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/pflag"
|
|
|
|
"golang.org/x/text/unicode/norm"
|
|
)
|
|
|
|
// CalculateModifyWindow works out modify window for Fses passed in -
|
|
// sets Config.ModifyWindow
|
|
//
|
|
// This is the largest modify window of all the fses in use, and the
|
|
// user configured value
|
|
func CalculateModifyWindow(fs ...Fs) {
|
|
for _, f := range fs {
|
|
if f != nil {
|
|
precision := f.Precision()
|
|
if precision > Config.ModifyWindow {
|
|
Config.ModifyWindow = precision
|
|
}
|
|
if precision == ModTimeNotSupported {
|
|
Infof(f, "Modify window not supported")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
Infof(fs[0], "Modify window is %s", Config.ModifyWindow)
|
|
}
|
|
|
|
// HashEquals checks to see if src == dst, but ignores empty strings
|
|
// and returns true if either is empty.
|
|
func HashEquals(src, dst string) bool {
|
|
if src == "" || dst == "" {
|
|
return true
|
|
}
|
|
return src == dst
|
|
}
|
|
|
|
// 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(src, dst Object) (equal bool, hash HashType, err error) {
|
|
common := src.Fs().Hashes().Overlap(dst.Fs().Hashes())
|
|
// Debugf(nil, "Shared hashes: %v", common)
|
|
if common.Count() == 0 {
|
|
return true, HashNone, nil
|
|
}
|
|
hash = common.GetOne()
|
|
srcHash, err := src.Hash(hash)
|
|
if err != nil {
|
|
Stats.Error()
|
|
Errorf(src, "Failed to calculate src hash: %v", err)
|
|
return false, hash, err
|
|
}
|
|
if srcHash == "" {
|
|
return true, HashNone, nil
|
|
}
|
|
dstHash, err := dst.Hash(hash)
|
|
if err != nil {
|
|
Stats.Error()
|
|
Errorf(dst, "Failed to calculate dst hash: %v", err)
|
|
return false, hash, err
|
|
}
|
|
if dstHash == "" {
|
|
return true, HashNone, nil
|
|
}
|
|
if srcHash != dstHash {
|
|
Debugf(src, "%v = %s (%v)", hash, srcHash, src.Fs())
|
|
Debugf(dst, "%v = %s (%v)", hash, dstHash, dst.Fs())
|
|
}
|
|
return srcHash == dstHash, hash, 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(src, dst Object) bool {
|
|
return equal(src, dst, Config.SizeOnly, Config.CheckSum)
|
|
}
|
|
|
|
func equal(src, dst Object, sizeOnly, checkSum bool) bool {
|
|
if !Config.IgnoreSize {
|
|
if src.Size() != dst.Size() {
|
|
Debugf(src, "Sizes differ")
|
|
return false
|
|
}
|
|
}
|
|
if sizeOnly {
|
|
Debugf(src, "Sizes identical")
|
|
return true
|
|
}
|
|
|
|
// Assert: Size is equal or being ignored
|
|
|
|
// If checking checksum and not modtime
|
|
if checkSum {
|
|
// Check the hash
|
|
same, hash, _ := CheckHashes(src, dst)
|
|
if !same {
|
|
Debugf(src, "%v differ", hash)
|
|
return false
|
|
}
|
|
if hash == HashNone {
|
|
Debugf(src, "Size of src and dst objects identical")
|
|
} else {
|
|
Debugf(src, "Size and %v of src and dst objects identical", hash)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Sizes the same so check the mtime
|
|
if Config.ModifyWindow == ModTimeNotSupported {
|
|
Debugf(src, "Sizes identical")
|
|
return true
|
|
}
|
|
srcModTime := src.ModTime()
|
|
dstModTime := dst.ModTime()
|
|
dt := dstModTime.Sub(srcModTime)
|
|
ModifyWindow := Config.ModifyWindow
|
|
if dt < ModifyWindow && dt > -ModifyWindow {
|
|
Debugf(src, "Size and modification time the same (differ by %s, within tolerance %s)", dt, ModifyWindow)
|
|
return true
|
|
}
|
|
|
|
Debugf(src, "Modification times differ by %s: %v, %v", dt, srcModTime, dstModTime)
|
|
|
|
// Check if the hashes are the same
|
|
same, hash, _ := CheckHashes(src, dst)
|
|
if !same {
|
|
Debugf(src, "%v differ", hash)
|
|
return false
|
|
}
|
|
if hash == HashNone {
|
|
// if couldn't check hash, return that they differ
|
|
return false
|
|
}
|
|
|
|
// mod time differs but hash is the same to reset mod time if required
|
|
if !Config.NoUpdateModTime {
|
|
if Config.DryRun {
|
|
Logf(src, "Not updating modification time as --dry-run")
|
|
} else {
|
|
// Size and hash the same but mtime different so update the
|
|
// mtime of the dst object here
|
|
err := dst.SetModTime(srcModTime)
|
|
if err == ErrorCantSetModTime {
|
|
Debugf(dst, "src and dst identical but can't set mod time without re-uploading")
|
|
return false
|
|
} else if err == ErrorCantSetModTimeWithoutDelete {
|
|
Debugf(dst, "src and dst identical but can't set mod time without deleting and re-uploading")
|
|
err = dst.Remove()
|
|
if err != nil {
|
|
Errorf(dst, "failed to delete before re-upload: %v", err)
|
|
}
|
|
return false
|
|
} else if err != nil {
|
|
Stats.Error()
|
|
Errorf(dst, "Failed to set modification time: %v", err)
|
|
} else {
|
|
Infof(src, "Updated modification time in destination")
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// MimeTypeFromName returns a guess at the mime type from the name
|
|
func MimeTypeFromName(remote string) (mimeType string) {
|
|
mimeType = mime.TypeByExtension(path.Ext(remote))
|
|
if !strings.ContainsRune(mimeType, '/') {
|
|
mimeType = "application/octet-stream"
|
|
}
|
|
return mimeType
|
|
}
|
|
|
|
// MimeType returns the MimeType from the object, either by calling
|
|
// the MimeTyper interface or using MimeTypeFromName
|
|
func MimeType(o ObjectInfo) (mimeType string) {
|
|
// Read the MimeType from the optional interface if available
|
|
if do, ok := o.(MimeTyper); ok {
|
|
mimeType = do.MimeType()
|
|
// Debugf(o, "Read MimeType as %q", mimeType)
|
|
if mimeType != "" {
|
|
return mimeType
|
|
}
|
|
}
|
|
return MimeTypeFromName(o.Remote())
|
|
}
|
|
|
|
// Used to remove a failed copy
|
|
//
|
|
// Returns whether the file was succesfully removed or not
|
|
func removeFailedCopy(dst Object) bool {
|
|
if dst == nil {
|
|
return false
|
|
}
|
|
Infof(dst, "Removing failed copy")
|
|
removeErr := dst.Remove()
|
|
if removeErr != nil {
|
|
Infof(dst, "Failed to remove failed copy: %s", removeErr)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Wrapper to override the remote for an object
|
|
type overrideRemoteObject struct {
|
|
Object
|
|
remote string
|
|
}
|
|
|
|
// Remote returns the overriden remote name
|
|
func (o *overrideRemoteObject) Remote() string {
|
|
return o.remote
|
|
}
|
|
|
|
// MimeType returns the mime type of the underlying object or "" if it
|
|
// can't be worked out
|
|
func (o *overrideRemoteObject) MimeType() string {
|
|
if do, ok := o.Object.(MimeTyper); ok {
|
|
return do.MimeType()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Check interface is satisfied
|
|
var _ MimeTyper = (*overrideRemoteObject)(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.
|
|
func Copy(f Fs, dst Object, remote string, src Object) (err error) {
|
|
if Config.DryRun {
|
|
Logf(src, "Not copying as --dry-run")
|
|
return nil
|
|
}
|
|
maxTries := Config.LowLevelRetries
|
|
tries := 0
|
|
doUpdate := dst != nil
|
|
// work out which hash to use - limit to 1 hash in common
|
|
var common HashSet
|
|
hashType := HashNone
|
|
if !Config.SizeOnly {
|
|
common = src.Fs().Hashes().Overlap(f.Hashes())
|
|
if common.Count() > 0 {
|
|
hashType = common.GetOne()
|
|
common = HashSet(hashType)
|
|
}
|
|
}
|
|
hashOption := &HashesOption{Hashes: common}
|
|
var actionTaken string
|
|
for {
|
|
// Try server side copy first - if has optional interface and
|
|
// is same underlying remote
|
|
actionTaken = "Copied (server side copy)"
|
|
if doCopy := f.Features().Copy; doCopy != nil && SameConfig(src.Fs(), f) {
|
|
var newDst Object
|
|
newDst, err = doCopy(src, remote)
|
|
if err == nil {
|
|
dst = newDst
|
|
}
|
|
} else {
|
|
err = ErrorCantCopy
|
|
}
|
|
// If can't server side copy, do it manually
|
|
if err == ErrorCantCopy {
|
|
var in0 io.ReadCloser
|
|
in0, err = src.Open(hashOption)
|
|
if err != nil {
|
|
err = errors.Wrap(err, "failed to open source object")
|
|
} else {
|
|
in := NewAccount(in0, src).WithBuffer() // account and buffer the transfer
|
|
var wrappedSrc ObjectInfo = src
|
|
// We try to pass the original object if possible
|
|
if src.Remote() != remote {
|
|
wrappedSrc = &overrideRemoteObject{Object: src, remote: remote}
|
|
}
|
|
if doUpdate {
|
|
actionTaken = "Copied (replaced existing)"
|
|
err = dst.Update(in, wrappedSrc, hashOption)
|
|
} else {
|
|
actionTaken = "Copied (new)"
|
|
dst, err = f.Put(in, wrappedSrc, hashOption)
|
|
}
|
|
closeErr := in.Close()
|
|
if err == nil {
|
|
err = closeErr
|
|
}
|
|
}
|
|
}
|
|
tries++
|
|
if tries >= maxTries {
|
|
break
|
|
}
|
|
// Retry if err returned a retry error
|
|
if IsRetryError(err) || ShouldRetry(err) {
|
|
Debugf(src, "Received error: %v - low level retry %d/%d", err, tries, maxTries)
|
|
continue
|
|
}
|
|
// otherwise finish
|
|
break
|
|
}
|
|
if err != nil {
|
|
Stats.Error()
|
|
Errorf(src, "Failed to copy: %v", err)
|
|
return err
|
|
}
|
|
|
|
// Verify sizes are the same after transfer
|
|
if !Config.IgnoreSize && src.Size() != dst.Size() {
|
|
Stats.Error()
|
|
err = errors.Errorf("corrupted on transfer: sizes differ %d vs %d", src.Size(), dst.Size())
|
|
Errorf(dst, "%v", err)
|
|
removeFailedCopy(dst)
|
|
return err
|
|
}
|
|
|
|
// Verify hashes are the same after transfer - ignoring blank hashes
|
|
// TODO(klauspost): This could be extended, so we always create a hash type matching
|
|
// the destination, and calculate it while sending.
|
|
if hashType != HashNone {
|
|
var srcSum string
|
|
srcSum, err = src.Hash(hashType)
|
|
if err != nil {
|
|
Stats.Error()
|
|
Errorf(src, "Failed to read src hash: %v", err)
|
|
} else if srcSum != "" {
|
|
var dstSum string
|
|
dstSum, err = dst.Hash(hashType)
|
|
if err != nil {
|
|
Stats.Error()
|
|
Errorf(dst, "Failed to read hash: %v", err)
|
|
} else if !Config.IgnoreChecksum && !HashEquals(srcSum, dstSum) {
|
|
Stats.Error()
|
|
err = errors.Errorf("corrupted on transfer: %v hash differ %q vs %q", hashType, srcSum, dstSum)
|
|
Errorf(dst, "%v", err)
|
|
removeFailedCopy(dst)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
Infof(src, actionTaken)
|
|
return err
|
|
}
|
|
|
|
// Move src object to dst or fdst if nil. If dst is nil then it uses
|
|
// remote as the name of the new object.
|
|
func Move(fdst Fs, dst Object, remote string, src Object) (err error) {
|
|
if Config.DryRun {
|
|
Logf(src, "Not moving as --dry-run")
|
|
return nil
|
|
}
|
|
// See if we have Move available
|
|
if doMove := fdst.Features().Move; doMove != nil && SameConfig(src.Fs(), fdst) {
|
|
// Delete destination if it exists
|
|
if dst != nil {
|
|
err = DeleteFile(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Move dst <- src
|
|
_, err := doMove(src, remote)
|
|
switch err {
|
|
case nil:
|
|
Infof(src, "Moved (server side)")
|
|
return nil
|
|
case ErrorCantMove:
|
|
Debugf(src, "Can't move, switching to copy")
|
|
default:
|
|
Stats.Error()
|
|
Errorf(src, "Couldn't move: %v", err)
|
|
return err
|
|
}
|
|
}
|
|
// Move not found or didn't work so copy dst <- src
|
|
err = Copy(fdst, dst, remote, src)
|
|
if err != nil {
|
|
Errorf(src, "Not deleting source as copy failed: %v", err)
|
|
return err
|
|
}
|
|
// Delete src if no error on copy
|
|
return DeleteFile(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) bool {
|
|
canMove := fdst.Features().Move != nil
|
|
canCopy := fdst.Features().Copy != nil
|
|
return canMove || canCopy
|
|
}
|
|
|
|
// 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(dst Object, backupDir Fs) (err error) {
|
|
Stats.Checking(dst.Remote())
|
|
action, actioned, actioning := "delete", "Deleted", "deleting"
|
|
if backupDir != nil {
|
|
action, actioned, actioning = "move into backup dir", "Moved into backup dir", "moving into backup dir"
|
|
}
|
|
if Config.DryRun {
|
|
Logf(dst, "Not %s as --dry-run", actioning)
|
|
} else if backupDir != nil {
|
|
if !SameConfig(dst.Fs(), backupDir) {
|
|
err = errors.New("parameter to --backup-dir has to be on the same remote as destination")
|
|
} else {
|
|
remoteWithSuffix := dst.Remote() + Config.Suffix
|
|
overwritten, _ := backupDir.NewObject(remoteWithSuffix)
|
|
err = Move(backupDir, overwritten, remoteWithSuffix, dst)
|
|
}
|
|
} else {
|
|
err = dst.Remove()
|
|
}
|
|
if err != nil {
|
|
Stats.Error()
|
|
Errorf(dst, "Couldn't %s: %v", action, err)
|
|
} else if !Config.DryRun {
|
|
Infof(dst, actioned)
|
|
}
|
|
Stats.DoneChecking(dst.Remote())
|
|
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(dst Object) (err error) {
|
|
return deleteFileWithBackupDir(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(toBeDeleted ObjectsChan, backupDir Fs) error {
|
|
var wg sync.WaitGroup
|
|
wg.Add(Config.Transfers)
|
|
var errorCount int32
|
|
for i := 0; i < Config.Transfers; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for dst := range toBeDeleted {
|
|
err := deleteFileWithBackupDir(dst, backupDir)
|
|
if err != nil {
|
|
atomic.AddInt32(&errorCount, 1)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
Infof(nil, "Waiting for deletions to finish")
|
|
wg.Wait()
|
|
if errorCount > 0 {
|
|
return errors.Errorf("failed to delete %d files", errorCount)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteFiles removes all the files passed in the channel
|
|
func DeleteFiles(toBeDeleted ObjectsChan) error {
|
|
return deleteFilesWithBackupDir(toBeDeleted, nil)
|
|
}
|
|
|
|
// Read a Objects into add() for the given Fs.
|
|
// dir is the start directory, "" for root
|
|
// If includeAll is specified all files will be added,
|
|
// otherwise only files passing the filter will be added.
|
|
//
|
|
// Each object is passed ito the function provided. If that returns
|
|
// an error then the listing will be aborted and that error returned.
|
|
func readFilesFn(fs Fs, includeAll bool, dir string, add func(Object) error) (err error) {
|
|
return Walk(fs, "", includeAll, Config.MaxDepth, func(dirPath string, entries DirEntries, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return entries.ForObjectError(add)
|
|
})
|
|
}
|
|
|
|
// DirEntries is a slice of Object or *Dir
|
|
type DirEntries []DirEntry
|
|
|
|
// Len is part of sort.Interface.
|
|
func (ds DirEntries) Len() int {
|
|
return len(ds)
|
|
}
|
|
|
|
// Swap is part of sort.Interface.
|
|
func (ds DirEntries) Swap(i, j int) {
|
|
ds[i], ds[j] = ds[j], ds[i]
|
|
}
|
|
|
|
// Less is part of sort.Interface.
|
|
func (ds DirEntries) Less(i, j int) bool {
|
|
return ds[i].Remote() < ds[j].Remote()
|
|
}
|
|
|
|
// ForObject runs the function supplied on every object in the entries
|
|
func (ds DirEntries) ForObject(fn func(o Object)) {
|
|
for _, entry := range ds {
|
|
o, ok := entry.(Object)
|
|
if ok {
|
|
fn(o)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ForObjectError runs the function supplied on every object in the entries
|
|
func (ds DirEntries) ForObjectError(fn func(o Object) error) error {
|
|
for _, entry := range ds {
|
|
o, ok := entry.(Object)
|
|
if ok {
|
|
err := fn(o)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ForDir runs the function supplied on every Directory in the entries
|
|
func (ds DirEntries) ForDir(fn func(dir Directory)) {
|
|
for _, entry := range ds {
|
|
dir, ok := entry.(Directory)
|
|
if ok {
|
|
fn(dir)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ForDirError runs the function supplied on every Directory in the entries
|
|
func (ds DirEntries) ForDirError(fn func(dir Directory) error) error {
|
|
for _, entry := range ds {
|
|
dir, ok := entry.(Directory)
|
|
if ok {
|
|
err := fn(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DirEntryType returns a string description of the DirEntry, either
|
|
// "object", "directory" or "unknown type XXX"
|
|
func DirEntryType(d DirEntry) string {
|
|
switch d.(type) {
|
|
case Object:
|
|
return "object"
|
|
case Directory:
|
|
return "directory"
|
|
}
|
|
return fmt.Sprintf("unknown type %T", d)
|
|
}
|
|
|
|
// ListDirSorted reads Object and *Dir into entries for the given Fs.
|
|
//
|
|
// dir is the start directory, "" for root
|
|
//
|
|
// If includeAll is specified all files will be added, otherwise only
|
|
// files and directories passing the filter will be added.
|
|
//
|
|
// Files will be returned in sorted order
|
|
func ListDirSorted(fs Fs, includeAll bool, dir string) (entries DirEntries, err error) {
|
|
// Get unfiltered entries from the fs
|
|
entries, err = fs.List(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return filterAndSortDir(entries, includeAll, dir, Config.Filter.IncludeObject, Config.Filter.IncludeDirectory)
|
|
}
|
|
|
|
// filter (if required) and check the entries, then sort them
|
|
func filterAndSortDir(entries DirEntries, includeAll bool, dir string,
|
|
IncludeObject func(o Object) bool,
|
|
IncludeDirectory func(remote string) bool) (newEntries DirEntries, err error) {
|
|
newEntries = entries[:0] // in place filter
|
|
prefix := ""
|
|
if dir != "" {
|
|
prefix = dir + "/"
|
|
}
|
|
for _, entry := range entries {
|
|
ok := true
|
|
// check includes and types
|
|
switch x := entry.(type) {
|
|
case Object:
|
|
// Make sure we don't delete excluded files if not required
|
|
if !includeAll && !IncludeObject(x) {
|
|
ok = false
|
|
Debugf(x, "Excluded from sync (and deletion)")
|
|
}
|
|
case Directory:
|
|
if !includeAll && !IncludeDirectory(x.Remote()) {
|
|
ok = false
|
|
Debugf(x, "Excluded from sync (and deletion)")
|
|
}
|
|
default:
|
|
return nil, errors.Errorf("unknown object type %T", entry)
|
|
}
|
|
// check remote name belongs in this directry
|
|
remote := entry.Remote()
|
|
switch {
|
|
case !ok:
|
|
// ignore
|
|
case !strings.HasPrefix(remote, prefix):
|
|
ok = false
|
|
Errorf(entry, "Entry doesn't belong in directory %q (too short) - ignoring", dir)
|
|
case remote == prefix:
|
|
ok = false
|
|
Errorf(entry, "Entry doesn't belong in directory %q (same as directory) - ignoring", dir)
|
|
case strings.ContainsRune(remote[len(prefix):], '/'):
|
|
ok = false
|
|
Errorf(entry, "Entry doesn't belong in directory %q (contains subdir) - ignoring", dir)
|
|
default:
|
|
// ok
|
|
}
|
|
if ok {
|
|
newEntries = append(newEntries, entry)
|
|
}
|
|
}
|
|
entries = newEntries
|
|
|
|
// Sort the directory entries by Remote
|
|
//
|
|
// We use a stable sort here just in case there are
|
|
// duplicates. Assuming the remote delivers the entries in a
|
|
// consistent order, this will give the best user experience
|
|
// in syncing as it will use the first entry for the sync
|
|
// comparison.
|
|
sort.Stable(entries)
|
|
return entries, nil
|
|
}
|
|
|
|
// Read a map of Object.Remote to Object for the given Fs.
|
|
// dir is the start directory, "" for root
|
|
// If includeAll is specified all files will be added,
|
|
// otherwise only files passing the filter will be added.
|
|
//
|
|
// This also detects duplicates and normalised duplicates
|
|
func readFilesMap(fs Fs, includeAll bool, dir string) (files map[string]Object, err error) {
|
|
files = make(map[string]Object)
|
|
normalised := make(map[string]struct{})
|
|
err = readFilesFn(fs, includeAll, dir, func(o Object) error {
|
|
remote := o.Remote()
|
|
normalisedRemote := strings.ToLower(norm.NFC.String(remote))
|
|
if _, ok := files[remote]; !ok {
|
|
files[remote] = o
|
|
if _, ok := normalised[normalisedRemote]; ok {
|
|
Logf(o, "File found with same name but different case on %v", o.Fs())
|
|
}
|
|
} else {
|
|
Logf(o, "Duplicate file detected")
|
|
}
|
|
normalised[normalisedRemote] = struct{}{}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
err = errors.Wrapf(err, "error listing: %s", fs)
|
|
}
|
|
return files, err
|
|
}
|
|
|
|
// readFilesMaps runs readFilesMap on fdst and fsrc at the same time
|
|
// dir is the start directory, "" for root
|
|
func readFilesMaps(fdst Fs, fdstIncludeAll bool, fsrc Fs, fsrcIncludeAll bool, dir string) (dstFiles, srcFiles map[string]Object, err error) {
|
|
var wg sync.WaitGroup
|
|
var srcErr, dstErr error
|
|
|
|
list := func(fs Fs, includeAll bool, pMap *map[string]Object, pErr *error) {
|
|
defer wg.Done()
|
|
Infof(fs, "Building file list")
|
|
files, listErr := readFilesMap(fs, includeAll, dir)
|
|
if listErr != nil {
|
|
Errorf(fs, "Error building file list: %v", listErr)
|
|
*pErr = listErr
|
|
} else {
|
|
Debugf(fs, "Done building file list")
|
|
*pMap = files
|
|
}
|
|
}
|
|
|
|
wg.Add(2)
|
|
go list(fdst, fdstIncludeAll, &dstFiles, &srcErr)
|
|
go list(fsrc, fsrcIncludeAll, &srcFiles, &dstErr)
|
|
wg.Wait()
|
|
|
|
if srcErr != nil {
|
|
err = srcErr
|
|
}
|
|
if dstErr != nil {
|
|
err = dstErr
|
|
}
|
|
return dstFiles, srcFiles, err
|
|
}
|
|
|
|
// SameConfig returns true if fdst and fsrc are using the same config
|
|
// file entry
|
|
func SameConfig(fdst, fsrc Info) bool {
|
|
return fdst.Name() == fsrc.Name()
|
|
}
|
|
|
|
// Same returns true if fdst and fsrc point to the same underlying Fs
|
|
func Same(fdst, fsrc Info) bool {
|
|
return SameConfig(fdst, fsrc) && fdst.Root() == fsrc.Root()
|
|
}
|
|
|
|
// Overlapping returns true if fdst and fsrc point to the same
|
|
// underlying Fs and they overlap.
|
|
func Overlapping(fdst, fsrc Info) bool {
|
|
if !SameConfig(fdst, fsrc) {
|
|
return false
|
|
}
|
|
// Return the Root with a trailing / if not empty
|
|
fixedRoot := func(f Info) string {
|
|
s := strings.Trim(f.Root(), "/")
|
|
if s != "" {
|
|
s += "/"
|
|
}
|
|
return s
|
|
}
|
|
fdstRoot := fixedRoot(fdst)
|
|
fsrcRoot := fixedRoot(fsrc)
|
|
return strings.HasPrefix(fdstRoot, fsrcRoot) || strings.HasPrefix(fsrcRoot, fdstRoot)
|
|
}
|
|
|
|
// checkIdentical checks to see if dst and src are identical
|
|
//
|
|
// it returns true if differences were found
|
|
// it also returns whether it couldn't be hashed
|
|
func checkIdentical(dst, src Object) (differ bool, noHash bool) {
|
|
same, hash, err := CheckHashes(src, dst)
|
|
if err != nil {
|
|
// CheckHashes will log and count errors
|
|
return true, false
|
|
}
|
|
if hash == HashNone {
|
|
return false, true
|
|
}
|
|
if !same {
|
|
Stats.Error()
|
|
Errorf(src, "%v differ", hash)
|
|
return true, false
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
// CheckFn checks the files in fsrc and fdst according to Size and
|
|
// hash using checkFunction on each file to check the hashes.
|
|
//
|
|
// checkFunction sees if dst and src are identical
|
|
//
|
|
// it returns true if differences were found
|
|
// it also returns whether it couldn't be hashed
|
|
func CheckFn(fdst, fsrc Fs, checkFunction func(a, b Object) (differ bool, noHash bool)) error {
|
|
dstFiles, srcFiles, err := readFilesMaps(fdst, false, fsrc, false, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
differences := int32(0)
|
|
noHashes := int32(0)
|
|
|
|
// FIXME could do this as it goes along and make it use less
|
|
// memory.
|
|
|
|
// Move all the common files into commonFiles and delete then
|
|
// from srcFiles and dstFiles
|
|
commonFiles := make(map[string][2]Object)
|
|
for remote, src := range srcFiles {
|
|
if dst, ok := dstFiles[remote]; ok {
|
|
commonFiles[remote] = [2]Object{dst, src}
|
|
delete(srcFiles, remote)
|
|
delete(dstFiles, remote)
|
|
}
|
|
}
|
|
|
|
Logf(fdst, "%d files not in %v", len(dstFiles), fsrc)
|
|
for _, dst := range dstFiles {
|
|
Stats.Error()
|
|
Errorf(dst, "File not in %v", fsrc)
|
|
atomic.AddInt32(&differences, 1)
|
|
}
|
|
|
|
Logf(fsrc, "%d files not in %s", len(srcFiles), fdst)
|
|
for _, src := range srcFiles {
|
|
Stats.Error()
|
|
Errorf(src, "File not in %v", fdst)
|
|
atomic.AddInt32(&differences, 1)
|
|
}
|
|
|
|
checks := make(chan [2]Object, Config.Transfers)
|
|
go func() {
|
|
for _, check := range commonFiles {
|
|
checks <- check
|
|
}
|
|
close(checks)
|
|
}()
|
|
|
|
checkIdentical := func(dst, src Object) (differ bool, noHash bool) {
|
|
Stats.Checking(src.Remote())
|
|
defer Stats.DoneChecking(src.Remote())
|
|
if src.Size() != dst.Size() {
|
|
Stats.Error()
|
|
Errorf(src, "Sizes differ")
|
|
return true, false
|
|
}
|
|
if Config.SizeOnly {
|
|
return false, false
|
|
}
|
|
return checkFunction(dst, src)
|
|
}
|
|
|
|
var checkerWg sync.WaitGroup
|
|
checkerWg.Add(Config.Checkers)
|
|
for i := 0; i < Config.Checkers; i++ {
|
|
go func() {
|
|
defer checkerWg.Done()
|
|
for check := range checks {
|
|
differ, noHash := checkIdentical(check[0], check[1])
|
|
if differ {
|
|
atomic.AddInt32(&differences, 1)
|
|
} else {
|
|
Debugf(check[0], "OK")
|
|
}
|
|
if noHash {
|
|
atomic.AddInt32(&noHashes, 1)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
Infof(fdst, "Waiting for checks to finish")
|
|
checkerWg.Wait()
|
|
Logf(fdst, "%d differences found", Stats.GetErrors())
|
|
if noHashes > 0 {
|
|
Logf(fdst, "%d hashes could not be checked", noHashes)
|
|
}
|
|
if differences > 0 {
|
|
return errors.Errorf("%d differences found", differences)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Check the files in fsrc and fdst according to Size and hash
|
|
func Check(fdst, fsrc Fs) error {
|
|
return CheckFn(fdst, fsrc, checkIdentical)
|
|
}
|
|
|
|
// ReadFill reads as much data from r into buf as it can
|
|
//
|
|
// It reads until the buffer is full or r.Read returned an error.
|
|
//
|
|
// This is io.ReadFull but when you just want as much data as
|
|
// possible, not an exact size of block.
|
|
func ReadFill(r io.Reader, buf []byte) (n int, err error) {
|
|
var nn int
|
|
for n < len(buf) && err == nil {
|
|
nn, err = r.Read(buf[n:])
|
|
n += nn
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
// CheckEqualReaders checks to see if in1 and in2 have the same
|
|
// content when read.
|
|
//
|
|
// it returns true if differences were found
|
|
func CheckEqualReaders(in1, in2 io.Reader) (differ bool, err error) {
|
|
const bufSize = 64 * 1024
|
|
buf1 := make([]byte, bufSize)
|
|
buf2 := make([]byte, bufSize)
|
|
for {
|
|
n1, err1 := ReadFill(in1, buf1)
|
|
n2, err2 := ReadFill(in2, buf2)
|
|
// check errors
|
|
if err1 != nil && err1 != io.EOF {
|
|
return true, err1
|
|
} else if err2 != nil && err2 != io.EOF {
|
|
return true, err2
|
|
}
|
|
// err1 && err2 are nil or io.EOF here
|
|
// process the data
|
|
if n1 != n2 || !bytes.Equal(buf1[:n1], buf2[:n2]) {
|
|
return true, nil
|
|
}
|
|
// if both streams finished the we have finished
|
|
if err1 == io.EOF && err2 == io.EOF {
|
|
break
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// CheckIdentical checks to see if dst and src are identical by
|
|
// reading all their bytes if necessary.
|
|
//
|
|
// it returns true if differences were found
|
|
func CheckIdentical(dst, src Object) (differ bool, err error) {
|
|
in1, err := dst.Open()
|
|
if err != nil {
|
|
return true, errors.Wrapf(err, "failed to open %q", dst)
|
|
}
|
|
in1 = NewAccount(in1, dst).WithBuffer() // account and buffer the transfer
|
|
defer CheckClose(in1, &err)
|
|
|
|
in2, err := src.Open()
|
|
if err != nil {
|
|
return true, errors.Wrapf(err, "failed to open %q", src)
|
|
}
|
|
in2 = NewAccount(in2, src).WithBuffer() // account and buffer the transfer
|
|
defer CheckClose(in2, &err)
|
|
|
|
return CheckEqualReaders(in1, in2)
|
|
}
|
|
|
|
// CheckDownload checks the files in fsrc and fdst according to Size
|
|
// and the actual contents of the files.
|
|
func CheckDownload(fdst, fsrc Fs) error {
|
|
check := func(a, b Object) (differ bool, noHash bool) {
|
|
differ, err := CheckIdentical(a, b)
|
|
if err != nil {
|
|
Stats.Error()
|
|
Errorf(a, "Failed to download: %v", err)
|
|
return true, true
|
|
}
|
|
return differ, false
|
|
}
|
|
return CheckFn(fdst, fsrc, check)
|
|
}
|
|
|
|
// ListFn lists the Fs to the supplied function
|
|
//
|
|
// Lists in parallel which may get them out of order
|
|
func ListFn(f Fs, fn func(Object)) error {
|
|
return Walk(f, "", false, Config.MaxDepth, func(dirPath string, entries DirEntries, err error) error {
|
|
if err != nil {
|
|
// FIXME count errors and carry on for listing
|
|
return err
|
|
}
|
|
entries.ForObject(fn)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// mutex for synchronized output
|
|
var outMutex sync.Mutex
|
|
|
|
// Synchronized fmt.Fprintf
|
|
//
|
|
// Ignores errors from Fprintf
|
|
func syncFprintf(w io.Writer, format string, a ...interface{}) {
|
|
outMutex.Lock()
|
|
defer outMutex.Unlock()
|
|
_, _ = fmt.Fprintf(w, format, a...)
|
|
}
|
|
|
|
// 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(f Fs, w io.Writer) error {
|
|
return ListFn(f, func(o Object) {
|
|
syncFprintf(w, "%9d %s\n", o.Size(), 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(f Fs, w io.Writer) error {
|
|
return ListFn(f, func(o Object) {
|
|
Stats.Checking(o.Remote())
|
|
modTime := o.ModTime()
|
|
Stats.DoneChecking(o.Remote())
|
|
syncFprintf(w, "%9d %s %s\n", o.Size(), modTime.Local().Format("2006-01-02 15:04:05.000000000"), o.Remote())
|
|
})
|
|
}
|
|
|
|
// Md5sum list the Fs to the supplied writer
|
|
//
|
|
// Produces the same output as the md5sum command - obeys includes and
|
|
// excludes
|
|
//
|
|
// Lists in parallel which may get them out of order
|
|
func Md5sum(f Fs, w io.Writer) error {
|
|
return hashLister(HashMD5, f, w)
|
|
}
|
|
|
|
// Sha1sum list the Fs to the supplied writer
|
|
//
|
|
// Obeys includes and excludes
|
|
//
|
|
// Lists in parallel which may get them out of order
|
|
func Sha1sum(f Fs, w io.Writer) error {
|
|
return hashLister(HashSHA1, f, w)
|
|
}
|
|
|
|
// DropboxHashSum list the Fs to the supplied writer
|
|
//
|
|
// Obeys includes and excludes
|
|
//
|
|
// Lists in parallel which may get them out of order
|
|
func DropboxHashSum(f Fs, w io.Writer) error {
|
|
return hashLister(HashDropbox, f, w)
|
|
}
|
|
|
|
func hashLister(ht HashType, f Fs, w io.Writer) error {
|
|
return ListFn(f, func(o Object) {
|
|
Stats.Checking(o.Remote())
|
|
sum, err := o.Hash(ht)
|
|
Stats.DoneChecking(o.Remote())
|
|
if err == ErrHashUnsupported {
|
|
sum = "UNSUPPORTED"
|
|
} else if err != nil {
|
|
Debugf(o, "Failed to read %v: %v", ht, err)
|
|
sum = "ERROR"
|
|
}
|
|
syncFprintf(w, "%*s %s\n", HashWidth[ht], sum, o.Remote())
|
|
})
|
|
}
|
|
|
|
// Count counts the objects and their sizes in the Fs
|
|
//
|
|
// Obeys includes and excludes
|
|
func Count(f Fs) (objects int64, size int64, err error) {
|
|
err = ListFn(f, func(o Object) {
|
|
atomic.AddInt64(&objects, 1)
|
|
atomic.AddInt64(&size, o.Size())
|
|
})
|
|
return
|
|
}
|
|
|
|
// ConfigMaxDepth returns the depth to use for a recursive or non recursive listing.
|
|
func ConfigMaxDepth(recursive bool) int {
|
|
depth := Config.MaxDepth
|
|
if !recursive && depth < 0 {
|
|
depth = 1
|
|
}
|
|
return depth
|
|
}
|
|
|
|
// ListDir lists the directories/buckets/containers in the Fs to the supplied writer
|
|
func ListDir(f Fs, w io.Writer) error {
|
|
return Walk(f, "", false, ConfigMaxDepth(false), func(dirPath string, entries DirEntries, err error) error {
|
|
if err != nil {
|
|
// FIXME count errors and carry on for listing
|
|
return err
|
|
}
|
|
entries.ForDir(func(dir Directory) {
|
|
if dir != nil {
|
|
syncFprintf(w, "%12d %13s %9d %s\n", dir.Size(), dir.ModTime().Format("2006-01-02 15:04:05"), dir.Items(), dir.Remote())
|
|
}
|
|
})
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// logDirName returns an object for the logger
|
|
func logDirName(f Fs, dir string) interface{} {
|
|
if dir != "" {
|
|
return dir
|
|
}
|
|
return f
|
|
}
|
|
|
|
// Mkdir makes a destination directory or container
|
|
func Mkdir(f Fs, dir string) error {
|
|
if Config.DryRun {
|
|
Logf(logDirName(f, dir), "Not making directory as dry run is set")
|
|
return nil
|
|
}
|
|
Debugf(logDirName(f, dir), "Making directory")
|
|
err := f.Mkdir(dir)
|
|
if err != nil {
|
|
Stats.Error()
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TryRmdir removes a container but not if not empty. It doesn't
|
|
// count errors but may return one.
|
|
func TryRmdir(f Fs, dir string) error {
|
|
if Config.DryRun {
|
|
Logf(logDirName(f, dir), "Not deleting as dry run is set")
|
|
return nil
|
|
}
|
|
Debugf(logDirName(f, dir), "Removing directory")
|
|
return f.Rmdir(dir)
|
|
}
|
|
|
|
// Rmdir removes a container but not if not empty
|
|
func Rmdir(f Fs, dir string) error {
|
|
err := TryRmdir(f, dir)
|
|
if err != nil {
|
|
Stats.Error()
|
|
return err
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Purge removes a container and all of its contents
|
|
func Purge(f Fs) error {
|
|
doFallbackPurge := true
|
|
var err error
|
|
if doPurge := f.Features().Purge; doPurge != nil {
|
|
doFallbackPurge = false
|
|
if Config.DryRun {
|
|
Logf(f, "Not purging as --dry-run set")
|
|
} else {
|
|
err = doPurge()
|
|
if err == ErrorCantPurge {
|
|
doFallbackPurge = true
|
|
}
|
|
}
|
|
}
|
|
if doFallbackPurge {
|
|
// DeleteFiles and Rmdir observe --dry-run
|
|
err = DeleteFiles(listToChan(f))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = Rmdirs(f, "")
|
|
}
|
|
if err != nil {
|
|
Stats.Error()
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Delete removes all the contents of a container. Unlike Purge, it
|
|
// obeys includes and excludes.
|
|
func Delete(f Fs) error {
|
|
delete := make(ObjectsChan, Config.Transfers)
|
|
delErr := make(chan error, 1)
|
|
go func() {
|
|
delErr <- DeleteFiles(delete)
|
|
}()
|
|
err := ListFn(f, func(o Object) {
|
|
delete <- o
|
|
})
|
|
close(delete)
|
|
delError := <-delErr
|
|
if err == nil {
|
|
err = delError
|
|
}
|
|
return err
|
|
}
|
|
|
|
// dedupeRename renames the objs slice to different names
|
|
func dedupeRename(remote string, objs []Object) {
|
|
f := objs[0].Fs()
|
|
doMove := f.Features().Move
|
|
if doMove == nil {
|
|
log.Fatalf("Fs %v doesn't support Move", f)
|
|
}
|
|
ext := path.Ext(remote)
|
|
base := remote[:len(remote)-len(ext)]
|
|
for i, o := range objs {
|
|
newName := fmt.Sprintf("%s-%d%s", base, i+1, ext)
|
|
if !Config.DryRun {
|
|
newObj, err := doMove(o, newName)
|
|
if err != nil {
|
|
Stats.Error()
|
|
Errorf(o, "Failed to rename: %v", err)
|
|
continue
|
|
}
|
|
Infof(newObj, "renamed from: %v", o)
|
|
} else {
|
|
Logf(remote, "Not renaming to %q as --dry-run", newName)
|
|
}
|
|
}
|
|
}
|
|
|
|
// dedupeDeleteAllButOne deletes all but the one in keep
|
|
func dedupeDeleteAllButOne(keep int, remote string, objs []Object) {
|
|
for i, o := range objs {
|
|
if i == keep {
|
|
continue
|
|
}
|
|
_ = DeleteFile(o)
|
|
}
|
|
Logf(remote, "Deleted %d extra copies", len(objs)-1)
|
|
}
|
|
|
|
// dedupeDeleteIdentical deletes all but one of identical (by hash) copies
|
|
func dedupeDeleteIdentical(remote string, objs []Object) []Object {
|
|
// See how many of these duplicates are identical
|
|
byHash := make(map[string][]Object, len(objs))
|
|
for _, o := range objs {
|
|
md5sum, err := o.Hash(HashMD5)
|
|
if err == nil {
|
|
byHash[md5sum] = append(byHash[md5sum], o)
|
|
}
|
|
}
|
|
|
|
// Delete identical duplicates, refilling obj with the ones remaining
|
|
objs = nil
|
|
for md5sum, hashObjs := range byHash {
|
|
if len(hashObjs) > 1 {
|
|
Logf(remote, "Deleting %d/%d identical duplicates (md5sum %q)", len(hashObjs)-1, len(hashObjs), md5sum)
|
|
for _, o := range hashObjs[1:] {
|
|
_ = DeleteFile(o)
|
|
}
|
|
}
|
|
objs = append(objs, hashObjs[0])
|
|
}
|
|
|
|
return objs
|
|
}
|
|
|
|
// dedupeInteractive interactively dedupes the slice of objects
|
|
func dedupeInteractive(remote string, objs []Object) {
|
|
fmt.Printf("%s: %d duplicates remain\n", remote, len(objs))
|
|
for i, o := range objs {
|
|
md5sum, err := o.Hash(HashMD5)
|
|
if err != nil {
|
|
md5sum = err.Error()
|
|
}
|
|
fmt.Printf(" %d: %12d bytes, %s, md5sum %32s\n", i+1, o.Size(), o.ModTime().Format("2006-01-02 15:04:05.000000000"), md5sum)
|
|
}
|
|
switch Command([]string{"sSkip and do nothing", "kKeep just one (choose which in next step)", "rRename all to be different (by changing file.jpg to file-1.jpg)"}) {
|
|
case 's':
|
|
case 'k':
|
|
keep := ChooseNumber("Enter the number of the file to keep", 1, len(objs))
|
|
dedupeDeleteAllButOne(keep-1, remote, objs)
|
|
case 'r':
|
|
dedupeRename(remote, objs)
|
|
}
|
|
}
|
|
|
|
type objectsSortedByModTime []Object
|
|
|
|
func (objs objectsSortedByModTime) Len() int { return len(objs) }
|
|
func (objs objectsSortedByModTime) Swap(i, j int) { objs[i], objs[j] = objs[j], objs[i] }
|
|
func (objs objectsSortedByModTime) Less(i, j int) bool {
|
|
return objs[i].ModTime().Before(objs[j].ModTime())
|
|
}
|
|
|
|
// DeduplicateMode is how the dedupe command chooses what to do
|
|
type DeduplicateMode int
|
|
|
|
// Deduplicate modes
|
|
const (
|
|
DeduplicateInteractive DeduplicateMode = iota // interactively ask the user
|
|
DeduplicateSkip // skip all conflicts
|
|
DeduplicateFirst // choose the first object
|
|
DeduplicateNewest // choose the newest object
|
|
DeduplicateOldest // choose the oldest object
|
|
DeduplicateRename // rename the objects
|
|
)
|
|
|
|
func (x DeduplicateMode) String() string {
|
|
switch x {
|
|
case DeduplicateInteractive:
|
|
return "interactive"
|
|
case DeduplicateSkip:
|
|
return "skip"
|
|
case DeduplicateFirst:
|
|
return "first"
|
|
case DeduplicateNewest:
|
|
return "newest"
|
|
case DeduplicateOldest:
|
|
return "oldest"
|
|
case DeduplicateRename:
|
|
return "rename"
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// Set a DeduplicateMode from a string
|
|
func (x *DeduplicateMode) Set(s string) error {
|
|
switch strings.ToLower(s) {
|
|
case "interactive":
|
|
*x = DeduplicateInteractive
|
|
case "skip":
|
|
*x = DeduplicateSkip
|
|
case "first":
|
|
*x = DeduplicateFirst
|
|
case "newest":
|
|
*x = DeduplicateNewest
|
|
case "oldest":
|
|
*x = DeduplicateOldest
|
|
case "rename":
|
|
*x = DeduplicateRename
|
|
default:
|
|
return errors.Errorf("Unknown mode for dedupe %q.", s)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Type of the value
|
|
func (x *DeduplicateMode) Type() string {
|
|
return "string"
|
|
}
|
|
|
|
// Check it satisfies the interface
|
|
var _ pflag.Value = (*DeduplicateMode)(nil)
|
|
|
|
// Deduplicate interactively finds duplicate files and offers to
|
|
// delete all but one or rename them to be different. Only useful with
|
|
// Google Drive which can have duplicate file names.
|
|
func Deduplicate(f Fs, mode DeduplicateMode) error {
|
|
Infof(f, "Looking for duplicates using %v mode.", mode)
|
|
files := map[string][]Object{}
|
|
err := Walk(f, "", true, Config.MaxDepth, func(dirPath string, entries DirEntries, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
entries.ForObject(func(o Object) {
|
|
remote := o.Remote()
|
|
files[remote] = append(files[remote], o)
|
|
})
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for remote, objs := range files {
|
|
if len(objs) > 1 {
|
|
Logf(remote, "Found %d duplicates - deleting identical copies", len(objs))
|
|
objs = dedupeDeleteIdentical(remote, objs)
|
|
if len(objs) <= 1 {
|
|
Logf(remote, "All duplicates removed")
|
|
continue
|
|
}
|
|
switch mode {
|
|
case DeduplicateInteractive:
|
|
dedupeInteractive(remote, objs)
|
|
case DeduplicateFirst:
|
|
dedupeDeleteAllButOne(0, remote, objs)
|
|
case DeduplicateNewest:
|
|
sort.Sort(objectsSortedByModTime(objs)) // sort oldest first
|
|
dedupeDeleteAllButOne(len(objs)-1, remote, objs)
|
|
case DeduplicateOldest:
|
|
sort.Sort(objectsSortedByModTime(objs)) // sort oldest first
|
|
dedupeDeleteAllButOne(0, remote, objs)
|
|
case DeduplicateRename:
|
|
dedupeRename(remote, objs)
|
|
case DeduplicateSkip:
|
|
// skip
|
|
default:
|
|
//skip
|
|
}
|
|
}
|
|
}
|
|
return 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(f Fs) ObjectsChan {
|
|
o := make(ObjectsChan, Config.Checkers)
|
|
go func() {
|
|
defer close(o)
|
|
_ = Walk(f, "", true, Config.MaxDepth, func(dirPath string, entries DirEntries, err error) error {
|
|
if err != nil {
|
|
if err == ErrorDirNotFound {
|
|
return nil
|
|
}
|
|
Stats.Error()
|
|
Errorf(nil, "Failed to list: %v", err)
|
|
return nil
|
|
}
|
|
entries.ForObject(func(obj Object) {
|
|
o <- obj
|
|
})
|
|
return nil
|
|
})
|
|
}()
|
|
return o
|
|
}
|
|
|
|
// CleanUp removes the trash for the Fs
|
|
func CleanUp(f Fs) error {
|
|
doCleanUp := f.Features().CleanUp
|
|
if doCleanUp == nil {
|
|
return errors.Errorf("%v doesn't support cleanup", f)
|
|
}
|
|
if Config.DryRun {
|
|
Logf(f, "Not running cleanup as --dry-run set")
|
|
return nil
|
|
}
|
|
return doCleanUp()
|
|
}
|
|
|
|
// wrap a Reader and a Closer together into a ReadCloser
|
|
type readCloser struct {
|
|
io.Reader
|
|
Closer io.Closer
|
|
}
|
|
|
|
// Close the Closer
|
|
func (r *readCloser) Close() error {
|
|
return r.Closer.Close()
|
|
}
|
|
|
|
// 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(f Fs, w io.Writer, offset, count int64) error {
|
|
var mu sync.Mutex
|
|
return ListFn(f, func(o Object) {
|
|
var err error
|
|
Stats.Transferring(o.Remote())
|
|
defer func() {
|
|
Stats.DoneTransferring(o.Remote(), err == nil)
|
|
}()
|
|
size := o.Size()
|
|
thisOffset := offset
|
|
if thisOffset < 0 {
|
|
thisOffset += size
|
|
}
|
|
// size remaining is now reduced by thisOffset
|
|
size -= thisOffset
|
|
var options []OpenOption
|
|
if thisOffset > 0 {
|
|
options = append(options, &SeekOption{Offset: thisOffset})
|
|
}
|
|
in, err := o.Open(options...)
|
|
if err != nil {
|
|
Stats.Error()
|
|
Errorf(o, "Failed to open: %v", err)
|
|
return
|
|
}
|
|
if count >= 0 {
|
|
in = &readCloser{Reader: &io.LimitedReader{R: in, N: count}, Closer: in}
|
|
// reduce remaining size to count
|
|
if size > count {
|
|
size = count
|
|
}
|
|
}
|
|
in = NewAccountSizeName(in, size, o.Remote()).WithBuffer() // account and buffer the transfer
|
|
defer func() {
|
|
err = in.Close()
|
|
if err != nil {
|
|
Stats.Error()
|
|
Errorf(o, "Failed to close: %v", err)
|
|
}
|
|
}()
|
|
// 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 {
|
|
Stats.Error()
|
|
Errorf(o, "Failed to send to output: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Rmdirs removes any empty directories (or directories only
|
|
// containing empty directories) under f, including f.
|
|
func Rmdirs(f Fs, dir string) error {
|
|
dirEmpty := make(map[string]bool)
|
|
dirEmpty[""] = true
|
|
err := Walk(f, dir, true, Config.MaxDepth, func(dirPath string, entries DirEntries, err error) error {
|
|
if err != nil {
|
|
Stats.Error()
|
|
Errorf(f, "Failed to list %q: %v", dirPath, err)
|
|
return nil
|
|
}
|
|
for _, entry := range entries {
|
|
switch x := entry.(type) {
|
|
case Directory:
|
|
// add a new directory as empty
|
|
dir := x.Remote()
|
|
_, found := dirEmpty[dir]
|
|
if !found {
|
|
dirEmpty[dir] = true
|
|
}
|
|
case 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 errors.Wrap(err, "failed to rmdirs")
|
|
}
|
|
// Now delete the empty directories, starting from the longest path
|
|
var toDelete []string
|
|
for dir, empty := range dirEmpty {
|
|
if empty {
|
|
toDelete = append(toDelete, dir)
|
|
}
|
|
}
|
|
sort.Strings(toDelete)
|
|
for i := len(toDelete) - 1; i >= 0; i-- {
|
|
dir := toDelete[i]
|
|
err := TryRmdir(f, dir)
|
|
if err != nil {
|
|
Stats.Error()
|
|
Errorf(dir, "Failed to rmdir: %v", err)
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// moveOrCopyFile moves or copies a single file possibly to a new name
|
|
func moveOrCopyFile(fdst Fs, fsrc Fs, dstFileName string, srcFileName string, cp bool) (err error) {
|
|
if fdst.Name() == fsrc.Name() && dstFileName == srcFileName {
|
|
Debugf(fdst, "don't need to copy/move %s, it is already at target location", dstFileName)
|
|
return nil
|
|
}
|
|
|
|
// Choose operations
|
|
Op := Move
|
|
if cp {
|
|
Op = Copy
|
|
}
|
|
|
|
// Find src object
|
|
srcObj, err := fsrc.NewObject(srcFileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Find dst object if it exists
|
|
dstObj, err := fdst.NewObject(dstFileName)
|
|
if err == ErrorObjectNotFound {
|
|
dstObj = nil
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
if NeedTransfer(dstObj, srcObj) {
|
|
Stats.Transferring(srcFileName)
|
|
err = Op(fdst, dstObj, dstFileName, srcObj)
|
|
Stats.DoneTransferring(srcFileName, err == nil)
|
|
} else {
|
|
Stats.Checking(srcFileName)
|
|
if !cp {
|
|
err = DeleteFile(srcObj)
|
|
}
|
|
defer Stats.DoneChecking(srcFileName)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// MoveFile moves a single file possibly to a new name
|
|
func MoveFile(fdst Fs, fsrc Fs, dstFileName string, srcFileName string) (err error) {
|
|
return moveOrCopyFile(fdst, fsrc, dstFileName, srcFileName, false)
|
|
}
|
|
|
|
// CopyFile moves a single file possibly to a new name
|
|
func CopyFile(fdst Fs, fsrc Fs, dstFileName string, srcFileName string) (err error) {
|
|
return moveOrCopyFile(fdst, fsrc, dstFileName, srcFileName, true)
|
|
}
|