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  }