Files
rclone/cmd/bisync/delta_list_test.go
T
f-denkena 1b52fc412d
build / lint (push) Canceled after 0s
build / android-all (push) Canceled after 0s
Build & Push Docker Images / Build Docker Image for linux/386 (push) Canceled after 0s
Build & Push Docker Images / Build Docker Image for linux/amd64 (push) Canceled after 0s
Build & Push Docker Images / Build Docker Image for linux/arm/v6 (push) Canceled after 0s
Build & Push Docker Images / Build Docker Image for linux/arm/v7 (push) Canceled after 0s
Build & Push Docker Images / Build Docker Image for linux/arm64 (push) Canceled after 0s
build / windows (push) Canceled after 0s
build / other_os (push) Canceled after 0s
build / mac_amd64 (push) Canceled after 0s
build / mac_arm64 (push) Canceled after 0s
build / linux (push) Canceled after 0s
build / go1.26 (push) Canceled after 0s
build / linux_386 (push) Canceled after 0s
Build & Push Docker Images / Merge & Push Final Docker Image (push) Canceled after 0s
bisync/dropbox: fail closed on listing collapse and cursor races
Refuse --delta-list without --check-access. Abort if a listing goes empty
or shrinks past --max-delete versus the prior snapshot (full relist and
local walk included). Persist cursor before listing so a crash cannot
pair a new listing with a stale cursor. Reconstruct delta remotes from
path_lower plus leaf casing; ListR probes a non-recursive list when the
recursive result is empty.
2026-09-09 23:53:01 +02:00

132 lines
4.1 KiB
Go

package bisync
import (
"errors"
"os"
"path/filepath"
"testing"
"time"
"github.com/rclone/rclone/fs/hash"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testListing() *fileList {
ls := newFileList()
ls.hash = hash.MD5
t0 := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
ls.put("keep.txt", 10, t0, "aaa", "", "-")
ls.put("gone.txt", 20, t0, "bbb", "", "-")
ls.put("dir/a.txt", 30, t0, "ccc", "", "-")
ls.put("dir/b.txt", 40, t0, "ddd", "", "-")
ls.put("dir", 0, t0, "", "", "d")
return ls
}
func TestApplyListingDeltas_UpsertAndDelete(t *testing.T) {
ls := testListing()
t1 := time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC)
err := applyListingDeltas(ls, []listingDelta{
{Remote: "keep.txt", Size: 11, ModTime: t1, Hash: "zzz", Flags: "-"},
{Remote: "new.txt", Size: 5, ModTime: t1, Hash: "eee", Flags: "-"},
{Remote: "gone.txt", Deleted: true},
}, deltaApplyOpts{MaxDelete: 50})
require.NoError(t, err)
assert.True(t, ls.has("keep.txt"))
assert.Equal(t, int64(11), ls.getSize("keep.txt"))
assert.Equal(t, "zzz", ls.getHash("keep.txt"))
assert.True(t, ls.has("new.txt"))
assert.False(t, ls.has("gone.txt"))
assert.True(t, ls.has("dir/a.txt"))
}
func TestApplyListingDeltas_FolderDeleteRemovesChildren(t *testing.T) {
ls := testListing()
err := applyListingDeltas(ls, []listingDelta{
{Remote: "dir", Deleted: true, Flags: "d"},
}, deltaApplyOpts{MaxDelete: 90})
require.NoError(t, err)
assert.False(t, ls.has("dir"))
assert.False(t, ls.has("dir/a.txt"))
assert.False(t, ls.has("dir/b.txt"))
assert.True(t, ls.has("keep.txt"))
}
func TestApplyListingDeltas_MaxDeleteAbortsWithoutMutation(t *testing.T) {
ls := testListing()
orig := len(ls.list)
err := applyListingDeltas(ls, []listingDelta{
{Remote: "keep.txt", Deleted: true},
{Remote: "gone.txt", Deleted: true},
{Remote: "dir/a.txt", Deleted: true},
}, deltaApplyOpts{MaxDelete: 50})
require.Error(t, err)
assert.True(t, errors.Is(err, errDeltaTooManyDeletes))
assert.Equal(t, orig, len(ls.list))
assert.True(t, ls.has("keep.txt"))
assert.True(t, ls.has("gone.txt"))
}
func TestApplyListingDeltas_ForceBypassesMaxDelete(t *testing.T) {
ls := testListing()
err := applyListingDeltas(ls, []listingDelta{
{Remote: "keep.txt", Deleted: true},
{Remote: "gone.txt", Deleted: true},
{Remote: "dir/a.txt", Deleted: true},
}, deltaApplyOpts{MaxDelete: 50, Force: true})
require.NoError(t, err)
assert.False(t, ls.has("keep.txt"))
}
func TestApplyListingDeltas_ResetLeavesListingUntouched(t *testing.T) {
ls := testListing()
orig := len(ls.list)
err := applyListingDeltas(ls, []listingDelta{
{Remote: "keep.txt", Deleted: true},
}, deltaApplyOpts{Reset: true, MaxDelete: 50})
require.Error(t, err)
assert.True(t, errors.Is(err, errDeltaCursorReset))
assert.Equal(t, orig, len(ls.list))
assert.True(t, ls.has("keep.txt"))
}
func TestAtomicCursorAndListing(t *testing.T) {
dir := t.TempDir()
listing := filepath.Join(dir, "path2.lst")
ls := testListing()
require.NoError(t, saveListingAndCursor(listing, "CUR1", "/Dropbox", ls))
got, root, err := loadCursor(listing)
require.NoError(t, err)
assert.Equal(t, "CUR1", got)
assert.Equal(t, "/Dropbox", root)
ls2, err := loadListingFile(listing)
require.NoError(t, err)
assert.True(t, ls2.has("keep.txt"))
require.NoError(t, saveListingAndCursor(listing, "CUR2", "/Dropbox", ls))
got, _, err = loadCursor(listing)
require.NoError(t, err)
assert.Equal(t, "CUR2", got)
}
func TestLoadCursor_MissingIsEmpty(t *testing.T) {
dir := t.TempDir()
listing := filepath.Join(dir, "nope.lst")
got, root, err := loadCursor(listing)
require.NoError(t, err)
assert.Empty(t, got)
assert.Empty(t, root)
_, err = os.Stat(cursorPath(listing))
assert.True(t, os.IsNotExist(err))
}
func TestGuardFileCount(t *testing.T) {
opt := deltaApplyOpts{MaxDelete: 50}
require.NoError(t, guardFileCount(0, 0, opt))
require.NoError(t, guardFileCount(100, 80, opt))
require.ErrorIs(t, guardFileCount(100, 0, opt), errDeltaListingEmptied)
require.ErrorIs(t, guardFileCount(100, 40, opt), errDeltaListingCollapsed)
require.NoError(t, guardFileCount(100, 0, deltaApplyOpts{MaxDelete: 50, Force: true}))
}