sync,copy: Fix --fast-list --create-empty-src-dirs and --exclude

Before this change, if --fast-list was in use while doing a sync or
copy with --create-empty-src-dirs and --exclude excluded all the files
from the directory (but not the directory), then the directory would
not be created.

This is also visible with `rclone tree` which uses the same tree
building approach as `rclone sync --fast-list` where the directories
would go missing from the tree view.

This was caused by not adding the parents of excluded files to the
directory tree.

See: https://forum.rclone.org/t/create-empty-src-dirs-issue-with-b2/30856
This commit is contained in:
Nick Craig-Wood 2022-05-20 14:54:04 +01:00
parent 28e43fe7af
commit da404dc0f2
2 changed files with 151 additions and 49 deletions

View file

@ -471,23 +471,39 @@ func walkRDirTree(ctx context.Context, f fs.Fs, startPath string, includeAll boo
defer mu.Unlock() defer mu.Unlock()
for _, entry := range entries { for _, entry := range entries {
slashes := strings.Count(entry.Remote(), "/") slashes := strings.Count(entry.Remote(), "/")
excluded := true
switch x := entry.(type) { switch x := entry.(type) {
case fs.Object: case fs.Object:
// Make sure we don't delete excluded files if not required // Make sure we don't delete excluded files if not required
if includeAll || fi.IncludeObject(ctx, x) { if includeAll || fi.IncludeObject(ctx, x) {
if maxLevel < 0 || slashes <= maxLevel-1 { if maxLevel < 0 || slashes <= maxLevel-1 {
dirs.Add(x) dirs.Add(x)
} else { excluded = false
// Make sure we include any parent directories of excluded objects
dirPath := x.Remote()
for ; slashes > maxLevel-1; slashes-- {
dirPath = parentDir(dirPath)
}
dirs.CheckParent(startPath, dirPath)
} }
} else { } else {
fs.Debugf(x, "Excluded from sync (and deletion)") fs.Debugf(x, "Excluded from sync (and deletion)")
} }
// Make sure we include any parent directories of excluded objects
if excluded {
dirPath := parentDir(x.Remote())
slashes--
if maxLevel >= 0 {
for ; slashes > maxLevel-1; slashes-- {
dirPath = parentDir(dirPath)
}
}
inc, err := includeDirectory(dirPath)
if err != nil {
return err
}
if inc || includeAll {
// If the directory doesn't exist already, create it
_, obj := dirs.Find(dirPath)
if obj == nil {
dirs.AddDir(fs.NewDir(dirPath, time.Now()))
}
}
}
// Check if we need to prune a directory later. // Check if we need to prune a directory later.
if !includeAll && len(fi.Opt.ExcludeFile) > 0 { if !includeAll && len(fi.Opt.ExcludeFile) > 0 {
basename := path.Base(x.Remote()) basename := path.Base(x.Remote())

View file

@ -448,17 +448,32 @@ func TestWalkRDirTree(t *testing.T) {
err error err error
root string root string
level int level int
exclude string
}{ }{
{fs.DirEntries{}, "/\n", nil, "", -1}, {
{fs.DirEntries{mockobject.Object("a")}, `/ entries: fs.DirEntries{},
want: "/\n",
level: -1,
},
{
entries: fs.DirEntries{mockobject.Object("a")},
want: `/
a a
`, nil, "", -1}, `,
{fs.DirEntries{mockobject.Object("a/b")}, `/ level: -1,
},
{
entries: fs.DirEntries{mockobject.Object("a/b")},
want: `/
a/ a/
a/ a/
b b
`, nil, "", -1}, `,
{fs.DirEntries{mockobject.Object("a/b/c/d")}, `/ level: -1,
},
{
entries: fs.DirEntries{mockobject.Object("a/b/c/d")},
want: `/
a/ a/
a/ a/
b/ b/
@ -466,9 +481,16 @@ a/b/
c/ c/
a/b/c/ a/b/c/
d d
`, nil, "", -1}, `,
{fs.DirEntries{mockobject.Object("a")}, "", errorBoom, "", -1}, level: -1,
{fs.DirEntries{ },
{
entries: fs.DirEntries{mockobject.Object("a")},
err: errorBoom,
level: -1,
},
{
entries: fs.DirEntries{
mockobject.Object("0/1/2/3"), mockobject.Object("0/1/2/3"),
mockobject.Object("4/5/6/7"), mockobject.Object("4/5/6/7"),
mockobject.Object("8/9/a/b"), mockobject.Object("8/9/a/b"),
@ -478,7 +500,8 @@ a/b/c/
mockobject.Object("o/p/q/r"), mockobject.Object("o/p/q/r"),
mockobject.Object("s/t/u/v"), mockobject.Object("s/t/u/v"),
mockobject.Object("w/x/y/z"), mockobject.Object("w/x/y/z"),
}, `/ },
want: `/
0/ 0/
4/ 4/
8/ 8/
@ -542,12 +565,16 @@ w/x/
y/ y/
w/x/y/ w/x/y/
z z
`, nil, "", -1}, `,
{fs.DirEntries{ level: -1,
},
{
entries: fs.DirEntries{
mockobject.Object("a/b/c/d/e/f1"), mockobject.Object("a/b/c/d/e/f1"),
mockobject.Object("a/b/c/d/e/f2"), mockobject.Object("a/b/c/d/e/f2"),
mockobject.Object("a/b/c/d/e/f3"), mockobject.Object("a/b/c/d/e/f3"),
}, `a/b/c/ },
want: `a/b/c/
d/ d/
a/b/c/d/ a/b/c/d/
e/ e/
@ -555,32 +582,91 @@ a/b/c/d/e/
f1 f1
f2 f2
f3 f3
`, nil, "a/b/c", -1}, `,
{fs.DirEntries{ root: "a/b/c",
level: -1,
},
{
entries: fs.DirEntries{
mockobject.Object("A"), mockobject.Object("A"),
mockobject.Object("a/B"), mockobject.Object("a/B"),
mockobject.Object("a/b/C"), mockobject.Object("a/b/C"),
mockobject.Object("a/b/c/D"), mockobject.Object("a/b/c/D"),
mockobject.Object("a/b/c/d/E"), mockobject.Object("a/b/c/d/E"),
}, `/ },
want: `/
A A
a/ a/
a/ a/
B B
b/ b/
`, nil, "", 2}, a/b/
{fs.DirEntries{ `,
level: 2,
},
{
entries: fs.DirEntries{
mockobject.Object("a/b/c"), mockobject.Object("a/b/c"),
mockobject.Object("a/b/c/d/e"), mockobject.Object("a/b/c/d/e"),
}, `/ },
want: `/
a/ a/
a/ a/
b/ b/
`, nil, "", 2}, a/b/
`,
level: 2,
},
{
entries: fs.DirEntries{
mockobject.Object("a/.bzEmpty"),
mockobject.Object("a/b1/.bzEmpty"),
mockobject.Object("a/b2/.bzEmpty"),
},
want: `/
a/
a/
.bzEmpty
b1/
b2/
a/b1/
.bzEmpty
a/b2/
.bzEmpty
`,
level: -1,
exclude: ""},
{
entries: fs.DirEntries{
mockobject.Object("a/.bzEmpty"),
mockobject.Object("a/b1/.bzEmpty"),
mockobject.Object("a/b2/.bzEmpty"),
},
want: `/
a/
a/
b1/
b2/
a/b1/
a/b2/
`,
level: -1,
exclude: ".bzEmpty",
},
} { } {
r, err := walkRDirTree(context.Background(), nil, test.root, true, test.level, makeListRCallback(test.entries, test.err)) ctx := context.Background()
assert.Equal(t, test.err, err, fmt.Sprintf("%+v", test)) if test.exclude != "" {
assert.Equal(t, test.want, r.String(), fmt.Sprintf("%+v", test)) fi, err := filter.NewFilter(nil)
require.NoError(t, err)
require.NoError(t, fi.Add(false, test.exclude))
// Change the active filter
ctx = filter.ReplaceConfig(ctx, fi)
}
r, err := walkRDirTree(ctx, nil, test.root, test.exclude == "", test.level, makeListRCallback(test.entries, test.err))
what := fmt.Sprintf("%+v", test)
assert.Equal(t, test.err, err, what)
assert.Equal(t, test.want, r.String(), what)
} }
} }