restore: support for zip, tar and tar.gz restore outputs (#482)

* restore: support for zip, tar and tar.gz restore outputs

Moved restore functionality to its own package.

* Fix enum values in the 'mode' flag

Co-authored-by: Julio López <julio+gh@kasten.io>
This commit is contained in:
Jarek Kowalski
2020-07-22 22:56:11 -07:00
committed by GitHub
parent 52e763158b
commit 8ead49b779
8 changed files with 594 additions and 118 deletions

View File

@@ -1,13 +1,19 @@
package cli
import (
"archive/zip"
"compress/gzip"
"context"
"os"
"path/filepath"
"strings"
kingpin "gopkg.in/alecthomas/kingpin.v2"
"github.com/pkg/errors"
"gopkg.in/alecthomas/kingpin.v2"
"github.com/kopia/kopia/fs/localfs"
"github.com/kopia/kopia/internal/units"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/snapshot/snapshotfs"
"github.com/kopia/kopia/snapshot/restore"
)
const (
@@ -41,23 +47,104 @@
var (
restoreCommand = app.Command("restore", restoreCommandHelp)
restoreCommandSourcePath = restoreCommand.Arg("source-path", restoreCommandSourcePathHelp).Required().String()
restoreCommandTargetPath = restoreCommand.Arg("target-path", "Path of the directory for the contents to be restored").Required().String()
restoreTargetPath = ""
restoreOverwriteDirectories = true
restoreOverwriteFiles = true
restoreMode = restoreModeAuto
)
const (
restoreModeLocal = "local"
restoreModeAuto = "auto"
restoreModeZip = "zip"
restoreModeZipNoCompress = "zip-nocompress"
restoreModeTar = "tar"
restoreModeTgz = "tgz"
)
func addRestoreFlags(cmd *kingpin.CmdClause) {
cmd.Arg("target-path", "Path of the directory for the contents to be restored").Required().StringVar(&restoreTargetPath)
cmd.Flag("overwrite-directories", "Overwrite existing directories").BoolVar(&restoreOverwriteDirectories)
cmd.Flag("overwrite-files", "Specifies whether or not to overwrite already existing files").
BoolVar(&restoreOverwriteFiles)
cmd.Flag("overwrite-files", "Specifies whether or not to overwrite already existing files").BoolVar(&restoreOverwriteFiles)
cmd.Flag("mode", "Override restore mode").EnumVar(&restoreMode, restoreModeAuto, restoreModeLocal, restoreModeZip, restoreModeZipNoCompress, restoreModeTar, restoreModeTgz)
}
func restoreOptions() localfs.CopyOptions {
return localfs.CopyOptions{
OverwriteDirectories: restoreOverwriteDirectories,
OverwriteFiles: restoreOverwriteFiles,
func restoreOutput() (restore.Output, error) {
p, err := filepath.Abs(restoreTargetPath)
if err != nil {
return nil, err
}
m := detectRestoreMode(restoreMode)
switch m {
case restoreModeLocal:
return &restore.FilesystemOutput{
TargetPath: p,
OverwriteDirectories: restoreOverwriteDirectories,
OverwriteFiles: restoreOverwriteFiles,
}, nil
case restoreModeZip, restoreModeZipNoCompress:
f, err := os.Create(restoreTargetPath)
if err != nil {
return nil, errors.Wrap(err, "unable to create output file")
}
method := zip.Deflate
if m == restoreModeZipNoCompress {
method = zip.Store
}
return restore.NewZipOutput(f, method), nil
case restoreModeTar:
f, err := os.Create(restoreTargetPath)
if err != nil {
return nil, errors.Wrap(err, "unable to create output file")
}
return restore.NewTarOutput(f), nil
case restoreModeTgz:
f, err := os.Create(restoreTargetPath)
if err != nil {
return nil, errors.Wrap(err, "unable to create output file")
}
return restore.NewTarOutput(gzip.NewWriter(f)), nil
default:
return nil, errors.Errorf("unknown mode %v", m)
}
}
func detectRestoreMode(m string) string {
if m != "auto" {
return m
}
switch {
case strings.HasSuffix(restoreTargetPath, ".zip"):
printStderr("Restoring to a zip file (%v)...\n", restoreTargetPath)
return restoreModeZip
case strings.HasSuffix(restoreTargetPath, ".tar"):
printStderr("Restoring to an uncompressed tar file (%v)...\n", restoreTargetPath)
return restoreModeTar
case strings.HasSuffix(restoreTargetPath, ".tar.gz") || strings.HasSuffix(restoreTargetPath, ".tgz"):
printStderr("Restoring to a tar+gzip file (%v)...\n", restoreTargetPath)
return restoreModeTgz
default:
printStderr("Restoring to local filesystem (%v)...\n", restoreTargetPath)
return restoreModeLocal
}
}
func printRestoreStats(st restore.Stats) {
printStderr("Restored %v files and %v directories (%v)\n", st.FileCount, st.DirCount, units.BytesStringBase10(st.TotalFileSize))
}
func runRestoreCommand(ctx context.Context, rep repo.Repository) error {
@@ -66,7 +153,19 @@ func runRestoreCommand(ctx context.Context, rep repo.Repository) error {
return err
}
return snapshotfs.RestoreRoot(ctx, rep, *restoreCommandTargetPath, oid, restoreOptions())
output, err := restoreOutput()
if err != nil {
return errors.Wrap(err, "unable to initialize output")
}
st, err := restore.Root(ctx, rep, output, oid)
if err != nil {
return err
}
printRestoreStats(st)
return nil
}
func init() {

View File

@@ -3,19 +3,32 @@
import (
"context"
"github.com/pkg/errors"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/manifest"
"github.com/kopia/kopia/snapshot/snapshotfs"
"github.com/kopia/kopia/snapshot/restore"
)
var (
snapshotRestoreCommand = snapshotCommands.Command("restore", "Restore a snapshot from the snapshot ID to the given target path")
snapshotRestoreSnapID = snapshotRestoreCommand.Arg("id", "Snapshot ID to be restored").Required().String()
snapshotRestoreTargetPath = snapshotRestoreCommand.Arg("target-path", "Path of the directory for the contents to be restored").Required().String()
snapshotRestoreCommand = snapshotCommands.Command("restore", "Restore a snapshot from the snapshot ID to the given target path")
snapshotRestoreSnapID = snapshotRestoreCommand.Arg("id", "Snapshot ID to be restored").Required().String()
)
func runSnapRestoreCommand(ctx context.Context, rep repo.Repository) error {
return snapshotfs.Restore(ctx, rep, *snapshotRestoreTargetPath, manifest.ID(*snapshotRestoreSnapID), restoreOptions())
output, err := restoreOutput()
if err != nil {
return errors.Wrap(err, "unable to initialize output")
}
st, err := restore.Snapshot(ctx, rep, output, manifest.ID(*snapshotRestoreSnapID))
if err != nil {
return err
}
printRestoreStats(st)
return nil
}
func init() {

View File

@@ -1,4 +1,5 @@
package localfs
// Package restore manages restoring filesystem snapshots.
package restore
import (
"context"
@@ -10,69 +11,76 @@
"github.com/pkg/errors"
"github.com/kopia/kopia/fs"
"github.com/kopia/kopia/fs/localfs"
)
// CopyOptions contains the options for copying a file system tree
type CopyOptions struct {
// FilesystemOutput contains the options for outputting a file system tree.
type FilesystemOutput struct {
// TargetPath for restore.
TargetPath string
// If a directory already exists, overwrite the directory.
OverwriteDirectories bool
// Indicate whether or not to overwrite existing files. When set to false,
// the copier does not modify already existing files and returns an error
// instead.
OverwriteFiles bool
}
// Copy copies e into targetPath in the local file system. If e is an
// fs.Directory, then the contents are recursively copied.
// The targetPath must not exist, except when the target path is the root
// directory. In that case, e must be a fs.Directory and its contents are copied
// to the root directory.
// Copy does not overwrite files or directories and returns an error in that
// case. It also returns an error when the the contents cannot be restored,
// for example due to an I/O error.
func Copy(ctx context.Context, targetPath string, e fs.Entry, opt CopyOptions) error {
targetPath, err := filepath.Abs(filepath.FromSlash(targetPath))
if err != nil {
return err
// BeginDirectory implements restore.Output interface.
func (o *FilesystemOutput) BeginDirectory(ctx context.Context, relativePath string, e fs.Directory) error {
path := filepath.Join(o.TargetPath, filepath.FromSlash(relativePath))
if err := o.createDirectory(ctx, path); err != nil {
return errors.Wrap(err, "error creating directory")
}
c := copier{CopyOptions: opt}
return c.copyEntry(ctx, e, targetPath)
return nil
}
type copier struct {
CopyOptions
// FinishDirectory implements restore.Output interface.
func (o *FilesystemOutput) FinishDirectory(ctx context.Context, relativePath string, e fs.Directory) error {
path := filepath.Join(o.TargetPath, filepath.FromSlash(relativePath))
if err := o.setAttributes(path, e); err != nil {
return errors.Wrap(err, "error setting attributes")
}
return nil
}
func (c *copier) copyEntry(ctx context.Context, e fs.Entry, targetPath string) error {
var err error
// Close implements restore.Output interface.
func (o *FilesystemOutput) Close(ctx context.Context) error {
return nil
}
switch e := e.(type) {
case fs.Directory:
err = c.copyDirectory(ctx, e, targetPath)
case fs.File:
err = c.copyFileContent(ctx, targetPath, e)
case fs.Symlink:
// Not yet implemented
log(ctx).Warningf("Not creating symlink %q from %v", targetPath, e)
return nil
default:
return errors.Errorf("invalid FS entry type for %q: %#v", targetPath, e)
// WriteFile implements restore.Output interface.
func (o *FilesystemOutput) WriteFile(ctx context.Context, relativePath string, f fs.File) error {
log(ctx).Infof("WriteFile %v %v", relativePath, f)
path := filepath.Join(o.TargetPath, filepath.FromSlash(relativePath))
if err := o.copyFileContent(ctx, path, f); err != nil {
return errors.Wrap(err, "error creating directory")
}
if err != nil {
return err
if err := o.setAttributes(path, f); err != nil {
return errors.Wrap(err, "error setting attributes")
}
return c.setAttributes(targetPath, e)
return nil
}
// CreateSymlink implements restore.Output interface.
func (o *FilesystemOutput) CreateSymlink(ctx context.Context, relativePath string, e fs.Symlink) error {
log(ctx).Debugf("create symlink not implemented yet")
return nil
}
// set permission, modification time and user/group ids on targetPath
func (c *copier) setAttributes(targetPath string, e fs.Entry) error {
func (o *FilesystemOutput) setAttributes(targetPath string, e fs.Entry) error {
const modBits = os.ModePerm | os.ModeSetgid | os.ModeSetuid | os.ModeSticky
le, err := NewEntry(targetPath)
le, err := localfs.NewEntry(targetPath)
if err != nil {
return errors.Wrap(err, "could not create local FS entry for "+targetPath)
}
@@ -102,37 +110,14 @@ func (c *copier) setAttributes(targetPath string, e fs.Entry) error {
return nil
}
func (c *copier) copyDirectory(ctx context.Context, d fs.Directory, targetPath string) error {
if err := c.createDirectory(ctx, targetPath); err != nil {
return err
}
return c.copyDirectoryContent(ctx, d, targetPath)
}
func (c *copier) copyDirectoryContent(ctx context.Context, d fs.Directory, targetPath string) error {
entries, err := d.Readdir(ctx)
if err != nil {
return err
}
for _, e := range entries {
if err := c.copyEntry(ctx, e, filepath.Join(targetPath, e.Name())); err != nil {
return err
}
}
return nil
}
func (c *copier) createDirectory(ctx context.Context, path string) error {
func (o *FilesystemOutput) createDirectory(ctx context.Context, path string) error {
switch stat, err := os.Stat(path); {
case os.IsNotExist(err):
return os.MkdirAll(path, 0700)
case err != nil:
return errors.Wrap(err, "failed to stat path "+path)
case stat.Mode().IsDir():
if !c.OverwriteDirectories {
if !o.OverwriteDirectories {
if empty, _ := isEmptyDirectory(path); !empty {
return errors.Errorf("non-empty directory already exists, not overwriting it: %q", path)
}
@@ -146,11 +131,11 @@ func (c *copier) createDirectory(ctx context.Context, path string) error {
}
}
func (c *copier) copyFileContent(ctx context.Context, targetPath string, f fs.File) error {
func (o *FilesystemOutput) copyFileContent(ctx context.Context, targetPath string, f fs.File) error {
switch _, err := os.Stat(targetPath); {
case os.IsNotExist(err): // copy file below
case err == nil:
if !c.OverwriteFiles {
if !o.OverwriteFiles {
return errors.Errorf("unable to create %q, it already exists", targetPath)
}

131
snapshot/restore/restore.go Normal file
View File

@@ -0,0 +1,131 @@
package restore
import (
"context"
"path"
"sync/atomic"
"github.com/pkg/errors"
"github.com/kopia/kopia/fs"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/logging"
"github.com/kopia/kopia/repo/manifest"
"github.com/kopia/kopia/repo/object"
"github.com/kopia/kopia/snapshot"
"github.com/kopia/kopia/snapshot/snapshotfs"
)
var log = logging.GetContextLoggerFunc("restore")
// Output encapsulates output for restore operation.
type Output interface {
BeginDirectory(ctx context.Context, relativePath string, e fs.Directory) error
FinishDirectory(ctx context.Context, relativePath string, e fs.Directory) error
WriteFile(ctx context.Context, relativePath string, e fs.File) error
CreateSymlink(ctx context.Context, relativePath string, e fs.Symlink) error
Close(ctx context.Context) error
}
// Stats represents restore statistics.
type Stats struct {
TotalFileSize int64
FileCount int32
DirCount int32
}
// Snapshot walks a snapshot root with given snapshot ID and restores it to the provided output.
func Snapshot(ctx context.Context, rep repo.Repository, output Output, snapID manifest.ID) (Stats, error) {
m, err := snapshot.LoadSnapshot(ctx, rep, snapID)
if err != nil {
return Stats{}, err
}
if m.RootEntry == nil {
return Stats{}, errors.Errorf("No root entry found in manifest (%v)", snapID)
}
rootEntry, err := snapshotfs.SnapshotRoot(rep, m)
if err != nil {
return Stats{}, err
}
return copyToOutput(ctx, output, rootEntry)
}
// Root walks a snapshot root with given object ID and restores it to the provided output.
func Root(ctx context.Context, rep repo.Repository, output Output, oid object.ID) (Stats, error) {
return copyToOutput(ctx, output, snapshotfs.DirectoryEntry(rep, oid, nil))
}
func copyToOutput(ctx context.Context, output Output, rootEntry fs.Entry) (Stats, error) {
c := copier{output: output}
if err := c.copyEntry(ctx, rootEntry, ""); err != nil {
return Stats{}, errors.Wrap(err, "error copying")
}
if err := c.output.Close(ctx); err != nil {
return Stats{}, errors.Wrap(err, "error closing output")
}
return c.stats, nil
}
type copier struct {
stats Stats
output Output
}
func (c *copier) copyEntry(ctx context.Context, e fs.Entry, targetPath string) error {
switch e := e.(type) {
case fs.Directory:
log(ctx).Debugf("dir: '%v'", targetPath)
return c.copyDirectory(ctx, e, targetPath)
case fs.File:
log(ctx).Debugf("file: '%v'", targetPath)
atomic.AddInt32(&c.stats.FileCount, 1)
atomic.AddInt64(&c.stats.TotalFileSize, e.Size())
return c.output.WriteFile(ctx, targetPath, e)
case fs.Symlink:
log(ctx).Debugf("symlink: '%v'", targetPath)
return c.output.CreateSymlink(ctx, targetPath, e)
default:
return errors.Errorf("invalid FS entry type for %q: %#v", targetPath, e)
}
}
func (c *copier) copyDirectory(ctx context.Context, d fs.Directory, targetPath string) error {
atomic.AddInt32(&c.stats.DirCount, 1)
if err := c.output.BeginDirectory(ctx, targetPath, d); err != nil {
return errors.Wrap(err, "create directory")
}
if err := c.copyDirectoryContent(ctx, d, targetPath); err != nil {
return errors.Wrap(err, "copy directory contents")
}
if err := c.output.FinishDirectory(ctx, targetPath, d); err != nil {
return errors.Wrap(err, "finish directory")
}
return nil
}
func (c *copier) copyDirectoryContent(ctx context.Context, d fs.Directory, targetPath string) error {
entries, err := d.Readdir(ctx)
if err != nil {
return err
}
for _, e := range entries {
if err := c.copyEntry(ctx, e, path.Join(targetPath, e.Name())); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,111 @@
package restore
import (
"archive/tar"
"context"
"io"
"github.com/pkg/errors"
"github.com/kopia/kopia/fs"
)
// TarOutput contains the options for outputting a file system tree to a tar or .tar.gz file.
type TarOutput struct {
w io.Closer
tf *tar.Writer
}
// BeginDirectory implements restore.Output interface.
func (o *TarOutput) BeginDirectory(ctx context.Context, relativePath string, d fs.Directory) error {
if relativePath == "" {
return nil
}
h := &tar.Header{
Name: relativePath + "/",
ModTime: d.ModTime(),
Mode: int64(d.Mode()),
Uid: int(d.Owner().UserID),
Gid: int(d.Owner().GroupID),
Typeflag: tar.TypeDir,
}
if err := o.tf.WriteHeader(h); err != nil {
return errors.Wrap(err, "error writing tar header")
}
return nil
}
// FinishDirectory implements restore.Output interface.
func (o *TarOutput) FinishDirectory(ctx context.Context, relativePath string, e fs.Directory) error {
return nil
}
// Close implements restore.Output interface.
func (o *TarOutput) Close(ctx context.Context) error {
if err := o.tf.Close(); err != nil {
return errors.Wrap(err, "error closing tar")
}
return o.w.Close()
}
// WriteFile implements restore.Output interface.
func (o *TarOutput) WriteFile(ctx context.Context, relativePath string, f fs.File) error {
r, err := f.Open(ctx)
if err != nil {
return errors.Wrap(err, "error opening file")
}
defer r.Close() //nolint:errcheck
h := &tar.Header{
Name: relativePath,
ModTime: f.ModTime(),
Size: f.Size(),
Mode: int64(f.Mode()),
Uid: int(f.Owner().UserID),
Gid: int(f.Owner().GroupID),
Typeflag: tar.TypeReg,
}
if err := o.tf.WriteHeader(h); err != nil {
return errors.Wrap(err, "error writing tar header")
}
if _, err := io.Copy(o.tf, r); err != nil {
return errors.Wrap(err, "error copying data to tar")
}
return nil
}
// CreateSymlink implements restore.Output interface.
func (o *TarOutput) CreateSymlink(ctx context.Context, relativePath string, l fs.Symlink) error {
target, err := l.Readlink(ctx)
if err != nil {
return errors.Wrap(err, "error reading link target")
}
h := &tar.Header{
Name: relativePath,
ModTime: l.ModTime(),
Mode: int64(l.Mode()),
Uid: int(l.Owner().UserID),
Gid: int(l.Owner().GroupID),
Typeflag: tar.TypeSymlink,
Linkname: target,
}
if err := o.tf.WriteHeader(h); err != nil {
return errors.Wrap(err, "error writing tar header")
}
return nil
}
// NewTarOutput creates new tar writer output.
func NewTarOutput(w io.WriteCloser) *TarOutput {
return &TarOutput{w, tar.NewWriter(w)}
}

View File

@@ -0,0 +1,76 @@
package restore
import (
"archive/zip"
"context"
"io"
"github.com/pkg/errors"
"github.com/kopia/kopia/fs"
)
// ZipOutput contains the options for outputting a file system tree to a zip file.
type ZipOutput struct {
w io.Closer
zf *zip.Writer
method uint16
}
// BeginDirectory implements restore.Output interface.
func (o *ZipOutput) BeginDirectory(ctx context.Context, relativePath string, e fs.Directory) error {
return nil
}
// FinishDirectory implements restore.Output interface.
func (o *ZipOutput) FinishDirectory(ctx context.Context, relativePath string, e fs.Directory) error {
return nil
}
// Close implements restore.Output interface.
func (o *ZipOutput) Close(ctx context.Context) error {
if err := o.zf.Close(); err != nil {
return errors.Wrap(err, "error closing zip")
}
return o.w.Close()
}
// WriteFile implements restore.Output interface.
func (o *ZipOutput) WriteFile(ctx context.Context, relativePath string, f fs.File) error {
r, err := f.Open(ctx)
if err != nil {
return errors.Wrap(err, "error opening file")
}
defer r.Close() //nolint:errcheck
h := &zip.FileHeader{
Name: relativePath,
Method: o.method,
}
h.Modified = f.ModTime()
h.SetMode(f.Mode())
w, err := o.zf.CreateHeader(h)
if err != nil {
return errors.Wrap(err, "error creating zip entry")
}
if _, err := io.Copy(w, r); err != nil {
return errors.Wrap(err, "error copying data to zip")
}
return nil
}
// CreateSymlink implements restore.Output interface.
func (o *ZipOutput) CreateSymlink(ctx context.Context, relativePath string, e fs.Symlink) error {
log(ctx).Debugf("create symlink not implemented yet")
return nil
}
// NewZipOutput creates new zip writer output.
func NewZipOutput(w io.WriteCloser, method uint16) *ZipOutput {
return &ZipOutput{w, zip.NewWriter(w), method}
}

View File

@@ -1,37 +0,0 @@
package snapshotfs
import (
"context"
"github.com/pkg/errors"
"github.com/kopia/kopia/fs/localfs"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/manifest"
"github.com/kopia/kopia/repo/object"
"github.com/kopia/kopia/snapshot"
)
// Restore walks a snapshot root with given snapshot ID and restores it to the local filesystem
func Restore(ctx context.Context, rep repo.Repository, targetPath string, snapID manifest.ID, opts localfs.CopyOptions) error {
m, err := snapshot.LoadSnapshot(ctx, rep, snapID)
if err != nil {
return err
}
if m.RootEntry == nil {
return errors.Errorf("No root entry found in manifest (%v)", snapID)
}
rootEntry, err := SnapshotRoot(rep, m)
if err != nil {
return err
}
return localfs.Copy(ctx, targetPath, rootEntry, opts)
}
// RestoreRoot walks a snapshot root with given object ID and restores it to the local filesystem
func RestoreRoot(ctx context.Context, rep repo.Repository, targetPath string, oid object.ID, opts localfs.CopyOptions) error {
return localfs.Copy(ctx, targetPath, DirectoryEntry(rep, oid, nil), opts)
}

View File

@@ -1,7 +1,12 @@
package endtoend_test
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"io"
"os"
"path/filepath"
"testing"
"time"
@@ -153,6 +158,46 @@ func TestSnapshotRestore(t *testing.T) {
// Restored contents should match source
compareDirs(t, source, restoreDir)
cases := []struct {
fname string
args []string
validator func(t *testing.T, fname string)
}{
// auto-detected formats
{fname: "output.zip", args: nil, validator: verifyValidZipFile},
{fname: "output.tar", args: nil, validator: verifyValidTarFile},
{fname: "output.tar.gz", args: nil, validator: verifyValidTarGzipFile},
{fname: "output.tgz", args: nil, validator: verifyValidTarGzipFile},
// forced formats
{fname: "output.nonzip.blah", args: []string{"--mode=zip"}, validator: verifyValidZipFile},
{fname: "output.nontar.blah", args: []string{"--mode=tar"}, validator: verifyValidTarFile},
{fname: "output.notargz.blah", args: []string{"--mode=tgz"}, validator: verifyValidTarGzipFile},
}
restoreArchiveDir := makeScratchDir(t)
t.Run("modes", func(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.fname, func(t *testing.T) {
t.Parallel()
fname := filepath.Join(restoreArchiveDir, tc.fname)
e.RunAndExpectSuccess(t, append([]string{"snapshot", "restore", snapID, fname}, tc.args...)...)
tc.validator(t, fname)
})
}
})
// create a directory whose name ends with '.zip' and override mode to force treating it as directory.
zipDir := filepath.Join(restoreArchiveDir, "outputdir.zip")
e.RunAndExpectSuccess(t, "snapshot", "restore", snapID, zipDir, "--mode=local")
// verify we got a directory
st, err := os.Stat(zipDir)
if err != nil || !st.IsDir() {
t.Errorf("unexpected stat() results on output.zip directory %v %v", st, err)
}
// Attempt to restore snapshot with an already-existing target directory
// It should fail because the directory is not empty
_ = os.MkdirAll(restoreFailDir, 0700)
@@ -165,3 +210,56 @@ func TestSnapshotRestore(t *testing.T) {
e.RunAndExpectFailure(t, "snapshot", "restore", "--no-overwrite-files", snapID, restoreDir)
}
func verifyValidZipFile(t *testing.T, fname string) {
t.Helper()
zr, err := zip.OpenReader(fname)
if err != nil {
t.Fatal(err)
}
defer zr.Close()
}
func verifyValidTarFile(t *testing.T, fname string) {
t.Helper()
f, err := os.Open(fname)
if err != nil {
t.Fatal(err)
}
defer f.Close()
verifyValidTarReader(t, tar.NewReader(f))
}
func verifyValidTarReader(t *testing.T, tr *tar.Reader) {
t.Helper()
_, err := tr.Next()
for err == nil {
_, err = tr.Next()
}
if err != io.EOF {
t.Errorf("invalid tar file: %v", err)
}
}
func verifyValidTarGzipFile(t *testing.T, fname string) {
t.Helper()
f, err := os.Open(fname)
if err != nil {
t.Fatal(err)
}
defer f.Close()
gz, err := gzip.NewReader(f)
if err != nil {
t.Fatal(err)
}
verifyValidTarReader(t, tar.NewReader(gz))
}