diff --git a/internal/archiver/tree.go b/internal/archiver/tree.go index 8adca2cc3..5835839b2 100644 --- a/internal/archiver/tree.go +++ b/internal/archiver/tree.go @@ -202,30 +202,53 @@ func (t Tree) String() string { // formatTree returns a text representation of the tree t. func formatTree(t Tree, indent string) (s string) { for name, node := range t.Nodes { - if node.Path != "" { - s += fmt.Sprintf("%v/%v, src %q\n", indent, name, node.Path) - continue - } - s += fmt.Sprintf("%v/%v, root %q, meta %q\n", indent, name, node.Root, node.FileInfoPath) + s += fmt.Sprintf("%v/%v, root %q, path %q, meta %q\n", indent, name, node.Root, node.Path, node.FileInfoPath) s += formatTree(node, indent+" ") } return s } -// prune removes sub-trees of leaf nodes. -func prune(t *Tree) { - // if the current tree is a leaf node (Path is set), remove all nodes, - // those are automatically included anyway. +// unrollTree unrolls the tree so that only leaf nodes have Path set. +func unrollTree(f fs.FS, t *Tree) error { + // if the current tree is a leaf node (Path is set) and has additional + // nodes, add the contents of Path to the nodes. if t.Path != "" && len(t.Nodes) > 0 { - t.FileInfoPath = "" - t.Nodes = nil - return + debug.Log("resolve path %v", t.Path) + entries, err := fs.ReadDirNames(f, t.Path) + if err != nil { + return err + } + + for _, entry := range entries { + if node, ok := t.Nodes[entry]; ok { + if node.Path == "" { + node.Path = f.Join(t.Path, entry) + t.Nodes[entry] = node + continue + } + + if node.Path == f.Join(t.Path, entry) { + continue + } + + return errors.Errorf("tree unrollTree: collision on path, node %#v, path %q", node, f.Join(t.Path, entry)) + continue + } + t.Nodes[entry] = Tree{Path: f.Join(t.Path, entry)} + } + t.Path = "" } for i, subtree := range t.Nodes { - prune(&subtree) + err := unrollTree(f, &subtree) + if err != nil { + return err + } + t.Nodes[i] = subtree } + + return nil } // NewTree creates a Tree from the target files/directories. @@ -248,7 +271,12 @@ func NewTree(fs fs.FS, targets []string) (*Tree, error) { } } - prune(tree) + debug.Log("before unroll:\n%v", tree) + err := unrollTree(fs, tree) + if err != nil { + return nil, err + } + debug.Log("result:\n%v", tree) return tree, nil } diff --git a/internal/archiver/tree_test.go b/internal/archiver/tree_test.go index f50bb510f..5d460bb37 100644 --- a/internal/archiver/tree_test.go +++ b/internal/archiver/tree_test.go @@ -7,6 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/restic/restic/internal/fs" + restictest "github.com/restic/restic/internal/test" ) func TestPathComponents(t *testing.T) { @@ -136,6 +137,7 @@ func TestRootDirectory(t *testing.T) { func TestTree(t *testing.T) { var tests = []struct { targets []string + src TestDir want Tree unix bool win bool @@ -227,39 +229,152 @@ func TestTree(t *testing.T) { }}, }, { + src: TestDir{ + "foo": TestDir{ + "file": TestFile{Content: "file content"}, + "work": TestFile{Content: "work file content"}, + }, + }, + targets: []string{"foo", "foo/work"}, + want: Tree{Nodes: map[string]Tree{ + "foo": Tree{ + Root: ".", + FileInfoPath: "foo", + Nodes: map[string]Tree{ + "file": Tree{Path: filepath.FromSlash("foo/file")}, + "work": Tree{Path: filepath.FromSlash("foo/work")}, + }, + }, + }}, + }, + { + src: TestDir{ + "foo": TestDir{ + "file": TestFile{Content: "file content"}, + "work": TestDir{ + "other": TestFile{Content: "other file content"}, + }, + }, + }, + targets: []string{"foo/work", "foo"}, + want: Tree{Nodes: map[string]Tree{ + "foo": Tree{ + Root: ".", + FileInfoPath: "foo", + Nodes: map[string]Tree{ + "file": Tree{Path: filepath.FromSlash("foo/file")}, + "work": Tree{Path: filepath.FromSlash("foo/work")}, + }, + }, + }}, + }, + { + src: TestDir{ + "foo": TestDir{ + "work": TestDir{ + "user1": TestFile{Content: "file content"}, + "user2": TestFile{Content: "other file content"}, + }, + }, + }, targets: []string{"foo/work", "foo/work/user2"}, want: Tree{Nodes: map[string]Tree{ "foo": Tree{Root: ".", FileInfoPath: "foo", Nodes: map[string]Tree{ "work": Tree{ - Path: filepath.FromSlash("foo/work"), + FileInfoPath: filepath.FromSlash("foo/work"), + Nodes: map[string]Tree{ + "user1": Tree{Path: filepath.FromSlash("foo/work/user1")}, + "user2": Tree{Path: filepath.FromSlash("foo/work/user2")}, + }, }, }}, }}, }, { + src: TestDir{ + "foo": TestDir{ + "work": TestDir{ + "user1": TestFile{Content: "file content"}, + "user2": TestFile{Content: "other file content"}, + }, + }, + }, targets: []string{"foo/work/user2", "foo/work"}, want: Tree{Nodes: map[string]Tree{ "foo": Tree{Root: ".", FileInfoPath: "foo", Nodes: map[string]Tree{ - "work": Tree{ - Path: filepath.FromSlash("foo/work"), + "work": Tree{FileInfoPath: filepath.FromSlash("foo/work"), + Nodes: map[string]Tree{ + "user1": Tree{Path: filepath.FromSlash("foo/work/user1")}, + "user2": Tree{Path: filepath.FromSlash("foo/work/user2")}, + }, }, }}, }}, }, { + src: TestDir{ + "foo": TestDir{ + "other": TestFile{Content: "file content"}, + "work": TestDir{ + "user2": TestDir{ + "data": TestDir{ + "secret": TestFile{Content: "secret file content"}, + }, + }, + "user3": TestDir{ + "important.txt": TestFile{Content: "important work"}, + }, + }, + }, + }, targets: []string{"foo/work/user2/data/secret", "foo"}, want: Tree{Nodes: map[string]Tree{ - "foo": Tree{Root: ".", Path: "foo"}, + "foo": Tree{Root: ".", FileInfoPath: "foo", Nodes: map[string]Tree{ + "other": Tree{Path: filepath.FromSlash("foo/other")}, + "work": Tree{FileInfoPath: filepath.FromSlash("foo/work"), Nodes: map[string]Tree{ + "user2": Tree{FileInfoPath: filepath.FromSlash("foo/work/user2"), Nodes: map[string]Tree{ + "data": Tree{FileInfoPath: filepath.FromSlash("foo/work/user2/data"), Nodes: map[string]Tree{ + "secret": Tree{ + Path: filepath.FromSlash("foo/work/user2/data/secret"), + }, + }}, + }}, + "user3": Tree{Path: filepath.FromSlash("foo/work/user3")}, + }}, + }}, }}, }, { - unix: true, - targets: []string{"/mnt/driveA", "/mnt/driveA/work/driveB"}, - want: Tree{Nodes: map[string]Tree{ - "mnt": Tree{Root: "/", FileInfoPath: filepath.FromSlash("/mnt"), Nodes: map[string]Tree{ - "driveA": Tree{ - Path: filepath.FromSlash("/mnt/driveA"), + src: TestDir{ + "mnt": TestDir{ + "driveA": TestDir{ + "work": TestDir{ + "driveB": TestDir{ + "secret": TestFile{Content: "secret file content"}, + }, + "test1": TestDir{ + "important.txt": TestFile{Content: "important work"}, + }, + }, + "test2": TestDir{ + "important.txt": TestFile{Content: "other important work"}, + }, }, + }, + }, + unix: true, + targets: []string{"mnt/driveA", "mnt/driveA/work/driveB"}, + want: Tree{Nodes: map[string]Tree{ + "mnt": Tree{Root: ".", FileInfoPath: filepath.FromSlash("mnt"), Nodes: map[string]Tree{ + "driveA": Tree{FileInfoPath: filepath.FromSlash("mnt/driveA"), Nodes: map[string]Tree{ + "work": Tree{FileInfoPath: filepath.FromSlash("mnt/driveA/work"), Nodes: map[string]Tree{ + "driveB": Tree{ + Path: filepath.FromSlash("mnt/driveA/work/driveB"), + }, + "test1": Tree{Path: filepath.FromSlash("mnt/driveA/work/test1")}, + }}, + "test2": Tree{Path: filepath.FromSlash("mnt/driveA/test2")}, + }}, }}, }}, }, @@ -320,6 +435,14 @@ func TestTree(t *testing.T) { t.Skip("skip test on unix") } + tempdir, cleanup := restictest.TempDir(t) + defer cleanup() + + TestCreateFiles(t, tempdir, test.src) + + back := fs.TestChdir(t, tempdir) + defer back() + tree, err := NewTree(fs.Local{}, test.targets) if test.mustError { if err == nil {