github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/cmd/sync/sync.go (about)

     1  // Package sync provides the sync command.
     2  package sync
     3  
     4  import (
     5  	"context"
     6  	"io"
     7  	"os"
     8  
     9  	mutex "sync" // renamed as "sync" already in use
    10  
    11  	"github.com/rclone/rclone/cmd"
    12  	"github.com/rclone/rclone/fs"
    13  	"github.com/rclone/rclone/fs/config/flags"
    14  	"github.com/rclone/rclone/fs/operations"
    15  	"github.com/rclone/rclone/fs/operations/operationsflags"
    16  	"github.com/rclone/rclone/fs/sync"
    17  	"github.com/spf13/cobra"
    18  )
    19  
    20  var (
    21  	createEmptySrcDirs = false
    22  	opt                = operations.LoggerOpt{}
    23  	loggerFlagsOpt     = operationsflags.AddLoggerFlagsOptions{}
    24  )
    25  
    26  func init() {
    27  	cmd.Root.AddCommand(commandDefinition)
    28  	cmdFlags := commandDefinition.Flags()
    29  	flags.BoolVarP(cmdFlags, &createEmptySrcDirs, "create-empty-src-dirs", "", createEmptySrcDirs, "Create empty source dirs on destination after sync", "")
    30  	operationsflags.AddLoggerFlags(cmdFlags, &opt, &loggerFlagsOpt)
    31  	// TODO: add same flags to move and copy
    32  }
    33  
    34  var lock mutex.Mutex
    35  
    36  func syncLoggerFn(ctx context.Context, sigil operations.Sigil, src, dst fs.DirEntry, err error) {
    37  	lock.Lock()
    38  	defer lock.Unlock()
    39  
    40  	if err == fs.ErrorIsDir && !opt.FilesOnly && opt.DestAfter != nil {
    41  		opt.PrintDestAfter(ctx, sigil, src, dst, err)
    42  		return
    43  	}
    44  
    45  	_, srcOk := src.(fs.Object)
    46  	_, dstOk := dst.(fs.Object)
    47  	var filename string
    48  	if !srcOk && !dstOk {
    49  		return
    50  	} else if srcOk && !dstOk {
    51  		filename = src.String()
    52  	} else {
    53  		filename = dst.String()
    54  	}
    55  
    56  	if sigil.Writer(opt) != nil {
    57  		operations.SyncFprintf(sigil.Writer(opt), "%s\n", filename)
    58  	}
    59  	if opt.Combined != nil {
    60  		operations.SyncFprintf(opt.Combined, "%c %s\n", sigil, filename)
    61  		fs.Debugf(nil, "Sync Logger: %s: %c %s\n", sigil.String(), sigil, filename)
    62  	}
    63  	if opt.DestAfter != nil {
    64  		opt.PrintDestAfter(ctx, sigil, src, dst, err)
    65  	}
    66  }
    67  
    68  // GetSyncLoggerOpt gets the options corresponding to the logger flags
    69  func GetSyncLoggerOpt(ctx context.Context, fdst fs.Fs, command *cobra.Command) (operations.LoggerOpt, func(), error) {
    70  	closers := []io.Closer{}
    71  
    72  	opt.LoggerFn = syncLoggerFn
    73  	if opt.TimeFormat == "max" {
    74  		opt.TimeFormat = operations.FormatForLSFPrecision(fdst.Precision())
    75  	}
    76  	opt.SetListFormat(ctx, command.Flags())
    77  	opt.NewListJSON(ctx, fdst, "")
    78  
    79  	open := func(name string, pout *io.Writer) error {
    80  		if name == "" {
    81  			return nil
    82  		}
    83  		if name == "-" {
    84  			*pout = os.Stdout
    85  			return nil
    86  		}
    87  		out, err := os.Create(name)
    88  		if err != nil {
    89  			return err
    90  		}
    91  		*pout = out
    92  		closers = append(closers, out)
    93  		return nil
    94  	}
    95  
    96  	if err := open(loggerFlagsOpt.Combined, &opt.Combined); err != nil {
    97  		return opt, nil, err
    98  	}
    99  	if err := open(loggerFlagsOpt.MissingOnSrc, &opt.MissingOnSrc); err != nil {
   100  		return opt, nil, err
   101  	}
   102  	if err := open(loggerFlagsOpt.MissingOnDst, &opt.MissingOnDst); err != nil {
   103  		return opt, nil, err
   104  	}
   105  	if err := open(loggerFlagsOpt.Match, &opt.Match); err != nil {
   106  		return opt, nil, err
   107  	}
   108  	if err := open(loggerFlagsOpt.Differ, &opt.Differ); err != nil {
   109  		return opt, nil, err
   110  	}
   111  	if err := open(loggerFlagsOpt.ErrFile, &opt.Error); err != nil {
   112  		return opt, nil, err
   113  	}
   114  	if err := open(loggerFlagsOpt.DestAfter, &opt.DestAfter); err != nil {
   115  		return opt, nil, err
   116  	}
   117  
   118  	close := func() {
   119  		for _, closer := range closers {
   120  			err := closer.Close()
   121  			if err != nil {
   122  				fs.Errorf(nil, "Failed to close report output: %v", err)
   123  			}
   124  		}
   125  	}
   126  
   127  	return opt, close, nil
   128  }
   129  
   130  func anyNotBlank(s ...string) bool {
   131  	for _, x := range s {
   132  		if x != "" {
   133  			return true
   134  		}
   135  	}
   136  	return false
   137  }
   138  
   139  var commandDefinition = &cobra.Command{
   140  	Use:   "sync source:path dest:path",
   141  	Short: `Make source and dest identical, modifying destination only.`,
   142  	Long: `
   143  Sync the source to the destination, changing the destination
   144  only.  Doesn't transfer files that are identical on source and
   145  destination, testing by size and modification time or MD5SUM.
   146  Destination is updated to match source, including deleting files
   147  if necessary (except duplicate objects, see below). If you don't
   148  want to delete files from destination, use the
   149  [copy](/commands/rclone_copy/) command instead.
   150  
   151  **Important**: Since this can cause data loss, test first with the
   152  ` + "`--dry-run` or the `--interactive`/`-i`" + ` flag.
   153  
   154      rclone sync --interactive SOURCE remote:DESTINATION
   155  
   156  Note that files in the destination won't be deleted if there were any
   157  errors at any point.  Duplicate objects (files with the same name, on
   158  those providers that support it) are also not yet handled.
   159  
   160  It is always the contents of the directory that is synced, not the
   161  directory itself. So when source:path is a directory, it's the contents of
   162  source:path that are copied, not the directory name and contents.  See
   163  extended explanation in the [copy](/commands/rclone_copy/) command if unsure.
   164  
   165  If dest:path doesn't exist, it is created and the source:path contents
   166  go there.
   167  
   168  It is not possible to sync overlapping remotes. However, you may exclude
   169  the destination from the sync with a filter rule or by putting an 
   170  exclude-if-present file inside the destination directory and sync to a
   171  destination that is inside the source directory.
   172  
   173  Rclone will sync the modification times of files and directories if
   174  the backend supports it. If metadata syncing is required then use the
   175  ` + "`--metadata`" + ` flag.
   176  
   177  Note that the modification time and metadata for the root directory
   178  will **not** be synced. See https://github.com/rclone/rclone/issues/7652
   179  for more info.
   180  
   181  **Note**: Use the ` + "`-P`" + `/` + "`--progress`" + ` flag to view real-time transfer statistics
   182  
   183  **Note**: Use the ` + "`rclone dedupe`" + ` command to deal with "Duplicate object/directory found in source/destination - ignoring" errors.
   184  See [this forum post](https://forum.rclone.org/t/sync-not-clearing-duplicates/14372) for more info.
   185  
   186  ## Logger Flags
   187  
   188  The ` + "`--differ`" + `, ` + "`--missing-on-dst`" + `, ` + "`--missing-on-src`" + `, ` +
   189  		"`--match`" + ` and ` + "`--error`" + ` flags write paths, one per line, to the file name (or
   190  stdout if it is ` + "`-`" + `) supplied. What they write is described in the
   191  help below. For example ` + "`--differ`" + ` will write all paths which are
   192  present on both the source and destination but different.
   193  
   194  The ` + "`--combined`" + ` flag will write a file (or stdout) which contains all
   195  file paths with a symbol and then a space and then the path to tell
   196  you what happened to it. These are reminiscent of diff files.
   197  
   198  - ` + "`= path`" + ` means path was found in source and destination and was identical
   199  - ` + "`- path`" + ` means path was missing on the source, so only in the destination
   200  - ` + "`+ path`" + ` means path was missing on the destination, so only in the source
   201  - ` + "`* path`" + ` means path was present in source and destination but different.
   202  - ` + "`! path`" + ` means there was an error reading or hashing the source or dest.
   203  
   204  The ` + "`--dest-after`" + ` flag writes a list file using the same format flags
   205  as [` + "`lsf`" + `](/commands/rclone_lsf/#synopsis) (including [customizable options
   206  for hash, modtime, etc.](/commands/rclone_lsf/#synopsis))
   207  Conceptually it is similar to rsync's ` + "`--itemize-changes`" + `, but not identical
   208  -- it should output an accurate list of what will be on the destination
   209  after the sync.
   210  
   211  Note that these logger flags have a few limitations, and certain scenarios
   212  are not currently supported:
   213  
   214  - ` + "`--max-duration`" + ` / ` + "`CutoffModeHard`" + `
   215  - ` + "`--compare-dest`" + ` / ` + "`--copy-dest`" + `
   216  - server-side moves of an entire dir at once
   217  - High-level retries, because there would be duplicates (use ` + "`--retries 1`" + ` to disable)
   218  - Possibly some unusual error scenarios
   219  
   220  Note also that each file is logged during the sync, as opposed to after, so it
   221  is most useful as a predictor of what SHOULD happen to each file
   222  (which may or may not match what actually DID.)
   223  `,
   224  	Annotations: map[string]string{
   225  		"groups": "Sync,Copy,Filter,Listing,Important",
   226  	},
   227  	Run: func(command *cobra.Command, args []string) {
   228  		cmd.CheckArgs(2, 2, command, args)
   229  		fsrc, srcFileName, fdst := cmd.NewFsSrcFileDst(args)
   230  		cmd.Run(true, true, command, func() error {
   231  			ctx := context.Background()
   232  			opt, close, err := GetSyncLoggerOpt(ctx, fdst, command)
   233  			if err != nil {
   234  				return err
   235  			}
   236  			defer close()
   237  
   238  			if anyNotBlank(loggerFlagsOpt.Combined, loggerFlagsOpt.MissingOnSrc, loggerFlagsOpt.MissingOnDst,
   239  				loggerFlagsOpt.Match, loggerFlagsOpt.Differ, loggerFlagsOpt.ErrFile, loggerFlagsOpt.DestAfter) {
   240  				ctx = operations.WithSyncLogger(ctx, opt)
   241  			}
   242  
   243  			if srcFileName == "" {
   244  				return sync.Sync(ctx, fdst, fsrc, createEmptySrcDirs)
   245  			}
   246  			return operations.CopyFile(ctx, fdst, fsrc, srcFileName, srcFileName)
   247  		})
   248  	},
   249  }