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 }