forked from TrueCloudLab/restic
restore: allow overwrite to replace empty directories and symlinks
With an already existing file tree an old directory or symlink may exist in a place where restore wants to create a new file. Thus, check for unexpected file types and clean up if necessary.
This commit is contained in:
parent
267cd62ae4
commit
f19b69af25
2 changed files with 156 additions and 27 deletions
|
@ -1,11 +1,15 @@
|
||||||
package restorer
|
package restorer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
stdfs "io/fs"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/cespare/xxhash/v2"
|
"github.com/cespare/xxhash/v2"
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/fs"
|
"github.com/restic/restic/internal/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,13 +43,26 @@ func newFilesWriter(count int) *filesWriter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFile(path string, createSize int64, sparse bool) (*os.File, error) {
|
func openFile(path string) (*os.File, error) {
|
||||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600)
|
f, err := os.OpenFile(path, os.O_WRONLY|fs.O_NOFOLLOW, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !fs.IsAccessDenied(err) {
|
return nil, err
|
||||||
return nil, err
|
}
|
||||||
}
|
fi, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
_ = f.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !fi.Mode().IsRegular() {
|
||||||
|
_ = f.Close()
|
||||||
|
return nil, fmt.Errorf("unexpected file type %v at %q", fi.Mode().Type(), path)
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFile(path string, createSize int64, sparse bool) (*os.File, error) {
|
||||||
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|fs.O_NOFOLLOW, 0600)
|
||||||
|
if err != nil && fs.IsAccessDenied(err) {
|
||||||
// If file is readonly, clear the readonly flag by resetting the
|
// If file is readonly, clear the readonly flag by resetting the
|
||||||
// permissions of the file and try again
|
// permissions of the file and try again
|
||||||
// as the metadata will be set again in the second pass and the
|
// as the metadata will be set again in the second pass and the
|
||||||
|
@ -53,40 +70,75 @@ func createFile(path string, createSize int64, sparse bool) (*os.File, error) {
|
||||||
if err = fs.ResetPermissions(path); err != nil {
|
if err = fs.ResetPermissions(path); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if f, err = os.OpenFile(path, os.O_WRONLY, 0600); err != nil {
|
if f, err = os.OpenFile(path, os.O_WRONLY|fs.O_NOFOLLOW, 0600); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if err != nil && (errors.Is(err, syscall.ELOOP) || errors.Is(err, syscall.EISDIR)) {
|
||||||
|
// symlink or directory, try to remove it later on
|
||||||
|
f = nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var fi stdfs.FileInfo
|
||||||
|
if f != nil {
|
||||||
|
// stat to check that we've opened a regular file
|
||||||
|
fi, err = f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
_ = f.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if f == nil || !fi.Mode().IsRegular() {
|
||||||
|
// close handle if we still have it
|
||||||
|
if f != nil {
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// not what we expected, try to get rid of it
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// create a new file, pass O_EXCL to make sure there are no surprises
|
||||||
|
f, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_EXCL|fs.O_NOFOLLOW, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fi, err = f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
_ = f.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ensureSize(f, fi, createSize, sparse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureSize(f *os.File, fi stdfs.FileInfo, createSize int64, sparse bool) (*os.File, error) {
|
||||||
if sparse {
|
if sparse {
|
||||||
err = truncateSparse(f, createSize)
|
err := truncateSparse(f, createSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = f.Close()
|
_ = f.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else if fi.Size() > createSize {
|
||||||
info, err := f.Stat()
|
// file is too long must shorten it
|
||||||
|
err := f.Truncate(createSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = f.Close()
|
_ = f.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if info.Size() > createSize {
|
} else if createSize > 0 {
|
||||||
// file is too long must shorten it
|
err := fs.PreallocateFile(f, createSize)
|
||||||
err = f.Truncate(createSize)
|
if err != nil {
|
||||||
if err != nil {
|
// Just log the preallocate error but don't let it cause the restore process to fail.
|
||||||
_ = f.Close()
|
// Preallocate might return an error if the filesystem (implementation) does not
|
||||||
return nil, err
|
// support preallocation or our parameters combination to the preallocate call
|
||||||
}
|
// This should yield a syscall.ENOTSUP error, but some other errors might also
|
||||||
} else if createSize > 0 {
|
// show up.
|
||||||
err := fs.PreallocateFile(f, createSize)
|
debug.Log("Failed to preallocate %v with size %v: %v", f.Name(), createSize, err)
|
||||||
if err != nil {
|
|
||||||
// Just log the preallocate error but don't let it cause the restore process to fail.
|
|
||||||
// Preallocate might return an error if the filesystem (implementation) does not
|
|
||||||
// support preallocation or our parameters combination to the preallocate call
|
|
||||||
// This should yield a syscall.ENOTSUP error, but some other errors might also
|
|
||||||
// show up.
|
|
||||||
debug.Log("Failed to preallocate %v with size %v: %v", path, createSize, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return f, nil
|
return f, nil
|
||||||
|
@ -110,7 +162,7 @@ func (w *filesWriter) writeToFile(path string, blob []byte, offset int64, create
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else if f, err = os.OpenFile(path, os.O_WRONLY, 0600); err != nil {
|
} else if f, err = openFile(path); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
package restorer
|
package restorer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/errors"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,3 +38,76 @@ func TestFilesWriterBasic(t *testing.T) {
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
rtest.Equals(t, []byte{2, 2}, buf)
|
rtest.Equals(t, []byte{2, 2}, buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateFile(t *testing.T) {
|
||||||
|
basepath := filepath.Join(t.TempDir(), "test")
|
||||||
|
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
create func(t testing.TB, path string)
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"file",
|
||||||
|
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) {
|
||||||
|
rtest.OK(t, os.Mkdir(path, 0o400))
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symlink",
|
||||||
|
func(t testing.TB, path string) {
|
||||||
|
rtest.OK(t, os.Symlink("./something", path))
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filled dir",
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
size int64
|
||||||
|
isSparse bool
|
||||||
|
}{
|
||||||
|
{5, false},
|
||||||
|
{21, false},
|
||||||
|
{100, false},
|
||||||
|
{5, true},
|
||||||
|
{21, true},
|
||||||
|
{100, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, sc := range scenarios {
|
||||||
|
t.Run(sc.name, func(t *testing.T) {
|
||||||
|
for _, test := range tests {
|
||||||
|
path := basepath + fmt.Sprintf("%v", i)
|
||||||
|
sc.create(t, path)
|
||||||
|
f, err := createFile(path, test.size, test.isSparse)
|
||||||
|
if sc.err == nil {
|
||||||
|
rtest.OK(t, err)
|
||||||
|
fi, err := f.Stat()
|
||||||
|
rtest.OK(t, err)
|
||||||
|
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())
|
||||||
|
} else {
|
||||||
|
rtest.Assert(t, errors.Is(err, sc.err), "unexpected error got %v expected %v", err, sc.err)
|
||||||
|
}
|
||||||
|
rtest.OK(t, os.RemoveAll(path))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue