mirror of
https://github.com/kopia/kopia.git
synced 2026-05-06 13:54:46 -04:00
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:
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
131
snapshot/restore/restore.go
Normal 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
|
||||
}
|
||||
111
snapshot/restore/tar_output.go
Normal file
111
snapshot/restore/tar_output.go
Normal 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)}
|
||||
}
|
||||
76
snapshot/restore/zip_output.go
Normal file
76
snapshot/restore/zip_output.go
Normal 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}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user