diff --git a/CHANGELOG.md b/CHANGELOG.md index dd5be92a6..3865f4b98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,10 @@ Important Changes in 0.X.Y included) in a restore, they are not loaded from the repo any more. https://github.com/restic/restic/pull/1044 + * Name collisions are now resolved by appending a counter. + https://github.com/restic/restic/issues/1179 + https://github.com/restic/restic/pull/1209 + Small changes ------------- diff --git a/internal/archiver/archiver.go b/internal/archiver/archiver.go index dd7bea06f..3117ec509 100644 --- a/internal/archiver/archiver.go +++ b/internal/archiver/archiver.go @@ -383,7 +383,22 @@ func (arch *Archiver) dirWorker(ctx context.Context, wg *sync.WaitGroup, p *rest panic("invalid null subtree restic.ID") } } - tree.Insert(node) + + // insert node into tree, resolve name collisions + name := node.Name + i := 0 + for { + i++ + err := tree.Insert(node) + if err == nil { + break + } + + newName := fmt.Sprintf("%v-%d", name, i) + fmt.Fprintf(os.Stderr, "%v: name collision for %q, renaming to %q\n", filepath.Dir(node.Path), node.Name, newName) + node.Name = newName + } + } node := &restic.Node{} diff --git a/internal/archiver/archiver_test.go b/internal/archiver/archiver_test.go index 2961d403c..aaaf02a22 100644 --- a/internal/archiver/archiver_test.go +++ b/internal/archiver/archiver_test.go @@ -4,6 +4,9 @@ import ( "bytes" "context" "io" + "io/ioutil" + "os" + "path/filepath" "testing" "time" @@ -312,3 +315,54 @@ func TestArchiveEmptySnapshot(t *testing.T) { t.Errorf("expected null snapshot for empty snapshot, got %v", sn) } } + +func chdir(t testing.TB, target string) (cleanup func()) { + curdir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + t.Logf("chdir to %v", target) + err = os.Chdir(target) + if err != nil { + t.Fatal(err) + } + + return func() { + t.Logf("chdir back to %v", curdir) + err := os.Chdir(curdir) + if err != nil { + t.Fatal(err) + } + } +} + +func TestArchiveNameCollision(t *testing.T) { + repo, cleanup := repository.TestRepository(t) + defer cleanup() + + dir, cleanup := TempDir(t) + defer cleanup() + + root := filepath.Join(dir, "root") + OK(t, os.MkdirAll(root, 0755)) + + OK(t, ioutil.WriteFile(filepath.Join(dir, "testfile"), []byte("testfile1"), 0644)) + OK(t, ioutil.WriteFile(filepath.Join(dir, "root", "testfile"), []byte("testfile2"), 0644)) + + defer chdir(t, root)() + + arch := archiver.New(repo) + + sn, id, err := arch.Snapshot(context.TODO(), nil, []string{"testfile", filepath.Join("..", "testfile")}, nil, "localhost", nil) + OK(t, err) + + t.Logf("snapshot archived as %v", id) + + tree, err := repo.LoadTree(context.TODO(), *sn.Tree) + OK(t, err) + + if len(tree.Nodes) != 2 { + t.Fatalf("tree has %d nodes, wanted 2: %v", len(tree.Nodes), tree.Nodes) + } +}