Files
rclone/cmd/bisync/rc.go
Nick Craig-Wood f191448b0d rc: flip auth default so all endpoints require auth unless opted out
Replace AuthRequired bool with NoAuth bool on the rc.Call struct and
flip the auth check logic. Previously endpoints were unauthenticated
by default and had to opt in with AuthRequired: true, which led to
security vulnerabilities when developers forgot to set the flag.

Now all endpoints require authentication by default. Only explicitly
safe read-only endpoints are marked with NoAuth: true:

- rc/noop
- rc/error
- rc/list
- core/version
- core/stats
- core/group-list
- core/transferred
- core/du
- cache/stats
- vfs/list
- vfs/stats
- vfs/queue
- job/status
- job/list

See GHSA-25qr-6mpr-f7qx, GHSA-jfwf-28xr-xw6q
2026-04-19 13:31:27 +01:00

209 lines
7.9 KiB
Go

//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
}