diff --git a/internal/dump/zip.go b/internal/dump/zip.go new file mode 100644 index 000000000..1c1035b08 --- /dev/null +++ b/internal/dump/zip.go @@ -0,0 +1,64 @@ +package dump + +import ( + "archive/zip" + "context" + "io" + "path/filepath" + + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/restic" +) + +type zipDumper struct { + w *zip.Writer +} + +// Statically ensure that zipDumper implements dumper. +var _ dumper = tarDumper{} + +// WriteZip will write the contents of the given tree, encoded as a zip to the given destination. +func WriteZip(ctx context.Context, repo restic.Repository, tree *restic.Tree, rootPath string, dst io.Writer) error { + dmp := zipDumper{w: zip.NewWriter(dst)} + + err := writeDump(ctx, repo, tree, rootPath, dmp, dst) + if err != nil { + dmp.w.Close() + return err + } + + return dmp.w.Close() +} + +func (dmp zipDumper) dumpNode(ctx context.Context, node *restic.Node, repo restic.Repository) error { + relPath, err := filepath.Rel("/", node.Path) + if err != nil { + return err + } + + header := &zip.FileHeader{ + Name: filepath.ToSlash(relPath), + UncompressedSize64: node.Size, + Modified: node.ModTime, + } + header.SetMode(node.Mode) + + if IsDir(node) { + header.Name += "/" + } + + w, err := dmp.w.CreateHeader(header) + if err != nil { + return errors.Wrap(err, "ZipHeader ") + } + + if IsLink(node) { + if _, err = w.Write([]byte(node.LinkTarget)); err != nil { + return errors.Wrap(err, "Write") + } + + return nil + } + + return GetNodeData(ctx, w, repo, node) +} diff --git a/internal/dump/zip_test.go b/internal/dump/zip_test.go new file mode 100644 index 000000000..70ad00275 --- /dev/null +++ b/internal/dump/zip_test.go @@ -0,0 +1,195 @@ +package dump + +import ( + "archive/zip" + "bytes" + "context" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/restic/restic/internal/archiver" + "github.com/restic/restic/internal/fs" + rtest "github.com/restic/restic/internal/test" +) + +func TestWriteZip(t *testing.T) { + tests := []struct { + name string + args archiver.TestDir + target string + }{ + { + name: "single file in root", + args: archiver.TestDir{ + "file": archiver.TestFile{Content: "string"}, + }, + target: "/", + }, + { + name: "multiple files in root", + args: archiver.TestDir{ + "file1": archiver.TestFile{Content: "string"}, + "file2": archiver.TestFile{Content: "string"}, + }, + target: "/", + }, + { + name: "multiple files and folders in root", + args: archiver.TestDir{ + "file1": archiver.TestFile{Content: "string"}, + "file2": archiver.TestFile{Content: "string"}, + "firstDir": archiver.TestDir{ + "another": archiver.TestFile{Content: "string"}, + }, + "secondDir": archiver.TestDir{ + "another2": archiver.TestFile{Content: "string"}, + }, + }, + target: "/", + }, + { + name: "file and symlink in root", + args: archiver.TestDir{ + "file1": archiver.TestFile{Content: "string"}, + "file2": archiver.TestSymlink{Target: "file1"}, + }, + target: "/", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tmpdir, repo, cleanup := prepareTempdirRepoSrc(t, tt.args) + defer cleanup() + + arch := archiver.New(repo, fs.Track{FS: fs.Local{}}, archiver.Options{}) + + back := rtest.Chdir(t, tmpdir) + defer back() + + sn, _, err := arch.Snapshot(ctx, []string{"."}, archiver.SnapshotOptions{}) + rtest.OK(t, err) + + tree, err := repo.LoadTree(ctx, *sn.Tree) + rtest.OK(t, err) + + dst := &bytes.Buffer{} + if err := WriteZip(ctx, repo, tree, tt.target, dst); err != nil { + t.Fatalf("WriteZip() error = %v", err) + } + if err := checkZip(t, tmpdir, dst); err != nil { + t.Errorf("WriteZip() = zip does not match: %v", err) + } + }) + } +} + +func readZipFile(f *zip.File) ([]byte, error) { + rc, err := f.Open() + if err != nil { + return nil, err + } + defer rc.Close() + + b := &bytes.Buffer{} + _, err = b.ReadFrom(rc) + if err != nil { + return nil, err + } + + return b.Bytes(), nil +} + +func checkZip(t *testing.T, testDir string, srcZip *bytes.Buffer) error { + z, err := zip.NewReader(bytes.NewReader(srcZip.Bytes()), int64(srcZip.Len())) + if err != nil { + return err + } + + fileNumber := 0 + zipFiles := len(z.File) + + err = filepath.Walk(testDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.Name() != filepath.Base(testDir) { + fileNumber++ + } + return nil + }) + if err != nil { + return err + } + + for _, f := range z.File { + matchPath := filepath.Join(testDir, f.Name) + match, err := os.Lstat(matchPath) + if err != nil { + return err + } + + // check metadata, zip header contains time rounded to seconds + fileTime := match.ModTime().Truncate(time.Second) + zipTime := f.Modified + if !fileTime.Equal(zipTime) { + return fmt.Errorf("modTime does not match, got: %s, want: %s", zipTime, fileTime) + } + if f.Mode() != match.Mode() { + return fmt.Errorf("mode does not match, got: %v [%08x], want: %v [%08x]", + f.Mode(), uint32(f.Mode()), match.Mode(), uint32(match.Mode())) + } + t.Logf("Mode is %v [%08x] for %s", f.Mode(), uint32(f.Mode()), f.Name) + + switch { + case f.FileInfo().IsDir(): + filebase := filepath.ToSlash(match.Name()) + if filepath.Base(f.Name) != filebase { + return fmt.Errorf("foldernames don't match got %v want %v", filepath.Base(f.Name), filebase) + } + if !strings.HasSuffix(f.Name, "/") { + return fmt.Errorf("foldernames must end with separator got %v", f.Name) + } + case f.Mode()&os.ModeSymlink != 0: + target, err := fs.Readlink(matchPath) + if err != nil { + return err + } + linkName, err := readZipFile(f) + if err != nil { + t.Fatal(err) + } + if target != string(linkName) { + return fmt.Errorf("symlink target does not match, got %s want %s", string(linkName), target) + } + default: + if uint64(match.Size()) != f.UncompressedSize64 { + return fmt.Errorf("size does not match got %v want %v", f.UncompressedSize64, match.Size()) + } + contentsFile, err := ioutil.ReadFile(matchPath) + if err != nil { + t.Fatal(err) + } + contentsZip, err := readZipFile(f) + if err != nil { + t.Fatal(err) + } + if string(contentsZip) != string(contentsFile) { + return fmt.Errorf("contents does not match, got %s want %s", contentsZip, contentsFile) + } + } + } + + if zipFiles != fileNumber { + return fmt.Errorf("not the same amount of files got %v want %v", zipFiles, fileNumber) + } + + return nil +}