github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/operations/logger.go (about) 1 package operations 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 10 "github.com/rclone/rclone/fs" 11 "github.com/rclone/rclone/fs/hash" 12 "github.com/spf13/pflag" 13 ) 14 15 // Sigil represents the rune (-+=*!?) used by Logger to categorize files by their match/differ/missing status. 16 type Sigil rune 17 18 // String converts sigil to more human-readable string 19 func (sigil Sigil) String() string { 20 switch sigil { 21 case '-': 22 return "MissingOnSrc" 23 case '+': 24 return "MissingOnDst" 25 case '=': 26 return "Match" 27 case '*': 28 return "Differ" 29 case '!': 30 return "Error" 31 // case '.': 32 // return "Completed" 33 case '?': 34 return "Other" 35 } 36 return "unknown" 37 } 38 39 // Writer directs traffic from sigil -> LoggerOpt.Writer 40 func (sigil Sigil) Writer(opt LoggerOpt) io.Writer { 41 switch sigil { 42 case '-': 43 return opt.MissingOnSrc 44 case '+': 45 return opt.MissingOnDst 46 case '=': 47 return opt.Match 48 case '*': 49 return opt.Differ 50 case '!': 51 return opt.Error 52 } 53 return nil 54 } 55 56 // Sigil constants 57 const ( 58 MissingOnSrc Sigil = '-' 59 MissingOnDst Sigil = '+' 60 Match Sigil = '=' 61 Differ Sigil = '*' 62 TransferError Sigil = '!' 63 Other Sigil = '?' // reserved but not currently used 64 ) 65 66 // LoggerFn uses fs.DirEntry instead of fs.Object so it can include Dirs 67 // For LoggerFn example, see bisync.WriteResults() or sync.SyncLoggerFn() 68 // Usage example: s.logger(ctx, operations.Differ, src, dst, nil) 69 type LoggerFn func(ctx context.Context, sigil Sigil, src, dst fs.DirEntry, err error) 70 type loggerContextKey struct{} 71 type loggerOptContextKey struct{} 72 73 var loggerKey = loggerContextKey{} 74 var loggerOptKey = loggerOptContextKey{} 75 76 // LoggerOpt contains options for the Sync Logger functions 77 // TODO: refactor Check in here too? 78 type LoggerOpt struct { 79 // Fdst, Fsrc fs.Fs // fses to check 80 // Check checkFn // function to use for checking 81 // OneWay bool // one way only? 82 LoggerFn LoggerFn // function to use for logging 83 Combined io.Writer // a file with file names with leading sigils 84 MissingOnSrc io.Writer // files only in the destination 85 MissingOnDst io.Writer // files only in the source 86 Match io.Writer // matching files 87 Differ io.Writer // differing files 88 Error io.Writer // files with errors of some kind 89 DestAfter io.Writer // files that exist on the destination post-sync 90 JSON *bytes.Buffer // used by bisync to read/write struct as JSON 91 DeleteModeOff bool //affects whether Logger expects MissingOnSrc to be deleted 92 93 // lsf options for destAfter 94 ListFormat ListFormat 95 JSONOpt ListJSONOpt 96 LJ *listJSON 97 Format string 98 TimeFormat string 99 Separator string 100 DirSlash bool 101 // Recurse bool 102 HashType hash.Type 103 FilesOnly bool 104 DirsOnly bool 105 Csv bool 106 Absolute bool 107 } 108 109 // WithLogger stores logger in ctx and returns a copy of ctx in which loggerKey = logger 110 func WithLogger(ctx context.Context, logger LoggerFn) context.Context { 111 return context.WithValue(ctx, loggerKey, logger) 112 } 113 114 // WithLoggerOpt stores loggerOpt in ctx and returns a copy of ctx in which loggerOptKey = loggerOpt 115 func WithLoggerOpt(ctx context.Context, loggerOpt LoggerOpt) context.Context { 116 return context.WithValue(ctx, loggerOptKey, loggerOpt) 117 } 118 119 // GetLogger attempts to retrieve LoggerFn from context, returns it if found, otherwise returns no-op function 120 func GetLogger(ctx context.Context) (LoggerFn, bool) { 121 logger, ok := ctx.Value(loggerKey).(LoggerFn) 122 if !ok { 123 logger = func(ctx context.Context, sigil Sigil, src, dst fs.DirEntry, err error) {} 124 } 125 return logger, ok 126 } 127 128 // GetLoggerOpt attempts to retrieve LoggerOpt from context, returns it if found, otherwise returns NewLoggerOpt() 129 func GetLoggerOpt(ctx context.Context) LoggerOpt { 130 loggerOpt, ok := ctx.Value(loggerOptKey).(LoggerOpt) 131 if ok { 132 return loggerOpt 133 } 134 return NewLoggerOpt() 135 } 136 137 // WithSyncLogger starts a new logger with the options passed in and saves it to ctx for retrieval later 138 func WithSyncLogger(ctx context.Context, opt LoggerOpt) context.Context { 139 ctx = WithLoggerOpt(ctx, opt) 140 return WithLogger(ctx, func(ctx context.Context, sigil Sigil, src, dst fs.DirEntry, err error) { 141 if opt.LoggerFn != nil { 142 opt.LoggerFn(ctx, sigil, src, dst, err) 143 } else { 144 SyncFprintf(opt.Combined, "%c %s\n", sigil, dst.Remote()) 145 } 146 }) 147 } 148 149 // NewLoggerOpt returns a new LoggerOpt struct with defaults 150 func NewLoggerOpt() LoggerOpt { 151 opt := LoggerOpt{ 152 Combined: new(bytes.Buffer), 153 MissingOnSrc: new(bytes.Buffer), 154 MissingOnDst: new(bytes.Buffer), 155 Match: new(bytes.Buffer), 156 Differ: new(bytes.Buffer), 157 Error: new(bytes.Buffer), 158 DestAfter: new(bytes.Buffer), 159 JSON: new(bytes.Buffer), 160 } 161 return opt 162 } 163 164 // Winner predicts which side (src or dst) should end up winning out on the dst. 165 type Winner struct { 166 Obj fs.DirEntry // the object that should exist on dst post-sync, if any 167 Side string // whether the winning object was from the src or dst 168 Err error // whether there's an error preventing us from predicting winner correctly (not whether there was a sync error more generally) 169 } 170 171 // WinningSide can be called in a LoggerFn to predict what the dest will look like post-sync 172 // 173 // This attempts to account for every case in which dst (intentionally) does not match src after a sync. 174 // 175 // Known issues / cases we can't confidently predict yet: 176 // 177 // --max-duration / CutoffModeHard 178 // --compare-dest / --copy-dest (because equal() is called multiple times for the same file) 179 // server-side moves of an entire dir at once (because we never get the individual file objects in the dir) 180 // High-level retries, because there would be dupes (use --retries 1 to disable) 181 // Possibly some error scenarios 182 func WinningSide(ctx context.Context, sigil Sigil, src, dst fs.DirEntry, err error) Winner { 183 winner := Winner{nil, "none", nil} 184 opt := GetLoggerOpt(ctx) 185 ci := fs.GetConfig(ctx) 186 187 if err == fs.ErrorIsDir { 188 winner.Err = err 189 if sigil == MissingOnSrc { 190 if (opt.DeleteModeOff || ci.DryRun) && dst != nil { 191 winner.Obj = dst 192 winner.Side = "dst" // whatever's on dst will remain so after DryRun 193 return winner 194 } 195 return winner // none, because dst should just get deleted 196 } 197 if sigil == MissingOnDst && ci.DryRun { 198 return winner // none, because it does not currently exist on dst, and will still not exist after DryRun 199 } else if ci.DryRun && dst != nil { 200 winner.Obj = dst 201 winner.Side = "dst" 202 } else if src != nil { 203 winner.Obj = src 204 winner.Side = "src" 205 } 206 return winner 207 } 208 209 _, srcOk := src.(fs.Object) 210 _, dstOk := dst.(fs.Object) 211 if !srcOk && !dstOk { 212 return winner // none, because we don't have enough info to continue. 213 } 214 215 switch sigil { 216 case MissingOnSrc: 217 if opt.DeleteModeOff || ci.DryRun { // i.e. it's a copy, not sync (or it's a DryRun) 218 winner.Obj = dst 219 winner.Side = "dst" // whatever's on dst will remain so after DryRun 220 return winner 221 } 222 return winner // none, because dst should just get deleted 223 case Match, Differ, MissingOnDst: 224 if sigil == MissingOnDst && ci.DryRun { 225 return winner // none, because it does not currently exist on dst, and will still not exist after DryRun 226 } 227 winner.Obj = src 228 winner.Side = "src" // presume dst will end up matching src unless changed below 229 if sigil == Match && (ci.SizeOnly || ci.CheckSum || ci.IgnoreSize || ci.UpdateOlder || ci.NoUpdateModTime) { 230 winner.Obj = dst 231 winner.Side = "dst" // ignore any differences with src because of user flags 232 } 233 if ci.IgnoreTimes { 234 winner.Obj = src 235 winner.Side = "src" // copy src to dst unconditionally 236 } 237 if (sigil == Match || sigil == Differ) && (ci.IgnoreExisting || ci.Immutable) { 238 winner.Obj = dst 239 winner.Side = "dst" // dst should remain unchanged if it already exists (and we know it does because it's Match or Differ) 240 } 241 if ci.DryRun { 242 winner.Obj = dst 243 winner.Side = "dst" // dst should remain unchanged after DryRun (note that we handled MissingOnDst earlier) 244 } 245 return winner 246 case TransferError: 247 winner.Obj = dst 248 winner.Side = "dst" // usually, dst should not change if there's an error 249 if dst == nil { 250 winner.Obj = src 251 winner.Side = "src" // but if for some reason we have a src and not a dst, go with it 252 } 253 if winner.Obj != nil { 254 if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, errors.New("max transfer duration reached as set by --max-duration")) { 255 winner.Err = err // we can't confidently predict what survives if CutoffModeHard 256 } 257 return winner // we know at least one of the objects 258 } 259 } 260 // should only make it this far if it's TransferError and both src and dst are nil 261 winner.Side = "none" 262 winner.Err = fmt.Errorf("unknown case -- can't determine winner. %v", err) 263 fs.Debugf(winner.Obj, "%v", winner.Err) 264 return winner 265 } 266 267 // SetListFormat sets opt.ListFormat for destAfter 268 // TODO: possibly refactor duplicate code from cmd/lsf, where this is mostly copied from 269 func (opt *LoggerOpt) SetListFormat(ctx context.Context, cmdFlags *pflag.FlagSet) { 270 // Work out if the separatorFlag was supplied or not 271 separatorFlag := cmdFlags.Lookup("separator") 272 separatorFlagSupplied := separatorFlag != nil && separatorFlag.Changed 273 // Default the separator to , if using CSV 274 if opt.Csv && !separatorFlagSupplied { 275 opt.Separator = "," 276 } 277 278 var list ListFormat 279 list.SetSeparator(opt.Separator) 280 list.SetCSV(opt.Csv) 281 list.SetDirSlash(opt.DirSlash) 282 list.SetAbsolute(opt.Absolute) 283 var JSONOpt = ListJSONOpt{ 284 NoModTime: true, 285 NoMimeType: true, 286 DirsOnly: opt.DirsOnly, 287 FilesOnly: opt.FilesOnly, 288 // Recurse: opt.Recurse, 289 } 290 291 for _, char := range opt.Format { 292 switch char { 293 case 'p': 294 list.AddPath() 295 case 't': 296 list.AddModTime(opt.TimeFormat) 297 JSONOpt.NoModTime = false 298 case 's': 299 list.AddSize() 300 case 'h': 301 list.AddHash(opt.HashType) 302 JSONOpt.ShowHash = true 303 JSONOpt.HashTypes = []string{opt.HashType.String()} 304 case 'i': 305 list.AddID() 306 case 'm': 307 list.AddMimeType() 308 JSONOpt.NoMimeType = false 309 case 'e': 310 list.AddEncrypted() 311 JSONOpt.ShowEncrypted = true 312 case 'o': 313 list.AddOrigID() 314 JSONOpt.ShowOrigIDs = true 315 case 'T': 316 list.AddTier() 317 case 'M': 318 list.AddMetadata() 319 JSONOpt.Metadata = true 320 default: 321 fs.Errorf(nil, "unknown format character %q", char) 322 } 323 } 324 opt.ListFormat = list 325 opt.JSONOpt = JSONOpt 326 } 327 328 // NewListJSON makes a new *listJSON for destAfter 329 func (opt *LoggerOpt) NewListJSON(ctx context.Context, fdst fs.Fs, remote string) { 330 opt.LJ, _ = newListJSON(ctx, fdst, remote, &opt.JSONOpt) 331 //fs.Debugf(nil, "%v", opt.LJ) 332 } 333 334 // JSONEntry returns a *ListJSONItem for destAfter 335 func (opt *LoggerOpt) JSONEntry(ctx context.Context, entry fs.DirEntry) (*ListJSONItem, error) { 336 return opt.LJ.entry(ctx, entry) 337 } 338 339 // PrintDestAfter writes a *ListJSONItem to opt.DestAfter 340 func (opt *LoggerOpt) PrintDestAfter(ctx context.Context, sigil Sigil, src, dst fs.DirEntry, err error) { 341 entry := WinningSide(ctx, sigil, src, dst, err) 342 if entry.Obj != nil { 343 JSONEntry, _ := opt.JSONEntry(ctx, entry.Obj) 344 _, _ = fmt.Fprintln(opt.DestAfter, opt.ListFormat.Format(JSONEntry)) 345 } 346 }