Merge pull request #5022 from MichaelEischer/extract-fs-code

Extract filesystem code from restic.Node
This commit is contained in:
Michael Eischer 2024-08-31 17:52:11 +02:00 committed by GitHub
commit e5cdae9c84
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 952 additions and 1086 deletions

View file

@ -248,7 +248,7 @@ func (arch *Archiver) trackItem(item string, previous, current *restic.Node, s I
// nodeFromFileInfo returns the restic node from an os.FileInfo. // nodeFromFileInfo returns the restic node from an os.FileInfo.
func (arch *Archiver) nodeFromFileInfo(snPath, filename string, fi os.FileInfo, ignoreXattrListError bool) (*restic.Node, error) { func (arch *Archiver) nodeFromFileInfo(snPath, filename string, fi os.FileInfo, ignoreXattrListError bool) (*restic.Node, error) {
node, err := restic.NodeFromFileInfo(filename, fi, ignoreXattrListError) node, err := fs.NodeFromFileInfo(filename, fi, ignoreXattrListError)
if !arch.WithAtime { if !arch.WithAtime {
node.AccessTime = node.ModTime node.AccessTime = node.ModTime
} }
@ -446,7 +446,7 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
} }
switch { switch {
case fs.IsRegularFile(fi): case fi.Mode().IsRegular():
debug.Log(" %v regular file", target) debug.Log(" %v regular file", target)
// check if the file has not changed before performing a fopen operation (more expensive, specially // check if the file has not changed before performing a fopen operation (more expensive, specially
@ -505,7 +505,7 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
} }
// make sure it's still a file // make sure it's still a file
if !fs.IsRegularFile(fi) { if !fi.Mode().IsRegular() {
err = errors.Errorf("file %v changed type, refusing to archive", fi.Name()) err = errors.Errorf("file %v changed type, refusing to archive", fi.Name())
_ = file.Close() _ = file.Close()
err = arch.error(abstarget, err) err = arch.error(abstarget, err)

View file

@ -557,7 +557,7 @@ func rename(t testing.TB, oldname, newname string) {
} }
func nodeFromFI(t testing.TB, filename string, fi os.FileInfo) *restic.Node { func nodeFromFI(t testing.TB, filename string, fi os.FileInfo) *restic.Node {
node, err := restic.NodeFromFileInfo(filename, fi, false) node, err := fs.NodeFromFileInfo(filename, fi, false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -1664,15 +1664,6 @@ type MockFS struct {
bytesRead map[string]int // tracks bytes read from all opened files bytesRead map[string]int // tracks bytes read from all opened files
} }
func (m *MockFS) Open(name string) (fs.File, error) {
f, err := m.FS.Open(name)
if err != nil {
return f, err
}
return MockFile{File: f, fs: m, filename: name}, nil
}
func (m *MockFS) OpenFile(name string, flag int, perm os.FileMode) (fs.File, error) { func (m *MockFS) OpenFile(name string, flag int, perm os.FileMode) (fs.File, error) {
f, err := m.FS.OpenFile(name, flag, perm) f, err := m.FS.OpenFile(name, flag, perm)
if err != nil { if err != nil {
@ -2061,14 +2052,6 @@ type TrackFS struct {
m sync.Mutex m sync.Mutex
} }
func (m *TrackFS) Open(name string) (fs.File, error) {
m.m.Lock()
m.opened[name]++
m.m.Unlock()
return m.FS.Open(name)
}
func (m *TrackFS) OpenFile(name string, flag int, perm os.FileMode) (fs.File, error) { func (m *TrackFS) OpenFile(name string, flag int, perm os.FileMode) (fs.File, error) {
m.m.Lock() m.m.Lock()
m.opened[name]++ m.opened[name]++
@ -2291,7 +2274,7 @@ func TestMetadataChanged(t *testing.T) {
// get metadata // get metadata
fi := lstat(t, "testfile") fi := lstat(t, "testfile")
want, err := restic.NodeFromFileInfo("testfile", fi, false) want, err := fs.NodeFromFileInfo("testfile", fi, false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View file

@ -48,7 +48,7 @@ func wrapFileInfo(fi os.FileInfo) os.FileInfo {
func statAndSnapshot(t *testing.T, repo archiverRepo, name string) (*restic.Node, *restic.Node) { func statAndSnapshot(t *testing.T, repo archiverRepo, name string) (*restic.Node, *restic.Node) {
fi := lstat(t, name) fi := lstat(t, name)
want, err := restic.NodeFromFileInfo(name, fi, false) want, err := fs.NodeFromFileInfo(name, fi, false)
rtest.OK(t, err) rtest.OK(t, err)
_, node := snapshot(t, repo, fs.Local{}, nil, name) _, node := snapshot(t, repo, fs.Local{}, nil, name)

View file

@ -50,7 +50,7 @@ func startFileSaver(ctx context.Context, t testing.TB) (*FileSaver, context.Cont
s := NewFileSaver(ctx, wg, saveBlob, pol, workers, workers) s := NewFileSaver(ctx, wg, saveBlob, pol, workers, workers)
s.NodeFromFileInfo = func(snPath, filename string, fi os.FileInfo, ignoreXattrListError bool) (*restic.Node, error) { s.NodeFromFileInfo = func(snPath, filename string, fi os.FileInfo, ignoreXattrListError bool) (*restic.Node, error) {
return restic.NodeFromFileInfo(filename, fi, ignoreXattrListError) return fs.NodeFromFileInfo(filename, fi, ignoreXattrListError)
} }
return s, ctx, wg return s, ctx, wg
@ -72,7 +72,7 @@ func TestFileSaver(t *testing.T) {
var results []FutureNode var results []FutureNode
for _, filename := range files { for _, filename := range files {
f, err := testFs.Open(filename) f, err := testFs.OpenFile(filename, os.O_RDONLY, 0)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View file

@ -169,7 +169,7 @@ func TestEnsureFiles(t testing.TB, target string, dir TestDir) {
} }
return nil return nil
case TestFile: case TestFile:
if !fs.IsRegularFile(fi) { if !fi.Mode().IsRegular() {
t.Errorf("is not a regular file: %v", path) t.Errorf("is not a regular file: %v", path)
return nil return nil
} }
@ -208,7 +208,7 @@ func TestEnsureFiles(t testing.TB, target string, dir TestDir) {
}) })
// then, traverse the directory again, looking for additional files // then, traverse the directory again, looking for additional files
err := fs.Walk(target, func(path string, fi os.FileInfo, err error) error { err := filepath.Walk(target, func(path string, fi os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }

View file

@ -122,7 +122,7 @@ func TestTestCreateFiles(t *testing.T) {
switch node := item.(type) { switch node := item.(type) {
case TestFile: case TestFile:
if !fs.IsRegularFile(fi) { if !fi.Mode().IsRegular() {
t.Errorf("is not regular file: %v", name) t.Errorf("is not regular file: %v", name)
continue continue
} }

View file

@ -142,7 +142,7 @@ func TestSetGetFileEA(t *testing.T) {
testFilePath, testFile := setupTestFile(t) testFilePath, testFile := setupTestFile(t)
testEAs := generateTestEAs(t, 3, testFilePath) testEAs := generateTestEAs(t, 3, testFilePath)
fileHandle := openFile(t, testFilePath, windows.FILE_ATTRIBUTE_NORMAL) fileHandle := openFile(t, testFilePath, windows.FILE_ATTRIBUTE_NORMAL)
defer closeFileHandle(t, testFilePath, testFile, fileHandle) defer testCloseFileHandle(t, testFilePath, testFile, fileHandle)
testSetGetEA(t, testFilePath, fileHandle, testEAs) testSetGetEA(t, testFilePath, fileHandle, testEAs)
} }
@ -154,7 +154,7 @@ func TestSetGetFolderEA(t *testing.T) {
testEAs := generateTestEAs(t, 3, testFolderPath) testEAs := generateTestEAs(t, 3, testFolderPath)
fileHandle := openFile(t, testFolderPath, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_BACKUP_SEMANTICS) fileHandle := openFile(t, testFolderPath, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_BACKUP_SEMANTICS)
defer closeFileHandle(t, testFolderPath, nil, fileHandle) defer testCloseFileHandle(t, testFolderPath, nil, fileHandle)
testSetGetEA(t, testFolderPath, fileHandle, testEAs) testSetGetEA(t, testFolderPath, fileHandle, testEAs)
} }
@ -212,7 +212,7 @@ func openFile(t *testing.T, path string, attributes uint32) windows.Handle {
return fileHandle return fileHandle
} }
func closeFileHandle(t *testing.T, testfilePath string, testFile *os.File, handle windows.Handle) { func testCloseFileHandle(t *testing.T, testfilePath string, testFile *os.File, handle windows.Handle) {
if testFile != nil { if testFile != nil {
err := testFile.Close() err := testFile.Close()
if err != nil { if err != nil {

View file

@ -3,7 +3,6 @@ package fs
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"time" "time"
) )
@ -75,15 +74,6 @@ func Lstat(name string) (os.FileInfo, error) {
return os.Lstat(fixpath(name)) return os.Lstat(fixpath(name))
} }
// Create creates the named file with mode 0666 (before umask), truncating
// it if it already exists. If successful, methods on the returned
// File can be used for I/O; the associated file descriptor has mode
// O_RDWR.
// If there is an error, it will be of type *PathError.
func Create(name string) (*os.File, error) {
return os.Create(fixpath(name))
}
// Open opens a file for reading. // Open opens a file for reading.
func Open(name string) (File, error) { func Open(name string) (File, error) {
return os.Open(fixpath(name)) return os.Open(fixpath(name))
@ -98,25 +88,6 @@ func OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) {
return os.OpenFile(fixpath(name), flag, perm) return os.OpenFile(fixpath(name), flag, perm)
} }
// Walk walks the file tree rooted at root, calling walkFn for each file or
// directory in the tree, including root. All errors that arise visiting files
// and directories are filtered by walkFn. The files are walked in lexical
// order, which makes the output deterministic but means that for very
// large directories Walk can be inefficient.
// Walk does not follow symbolic links.
func Walk(root string, walkFn filepath.WalkFunc) error {
return filepath.Walk(fixpath(root), walkFn)
}
// RemoveIfExists removes a file, returning no error if it does not exist.
func RemoveIfExists(filename string) error {
err := os.Remove(filename)
if err != nil && os.IsNotExist(err) {
err = nil
}
return err
}
// Chtimes changes the access and modification times of the named file, // Chtimes changes the access and modification times of the named file,
// similar to the Unix utime() or utimes() functions. // similar to the Unix utime() or utimes() functions.
// //

View file

@ -18,16 +18,6 @@ func (fs Local) VolumeName(path string) string {
return filepath.VolumeName(path) return filepath.VolumeName(path)
} }
// Open opens a file for reading.
func (fs Local) Open(name string) (File, error) {
f, err := os.Open(fixpath(name))
if err != nil {
return nil, err
}
_ = setFlags(f)
return f, nil
}
// OpenFile is the generalized open call; most users will use Open // OpenFile is the generalized open call; most users will use Open
// or Create instead. It opens the named file with specified flag // or Create instead. It opens the named file with specified flag
// (O_RDONLY etc.) and perm, (0666 etc.) if applicable. If successful, // (O_RDONLY etc.) and perm, (0666 etc.) if applicable. If successful,

View file

@ -125,11 +125,6 @@ func (fs *LocalVss) DeleteSnapshots() {
fs.snapshots = activeSnapshots fs.snapshots = activeSnapshots
} }
// Open wraps the Open method of the underlying file system.
func (fs *LocalVss) Open(name string) (File, error) {
return os.Open(fs.snapshotPath(name))
}
// OpenFile wraps the Open method of the underlying file system. // OpenFile wraps the Open method of the underlying file system.
func (fs *LocalVss) OpenFile(name string, flag int, perm os.FileMode) (File, error) { func (fs *LocalVss) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
return os.OpenFile(fs.snapshotPath(name), flag, perm) return os.OpenFile(fs.snapshotPath(name), flag, perm)

View file

@ -39,29 +39,6 @@ func (fs *Reader) VolumeName(_ string) string {
return "" return ""
} }
// Open opens a file for reading.
func (fs *Reader) Open(name string) (f File, err error) {
switch name {
case fs.Name:
fs.open.Do(func() {
f = newReaderFile(fs.ReadCloser, fs.fi(), fs.AllowEmptyFile)
})
if f == nil {
return nil, pathError("open", name, syscall.EIO)
}
return f, nil
case "/", ".":
f = fakeDir{
entries: []os.FileInfo{fs.fi()},
}
return f, nil
}
return nil, pathError("open", name, syscall.ENOENT)
}
func (fs *Reader) fi() os.FileInfo { func (fs *Reader) fi() os.FileInfo {
return fakeFileInfo{ return fakeFileInfo{
name: fs.Name, name: fs.Name,
@ -82,6 +59,8 @@ func (fs *Reader) OpenFile(name string, flag int, _ os.FileMode) (f File, err er
fmt.Errorf("invalid combination of flags 0x%x", flag)) fmt.Errorf("invalid combination of flags 0x%x", flag))
} }
switch name {
case fs.Name:
fs.open.Do(func() { fs.open.Do(func() {
f = newReaderFile(fs.ReadCloser, fs.fi(), fs.AllowEmptyFile) f = newReaderFile(fs.ReadCloser, fs.fi(), fs.AllowEmptyFile)
}) })
@ -91,6 +70,14 @@ func (fs *Reader) OpenFile(name string, flag int, _ os.FileMode) (f File, err er
} }
return f, nil return f, nil
case "/", ".":
f = fakeDir{
entries: []os.FileInfo{fs.fi()},
}
return f, nil
}
return nil, pathError("open", name, syscall.ENOENT)
} }
// Stat returns a FileInfo describing the named file. If there is an error, it // Stat returns a FileInfo describing the named file. If there is an error, it

View file

@ -15,27 +15,6 @@ import (
"github.com/restic/restic/internal/test" "github.com/restic/restic/internal/test"
) )
func verifyFileContentOpen(t testing.TB, fs FS, filename string, want []byte) {
f, err := fs.Open(filename)
if err != nil {
t.Fatal(err)
}
buf, err := io.ReadAll(f)
if err != nil {
t.Fatal(err)
}
err = f.Close()
if err != nil {
t.Fatal(err)
}
if !cmp.Equal(want, buf) {
t.Error(cmp.Diff(want, buf))
}
}
func verifyFileContentOpenFile(t testing.TB, fs FS, filename string, want []byte) { func verifyFileContentOpenFile(t testing.TB, fs FS, filename string, want []byte) {
f, err := fs.OpenFile(filename, O_RDONLY, 0) f, err := fs.OpenFile(filename, O_RDONLY, 0)
if err != nil { if err != nil {
@ -58,7 +37,7 @@ func verifyFileContentOpenFile(t testing.TB, fs FS, filename string, want []byte
} }
func verifyDirectoryContents(t testing.TB, fs FS, dir string, want []string) { func verifyDirectoryContents(t testing.TB, fs FS, dir string, want []string) {
f, err := fs.Open(dir) f, err := fs.OpenFile(dir, os.O_RDONLY, 0)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -96,7 +75,7 @@ func (s fiSlice) Swap(i, j int) {
} }
func verifyDirectoryContentsFI(t testing.TB, fs FS, dir string, want []os.FileInfo) { func verifyDirectoryContentsFI(t testing.TB, fs FS, dir string, want []os.FileInfo) {
f, err := fs.Open(dir) f, err := fs.OpenFile(dir, os.O_RDONLY, 0)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -219,12 +198,6 @@ func TestFSReader(t *testing.T) {
verifyDirectoryContentsFI(t, fs, ".", []os.FileInfo{fi}) verifyDirectoryContentsFI(t, fs, ".", []os.FileInfo{fi})
}, },
}, },
{
name: "file/Open",
f: func(t *testing.T, fs FS) {
verifyFileContentOpen(t, fs, filename, data)
},
},
{ {
name: "file/OpenFile", name: "file/OpenFile",
f: func(t *testing.T, fs FS) { f: func(t *testing.T, fs FS) {
@ -245,7 +218,7 @@ func TestFSReader(t *testing.T) {
{ {
name: "file/Stat", name: "file/Stat",
f: func(t *testing.T, fs FS) { f: func(t *testing.T, fs FS) {
f, err := fs.Open(filename) f, err := fs.OpenFile(filename, os.O_RDONLY, 0)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -417,7 +390,7 @@ func TestFSReaderMinFileSize(t *testing.T) {
AllowEmptyFile: test.allowEmpty, AllowEmptyFile: test.allowEmpty,
} }
f, err := fs.Open("testfile") f, err := fs.OpenFile("testfile", os.O_RDONLY, 0)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View file

@ -15,16 +15,6 @@ type Track struct {
FS FS
} }
// Open wraps the Open method of the underlying file system.
func (fs Track) Open(name string) (File, error) {
f, err := fs.FS.Open(fixpath(name))
if err != nil {
return nil, err
}
return newTrackFile(debug.Stack(), name, f), nil
}
// OpenFile wraps the OpenFile method of the underlying file system. // OpenFile wraps the OpenFile method of the underlying file system.
func (fs Track) OpenFile(name string, flag int, perm os.FileMode) (File, error) { func (fs Track) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
f, err := fs.FS.OpenFile(fixpath(name), flag, perm) f, err := fs.FS.OpenFile(fixpath(name), flag, perm)

View file

@ -1,13 +0,0 @@
package fs
import "os"
// IsRegularFile returns true if fi belongs to a normal file. If fi is nil,
// false is returned.
func IsRegularFile(fi os.FileInfo) bool {
if fi == nil {
return false
}
return fi.Mode()&os.ModeType == 0
}

View file

@ -7,7 +7,6 @@ import (
// FS bundles all methods needed for a file system. // FS bundles all methods needed for a file system.
type FS interface { type FS interface {
Open(name string) (File, error)
OpenFile(name string, flag int, perm os.FileMode) (File, error) OpenFile(name string, flag int, perm os.FileMode) (File, error)
Stat(name string) (os.FileInfo, error) Stat(name string) (os.FileInfo, error)
Lstat(name string) (os.FileInfo, error) Lstat(name string) (os.FileInfo, error)

View file

@ -1,7 +1,7 @@
//go:build !freebsd && !windows //go:build !freebsd && !windows
// +build !freebsd,!windows // +build !freebsd,!windows
package restic package fs
import "golang.org/x/sys/unix" import "golang.org/x/sys/unix"

334
internal/fs/node.go Normal file
View file

@ -0,0 +1,334 @@
package fs
import (
"os"
"os/user"
"strconv"
"sync"
"syscall"
"time"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic"
)
// NodeFromFileInfo returns a new node from the given path and FileInfo. It
// returns the first error that is encountered, together with a node.
func NodeFromFileInfo(path string, fi os.FileInfo, ignoreXattrListError bool) (*restic.Node, error) {
mask := os.ModePerm | os.ModeType | os.ModeSetuid | os.ModeSetgid | os.ModeSticky
node := &restic.Node{
Path: path,
Name: fi.Name(),
Mode: fi.Mode() & mask,
ModTime: fi.ModTime(),
}
node.Type = nodeTypeFromFileInfo(fi)
if node.Type == "file" {
node.Size = uint64(fi.Size())
}
err := nodeFillExtra(node, path, fi, ignoreXattrListError)
return node, err
}
func nodeTypeFromFileInfo(fi os.FileInfo) string {
switch fi.Mode() & os.ModeType {
case 0:
return "file"
case os.ModeDir:
return "dir"
case os.ModeSymlink:
return "symlink"
case os.ModeDevice | os.ModeCharDevice:
return "chardev"
case os.ModeDevice:
return "dev"
case os.ModeNamedPipe:
return "fifo"
case os.ModeSocket:
return "socket"
case os.ModeIrregular:
return "irregular"
}
return ""
}
func nodeFillExtra(node *restic.Node, path string, fi os.FileInfo, ignoreXattrListError bool) error {
stat, ok := toStatT(fi.Sys())
if !ok {
// fill minimal info with current values for uid, gid
node.UID = uint32(os.Getuid())
node.GID = uint32(os.Getgid())
node.ChangeTime = node.ModTime
return nil
}
node.Inode = uint64(stat.ino())
node.DeviceID = uint64(stat.dev())
nodeFillTimes(node, stat)
nodeFillUser(node, stat)
switch node.Type {
case "file":
node.Size = uint64(stat.size())
node.Links = uint64(stat.nlink())
case "dir":
case "symlink":
var err error
node.LinkTarget, err = Readlink(path)
node.Links = uint64(stat.nlink())
if err != nil {
return errors.WithStack(err)
}
case "dev":
node.Device = uint64(stat.rdev())
node.Links = uint64(stat.nlink())
case "chardev":
node.Device = uint64(stat.rdev())
node.Links = uint64(stat.nlink())
case "fifo":
case "socket":
default:
return errors.Errorf("unsupported file type %q", node.Type)
}
allowExtended, err := nodeFillGenericAttributes(node, path, fi, stat)
if allowExtended {
// Skip processing ExtendedAttributes if allowExtended is false.
err = errors.CombineErrors(err, nodeFillExtendedAttributes(node, path, ignoreXattrListError))
}
return err
}
func nodeFillTimes(node *restic.Node, stat *statT) {
ctim := stat.ctim()
atim := stat.atim()
node.ChangeTime = time.Unix(ctim.Unix())
node.AccessTime = time.Unix(atim.Unix())
}
func nodeFillUser(node *restic.Node, stat *statT) {
uid, gid := stat.uid(), stat.gid()
node.UID, node.GID = uid, gid
node.User = lookupUsername(uid)
node.Group = lookupGroup(gid)
}
var (
uidLookupCache = make(map[uint32]string)
uidLookupCacheMutex = sync.RWMutex{}
)
// Cached user name lookup by uid. Returns "" when no name can be found.
func lookupUsername(uid uint32) string {
uidLookupCacheMutex.RLock()
username, ok := uidLookupCache[uid]
uidLookupCacheMutex.RUnlock()
if ok {
return username
}
u, err := user.LookupId(strconv.Itoa(int(uid)))
if err == nil {
username = u.Username
}
uidLookupCacheMutex.Lock()
uidLookupCache[uid] = username
uidLookupCacheMutex.Unlock()
return username
}
var (
gidLookupCache = make(map[uint32]string)
gidLookupCacheMutex = sync.RWMutex{}
)
// Cached group name lookup by gid. Returns "" when no name can be found.
func lookupGroup(gid uint32) string {
gidLookupCacheMutex.RLock()
group, ok := gidLookupCache[gid]
gidLookupCacheMutex.RUnlock()
if ok {
return group
}
g, err := user.LookupGroupId(strconv.Itoa(int(gid)))
if err == nil {
group = g.Name
}
gidLookupCacheMutex.Lock()
gidLookupCache[gid] = group
gidLookupCacheMutex.Unlock()
return group
}
// NodeCreateAt creates the node at the given path but does NOT restore node meta data.
func NodeCreateAt(node *restic.Node, path string) error {
debug.Log("create node %v at %v", node.Name, path)
switch node.Type {
case "dir":
if err := nodeCreateDirAt(node, path); err != nil {
return err
}
case "file":
if err := nodeCreateFileAt(path); err != nil {
return err
}
case "symlink":
if err := nodeCreateSymlinkAt(node, path); err != nil {
return err
}
case "dev":
if err := nodeCreateDevAt(node, path); err != nil {
return err
}
case "chardev":
if err := nodeCreateCharDevAt(node, path); err != nil {
return err
}
case "fifo":
if err := nodeCreateFifoAt(path); err != nil {
return err
}
case "socket":
return nil
default:
return errors.Errorf("filetype %q not implemented", node.Type)
}
return nil
}
func nodeCreateDirAt(node *restic.Node, path string) error {
err := Mkdir(path, node.Mode)
if err != nil && !os.IsExist(err) {
return errors.WithStack(err)
}
return nil
}
func nodeCreateFileAt(path string) error {
f, err := OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
return errors.WithStack(err)
}
if err := f.Close(); err != nil {
return errors.WithStack(err)
}
return nil
}
func nodeCreateSymlinkAt(node *restic.Node, path string) error {
if err := Symlink(node.LinkTarget, path); err != nil {
return errors.WithStack(err)
}
return nil
}
func nodeCreateDevAt(node *restic.Node, path string) error {
return mknod(path, syscall.S_IFBLK|0600, node.Device)
}
func nodeCreateCharDevAt(node *restic.Node, path string) error {
return mknod(path, syscall.S_IFCHR|0600, node.Device)
}
func nodeCreateFifoAt(path string) error {
return mkfifo(path, 0600)
}
func mkfifo(path string, mode uint32) (err error) {
return mknod(path, mode|syscall.S_IFIFO, 0)
}
// NodeRestoreMetadata restores node metadata
func NodeRestoreMetadata(node *restic.Node, path string, warn func(msg string)) error {
err := nodeRestoreMetadata(node, path, warn)
if err != nil {
// It is common to have permission errors for folders like /home
// unless you're running as root, so ignore those.
if os.Geteuid() > 0 && errors.Is(err, os.ErrPermission) {
debug.Log("not running as root, ignoring permission error for %v: %v",
path, err)
return nil
}
debug.Log("restoreMetadata(%s) error %v", path, err)
}
return err
}
func nodeRestoreMetadata(node *restic.Node, path string, warn func(msg string)) error {
var firsterr error
if err := lchown(path, int(node.UID), int(node.GID)); err != nil {
firsterr = errors.WithStack(err)
}
if err := nodeRestoreExtendedAttributes(node, path); err != nil {
debug.Log("error restoring extended attributes for %v: %v", path, err)
if firsterr == nil {
firsterr = err
}
}
if err := nodeRestoreGenericAttributes(node, path, warn); err != nil {
debug.Log("error restoring generic attributes for %v: %v", path, err)
if firsterr == nil {
firsterr = err
}
}
if err := NodeRestoreTimestamps(node, path); err != nil {
debug.Log("error restoring timestamps for %v: %v", path, err)
if firsterr == nil {
firsterr = err
}
}
// Moving RestoreTimestamps and restoreExtendedAttributes calls above as for readonly files in windows
// calling Chmod below will no longer allow any modifications to be made on the file and the
// calls above would fail.
if node.Type != "symlink" {
if err := Chmod(path, node.Mode); err != nil {
if firsterr == nil {
firsterr = errors.WithStack(err)
}
}
}
return firsterr
}
func NodeRestoreTimestamps(node *restic.Node, path string) error {
var utimes = [...]syscall.Timespec{
syscall.NsecToTimespec(node.AccessTime.UnixNano()),
syscall.NsecToTimespec(node.ModTime.UnixNano()),
}
if node.Type == "symlink" {
return nodeRestoreSymlinkTimestamps(path, utimes)
}
if err := syscall.UtimesNano(path, utimes[:]); err != nil {
return errors.Wrap(err, "UtimesNano")
}
return nil
}

51
internal/fs/node_aix.go Normal file
View file

@ -0,0 +1,51 @@
//go:build aix
// +build aix
package fs
import (
"os"
"syscall"
"github.com/restic/restic/internal/restic"
)
func nodeRestoreSymlinkTimestamps(_ string, _ [2]syscall.Timespec) error {
return nil
}
// AIX has a funny timespec type in syscall, with 32-bit nanoseconds.
// golang.org/x/sys/unix handles this cleanly, but we're stuck with syscall
// because os.Stat returns a syscall type in its os.FileInfo.Sys().
func toTimespec(t syscall.StTimespec_t) syscall.Timespec {
return syscall.Timespec{Sec: t.Sec, Nsec: int64(t.Nsec)}
}
func (s statT) atim() syscall.Timespec { return toTimespec(s.Atim) }
func (s statT) mtim() syscall.Timespec { return toTimespec(s.Mtim) }
func (s statT) ctim() syscall.Timespec { return toTimespec(s.Ctim) }
// nodeRestoreExtendedAttributes is a no-op on AIX.
func nodeRestoreExtendedAttributes(_ *restic.Node, _ string) error {
return nil
}
// nodeFillExtendedAttributes is a no-op on AIX.
func nodeFillExtendedAttributes(_ *restic.Node, _ string, _ bool) error {
return nil
}
// isListxattrPermissionError is a no-op on AIX.
func isListxattrPermissionError(_ error) bool {
return false
}
// nodeRestoreGenericAttributes is no-op on AIX.
func nodeRestoreGenericAttributes(node *restic.Node, _ string, warn func(msg string)) error {
return restic.HandleAllUnknownGenericAttributesFound(node.GenericAttributes, warn)
}
// nodeFillGenericAttributes is a no-op on AIX.
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
return true, nil
}

View file

@ -1,8 +1,8 @@
package restic package fs
import "syscall" import "syscall"
func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error { func nodeRestoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
return nil return nil
} }

View file

@ -1,11 +1,11 @@
//go:build freebsd //go:build freebsd
// +build freebsd // +build freebsd
package restic package fs
import "syscall" import "syscall"
func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error { func nodeRestoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
return nil return nil
} }

View file

@ -1,4 +1,4 @@
package restic package fs
import ( import (
"path/filepath" "path/filepath"
@ -7,11 +7,10 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs"
) )
func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error { func nodeRestoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
dir, err := fs.Open(filepath.Dir(path)) dir, err := Open(filepath.Dir(path))
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }

View file

@ -0,0 +1,41 @@
package fs
import (
"os"
"syscall"
"github.com/restic/restic/internal/restic"
)
func nodeRestoreSymlinkTimestamps(_ string, _ [2]syscall.Timespec) error {
return nil
}
func (s statT) atim() syscall.Timespec { return s.Atimespec }
func (s statT) mtim() syscall.Timespec { return s.Mtimespec }
func (s statT) ctim() syscall.Timespec { return s.Ctimespec }
// nodeRestoreExtendedAttributes is a no-op on netbsd.
func nodeRestoreExtendedAttributes(_ *restic.Node, _ string) error {
return nil
}
// nodeFillExtendedAttributes is a no-op on netbsd.
func nodeFillExtendedAttributes(_ *restic.Node, _ string, _ bool) error {
return nil
}
// isListxattrPermissionError is a no-op on netbsd.
func isListxattrPermissionError(_ error) bool {
return false
}
// nodeRestoreGenericAttributes is no-op on netbsd.
func nodeRestoreGenericAttributes(node *restic.Node, _ string, warn func(msg string)) error {
return restic.HandleAllUnknownGenericAttributesFound(node.GenericAttributes, warn)
}
// nodeFillGenericAttributes is a no-op on netbsd.
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
return true, nil
}

View file

@ -0,0 +1,41 @@
package fs
import (
"os"
"syscall"
"github.com/restic/restic/internal/restic"
)
func nodeRestoreSymlinkTimestamps(_ string, _ [2]syscall.Timespec) error {
return nil
}
func (s statT) atim() syscall.Timespec { return s.Atim }
func (s statT) mtim() syscall.Timespec { return s.Mtim }
func (s statT) ctim() syscall.Timespec { return s.Ctim }
// nodeRestoreExtendedAttributes is a no-op on openbsd.
func nodeRestoreExtendedAttributes(_ *restic.Node, _ string) error {
return nil
}
// nodeFillExtendedAttributes is a no-op on openbsd.
func nodeFillExtendedAttributes(_ *restic.Node, _ string, _ bool) error {
return nil
}
// isListxattrPermissionError is a no-op on openbsd.
func isListxattrPermissionError(_ error) bool {
return false
}
// nodeRestoreGenericAttributes is no-op on openbsd.
func nodeRestoreGenericAttributes(node *restic.Node, _ string, warn func(msg string)) error {
return restic.HandleAllUnknownGenericAttributesFound(node.GenericAttributes, warn)
}
// fillGenericAttributes is a no-op on openbsd.
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
return true, nil
}

View file

@ -1,8 +1,8 @@
package restic package fs
import "syscall" import "syscall"
func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error { func nodeRestoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
return nil return nil
} }

324
internal/fs/node_test.go Normal file
View file

@ -0,0 +1,324 @@
package fs
import (
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/test"
rtest "github.com/restic/restic/internal/test"
)
func BenchmarkNodeFillUser(t *testing.B) {
tempfile, err := os.CreateTemp("", "restic-test-temp-")
if err != nil {
t.Fatal(err)
}
fi, err := tempfile.Stat()
if err != nil {
t.Fatal(err)
}
path := tempfile.Name()
t.ResetTimer()
for i := 0; i < t.N; i++ {
_, err := NodeFromFileInfo(path, fi, false)
rtest.OK(t, err)
}
rtest.OK(t, tempfile.Close())
rtest.RemoveAll(t, tempfile.Name())
}
func BenchmarkNodeFromFileInfo(t *testing.B) {
tempfile, err := os.CreateTemp("", "restic-test-temp-")
if err != nil {
t.Fatal(err)
}
fi, err := tempfile.Stat()
if err != nil {
t.Fatal(err)
}
path := tempfile.Name()
t.ResetTimer()
for i := 0; i < t.N; i++ {
_, err := NodeFromFileInfo(path, fi, false)
if err != nil {
t.Fatal(err)
}
}
rtest.OK(t, tempfile.Close())
rtest.RemoveAll(t, tempfile.Name())
}
func parseTime(s string) time.Time {
t, err := time.Parse("2006-01-02 15:04:05.999", s)
if err != nil {
panic(err)
}
return t.Local()
}
var nodeTests = []restic.Node{
{
Name: "testFile",
Type: "file",
Content: restic.IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0604,
ModTime: parseTime("2015-05-14 21:07:23.111"),
AccessTime: parseTime("2015-05-14 21:07:24.222"),
ChangeTime: parseTime("2015-05-14 21:07:25.333"),
},
{
Name: "testSuidFile",
Type: "file",
Content: restic.IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0755 | os.ModeSetuid,
ModTime: parseTime("2015-05-14 21:07:23.111"),
AccessTime: parseTime("2015-05-14 21:07:24.222"),
ChangeTime: parseTime("2015-05-14 21:07:25.333"),
},
{
Name: "testSuidFile2",
Type: "file",
Content: restic.IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0755 | os.ModeSetgid,
ModTime: parseTime("2015-05-14 21:07:23.111"),
AccessTime: parseTime("2015-05-14 21:07:24.222"),
ChangeTime: parseTime("2015-05-14 21:07:25.333"),
},
{
Name: "testSticky",
Type: "file",
Content: restic.IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0755 | os.ModeSticky,
ModTime: parseTime("2015-05-14 21:07:23.111"),
AccessTime: parseTime("2015-05-14 21:07:24.222"),
ChangeTime: parseTime("2015-05-14 21:07:25.333"),
},
{
Name: "testDir",
Type: "dir",
Subtree: nil,
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0750 | os.ModeDir,
ModTime: parseTime("2015-05-14 21:07:23.111"),
AccessTime: parseTime("2015-05-14 21:07:24.222"),
ChangeTime: parseTime("2015-05-14 21:07:25.333"),
},
{
Name: "testSymlink",
Type: "symlink",
LinkTarget: "invalid",
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0777 | os.ModeSymlink,
ModTime: parseTime("2015-05-14 21:07:23.111"),
AccessTime: parseTime("2015-05-14 21:07:24.222"),
ChangeTime: parseTime("2015-05-14 21:07:25.333"),
},
// include "testFile" and "testDir" again with slightly different
// metadata, so we can test if CreateAt works with pre-existing files.
{
Name: "testFile",
Type: "file",
Content: restic.IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0604,
ModTime: parseTime("2005-05-14 21:07:03.111"),
AccessTime: parseTime("2005-05-14 21:07:04.222"),
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
},
{
Name: "testDir",
Type: "dir",
Subtree: nil,
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0750 | os.ModeDir,
ModTime: parseTime("2005-05-14 21:07:03.111"),
AccessTime: parseTime("2005-05-14 21:07:04.222"),
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
},
{
Name: "testXattrFile",
Type: "file",
Content: restic.IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0604,
ModTime: parseTime("2005-05-14 21:07:03.111"),
AccessTime: parseTime("2005-05-14 21:07:04.222"),
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
ExtendedAttributes: []restic.ExtendedAttribute{
{Name: "user.foo", Value: []byte("bar")},
},
},
{
Name: "testXattrDir",
Type: "dir",
Subtree: nil,
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0750 | os.ModeDir,
ModTime: parseTime("2005-05-14 21:07:03.111"),
AccessTime: parseTime("2005-05-14 21:07:04.222"),
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
ExtendedAttributes: []restic.ExtendedAttribute{
{Name: "user.foo", Value: []byte("bar")},
},
},
{
Name: "testXattrFileMacOSResourceFork",
Type: "file",
Content: restic.IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0604,
ModTime: parseTime("2005-05-14 21:07:03.111"),
AccessTime: parseTime("2005-05-14 21:07:04.222"),
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
ExtendedAttributes: []restic.ExtendedAttribute{
{Name: "com.apple.ResourceFork", Value: []byte("bar")},
},
},
}
func TestNodeRestoreAt(t *testing.T) {
tempdir := t.TempDir()
for _, test := range nodeTests {
t.Run("", func(t *testing.T) {
var nodePath string
if test.ExtendedAttributes != nil {
if runtime.GOOS == "windows" {
// In windows extended attributes are case insensitive and windows returns
// the extended attributes in UPPER case.
// Update the tests to use UPPER case xattr names for windows.
extAttrArr := test.ExtendedAttributes
// Iterate through the array using pointers
for i := 0; i < len(extAttrArr); i++ {
extAttrArr[i].Name = strings.ToUpper(extAttrArr[i].Name)
}
}
for _, attr := range test.ExtendedAttributes {
if strings.HasPrefix(attr.Name, "com.apple.") && runtime.GOOS != "darwin" {
t.Skipf("attr %v only relevant on macOS", attr.Name)
}
}
// tempdir might be backed by a filesystem that does not support
// extended attributes
nodePath = test.Name
defer func() {
_ = os.Remove(nodePath)
}()
} else {
nodePath = filepath.Join(tempdir, test.Name)
}
rtest.OK(t, NodeCreateAt(&test, nodePath))
rtest.OK(t, NodeRestoreMetadata(&test, nodePath, func(msg string) { rtest.OK(t, fmt.Errorf("Warning triggered for path: %s: %s", nodePath, msg)) }))
fi, err := os.Lstat(nodePath)
rtest.OK(t, err)
n2, err := NodeFromFileInfo(nodePath, fi, false)
rtest.OK(t, err)
n3, err := NodeFromFileInfo(nodePath, fi, true)
rtest.OK(t, err)
rtest.Assert(t, n2.Equals(*n3), "unexpected node info mismatch %v", cmp.Diff(n2, n3))
rtest.Assert(t, test.Name == n2.Name,
"%v: name doesn't match (%v != %v)", test.Type, test.Name, n2.Name)
rtest.Assert(t, test.Type == n2.Type,
"%v: type doesn't match (%v != %v)", test.Type, test.Type, n2.Type)
rtest.Assert(t, test.Size == n2.Size,
"%v: size doesn't match (%v != %v)", test.Size, test.Size, n2.Size)
if runtime.GOOS != "windows" {
rtest.Assert(t, test.UID == n2.UID,
"%v: UID doesn't match (%v != %v)", test.Type, test.UID, n2.UID)
rtest.Assert(t, test.GID == n2.GID,
"%v: GID doesn't match (%v != %v)", test.Type, test.GID, n2.GID)
if test.Type != "symlink" {
// On OpenBSD only root can set sticky bit (see sticky(8)).
if runtime.GOOS != "openbsd" && runtime.GOOS != "netbsd" && runtime.GOOS != "solaris" && test.Name == "testSticky" {
rtest.Assert(t, test.Mode == n2.Mode,
"%v: mode doesn't match (0%o != 0%o)", test.Type, test.Mode, n2.Mode)
}
}
}
AssertFsTimeEqual(t, "AccessTime", test.Type, test.AccessTime, n2.AccessTime)
AssertFsTimeEqual(t, "ModTime", test.Type, test.ModTime, n2.ModTime)
if len(n2.ExtendedAttributes) == 0 {
n2.ExtendedAttributes = nil
}
rtest.Assert(t, reflect.DeepEqual(test.ExtendedAttributes, n2.ExtendedAttributes),
"%v: xattrs don't match (%v != %v)", test.Name, test.ExtendedAttributes, n2.ExtendedAttributes)
})
}
}
func AssertFsTimeEqual(t *testing.T, label string, nodeType string, t1 time.Time, t2 time.Time) {
var equal bool
// Go currently doesn't support setting timestamps of symbolic links on darwin and bsd
if nodeType == "symlink" {
switch runtime.GOOS {
case "darwin", "freebsd", "openbsd", "netbsd", "solaris":
return
}
}
switch runtime.GOOS {
case "darwin":
// HFS+ timestamps don't support sub-second precision,
// see https://en.wikipedia.org/wiki/Comparison_of_file_systems
diff := int(t1.Sub(t2).Seconds())
equal = diff == 0
default:
equal = t1.Equal(t2)
}
rtest.Assert(t, equal, "%s: %s doesn't match (%v != %v)", label, nodeType, t1, t2)
}
func TestNodeRestoreMetadataError(t *testing.T) {
tempdir := t.TempDir()
node := &nodeTests[0]
nodePath := filepath.Join(tempdir, node.Name)
// This will fail because the target file does not exist
err := NodeRestoreMetadata(node, nodePath, func(msg string) { rtest.OK(t, fmt.Errorf("Warning triggered for path: %s: %s", nodePath, msg)) })
test.Assert(t, errors.Is(err, os.ErrNotExist), "failed for an unexpected reason")
}

View file

@ -1,7 +1,7 @@
//go:build !windows //go:build !windows
// +build !windows // +build !windows
package restic package fs
import ( import (
"os" "os"

View file

@ -1,7 +1,7 @@
//go:build !windows //go:build !windows
// +build !windows // +build !windows
package restic package fs
import ( import (
"os" "os"
@ -11,6 +11,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
) )
@ -27,7 +28,7 @@ func stat(t testing.TB, filename string) (fi os.FileInfo, ok bool) {
return fi, true return fi, true
} }
func checkFile(t testing.TB, stat *syscall.Stat_t, node *Node) { func checkFile(t testing.TB, stat *syscall.Stat_t, node *restic.Node) {
t.Helper() t.Helper()
if uint32(node.Mode.Perm()) != uint32(stat.Mode&0777) { if uint32(node.Mode.Perm()) != uint32(stat.Mode&0777) {
t.Errorf("Mode does not match, want %v, got %v", stat.Mode&0777, node.Mode) t.Errorf("Mode does not match, want %v, got %v", stat.Mode&0777, node.Mode)
@ -80,7 +81,7 @@ func checkFile(t testing.TB, stat *syscall.Stat_t, node *Node) {
} }
func checkDevice(t testing.TB, stat *syscall.Stat_t, node *Node) { func checkDevice(t testing.TB, stat *syscall.Stat_t, node *restic.Node) {
if node.Device != uint64(stat.Rdev) { if node.Device != uint64(stat.Rdev) {
t.Errorf("Rdev does not match, want %v, got %v", stat.Rdev, node.Device) t.Errorf("Rdev does not match, want %v, got %v", stat.Rdev, node.Device)
} }

View file

@ -1,4 +1,4 @@
package restic package fs
import ( import (
"encoding/json" "encoding/json"
@ -14,7 +14,7 @@ import (
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/restic"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
@ -56,7 +56,7 @@ func lchown(_ string, _ int, _ int) (err error) {
} }
// restoreSymlinkTimestamps restores timestamps for symlinks // restoreSymlinkTimestamps restores timestamps for symlinks
func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error { func nodeRestoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
// tweaked version of UtimesNano from go/src/syscall/syscall_windows.go // tweaked version of UtimesNano from go/src/syscall/syscall_windows.go
pathp, e := syscall.UTF16PtrFromString(path) pathp, e := syscall.UTF16PtrFromString(path)
if e != nil { if e != nil {
@ -82,12 +82,12 @@ func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespe
} }
// restore extended attributes for windows // restore extended attributes for windows
func (node Node) restoreExtendedAttributes(path string) (err error) { func nodeRestoreExtendedAttributes(node *restic.Node, path string) (err error) {
count := len(node.ExtendedAttributes) count := len(node.ExtendedAttributes)
if count > 0 { if count > 0 {
eas := make([]fs.ExtendedAttribute, count) eas := make([]ExtendedAttribute, count)
for i, attr := range node.ExtendedAttributes { for i, attr := range node.ExtendedAttributes {
eas[i] = fs.ExtendedAttribute{Name: attr.Name, Value: attr.Value} eas[i] = ExtendedAttribute{Name: attr.Name, Value: attr.Value}
} }
if errExt := restoreExtendedAttributes(node.Type, path, eas); errExt != nil { if errExt := restoreExtendedAttributes(node.Type, path, eas); errExt != nil {
return errExt return errExt
@ -97,9 +97,9 @@ func (node Node) restoreExtendedAttributes(path string) (err error) {
} }
// fill extended attributes in the node. This also includes the Generic attributes for windows. // fill extended attributes in the node. This also includes the Generic attributes for windows.
func (node *Node) fillExtendedAttributes(path string, _ bool) (err error) { func nodeFillExtendedAttributes(node *restic.Node, path string, _ bool) (err error) {
var fileHandle windows.Handle var fileHandle windows.Handle
if fileHandle, err = fs.OpenHandleForEA(node.Type, path, false); fileHandle == 0 { if fileHandle, err = OpenHandleForEA(node.Type, path, false); fileHandle == 0 {
return nil return nil
} }
if err != nil { if err != nil {
@ -107,8 +107,8 @@ func (node *Node) fillExtendedAttributes(path string, _ bool) (err error) {
} }
defer closeFileHandle(fileHandle, path) // Replaced inline defer with named function call defer closeFileHandle(fileHandle, path) // Replaced inline defer with named function call
//Get the windows Extended Attributes using the file handle //Get the windows Extended Attributes using the file handle
var extAtts []fs.ExtendedAttribute var extAtts []ExtendedAttribute
extAtts, err = fs.GetFileEA(fileHandle) extAtts, err = GetFileEA(fileHandle)
debug.Log("fillExtendedAttributes(%v) %v", path, extAtts) debug.Log("fillExtendedAttributes(%v) %v", path, extAtts)
if err != nil { if err != nil {
return errors.Errorf("get EA failed for path %v, with: %v", path, err) return errors.Errorf("get EA failed for path %v, with: %v", path, err)
@ -119,7 +119,7 @@ func (node *Node) fillExtendedAttributes(path string, _ bool) (err error) {
//Fill the ExtendedAttributes in the node using the name/value pairs in the windows EA //Fill the ExtendedAttributes in the node using the name/value pairs in the windows EA
for _, attr := range extAtts { for _, attr := range extAtts {
extendedAttr := ExtendedAttribute{ extendedAttr := restic.ExtendedAttribute{
Name: attr.Name, Name: attr.Name,
Value: attr.Value, Value: attr.Value,
} }
@ -139,9 +139,9 @@ func closeFileHandle(fileHandle windows.Handle, path string) {
// restoreExtendedAttributes handles restore of the Windows Extended Attributes to the specified path. // restoreExtendedAttributes handles restore of the Windows Extended Attributes to the specified path.
// The Windows API requires setting of all the Extended Attributes in one call. // The Windows API requires setting of all the Extended Attributes in one call.
func restoreExtendedAttributes(nodeType, path string, eas []fs.ExtendedAttribute) (err error) { func restoreExtendedAttributes(nodeType, path string, eas []ExtendedAttribute) (err error) {
var fileHandle windows.Handle var fileHandle windows.Handle
if fileHandle, err = fs.OpenHandleForEA(nodeType, path, true); fileHandle == 0 { if fileHandle, err = OpenHandleForEA(nodeType, path, true); fileHandle == 0 {
return nil return nil
} }
if err != nil { if err != nil {
@ -150,7 +150,7 @@ func restoreExtendedAttributes(nodeType, path string, eas []fs.ExtendedAttribute
defer closeFileHandle(fileHandle, path) // Replaced inline defer with named function call defer closeFileHandle(fileHandle, path) // Replaced inline defer with named function call
// clear old unexpected xattrs by setting them to an empty value // clear old unexpected xattrs by setting them to an empty value
oldEAs, err := fs.GetFileEA(fileHandle) oldEAs, err := GetFileEA(fileHandle)
if err != nil { if err != nil {
return err return err
} }
@ -165,11 +165,11 @@ func restoreExtendedAttributes(nodeType, path string, eas []fs.ExtendedAttribute
} }
if !found { if !found {
eas = append(eas, fs.ExtendedAttribute{Name: oldEA.Name, Value: nil}) eas = append(eas, ExtendedAttribute{Name: oldEA.Name, Value: nil})
} }
} }
if err = fs.SetFileEA(fileHandle, eas); err != nil { if err = SetFileEA(fileHandle, eas); err != nil {
return errors.Errorf("set EA failed for path %v, with: %v", path, err) return errors.Errorf("set EA failed for path %v, with: %v", path, err)
} }
return nil return nil
@ -210,7 +210,7 @@ func (s statT) ctim() syscall.Timespec {
} }
// restoreGenericAttributes restores generic attributes for Windows // restoreGenericAttributes restores generic attributes for Windows
func (node Node) restoreGenericAttributes(path string, warn func(msg string)) (err error) { func nodeRestoreGenericAttributes(node *restic.Node, path string, warn func(msg string)) (err error) {
if len(node.GenericAttributes) == 0 { if len(node.GenericAttributes) == 0 {
return nil return nil
} }
@ -230,19 +230,19 @@ func (node Node) restoreGenericAttributes(path string, warn func(msg string)) (e
} }
} }
if windowsAttributes.SecurityDescriptor != nil { if windowsAttributes.SecurityDescriptor != nil {
if err := fs.SetSecurityDescriptor(path, windowsAttributes.SecurityDescriptor); err != nil { if err := SetSecurityDescriptor(path, windowsAttributes.SecurityDescriptor); err != nil {
errs = append(errs, fmt.Errorf("error restoring security descriptor for: %s : %v", path, err)) errs = append(errs, fmt.Errorf("error restoring security descriptor for: %s : %v", path, err))
} }
} }
HandleUnknownGenericAttributesFound(unknownAttribs, warn) restic.HandleUnknownGenericAttributesFound(unknownAttribs, warn)
return errors.CombineErrors(errs...) return errors.CombineErrors(errs...)
} }
// genericAttributesToWindowsAttrs converts the generic attributes map to a WindowsAttributes and also returns a string of unknown attributes that it could not convert. // genericAttributesToWindowsAttrs converts the generic attributes map to a WindowsAttributes and also returns a string of unknown attributes that it could not convert.
func genericAttributesToWindowsAttrs(attrs map[GenericAttributeType]json.RawMessage) (windowsAttributes WindowsAttributes, unknownAttribs []GenericAttributeType, err error) { func genericAttributesToWindowsAttrs(attrs map[restic.GenericAttributeType]json.RawMessage) (windowsAttributes WindowsAttributes, unknownAttribs []restic.GenericAttributeType, err error) {
waValue := reflect.ValueOf(&windowsAttributes).Elem() waValue := reflect.ValueOf(&windowsAttributes).Elem()
unknownAttribs, err = genericAttributesToOSAttrs(attrs, reflect.TypeOf(windowsAttributes), &waValue, "windows") unknownAttribs, err = restic.GenericAttributesToOSAttrs(attrs, reflect.TypeOf(windowsAttributes), &waValue, "windows")
return windowsAttributes, unknownAttribs, err return windowsAttributes, unknownAttribs, err
} }
@ -289,14 +289,14 @@ func fixEncryptionAttribute(path string, attrs *uint32, pathPointer *uint16) (er
// File should be encrypted. // File should be encrypted.
err = encryptFile(pathPointer) err = encryptFile(pathPointer)
if err != nil { if err != nil {
if fs.IsAccessDenied(err) || errors.Is(err, windows.ERROR_FILE_READ_ONLY) { if IsAccessDenied(err) || errors.Is(err, windows.ERROR_FILE_READ_ONLY) {
// If existing file already has readonly or system flag, encrypt file call fails. // If existing file already has readonly or system flag, encrypt file call fails.
// The readonly and system flags will be set again at the end of this func if they are needed. // The readonly and system flags will be set again at the end of this func if they are needed.
err = fs.ResetPermissions(path) err = ResetPermissions(path)
if err != nil { if err != nil {
return fmt.Errorf("failed to encrypt file: failed to reset permissions: %s : %v", path, err) return fmt.Errorf("failed to encrypt file: failed to reset permissions: %s : %v", path, err)
} }
err = fs.ClearSystem(path) err = ClearSystem(path)
if err != nil { if err != nil {
return fmt.Errorf("failed to encrypt file: failed to clear system flag: %s : %v", path, err) return fmt.Errorf("failed to encrypt file: failed to clear system flag: %s : %v", path, err)
} }
@ -317,14 +317,14 @@ func fixEncryptionAttribute(path string, attrs *uint32, pathPointer *uint16) (er
// File should not be encrypted, but its already encrypted. Decrypt it. // File should not be encrypted, but its already encrypted. Decrypt it.
err = decryptFile(pathPointer) err = decryptFile(pathPointer)
if err != nil { if err != nil {
if fs.IsAccessDenied(err) || errors.Is(err, windows.ERROR_FILE_READ_ONLY) { if IsAccessDenied(err) || errors.Is(err, windows.ERROR_FILE_READ_ONLY) {
// If existing file already has readonly or system flag, decrypt file call fails. // If existing file already has readonly or system flag, decrypt file call fails.
// The readonly and system flags will be set again after this func if they are needed. // The readonly and system flags will be set again after this func if they are needed.
err = fs.ResetPermissions(path) err = ResetPermissions(path)
if err != nil { if err != nil {
return fmt.Errorf("failed to encrypt file: failed to reset permissions: %s : %v", path, err) return fmt.Errorf("failed to encrypt file: failed to reset permissions: %s : %v", path, err)
} }
err = fs.ClearSystem(path) err = ClearSystem(path)
if err != nil { if err != nil {
return fmt.Errorf("failed to decrypt file: failed to clear system flag: %s : %v", path, err) return fmt.Errorf("failed to decrypt file: failed to clear system flag: %s : %v", path, err)
} }
@ -361,11 +361,11 @@ func decryptFile(pathPointer *uint16) error {
return nil return nil
} }
// fillGenericAttributes fills in the generic attributes for windows like File Attributes, // nodeFillGenericAttributes fills in the generic attributes for windows like File Attributes,
// Created time and Security Descriptors. // Created time and Security Descriptors.
// It also checks if the volume supports extended attributes and stores the result in a map // It also checks if the volume supports extended attributes and stores the result in a map
// so that it does not have to be checked again for subsequent calls for paths in the same volume. // so that it does not have to be checked again for subsequent calls for paths in the same volume.
func (node *Node) fillGenericAttributes(path string, fi os.FileInfo, stat *statT) (allowExtended bool, err error) { func nodeFillGenericAttributes(node *restic.Node, path string, fi os.FileInfo, stat *statT) (allowExtended bool, err error) {
if strings.Contains(filepath.Base(path), ":") { if strings.Contains(filepath.Base(path), ":") {
// Do not process for Alternate Data Streams in Windows // Do not process for Alternate Data Streams in Windows
// Also do not allow processing of extended attributes for ADS. // Also do not allow processing of extended attributes for ADS.
@ -392,7 +392,7 @@ func (node *Node) fillGenericAttributes(path string, fi os.FileInfo, stat *statT
if err != nil { if err != nil {
return false, err return false, err
} }
if sd, err = fs.GetSecurityDescriptor(path); err != nil { if sd, err = GetSecurityDescriptor(path); err != nil {
return allowExtended, err return allowExtended, err
} }
} }
@ -422,7 +422,7 @@ func checkAndStoreEASupport(path string) (isEASupportedVolume bool, err error) {
return eaSupportedValue.(bool), nil return eaSupportedValue.(bool), nil
} }
// If not found, check if EA is supported with manually prepared volume name // If not found, check if EA is supported with manually prepared volume name
isEASupportedVolume, err = fs.PathSupportsExtendedAttributes(volumeName + `\`) isEASupportedVolume, err = PathSupportsExtendedAttributes(volumeName + `\`)
// If the prepared volume name is not valid, we will fetch the actual volume name next. // If the prepared volume name is not valid, we will fetch the actual volume name next.
if err != nil && !errors.Is(err, windows.DNS_ERROR_INVALID_NAME) { if err != nil && !errors.Is(err, windows.DNS_ERROR_INVALID_NAME) {
debug.Log("Error checking if extended attributes are supported for prepared volume name %s: %v", volumeName, err) debug.Log("Error checking if extended attributes are supported for prepared volume name %s: %v", volumeName, err)
@ -432,7 +432,7 @@ func checkAndStoreEASupport(path string) (isEASupportedVolume bool, err error) {
} }
} }
// If an entry is not found, get the actual volume name using the GetVolumePathName function // If an entry is not found, get the actual volume name using the GetVolumePathName function
volumeNameActual, err := fs.GetVolumePathName(path) volumeNameActual, err := GetVolumePathName(path)
if err != nil { if err != nil {
debug.Log("Error getting actual volume name %s for path %s: %v", volumeName, path, err) debug.Log("Error getting actual volume name %s for path %s: %v", volumeName, path, err)
// There can be multiple errors like path does not exist, bad network path, etc. // There can be multiple errors like path does not exist, bad network path, etc.
@ -447,7 +447,7 @@ func checkAndStoreEASupport(path string) (isEASupportedVolume bool, err error) {
return eaSupportedValue.(bool), nil return eaSupportedValue.(bool), nil
} }
// If the actual volume name is different and is not in the map, again check if the new volume supports extended attributes with the actual volume name // If the actual volume name is different and is not in the map, again check if the new volume supports extended attributes with the actual volume name
isEASupportedVolume, err = fs.PathSupportsExtendedAttributes(volumeNameActual + `\`) isEASupportedVolume, err = PathSupportsExtendedAttributes(volumeNameActual + `\`)
// Debug log for cases where the prepared volume name is not valid // Debug log for cases where the prepared volume name is not valid
if err != nil { if err != nil {
debug.Log("Error checking if extended attributes are supported for actual volume name %s: %v", volumeNameActual, err) debug.Log("Error checking if extended attributes are supported for actual volume name %s: %v", volumeNameActual, err)
@ -496,10 +496,10 @@ func prepareVolumeName(path string) (volumeName string, err error) {
} }
// windowsAttrsToGenericAttributes converts the WindowsAttributes to a generic attributes map using reflection // windowsAttrsToGenericAttributes converts the WindowsAttributes to a generic attributes map using reflection
func WindowsAttrsToGenericAttributes(windowsAttributes WindowsAttributes) (attrs map[GenericAttributeType]json.RawMessage, err error) { func WindowsAttrsToGenericAttributes(windowsAttributes WindowsAttributes) (attrs map[restic.GenericAttributeType]json.RawMessage, err error) {
// Get the value of the WindowsAttributes // Get the value of the WindowsAttributes
windowsAttributesValue := reflect.ValueOf(windowsAttributes) windowsAttributesValue := reflect.ValueOf(windowsAttributes)
return osAttrsToGenericAttributes(reflect.TypeOf(windowsAttributes), &windowsAttributesValue, runtime.GOOS) return restic.OSAttrsToGenericAttributes(reflect.TypeOf(windowsAttributes), &windowsAttributesValue, runtime.GOOS)
} }
// getCreationTime gets the value for the WindowsAttribute CreationTime in a windows specific time format. // getCreationTime gets the value for the WindowsAttribute CreationTime in a windows specific time format.

View file

@ -1,7 +1,7 @@
//go:build windows //go:build windows
// +build windows // +build windows
package restic package fs
import ( import (
"encoding/base64" "encoding/base64"
@ -15,7 +15,7 @@ import (
"time" "time"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/test" "github.com/restic/restic/internal/test"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
@ -23,10 +23,10 @@ import (
func TestRestoreSecurityDescriptors(t *testing.T) { func TestRestoreSecurityDescriptors(t *testing.T) {
t.Parallel() t.Parallel()
tempDir := t.TempDir() tempDir := t.TempDir()
for i, sd := range fs.TestFileSDs { for i, sd := range TestFileSDs {
testRestoreSecurityDescriptor(t, sd, tempDir, "file", fmt.Sprintf("testfile%d", i)) testRestoreSecurityDescriptor(t, sd, tempDir, "file", fmt.Sprintf("testfile%d", i))
} }
for i, sd := range fs.TestDirSDs { for i, sd := range TestDirSDs {
testRestoreSecurityDescriptor(t, sd, tempDir, "dir", fmt.Sprintf("testdir%d", i)) testRestoreSecurityDescriptor(t, sd, tempDir, "dir", fmt.Sprintf("testdir%d", i))
} }
} }
@ -42,22 +42,22 @@ func testRestoreSecurityDescriptor(t *testing.T, sd string, tempDir, fileType, f
expectedNode := getNode(fileName, fileType, genericAttributes) expectedNode := getNode(fileName, fileType, genericAttributes)
// Restore the file/dir and restore the meta data including the security descriptors. // Restore the file/dir and restore the meta data including the security descriptors.
testPath, node := restoreAndGetNode(t, tempDir, expectedNode, false) testPath, node := restoreAndGetNode(t, tempDir, &expectedNode, false)
// Get the security descriptor from the node constructed from the file info of the restored path. // Get the security descriptor from the node constructed from the file info of the restored path.
sdByteFromRestoredNode := getWindowsAttr(t, testPath, node).SecurityDescriptor sdByteFromRestoredNode := getWindowsAttr(t, testPath, node).SecurityDescriptor
// Get the security descriptor for the test path after the restore. // Get the security descriptor for the test path after the restore.
sdBytesFromRestoredPath, err := fs.GetSecurityDescriptor(testPath) sdBytesFromRestoredPath, err := GetSecurityDescriptor(testPath)
test.OK(t, errors.Wrapf(err, "Error while getting the security descriptor for: %s", testPath)) test.OK(t, errors.Wrapf(err, "Error while getting the security descriptor for: %s", testPath))
// Compare the input SD and the SD got from the restored file. // Compare the input SD and the SD got from the restored file.
fs.CompareSecurityDescriptors(t, testPath, sdInputBytes, *sdBytesFromRestoredPath) CompareSecurityDescriptors(t, testPath, sdInputBytes, *sdBytesFromRestoredPath)
// Compare the SD got from node constructed from the restored file info and the SD got directly from the restored file. // Compare the SD got from node constructed from the restored file info and the SD got directly from the restored file.
fs.CompareSecurityDescriptors(t, testPath, *sdByteFromRestoredNode, *sdBytesFromRestoredPath) CompareSecurityDescriptors(t, testPath, *sdByteFromRestoredNode, *sdBytesFromRestoredPath)
} }
func getNode(name string, fileType string, genericAttributes map[GenericAttributeType]json.RawMessage) Node { func getNode(name string, fileType string, genericAttributes map[restic.GenericAttributeType]json.RawMessage) restic.Node {
return Node{ return restic.Node{
Name: name, Name: name,
Type: fileType, Type: fileType,
Mode: 0644, Mode: 0644,
@ -68,7 +68,7 @@ func getNode(name string, fileType string, genericAttributes map[GenericAttribut
} }
} }
func getWindowsAttr(t *testing.T, testPath string, node *Node) WindowsAttributes { func getWindowsAttr(t *testing.T, testPath string, node *restic.Node) WindowsAttributes {
windowsAttributes, unknownAttribs, err := genericAttributesToWindowsAttrs(node.GenericAttributes) windowsAttributes, unknownAttribs, err := genericAttributesToWindowsAttrs(node.GenericAttributes)
test.OK(t, errors.Wrapf(err, "Error getting windows attr from generic attr: %s", testPath)) test.OK(t, errors.Wrapf(err, "Error getting windows attr from generic attr: %s", testPath))
test.Assert(t, len(unknownAttribs) == 0, "Unknown attribs found: %s for: %s", unknownAttribs, testPath) test.Assert(t, len(unknownAttribs) == 0, "Unknown attribs found: %s for: %s", unknownAttribs, testPath)
@ -83,12 +83,12 @@ func TestRestoreCreationTime(t *testing.T) {
creationTimeAttribute := getCreationTime(fi, path) creationTimeAttribute := getCreationTime(fi, path)
test.OK(t, errors.Wrapf(err, "Could not get creation time for path: %s", path)) test.OK(t, errors.Wrapf(err, "Could not get creation time for path: %s", path))
//Using the temp dir creation time as the test creation time for the test file and folder //Using the temp dir creation time as the test creation time for the test file and folder
runGenericAttributesTest(t, path, TypeCreationTime, WindowsAttributes{CreationTime: creationTimeAttribute}, false) runGenericAttributesTest(t, path, restic.TypeCreationTime, WindowsAttributes{CreationTime: creationTimeAttribute}, false)
} }
func TestRestoreFileAttributes(t *testing.T) { func TestRestoreFileAttributes(t *testing.T) {
t.Parallel() t.Parallel()
genericAttributeName := TypeFileAttributes genericAttributeName := restic.TypeFileAttributes
tempDir := t.TempDir() tempDir := t.TempDir()
normal := uint32(syscall.FILE_ATTRIBUTE_NORMAL) normal := uint32(syscall.FILE_ATTRIBUTE_NORMAL)
hidden := uint32(syscall.FILE_ATTRIBUTE_HIDDEN) hidden := uint32(syscall.FILE_ATTRIBUTE_HIDDEN)
@ -110,7 +110,7 @@ func TestRestoreFileAttributes(t *testing.T) {
for i, fileAttr := range fileAttributes { for i, fileAttr := range fileAttributes {
genericAttrs, err := WindowsAttrsToGenericAttributes(fileAttr) genericAttrs, err := WindowsAttrsToGenericAttributes(fileAttr)
test.OK(t, err) test.OK(t, err)
expectedNodes := []Node{ expectedNodes := []restic.Node{
{ {
Name: fmt.Sprintf("testfile%d", i), Name: fmt.Sprintf("testfile%d", i),
Type: "file", Type: "file",
@ -143,7 +143,7 @@ func TestRestoreFileAttributes(t *testing.T) {
for i, folderAttr := range folderAttributes { for i, folderAttr := range folderAttributes {
genericAttrs, err := WindowsAttrsToGenericAttributes(folderAttr) genericAttrs, err := WindowsAttrsToGenericAttributes(folderAttr)
test.OK(t, err) test.OK(t, err)
expectedNodes := []Node{ expectedNodes := []restic.Node{
{ {
Name: fmt.Sprintf("testdirectory%d", i), Name: fmt.Sprintf("testdirectory%d", i),
Type: "dir", Type: "dir",
@ -158,10 +158,10 @@ func TestRestoreFileAttributes(t *testing.T) {
} }
} }
func runGenericAttributesTest(t *testing.T, tempDir string, genericAttributeName GenericAttributeType, genericAttributeExpected WindowsAttributes, warningExpected bool) { func runGenericAttributesTest(t *testing.T, tempDir string, genericAttributeName restic.GenericAttributeType, genericAttributeExpected WindowsAttributes, warningExpected bool) {
genericAttributes, err := WindowsAttrsToGenericAttributes(genericAttributeExpected) genericAttributes, err := WindowsAttrsToGenericAttributes(genericAttributeExpected)
test.OK(t, err) test.OK(t, err)
expectedNodes := []Node{ expectedNodes := []restic.Node{
{ {
Name: "testfile", Name: "testfile",
Type: "file", Type: "file",
@ -183,10 +183,10 @@ func runGenericAttributesTest(t *testing.T, tempDir string, genericAttributeName
} }
runGenericAttributesTestForNodes(t, expectedNodes, tempDir, genericAttributeName, genericAttributeExpected, warningExpected) runGenericAttributesTestForNodes(t, expectedNodes, tempDir, genericAttributeName, genericAttributeExpected, warningExpected)
} }
func runGenericAttributesTestForNodes(t *testing.T, expectedNodes []Node, tempDir string, genericAttr GenericAttributeType, genericAttributeExpected WindowsAttributes, warningExpected bool) { func runGenericAttributesTestForNodes(t *testing.T, expectedNodes []restic.Node, tempDir string, genericAttr restic.GenericAttributeType, genericAttributeExpected WindowsAttributes, warningExpected bool) {
for _, testNode := range expectedNodes { for _, testNode := range expectedNodes {
testPath, node := restoreAndGetNode(t, tempDir, testNode, warningExpected) testPath, node := restoreAndGetNode(t, tempDir, &testNode, warningExpected)
rawMessage := node.GenericAttributes[genericAttr] rawMessage := node.GenericAttributes[genericAttr]
genericAttrsExpected, err := WindowsAttrsToGenericAttributes(genericAttributeExpected) genericAttrsExpected, err := WindowsAttrsToGenericAttributes(genericAttributeExpected)
test.OK(t, err) test.OK(t, err)
@ -195,7 +195,7 @@ func runGenericAttributesTestForNodes(t *testing.T, expectedNodes []Node, tempDi
} }
} }
func restoreAndGetNode(t *testing.T, tempDir string, testNode Node, warningExpected bool) (string, *Node) { func restoreAndGetNode(t *testing.T, tempDir string, testNode *restic.Node, warningExpected bool) (string, *restic.Node) {
testPath := filepath.Join(tempDir, "001", testNode.Name) testPath := filepath.Join(tempDir, "001", testNode.Name)
err := os.MkdirAll(filepath.Dir(testPath), testNode.Mode) err := os.MkdirAll(filepath.Dir(testPath), testNode.Mode)
test.OK(t, errors.Wrapf(err, "Failed to create parent directories for: %s", testPath)) test.OK(t, errors.Wrapf(err, "Failed to create parent directories for: %s", testPath))
@ -211,7 +211,7 @@ func restoreAndGetNode(t *testing.T, tempDir string, testNode Node, warningExpec
test.OK(t, errors.Wrapf(err, "Failed to create test directory: %s", testPath)) test.OK(t, errors.Wrapf(err, "Failed to create test directory: %s", testPath))
} }
err = testNode.RestoreMetadata(testPath, func(msg string) { err = NodeRestoreMetadata(testNode, testPath, func(msg string) {
if warningExpected { if warningExpected {
test.Assert(t, warningExpected, "Warning triggered as expected: %s", msg) test.Assert(t, warningExpected, "Warning triggered as expected: %s", msg)
} else { } else {
@ -230,16 +230,16 @@ func restoreAndGetNode(t *testing.T, tempDir string, testNode Node, warningExpec
return testPath, nodeFromFileInfo return testPath, nodeFromFileInfo
} }
const TypeSomeNewAttribute GenericAttributeType = "MockAttributes.SomeNewAttribute" const TypeSomeNewAttribute restic.GenericAttributeType = "MockAttributes.SomeNewAttribute"
func TestNewGenericAttributeType(t *testing.T) { func TestNewGenericAttributeType(t *testing.T) {
t.Parallel() t.Parallel()
newGenericAttribute := map[GenericAttributeType]json.RawMessage{} newGenericAttribute := map[restic.GenericAttributeType]json.RawMessage{}
newGenericAttribute[TypeSomeNewAttribute] = []byte("any value") newGenericAttribute[TypeSomeNewAttribute] = []byte("any value")
tempDir := t.TempDir() tempDir := t.TempDir()
expectedNodes := []Node{ expectedNodes := []restic.Node{
{ {
Name: "testfile", Name: "testfile",
Type: "file", Type: "file",
@ -260,7 +260,7 @@ func TestNewGenericAttributeType(t *testing.T) {
}, },
} }
for _, testNode := range expectedNodes { for _, testNode := range expectedNodes {
testPath, node := restoreAndGetNode(t, tempDir, testNode, true) testPath, node := restoreAndGetNode(t, tempDir, &testNode, true)
_, ua, err := genericAttributesToWindowsAttrs(node.GenericAttributes) _, ua, err := genericAttributesToWindowsAttrs(node.GenericAttributes)
test.OK(t, err) test.OK(t, err)
// Since this GenericAttribute is unknown to this version of the software, it will not get set on the file. // Since this GenericAttribute is unknown to this version of the software, it will not get set on the file.
@ -271,7 +271,7 @@ func TestNewGenericAttributeType(t *testing.T) {
func TestRestoreExtendedAttributes(t *testing.T) { func TestRestoreExtendedAttributes(t *testing.T) {
t.Parallel() t.Parallel()
tempDir := t.TempDir() tempDir := t.TempDir()
expectedNodes := []Node{ expectedNodes := []restic.Node{
{ {
Name: "testfile", Name: "testfile",
Type: "file", Type: "file",
@ -279,7 +279,7 @@ func TestRestoreExtendedAttributes(t *testing.T) {
ModTime: parseTime("2005-05-14 21:07:03.111"), ModTime: parseTime("2005-05-14 21:07:03.111"),
AccessTime: parseTime("2005-05-14 21:07:04.222"), AccessTime: parseTime("2005-05-14 21:07:04.222"),
ChangeTime: parseTime("2005-05-14 21:07:05.333"), ChangeTime: parseTime("2005-05-14 21:07:05.333"),
ExtendedAttributes: []ExtendedAttribute{ ExtendedAttributes: []restic.ExtendedAttribute{
{"user.foo", []byte("bar")}, {"user.foo", []byte("bar")},
}, },
}, },
@ -290,13 +290,13 @@ func TestRestoreExtendedAttributes(t *testing.T) {
ModTime: parseTime("2005-05-14 21:07:03.111"), ModTime: parseTime("2005-05-14 21:07:03.111"),
AccessTime: parseTime("2005-05-14 21:07:04.222"), AccessTime: parseTime("2005-05-14 21:07:04.222"),
ChangeTime: parseTime("2005-05-14 21:07:05.333"), ChangeTime: parseTime("2005-05-14 21:07:05.333"),
ExtendedAttributes: []ExtendedAttribute{ ExtendedAttributes: []restic.ExtendedAttribute{
{"user.foo", []byte("bar")}, {"user.foo", []byte("bar")},
}, },
}, },
} }
for _, testNode := range expectedNodes { for _, testNode := range expectedNodes {
testPath, node := restoreAndGetNode(t, tempDir, testNode, false) testPath, node := restoreAndGetNode(t, tempDir, &testNode, false)
var handle windows.Handle var handle windows.Handle
var err error var err error
@ -312,12 +312,12 @@ func TestRestoreExtendedAttributes(t *testing.T) {
test.OK(t, errors.Wrapf(err, "Error closing file for: %s", testPath)) test.OK(t, errors.Wrapf(err, "Error closing file for: %s", testPath))
}() }()
extAttr, err := fs.GetFileEA(handle) extAttr, err := GetFileEA(handle)
test.OK(t, errors.Wrapf(err, "Error getting extended attributes for: %s", testPath)) test.OK(t, errors.Wrapf(err, "Error getting extended attributes for: %s", testPath))
test.Equals(t, len(node.ExtendedAttributes), len(extAttr)) test.Equals(t, len(node.ExtendedAttributes), len(extAttr))
for _, expectedExtAttr := range node.ExtendedAttributes { for _, expectedExtAttr := range node.ExtendedAttributes {
var foundExtAttr *fs.ExtendedAttribute var foundExtAttr *ExtendedAttribute
for _, ea := range extAttr { for _, ea := range extAttr {
if strings.EqualFold(ea.Name, expectedExtAttr.Name) { if strings.EqualFold(ea.Name, expectedExtAttr.Name) {
foundExtAttr = &ea foundExtAttr = &ea
@ -491,13 +491,13 @@ func TestPrepareVolumeName(t *testing.T) {
test.Equals(t, tc.expectedVolume, volume) test.Equals(t, tc.expectedVolume, volume)
if tc.isRealPath { if tc.isRealPath {
isEASupportedVolume, err := fs.PathSupportsExtendedAttributes(volume + `\`) isEASupportedVolume, err := PathSupportsExtendedAttributes(volume + `\`)
// If the prepared volume name is not valid, we will next fetch the actual volume name. // If the prepared volume name is not valid, we will next fetch the actual volume name.
test.OK(t, err) test.OK(t, err)
test.Equals(t, tc.expectedEASupported, isEASupportedVolume) test.Equals(t, tc.expectedEASupported, isEASupportedVolume)
actualVolume, err := fs.GetVolumePathName(tc.path) actualVolume, err := GetVolumePathName(tc.path)
test.OK(t, err) test.OK(t, err)
test.Equals(t, tc.expectedVolume, actualVolume) test.Equals(t, tc.expectedVolume, actualVolume)
} }

View file

@ -1,7 +1,7 @@
//go:build darwin || freebsd || linux || solaris //go:build darwin || freebsd || linux || solaris
// +build darwin freebsd linux solaris // +build darwin freebsd linux solaris
package restic package fs
import ( import (
"fmt" "fmt"
@ -10,6 +10,7 @@ import (
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic"
"github.com/pkg/xattr" "github.com/pkg/xattr"
) )
@ -27,7 +28,7 @@ func listxattr(path string) ([]string, error) {
return l, handleXattrErr(err) return l, handleXattrErr(err)
} }
func IsListxattrPermissionError(err error) bool { func isListxattrPermissionError(err error) bool {
var xerr *xattr.Error var xerr *xattr.Error
if errors.As(err, &xerr) { if errors.As(err, &xerr) {
return xerr.Op == "xattr.list" && errors.Is(xerr.Err, os.ErrPermission) return xerr.Op == "xattr.list" && errors.Is(xerr.Err, os.ErrPermission)
@ -64,17 +65,17 @@ func handleXattrErr(err error) error {
} }
} }
// restoreGenericAttributes is no-op. // nodeRestoreGenericAttributes is no-op.
func (node *Node) restoreGenericAttributes(_ string, warn func(msg string)) error { func nodeRestoreGenericAttributes(node *restic.Node, _ string, warn func(msg string)) error {
return node.handleAllUnknownGenericAttributesFound(warn) return restic.HandleAllUnknownGenericAttributesFound(node.GenericAttributes, warn)
} }
// fillGenericAttributes is a no-op. // nodeFillGenericAttributes is a no-op.
func (node *Node) fillGenericAttributes(_ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) { func nodeFillGenericAttributes(_ *restic.Node, _ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
return true, nil return true, nil
} }
func (node Node) restoreExtendedAttributes(path string) error { func nodeRestoreExtendedAttributes(node *restic.Node, path string) error {
expectedAttrs := map[string]struct{}{} expectedAttrs := map[string]struct{}{}
for _, attr := range node.ExtendedAttributes { for _, attr := range node.ExtendedAttributes {
err := setxattr(path, attr.Name, attr.Value) err := setxattr(path, attr.Name, attr.Value)
@ -101,24 +102,24 @@ func (node Node) restoreExtendedAttributes(path string) error {
return nil return nil
} }
func (node *Node) fillExtendedAttributes(path string, ignoreListError bool) error { func nodeFillExtendedAttributes(node *restic.Node, path string, ignoreListError bool) error {
xattrs, err := listxattr(path) xattrs, err := listxattr(path)
debug.Log("fillExtendedAttributes(%v) %v %v", path, xattrs, err) debug.Log("fillExtendedAttributes(%v) %v %v", path, xattrs, err)
if err != nil { if err != nil {
if ignoreListError && IsListxattrPermissionError(err) { if ignoreListError && isListxattrPermissionError(err) {
return nil return nil
} }
return err return err
} }
node.ExtendedAttributes = make([]ExtendedAttribute, 0, len(xattrs)) node.ExtendedAttributes = make([]restic.ExtendedAttribute, 0, len(xattrs))
for _, attr := range xattrs { for _, attr := range xattrs {
attrVal, err := getxattr(path, attr) attrVal, err := getxattr(path, attr)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "can not obtain extended attribute %v for %v:\n", attr, path) fmt.Fprintf(os.Stderr, "can not obtain extended attribute %v for %v:\n", attr, path)
continue continue
} }
attr := ExtendedAttribute{ attr := restic.ExtendedAttribute{
Name: attr, Name: attr,
Value: attrVal, Value: attrVal,
} }

View file

@ -1,7 +1,7 @@
//go:build darwin || freebsd || linux || solaris || windows //go:build darwin || freebsd || linux || solaris || windows
// +build darwin freebsd linux solaris windows // +build darwin freebsd linux solaris windows
package restic package fs
import ( import (
"os" "os"
@ -10,10 +10,11 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
) )
func setAndVerifyXattr(t *testing.T, file string, attrs []ExtendedAttribute) { func setAndVerifyXattr(t *testing.T, file string, attrs []restic.ExtendedAttribute) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
// windows seems to convert the xattr name to upper case // windows seems to convert the xattr name to upper case
for i := range attrs { for i := range attrs {
@ -21,18 +22,18 @@ func setAndVerifyXattr(t *testing.T, file string, attrs []ExtendedAttribute) {
} }
} }
node := Node{ node := &restic.Node{
Type: "file", Type: "file",
ExtendedAttributes: attrs, ExtendedAttributes: attrs,
} }
rtest.OK(t, node.restoreExtendedAttributes(file)) rtest.OK(t, nodeRestoreExtendedAttributes(node, file))
nodeActual := Node{ nodeActual := &restic.Node{
Type: "file", Type: "file",
} }
rtest.OK(t, nodeActual.fillExtendedAttributes(file, false)) rtest.OK(t, nodeFillExtendedAttributes(nodeActual, file, false))
rtest.Assert(t, nodeActual.sameExtendedAttributes(node), "xattr mismatch got %v expected %v", nodeActual.ExtendedAttributes, node.ExtendedAttributes) rtest.Assert(t, nodeActual.Equals(*node), "xattr mismatch got %v expected %v", nodeActual.ExtendedAttributes, node.ExtendedAttributes)
} }
func TestOverwriteXattr(t *testing.T) { func TestOverwriteXattr(t *testing.T) {
@ -40,14 +41,14 @@ func TestOverwriteXattr(t *testing.T) {
file := filepath.Join(dir, "file") file := filepath.Join(dir, "file")
rtest.OK(t, os.WriteFile(file, []byte("hello world"), 0o600)) rtest.OK(t, os.WriteFile(file, []byte("hello world"), 0o600))
setAndVerifyXattr(t, file, []ExtendedAttribute{ setAndVerifyXattr(t, file, []restic.ExtendedAttribute{
{ {
Name: "user.foo", Name: "user.foo",
Value: []byte("bar"), Value: []byte("bar"),
}, },
}) })
setAndVerifyXattr(t, file, []ExtendedAttribute{ setAndVerifyXattr(t, file, []restic.ExtendedAttribute{
{ {
Name: "user.other", Name: "user.other",
Value: []byte("some"), Value: []byte("some"),

View file

@ -1,7 +1,7 @@
//go:build darwin || freebsd || linux || solaris //go:build darwin || freebsd || linux || solaris
// +build darwin freebsd linux solaris // +build darwin freebsd linux solaris
package restic package fs
import ( import (
"os" "os"
@ -19,10 +19,10 @@ func TestIsListxattrPermissionError(t *testing.T) {
} }
err := handleXattrErr(xerr) err := handleXattrErr(xerr)
rtest.Assert(t, err != nil, "missing error") rtest.Assert(t, err != nil, "missing error")
rtest.Assert(t, IsListxattrPermissionError(err), "expected IsListxattrPermissionError to return true for %v", err) rtest.Assert(t, isListxattrPermissionError(err), "expected IsListxattrPermissionError to return true for %v", err)
xerr.Err = os.ErrNotExist xerr.Err = os.ErrNotExist
err = handleXattrErr(xerr) err = handleXattrErr(xerr)
rtest.Assert(t, err != nil, "missing error") rtest.Assert(t, err != nil, "missing error")
rtest.Assert(t, !IsListxattrPermissionError(err), "expected IsListxattrPermissionError to return false for %v", err) rtest.Assert(t, !isListxattrPermissionError(err), "expected IsListxattrPermissionError to return false for %v", err)
} }

View file

@ -6,7 +6,6 @@ import (
"crypto/sha256" "crypto/sha256"
"io" "io"
"os" "os"
"runtime"
"sync" "sync"
"github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/backend"
@ -186,14 +185,6 @@ func (r *Repository) savePacker(ctx context.Context, t restic.BlobType, p *packe
return errors.Wrap(err, "close tempfile") return errors.Wrap(err, "close tempfile")
} }
// on windows the tempfile is automatically deleted on close
if runtime.GOOS != "windows" {
err = fs.RemoveIfExists(p.tmpfile.Name())
if err != nil {
return errors.WithStack(err)
}
}
// update blobs in the index // update blobs in the index
debug.Log(" updating blobs %v to pack %v", p.Packer.Blobs(), id) debug.Log(" updating blobs %v to pack %v", p.Packer.Blobs(), id)
r.idx.StorePack(id, p.Packer.Blobs()) r.idx.StorePack(id, p.Packer.Blobs())

View file

@ -1,16 +1,13 @@
package restic package restic
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"os/user"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"unicode/utf8" "unicode/utf8"
@ -19,7 +16,6 @@ import (
"bytes" "bytes"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/fs"
) )
// ExtendedAttribute is a tuple storing the xattr name and value for various filesystems. // ExtendedAttribute is a tuple storing the xattr name and value for various filesystems.
@ -134,49 +130,6 @@ func (node Node) String() string {
mode|node.Mode, node.UID, node.GID, node.Size, node.ModTime, node.Name) mode|node.Mode, node.UID, node.GID, node.Size, node.ModTime, node.Name)
} }
// NodeFromFileInfo returns a new node from the given path and FileInfo. It
// returns the first error that is encountered, together with a node.
func NodeFromFileInfo(path string, fi os.FileInfo, ignoreXattrListError bool) (*Node, error) {
mask := os.ModePerm | os.ModeType | os.ModeSetuid | os.ModeSetgid | os.ModeSticky
node := &Node{
Path: path,
Name: fi.Name(),
Mode: fi.Mode() & mask,
ModTime: fi.ModTime(),
}
node.Type = nodeTypeFromFileInfo(fi)
if node.Type == "file" {
node.Size = uint64(fi.Size())
}
err := node.fillExtra(path, fi, ignoreXattrListError)
return node, err
}
func nodeTypeFromFileInfo(fi os.FileInfo) string {
switch fi.Mode() & os.ModeType {
case 0:
return "file"
case os.ModeDir:
return "dir"
case os.ModeSymlink:
return "symlink"
case os.ModeDevice | os.ModeCharDevice:
return "chardev"
case os.ModeDevice:
return "dev"
case os.ModeNamedPipe:
return "fifo"
case os.ModeSocket:
return "socket"
case os.ModeIrregular:
return "irregular"
}
return ""
}
// GetExtendedAttribute gets the extended attribute. // GetExtendedAttribute gets the extended attribute.
func (node Node) GetExtendedAttribute(a string) []byte { func (node Node) GetExtendedAttribute(a string) []byte {
for _, attr := range node.ExtendedAttributes { for _, attr := range node.ExtendedAttributes {
@ -187,186 +140,6 @@ func (node Node) GetExtendedAttribute(a string) []byte {
return nil return nil
} }
// CreateAt creates the node at the given path but does NOT restore node meta data.
func (node *Node) CreateAt(ctx context.Context, path string, repo BlobLoader) error {
debug.Log("create node %v at %v", node.Name, path)
switch node.Type {
case "dir":
if err := node.createDirAt(path); err != nil {
return err
}
case "file":
if err := node.createFileAt(ctx, path, repo); err != nil {
return err
}
case "symlink":
if err := node.createSymlinkAt(path); err != nil {
return err
}
case "dev":
if err := node.createDevAt(path); err != nil {
return err
}
case "chardev":
if err := node.createCharDevAt(path); err != nil {
return err
}
case "fifo":
if err := node.createFifoAt(path); err != nil {
return err
}
case "socket":
return nil
default:
return errors.Errorf("filetype %q not implemented", node.Type)
}
return nil
}
// RestoreMetadata restores node metadata
func (node Node) RestoreMetadata(path string, warn func(msg string)) error {
err := node.restoreMetadata(path, warn)
if err != nil {
// It is common to have permission errors for folders like /home
// unless you're running as root, so ignore those.
if os.Geteuid() > 0 && errors.Is(err, os.ErrPermission) {
debug.Log("not running as root, ignoring permission error for %v: %v",
path, err)
return nil
}
debug.Log("restoreMetadata(%s) error %v", path, err)
}
return err
}
func (node Node) restoreMetadata(path string, warn func(msg string)) error {
var firsterr error
if err := lchown(path, int(node.UID), int(node.GID)); err != nil {
firsterr = errors.WithStack(err)
}
if err := node.restoreExtendedAttributes(path); err != nil {
debug.Log("error restoring extended attributes for %v: %v", path, err)
if firsterr == nil {
firsterr = err
}
}
if err := node.restoreGenericAttributes(path, warn); err != nil {
debug.Log("error restoring generic attributes for %v: %v", path, err)
if firsterr == nil {
firsterr = err
}
}
if err := node.RestoreTimestamps(path); err != nil {
debug.Log("error restoring timestamps for %v: %v", path, err)
if firsterr == nil {
firsterr = err
}
}
// Moving RestoreTimestamps and restoreExtendedAttributes calls above as for readonly files in windows
// calling Chmod below will no longer allow any modifications to be made on the file and the
// calls above would fail.
if node.Type != "symlink" {
if err := fs.Chmod(path, node.Mode); err != nil {
if firsterr == nil {
firsterr = errors.WithStack(err)
}
}
}
return firsterr
}
func (node Node) RestoreTimestamps(path string) error {
var utimes = [...]syscall.Timespec{
syscall.NsecToTimespec(node.AccessTime.UnixNano()),
syscall.NsecToTimespec(node.ModTime.UnixNano()),
}
if node.Type == "symlink" {
return node.restoreSymlinkTimestamps(path, utimes)
}
if err := syscall.UtimesNano(path, utimes[:]); err != nil {
return errors.Wrap(err, "UtimesNano")
}
return nil
}
func (node Node) createDirAt(path string) error {
err := fs.Mkdir(path, node.Mode)
if err != nil && !os.IsExist(err) {
return errors.WithStack(err)
}
return nil
}
func (node Node) createFileAt(ctx context.Context, path string, repo BlobLoader) error {
f, err := fs.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
return errors.WithStack(err)
}
err = node.writeNodeContent(ctx, repo, f)
closeErr := f.Close()
if err != nil {
return err
}
if closeErr != nil {
return errors.WithStack(closeErr)
}
return nil
}
func (node Node) writeNodeContent(ctx context.Context, repo BlobLoader, f *os.File) error {
var buf []byte
for _, id := range node.Content {
buf, err := repo.LoadBlob(ctx, DataBlob, id, buf)
if err != nil {
return err
}
_, err = f.Write(buf)
if err != nil {
return errors.WithStack(err)
}
}
return nil
}
func (node Node) createSymlinkAt(path string) error {
if err := fs.Symlink(node.LinkTarget, path); err != nil {
return errors.WithStack(err)
}
return nil
}
func (node *Node) createDevAt(path string) error {
return mknod(path, syscall.S_IFBLK|0600, node.Device)
}
func (node *Node) createCharDevAt(path string) error {
return mknod(path, syscall.S_IFCHR|0600, node.Device)
}
func (node *Node) createFifoAt(path string) error {
return mkfifo(path, 0600)
}
// FixTime returns a time.Time which can safely be used to marshal as JSON. If // FixTime returns a time.Time which can safely be used to marshal as JSON. If
// the timestamp is earlier than year zero, the year is set to zero. In the same // the timestamp is earlier than year zero, the year is set to zero. In the same
// way, if the year is larger than 9999, the year is set to 9999. Other than // way, if the year is larger than 9999, the year is set to 9999. Other than
@ -601,127 +374,6 @@ func deepEqual(map1, map2 map[GenericAttributeType]json.RawMessage) bool {
return true return true
} }
func (node *Node) fillUser(stat *statT) {
uid, gid := stat.uid(), stat.gid()
node.UID, node.GID = uid, gid
node.User = lookupUsername(uid)
node.Group = lookupGroup(gid)
}
var (
uidLookupCache = make(map[uint32]string)
uidLookupCacheMutex = sync.RWMutex{}
)
// Cached user name lookup by uid. Returns "" when no name can be found.
func lookupUsername(uid uint32) string {
uidLookupCacheMutex.RLock()
username, ok := uidLookupCache[uid]
uidLookupCacheMutex.RUnlock()
if ok {
return username
}
u, err := user.LookupId(strconv.Itoa(int(uid)))
if err == nil {
username = u.Username
}
uidLookupCacheMutex.Lock()
uidLookupCache[uid] = username
uidLookupCacheMutex.Unlock()
return username
}
var (
gidLookupCache = make(map[uint32]string)
gidLookupCacheMutex = sync.RWMutex{}
)
// Cached group name lookup by gid. Returns "" when no name can be found.
func lookupGroup(gid uint32) string {
gidLookupCacheMutex.RLock()
group, ok := gidLookupCache[gid]
gidLookupCacheMutex.RUnlock()
if ok {
return group
}
g, err := user.LookupGroupId(strconv.Itoa(int(gid)))
if err == nil {
group = g.Name
}
gidLookupCacheMutex.Lock()
gidLookupCache[gid] = group
gidLookupCacheMutex.Unlock()
return group
}
func (node *Node) fillExtra(path string, fi os.FileInfo, ignoreXattrListError bool) error {
stat, ok := toStatT(fi.Sys())
if !ok {
// fill minimal info with current values for uid, gid
node.UID = uint32(os.Getuid())
node.GID = uint32(os.Getgid())
node.ChangeTime = node.ModTime
return nil
}
node.Inode = uint64(stat.ino())
node.DeviceID = uint64(stat.dev())
node.fillTimes(stat)
node.fillUser(stat)
switch node.Type {
case "file":
node.Size = uint64(stat.size())
node.Links = uint64(stat.nlink())
case "dir":
case "symlink":
var err error
node.LinkTarget, err = fs.Readlink(path)
node.Links = uint64(stat.nlink())
if err != nil {
return errors.WithStack(err)
}
case "dev":
node.Device = uint64(stat.rdev())
node.Links = uint64(stat.nlink())
case "chardev":
node.Device = uint64(stat.rdev())
node.Links = uint64(stat.nlink())
case "fifo":
case "socket":
default:
return errors.Errorf("unsupported file type %q", node.Type)
}
allowExtended, err := node.fillGenericAttributes(path, fi, stat)
if allowExtended {
// Skip processing ExtendedAttributes if allowExtended is false.
err = errors.CombineErrors(err, node.fillExtendedAttributes(path, ignoreXattrListError))
}
return err
}
func mkfifo(path string, mode uint32) (err error) {
return mknod(path, mode|syscall.S_IFIFO, 0)
}
func (node *Node) fillTimes(stat *statT) {
ctim := stat.ctim()
atim := stat.atim()
node.ChangeTime = time.Unix(ctim.Unix())
node.AccessTime = time.Unix(atim.Unix())
}
// HandleUnknownGenericAttributesFound is used for handling and distinguing between scenarios related to future versions and cross-OS repositories // HandleUnknownGenericAttributesFound is used for handling and distinguing between scenarios related to future versions and cross-OS repositories
func HandleUnknownGenericAttributesFound(unknownAttribs []GenericAttributeType, warn func(msg string)) { func HandleUnknownGenericAttributesFound(unknownAttribs []GenericAttributeType, warn func(msg string)) {
for _, unknownAttrib := range unknownAttribs { for _, unknownAttrib := range unknownAttribs {
@ -746,11 +398,11 @@ func handleUnknownGenericAttributeFound(genericAttributeType GenericAttributeTyp
} }
} }
// handleAllUnknownGenericAttributesFound performs validations for all generic attributes in the node. // HandleAllUnknownGenericAttributesFound performs validations for all generic attributes of a node.
// This is not used on windows currently because windows has handling for generic attributes. // This is not used on windows currently because windows has handling for generic attributes.
// nolint:unused // nolint:unused
func (node Node) handleAllUnknownGenericAttributesFound(warn func(msg string)) error { func HandleAllUnknownGenericAttributesFound(attributes map[GenericAttributeType]json.RawMessage, warn func(msg string)) error {
for name := range node.GenericAttributes { for name := range attributes {
handleUnknownGenericAttributeFound(name, warn) handleUnknownGenericAttributeFound(name, warn)
} }
return nil return nil
@ -770,9 +422,8 @@ func checkGenericAttributeNameNotHandledAndPut(value GenericAttributeType) bool
// The functions below are common helper functions which can be used for generic attributes support // The functions below are common helper functions which can be used for generic attributes support
// across different OS. // across different OS.
// genericAttributesToOSAttrs gets the os specific attribute from the generic attribute using reflection // GenericAttributesToOSAttrs gets the os specific attribute from the generic attribute using reflection
// nolint:unused func GenericAttributesToOSAttrs(attrs map[GenericAttributeType]json.RawMessage, attributeType reflect.Type, attributeValuePtr *reflect.Value, keyPrefix string) (unknownAttribs []GenericAttributeType, err error) {
func genericAttributesToOSAttrs(attrs map[GenericAttributeType]json.RawMessage, attributeType reflect.Type, attributeValuePtr *reflect.Value, keyPrefix string) (unknownAttribs []GenericAttributeType, err error) {
attributeValue := *attributeValuePtr attributeValue := *attributeValuePtr
for key, rawMsg := range attrs { for key, rawMsg := range attrs {
@ -796,20 +447,17 @@ func genericAttributesToOSAttrs(attrs map[GenericAttributeType]json.RawMessage,
} }
// getFQKey gets the fully qualified key for the field // getFQKey gets the fully qualified key for the field
// nolint:unused
func getFQKey(field reflect.StructField, keyPrefix string) GenericAttributeType { func getFQKey(field reflect.StructField, keyPrefix string) GenericAttributeType {
return GenericAttributeType(fmt.Sprintf("%s.%s", keyPrefix, field.Tag.Get("generic"))) return GenericAttributeType(fmt.Sprintf("%s.%s", keyPrefix, field.Tag.Get("generic")))
} }
// getFQKeyByIndex gets the fully qualified key for the field index // getFQKeyByIndex gets the fully qualified key for the field index
// nolint:unused
func getFQKeyByIndex(attributeType reflect.Type, index int, keyPrefix string) GenericAttributeType { func getFQKeyByIndex(attributeType reflect.Type, index int, keyPrefix string) GenericAttributeType {
return getFQKey(attributeType.Field(index), keyPrefix) return getFQKey(attributeType.Field(index), keyPrefix)
} }
// osAttrsToGenericAttributes gets the generic attribute from the os specific attribute using reflection // OSAttrsToGenericAttributes gets the generic attribute from the os specific attribute using reflection
// nolint:unused func OSAttrsToGenericAttributes(attributeType reflect.Type, attributeValuePtr *reflect.Value, keyPrefix string) (attrs map[GenericAttributeType]json.RawMessage, err error) {
func osAttrsToGenericAttributes(attributeType reflect.Type, attributeValuePtr *reflect.Value, keyPrefix string) (attrs map[GenericAttributeType]json.RawMessage, err error) {
attributeValue := *attributeValuePtr attributeValue := *attributeValuePtr
attrs = make(map[GenericAttributeType]json.RawMessage) attrs = make(map[GenericAttributeType]json.RawMessage)

View file

@ -1,49 +0,0 @@
//go:build aix
// +build aix
package restic
import (
"os"
"syscall"
)
func (node Node) restoreSymlinkTimestamps(_ string, _ [2]syscall.Timespec) error {
return nil
}
// AIX has a funny timespec type in syscall, with 32-bit nanoseconds.
// golang.org/x/sys/unix handles this cleanly, but we're stuck with syscall
// because os.Stat returns a syscall type in its os.FileInfo.Sys().
func toTimespec(t syscall.StTimespec_t) syscall.Timespec {
return syscall.Timespec{Sec: t.Sec, Nsec: int64(t.Nsec)}
}
func (s statT) atim() syscall.Timespec { return toTimespec(s.Atim) }
func (s statT) mtim() syscall.Timespec { return toTimespec(s.Mtim) }
func (s statT) ctim() syscall.Timespec { return toTimespec(s.Ctim) }
// restoreExtendedAttributes is a no-op on AIX.
func (node Node) restoreExtendedAttributes(_ string) error {
return nil
}
// fillExtendedAttributes is a no-op on AIX.
func (node *Node) fillExtendedAttributes(_ string, _ bool) error {
return nil
}
// IsListxattrPermissionError is a no-op on AIX.
func IsListxattrPermissionError(_ error) bool {
return false
}
// restoreGenericAttributes is no-op on AIX.
func (node *Node) restoreGenericAttributes(_ string, warn func(msg string)) error {
return node.handleAllUnknownGenericAttributesFound(warn)
}
// fillGenericAttributes is a no-op on AIX.
func (node *Node) fillGenericAttributes(_ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
return true, nil
}

View file

@ -1,39 +0,0 @@
package restic
import (
"os"
"syscall"
)
func (node Node) restoreSymlinkTimestamps(_ string, _ [2]syscall.Timespec) error {
return nil
}
func (s statT) atim() syscall.Timespec { return s.Atimespec }
func (s statT) mtim() syscall.Timespec { return s.Mtimespec }
func (s statT) ctim() syscall.Timespec { return s.Ctimespec }
// restoreExtendedAttributes is a no-op on netbsd.
func (node Node) restoreExtendedAttributes(_ string) error {
return nil
}
// fillExtendedAttributes is a no-op on netbsd.
func (node *Node) fillExtendedAttributes(_ string, _ bool) error {
return nil
}
// IsListxattrPermissionError is a no-op on netbsd.
func IsListxattrPermissionError(_ error) bool {
return false
}
// restoreGenericAttributes is no-op on netbsd.
func (node *Node) restoreGenericAttributes(_ string, warn func(msg string)) error {
return node.handleAllUnknownGenericAttributesFound(warn)
}
// fillGenericAttributes is a no-op on netbsd.
func (node *Node) fillGenericAttributes(_ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
return true, nil
}

View file

@ -1,39 +0,0 @@
package restic
import (
"os"
"syscall"
)
func (node Node) restoreSymlinkTimestamps(_ string, _ [2]syscall.Timespec) error {
return nil
}
func (s statT) atim() syscall.Timespec { return s.Atim }
func (s statT) mtim() syscall.Timespec { return s.Mtim }
func (s statT) ctim() syscall.Timespec { return s.Ctim }
// restoreExtendedAttributes is a no-op on openbsd.
func (node Node) restoreExtendedAttributes(_ string) error {
return nil
}
// fillExtendedAttributes is a no-op on openbsd.
func (node *Node) fillExtendedAttributes(_ string, _ bool) error {
return nil
}
// IsListxattrPermissionError is a no-op on openbsd.
func IsListxattrPermissionError(_ error) bool {
return false
}
// restoreGenericAttributes is no-op on openbsd.
func (node *Node) restoreGenericAttributes(_ string, warn func(msg string)) error {
return node.handleAllUnknownGenericAttributesFound(warn)
}
// fillGenericAttributes is a no-op on openbsd.
func (node *Node) fillGenericAttributes(_ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
return true, nil
}

View file

@ -1,318 +1,14 @@
package restic package restic
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing" "testing"
"time" "time"
"github.com/google/go-cmp/cmp"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/test" "github.com/restic/restic/internal/test"
rtest "github.com/restic/restic/internal/test"
) )
func BenchmarkNodeFillUser(t *testing.B) {
tempfile, err := os.CreateTemp("", "restic-test-temp-")
if err != nil {
t.Fatal(err)
}
fi, err := tempfile.Stat()
if err != nil {
t.Fatal(err)
}
path := tempfile.Name()
t.ResetTimer()
for i := 0; i < t.N; i++ {
_, err := NodeFromFileInfo(path, fi, false)
rtest.OK(t, err)
}
rtest.OK(t, tempfile.Close())
rtest.RemoveAll(t, tempfile.Name())
}
func BenchmarkNodeFromFileInfo(t *testing.B) {
tempfile, err := os.CreateTemp("", "restic-test-temp-")
if err != nil {
t.Fatal(err)
}
fi, err := tempfile.Stat()
if err != nil {
t.Fatal(err)
}
path := tempfile.Name()
t.ResetTimer()
for i := 0; i < t.N; i++ {
_, err := NodeFromFileInfo(path, fi, false)
if err != nil {
t.Fatal(err)
}
}
rtest.OK(t, tempfile.Close())
rtest.RemoveAll(t, tempfile.Name())
}
func parseTime(s string) time.Time {
t, err := time.Parse("2006-01-02 15:04:05.999", s)
if err != nil {
panic(err)
}
return t.Local()
}
var nodeTests = []Node{
{
Name: "testFile",
Type: "file",
Content: IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0604,
ModTime: parseTime("2015-05-14 21:07:23.111"),
AccessTime: parseTime("2015-05-14 21:07:24.222"),
ChangeTime: parseTime("2015-05-14 21:07:25.333"),
},
{
Name: "testSuidFile",
Type: "file",
Content: IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0755 | os.ModeSetuid,
ModTime: parseTime("2015-05-14 21:07:23.111"),
AccessTime: parseTime("2015-05-14 21:07:24.222"),
ChangeTime: parseTime("2015-05-14 21:07:25.333"),
},
{
Name: "testSuidFile2",
Type: "file",
Content: IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0755 | os.ModeSetgid,
ModTime: parseTime("2015-05-14 21:07:23.111"),
AccessTime: parseTime("2015-05-14 21:07:24.222"),
ChangeTime: parseTime("2015-05-14 21:07:25.333"),
},
{
Name: "testSticky",
Type: "file",
Content: IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0755 | os.ModeSticky,
ModTime: parseTime("2015-05-14 21:07:23.111"),
AccessTime: parseTime("2015-05-14 21:07:24.222"),
ChangeTime: parseTime("2015-05-14 21:07:25.333"),
},
{
Name: "testDir",
Type: "dir",
Subtree: nil,
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0750 | os.ModeDir,
ModTime: parseTime("2015-05-14 21:07:23.111"),
AccessTime: parseTime("2015-05-14 21:07:24.222"),
ChangeTime: parseTime("2015-05-14 21:07:25.333"),
},
{
Name: "testSymlink",
Type: "symlink",
LinkTarget: "invalid",
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0777 | os.ModeSymlink,
ModTime: parseTime("2015-05-14 21:07:23.111"),
AccessTime: parseTime("2015-05-14 21:07:24.222"),
ChangeTime: parseTime("2015-05-14 21:07:25.333"),
},
// include "testFile" and "testDir" again with slightly different
// metadata, so we can test if CreateAt works with pre-existing files.
{
Name: "testFile",
Type: "file",
Content: IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0604,
ModTime: parseTime("2005-05-14 21:07:03.111"),
AccessTime: parseTime("2005-05-14 21:07:04.222"),
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
},
{
Name: "testDir",
Type: "dir",
Subtree: nil,
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0750 | os.ModeDir,
ModTime: parseTime("2005-05-14 21:07:03.111"),
AccessTime: parseTime("2005-05-14 21:07:04.222"),
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
},
{
Name: "testXattrFile",
Type: "file",
Content: IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0604,
ModTime: parseTime("2005-05-14 21:07:03.111"),
AccessTime: parseTime("2005-05-14 21:07:04.222"),
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
ExtendedAttributes: []ExtendedAttribute{
{"user.foo", []byte("bar")},
},
},
{
Name: "testXattrDir",
Type: "dir",
Subtree: nil,
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0750 | os.ModeDir,
ModTime: parseTime("2005-05-14 21:07:03.111"),
AccessTime: parseTime("2005-05-14 21:07:04.222"),
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
ExtendedAttributes: []ExtendedAttribute{
{"user.foo", []byte("bar")},
},
},
{
Name: "testXattrFileMacOSResourceFork",
Type: "file",
Content: IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0604,
ModTime: parseTime("2005-05-14 21:07:03.111"),
AccessTime: parseTime("2005-05-14 21:07:04.222"),
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
ExtendedAttributes: []ExtendedAttribute{
{"com.apple.ResourceFork", []byte("bar")},
},
},
}
func TestNodeRestoreAt(t *testing.T) {
tempdir := t.TempDir()
for _, test := range nodeTests {
t.Run("", func(t *testing.T) {
var nodePath string
if test.ExtendedAttributes != nil {
if runtime.GOOS == "windows" {
// In windows extended attributes are case insensitive and windows returns
// the extended attributes in UPPER case.
// Update the tests to use UPPER case xattr names for windows.
extAttrArr := test.ExtendedAttributes
// Iterate through the array using pointers
for i := 0; i < len(extAttrArr); i++ {
extAttrArr[i].Name = strings.ToUpper(extAttrArr[i].Name)
}
}
for _, attr := range test.ExtendedAttributes {
if strings.HasPrefix(attr.Name, "com.apple.") && runtime.GOOS != "darwin" {
t.Skipf("attr %v only relevant on macOS", attr.Name)
}
}
// tempdir might be backed by a filesystem that does not support
// extended attributes
nodePath = test.Name
defer func() {
_ = os.Remove(nodePath)
}()
} else {
nodePath = filepath.Join(tempdir, test.Name)
}
rtest.OK(t, test.CreateAt(context.TODO(), nodePath, nil))
rtest.OK(t, test.RestoreMetadata(nodePath, func(msg string) { rtest.OK(t, fmt.Errorf("Warning triggered for path: %s: %s", nodePath, msg)) }))
fi, err := os.Lstat(nodePath)
rtest.OK(t, err)
n2, err := NodeFromFileInfo(nodePath, fi, false)
rtest.OK(t, err)
n3, err := NodeFromFileInfo(nodePath, fi, true)
rtest.OK(t, err)
rtest.Assert(t, n2.Equals(*n3), "unexpected node info mismatch %v", cmp.Diff(n2, n3))
rtest.Assert(t, test.Name == n2.Name,
"%v: name doesn't match (%v != %v)", test.Type, test.Name, n2.Name)
rtest.Assert(t, test.Type == n2.Type,
"%v: type doesn't match (%v != %v)", test.Type, test.Type, n2.Type)
rtest.Assert(t, test.Size == n2.Size,
"%v: size doesn't match (%v != %v)", test.Size, test.Size, n2.Size)
if runtime.GOOS != "windows" {
rtest.Assert(t, test.UID == n2.UID,
"%v: UID doesn't match (%v != %v)", test.Type, test.UID, n2.UID)
rtest.Assert(t, test.GID == n2.GID,
"%v: GID doesn't match (%v != %v)", test.Type, test.GID, n2.GID)
if test.Type != "symlink" {
// On OpenBSD only root can set sticky bit (see sticky(8)).
if runtime.GOOS != "openbsd" && runtime.GOOS != "netbsd" && runtime.GOOS != "solaris" && test.Name == "testSticky" {
rtest.Assert(t, test.Mode == n2.Mode,
"%v: mode doesn't match (0%o != 0%o)", test.Type, test.Mode, n2.Mode)
}
}
}
AssertFsTimeEqual(t, "AccessTime", test.Type, test.AccessTime, n2.AccessTime)
AssertFsTimeEqual(t, "ModTime", test.Type, test.ModTime, n2.ModTime)
if len(n2.ExtendedAttributes) == 0 {
n2.ExtendedAttributes = nil
}
rtest.Assert(t, reflect.DeepEqual(test.ExtendedAttributes, n2.ExtendedAttributes),
"%v: xattrs don't match (%v != %v)", test.Name, test.ExtendedAttributes, n2.ExtendedAttributes)
})
}
}
func AssertFsTimeEqual(t *testing.T, label string, nodeType string, t1 time.Time, t2 time.Time) {
var equal bool
// Go currently doesn't support setting timestamps of symbolic links on darwin and bsd
if nodeType == "symlink" {
switch runtime.GOOS {
case "darwin", "freebsd", "openbsd", "netbsd", "solaris":
return
}
}
switch runtime.GOOS {
case "darwin":
// HFS+ timestamps don't support sub-second precision,
// see https://en.wikipedia.org/wiki/Comparison_of_file_systems
diff := int(t1.Sub(t2).Seconds())
equal = diff == 0
default:
equal = t1.Equal(t2)
}
rtest.Assert(t, equal, "%s: %s doesn't match (%v != %v)", label, nodeType, t1, t2)
}
func parseTimeNano(t testing.TB, s string) time.Time { func parseTimeNano(t testing.TB, s string) time.Time {
// 2006-01-02T15:04:05.999999999Z07:00 // 2006-01-02T15:04:05.999999999Z07:00
ts, err := time.Parse(time.RFC3339Nano, s) ts, err := time.Parse(time.RFC3339Nano, s)
@ -398,14 +94,3 @@ func TestSymlinkSerializationFormat(t *testing.T) {
test.Assert(t, n2.LinkTargetRaw == nil, "quoted link target is just a helper field and must be unset after decoding") test.Assert(t, n2.LinkTargetRaw == nil, "quoted link target is just a helper field and must be unset after decoding")
} }
} }
func TestNodeRestoreMetadataError(t *testing.T) {
tempdir := t.TempDir()
node := nodeTests[0]
nodePath := filepath.Join(tempdir, node.Name)
// This will fail because the target file does not exist
err := node.RestoreMetadata(nodePath, func(msg string) { rtest.OK(t, fmt.Errorf("Warning triggered for path: %s: %s", nodePath, msg)) })
test.Assert(t, errors.Is(err, os.ErrNotExist), "failed for an unexpected reason")
}

View file

@ -10,6 +10,7 @@ import (
"testing" "testing"
"github.com/restic/restic/internal/archiver" "github.com/restic/restic/internal/archiver"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
@ -86,7 +87,7 @@ func TestNodeComparison(t *testing.T) {
fi, err := os.Lstat("tree_test.go") fi, err := os.Lstat("tree_test.go")
rtest.OK(t, err) rtest.OK(t, err)
node, err := restic.NodeFromFileInfo("tree_test.go", fi, false) node, err := fs.NodeFromFileInfo("tree_test.go", fi, false)
rtest.OK(t, err) rtest.OK(t, err)
n2 := *node n2 := *node
@ -127,7 +128,7 @@ func TestTreeEqualSerialization(t *testing.T) {
for _, fn := range files[:i] { for _, fn := range files[:i] {
fi, err := os.Lstat(fn) fi, err := os.Lstat(fn)
rtest.OK(t, err) rtest.OK(t, err)
node, err := restic.NodeFromFileInfo(fn, fi, false) node, err := fs.NodeFromFileInfo(fn, fi, false)
rtest.OK(t, err) rtest.OK(t, err)
rtest.OK(t, tree.Insert(node)) rtest.OK(t, tree.Insert(node))

View file

@ -2,7 +2,6 @@ package restorer
import ( import (
"fmt" "fmt"
stdfs "io/fs"
"os" "os"
"sync" "sync"
"syscall" "syscall"
@ -82,7 +81,7 @@ func createFile(path string, createSize int64, sparse bool, allowRecursiveDelete
return nil, err return nil, err
} }
var fi stdfs.FileInfo var fi os.FileInfo
if f != nil { if f != nil {
// stat to check that we've opened a regular file // stat to check that we've opened a regular file
fi, err = f.Stat() fi, err = f.Stat()
@ -135,7 +134,7 @@ func createFile(path string, createSize int64, sparse bool, allowRecursiveDelete
return ensureSize(f, fi, createSize, sparse) return ensureSize(f, fi, createSize, sparse)
} }
func ensureSize(f *os.File, fi stdfs.FileInfo, createSize int64, sparse bool) (*os.File, error) { func ensureSize(f *os.File, fi os.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 {

View file

@ -265,14 +265,14 @@ func (res *Restorer) traverseTreeInner(ctx context.Context, target, location str
return filenames, hasRestored, nil return filenames, hasRestored, nil
} }
func (res *Restorer) restoreNodeTo(ctx context.Context, node *restic.Node, target, location string) error { func (res *Restorer) restoreNodeTo(node *restic.Node, target, location string) error {
if !res.opts.DryRun { if !res.opts.DryRun {
debug.Log("restoreNode %v %v %v", node.Name, target, location) debug.Log("restoreNode %v %v %v", node.Name, target, location)
if err := fs.Remove(target); err != nil && !errors.Is(err, os.ErrNotExist) { if err := fs.Remove(target); err != nil && !errors.Is(err, os.ErrNotExist) {
return errors.Wrap(err, "RemoveNode") return errors.Wrap(err, "RemoveNode")
} }
err := node.CreateAt(ctx, target, res.repo) err := fs.NodeCreateAt(node, target)
if err != nil { if err != nil {
debug.Log("node.CreateAt(%s) error %v", target, err) debug.Log("node.CreateAt(%s) error %v", target, err)
return err return err
@ -288,7 +288,7 @@ func (res *Restorer) restoreNodeMetadataTo(node *restic.Node, target, location s
return nil return nil
} }
debug.Log("restoreNodeMetadata %v %v %v", node.Name, target, location) debug.Log("restoreNodeMetadata %v %v %v", node.Name, target, location)
err := node.RestoreMetadata(target, res.Warn) err := fs.NodeRestoreMetadata(node, target, res.Warn)
if err != nil { if err != nil {
debug.Log("node.RestoreMetadata(%s) error %v", target, err) debug.Log("node.RestoreMetadata(%s) error %v", target, err)
} }
@ -435,7 +435,7 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) (uint64, error)
debug.Log("second pass, visitNode: restore node %q", location) debug.Log("second pass, visitNode: restore node %q", location)
if node.Type != "file" { if node.Type != "file" {
_, err := res.withOverwriteCheck(ctx, node, target, location, false, nil, func(_ bool, _ *fileState) error { _, err := res.withOverwriteCheck(ctx, node, target, location, false, nil, func(_ bool, _ *fileState) error {
return res.restoreNodeTo(ctx, node, target, location) return res.restoreNodeTo(node, target, location)
}) })
return err return err
} }

View file

@ -16,6 +16,7 @@ import (
"unsafe" "unsafe"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/test" "github.com/restic/restic/internal/test"
@ -263,7 +264,7 @@ func setup(t *testing.T, nodesMap map[string]Node) *Restorer {
//If the node is a directory add FILE_ATTRIBUTE_DIRECTORY to attributes //If the node is a directory add FILE_ATTRIBUTE_DIRECTORY to attributes
fileattr |= windows.FILE_ATTRIBUTE_DIRECTORY fileattr |= windows.FILE_ATTRIBUTE_DIRECTORY
} }
attrs, err := restic.WindowsAttrsToGenericAttributes(restic.WindowsAttributes{FileAttributes: &fileattr}) attrs, err := fs.WindowsAttrsToGenericAttributes(fs.WindowsAttributes{FileAttributes: &fileattr})
test.OK(t, err) test.OK(t, err)
return attrs return attrs
} }