github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/hasher/commands.go (about)

     1  package hasher
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"path"
     8  
     9  	"github.com/rclone/rclone/fs"
    10  	"github.com/rclone/rclone/fs/accounting"
    11  	"github.com/rclone/rclone/fs/cache"
    12  	"github.com/rclone/rclone/fs/fspath"
    13  	"github.com/rclone/rclone/fs/hash"
    14  	"github.com/rclone/rclone/fs/operations"
    15  	"github.com/rclone/rclone/lib/kv"
    16  )
    17  
    18  // Command the backend to run a named command
    19  //
    20  // The command run is name
    21  // args may be used to read arguments from
    22  // opts may be used to read optional arguments from
    23  //
    24  // The result should be capable of being JSON encoded
    25  // If it is a string or a []string it will be shown to the user
    26  // otherwise it will be JSON encoded and shown to the user like that
    27  func (f *Fs) Command(ctx context.Context, name string, arg []string, opt map[string]string) (out interface{}, err error) {
    28  	switch name {
    29  	case "drop":
    30  		return nil, f.db.Stop(true)
    31  	case "dump", "fulldump":
    32  		return nil, f.dbDump(ctx, name == "fulldump", "")
    33  	case "import", "stickyimport":
    34  		sticky := name == "stickyimport"
    35  		if len(arg) != 2 {
    36  			return nil, errors.New("please provide checksum type and path to sum file")
    37  		}
    38  		return nil, f.dbImport(ctx, arg[0], arg[1], sticky)
    39  	default:
    40  		return nil, fs.ErrorCommandNotFound
    41  	}
    42  }
    43  
    44  var commandHelp = []fs.CommandHelp{{
    45  	Name:  "drop",
    46  	Short: "Drop cache",
    47  	Long: `Completely drop checksum cache.
    48  Usage Example:
    49      rclone backend drop hasher:
    50  `,
    51  }, {
    52  	Name:  "dump",
    53  	Short: "Dump the database",
    54  	Long:  "Dump cache records covered by the current remote",
    55  }, {
    56  	Name:  "fulldump",
    57  	Short: "Full dump of the database",
    58  	Long:  "Dump all cache records in the database",
    59  }, {
    60  	Name:  "import",
    61  	Short: "Import a SUM file",
    62  	Long: `Amend hash cache from a SUM file and bind checksums to files by size/time.
    63  Usage Example:
    64      rclone backend import hasher:subdir md5 /path/to/sum.md5
    65  `,
    66  }, {
    67  	Name:  "stickyimport",
    68  	Short: "Perform fast import of a SUM file",
    69  	Long: `Fill hash cache from a SUM file without verifying file fingerprints.
    70  Usage Example:
    71      rclone backend stickyimport hasher:subdir md5 remote:path/to/sum.md5
    72  `,
    73  }}
    74  
    75  func (f *Fs) dbDump(ctx context.Context, full bool, root string) error {
    76  	if root == "" {
    77  		remoteFs, err := cache.Get(ctx, f.opt.Remote)
    78  		if err != nil {
    79  			return err
    80  		}
    81  		root = fspath.JoinRootPath(remoteFs.Root(), f.Root())
    82  	}
    83  	if f.db == nil {
    84  		if f.opt.MaxAge == 0 {
    85  			fs.Errorf(f, "db not found. (disabled with max_age = 0)")
    86  		} else {
    87  			fs.Errorf(f, "db not found.")
    88  		}
    89  		return kv.ErrInactive
    90  	}
    91  	op := &kvDump{
    92  		full: full,
    93  		root: root,
    94  		path: f.db.Path(),
    95  		fs:   f,
    96  	}
    97  	err := f.db.Do(false, op)
    98  	if err == kv.ErrEmpty {
    99  		fs.Infof(op.path, "empty")
   100  		err = nil
   101  	}
   102  	return err
   103  }
   104  
   105  func (f *Fs) dbImport(ctx context.Context, hashName, sumRemote string, sticky bool) error {
   106  	var hashType hash.Type
   107  	if err := hashType.Set(hashName); err != nil {
   108  		return err
   109  	}
   110  	if hashType == hash.None {
   111  		return errors.New("please provide a valid hash type")
   112  	}
   113  	if !f.suppHashes.Contains(hashType) {
   114  		return errors.New("unsupported hash type")
   115  	}
   116  	if !f.keepHashes.Contains(hashType) {
   117  		fs.Infof(nil, "Need not import hashes of this type")
   118  		return nil
   119  	}
   120  
   121  	_, sumPath, err := fspath.SplitFs(sumRemote)
   122  	if err != nil {
   123  		return err
   124  	}
   125  	sumFs, err := cache.Get(ctx, sumRemote)
   126  	switch err {
   127  	case fs.ErrorIsFile:
   128  		// ok
   129  	case nil:
   130  		return fmt.Errorf("not a file: %s", sumRemote)
   131  	default:
   132  		return err
   133  	}
   134  
   135  	sumObj, err := sumFs.NewObject(ctx, path.Base(sumPath))
   136  	if err != nil {
   137  		return fmt.Errorf("cannot open sum file: %w", err)
   138  	}
   139  	hashes, err := operations.ParseSumFile(ctx, sumObj)
   140  	if err != nil {
   141  		return fmt.Errorf("failed to parse sum file: %w", err)
   142  	}
   143  
   144  	if sticky {
   145  		rootPath := f.Fs.Root()
   146  		for remote, hashVal := range hashes {
   147  			key := path.Join(rootPath, remote)
   148  			hashSums := operations.HashSums{hashName: hashVal}
   149  			if err := f.putRawHashes(ctx, key, anyFingerprint, hashSums); err != nil {
   150  				fs.Errorf(nil, "%s: failed to import: %v", remote, err)
   151  			}
   152  		}
   153  		fs.Infof(nil, "Summary: %d checksum(s) imported", len(hashes))
   154  		return nil
   155  	}
   156  
   157  	const longImportThreshold = 100
   158  	if len(hashes) > longImportThreshold {
   159  		fs.Infof(nil, "Importing %d checksums. Please wait...", len(hashes))
   160  	}
   161  
   162  	doneCount := 0
   163  	err = operations.ListFn(ctx, f, func(obj fs.Object) {
   164  		remote := obj.Remote()
   165  		hash := hashes[remote]
   166  		hashes[remote] = "" // mark as handled
   167  		o, ok := obj.(*Object)
   168  		if ok && hash != "" {
   169  			if err := o.putHashes(ctx, hashMap{hashType: hash}); err != nil {
   170  				fs.Errorf(nil, "%s: failed to import: %v", remote, err)
   171  			}
   172  			accounting.Stats(ctx).NewCheckingTransfer(obj, "importing").Done(ctx, err)
   173  			doneCount++
   174  		}
   175  	})
   176  	if err != nil {
   177  		fs.Errorf(nil, "Import failed: %v", err)
   178  	}
   179  	skipCount := 0
   180  	for remote, emptyOrDone := range hashes {
   181  		if emptyOrDone != "" {
   182  			fs.Infof(nil, "Skip vanished object: %s", remote)
   183  			skipCount++
   184  		}
   185  	}
   186  	fs.Infof(nil, "Summary: %d imported, %d skipped", doneCount, skipCount)
   187  	return err
   188  }