From d265ec64f2411bfb259f83f33b4f39fe4e592725 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sat, 1 Jun 2024 22:21:16 +0200 Subject: [PATCH] restore: correctly handle existing hardlinks With hardlinks there's no efficient way to detect which files are linked with each other. Thus, just start from scratch when restore has to modify a hardlinked file. --- internal/restorer/fileswriter.go | 13 ++++++++- internal/restorer/fileswriter_test.go | 41 ++++++++++++++++++--------- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/internal/restorer/fileswriter.go b/internal/restorer/fileswriter.go index 39ad65da8..9e9b6b01c 100644 --- a/internal/restorer/fileswriter.go +++ b/internal/restorer/fileswriter.go @@ -89,7 +89,18 @@ func createFile(path string, createSize int64, sparse bool) (*os.File, error) { return nil, err } } - if f == nil || !fi.Mode().IsRegular() { + + mustReplace := f == nil || !fi.Mode().IsRegular() + if !mustReplace { + ex := fs.ExtendedStat(fi) + if ex.Links > 1 { + // there is no efficient way to find out which other files might be linked to this file + // thus nuke the existing file and start with a fresh one + mustReplace = true + } + } + + if mustReplace { // close handle if we still have it if f != nil { if err := f.Close(); err != nil { diff --git a/internal/restorer/fileswriter_test.go b/internal/restorer/fileswriter_test.go index 74bf479bb..b4252a96e 100644 --- a/internal/restorer/fileswriter_test.go +++ b/internal/restorer/fileswriter_test.go @@ -45,36 +45,46 @@ func TestCreateFile(t *testing.T) { scenarios := []struct { name string create func(t testing.TB, path string) + check func(t testing.TB, path string) err error }{ { - "file", - func(t testing.TB, path string) { + name: "file", + create: func(t testing.TB, path string) { rtest.OK(t, os.WriteFile(path, []byte("test-test-test-data"), 0o400)) }, - nil, }, { - "empty dir", - func(t testing.TB, path string) { + name: "empty dir", + create: func(t testing.TB, path string) { rtest.OK(t, os.Mkdir(path, 0o400)) }, - nil, }, { - "symlink", - func(t testing.TB, path string) { + name: "symlink", + create: func(t testing.TB, path string) { rtest.OK(t, os.Symlink("./something", path)) }, - nil, }, { - "filled dir", - func(t testing.TB, path string) { + name: "filled dir", + create: func(t testing.TB, path string) { rtest.OK(t, os.Mkdir(path, 0o700)) rtest.OK(t, os.WriteFile(filepath.Join(path, "file"), []byte("data"), 0o400)) }, - syscall.ENOTEMPTY, + err: syscall.ENOTEMPTY, + }, + { + name: "hardlinks", + create: func(t testing.TB, path string) { + rtest.OK(t, os.WriteFile(path, []byte("test-test-test-data"), 0o400)) + rtest.OK(t, os.Link(path, path+"h")) + }, + check: func(t testing.TB, path string) { + data, err := os.ReadFile(path + "h") + rtest.OK(t, err) + rtest.Equals(t, "test-test-test-data", string(data), "unexpected content change") + }, }, } @@ -92,8 +102,8 @@ func TestCreateFile(t *testing.T) { for i, sc := range scenarios { t.Run(sc.name, func(t *testing.T) { - for _, test := range tests { - path := basepath + fmt.Sprintf("%v", i) + for j, test := range tests { + path := basepath + fmt.Sprintf("%v%v", i, j) sc.create(t, path) f, err := createFile(path, test.size, test.isSparse) if sc.err == nil { @@ -103,6 +113,9 @@ func TestCreateFile(t *testing.T) { rtest.Assert(t, fi.Mode().IsRegular(), "wrong filetype %v", fi.Mode()) rtest.Assert(t, fi.Size() <= test.size, "unexpected file size expected %v, got %v", test.size, fi.Size()) rtest.OK(t, f.Close()) + if sc.check != nil { + sc.check(t, path) + } } else { rtest.Assert(t, errors.Is(err, sc.err), "unexpected error got %v expected %v", err, sc.err) }