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:
Michael Eischer
2024-06-01 21:56:56 +02:00
parent 267cd62ae4
commit f19b69af25
2 changed files with 156 additions and 27 deletions

View File

@@ -1,11 +1,15 @@
package restorer
import (
"fmt"
stdfs "io/fs"
"os"
"sync"
"syscall"
"github.com/cespare/xxhash/v2"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"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) {
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600)
func openFile(path string) (*os.File, error) {
f, err := os.OpenFile(path, os.O_WRONLY|fs.O_NOFOLLOW, 0600)
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
// permissions of the file and try again
// 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 {
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 ensureSize(f, fi, createSize, sparse)
}
func ensureSize(f *os.File, fi stdfs.FileInfo, createSize int64, sparse bool) (*os.File, error) {
if sparse {
err = truncateSparse(f, createSize)
err := truncateSparse(f, createSize)
if err != nil {
_ = f.Close()
return nil, err
}
} else {
info, err := f.Stat()
} else if fi.Size() > createSize {
// file is too long must shorten it
err := f.Truncate(createSize)
if err != nil {
_ = f.Close()
return nil, err
}
if info.Size() > createSize {
// file is too long must shorten it
err = f.Truncate(createSize)
if err != nil {
_ = f.Close()
return nil, err
}
} else if createSize > 0 {
err := fs.PreallocateFile(f, createSize)
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)
}
} else if createSize > 0 {
err := fs.PreallocateFile(f, createSize)
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", f.Name(), createSize, err)
}
}
return f, nil
@@ -110,7 +162,7 @@ func (w *filesWriter) writeToFile(path string, blob []byte, offset int64, create
if err != nil {
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
}