2019-08-13 16:27:20 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
|
|
|
|
"github.com/restic/restic/internal/debug"
|
|
|
|
"github.com/restic/restic/internal/errors"
|
|
|
|
"github.com/restic/restic/internal/restic"
|
|
|
|
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
)
|
|
|
|
|
|
|
|
var cmdCopy = &cobra.Command{
|
|
|
|
Use: "copy [flags] [snapshotID ...]",
|
|
|
|
Short: "Copy snapshots from one repository to another",
|
|
|
|
Long: `
|
|
|
|
The "copy" command copies one or more snapshots from one repository to another
|
|
|
|
repository. Note that this will have to read (download) and write (upload) the
|
|
|
|
entire snapshot(s) due to the different encryption keys on the source and
|
|
|
|
destination, and that transferred files are not re-chunked, which may break
|
|
|
|
their deduplication.
|
|
|
|
`,
|
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
|
|
return runCopy(copyOptions, globalOptions, args)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// CopyOptions bundles all options for the copy command.
|
|
|
|
type CopyOptions struct {
|
|
|
|
Repo string
|
|
|
|
PasswordFile string
|
|
|
|
PasswordCommand string
|
|
|
|
KeyHint string
|
|
|
|
Hosts []string
|
|
|
|
Tags restic.TagLists
|
|
|
|
Paths []string
|
|
|
|
}
|
|
|
|
|
|
|
|
var copyOptions CopyOptions
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
cmdRoot.AddCommand(cmdCopy)
|
|
|
|
|
|
|
|
f := cmdCopy.Flags()
|
|
|
|
f.StringVarP(©Options.Repo, "repo2", "", os.Getenv("RESTIC_REPOSITORY2"), "destination repository to copy snapshots to (default: $RESTIC_REPOSITORY2)")
|
|
|
|
f.StringVarP(©Options.PasswordFile, "password-file2", "", os.Getenv("RESTIC_PASSWORD_FILE2"), "read the destination repository password from a file (default: $RESTIC_PASSWORD_FILE2)")
|
|
|
|
f.StringVarP(©Options.KeyHint, "key-hint2", "", os.Getenv("RESTIC_KEY_HINT2"), "key ID of key to try decrypting the destination repository first (default: $RESTIC_KEY_HINT2)")
|
|
|
|
f.StringVarP(©Options.PasswordCommand, "password-command2", "", os.Getenv("RESTIC_PASSWORD_COMMAND2"), "specify a shell command to obtain a password for the destination repository (default: $RESTIC_PASSWORD_COMMAND2)")
|
|
|
|
|
|
|
|
f.StringArrayVarP(©Options.Hosts, "host", "H", nil, "only consider snapshots for this `host`, when no snapshot ID is given (can be specified multiple times)")
|
|
|
|
f.Var(©Options.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot ID is given")
|
|
|
|
f.StringArrayVar(©Options.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot ID is given")
|
|
|
|
}
|
|
|
|
|
|
|
|
func runCopy(opts CopyOptions, gopts GlobalOptions, args []string) error {
|
|
|
|
if opts.Repo == "" {
|
|
|
|
return errors.Fatal("Please specify a destination repository location (--repo2)")
|
|
|
|
}
|
|
|
|
var err error
|
|
|
|
dstGopts := gopts
|
|
|
|
dstGopts.Repo = opts.Repo
|
|
|
|
dstGopts.PasswordFile = opts.PasswordFile
|
|
|
|
dstGopts.PasswordCommand = opts.PasswordCommand
|
|
|
|
dstGopts.KeyHint = opts.KeyHint
|
|
|
|
dstGopts.password, err = resolvePassword(dstGopts, "RESTIC_PASSWORD2")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
dstGopts.password, err = ReadPassword(dstGopts, "enter password for destination repository: ")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(gopts.ctx)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
srcRepo, err := OpenRepository(gopts)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
dstRepo, err := OpenRepository(dstGopts)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
srcLock, err := lockRepo(srcRepo)
|
|
|
|
defer unlockRepo(srcLock)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
dstLock, err := lockRepo(dstRepo)
|
|
|
|
defer unlockRepo(dstLock)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
debug.Log("Loading source index")
|
|
|
|
if err := srcRepo.LoadIndex(ctx); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
debug.Log("Loading destination index")
|
|
|
|
if err := dstRepo.LoadIndex(ctx); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for sn := range FindFilteredSnapshots(ctx, srcRepo, opts.Hosts, opts.Tags, opts.Paths, args) {
|
|
|
|
Verbosef("snapshot %s of %v at %s)\n", sn.ID().Str(), sn.Paths, sn.Time)
|
|
|
|
Verbosef(" copy started, this may take a while...\n")
|
|
|
|
|
|
|
|
if err := copyTree(ctx, srcRepo, dstRepo, *sn.Tree); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
debug.Log("tree copied")
|
|
|
|
|
|
|
|
if err = dstRepo.Flush(ctx); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-06-19 10:15:37 +00:00
|
|
|
debug.Log("flushed packs and saved index")
|
2019-08-13 16:27:20 +00:00
|
|
|
|
|
|
|
// save snapshot
|
|
|
|
sn.Parent = nil // Parent does not have relevance in the new repo.
|
|
|
|
sn.Original = nil // Original does not have relevance in the new repo.
|
|
|
|
newID, err := dstRepo.SaveJSONUnpacked(ctx, restic.SnapshotFile, sn)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
Verbosef("snapshot %s saved\n", newID.Str())
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func copyTree(ctx context.Context, srcRepo, dstRepo restic.Repository, treeID restic.ID) error {
|
|
|
|
tree, err := srcRepo.LoadTree(ctx, treeID)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("LoadTree(%v) returned error %v", treeID.Str(), err)
|
|
|
|
}
|
|
|
|
// Do we already have this tree blob?
|
|
|
|
if !dstRepo.Index().Has(treeID, restic.TreeBlob) {
|
|
|
|
newTreeID, err := dstRepo.SaveTree(ctx, tree)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("SaveTree(%v) returned error %v", treeID.Str(), err)
|
|
|
|
}
|
|
|
|
// Assurance only.
|
|
|
|
if newTreeID != treeID {
|
|
|
|
return fmt.Errorf("SaveTree(%v) returned unexpected id %s", treeID.Str(), newTreeID.Str())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: keep only one (big) buffer around.
|
|
|
|
// TODO: parellize this stuff, likely only needed inside a tree.
|
|
|
|
|
|
|
|
for _, entry := range tree.Nodes {
|
|
|
|
// If it is a directory, recurse
|
|
|
|
if entry.Type == "dir" && entry.Subtree != nil {
|
|
|
|
if err := copyTree(ctx, srcRepo, dstRepo, *entry.Subtree); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Copy the blobs for this file.
|
|
|
|
for _, blobID := range entry.Content {
|
|
|
|
// Do we already have this data blob?
|
|
|
|
if dstRepo.Index().Has(blobID, restic.DataBlob) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
debug.Log("Copying blob %s\n", blobID.Str())
|
2020-08-22 10:14:42 +00:00
|
|
|
buf, err := srcRepo.LoadBlob(ctx, restic.DataBlob, blobID, nil)
|
2019-08-13 16:27:20 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("LoadBlob(%v) returned error %v", blobID, err)
|
|
|
|
}
|
|
|
|
|
2020-08-22 10:14:42 +00:00
|
|
|
_, _, err = dstRepo.SaveBlob(ctx, restic.DataBlob, buf, blobID, false)
|
2019-08-13 16:27:20 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("SaveBlob(%v) returned error %v", blobID, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|