mirror of https://github.com/restic/restic.git
Merge 1743de53e4
into c6311c1e32
This commit is contained in:
commit
0e0efafad3
|
@ -0,0 +1,9 @@
|
|||
Enhancement: Back up and restore Windows Alternate Data Streams
|
||||
|
||||
Restic did not back up Alternate Data Streams in Windows. Restic now backs up Alternate Data Streams (ADS) and restores them back to the main files.
|
||||
The Alternate Data Streams are backed up like any other normal files, and the full name of the stream is stored as the name of the file.
|
||||
During restore, the ADS are restored and attached to the original files as Alternate Data Streams.
|
||||
For progress and summary, the ADS are not counted in the file counts, but the sizes of the ADS files are counted.
|
||||
|
||||
https://github.com/restic/restic/pull/4614
|
||||
https://github.com/restic/restic/issues/1401
|
|
@ -236,18 +236,20 @@ func (arch *Archiver) SaveDir(ctx context.Context, snPath string, dir string, fi
|
|||
if err != nil {
|
||||
return FutureNode{}, err
|
||||
}
|
||||
sort.Strings(names)
|
||||
pathnames := arch.preProcessPaths(dir, names)
|
||||
sort.Strings(pathnames)
|
||||
|
||||
nodes := make([]FutureNode, 0, len(names))
|
||||
nodes := make([]FutureNode, 0, len(pathnames))
|
||||
|
||||
for _, name := range names {
|
||||
for _, pathname := range pathnames {
|
||||
// test if context has been cancelled
|
||||
if ctx.Err() != nil {
|
||||
debug.Log("context has been cancelled, aborting")
|
||||
return FutureNode{}, ctx.Err()
|
||||
}
|
||||
name := getNameFromPathname(pathname)
|
||||
pathname := arch.processPath(dir, pathname)
|
||||
|
||||
pathname := arch.FS.Join(dir, name)
|
||||
oldNode := previous.Find(name)
|
||||
snItem := join(snPath, name)
|
||||
fn, excluded, err := arch.Save(ctx, snItem, pathname, oldNode)
|
||||
|
@ -260,7 +262,7 @@ func (arch *Archiver) SaveDir(ctx context.Context, snPath string, dir string, fi
|
|||
continue
|
||||
}
|
||||
|
||||
return FutureNode{}, err
|
||||
return FutureNode{}, errors.Wrap(err, "error saving a target (file or directory)")
|
||||
}
|
||||
|
||||
if excluded {
|
||||
|
@ -349,6 +351,11 @@ func (arch *Archiver) Save(ctx context.Context, snPath, target string, previous
|
|||
if err != nil {
|
||||
return FutureNode{}, false, err
|
||||
}
|
||||
//In case of windows ADS files for checking include and excludes we use the main file which has the ADS files attached.
|
||||
//For Unix, the main file is the same as there is no ADS. So targetMain is always the same as target.
|
||||
//After checking the exclusion for actually processing the file, we use the full file name including ads portion if any.
|
||||
targetMain := fs.SanitizeMainFileName(target)
|
||||
abstargetMain := fs.SanitizeMainFileName(abstarget)
|
||||
|
||||
// exclude files by path before running Lstat to reduce number of lstat calls
|
||||
if !arch.SelectByName(abstarget) {
|
||||
|
@ -357,7 +364,7 @@ func (arch *Archiver) Save(ctx context.Context, snPath, target string, previous
|
|||
}
|
||||
|
||||
// get file info and run remaining select functions that require file information
|
||||
fi, err := arch.FS.Lstat(target)
|
||||
fiMain, err := arch.FS.Lstat(targetMain)
|
||||
if err != nil {
|
||||
debug.Log("lstat() for %v returned error: %v", target, err)
|
||||
err = arch.error(abstarget, err)
|
||||
|
@ -366,10 +373,15 @@ func (arch *Archiver) Save(ctx context.Context, snPath, target string, previous
|
|||
}
|
||||
return FutureNode{}, true, nil
|
||||
}
|
||||
if !arch.Select(abstarget, fi) {
|
||||
if !arch.Select(abstargetMain, fiMain) {
|
||||
debug.Log("%v is excluded", target)
|
||||
return FutureNode{}, true, nil
|
||||
}
|
||||
var fi os.FileInfo
|
||||
fi, shouldReturn, fn, excluded, err := arch.processTargets(target, targetMain, abstarget, fiMain)
|
||||
if shouldReturn {
|
||||
return fn, excluded, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case fs.IsRegularFile(fi):
|
||||
|
@ -659,8 +671,9 @@ func readdirnames(filesystem fs.FS, dir string, flags int) ([]string, error) {
|
|||
func resolveRelativeTargets(filesys fs.FS, targets []string) ([]string, error) {
|
||||
debug.Log("targets before resolving: %v", targets)
|
||||
result := make([]string, 0, len(targets))
|
||||
preProcessTargets(filesys, &targets)
|
||||
for _, target := range targets {
|
||||
target = filesys.Clean(target)
|
||||
target = processTarget(filesys, target)
|
||||
pc, _ := pathComponents(filesys, target, false)
|
||||
if len(pc) > 0 {
|
||||
result = append(result, target)
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package archiver
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/restic/restic/internal/fs"
|
||||
)
|
||||
|
||||
// preProcessTargets performs preprocessing of the targets before the loop.
|
||||
// It is a no-op on non-windows OS as we do not need to do an
|
||||
// extra iteration on the targets before the loop.
|
||||
// We process each target inside the loop.
|
||||
func preProcessTargets(_ fs.FS, _ *[]string) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
// processTarget processes each target in the loop.
|
||||
// In case of non-windows OS it uses the passed filesys to clean the target.
|
||||
func processTarget(filesys fs.FS, target string) string {
|
||||
return filesys.Clean(target)
|
||||
}
|
||||
|
||||
// preProcessPaths processes paths before looping.
|
||||
func (arch *Archiver) preProcessPaths(_ string, names []string) (paths []string) {
|
||||
// In case of non-windows OS this is no-op as we process the paths within the loop
|
||||
// and avoid the extra looping before hand.
|
||||
return names
|
||||
}
|
||||
|
||||
// processPath processes the path in the loop.
|
||||
func (arch *Archiver) processPath(dir string, name string) (path string) {
|
||||
//In case of non-windows OS we prepare the path in the loop.
|
||||
return arch.FS.Join(dir, name)
|
||||
}
|
||||
|
||||
// getNameFromPathname gets the name from pathname.
|
||||
// In case for non-windows the pathname is same as the name.
|
||||
func getNameFromPathname(pathname string) (name string) {
|
||||
return pathname
|
||||
}
|
||||
|
||||
// processTargets is no-op for non-windows OS
|
||||
func (arch *Archiver) processTargets(_ string, _ string, _ string, fiMain os.FileInfo) (fi os.FileInfo, shouldReturn bool, fn FutureNode, excluded bool, err error) {
|
||||
return fiMain, false, FutureNode{}, false, nil
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package archiver
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/fs"
|
||||
)
|
||||
|
||||
// preProcessTargets performs preprocessing of the targets before the loop.
|
||||
// For Windows, it cleans each target and it also adds ads stream for each
|
||||
// target to the targets array.
|
||||
// We read the ADS from each file and add them as independent Nodes with
|
||||
// the full ADS name as the name of the file.
|
||||
// During restore the ADS files are restored using the ADS name and that
|
||||
// automatically attaches them as ADS to the main file.
|
||||
func preProcessTargets(filesys fs.FS, targets *[]string) {
|
||||
for _, target := range *targets {
|
||||
target = filesys.Clean(target)
|
||||
addADSStreams(target, targets)
|
||||
}
|
||||
}
|
||||
|
||||
// processTarget processes each target in the loop.
|
||||
// In case of windows the clean up of target is already done
|
||||
// in preProcessTargets before the loop, hence this is no-op.
|
||||
func processTarget(_ fs.FS, target string) string {
|
||||
return target
|
||||
}
|
||||
|
||||
// getNameFromPathname gets the name from pathname.
|
||||
// In case for windows the pathname is the full path, so it need to get the base name.
|
||||
func getNameFromPathname(pathname string) (name string) {
|
||||
return filepath.Base(pathname)
|
||||
}
|
||||
|
||||
// preProcessPaths processes paths before looping.
|
||||
func (arch *Archiver) preProcessPaths(dir string, names []string) (paths []string) {
|
||||
// In case of windows we want to add the ADS paths as well before sorting.
|
||||
return arch.getPathsIncludingADS(dir, names)
|
||||
}
|
||||
|
||||
// processPath processes the path in the loop.
|
||||
func (arch *Archiver) processPath(_ string, name string) (path string) {
|
||||
// In case of windows we have already prepared the paths before the loop.
|
||||
// Hence this is a no-op.
|
||||
return name
|
||||
}
|
||||
|
||||
// getPathsIncludingADS iterates all passed path names and adds the ads
|
||||
// contained in those paths before returning all full paths including ads
|
||||
func (arch *Archiver) getPathsIncludingADS(dir string, names []string) []string {
|
||||
paths := make([]string, 0, len(names))
|
||||
|
||||
for _, name := range names {
|
||||
pathname := arch.FS.Join(dir, name)
|
||||
paths = append(paths, pathname)
|
||||
addADSStreams(pathname, &paths)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// addADSStreams gets the ads streams if any in the pathname passed and adds them to the passed paths
|
||||
func addADSStreams(pathname string, paths *[]string) {
|
||||
success, adsStreams, err := fs.GetADStreamNames(pathname)
|
||||
if success {
|
||||
streamCount := len(adsStreams)
|
||||
if streamCount > 0 {
|
||||
debug.Log("ADS Streams for file: %s, streams: %v", pathname, adsStreams)
|
||||
for i := 0; i < streamCount; i++ {
|
||||
adsStream := adsStreams[i]
|
||||
adsPath := pathname + adsStream
|
||||
*paths = append(*paths, adsPath)
|
||||
}
|
||||
}
|
||||
} else if err != nil {
|
||||
debug.Log("No ADS found for path: %s, err: %v", pathname, err)
|
||||
}
|
||||
}
|
||||
|
||||
// processTargets in windows performs Lstat for the ADS files since the file info would not be available for them yet.
|
||||
func (arch *Archiver) processTargets(target string, targetMain string, abstarget string, fiMain os.FileInfo) (fi os.FileInfo, shouldReturn bool, fn FutureNode, excluded bool, err error) {
|
||||
if target != targetMain {
|
||||
//If this is an ADS file we need to Lstat again for the file info.
|
||||
fi, err = arch.FS.Lstat(target)
|
||||
if err != nil {
|
||||
debug.Log("lstat() for %v returned error: %v", target, err)
|
||||
err = arch.error(abstarget, err)
|
||||
if err != nil {
|
||||
return nil, true, FutureNode{}, false, errors.WithStack(err)
|
||||
}
|
||||
//If this is an ads file, shouldReturn should be true because we want to
|
||||
// skip the remaining processing of the file.
|
||||
return nil, true, FutureNode{}, true, nil
|
||||
}
|
||||
} else {
|
||||
fi = fiMain
|
||||
}
|
||||
return fi, false, FutureNode{}, false, nil
|
||||
}
|
|
@ -4,7 +4,18 @@
|
|||
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"
|
||||
restictest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
type wrappedFileInfo struct {
|
||||
|
@ -26,3 +37,207 @@ func wrapFileInfo(fi os.FileInfo) os.FileInfo {
|
|||
|
||||
return res
|
||||
}
|
||||
|
||||
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 := restictest.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)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
restictest.Equals(t, uint64(test.stat.DataBlobs), uint64(stat.DataBlobs))
|
||||
restictest.Equals(t, uint64(test.stat.TreeBlobs), uint64(stat.TreeBlobs))
|
||||
restictest.Equals(t, test.stat.DataSize, stat.DataSize)
|
||||
restictest.Equals(t, test.stat.DataSizeInRepo, stat.DataSizeInRepo)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,11 +68,11 @@ func (s TestSymlink) String() string {
|
|||
func TestCreateFiles(t testing.TB, target string, dir TestDir) {
|
||||
t.Helper()
|
||||
for name, item := range dir {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/fs"
|
||||
)
|
||||
|
||||
// ErrBadString is returned when Match is called with the empty string as the
|
||||
|
@ -200,9 +201,9 @@ func match(pattern Pattern, strs []string) (matched bool, err error) {
|
|||
for i := len(pattern.parts) - 1; i >= 0; i-- {
|
||||
var ok bool
|
||||
if pattern.parts[i].isSimple {
|
||||
ok = pattern.parts[i].pattern == strs[offset+i]
|
||||
ok = pattern.parts[i].pattern == fs.SanitizeMainFileName(strs[offset+i])
|
||||
} else {
|
||||
ok, err = filepath.Match(pattern.parts[i].pattern, strs[offset+i])
|
||||
ok, err = filepath.Match(pattern.parts[i].pattern, fs.SanitizeMainFileName(strs[offset+i]))
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "Match")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32dll = syscall.NewLazyDLL("kernel32.dll")
|
||||
findFirstStreamW = kernel32dll.NewProc("FindFirstStreamW")
|
||||
findNextStreamW = kernel32dll.NewProc("FindNextStreamW")
|
||||
findClose = kernel32dll.NewProc("FindClose")
|
||||
)
|
||||
|
||||
type (
|
||||
HANDLE uintptr
|
||||
)
|
||||
|
||||
const (
|
||||
maxPath = 296
|
||||
streamInfoLevelStandard = 0
|
||||
invalidFileHandle = ^HANDLE(0)
|
||||
)
|
||||
|
||||
type Win32FindStreamData struct {
|
||||
size int64
|
||||
name [maxPath]uint16
|
||||
}
|
||||
|
||||
/*
|
||||
HANDLE WINAPI FindFirstStreamW(
|
||||
__in LPCWSTR lpFileName,
|
||||
__in STREAM_INFO_LEVELS InfoLevel, (0 standard, 1 max infos)
|
||||
__out LPVOID lpFindStreamData, (return information about file in a WIN32_FIND_STREAM_DATA if 0 is given in infos_level
|
||||
__reserved DWORD dwFlags (Reserved for future use. This parameter must be zero.) cf: doc
|
||||
);
|
||||
https://msdn.microsoft.com/en-us/library/aa364424(v=vs.85).aspx
|
||||
*/
|
||||
// GetADStreamNames returns the ads stream names for the passed fileName.
|
||||
// If success is true, it means ADS files were found.
|
||||
func GetADStreamNames(fileName string) (success bool, streamNames []string, err error) {
|
||||
h, success, firstname, err := findFirstStream(fileName)
|
||||
defer closeHandle(h)
|
||||
if success {
|
||||
if !strings.Contains(firstname, "::") {
|
||||
//If fileName is a directory which has ADS, the ADS name comes in the first stream itself between the two :
|
||||
//file ads firstname comes as ::$DATA
|
||||
streamNames = append(streamNames, firstname)
|
||||
}
|
||||
for {
|
||||
endStream, name, err2 := findNextStream(h)
|
||||
err = err2
|
||||
if endStream {
|
||||
break
|
||||
}
|
||||
streamNames = append(streamNames, name)
|
||||
}
|
||||
}
|
||||
// If the handle is found successfully, success is true, but the windows api
|
||||
// still returns an error object. It doesn't mean that an error occurred.
|
||||
if isHandleEOFError(err) {
|
||||
// This error is expected, we don't need to expose it.
|
||||
err = nil
|
||||
}
|
||||
return success, streamNames, err
|
||||
}
|
||||
|
||||
// findFirstStream gets the handle and stream type for the first stream
|
||||
// If the handle is found successfully, success is true, but the windows api
|
||||
// still returns an error object. It doesn't mean that an error occurred.
|
||||
func findFirstStream(fileName string) (handle HANDLE, success bool, streamType string, err error) {
|
||||
fsd := &Win32FindStreamData{}
|
||||
|
||||
ptr, err := syscall.UTF16PtrFromString(fileName)
|
||||
if err != nil {
|
||||
return invalidFileHandle, false, "<nil>", err
|
||||
}
|
||||
ret, _, err := findFirstStreamW.Call(
|
||||
uintptr(unsafe.Pointer(ptr)),
|
||||
streamInfoLevelStandard,
|
||||
uintptr(unsafe.Pointer(fsd)),
|
||||
0,
|
||||
)
|
||||
h := HANDLE(ret)
|
||||
|
||||
streamType = windows.UTF16ToString(fsd.name[:])
|
||||
return h, h != invalidFileHandle, streamType, err
|
||||
}
|
||||
|
||||
// findNextStream finds the next ads stream name
|
||||
// endStream indicites if this is the last stream, name is the stream name.
|
||||
// err being returned does not mean an error occurred.
|
||||
func findNextStream(handle HANDLE) (endStream bool, name string, err error) {
|
||||
fsd := &Win32FindStreamData{}
|
||||
ret, _, err := findNextStreamW.Call(
|
||||
uintptr(handle),
|
||||
uintptr(unsafe.Pointer(fsd)),
|
||||
)
|
||||
name = windows.UTF16ToString(fsd.name[:])
|
||||
return ret != 1, name, err
|
||||
}
|
||||
|
||||
// closeHandle closes the passed handle
|
||||
func closeHandle(handle HANDLE) bool {
|
||||
ret, _, _ := findClose.Call(
|
||||
uintptr(handle),
|
||||
)
|
||||
return ret != 0
|
||||
}
|
||||
|
||||
// TrimAds trims the ads file part from the passed filename and returns the base name.
|
||||
func TrimAds(str string) string {
|
||||
dir, filename := filepath.Split(str)
|
||||
if strings.Contains(filename, ":") {
|
||||
out := filepath.Join(dir, strings.Split(filename, ":")[0])
|
||||
return out
|
||||
} else {
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
// IsAds checks if the passed file name is an ads file.
|
||||
func IsAds(str string) bool {
|
||||
filename := filepath.Base(str)
|
||||
// Only ADS filenames can contain ":" in windows.
|
||||
return strings.Contains(filename, ":")
|
||||
}
|
||||
|
||||
// isHandleEOFError checks if the error is ERROR_HANDLE_EOF
|
||||
func isHandleEOFError(err error) bool {
|
||||
// Use a type assertion to check if the error is of type syscall.Errno
|
||||
if errno, ok := err.(syscall.Errno); ok {
|
||||
// Compare the error code to the expected value
|
||||
return errno == syscall.ERROR_HANDLE_EOF
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package fs_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/fs"
|
||||
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 := fs.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 := fs.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 := fs.IsAds(test.input)
|
||||
rtest.Equals(t, test.result, output)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -48,3 +48,15 @@ func Chmod(name string, mode os.FileMode) error {
|
|||
|
||||
return err
|
||||
}
|
||||
|
||||
// IsMainFile specifies if this is the main file or a secondary attached file (like ADS in case of windows)
|
||||
func IsMainFile(_ string) bool {
|
||||
// no-op - In case of OS other than windows this is always true
|
||||
return true
|
||||
}
|
||||
|
||||
// SanitizeMainFileName will only keep the main file and remove the secondary file.
|
||||
func SanitizeMainFileName(str string) string {
|
||||
// no-op - In case of non-windows there is no secondary file
|
||||
return str
|
||||
}
|
||||
|
|
|
@ -77,3 +77,18 @@ func TempFile(dir, prefix string) (f *os.File, err error) {
|
|||
func Chmod(name string, mode os.FileMode) error {
|
||||
return os.Chmod(fixpath(name), mode)
|
||||
}
|
||||
|
||||
// IsMainFile specifies if this is the main file or a secondary attached file (like ADS in case of windows)
|
||||
// This is used for functionalities we want to skip for secondary (ads) files.
|
||||
// Eg. For Windows we do not want to count the secondary files
|
||||
func IsMainFile(name string) bool {
|
||||
return !IsAds(name)
|
||||
}
|
||||
|
||||
// SanitizeMainFileName will only keep the main file and remove the secondary file like ADS from the name.
|
||||
func SanitizeMainFileName(str string) string {
|
||||
// The ADS is essentially a part of the main file. So for any functionality that
|
||||
// needs to consider the main file, like filtering, we need to derive the main file name
|
||||
// from the ADS name.
|
||||
return TrimAds(str)
|
||||
}
|
||||
|
|
|
@ -51,14 +51,7 @@ func (w *filesWriter) writeToFile(path string, blob []byte, offset int64, create
|
|||
return wr, nil
|
||||
}
|
||||
|
||||
var flags int
|
||||
if createSize >= 0 {
|
||||
flags = os.O_CREATE | os.O_TRUNC | os.O_WRONLY
|
||||
} else {
|
||||
flags = os.O_WRONLY
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(path, flags, 0600)
|
||||
f, err := w.OpenFile(createSize, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package restorer
|
||||
|
||||
import "os"
|
||||
|
||||
// OpenFile opens the file with create, truncate and write only options if
|
||||
// createSize is specified greater than 0 i.e. if the file hasn't already
|
||||
// been created. Otherwise it opens the file with only write only option.
|
||||
func (*filesWriter) OpenFile(createSize int64, path string) (*os.File, error) {
|
||||
var flags int
|
||||
if createSize >= 0 {
|
||||
flags = os.O_CREATE | os.O_TRUNC | os.O_WRONLY
|
||||
} else {
|
||||
flags = os.O_WRONLY
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(path, flags, 0600)
|
||||
return f, err
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package restorer
|
||||
|
||||
import "os"
|
||||
|
||||
// OpenFile opens the file with truncate and write only options.
|
||||
// In case of windows, it first attempts to open an existing file,
|
||||
// and only if the file does not exist, it opens it with create option
|
||||
// in order to create the file. This is done, otherwise if the ads stream
|
||||
// is written first (in which case it automatically creates an empty main
|
||||
// file before writing the stream) and then when the main file is written
|
||||
// later, the ads stream can be overwritten.
|
||||
func (*filesWriter) OpenFile(createSize int64, path string) (*os.File, error) {
|
||||
var flags int
|
||||
var f *os.File
|
||||
var err error
|
||||
// TODO optimize this. When GenericAttribute change is merged, we can
|
||||
// leverage that here. If a file has ADS, we will have a GenericAttribute of
|
||||
// type TypeADS added in the file with values as a string of ads stream names
|
||||
// and we will do the following only if the TypeADS attribute is found in the node.
|
||||
// Otherwise we will directly just use the create option while opening the file.
|
||||
if createSize >= 0 {
|
||||
flags = os.O_TRUNC | os.O_WRONLY
|
||||
f, err = os.OpenFile(path, flags, 0600)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
//If file not exists open with create flag
|
||||
flags = os.O_CREATE | os.O_TRUNC | os.O_WRONLY
|
||||
f, err = os.OpenFile(path, flags, 0600)
|
||||
}
|
||||
} else {
|
||||
flags = os.O_WRONLY
|
||||
f, err = os.OpenFile(path, flags, 0600)
|
||||
}
|
||||
|
||||
return f, err
|
||||
}
|
|
@ -242,7 +242,7 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) error {
|
|||
enterDir: func(_ *restic.Node, target, location string) error {
|
||||
debug.Log("first pass, enterDir: mkdir %q, leaveDir should restore metadata", location)
|
||||
if res.progress != nil {
|
||||
res.progress.AddFile(0)
|
||||
res.addFile(node, 0)
|
||||
}
|
||||
// create dir with default permissions
|
||||
// #leaveDir restores dir metadata after visiting all children
|
||||
|
@ -260,14 +260,14 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) error {
|
|||
|
||||
if node.Type != "file" {
|
||||
if res.progress != nil {
|
||||
res.progress.AddFile(0)
|
||||
res.addFile(node, 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if node.Size == 0 {
|
||||
if res.progress != nil {
|
||||
res.progress.AddFile(node.Size)
|
||||
res.addFile(node, node.Size)
|
||||
}
|
||||
return nil // deal with empty files later
|
||||
}
|
||||
|
@ -276,7 +276,7 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) error {
|
|||
if idx.Has(node.Inode, node.DeviceID) {
|
||||
if res.progress != nil {
|
||||
// a hardlinked file does not increase the restore size
|
||||
res.progress.AddFile(0)
|
||||
res.addFile(node, 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -284,7 +284,7 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) error {
|
|||
}
|
||||
|
||||
if res.progress != nil {
|
||||
res.progress.AddFile(node.Size)
|
||||
res.addFile(node, node.Size)
|
||||
}
|
||||
|
||||
filerestorer.addFile(location, node.Content, int64(node.Size))
|
||||
|
@ -336,6 +336,15 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (res *Restorer) addFile(node *restic.Node, size uint64) {
|
||||
if fs.IsMainFile(node.Name) {
|
||||
res.progress.AddFile(size)
|
||||
} else {
|
||||
// If this is not the main file, we just want to update the size and not the count.
|
||||
res.progress.AddSize(size)
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot returns the snapshot this restorer is configured to use.
|
||||
func (res *Restorer) Snapshot() *restic.Snapshot {
|
||||
return res.sn
|
||||
|
|
|
@ -4,12 +4,21 @@
|
|||
package restorer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
|
@ -33,3 +42,285 @@ func getBlockCount(t *testing.T, filename string) int64 {
|
|||
|
||||
return int64(math.Ceil(float64(result) / 512))
|
||||
}
|
||||
|
||||
type AdsTestInfo struct {
|
||||
dirName string
|
||||
fileOrder []int
|
||||
fileStreamNames []string
|
||||
Overwrite bool
|
||||
}
|
||||
|
||||
type NamedNode struct {
|
||||
name string
|
||||
node Node
|
||||
}
|
||||
|
||||
type OrderedSnapshot struct {
|
||||
nodes []NamedNode
|
||||
}
|
||||
|
||||
type OrderedDir struct {
|
||||
Nodes []NamedNode
|
||||
Mode os.FileMode
|
||||
ModTime time.Time
|
||||
}
|
||||
|
||||
func TestOrderedAdsFile(t *testing.T) {
|
||||
|
||||
files := []string{"mainadsfile.text", "mainadsfile.text:datastream1:$DATA", "mainadsfile.text:datastream2:$DATA"}
|
||||
dataArray := []string{"Main file data.", "First data stream.", "Second data stream."}
|
||||
var tests = map[string]AdsTestInfo{
|
||||
"main-stream-first": {
|
||||
dirName: "dir", fileStreamNames: files,
|
||||
fileOrder: []int{0, 1, 2},
|
||||
},
|
||||
"second-stream-first": {
|
||||
dirName: "dir", fileStreamNames: files,
|
||||
fileOrder: []int{1, 0, 2},
|
||||
},
|
||||
"main-stream-first-already-exists": {
|
||||
dirName: "dir", fileStreamNames: files,
|
||||
fileOrder: []int{0, 1, 2},
|
||||
Overwrite: true,
|
||||
},
|
||||
"second-stream-first-already-exists": {
|
||||
dirName: "dir", fileStreamNames: files,
|
||||
fileOrder: []int{1, 0, 2},
|
||||
Overwrite: true,
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tempdir := rtest.TempDir(t)
|
||||
|
||||
nodes := getOrderedAdsNodes(test.dirName, test.fileOrder, test.fileStreamNames[:], dataArray)
|
||||
|
||||
res := setup(t, nodes)
|
||||
|
||||
if test.Overwrite {
|
||||
|
||||
os.Mkdir(path.Join(tempdir, test.dirName), os.ModeDir)
|
||||
//Create existing files
|
||||
for _, f := range files {
|
||||
data := []byte("This is some dummy data.")
|
||||
|
||||
filepath := path.Join(tempdir, test.dirName, f)
|
||||
// Write the data to the file
|
||||
err := os.WriteFile(path.Clean(filepath), data, 0644)
|
||||
rtest.OK(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
res.SelectFilter = adsConflictFilter
|
||||
|
||||
err := res.RestoreTo(ctx, tempdir)
|
||||
rtest.OK(t, err)
|
||||
|
||||
for _, fileIndex := range test.fileOrder {
|
||||
currentFile := test.fileStreamNames[fileIndex]
|
||||
|
||||
fp := path.Join(tempdir, test.dirName, currentFile)
|
||||
|
||||
fi, err1 := os.Stat(fp)
|
||||
rtest.Assert(t, !errors.Is(err1, os.ErrNotExist), "The file "+currentFile+" does not exist")
|
||||
|
||||
size := fi.Size()
|
||||
rtest.Assert(t, size > 0, "The file "+currentFile+" exists but is empty")
|
||||
|
||||
content, err := os.ReadFile(fp)
|
||||
rtest.OK(t, err)
|
||||
contentString := string(content)
|
||||
rtest.Assert(t, contentString == dataArray[fileIndex], "The file "+currentFile+" exists but the content is not overwritten")
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getOrderedAdsNodes(dir string, order []int, allFileNames []string, dataArray []string) []NamedNode {
|
||||
|
||||
getFileNodes := func() []NamedNode {
|
||||
nodes := []NamedNode{}
|
||||
|
||||
for _, index := range order {
|
||||
file := allFileNames[index]
|
||||
nodes = append(nodes, NamedNode{
|
||||
name: file,
|
||||
node: File{
|
||||
ModTime: time.Now(),
|
||||
Data: dataArray[index],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
return []NamedNode{
|
||||
{
|
||||
name: dir,
|
||||
node: OrderedDir{
|
||||
Mode: normalizeFileMode(0750 | os.ModeDir),
|
||||
ModTime: time.Now(),
|
||||
Nodes: getFileNodes(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func adsConflictFilter(item string, dstpath string, node *restic.Node) (selectedForRestore bool, childMayBeSelected bool) {
|
||||
switch filepath.ToSlash(item) {
|
||||
case "/dir":
|
||||
childMayBeSelected = true
|
||||
case "/dir/mainadsfile.text":
|
||||
selectedForRestore = true
|
||||
childMayBeSelected = false
|
||||
case "/dir/mainadsfile.text:datastream1:$DATA":
|
||||
selectedForRestore = true
|
||||
childMayBeSelected = false
|
||||
case "/dir/mainadsfile.text:datastream2:$DATA":
|
||||
selectedForRestore = true
|
||||
childMayBeSelected = false
|
||||
case "/dir/dir":
|
||||
selectedForRestore = true
|
||||
childMayBeSelected = true
|
||||
case "/dir/dir:dirstream1:$DATA":
|
||||
selectedForRestore = true
|
||||
childMayBeSelected = false
|
||||
case "/dir/dir:dirstream2:$DATA":
|
||||
selectedForRestore = true
|
||||
childMayBeSelected = false
|
||||
}
|
||||
return selectedForRestore, childMayBeSelected
|
||||
}
|
||||
|
||||
func setup(t *testing.T, namedNodes []NamedNode) *Restorer {
|
||||
|
||||
repo := repository.TestRepository(t)
|
||||
|
||||
sn, _ := saveOrderedSnapshot(t, repo, OrderedSnapshot{
|
||||
nodes: namedNodes,
|
||||
})
|
||||
|
||||
res := NewRestorer(repo, sn, false, nil)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func saveDirOrdered(t testing.TB, repo restic.Repository, namedNodes []NamedNode, inode uint64) restic.ID {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
tree := &restic.Tree{}
|
||||
for _, namedNode := range namedNodes {
|
||||
name := namedNode.name
|
||||
n := namedNode.node
|
||||
inode++
|
||||
switch node := n.(type) {
|
||||
case File:
|
||||
fi := n.(File).Inode
|
||||
if fi == 0 {
|
||||
fi = inode
|
||||
}
|
||||
lc := n.(File).Links
|
||||
if lc == 0 {
|
||||
lc = 1
|
||||
}
|
||||
fc := []restic.ID{}
|
||||
if len(n.(File).Data) > 0 {
|
||||
fc = append(fc, saveFile(t, repo, node))
|
||||
}
|
||||
mode := node.Mode
|
||||
if mode == 0 {
|
||||
mode = 0644
|
||||
}
|
||||
err := tree.Insert(&restic.Node{
|
||||
Type: "file",
|
||||
Mode: mode,
|
||||
ModTime: node.ModTime,
|
||||
Name: name,
|
||||
UID: uint32(os.Getuid()),
|
||||
GID: uint32(os.Getgid()),
|
||||
Content: fc,
|
||||
Size: uint64(len(n.(File).Data)),
|
||||
Inode: fi,
|
||||
Links: lc,
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
case Dir:
|
||||
id := saveDir(t, repo, node.Nodes, inode)
|
||||
|
||||
mode := node.Mode
|
||||
if mode == 0 {
|
||||
mode = 0755
|
||||
}
|
||||
|
||||
err := tree.Insert(&restic.Node{
|
||||
Type: "dir",
|
||||
Mode: mode,
|
||||
ModTime: node.ModTime,
|
||||
Name: name,
|
||||
UID: uint32(os.Getuid()),
|
||||
GID: uint32(os.Getgid()),
|
||||
Subtree: &id,
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
case OrderedDir:
|
||||
id := saveDirOrdered(t, repo, node.Nodes, inode)
|
||||
|
||||
mode := node.Mode
|
||||
if mode == 0 {
|
||||
mode = 0755
|
||||
}
|
||||
|
||||
err := tree.Insert(&restic.Node{
|
||||
Type: "dir",
|
||||
Mode: mode,
|
||||
ModTime: node.ModTime,
|
||||
Name: name,
|
||||
UID: uint32(os.Getuid()),
|
||||
GID: uint32(os.Getgid()),
|
||||
Subtree: &id,
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
default:
|
||||
t.Fatalf("unknown node type %T", node)
|
||||
}
|
||||
}
|
||||
|
||||
id, err := restic.SaveTree(ctx, repo, tree)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func saveOrderedSnapshot(t testing.TB, repo restic.Repository, snapshot OrderedSnapshot) (*restic.Snapshot, restic.ID) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
wg, wgCtx := errgroup.WithContext(ctx)
|
||||
repo.StartPackUploader(wgCtx, wg)
|
||||
treeID := saveDirOrdered(t, repo, snapshot.nodes, 1000)
|
||||
err := repo.Flush(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sn, err := restic.NewSnapshot([]string{"test"}, nil, "", time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sn.Tree = &treeID
|
||||
id, err := restic.SaveSnapshot(ctx, repo, sn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return sn, id
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/archiver"
|
||||
"github.com/restic/restic/internal/fs"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
@ -180,19 +181,25 @@ func (p *Progress) CompleteItem(item string, previous, current *restic.Node, s a
|
|||
case previous == nil:
|
||||
p.printer.CompleteItem("file new", item, s, d)
|
||||
p.mu.Lock()
|
||||
p.summary.Files.New++
|
||||
if fs.IsMainFile(item) {
|
||||
p.summary.Files.New++
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
case previous.Equals(*current):
|
||||
p.printer.CompleteItem("file unchanged", item, s, d)
|
||||
p.mu.Lock()
|
||||
p.summary.Files.Unchanged++
|
||||
if fs.IsMainFile(item) {
|
||||
p.summary.Files.Unchanged++
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
default:
|
||||
p.printer.CompleteItem("file modified", item, s, d)
|
||||
p.mu.Lock()
|
||||
p.summary.Files.Changed++
|
||||
if fs.IsMainFile(item) {
|
||||
p.summary.Files.Changed++
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/fs"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
|
@ -61,8 +62,15 @@ func (p *Progress) update(runtime time.Duration, final bool) {
|
|||
func (p *Progress) AddFile(size uint64) {
|
||||
p.m.Lock()
|
||||
defer p.m.Unlock()
|
||||
|
||||
p.filesTotal++
|
||||
|
||||
p.allBytesTotal += size
|
||||
}
|
||||
|
||||
// AddSize starts tracking a new file with the given size
|
||||
func (p *Progress) AddSize(size uint64) {
|
||||
p.m.Lock()
|
||||
defer p.m.Unlock()
|
||||
p.allBytesTotal += size
|
||||
}
|
||||
|
||||
|
@ -81,7 +89,9 @@ func (p *Progress) AddProgress(name string, bytesWrittenPortion uint64, bytesTot
|
|||
p.allBytesWritten += bytesWrittenPortion
|
||||
if entry.bytesWritten == entry.bytesTotal {
|
||||
delete(p.progressInfoMap, name)
|
||||
p.filesFinished++
|
||||
if fs.IsMainFile(name) {
|
||||
p.filesFinished++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue