Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
bisync: reject --max-delete above 100
Before this change, bisync treated a `--max-delete` above 100 as 100%, so
the `--max-delete` check never triggered. A run that left no file on a side
unchanged was still stopped, by the "all files were changed" or empty
listing checks, but one that deleted most files while leaving even one
unchanged was not.

For bisync, `--max-delete` has always been a
percentage, "in range 0..100" as Ivan put it when he designed it
(#5164 (comment)), but the
range was never enforced. So a value meant as a file count, such as
`--max-delete 500`, turned the check off. Now that the rc takes
`--max-delete` from the global config too, that includes a `--max-delete`
the rcd itself was started with.

After this change, a `--max-delete` above 100 is an error, as the rc's
`maxDelete` parameter already was.
  • Loading branch information
nielash committed Sep 30, 2026
commit c2d9ac974eaee7d6973978c872b056008ea88cdf
9 changes: 5 additions & 4 deletions cmd/bisync/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,22 +203,23 @@ var commandDefinition = &cobra.Command{
},
}

func (opt *Options) applyContext(ctx context.Context) {
func (opt *Options) applyContext(ctx context.Context) error {
maxDelete := DefaultMaxDelete
ci := fs.GetConfig(ctx)
if ci.MaxDelete > 100 {
return fmt.Errorf("--max-delete is a percentage for bisync and must be from 0 to 100, not %d", ci.MaxDelete)
}
if ci.MaxDelete >= 0 {
maxDelete = int(ci.MaxDelete)
}
if maxDelete < 0 {
maxDelete = 0
}
if maxDelete > 100 {
maxDelete = 100
}
opt.MaxDelete = maxDelete
// reset MaxDelete for fs/operations, bisync handles this parameter specially
ci.MaxDelete = -1
opt.DryRun = ci.DryRun
return nil
}

func (opt *Options) setDryRun(ctx context.Context) context.Context {
Expand Down
4 changes: 3 additions & 1 deletion cmd/bisync/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ func Bisync(ctx context.Context, fs1, fs2 fs.Fs, optArg *Options) (err error) {
opt := *optArg // ensure that input is never changed
// applyContext changes the config, so give it a copy
ctx, _ = fs.AddConfig(ctx)
opt.applyContext(ctx)
if err = opt.applyContext(ctx); err != nil {
return err
}
b := &bisyncRun{
fs1: fs1,
fs2: fs2,
Expand Down
50 changes: 30 additions & 20 deletions cmd/bisync/rc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ func newRcBisync(t *testing.T, n int) (path1, path2 string, run func(in rc.Param
}

// newRcBisyncCtx creates Path1 with n files, resyncs it to Path2, and returns
// the paths and a function which runs sync/bisync on them as an rc job. The
// jobs run in ctx, whose config stands in for the flags the rcd was started
// with.
// the paths and a function which runs sync/bisync on them as an rc job in
// ctx, whose config stands in for the flags the rcd was started with
func newRcBisyncCtx(ctx context.Context, t *testing.T, n int) (path1, path2 string, run func(in rc.Params) error) {
if *fstest.RemoteName != "" {
t.Skip("Skipping test on non local remote")
Expand All @@ -39,15 +38,15 @@ func newRcBisyncCtx(ctx context.Context, t *testing.T, n int) (path1, path2 stri
}
call := rc.Calls.Get("sync/bisync")
require.NotNil(t, call)
run = func(in rc.Params) error {
runIn := func(ctx context.Context, in rc.Params) error {
in["path1"] = path1
in["path2"] = path2
in["workdir"] = workdir
_, _, err := jobs.NewJob(ctx, call.Fn, in)
return err
}
require.NoError(t, run(rc.Params{"resync": true}))
return path1, path2, run
require.NoError(t, runIn(context.Background(), rc.Params{"resync": true}))
return path1, path2, func(in rc.Params) error { return runIn(ctx, in) }
}

func TestRcDryRun(t *testing.T) {
Expand Down Expand Up @@ -80,20 +79,21 @@ func TestRcMaxDelete(t *testing.T) {
for _, test := range []struct {
name string
in rc.Params
wantErr bool
wantErr string
}{
{"default", rc.Params{}, false},
{"maxDelete", rc.Params{"maxDelete": 5}, true},
{"max_delete allows", rc.Params{"max_delete": 20}, false},
{"max_delete aborts", rc.Params{"max_delete": 5}, true},
{"_config MaxDelete aborts", rc.Params{"_config": rc.Params{"MaxDelete": 5}}, true},
{"default", rc.Params{}, ""},
{"maxDelete", rc.Params{"maxDelete": 5}, "too many deletes"},
{"max_delete allows", rc.Params{"max_delete": 20}, ""},
{"max_delete aborts", rc.Params{"max_delete": 5}, "too many deletes"},
{"_config MaxDelete aborts", rc.Params{"_config": rc.Params{"MaxDelete": 5}}, "too many deletes"},
{"max_delete over 100", rc.Params{"max_delete": 500}, "--max-delete"},
} {
t.Run(test.name, func(t *testing.T) {
path1, path2, run := newRcBisync(t, 10)
require.NoError(t, os.Remove(filepath.Join(path1, "file0.txt")))
err := run(test.in)
if test.wantErr {
assert.ErrorContains(t, err, "too many deletes")
if test.wantErr != "" {
assert.ErrorContains(t, err, test.wantErr)
assert.FileExists(t, filepath.Join(path2, "file0.txt"))
} else {
require.NoError(t, err)
Expand All @@ -105,10 +105,20 @@ func TestRcMaxDelete(t *testing.T) {

// Test a --max-delete the rcd was started with applies, as on the command line
func TestRcMaxDeleteFromRcd(t *testing.T) {
ctx, ci := fs.AddConfig(context.Background())
ci.MaxDelete = 5
path1, path2, run := newRcBisyncCtx(ctx, t, 10)
require.NoError(t, os.Remove(filepath.Join(path1, "file0.txt")))
assert.ErrorContains(t, run(rc.Params{}), "too many deletes")
assert.FileExists(t, filepath.Join(path2, "file0.txt"))
for _, test := range []struct {
maxDelete int64
wantErr string
}{
{5, "too many deletes"},
{500, "--max-delete"},
} {
t.Run(fmt.Sprint(test.maxDelete), func(t *testing.T) {
ctx, ci := fs.AddConfig(context.Background())
ci.MaxDelete = test.maxDelete
path1, path2, run := newRcBisyncCtx(ctx, t, 10)
require.NoError(t, os.Remove(filepath.Join(path1, "file0.txt")))
assert.ErrorContains(t, run(rc.Params{}), test.wantErr)
assert.FileExists(t, filepath.Join(path2, "file0.txt"))
})
}
}
1 change: 1 addition & 0 deletions docs/content/bisync.md
Original file line number Diff line number Diff line change
Expand Up @@ -1941,6 +1941,7 @@ the `--max-delete` safety check when used with `--track-renames`.
`_config` or the flat `dry_run` option.
- Fixed an issue causing the rc `sync/bisync` call to ignore the global
`--max-delete` and use a limit of 0% when `maxDelete` was not set.
- `--max-delete` now enforces a maximum of 100.

### `v1.74.2`

Expand Down