mirror of
https://github.com/restic/restic.git
synced 2025-03-30 00:00:14 +01:00

For `restic backup`, support new flags `--changed-files-from-verbatim` and `--changed-files-from-raw` to read the files/dirs that actually have changed from a file (or multiple files). Directories that don't (directly or indirectly) contain any changed files/dirs will reuse the corresponding subtree of the parent snapshot. This option is useful for higher-level backup tools which use restic as a backend but have their own mechanism of figuring out which files have changed (e.g., using zfs or btrfs diff tools). We require to explicitly pass `--parent` as a protection mechanism in order to make sure the higher-level backup tool and restic agree on the parent snapshot. Though the caller can circumvent this protection mechanism by passing `--parent latest`. Caveat: since device IDs are unstable (across reboots or across different zfs/btrfs snapshots of the same subvolume), the parent snapshot and current snapshot might have mismatching device IDs. In this case, the feature will still reuse subtrees of the parent snapshot (under the conditions mentioned above), so we end up with a snapshot that contains subtrees with different `device_id` values, even if there was only a single mountpoint in play. For now, we could simply document this caveat and discourage users who rely on correct restoration of hardlinks from using this feature. When https://github.com/restic/restic/issues/3041 is properly fixed in the future, then this caveat probably goes away, too. The idea for this feature emerged here: https://github.com/restic/restic/issues/1502#issuecomment-1721956623
323 lines
7.8 KiB
Go
323 lines
7.8 KiB
Go
package archiver
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/restic/restic/internal/crypto"
|
|
"github.com/restic/restic/internal/debug"
|
|
"github.com/restic/restic/internal/fs"
|
|
"github.com/restic/restic/internal/restic"
|
|
)
|
|
|
|
// TestSnapshot creates a new snapshot of path.
|
|
func TestSnapshot(t testing.TB, repo restic.Repository, path string, parent *restic.ID) *restic.Snapshot {
|
|
arch := New(repo, fs.Local{}, Options{})
|
|
opts := SnapshotOptions{
|
|
Time: time.Now(),
|
|
Hostname: "localhost",
|
|
Tags: []string{"test"},
|
|
}
|
|
if parent != nil {
|
|
sn, err := restic.LoadSnapshot(context.TODO(), arch.Repo, *parent)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opts.ParentSnapshot = sn
|
|
}
|
|
sn, _, err := arch.Snapshot(context.TODO(), []string{path}, nil, opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return sn
|
|
}
|
|
|
|
// TestDir describes a directory structure to create for a test.
|
|
type TestDir map[string]interface{}
|
|
|
|
func (d TestDir) String() string {
|
|
return "<Dir>"
|
|
}
|
|
|
|
// TestFile describes a file created for a test.
|
|
type TestFile struct {
|
|
Content string
|
|
}
|
|
|
|
func (f TestFile) String() string {
|
|
return "<File>"
|
|
}
|
|
|
|
// TestSymlink describes a symlink created for a test.
|
|
type TestSymlink struct {
|
|
Target string
|
|
}
|
|
|
|
func (s TestSymlink) String() string {
|
|
return "<Symlink>"
|
|
}
|
|
|
|
// TestCreateFiles creates a directory structure described by dir at target,
|
|
// which must already exist. On Windows, symlinks aren't created.
|
|
func TestCreateFiles(t testing.TB, target string, dir TestDir) {
|
|
t.Helper()
|
|
for name, item := range dir {
|
|
targetPath := filepath.Join(target, name)
|
|
|
|
switch it := item.(type) {
|
|
case TestFile:
|
|
err := os.WriteFile(targetPath, []byte(it.Content), 0644)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case TestSymlink:
|
|
err := fs.Symlink(filepath.FromSlash(it.Target), targetPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case TestDir:
|
|
err := fs.Mkdir(targetPath, 0755)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
TestCreateFiles(t, targetPath, it)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestWalkFunc is used by TestWalkFiles to traverse the dir. When an error is
|
|
// returned, traversal stops and the surrounding test is marked as failed.
|
|
type TestWalkFunc func(path string, item interface{}) error
|
|
|
|
// TestWalkFiles runs fn for each file/directory in dir, the filename will be
|
|
// constructed with target as the prefix. Symlinks on Windows are ignored.
|
|
func TestWalkFiles(t testing.TB, target string, dir TestDir, fn TestWalkFunc) {
|
|
t.Helper()
|
|
for name, item := range dir {
|
|
targetPath := filepath.Join(target, name)
|
|
|
|
err := fn(targetPath, item)
|
|
if err != nil {
|
|
t.Fatalf("TestWalkFunc returned error for %v: %v", targetPath, err)
|
|
return
|
|
}
|
|
|
|
if dir, ok := item.(TestDir); ok {
|
|
TestWalkFiles(t, targetPath, dir, fn)
|
|
}
|
|
}
|
|
}
|
|
|
|
// fixpath removes UNC paths (starting with `\\?`) on windows. On Linux, it's a noop.
|
|
func fixpath(item string) string {
|
|
if runtime.GOOS != "windows" {
|
|
return item
|
|
}
|
|
if strings.HasPrefix(item, `\\?`) {
|
|
return item[4:]
|
|
}
|
|
return item
|
|
}
|
|
|
|
// TestEnsureFiles tests if the directory structure at target is the same as
|
|
// described in dir.
|
|
func TestEnsureFiles(t testing.TB, target string, dir TestDir) {
|
|
t.Helper()
|
|
pathsChecked := make(map[string]struct{})
|
|
|
|
// first, test that all items are there
|
|
TestWalkFiles(t, target, dir, func(path string, item interface{}) error {
|
|
fi, err := fs.Lstat(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch node := item.(type) {
|
|
case TestDir:
|
|
if !fi.IsDir() {
|
|
t.Errorf("is not a directory: %v", path)
|
|
}
|
|
return nil
|
|
case TestFile:
|
|
if !fs.IsRegularFile(fi) {
|
|
t.Errorf("is not a regular file: %v", path)
|
|
return nil
|
|
}
|
|
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if string(content) != node.Content {
|
|
t.Errorf("wrong content for %v, want %q, got %q", path, node.Content, content)
|
|
}
|
|
case TestSymlink:
|
|
if fi.Mode()&os.ModeType != os.ModeSymlink {
|
|
t.Errorf("is not a symlink: %v", path)
|
|
return nil
|
|
}
|
|
|
|
target, err := fs.Readlink(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if target != node.Target {
|
|
t.Errorf("wrong target for %v, want %v, got %v", path, node.Target, target)
|
|
}
|
|
}
|
|
|
|
pathsChecked[path] = struct{}{}
|
|
|
|
for parent := filepath.Dir(path); parent != target; parent = filepath.Dir(parent) {
|
|
pathsChecked[parent] = struct{}{}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
// then, traverse the directory again, looking for additional files
|
|
err := fs.Walk(target, func(path string, fi os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
path = fixpath(path)
|
|
|
|
if path == target {
|
|
return nil
|
|
}
|
|
|
|
_, ok := pathsChecked[path]
|
|
if !ok {
|
|
t.Errorf("additional item found: %v %v", path, fi.Mode())
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// TestEnsureFileContent checks if the file in the repo is the same as file.
|
|
func TestEnsureFileContent(ctx context.Context, t testing.TB, repo restic.Repository, filename string, node *restic.Node, file TestFile) {
|
|
if int(node.Size) != len(file.Content) {
|
|
t.Fatalf("%v: wrong node size: want %d, got %d", filename, node.Size, len(file.Content))
|
|
return
|
|
}
|
|
|
|
content := make([]byte, crypto.CiphertextLength(len(file.Content)))
|
|
pos := 0
|
|
for _, id := range node.Content {
|
|
part, err := repo.LoadBlob(ctx, restic.DataBlob, id, content[pos:])
|
|
if err != nil {
|
|
t.Fatalf("error loading blob %v: %v", id.Str(), err)
|
|
return
|
|
}
|
|
|
|
copy(content[pos:pos+len(part)], part)
|
|
pos += len(part)
|
|
}
|
|
|
|
content = content[:pos]
|
|
|
|
if string(content) != file.Content {
|
|
t.Fatalf("%v: wrong content returned, want %q, got %q", filename, file.Content, content)
|
|
}
|
|
}
|
|
|
|
// TestEnsureTree checks that the tree ID in the repo matches dir. On Windows,
|
|
// Symlinks are ignored.
|
|
func TestEnsureTree(ctx context.Context, t testing.TB, prefix string, repo restic.Repository, treeID restic.ID, dir TestDir) {
|
|
t.Helper()
|
|
|
|
tree, err := restic.LoadTree(ctx, repo, treeID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
|
|
var nodeNames []string
|
|
for _, node := range tree.Nodes {
|
|
nodeNames = append(nodeNames, node.Name)
|
|
}
|
|
debug.Log("%v (%v) %v", prefix, treeID.Str(), nodeNames)
|
|
|
|
checked := make(map[string]struct{})
|
|
for _, node := range tree.Nodes {
|
|
nodePrefix := path.Join(prefix, node.Name)
|
|
|
|
entry, ok := dir[node.Name]
|
|
if !ok {
|
|
t.Errorf("unexpected tree node %q found, want: %#v", node.Name, dir)
|
|
return
|
|
}
|
|
|
|
checked[node.Name] = struct{}{}
|
|
|
|
switch e := entry.(type) {
|
|
case TestDir:
|
|
if node.Type != "dir" {
|
|
t.Errorf("tree node %v has wrong type %q, want %q", nodePrefix, node.Type, "dir")
|
|
return
|
|
}
|
|
|
|
if node.Subtree == nil {
|
|
t.Errorf("tree node %v has nil subtree", nodePrefix)
|
|
return
|
|
}
|
|
|
|
TestEnsureTree(ctx, t, path.Join(prefix, node.Name), repo, *node.Subtree, e)
|
|
case TestFile:
|
|
if node.Type != "file" {
|
|
t.Errorf("tree node %v has wrong type %q, want %q", nodePrefix, node.Type, "file")
|
|
}
|
|
TestEnsureFileContent(ctx, t, repo, nodePrefix, node, e)
|
|
case TestSymlink:
|
|
if node.Type != "symlink" {
|
|
t.Errorf("tree node %v has wrong type %q, want %q", nodePrefix, node.Type, "file")
|
|
}
|
|
|
|
if e.Target != node.LinkTarget {
|
|
t.Errorf("symlink %v has wrong target, want %q, got %q", nodePrefix, e.Target, node.LinkTarget)
|
|
}
|
|
}
|
|
}
|
|
|
|
for name := range dir {
|
|
_, ok := checked[name]
|
|
if !ok {
|
|
t.Errorf("tree %v: expected node %q not found, has: %v", prefix, name, nodeNames)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestEnsureSnapshot tests if the snapshot in the repo has exactly the same
|
|
// structure as dir. On Windows, Symlinks are ignored.
|
|
func TestEnsureSnapshot(t testing.TB, repo restic.Repository, snapshotID restic.ID, dir TestDir) {
|
|
t.Helper()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
sn, err := restic.LoadSnapshot(ctx, repo, snapshotID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
|
|
if sn.Tree == nil {
|
|
t.Fatal("snapshot has nil tree ID")
|
|
return
|
|
}
|
|
|
|
TestEnsureTree(ctx, t, "/", repo, *sn.Tree, dir)
|
|
}
|