diff --git a/cmd/restic/cmd_dump.go b/cmd/restic/cmd_dump.go index 978b64616..724fa172f 100644 --- a/cmd/restic/cmd_dump.go +++ b/cmd/restic/cmd_dump.go @@ -76,56 +76,138 @@ func splitPath(p string) []string { return append(s, f) } -func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.BlobLoader, prefix string, pathComponents []string, d *dump.Dumper, canWriteArchiveFunc func() error) error { +func preparePathList(pathComponentsList []string) [][]string { + pathSet := make(map[string]struct{}) + + for _, p := range pathComponentsList { + p = filepath.Clean(p) + splittedPath := splitPath(p) + if len(splittedPath) == 1 && splittedPath[0] == "" { + return [][]string{{""}} + } + + pathStr := path.Join(splittedPath...) + pathSet[pathStr] = struct{}{} + } + + // Use a set to store the filtered paths + filteredSet := make(map[string][]string) + + for _, p := range pathComponentsList { + splittedPath := splitPath(path.Clean(p)) + + isCovered := false + // Check if any prefix of the path exists in the pathSet + for i := len(splittedPath) - 1; i >= 1; i-- { + prefixPath := path.Join(splittedPath[0:i]...) + if _, exists := pathSet[prefixPath]; exists { + isCovered = true + break + } + } + if !isCovered { + pathStr := path.Join(splittedPath...) + filteredSet[pathStr] = splittedPath + } + } + + // Convert the values of the set to a list + filteredList := make([][]string, 0, len(filteredSet)) + for _, splittedPath := range filteredSet { + filteredList = append(filteredList, splittedPath) + } + + return filteredList +} + +func filterPathComponents(pathComponentsList [][]string, prefix string) [][]string { + var filteredList [][]string + for _, pathComponents := range pathComponentsList { + if len(pathComponents) > 1 && pathComponents[0] == prefix { + filteredList = append(filteredList, pathComponents[1:]) + } + } + return filteredList +} + +func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.BlobLoader, prefix string, pathComponentsList [][]string, d *dump.Dumper, canWriteArchiveFunc func() error) error { // If we print / we need to assume that there are multiple nodes at that // level in the tree. - if pathComponents[0] == "" { + if pathComponentsList[0][0] == "" { if err := canWriteArchiveFunc(); err != nil { return err } return d.DumpTree(ctx, tree, "/") } - item := filepath.Join(prefix, pathComponents[0]) - l := len(pathComponents) + // make a set of the nodes that we dumped + dumpedNodes := make(map[string]struct{}) + for _, node := range tree.Nodes { if ctx.Err() != nil { return ctx.Err() } - // If dumping something in the highest level it will just take the - // first item it finds and dump that according to the switch case below. - if node.Name == pathComponents[0] { - switch { - case l == 1 && node.Type == restic.NodeTypeFile: - return d.WriteNode(ctx, node) - case l > 1 && node.Type == restic.NodeTypeDir: - subtree, err := restic.LoadTree(ctx, repo, *node.Subtree) - if err != nil { - return errors.Wrapf(err, "cannot load subtree for %q", item) + out: + // iterate over all pathComponents + for _, pathComponents := range pathComponentsList { + item := filepath.Join(prefix, pathComponents[0]) + l := len(pathComponents) + + // If dumping something in the highest level it will just take the + // first item it finds and dump that according to the switch case below. + if node.Name == pathComponents[0] { + switch { + case l == 1 && node.Type == restic.NodeTypeFile: + d.WriteNode(ctx, node) + dumpedNodes[node.Name] = struct{}{} + break out + case l > 1 && node.Type == restic.NodeTypeDir: + subtree, err := restic.LoadTree(ctx, repo, *node.Subtree) + if err != nil { + return errors.Wrapf(err, "cannot load subtree for %q", item) + } + + newComponentsList := filterPathComponents(pathComponentsList, node.Name) + + err = printFromTree(ctx, subtree, repo, item, newComponentsList, d, canWriteArchiveFunc) + if err != nil { + return err + } + dumpedNodes[node.Name] = struct{}{} + break out + case node.Type == restic.NodeTypeDir: + if err := canWriteArchiveFunc(); err != nil { + return err + } + subtree, err := restic.LoadTree(ctx, repo, *node.Subtree) + if err != nil { + return err + } + d.DumpTree(ctx, subtree, item) + dumpedNodes[node.Name] = struct{}{} + break out + case l > 1: + return fmt.Errorf("%q should be a dir, but is a %q", item, node.Type) + case node.Type != restic.NodeTypeFile: + return fmt.Errorf("%q should be a file, but is a %q", item, node.Type) } - return printFromTree(ctx, subtree, repo, item, pathComponents[1:], d, canWriteArchiveFunc) - case node.Type == restic.NodeTypeDir: - if err := canWriteArchiveFunc(); err != nil { - return err - } - subtree, err := restic.LoadTree(ctx, repo, *node.Subtree) - if err != nil { - return err - } - return d.DumpTree(ctx, subtree, item) - case l > 1: - return fmt.Errorf("%q should be a dir, but is a %q", item, node.Type) - case node.Type != restic.NodeTypeFile: - return fmt.Errorf("%q should be a file, but is a %q", item, node.Type) } } } - return fmt.Errorf("path %q not found in snapshot", item) + + // Check if all paths were found in the snapshot + for _, item := range pathComponentsList { + if _, ok := dumpedNodes[item[0]]; !ok { + return fmt.Errorf("path %q not found in snapshot", filepath.Join(prefix, item[0])) + } + } + + return nil } func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []string) error { - if len(args) != 2 { + if len(args) < 2 { return errors.Fatal("no file and no snapshot ID specified") } @@ -136,11 +218,8 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args [] } snapshotIDString := args[0] - pathToPrint := args[1] - debug.Log("dump file %q from %q", pathToPrint, snapshotIDString) - - splittedPath := splitPath(path.Clean(pathToPrint)) + debug.Log("dump file started for %d paths from %q", (len(args) - 1), snapshotIDString) ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) if err != nil { @@ -190,7 +269,11 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args [] } d := dump.New(opts.Archive, repo, outputFileWriter) - err = printFromTree(ctx, tree, repo, "/", splittedPath, d, canWriteArchiveFunc) + defer d.Close() + + splittedPathList := preparePathList(args[1:]) + + err = printFromTree(ctx, tree, repo, "/", splittedPathList, d, canWriteArchiveFunc) if err != nil { return errors.Fatalf("cannot dump file: %v", err) } diff --git a/cmd/restic/cmd_dump_test.go b/cmd/restic/cmd_dump_test.go index aa43117ee..1e6a46922 100644 --- a/cmd/restic/cmd_dump_test.go +++ b/cmd/restic/cmd_dump_test.go @@ -25,3 +25,31 @@ func TestDumpSplitPath(t *testing.T) { rtest.Equals(t, path.result, parts) } } + +func TestDumpPreparePathList(t *testing.T) { + testPaths := []struct { + paths []string + result [][]string + }{ + { + []string{"test", "test/dir", "test/dir/sub"}, + [][]string{{"test"}}, + }, + { + []string{"/", "man", "doc", "doc/icons"}, + [][]string{{""}}, + }, + { + []string{"doc"}, + [][]string{{"doc"}}, + }, + { + []string{"man/", "doc", "doc/icons"}, + [][]string{{"man"}, {"doc"}}, + }, + } + for _, path := range testPaths { + parts := preparePathList(path.paths) + rtest.Equals(t, path.result, parts) + } +} diff --git a/internal/dump/common.go b/internal/dump/common.go index b4741302e..c92636ccb 100644 --- a/internal/dump/common.go +++ b/internal/dump/common.go @@ -1,6 +1,9 @@ package dump import ( + "archive/tar" + "archive/zip" + "compress/gzip" "context" "io" "path" @@ -14,10 +17,13 @@ import ( // A Dumper writes trees and files from a repository to a Writer // in an archive format. type Dumper struct { - cache *bloblru.Cache - format string - repo restic.Loader - w io.Writer + cache *bloblru.Cache + format string + repo restic.Loader + w io.Writer + gzipWriter *gzip.Writer + zipWriter *zip.Writer + tarWriter *tar.Writer } func New(format string, repo restic.Loader, w io.Writer) *Dumper { @@ -29,6 +35,28 @@ func New(format string, repo restic.Loader, w io.Writer) *Dumper { } } +func (d *Dumper) Close() error { + if d.tarWriter != nil { + err := d.tarWriter.Close() + if err != nil { + return errors.Wrap(err, "Close tarWriter") + } + + if d.gzipWriter != nil { + err := d.gzipWriter.Close() + if err != nil { + return errors.Wrap(err, "Close gzipWriter") + } + } + } + + if d.zipWriter != nil { + return d.zipWriter.Close() + } + + return nil +} + func (d *Dumper) DumpTree(ctx context.Context, tree *restic.Tree, rootPath string) error { ctx, cancel := context.WithCancel(ctx) defer cancel() diff --git a/internal/dump/common_test.go b/internal/dump/common_test.go index afd19df63..03f6f03e2 100644 --- a/internal/dump/common_test.go +++ b/internal/dump/common_test.go @@ -89,6 +89,8 @@ func WriteTest(t *testing.T, format string, cd CheckDump) { if err := d.DumpTree(ctx, tree, tt.target); err != nil { t.Fatalf("Dumper.Run error = %v", err) } + d.Close() + if err := cd(t, tmpdir, dst); err != nil { t.Errorf("WriteDump() = does not match: %v", err) } diff --git a/internal/dump/tar.go b/internal/dump/tar.go index c5933d4f8..f8a5b98f1 100644 --- a/internal/dump/tar.go +++ b/internal/dump/tar.go @@ -8,22 +8,16 @@ import ( "path/filepath" "github.com/restic/restic/internal/debug" - "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" ) func (d *Dumper) dumpTar(ctx context.Context, ch <-chan *restic.Node) (err error) { - w := tar.NewWriter(d.w) - - defer func() { - if err == nil { - err = w.Close() - err = errors.Wrap(err, "Close") - } - }() + if d.tarWriter == nil { + d.tarWriter = tar.NewWriter(d.w) + } for node := range ch { - if err := d.dumpNodeTar(ctx, node, w); err != nil { + if err := d.dumpNodeTar(ctx, node); err != nil { return err } } @@ -48,7 +42,7 @@ func tarIdentifier(id uint32) int { return int(id) } -func (d *Dumper) dumpNodeTar(ctx context.Context, node *restic.Node, w *tar.Writer) error { +func (d *Dumper) dumpNodeTar(ctx context.Context, node *restic.Node) error { relPath, err := filepath.Rel("/", node.Path) if err != nil { return err @@ -93,11 +87,11 @@ func (d *Dumper) dumpNodeTar(ctx context.Context, node *restic.Node, w *tar.Writ header.Name += "/" } - err = w.WriteHeader(header) + err = d.tarWriter.WriteHeader(header) if err != nil { return fmt.Errorf("writing header for %q: %w", node.Path, err) } - return d.writeNode(ctx, w, node) + return d.writeNode(ctx, d.tarWriter, node) } func parseXattrs(xattrs []restic.ExtendedAttribute) map[string]string { diff --git a/internal/dump/zip.go b/internal/dump/zip.go index 17aeb4829..0f15b256b 100644 --- a/internal/dump/zip.go +++ b/internal/dump/zip.go @@ -10,24 +10,19 @@ import ( ) func (d *Dumper) dumpZip(ctx context.Context, ch <-chan *restic.Node) (err error) { - w := zip.NewWriter(d.w) - - defer func() { - if err == nil { - err = w.Close() - err = errors.Wrap(err, "Close") - } - }() + if d.zipWriter == nil { + d.zipWriter = zip.NewWriter(d.w) + } for node := range ch { - if err := d.dumpNodeZip(ctx, node, w); err != nil { + if err := d.dumpNodeZip(ctx, node); err != nil { return err } } return nil } -func (d *Dumper) dumpNodeZip(ctx context.Context, node *restic.Node, zw *zip.Writer) error { +func (d *Dumper) dumpNodeZip(ctx context.Context, node *restic.Node) error { relPath, err := filepath.Rel("/", node.Path) if err != nil { return err @@ -47,7 +42,7 @@ func (d *Dumper) dumpNodeZip(ctx context.Context, node *restic.Node, zw *zip.Wri header.Name += "/" } - w, err := zw.CreateHeader(header) + w, err := d.zipWriter.CreateHeader(header) if err != nil { return errors.Wrap(err, "ZipHeader") }