From d53c3d85ed83ec9fee412ee3a2620d9a2e0939b8 Mon Sep 17 00:00:00 2001 From: aneesh-n <99904+aneesh-n@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:01:59 +0530 Subject: [PATCH] Add test cases for ADS --- internal/archiver/archiver_test.go | 2 +- internal/archiver/archiver_windows_test.go | 223 +++++++ internal/archiver/testing.go | 4 +- internal/archiver/testing_unix.go | 20 + internal/archiver/testing_windows.go | 43 ++ internal/restic/ads_windows_test.go | 187 ++++++ internal/restorer/fileswriter_test.go | 16 +- internal/restorer/restorer_test.go | 127 +++- internal/restorer/restorer_unix_test.go | 6 +- internal/restorer/restorer_windows_test.go | 725 +++++++++++++-------- internal/ui/restore/progress_test.go | 51 +- 11 files changed, 1063 insertions(+), 341 deletions(-) create mode 100644 internal/archiver/archiver_windows_test.go create mode 100644 internal/archiver/testing_unix.go create mode 100644 internal/archiver/testing_windows.go create mode 100644 internal/restic/ads_windows_test.go diff --git a/internal/archiver/archiver_test.go b/internal/archiver/archiver_test.go index fcc3d465d..0e0532547 100644 --- a/internal/archiver/archiver_test.go +++ b/internal/archiver/archiver_test.go @@ -1847,7 +1847,7 @@ func TestArchiverParent(t *testing.T) { } func TestArchiverErrorReporting(t *testing.T) { - ignoreErrorForBasename := func(basename string) ErrorFunc { + ignoreErrorForBasename := func(_ string) ErrorFunc { return func(item string, err error) error { if filepath.Base(item) == "targetfile" { t.Logf("ignoring error for targetfile: %v", err) diff --git a/internal/archiver/archiver_windows_test.go b/internal/archiver/archiver_windows_test.go new file mode 100644 index 000000000..b265d4d0c --- /dev/null +++ b/internal/archiver/archiver_windows_test.go @@ -0,0 +1,223 @@ +//go:build windows +// +build windows + +package archiver + +import ( + "context" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/restic/restic/internal/checker" + "github.com/restic/restic/internal/filter" + "github.com/restic/restic/internal/fs" + "github.com/restic/restic/internal/restic" + rtest "github.com/restic/restic/internal/test" +) + +func TestArchiverSnapshotWithAds(t *testing.T) { + // The toplevel directory is not counted in the ItemStats + var tests = []struct { + name string + src TestDir + targets []string + want TestDir + stat ItemStats + exclude []string + }{ + { + name: "Ads_directory_Basic", + src: TestDir{ + "dir": TestDir{ + "targetfile.txt": TestFile{Content: string("foobar")}, + "targetfile.txt:Stream1:$DATA": TestFile{Content: string("stream 1")}, + "targetfile.txt:Stream2:$DATA": TestFile{Content: string("stream 2")}, + }, + }, + targets: []string{"dir"}, + stat: ItemStats{3, 22, 246 + 22, 2, 0, 768}, + }, + { + name: "Ads_folder_with_dir_streams", + src: TestDir{ + "dir": TestDir{ + ":Stream1:$DATA": TestFile{Content: string("stream 1")}, + ":Stream2:$DATA": TestFile{Content: string("stream 2")}, + }, + }, + targets: []string{"dir"}, + want: TestDir{ + "dir": TestDir{}, + "dir:Stream1:$DATA": TestFile{Content: string("stream 1")}, + "dir:Stream2:$DATA": TestFile{Content: string("stream 2")}, + }, + stat: ItemStats{2, 16, 164 + 16, 2, 0, 563}, + }, + { + name: "single_Ads_file", + src: TestDir{ + "targetfile.txt": TestFile{Content: string("foobar")}, + "targetfile.txt:Stream1:$DATA": TestFile{Content: string("stream 1")}, + "targetfile.txt:Stream2:$DATA": TestFile{Content: string("stream 2")}, + }, + targets: []string{"targetfile.txt"}, + stat: ItemStats{3, 22, 246 + 22, 1, 0, 457}, + }, + { + name: "Ads_all_types", + src: TestDir{ + "dir": TestDir{ + "adsfile.txt": TestFile{Content: string("foobar")}, + "adsfile.txt:Stream1:$DATA": TestFile{Content: string("stream 1")}, + "adsfile.txt:Stream2:$DATA": TestFile{Content: string("stream 2")}, + ":dirstream1:$DATA": TestFile{Content: string("stream 3")}, + ":dirstream2:$DATA": TestFile{Content: string("stream 4")}, + }, + "targetfile.txt": TestFile{Content: string("foobar")}, + "targetfile.txt:Stream1:$DATA": TestFile{Content: string("stream 1")}, + "targetfile.txt:Stream2:$DATA": TestFile{Content: string("stream 2")}, + }, + want: TestDir{ + "dir": TestDir{ + "adsfile.txt": TestFile{Content: string("foobar")}, + "adsfile.txt:Stream1:$DATA": TestFile{Content: string("stream 1")}, + "adsfile.txt:Stream2:$DATA": TestFile{Content: string("stream 2")}, + }, + "dir:dirstream1:$DATA": TestFile{Content: string("stream 3")}, + "dir:dirstream2:$DATA": TestFile{Content: string("stream 4")}, + "targetfile.txt": TestFile{Content: string("foobar")}, + "targetfile.txt:Stream1:$DATA": TestFile{Content: string("stream 1")}, + "targetfile.txt:Stream2:$DATA": TestFile{Content: string("stream 2")}, + }, + targets: []string{"targetfile.txt", "dir"}, + stat: ItemStats{5, 38, 410 + 38, 2, 0, 1133}, + }, + { + name: "Ads_directory_exclusion", + src: TestDir{ + "dir": TestDir{ + "adsfile.txt": TestFile{Content: string("foobar")}, + "adsfile.txt:Stream1:$DATA": TestFile{Content: string("stream 1")}, + "adsfile.txt:Stream2:$DATA": TestFile{Content: string("stream 2")}, + ":dirstream1:$DATA": TestFile{Content: string("stream 3")}, + ":dirstream2:$DATA": TestFile{Content: string("stream 4")}, + }, + "targetfile.txt": TestFile{Content: string("foobar")}, + "targetfile.txt:Stream1:$DATA": TestFile{Content: string("stream 1")}, + "targetfile.txt:Stream2:$DATA": TestFile{Content: string("stream 2")}, + }, + want: TestDir{ + "targetfile.txt": TestFile{Content: string("foobar")}, + "targetfile.txt:Stream1:$DATA": TestFile{Content: string("stream 1")}, + "targetfile.txt:Stream2:$DATA": TestFile{Content: string("stream 2")}, + }, + targets: []string{"targetfile.txt", "dir"}, + exclude: []string{"*\\dir*"}, + stat: ItemStats{3, 22, 268, 1, 0, 1133}, + }, + { + name: "Ads_backup_file_exclusion", + src: TestDir{ + "dir": TestDir{ + "adsfile.txt": TestFile{Content: string("foobar")}, + "adsfile.txt:Stream1:$DATA": TestFile{Content: string("stream 1")}, + "adsfile.txt:Stream2:$DATA": TestFile{Content: string("stream 2")}, + ":dirstream1:$DATA": TestFile{Content: string("stream 3")}, + ":dirstream2:$DATA": TestFile{Content: string("stream 4")}, + }, + "targetfile.txt": TestFile{Content: string("foobar")}, + "targetfile.txt:Stream1:$DATA": TestFile{Content: string("stream 1")}, + "targetfile.txt:Stream2:$DATA": TestFile{Content: string("stream 2")}, + }, + want: TestDir{ + "dir": TestDir{}, + "dir:dirstream1:$DATA": TestFile{Content: string("stream 3")}, + "dir:dirstream2:$DATA": TestFile{Content: string("stream 4")}, + "targetfile.txt": TestFile{Content: string("foobar")}, + "targetfile.txt:Stream1:$DATA": TestFile{Content: string("stream 1")}, + "targetfile.txt:Stream2:$DATA": TestFile{Content: string("stream 2")}, + }, + targets: []string{"targetfile.txt", "dir"}, + exclude: []string{"*\\dir\\adsfile.txt"}, + stat: ItemStats{5, 38, 448, 2, 0, 2150}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tempdir, repo := prepareTempdirRepoSrc(t, test.src) + + testFS := fs.Track{FS: fs.Local{}} + + arch := New(repo, testFS, Options{}) + + if len(test.exclude) != 0 { + parsedPatterns := filter.ParsePatterns(test.exclude) + arch.SelectByName = func(item string) bool { + //if + if matched, err := filter.List(parsedPatterns, item); err == nil && matched { + return false + } else { + return true + } + + } + } + + var stat *ItemStats = &ItemStats{} + lock := &sync.Mutex{} + + arch.CompleteItem = func(item string, previous, current *restic.Node, s ItemStats, d time.Duration) { + lock.Lock() + defer lock.Unlock() + stat.Add(s) + } + back := rtest.Chdir(t, tempdir) + defer back() + + var targets []string + for _, target := range test.targets { + targets = append(targets, os.ExpandEnv(target)) + } + + sn, snapshotID, _, err := arch.Snapshot(ctx, targets, SnapshotOptions{Time: time.Now(), Excludes: test.exclude}) + if err != nil { + t.Fatal(err) + } + + t.Logf("saved as %v", snapshotID.Str()) + + want := test.want + if want == nil { + want = test.src + } + + TestEnsureSnapshot(t, repo, snapshotID, want) + + checker.TestCheckRepo(t, repo, false) + + // check that the snapshot contains the targets with absolute paths + for i, target := range sn.Paths { + atarget, err := filepath.Abs(test.targets[i]) + if err != nil { + t.Fatal(err) + } + + if target != atarget { + t.Errorf("wrong path in snapshot: want %v, got %v", atarget, target) + } + } + + rtest.Equals(t, uint64(test.stat.DataBlobs), uint64(stat.DataBlobs)) + rtest.Equals(t, uint64(test.stat.TreeBlobs), uint64(stat.TreeBlobs)) + rtest.Equals(t, test.stat.DataSize, stat.DataSize) + rtest.Equals(t, test.stat.DataSizeInRepo, stat.DataSizeInRepo) + }) + } +} diff --git a/internal/archiver/testing.go b/internal/archiver/testing.go index e555a70d6..0b9fcad05 100644 --- a/internal/archiver/testing.go +++ b/internal/archiver/testing.go @@ -86,11 +86,11 @@ func TestCreateFiles(t testing.TB, target string, dir TestDir) { for _, name := range names { item := dir[name] - targetPath := filepath.Join(target, name) + targetPath := getTargetPath(target, name) switch it := item.(type) { case TestFile: - err := os.WriteFile(targetPath, []byte(it.Content), 0644) + err := writeFile(t, targetPath, it.Content) if err != nil { t.Fatal(err) } diff --git a/internal/archiver/testing_unix.go b/internal/archiver/testing_unix.go new file mode 100644 index 000000000..3a328a291 --- /dev/null +++ b/internal/archiver/testing_unix.go @@ -0,0 +1,20 @@ +//go:build !windows +// +build !windows + +package archiver + +import ( + "os" + "path/filepath" + "testing" +) + +// getTargetPath gets the target path from the target and the name +func getTargetPath(target string, name string) (targetPath string) { + return filepath.Join(target, name) +} + +// writeFile writes the content to the file at the targetPath +func writeFile(_ testing.TB, targetPath string, content string) (err error) { + return os.WriteFile(targetPath, []byte(content), 0644) +} diff --git a/internal/archiver/testing_windows.go b/internal/archiver/testing_windows.go new file mode 100644 index 000000000..c0b5beb50 --- /dev/null +++ b/internal/archiver/testing_windows.go @@ -0,0 +1,43 @@ +package archiver + +import ( + "os" + "path/filepath" + "testing" + + "github.com/restic/restic/internal/fs" +) + +// getTargetPath gets the target path from the target and the name +func getTargetPath(target string, name string) (targetPath string) { + if name[0] == ':' { + // If the first char of the name is :, append the name to the targetPath. + // This switch is useful for cases like creating directories having ads attributes attached. + // Without this, if we put the directory ads creation at top level, eg. "dir" and "dir:dirstream1:$DATA", + // since they can be created in any order it could first create an empty file called "dir" with the ads + // stream and then the dir creation fails. + targetPath = target + name + } else { + targetPath = filepath.Join(target, name) + } + return targetPath +} + +// writeFile writes the content to the file at the targetPath +func writeFile(t testing.TB, targetPath string, content string) (err error) { + //For windows, create file only if it doesn't exist. Otherwise ads streams may get overwritten. + f, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_TRUNC, 0644) + + if os.IsNotExist(err) { + f, err = os.OpenFile(targetPath, os.O_WRONLY|fs.O_CREATE|os.O_TRUNC, 0644) + } + + if err != nil { + t.Fatal(err) + } + _, err = f.Write([]byte(content)) + if err1 := f.Close(); err1 != nil && err == nil { + err = err1 + } + return err +} diff --git a/internal/restic/ads_windows_test.go b/internal/restic/ads_windows_test.go new file mode 100644 index 000000000..ab225c3ca --- /dev/null +++ b/internal/restic/ads_windows_test.go @@ -0,0 +1,187 @@ +//go:build windows +// +build windows + +package restic + +import ( + "math/rand" + "os" + "strconv" + "sync" + "testing" + + rtest "github.com/restic/restic/internal/test" +) + +var ( + testFileName = "TestingAds.txt" + testFilePath string + adsFileName = ":AdsName" + testData = "This is the main data stream." + testDataAds = "This is an alternate data stream " + goWG sync.WaitGroup + dataSize int +) + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +func TestAdsFile(t *testing.T) { + // create a temp test file + + for i := 0; i < 5; i++ { + dataSize = 10000000 * i + testData = testData + randStringBytesRmndr(dataSize) + testDataAds = testDataAds + randStringBytesRmndr(dataSize) + //Testing with multiple ads streams in sequence. + testAdsForCount(i, t) + } + +} + +func testAdsForCount(adsTestCount int, t *testing.T) { + makeTestFile(adsTestCount) + defer os.Remove(testFilePath) + + success, streams, errGA := GetADStreamNames(testFilePath) + + rtest.Assert(t, success, "GetADStreamNames status. error: %v", errGA) + rtest.Assert(t, len(streams) == adsTestCount, "Stream found: %v", streams) + + adsCount := len(streams) + + goWG.Add(1) + + go ReadMain(t) + + goWG.Add(adsCount) + for i := 0; i < adsCount; i++ { + //Writing ADS to the file concurrently + go ReadAds(i, t) + } + goWG.Wait() + os.Remove(testFilePath) +} + +func ReadMain(t *testing.T) { + defer goWG.Done() + data, errR := os.ReadFile(testFilePath) + rtest.OK(t, errR) + dataString := string(data) + rtest.Assert(t, dataString == testData, "Data read: %v", len(dataString)) +} + +func ReadAds(i int, t *testing.T) { + defer goWG.Done() + dataAds, errAds := os.ReadFile(testFilePath + adsFileName + strconv.Itoa(i)) + rtest.OK(t, errAds) + + rtest.Assert(t, errAds == nil, "GetADStreamNames status. error: %v", errAds) + dataStringAds := string(dataAds) + rtest.Assert(t, dataStringAds == testDataAds+strconv.Itoa(i)+".\n", "Ads Data read: %v", len(dataStringAds)) +} + +func makeTestFile(adsCount int) error { + f, err := os.CreateTemp("", testFileName) + if err != nil { + panic(err) + } + testFilePath = f.Name() + + defer f.Close() + if adsCount == 0 || adsCount == 1 { + goWG.Add(1) + //Writing main file + go WriteMain(err, f) + } + + goWG.Add(adsCount) + for i := 0; i < adsCount; i++ { + //Writing ADS to the file concurrently while main file also gets written + go WriteADS(i) + if i == 1 { + //Testing some cases where the main file writing may start after the ads streams writing has started. + //These cases are tested when adsCount > 1. In this case we start writing the main file after starting to write ads. + goWG.Add(1) + go WriteMain(err, f) + } + } + goWG.Wait() + return nil +} + +func WriteMain(err error, f *os.File) (bool, error) { + defer goWG.Done() + + _, err1 := f.Write([]byte(testData)) + if err1 != nil { + return true, err + } + return false, err +} + +func WriteADS(i int) (bool, error) { + defer goWG.Done() + a, err := os.Create(testFilePath + adsFileName + strconv.Itoa(i)) + if err != nil { + return true, err + } + defer a.Close() + + _, err = a.Write([]byte(testDataAds + strconv.Itoa(i) + ".\n")) + if err != nil { + return true, err + } + return false, nil +} + +func randStringBytesRmndr(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))] + } + return string(b) +} + +func TestTrimAds(t *testing.T) { + tests := []struct { + input string + output string + }{ + {input: "d:\\test.txt:stream1:$DATA", output: "d:\\test.txt"}, + {input: "test.txt:stream1:$DATA", output: "test.txt"}, + {input: "test.txt", output: "test.txt"}, + {input: "\\abc\\test.txt:stream1:$DATA", output: "\\abc\\test.txt"}, + {input: "\\abc\\", output: "\\abc\\"}, + {input: "\\", output: "\\"}, + } + + for _, test := range tests { + + t.Run("", func(t *testing.T) { + output := TrimAds(test.input) + rtest.Equals(t, test.output, output) + }) + } +} + +func TestIsAds(t *testing.T) { + tests := []struct { + input string + result bool + }{ + {input: "d:\\test.txt:stream1:$DATA", result: true}, + {input: "test.txt:stream1:$DATA", result: true}, + {input: "test.txt", result: false}, + {input: "\\abc\\test.txt:stream1:$DATA", result: true}, + {input: "\\abc\\", result: false}, + {input: "\\", result: false}, + } + + for _, test := range tests { + + t.Run("", func(t *testing.T) { + output := IsAds(test.input) + rtest.Equals(t, test.result, output) + }) + } +} diff --git a/internal/restorer/fileswriter_test.go b/internal/restorer/fileswriter_test.go index 9ea8767b8..948cb1d3b 100644 --- a/internal/restorer/fileswriter_test.go +++ b/internal/restorer/fileswriter_test.go @@ -18,16 +18,16 @@ func TestFilesWriterBasic(t *testing.T) { f1 := dir + "/f1" f2 := dir + "/f2" - rtest.OK(t, w.writeToFile(f1, []byte{1}, 0, 2, false)) + rtest.OK(t, w.writeToFile(f1, []byte{1}, 0, 2, &fileInfo{})) rtest.Equals(t, 0, len(w.buckets[0].files)) - rtest.OK(t, w.writeToFile(f2, []byte{2}, 0, 2, false)) + rtest.OK(t, w.writeToFile(f2, []byte{2}, 0, 2, &fileInfo{})) rtest.Equals(t, 0, len(w.buckets[0].files)) - rtest.OK(t, w.writeToFile(f1, []byte{1}, 1, -1, false)) + rtest.OK(t, w.writeToFile(f1, []byte{1}, 1, -1, &fileInfo{})) rtest.Equals(t, 0, len(w.buckets[0].files)) - rtest.OK(t, w.writeToFile(f2, []byte{2}, 1, -1, false)) + rtest.OK(t, w.writeToFile(f2, []byte{2}, 1, -1, &fileInfo{})) rtest.Equals(t, 0, len(w.buckets[0].files)) buf, err := os.ReadFile(f1) @@ -48,13 +48,13 @@ func TestFilesWriterRecursiveOverwrite(t *testing.T) { // must error if recursive delete is not allowed w := newFilesWriter(1, false) - err := w.writeToFile(path, []byte{1}, 0, 2, false) + err := w.writeToFile(path, []byte{1}, 0, 2, &fileInfo{}) rtest.Assert(t, errors.Is(err, notEmptyDirError()), "unexpected error got %v", err) rtest.Equals(t, 0, len(w.buckets[0].files)) // must replace directory w = newFilesWriter(1, true) - rtest.OK(t, w.writeToFile(path, []byte{1, 1}, 0, 2, false)) + rtest.OK(t, w.writeToFile(path, []byte{1, 1}, 0, 2, &fileInfo{})) rtest.Equals(t, 0, len(w.buckets[0].files)) buf, err := os.ReadFile(path) @@ -133,7 +133,7 @@ func TestCreateFile(t *testing.T) { for j, test := range tests { path := basepath + fmt.Sprintf("%v%v", i, j) sc.create(t, path) - f, err := createFile(path, test.size, test.isSparse, false) + f, err := createOrOpenFile(path, test.size, &fileInfo{sparse: test.isSparse}, false) if sc.err == nil { rtest.OK(t, err) fi, err := f.Stat() @@ -161,7 +161,7 @@ func TestCreateFileRecursiveDelete(t *testing.T) { rtest.OK(t, os.WriteFile(filepath.Join(path, "file"), []byte("data"), 0o400)) // replace it - f, err := createFile(path, 42, false, true) + f, err := createOrOpenFile(path, 42, &fileInfo{sparse: false}, true) rtest.OK(t, err) fi, err := f.Stat() rtest.OK(t, err) diff --git a/internal/restorer/restorer_test.go b/internal/restorer/restorer_test.go index e0306ce01..012afca84 100644 --- a/internal/restorer/restorer_test.go +++ b/internal/restorer/restorer_test.go @@ -27,7 +27,11 @@ import ( "golang.org/x/sync/errgroup" ) -type Node interface{} +type Node interface { + IsAds() bool + HasAds() bool + Attributes() *FileAttributes +} type Snapshot struct { Nodes map[string]Node @@ -41,6 +45,20 @@ type File struct { Mode os.FileMode ModTime time.Time attributes *FileAttributes + isAds bool + hasAds bool +} + +func (f File) IsAds() bool { + return f.isAds +} + +func (f File) HasAds() bool { + return f.hasAds +} + +func (f File) Attributes() *FileAttributes { + return f.attributes } type Symlink struct { @@ -48,11 +66,38 @@ type Symlink struct { ModTime time.Time } +func (s Symlink) IsAds() bool { + return false +} + +func (s Symlink) HasAds() bool { + return false +} + +func (s Symlink) Attributes() *FileAttributes { + return nil +} + type Dir struct { Nodes map[string]Node Mode os.FileMode ModTime time.Time attributes *FileAttributes + isAds bool + hasAds bool +} + +func (_ Dir) IsAds() bool { + // Dir itself can not be an ADS + return false +} + +func (d Dir) HasAds() bool { + return d.hasAds +} + +func (d Dir) Attributes() *FileAttributes { + return d.attributes } type FileAttributes struct { @@ -75,7 +120,9 @@ func saveFile(t testing.TB, repo restic.BlobSaver, data string) restic.ID { return id } -func saveDir(t testing.TB, repo restic.BlobSaver, nodes map[string]Node, inode uint64, getGenericAttributes func(attr *FileAttributes, isDir bool) (genericAttributes map[restic.GenericAttributeType]json.RawMessage)) restic.ID { +func saveDir(t testing.TB, repo restic.BlobSaver, nodes map[string]Node, inode uint64, + getFileAttributes func(attr *FileAttributes, isDir bool) (fileAttributes map[restic.GenericAttributeType]json.RawMessage), + getAdsAttributes func(path string, hasAds bool, isAds bool) (genericAttributes map[restic.GenericAttributeType]json.RawMessage)) restic.ID { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -107,6 +154,8 @@ func saveDir(t testing.TB, repo restic.BlobSaver, nodes map[string]Node, inode u if mode == 0 { mode = 0644 } + genericAttributes := getGenericAttributes(name, node, getFileAttributes, getAdsAttributes) + err := tree.Insert(&restic.Node{ Type: restic.NodeTypeFile, Mode: mode, @@ -118,7 +167,7 @@ func saveDir(t testing.TB, repo restic.BlobSaver, nodes map[string]Node, inode u Size: uint64(size), Inode: fi, Links: lc, - GenericAttributes: getGenericAttributes(node.attributes, false), + GenericAttributes: genericAttributes, }) rtest.OK(t, err) case Symlink: @@ -135,7 +184,7 @@ func saveDir(t testing.TB, repo restic.BlobSaver, nodes map[string]Node, inode u }) rtest.OK(t, err) case Dir: - id := saveDir(t, repo, node.Nodes, inode, getGenericAttributes) + id := saveDir(t, repo, node.Nodes, inode, getFileAttributes, getAdsAttributes) mode := node.Mode if mode == 0 { @@ -150,14 +199,26 @@ func saveDir(t testing.TB, repo restic.BlobSaver, nodes map[string]Node, inode u UID: uint32(os.Getuid()), GID: uint32(os.Getgid()), Subtree: &id, - GenericAttributes: getGenericAttributes(node.attributes, false), + GenericAttributes: getGenericAttributes(name, node, getFileAttributes, getAdsAttributes), }) rtest.OK(t, err) default: t.Fatalf("unknown node type %T", node) } } - + // // Before saving the tree, ensure any raw JSON messages are properly encoded + // for _, v := range tree.Nodes { + // if strings.Contains(v.Name, ":") { + // // Escape any invalid JSON by converting to string and escaping special characters + // v.Name = strings.ReplaceAll(v.Name, ":", "") + // } + // for k2, v2 := range v.GenericAttributes { + // if strings.Contains(string(v2), ":") { + // // Escape any invalid JSON by converting to string and escaping special characters + // v.GenericAttributes[k2] = []byte(strings.ReplaceAll(string(v2), ":", "\\:")) + // } + // } + // } id, err := restic.SaveTree(ctx, repo, tree) if err != nil { t.Fatal(err) @@ -166,13 +227,28 @@ func saveDir(t testing.TB, repo restic.BlobSaver, nodes map[string]Node, inode u return id } -func saveSnapshot(t testing.TB, repo restic.Repository, snapshot Snapshot, getGenericAttributes func(attr *FileAttributes, isDir bool) (genericAttributes map[restic.GenericAttributeType]json.RawMessage)) (*restic.Snapshot, restic.ID) { +func getGenericAttributes(name string, node Node, getFileAttributes func(attr *FileAttributes, isDir bool) map[restic.GenericAttributeType]json.RawMessage, + getAdsAttributes func(path string, hasAds bool, isAds bool) map[restic.GenericAttributeType]json.RawMessage) map[restic.GenericAttributeType]json.RawMessage { + genericAttributes := getFileAttributes(node.Attributes(), false) + if node.HasAds() || node.IsAds() { + if genericAttributes == nil { + genericAttributes = map[restic.GenericAttributeType]json.RawMessage{} + } + for k, v := range getAdsAttributes(name, node.HasAds(), node.IsAds()) { + genericAttributes[k] = v + } + } + return genericAttributes +} + +func saveSnapshot(t testing.TB, repo restic.Repository, snapshot Snapshot, getFileAttributes func(attr *FileAttributes, isDir bool) map[restic.GenericAttributeType]json.RawMessage, + getAdsAttributes func(path string, hasAds bool, isAds bool) map[restic.GenericAttributeType]json.RawMessage) (*restic.Snapshot, restic.ID) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() wg, wgCtx := errgroup.WithContext(ctx) repo.StartPackUploader(wgCtx, wg) - treeID := saveDir(t, repo, snapshot.Nodes, 1000, getGenericAttributes) + treeID := saveDir(t, repo, snapshot.Nodes, 1000, getFileAttributes, getAdsAttributes) err := repo.Flush(ctx) if err != nil { t.Fatal(err) @@ -192,7 +268,12 @@ func saveSnapshot(t testing.TB, repo restic.Repository, snapshot Snapshot, getGe return sn, id } -var noopGetGenericAttributes = func(attr *FileAttributes, isDir bool) (genericAttributes map[restic.GenericAttributeType]json.RawMessage) { +var noopGetFileAttributes = func(attr *FileAttributes, isDir bool) (fileAttributes map[restic.GenericAttributeType]json.RawMessage) { + // No-op + return nil +} + +var noopGetAdsAttributes = func(path string, hasAds bool, isAds bool) (adsAttribute map[restic.GenericAttributeType]json.RawMessage) { // No-op return nil } @@ -372,7 +453,7 @@ func TestRestorer(t *testing.T) { for _, test := range tests { t.Run("", func(t *testing.T) { repo := repository.TestRepository(t) - sn, id := saveSnapshot(t, repo, test.Snapshot, noopGetGenericAttributes) + sn, id := saveSnapshot(t, repo, test.Snapshot, noopGetFileAttributes, noopGetAdsAttributes) t.Logf("snapshot saved as %v", id.Str()) res := NewRestorer(repo, sn, Options{}) @@ -483,7 +564,7 @@ func TestRestorerRelative(t *testing.T) { t.Run("", func(t *testing.T) { repo := repository.TestRepository(t) - sn, id := saveSnapshot(t, repo, test.Snapshot, noopGetGenericAttributes) + sn, id := saveSnapshot(t, repo, test.Snapshot, noopGetFileAttributes, noopGetAdsAttributes) t.Logf("snapshot saved as %v", id.Str()) res := NewRestorer(repo, sn, Options{}) @@ -746,7 +827,7 @@ func TestRestorerTraverseTree(t *testing.T) { for _, test := range tests { t.Run("", func(t *testing.T) { repo := repository.TestRepository(t) - sn, _ := saveSnapshot(t, repo, test.Snapshot, noopGetGenericAttributes) + sn, _ := saveSnapshot(t, repo, test.Snapshot, noopGetFileAttributes, noopGetAdsAttributes) // set Delete option to enable tracking filenames in a directory res := NewRestorer(repo, sn, Options{Delete: true}) @@ -823,7 +904,7 @@ func TestRestorerConsistentTimestampsAndPermissions(t *testing.T) { }, }, }, - }, noopGetGenericAttributes) + }, noopGetFileAttributes, noopGetAdsAttributes) res := NewRestorer(repo, sn, Options{}) @@ -878,7 +959,7 @@ func TestVerifyCancel(t *testing.T) { } repo := repository.TestRepository(t) - sn, _ := saveSnapshot(t, repo, snapshot, noopGetGenericAttributes) + sn, _ := saveSnapshot(t, repo, snapshot, noopGetFileAttributes, noopGetAdsAttributes) res := NewRestorer(repo, sn, Options{}) @@ -961,7 +1042,7 @@ func saveSnapshotsAndOverwrite(t *testing.T, baseSnapshot Snapshot, overwriteSna defer cancel() // base snapshot - sn, id := saveSnapshot(t, repo, baseSnapshot, noopGetGenericAttributes) + sn, id := saveSnapshot(t, repo, baseSnapshot, noopGetFileAttributes, noopGetAdsAttributes) t.Logf("base snapshot saved as %v", id.Str()) res := NewRestorer(repo, sn, baseOptions) @@ -969,7 +1050,7 @@ func saveSnapshotsAndOverwrite(t *testing.T, baseSnapshot Snapshot, overwriteSna rtest.OK(t, err) // overwrite snapshot - sn, id = saveSnapshot(t, repo, overwriteSnapshot, noopGetGenericAttributes) + sn, id = saveSnapshot(t, repo, overwriteSnapshot, noopGetFileAttributes, noopGetAdsAttributes) t.Logf("overwrite snapshot saved as %v", id.Str()) res = NewRestorer(repo, sn, overwriteOptions) countRestoredFiles, err := res.RestoreTo(ctx, tempdir) @@ -1252,7 +1333,7 @@ func TestRestoreModified(t *testing.T) { defer cancel() for _, snapshot := range snapshots { - sn, id := saveSnapshot(t, repo, snapshot, noopGetGenericAttributes) + sn, id := saveSnapshot(t, repo, snapshot, noopGetFileAttributes, noopGetAdsAttributes) t.Logf("snapshot saved as %v", id.Str()) res := NewRestorer(repo, sn, Options{Overwrite: OverwriteIfChanged}) @@ -1279,7 +1360,7 @@ func TestRestoreIfChanged(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - sn, id := saveSnapshot(t, repo, snapshot, noopGetGenericAttributes) + sn, id := saveSnapshot(t, repo, snapshot, noopGetFileAttributes, noopGetAdsAttributes) t.Logf("snapshot saved as %v", id.Str()) res := NewRestorer(repo, sn, Options{}) @@ -1336,7 +1417,7 @@ func TestRestoreDryRun(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - sn, id := saveSnapshot(t, repo, snapshot, noopGetGenericAttributes) + sn, id := saveSnapshot(t, repo, snapshot, noopGetFileAttributes, noopGetAdsAttributes) t.Logf("snapshot saved as %v", id.Str()) res := NewRestorer(repo, sn, Options{DryRun: true}) @@ -1365,7 +1446,7 @@ func TestRestoreDryRunDelete(t *testing.T) { rtest.OK(t, err) rtest.OK(t, f.Close()) - sn, _ := saveSnapshot(t, repo, snapshot, noopGetGenericAttributes) + sn, _ := saveSnapshot(t, repo, snapshot, noopGetFileAttributes, noopGetAdsAttributes) res := NewRestorer(repo, sn, Options{DryRun: true, Delete: true}) _, err = res.RestoreTo(ctx, tempdir) rtest.OK(t, err) @@ -1417,7 +1498,7 @@ func TestRestoreDelete(t *testing.T) { }, "anotherfile": File{Data: "content: file\n"}, }, - }, noopGetGenericAttributes) + }, noopGetFileAttributes, noopGetAdsAttributes) // should delete files that no longer exist in the snapshot deleteSn, _ := saveSnapshot(t, repo, Snapshot{ @@ -1429,7 +1510,7 @@ func TestRestoreDelete(t *testing.T) { }, }, }, - }, noopGetGenericAttributes) + }, noopGetFileAttributes, noopGetAdsAttributes) tests := []struct { selectFilter func(item string, isDir bool) (selectedForRestore bool, childMayBeSelected bool) @@ -1524,7 +1605,7 @@ func TestRestoreToFile(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - sn, _ := saveSnapshot(t, repo, snapshot, noopGetGenericAttributes) + sn, _ := saveSnapshot(t, repo, snapshot, noopGetFileAttributes, noopGetAdsAttributes) res := NewRestorer(repo, sn, Options{}) _, err := res.RestoreTo(ctx, tempdir) rtest.Assert(t, strings.Contains(err.Error(), "cannot create target directory"), "unexpected error %v", err) diff --git a/internal/restorer/restorer_unix_test.go b/internal/restorer/restorer_unix_test.go index c4e8149b2..a25d8ee40 100644 --- a/internal/restorer/restorer_unix_test.go +++ b/internal/restorer/restorer_unix_test.go @@ -29,7 +29,7 @@ func TestRestorerRestoreEmptyHardlinkedFields(t *testing.T) { }, }, }, - }, noopGetGenericAttributes) + }, noopGetFileAttributes, noopGetAdsAttributes) res := NewRestorer(repo, sn, Options{}) @@ -86,7 +86,7 @@ func testRestorerProgressBar(t *testing.T, dryRun bool) { }, "file2": File{Links: 1, Inode: 2, Data: "example"}, }, - }, noopGetGenericAttributes) + }, noopGetFileAttributes, noopGetAdsAttributes) mock := &printerMock{} progress := restoreui.NewProgress(mock, 0) @@ -122,7 +122,7 @@ func TestRestorePermissions(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - sn, id := saveSnapshot(t, repo, snapshot, noopGetGenericAttributes) + sn, id := saveSnapshot(t, repo, snapshot, noopGetFileAttributes, noopGetAdsAttributes) t.Logf("snapshot saved as %v", id.Str()) res := NewRestorer(repo, sn, Options{}) diff --git a/internal/restorer/restorer_windows_test.go b/internal/restorer/restorer_windows_test.go index 4764bed2d..27e671ab4 100644 --- a/internal/restorer/restorer_windows_test.go +++ b/internal/restorer/restorer_windows_test.go @@ -9,7 +9,7 @@ import ( "math" "os" "path" - "path/filepath" + "strings" "syscall" "testing" "time" @@ -18,11 +18,17 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" - "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test" + restoreui "github.com/restic/restic/internal/ui/restore" "golang.org/x/sys/windows" ) +// Index of the main file stream for testing streams when the restoration order is different. +// This is mainly used to test scenarios in which the main file stream is restored after restoring another stream. +// Handling this scenario is important as creating the main file will usually replace the existing streams. +// '-1' is used because this will allow us to handle the ads stream indexes as they are. We just need to insert the main file index in the place we want it to be restored. +const MAIN_STREAM_ORDER_INDEX = -1 + func getBlockCount(t *testing.T, filename string) int64 { libkernel32 := windows.NewLazySystemDLL("kernel32.dll") err := libkernel32.Load() @@ -49,12 +55,15 @@ type DataStreamInfo struct { data string } -type NodeInfo struct { - DataStreamInfo - parentDir string - attributes FileAttributes - Exists bool - IsDirectory bool +type NodeTestInfo struct { + DataStreamInfo //The main data stream of the file + parentDir string + attributes *FileAttributes + IsDirectory bool + //The order for restoration of streams in Ads streams + //We also includes main stream index (-1) to the order to indcate when the main file should be restored. + StreamRestoreOrder []int + AdsStreams []DataStreamInfo //Alternate streams of the node } func TestFileAttributeCombination(t *testing.T) { @@ -71,27 +80,7 @@ func testFileAttributeCombination(t *testing.T, isEmpty bool) { attributeCombinations := generateCombinations(5, []bool{}) fileName := "TestFile.txt" - // Iterate through each attribute combination - for _, attr1 := range attributeCombinations { - - //Set up the required file information - fileInfo := NodeInfo{ - DataStreamInfo: getDataStreamInfo(isEmpty, fileName), - parentDir: "dir", - attributes: getFileAttributes(attr1), - Exists: false, - } - - //Get the current test name - testName := getCombinationTestName(fileInfo, fileName, fileInfo.attributes) - - //Run test - t.Run(testName, func(t *testing.T) { - mainFilePath := runAttributeTests(t, fileInfo, fileInfo.attributes) - - verifyFileRestores(isEmpty, mainFilePath, t, fileInfo) - }) - } + testAttributeCombinations(t, attributeCombinations, fileName, isEmpty, false, false, NodeTestInfo{}) } func generateCombinations(n int, prefix []bool) [][]bool { @@ -112,23 +101,68 @@ func generateCombinations(n int, prefix []bool) [][]bool { return append(permsTrue, permsFalse...) } -func getDataStreamInfo(isEmpty bool, fileName string) DataStreamInfo { +func testAttributeCombinations(t *testing.T, attributeCombinations [][]bool, nodeName string, isEmpty, isDirectory, createExisting bool, existingNode NodeTestInfo) { + // Iterate through each attribute combination + for _, attr1 := range attributeCombinations { + + //Set up the node that needs to be restored + nodeInfo := NodeTestInfo{ + DataStreamInfo: getDummyDataStream(isEmpty || isDirectory, nodeName, false), + parentDir: "dir", + attributes: convertToFileAttributes(attr1, isDirectory), + IsDirectory: isDirectory, + } + + //Get the current test name + testName := getCombinationTestName(nodeInfo, nodeName, createExisting, existingNode) + + //Run test + t.Run(testName, func(t *testing.T) { + + // run the test and verify attributes + mainPath := runAttributeTests(t, nodeInfo, createExisting, existingNode) + + //verify node restoration + verifyRestores(t, isEmpty || isDirectory, mainPath, nodeInfo.DataStreamInfo) + }) + } +} + +func getDummyDataStream(isEmptyOrDirectory bool, mainStreamName string, isExisting bool) DataStreamInfo { var dataStreamInfo DataStreamInfo - if isEmpty { + + // Set only the name if the node is empty or is a directory. + if isEmptyOrDirectory { dataStreamInfo = DataStreamInfo{ - name: fileName, + name: mainStreamName, } } else { + data := "Main file data stream." + if isExisting { + //Use a differnt data for existing files + data = "Existing file data" + } dataStreamInfo = DataStreamInfo{ - name: fileName, - data: "Main file data stream.", + name: mainStreamName, + data: data, } } return dataStreamInfo } -func getFileAttributes(values []bool) FileAttributes { - return FileAttributes{ +// Convert boolean values to file attributes +func convertToFileAttributes(values []bool, isDirectory bool) *FileAttributes { + if isDirectory { + return &FileAttributes{ + // readonly not valid for directories + Hidden: values[0], + System: values[1], + Archive: values[2], + Encrypted: values[3], + } + } + + return &FileAttributes{ ReadOnly: values[0], Hidden: values[1], System: values[2], @@ -137,7 +171,8 @@ func getFileAttributes(values []bool) FileAttributes { } } -func getCombinationTestName(fi NodeInfo, fileName string, overwriteAttr FileAttributes) string { +// generate name for the provide attribute combination +func getCombinationTestName(fi NodeTestInfo, fileName string, createExisiting bool, existingNode NodeTestInfo) string { if fi.attributes.ReadOnly { fileName += "-ReadOnly" } @@ -153,105 +188,61 @@ func getCombinationTestName(fi NodeInfo, fileName string, overwriteAttr FileAttr if fi.attributes.Encrypted { fileName += "-Encrypted" } - if fi.Exists { - fileName += "-Overwrite" - if overwriteAttr.ReadOnly { - fileName += "-R" - } - if overwriteAttr.Hidden { - fileName += "-H" - } - if overwriteAttr.System { - fileName += "-S" - } - if overwriteAttr.Archive { - fileName += "-A" - } - if overwriteAttr.Encrypted { - fileName += "-E" - } + if !createExisiting { + return fileName + } + + // Additonal name for existing file attributes test + fileName += "-Overwrite" + if existingNode.attributes.ReadOnly { + fileName += "-R" + } + if existingNode.attributes.Hidden { + fileName += "-H" + } + if existingNode.attributes.System { + fileName += "-S" + } + if existingNode.attributes.Archive { + fileName += "-A" + } + if existingNode.attributes.Encrypted { + fileName += "-E" } return fileName } -func runAttributeTests(t *testing.T, fileInfo NodeInfo, existingFileAttr FileAttributes) string { +func runAttributeTests(t *testing.T, fileInfo NodeTestInfo, createExisting bool, existingNodeInfo NodeTestInfo) string { testDir := t.TempDir() - res, _ := setupWithFileAttributes(t, fileInfo, testDir, existingFileAttr) + runRestorerTest(t, fileInfo, testDir, createExisting, existingNodeInfo) + + mainFilePath := path.Join(testDir, fileInfo.parentDir, fileInfo.name) + verifyAttributes(t, mainFilePath, fileInfo.attributes) + return mainFilePath +} + +func runRestorerTest(t *testing.T, nodeInfo NodeTestInfo, testDir string, createExisting bool, existingNodeInfo NodeTestInfo) { + res := setup(t, nodeInfo, testDir, createExisting, existingNodeInfo) ctx, cancel := context.WithCancel(context.Background()) defer cancel() _, err := res.RestoreTo(ctx, testDir) rtest.OK(t, err) - - mainFilePath := path.Join(testDir, fileInfo.parentDir, fileInfo.name) - //Verify restore - verifyFileAttributes(t, mainFilePath, fileInfo.attributes) - return mainFilePath } -func setupWithFileAttributes(t *testing.T, nodeInfo NodeInfo, testDir string, existingFileAttr FileAttributes) (*Restorer, []int) { +func setup(t *testing.T, nodeInfo NodeTestInfo, testDir string, createExisitingFile bool, existingNodeInfo NodeTestInfo) *Restorer { t.Helper() - if nodeInfo.Exists { - if !nodeInfo.IsDirectory { - err := os.MkdirAll(path.Join(testDir, nodeInfo.parentDir), os.ModeDir) - rtest.OK(t, err) - filepath := path.Join(testDir, nodeInfo.parentDir, nodeInfo.name) - if existingFileAttr.Encrypted { - err := createEncryptedFileWriteData(filepath, nodeInfo) - rtest.OK(t, err) - } else { - // Write the data to the file - file, err := os.OpenFile(path.Clean(filepath), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) - rtest.OK(t, err) - _, err = file.Write([]byte(nodeInfo.data)) - rtest.OK(t, err) - - err = file.Close() - rtest.OK(t, err) - } - } else { - err := os.MkdirAll(path.Join(testDir, nodeInfo.parentDir, nodeInfo.name), os.ModeDir) - rtest.OK(t, err) - } - - pathPointer, err := syscall.UTF16PtrFromString(path.Join(testDir, nodeInfo.parentDir, nodeInfo.name)) - rtest.OK(t, err) - syscall.SetFileAttributes(pathPointer, getAttributeValue(&existingFileAttr)) + if createExisitingFile { + createExisting(t, testDir, existingNodeInfo) } - index := 0 + if !nodeInfo.IsDirectory && nodeInfo.StreamRestoreOrder == nil { + nodeInfo.StreamRestoreOrder = []int{MAIN_STREAM_ORDER_INDEX} + } - order := []int{} - streams := []DataStreamInfo{} - if !nodeInfo.IsDirectory { - order = append(order, index) - index++ - streams = append(streams, nodeInfo.DataStreamInfo) - } - return setup(t, getNodes(nodeInfo.parentDir, nodeInfo.name, order, streams, nodeInfo.IsDirectory, &nodeInfo.attributes)), order -} + nodesMap := getNodes(nodeInfo.parentDir, nodeInfo) -func createEncryptedFileWriteData(filepath string, fileInfo NodeInfo) (err error) { - var ptr *uint16 - if ptr, err = windows.UTF16PtrFromString(filepath); err != nil { - return err - } - var handle windows.Handle - //Create the file with encrypted flag - if handle, err = windows.CreateFile(ptr, uint32(windows.GENERIC_READ|windows.GENERIC_WRITE), uint32(windows.FILE_SHARE_READ), nil, uint32(windows.CREATE_ALWAYS), windows.FILE_ATTRIBUTE_ENCRYPTED, 0); err != nil { - return err - } - //Write data to file - if _, err = windows.Write(handle, []byte(fileInfo.data)); err != nil { - return err - } - //Close handle - return windows.CloseHandle(handle) -} - -func setup(t *testing.T, nodesMap map[string]Node) *Restorer { - repo := repository.TestRepository(t) getFileAttributes := func(attr *FileAttributes, isDir bool) (genericAttributes map[restic.GenericAttributeType]json.RawMessage) { if attr == nil { return @@ -263,14 +254,43 @@ func setup(t *testing.T, nodesMap map[string]Node) *Restorer { //If the node is a directory add FILE_ATTRIBUTE_DIRECTORY to attributes fileattr |= windows.FILE_ATTRIBUTE_DIRECTORY } + attrs, err := restic.WindowsAttrsToGenericAttributes(restic.WindowsAttributes{FileAttributes: &fileattr}) - test.OK(t, err) + rtest.OK(t, err) return attrs } + + getAdsAttributes := func(path string, hasAds, isAds bool) map[restic.GenericAttributeType]json.RawMessage { + if isAds { + windowsAttr := restic.WindowsAttributes{ + IsADS: &isAds, + } + attrs, err := restic.WindowsAttrsToGenericAttributes(windowsAttr) + rtest.OK(t, err) + return attrs + } else if hasAds { + //Find ads names by recursively searching through nodes + //This is needed when multiple levels of parent directories are defined for ads file + adsNames := findAdsNamesRecursively(nodesMap, path, []string{}) + windowsAttr := restic.WindowsAttributes{ + HasADS: &adsNames, + } + attrs, err := restic.WindowsAttrsToGenericAttributes(windowsAttr) + rtest.OK(t, err) + return attrs + } else { + return map[restic.GenericAttributeType]json.RawMessage{} + } + } + + repo := repository.TestRepository(t) sn, _ := saveSnapshot(t, repo, Snapshot{ Nodes: nodesMap, - }, getFileAttributes) - res := NewRestorer(repo, sn, Options{}) + }, getFileAttributes, getAdsAttributes) + + mock := &printerMock{} + progress := restoreui.NewProgress(mock, 0) + res := NewRestorer(repo, sn, Options{Progress: progress}) return res } @@ -294,12 +314,63 @@ func getAttributeValue(attr *FileAttributes) uint32 { return fileattr } -func getNodes(dir string, mainNodeName string, order []int, streams []DataStreamInfo, isDirectory bool, attributes *FileAttributes) map[string]Node { +func createExisting(t *testing.T, testDir string, nodeInfo NodeTestInfo) { + //Create directory or file for testing with node already exist in the folder. + if !nodeInfo.IsDirectory { + err := os.MkdirAll(path.Join(testDir, nodeInfo.parentDir), os.ModeDir) + rtest.OK(t, err) + + filepath := path.Join(testDir, nodeInfo.parentDir, nodeInfo.name) + createTestFile(t, nodeInfo.attributes.Encrypted, filepath, nodeInfo.DataStreamInfo) + } else { + err := os.MkdirAll(path.Join(testDir, nodeInfo.parentDir, nodeInfo.name), os.ModeDir) + rtest.OK(t, err) + } + + //Create ads streams if any + if len(nodeInfo.AdsStreams) > 0 { + for _, stream := range nodeInfo.AdsStreams { + filepath := path.Join(testDir, nodeInfo.parentDir, stream.name) + createTestFile(t, nodeInfo.attributes.Encrypted, filepath, stream) + } + } + + //Set attributes + pathPointer, err := syscall.UTF16PtrFromString(path.Join(testDir, nodeInfo.parentDir, nodeInfo.name)) + rtest.OK(t, err) + + syscall.SetFileAttributes(pathPointer, getAttributeValue(nodeInfo.attributes)) +} + +func createTestFile(t *testing.T, isEncrypted bool, filepath string, stream DataStreamInfo) { + + var attribute uint32 = windows.FILE_ATTRIBUTE_NORMAL + if isEncrypted { + attribute = windows.FILE_ATTRIBUTE_ENCRYPTED + } + + var ptr *uint16 + ptr, err := windows.UTF16PtrFromString(filepath) + rtest.OK(t, err) + + //Create the file with attribute flag + handle, err := windows.CreateFile(ptr, uint32(windows.GENERIC_READ|windows.GENERIC_WRITE), uint32(windows.FILE_SHARE_READ), nil, uint32(windows.CREATE_ALWAYS), attribute, 0) + rtest.OK(t, err) + + //Write data to file + _, err = windows.Write(handle, []byte(stream.data)) + rtest.OK(t, err) + + //Close handle + rtest.OK(t, windows.CloseHandle(handle)) +} + +func getNodes(dir string, node NodeTestInfo) map[string]Node { var mode os.FileMode - if isDirectory { + if node.IsDirectory { mode = os.FileMode(2147484159) } else { - if attributes != nil && attributes.ReadOnly { + if node.attributes != nil && node.attributes.ReadOnly { mode = os.FileMode(0o444) } else { mode = os.FileMode(0o666) @@ -308,32 +379,45 @@ func getNodes(dir string, mainNodeName string, order []int, streams []DataStream getFileNodes := func() map[string]Node { nodes := map[string]Node{} - if isDirectory { + if node.IsDirectory { //Add a directory node at the same level as the other streams - nodes[mainNodeName] = Dir{ + nodes[node.name] = Dir{ ModTime: time.Now(), - attributes: attributes, + hasAds: len(node.AdsStreams) > 1, + attributes: node.attributes, Mode: mode, } } - if len(streams) > 0 { - for _, index := range order { - stream := streams[index] - - var attr *FileAttributes = nil - if mainNodeName == stream.name { - attr = attributes - } else if attributes != nil && attributes.Encrypted { - //Set encrypted attribute - attr = &FileAttributes{Encrypted: true} + // Add nodes to the node map in the order we want. + // This ensures the restoration of nodes in the specific order. + for _, index := range node.StreamRestoreOrder { + if index == MAIN_STREAM_ORDER_INDEX && !node.IsDirectory { + //If main file then use the data stream from nodeinfo + nodes[node.DataStreamInfo.name] = File{ + ModTime: time.Now(), + Data: node.DataStreamInfo.data, + Mode: mode, + attributes: node.attributes, + hasAds: len(node.AdsStreams) > 1, + isAds: false, + } + } else { + //Else take the node from the AdsStreams of the node + attr := &FileAttributes{} + if node.attributes != nil && node.attributes.Encrypted { + //Setting the encrypted attribute for ads streams. + //This is needed when an encrypted ads stream is restored first, we need to create the file with encrypted attribute. + attr.Encrypted = true } - nodes[stream.name] = File{ + nodes[node.AdsStreams[index].name] = File{ ModTime: time.Now(), - Data: stream.data, + Data: node.AdsStreams[index].data, Mode: mode, attributes: attr, + hasAds: false, + isAds: true, } } } @@ -349,103 +433,72 @@ func getNodes(dir string, mainNodeName string, order []int, streams []DataStream } } -func verifyFileAttributes(t *testing.T, mainFilePath string, attr FileAttributes) { +func findAdsNamesRecursively(nodesMap map[string]Node, path string, adsNames []string) []string { + for name, node := range nodesMap { + if restic.TrimAds(name) == path && name != path { + adsNames = append(adsNames, strings.Replace(name, path, "", -1)) + } else if dir, ok := node.(Dir); ok && len(dir.Nodes) > 0 { + adsNames = findAdsNamesRecursively(dir.Nodes, path, adsNames) + } + } + return adsNames +} + +func verifyAttributes(t *testing.T, mainFilePath string, attr *FileAttributes) { ptr, err := windows.UTF16PtrFromString(mainFilePath) rtest.OK(t, err) - //Get file attributes using syscall + fileAttributes, err := syscall.GetFileAttributes(ptr) rtest.OK(t, err) //Test positive and negative scenarios if attr.ReadOnly { - rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_READONLY != 0, "Expected read only attribute.") + rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_READONLY != 0, "Expected read only attibute.") } else { - rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_READONLY == 0, "Unexpected read only attribute.") + rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_READONLY == 0, "Unexpected read only attibute.") } if attr.Hidden { - rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_HIDDEN != 0, "Expected hidden attribute.") + rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_HIDDEN != 0, "Expected hidden attibute.") } else { - rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_HIDDEN == 0, "Unexpected hidden attribute.") + rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_HIDDEN == 0, "Unexpected hidden attibute.") } if attr.System { - rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_SYSTEM != 0, "Expected system attribute.") + rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_SYSTEM != 0, "Expected system attibute.") } else { - rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_SYSTEM == 0, "Unexpected system attribute.") + rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_SYSTEM == 0, "Unexpected system attibute.") } if attr.Archive { - rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_ARCHIVE != 0, "Expected archive attribute.") + rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_ARCHIVE != 0, "Expected archive attibute.") } else { - rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_ARCHIVE == 0, "Unexpected archive attribute.") + rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_ARCHIVE == 0, "Unexpected archive attibute.") } if attr.Encrypted { - rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_ENCRYPTED != 0, "Expected encrypted attribute.") + rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_ENCRYPTED != 0, "Expected encrypted attibute.") } else { - rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_ENCRYPTED == 0, "Unexpected encrypted attribute.") + rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_ENCRYPTED == 0, "Unexpected encrypted attibute.") } } -func verifyFileRestores(isEmpty bool, mainFilePath string, t *testing.T, fileInfo NodeInfo) { - if isEmpty { - _, err1 := os.Stat(mainFilePath) - rtest.Assert(t, !errors.Is(err1, os.ErrNotExist), "The file "+fileInfo.name+" does not exist") - } else { +func verifyRestores(t *testing.T, isEmptyOrDirectory bool, path string, dsInfo DataStreamInfo) { + fi, err1 := os.Stat(path) + rtest.Assert(t, !errors.Is(err1, os.ErrNotExist), "The node "+dsInfo.name+" does not exist") - verifyMainFileRestore(t, mainFilePath, fileInfo) + //If the node is not a directoru or should not be empty, check its contents. + if !isEmptyOrDirectory { + size := fi.Size() + rtest.Assert(t, size > 0, "The file "+dsInfo.name+" exists but is empty") + + content, err := os.ReadFile(path) + rtest.OK(t, err) + rtest.Assert(t, string(content) == dsInfo.data, "The file "+dsInfo.name+" exists but the content is not overwritten") } } -func verifyMainFileRestore(t *testing.T, mainFilePath string, fileInfo NodeInfo) { - fi, err1 := os.Stat(mainFilePath) - rtest.Assert(t, !errors.Is(err1, os.ErrNotExist), "The file "+fileInfo.name+" does not exist") - - size := fi.Size() - rtest.Assert(t, size > 0, "The file "+fileInfo.name+" exists but is empty") - - content, err := os.ReadFile(mainFilePath) - rtest.OK(t, err) - rtest.Assert(t, string(content) == fileInfo.data, "The file "+fileInfo.name+" exists but the content is not overwritten") -} - func TestDirAttributeCombination(t *testing.T) { t.Parallel() attributeCombinations := generateCombinations(4, []bool{}) dirName := "TestDir" - // Iterate through each attribute combination - for _, attr1 := range attributeCombinations { - - //Set up the required directory information - dirInfo := NodeInfo{ - DataStreamInfo: DataStreamInfo{ - name: dirName, - }, - parentDir: "dir", - attributes: getDirFileAttributes(attr1), - Exists: false, - IsDirectory: true, - } - - //Get the current test name - testName := getCombinationTestName(dirInfo, dirName, dirInfo.attributes) - - //Run test - t.Run(testName, func(t *testing.T) { - mainDirPath := runAttributeTests(t, dirInfo, dirInfo.attributes) - - //Check directory exists - _, err1 := os.Stat(mainDirPath) - rtest.Assert(t, !errors.Is(err1, os.ErrNotExist), "The directory "+dirInfo.name+" does not exist") - }) - } -} - -func getDirFileAttributes(values []bool) FileAttributes { - return FileAttributes{ - // readonly not valid for directories - Hidden: values[0], - System: values[1], - Archive: values[2], - Encrypted: values[3], - } + testAttributeCombinations(t, attributeCombinations, dirName, false, true, false, NodeTestInfo{}) } func TestFileAttributeCombinationsOverwrite(t *testing.T) { @@ -460,39 +513,31 @@ func testFileAttributeCombinationsOverwrite(t *testing.T, isEmpty bool) { t.Parallel() //Get attribute combinations attributeCombinations := generateCombinations(5, []bool{}) - //Get overwrite file attribute combinations + //Get existing file attribute combinations overwriteCombinations := generateCombinations(5, []bool{}) fileName := "TestOverwriteFile" - //Iterate through each attribute combination - for _, attr1 := range attributeCombinations { + testAttributeCombinationsOverwrite(t, attributeCombinations, overwriteCombinations, isEmpty, fileName, false) +} - fileInfo := NodeInfo{ - DataStreamInfo: getDataStreamInfo(isEmpty, fileName), +func testAttributeCombinationsOverwrite(t *testing.T, attributeCombinations [][]bool, overwriteCombinations [][]bool, isEmpty bool, nodeName string, isDirectory bool) { + // Convert existing attributes boolean value combinations to FileAttributes list + existingFileAttribute := []FileAttributes{} + for _, overwrite := range overwriteCombinations { + existingFileAttribute = append(existingFileAttribute, *convertToFileAttributes(overwrite, isDirectory)) + } + + //Iterate through each existing attribute combination + for _, existingFileAttr := range existingFileAttribute { + exisitngNodeInfo := NodeTestInfo{ + DataStreamInfo: getDummyDataStream(isEmpty || isDirectory, nodeName, true), parentDir: "dir", - attributes: getFileAttributes(attr1), - Exists: true, + attributes: &existingFileAttr, + IsDirectory: isDirectory, } - overwriteFileAttributes := []FileAttributes{} - - for _, overwrite := range overwriteCombinations { - overwriteFileAttributes = append(overwriteFileAttributes, getFileAttributes(overwrite)) - } - - //Iterate through each overwrite attribute combination - for _, overwriteFileAttr := range overwriteFileAttributes { - //Get the test name - testName := getCombinationTestName(fileInfo, fileName, overwriteFileAttr) - - //Run test - t.Run(testName, func(t *testing.T) { - mainFilePath := runAttributeTests(t, fileInfo, overwriteFileAttr) - - verifyFileRestores(isEmpty, mainFilePath, t, fileInfo) - }) - } + testAttributeCombinations(t, attributeCombinations, nodeName, isEmpty, isDirectory, true, exisitngNodeInfo) } } @@ -500,76 +545,182 @@ func TestDirAttributeCombinationsOverwrite(t *testing.T) { t.Parallel() //Get attribute combinations attributeCombinations := generateCombinations(4, []bool{}) - //Get overwrite dir attribute combinations + //Get existing dir attribute combinations overwriteCombinations := generateCombinations(4, []bool{}) dirName := "TestOverwriteDir" - //Iterate through each attribute combination - for _, attr1 := range attributeCombinations { + testAttributeCombinationsOverwrite(t, attributeCombinations, overwriteCombinations, true, dirName, true) +} - dirInfo := NodeInfo{ - DataStreamInfo: DataStreamInfo{ - name: dirName, - }, - parentDir: "dir", - attributes: getDirFileAttributes(attr1), - Exists: true, - IsDirectory: true, - } +func TestOrderedAdsFile(t *testing.T) { + dataStreams := []DataStreamInfo{ + {"OrderedAdsFile.text:datastream1:$DATA", "First data stream."}, + {"OrderedAdsFile.text:datastream2:$DATA", "Second data stream."}, + } - overwriteDirFileAttributes := []FileAttributes{} + var tests = map[string]struct { + fileOrder []int + Exists bool + }{ + "main-stream-first": { + fileOrder: []int{MAIN_STREAM_ORDER_INDEX, 0, 1}, + }, + "second-stream-first": { + fileOrder: []int{0, MAIN_STREAM_ORDER_INDEX, 1}, + }, + "main-stream-first-already-exists": { + fileOrder: []int{MAIN_STREAM_ORDER_INDEX, 0, 1}, + Exists: true, + }, + "second-stream-first-already-exists": { + fileOrder: []int{0, MAIN_STREAM_ORDER_INDEX, 1}, + Exists: true, + }, + } - for _, overwrite := range overwriteCombinations { - overwriteDirFileAttributes = append(overwriteDirFileAttributes, getDirFileAttributes(overwrite)) - } + mainStreamName := "OrderedAdsFile.text" + dir := "dir" - //Iterate through each overwrite attribute combinations - for _, overwriteDirAttr := range overwriteDirFileAttributes { - //Get the test name - testName := getCombinationTestName(dirInfo, dirName, overwriteDirAttr) + for name, test := range tests { + t.Run(name, func(t *testing.T) { + tempdir := rtest.TempDir(t) - //Run test - t.Run(testName, func(t *testing.T) { - mainDirPath := runAttributeTests(t, dirInfo, dirInfo.attributes) + nodeInfo := NodeTestInfo{ + parentDir: dir, + attributes: &FileAttributes{}, + DataStreamInfo: getDummyDataStream(false, mainStreamName, false), + StreamRestoreOrder: test.fileOrder, + AdsStreams: dataStreams, + } - //Check directory exists - _, err1 := os.Stat(mainDirPath) - rtest.Assert(t, !errors.Is(err1, os.ErrNotExist), "The directory "+dirInfo.name+" does not exist") - }) - } + exisitingNode := NodeTestInfo{} + if test.Exists { + exisitingNode = NodeTestInfo{ + parentDir: dir, + attributes: &FileAttributes{}, + DataStreamInfo: getDummyDataStream(false, mainStreamName, true), + StreamRestoreOrder: test.fileOrder, + AdsStreams: dataStreams, + } + } + + runRestorerTest(t, nodeInfo, tempdir, test.Exists, exisitingNode) + verifyRestoreOrder(t, nodeInfo, tempdir) + }) } } -func TestRestoreDeleteCaseInsensitive(t *testing.T) { - repo := repository.TestRepository(t) - tempdir := rtest.TempDir(t) +func verifyRestoreOrder(t *testing.T, nodeInfo NodeTestInfo, tempdir string) { + for _, fileIndex := range nodeInfo.StreamRestoreOrder { - sn, _ := saveSnapshot(t, repo, Snapshot{ - Nodes: map[string]Node{ - "anotherfile": File{Data: "content: file\n"}, - }, - }, noopGetGenericAttributes) + var stream DataStreamInfo + if fileIndex == MAIN_STREAM_ORDER_INDEX { + stream = nodeInfo.DataStreamInfo + } else { + stream = nodeInfo.AdsStreams[fileIndex] + } - // should delete files that no longer exist in the snapshot - deleteSn, _ := saveSnapshot(t, repo, Snapshot{ - Nodes: map[string]Node{ - "AnotherfilE": File{Data: "content: file\n"}, - }, - }, noopGetGenericAttributes) - - res := NewRestorer(repo, sn, Options{}) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - _, err := res.RestoreTo(ctx, tempdir) - rtest.OK(t, err) - - res = NewRestorer(repo, deleteSn, Options{Delete: true}) - _, err = res.RestoreTo(ctx, tempdir) - rtest.OK(t, err) - - // anotherfile must still exist - _, err = os.Stat(filepath.Join(tempdir, "anotherfile")) - rtest.OK(t, err) + fp := path.Join(tempdir, nodeInfo.parentDir, stream.name) + verifyRestores(t, false, fp, stream) + } +} + +func TestExistingStreamRemoval(t *testing.T) { + tempdir := rtest.TempDir(t) + dirName := "dir" + mainFileName := "TestExistingStream.text" + + existingFileStreams := []DataStreamInfo{ + {"TestExistingStream.text:datastream1:$DATA", "Existing stream 1."}, + {"TestExistingStream.text:datastream2:$DATA", "Existing stream 2."}, + {"TestExistingStream.text:datastream3:$DATA", "Existing stream 3."}, + {"TestExistingStream.text:datastream4:$DATA", "Existing stream 4."}, + } + + restoringStreams := []DataStreamInfo{ + {"TestExistingStream.text:datastream1:$DATA", "First data stream."}, + {"TestExistingStream.text:datastream2:$DATA", "Second data stream."}, + } + + nodeInfo := NodeTestInfo{ + parentDir: dirName, + attributes: &FileAttributes{}, + DataStreamInfo: DataStreamInfo{ + name: mainFileName, + data: "Main file data.", + }, + StreamRestoreOrder: []int{MAIN_STREAM_ORDER_INDEX, 0, 1}, + AdsStreams: restoringStreams, + } + + existingNodeInfo := NodeTestInfo{ + parentDir: dirName, + attributes: &FileAttributes{}, + DataStreamInfo: DataStreamInfo{ + name: mainFileName, + data: "Existing main stream.", + }, + StreamRestoreOrder: []int{MAIN_STREAM_ORDER_INDEX, 0, 1, 2, 3, 4}, + AdsStreams: existingFileStreams} + + runRestorerTest(t, nodeInfo, tempdir, true, existingNodeInfo) + verifyExistingStreamRemoval(t, existingFileStreams, tempdir, dirName, restoringStreams) + + dirPath := path.Join(tempdir, nodeInfo.parentDir, nodeInfo.name) + verifyRestores(t, true, dirPath, nodeInfo.DataStreamInfo) +} + +func verifyExistingStreamRemoval(t *testing.T, existingFileStreams []DataStreamInfo, tempdir string, dirName string, restoredStreams []DataStreamInfo) { + for _, currentFile := range existingFileStreams { + fp := path.Join(tempdir, dirName, currentFile.name) + + existsInRestored := existsInStreamList(currentFile.name, restoredStreams) + if !existsInRestored { + //Stream that doesn't exist in the restored stream list must have been removed. + _, err1 := os.Stat(fp) + rtest.Assert(t, errors.Is(err1, os.ErrNotExist), "The file "+currentFile.name+" should not exist") + } + } + + for _, currentFile := range restoredStreams { + fp := path.Join(tempdir, dirName, currentFile.name) + verifyRestores(t, false, fp, currentFile) + } +} + +func existsInStreamList(name string, streams []DataStreamInfo) bool { + for _, value := range streams { + if value.name == name { + return true + } + } + return false +} + +func TestAdsDirectory(t *testing.T) { + streams := []DataStreamInfo{ + {"TestDirStream:datastream1:$DATA", "First dir stream."}, + {"TestDirStream:datastream2:$DATA", "Second dir stream."}, + } + + nodeinfo := NodeTestInfo{ + parentDir: "dir", + attributes: &FileAttributes{}, + DataStreamInfo: DataStreamInfo{name: "TestDirStream"}, + IsDirectory: true, + StreamRestoreOrder: []int{0, 1}, + AdsStreams: streams, + } + + tempDir := t.TempDir() + runRestorerTest(t, nodeinfo, tempDir, false, NodeTestInfo{}) + + for _, stream := range streams { + fp := path.Join(tempDir, nodeinfo.parentDir, stream.name) + verifyRestores(t, false, fp, stream) + } + + dirPath := path.Join(tempDir, nodeinfo.parentDir, nodeinfo.name) + verifyRestores(t, true, dirPath, nodeinfo.DataStreamInfo) } diff --git a/internal/ui/restore/progress_test.go b/internal/ui/restore/progress_test.go index b6f72726c..bc555de25 100644 --- a/internal/ui/restore/progress_test.go +++ b/internal/ui/restore/progress_test.go @@ -4,7 +4,10 @@ import ( "testing" "time" + "encoding/json" + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/test" ) @@ -94,9 +97,12 @@ func TestFirstProgressOnAFile(t *testing.T) { expectedBytesWritten := uint64(5) expectedBytesTotal := uint64(100) + attrs := map[restic.GenericAttributeType]json.RawMessage{ + restic.TypeIsADS: json.RawMessage(`false`), + } result, items, _ := testProgress(func(progress *Progress) bool { progress.AddFile(expectedBytesTotal) - progress.AddProgress("test", ActionFileUpdated, expectedBytesWritten, expectedBytesTotal) + progress.AddProgress("test", ActionFileUpdated, expectedBytesWritten, expectedBytesTotal, attrs) return false }) test.Equals(t, printerTrace{ @@ -107,12 +113,14 @@ func TestFirstProgressOnAFile(t *testing.T) { func TestLastProgressOnAFile(t *testing.T) { fileSize := uint64(100) - + attrs := map[restic.GenericAttributeType]json.RawMessage{ + restic.TypeIsADS: json.RawMessage(`false`), + } result, items, _ := testProgress(func(progress *Progress) bool { progress.AddFile(fileSize) - progress.AddProgress("test", ActionFileUpdated, 30, fileSize) - progress.AddProgress("test", ActionFileUpdated, 35, fileSize) - progress.AddProgress("test", ActionFileUpdated, 35, fileSize) + progress.AddProgress("test", ActionFileUpdated, 30, fileSize, attrs) + progress.AddProgress("test", ActionFileUpdated, 35, fileSize, attrs) + progress.AddProgress("test", ActionFileUpdated, 35, fileSize, attrs) return false }) test.Equals(t, printerTrace{ @@ -125,13 +133,15 @@ func TestLastProgressOnAFile(t *testing.T) { func TestLastProgressOnLastFile(t *testing.T) { fileSize := uint64(100) - + attrs := map[restic.GenericAttributeType]json.RawMessage{ + restic.TypeIsADS: json.RawMessage(`false`), + } result, items, _ := testProgress(func(progress *Progress) bool { progress.AddFile(fileSize) progress.AddFile(50) - progress.AddProgress("test1", ActionFileUpdated, 50, 50) - progress.AddProgress("test2", ActionFileUpdated, 50, fileSize) - progress.AddProgress("test2", ActionFileUpdated, 50, fileSize) + progress.AddProgress("test1", ActionFileUpdated, 50, 50, attrs) + progress.AddProgress("test2", ActionFileUpdated, 50, fileSize, attrs) + progress.AddProgress("test2", ActionFileUpdated, 50, fileSize, attrs) return false }) test.Equals(t, printerTrace{ @@ -145,12 +155,14 @@ func TestLastProgressOnLastFile(t *testing.T) { func TestSummaryOnSuccess(t *testing.T) { fileSize := uint64(100) - + attrs := map[restic.GenericAttributeType]json.RawMessage{ + restic.TypeIsADS: json.RawMessage(`false`), + } result, _, _ := testProgress(func(progress *Progress) bool { progress.AddFile(fileSize) progress.AddFile(50) - progress.AddProgress("test1", ActionFileUpdated, 50, 50) - progress.AddProgress("test2", ActionFileUpdated, fileSize, fileSize) + progress.AddProgress("test1", ActionFileUpdated, 50, 50, attrs) + progress.AddProgress("test2", ActionFileUpdated, fileSize, fileSize, attrs) return true }) test.Equals(t, printerTrace{ @@ -160,12 +172,14 @@ func TestSummaryOnSuccess(t *testing.T) { func TestSummaryOnErrors(t *testing.T) { fileSize := uint64(100) - + attrs := map[restic.GenericAttributeType]json.RawMessage{ + restic.TypeIsADS: json.RawMessage(`false`), + } result, _, _ := testProgress(func(progress *Progress) bool { progress.AddFile(fileSize) progress.AddFile(50) - progress.AddProgress("test1", ActionFileUpdated, 50, 50) - progress.AddProgress("test2", ActionFileUpdated, fileSize/2, fileSize) + progress.AddProgress("test1", ActionFileUpdated, 50, 50, attrs) + progress.AddProgress("test2", ActionFileUpdated, fileSize/2, fileSize, attrs) return true }) test.Equals(t, printerTrace{ @@ -194,8 +208,11 @@ func TestProgressTypes(t *testing.T) { _, items, _ := testProgress(func(progress *Progress) bool { progress.AddFile(fileSize) progress.AddFile(0) - progress.AddProgress("dir", ActionDirRestored, fileSize, fileSize) - progress.AddProgress("new", ActionFileRestored, 0, 0) + attrs := map[restic.GenericAttributeType]json.RawMessage{ + restic.TypeIsADS: json.RawMessage(`false`), + } + progress.AddProgress("dir", ActionDirRestored, fileSize, fileSize, attrs) + progress.AddProgress("new", ActionFileRestored, 0, 0, attrs) progress.ReportDeletion("del") return true })