//go:generate go run help.go rc.md package bisync import ( "context" _ "embed" "errors" "log" "path/filepath" "strconv" "strings" "github.com/rclone/rclone/cmd/bisync/bilib" "github.com/rclone/rclone/fs" fslog "github.com/rclone/rclone/fs/log" "github.com/rclone/rclone/fs/rc" ) func addRC() { rc.Add(rc.Call{ Path: "sync/bisync", Fn: rcBisync, Title: shortHelp, Help: rcHelp, }) } //go:embed rc.md var rcHelp string var shortHelp = `Perform bidirectional synchronization between two paths.` var longHelp = shortHelp + MakeHelp(` [Bisync](https://rclone.org/bisync/) provides a bidirectional cloud sync solution in rclone. It retains the Path1 and Path2 filesystem listings from the prior run. On each successive run it will: - list files on Path1 and Path2, and check for changes on each side. Changes include ||New||, ||Newer||, ||Older||, and ||Deleted|| files. - Propagate changes on Path1 to Path2, and vice-versa. Bisync is considered an **advanced command**, so use with care. Make sure you have read and understood the entire [manual](https://rclone.org/bisync) (especially the [Limitations](https://rclone.org/bisync/#limitations) section) before using, or data loss can result. Questions can be asked in the [Rclone Forum](https://forum.rclone.org/). See [full bisync description](https://rclone.org/bisync/) for details. `) // MakeHelp replaces some dynamic variables for the help docs func MakeHelp(help string) string { replacer := strings.NewReplacer( "||", "`", "{MAXDELETE}", strconv.Itoa(DefaultMaxDelete), "{CHECKFILE}", DefaultCheckFilename, "{WORKDIR}", DefaultWorkdir, ) return replacer.Replace(help) } func rcBisync(ctx context.Context, in rc.Params) (out rc.Params, err error) { opt := &Options{} octx, ci := fs.AddConfig(ctx) if dryRun, err := in.GetBool("dryRun"); err == nil { ci.DryRun = dryRun opt.DryRun = dryRun } else if rc.NotErrParamNotFound(err) { return nil, err } if maxDelete, err := in.GetInt64("maxDelete"); err == nil { if maxDelete < 0 || maxDelete > 100 { return nil, rc.NewErrParamInvalid(errors.New("maxDelete must be a percentage between 0 and 100")) } opt.MaxDelete = int(maxDelete) } else if rc.NotErrParamNotFound(err) { return nil, err } if opt.Resync, err = in.GetBool("resync"); rc.NotErrParamNotFound(err) { fs.Debugf("resync", "optional parameter is missing. using default value: %v", opt.Resync) } if opt.CheckAccess, err = in.GetBool("checkAccess"); rc.NotErrParamNotFound(err) { fs.Debugf("checkAccess", "optional parameter is missing. using default value: %v", opt.CheckAccess) } if opt.Force, err = in.GetBool("force"); rc.NotErrParamNotFound(err) { fs.Debugf("force", "optional parameter is missing. using default value: %v", opt.Force) } if opt.CreateEmptySrcDirs, err = in.GetBool("createEmptySrcDirs"); rc.NotErrParamNotFound(err) { fs.Debugf("createEmptySrcDirs", "optional parameter is missing. using default value: %v", opt.CreateEmptySrcDirs) } if opt.RemoveEmptyDirs, err = in.GetBool("removeEmptyDirs"); rc.NotErrParamNotFound(err) { fs.Debugf("removeEmptyDirs", "optional parameter is missing. using default value: %v", opt.RemoveEmptyDirs) } if opt.NoCleanup, err = in.GetBool("noCleanup"); rc.NotErrParamNotFound(err) { fs.Debugf("noCleanup", "optional parameter is missing. using default value: %v", opt.NoCleanup) } if opt.IgnoreListingChecksum, err = in.GetBool("ignoreListingChecksum"); rc.NotErrParamNotFound(err) { fs.Debugf("ignoreListingChecksum", "optional parameter is missing. using default value: %v", opt.IgnoreListingChecksum) } if opt.Resilient, err = in.GetBool("resilient"); rc.NotErrParamNotFound(err) { fs.Debugf("resilient", "optional parameter is missing. using default value: %v", opt.Resilient) } if opt.CheckFilename, err = in.GetString("checkFilename"); rc.NotErrParamNotFound(err) { opt.CheckFilename = DefaultCheckFilename fs.Debugf("checkFilename", "optional parameter is missing. using default value: %v", opt.CheckFilename) } if opt.FiltersFile, err = in.GetString("filtersFile"); rc.NotErrParamNotFound(err) { fs.Debugf("filtersFile", "optional parameter is missing. using default value: %v", opt.FiltersFile) } if opt.Workdir, err = in.GetString("workdir"); rc.NotErrParamNotFound(err) { // "" sets correct default later fs.Debugf("workdir", "optional parameter is missing. using default value: %v", opt.Workdir) } if opt.BackupDir1, err = in.GetString("backupDir1"); rc.NotErrParamNotFound(err) { // we accept an alternate capitalization here for backward compatibility. if opt.BackupDir1, err = in.GetString("backupdir1"); rc.NotErrParamNotFound(err) { fs.Debugf("backupDir1", "optional parameter is missing. using default value: %v", opt.BackupDir1) } } if opt.BackupDir2, err = in.GetString("backupDir2"); rc.NotErrParamNotFound(err) { // we accept an alternate capitalization here for backward compatibility. if opt.BackupDir2, err = in.GetString("backupdir2"); rc.NotErrParamNotFound(err) { fs.Debugf("backupDir2", "optional parameter is missing. using default value: %v", opt.BackupDir2) } } if err = setEnum(in, "checkSync", "true", opt.CheckSync.Set); err != nil { return nil, err } if err = setEnum(in, "resyncMode", opt.ResyncMode.String(), opt.ResyncMode.Set); err != nil { return nil, err } if err = setEnum(in, "conflictResolve", opt.ConflictResolve.String(), opt.ConflictResolve.Set); err != nil { return nil, err } if err = setEnum(in, "conflictLoser", opt.ConflictLoser.String(), opt.ConflictLoser.Set); err != nil { return nil, err } if opt.ConflictSuffixFlag, err = in.GetString("conflictSuffix"); rc.NotErrParamNotFound(err) { fs.Debugf("conflictSuffix", "optional parameter is missing. using default value: %v", opt.ConflictSuffixFlag) } if opt.Recover, err = in.GetBool("recover"); rc.NotErrParamNotFound(err) { fs.Debugf("recover", "optional parameter is missing. using default value: %v", opt.Recover) } if opt.CompareFlag, err = in.GetString("compare"); rc.NotErrParamNotFound(err) { fs.Debugf("compare", "optional parameter is missing. using default value: %v", opt.CompareFlag) } if opt.Compare.NoSlowHash, err = in.GetBool("noSlowHash"); rc.NotErrParamNotFound(err) { fs.Debugf("noSlowHash", "optional parameter is missing. using default value: %v", opt.Compare.NoSlowHash) } if opt.Compare.SlowHashSyncOnly, err = in.GetBool("slowHashSyncOnly"); rc.NotErrParamNotFound(err) { fs.Debugf("slowHashSyncOnly", "optional parameter is missing. using default value: %v", opt.Compare.SlowHashSyncOnly) } if opt.Compare.DownloadHash, err = in.GetBool("downloadHash"); rc.NotErrParamNotFound(err) { fs.Debugf("downloadHash", "optional parameter is missing. using default value: %v", opt.Compare.DownloadHash) } if opt.MaxLock, err = in.GetFsDuration("maxLock"); rc.NotErrParamNotFound(err) { opt.MaxLock = 0 fs.Debugf("maxLock", "optional parameter is missing. using default value: %v", opt.MaxLock) } fs1, err := rc.GetFsNamed(octx, in, "path1") if err != nil { return nil, err } fs2, err := rc.GetFsNamed(octx, in, "path2") if err != nil { return nil, err } output := bilib.CaptureOutput(func() { err = Bisync(octx, fs1, fs2, opt) }) workDir, _ := filepath.Abs(DefaultWorkdir) if opt.Workdir != "" { workDir, _ = filepath.Abs(opt.Workdir) } basePath := bilib.BasePath(ctx, workDir, fs1, fs2) _, _ = log.Writer().Write(output) return rc.Params{ "output": string(output), "session": bilib.SessionName(fs1, fs2), "workDir": workDir, "basePath": basePath, "listing1": basePath + ".path1.lst", "listing2": basePath + ".path2.lst", "logFile": fslog.Opt.File, }, err } func setEnum(in rc.Params, name string, defaultVal string, set func(s string) error) error { v, err := in.GetString(name) if rc.NotErrParamNotFound(err) || v == "" { v = defaultVal fs.Debugf(name, "optional parameter is missing. using default value: %v", v) } if err := set(v); err != nil { return err } return nil }