mirror of
https://github.com/octoleo/restic.git
synced 2024-12-23 03:18:55 +00:00
Merge pull request #2606 from middelink/fix-323
Add copy functionality.
This commit is contained in:
commit
ea81a0e282
13
changelog/unreleased/issue-323
Normal file
13
changelog/unreleased/issue-323
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
Enhancement: Add command for copying snapshots between repositories
|
||||||
|
|
||||||
|
We've added a copy command, allowing you to copy snapshots from one
|
||||||
|
repository to another.
|
||||||
|
|
||||||
|
Note that this process will have to read (download) and write (upload) the
|
||||||
|
entire snapshot(s) due to the different encryption keys used on the source
|
||||||
|
and destination repository. Also, the transferred files are not re-chunked,
|
||||||
|
which may break deduplication between files already stored in the
|
||||||
|
destination repo and files copied there using this command.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/323
|
||||||
|
https://github.com/restic/restic/pull/2606
|
259
cmd/restic/cmd_copy.go
Normal file
259
cmd/restic/cmd_copy.go
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
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
|
||||||
|
password 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
|
||||||
|
if opts.password != "" {
|
||||||
|
dstGopts.password = opts.password
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
dstSnapshotByOriginal := make(map[restic.ID][]*restic.Snapshot)
|
||||||
|
for sn := range FindFilteredSnapshots(ctx, dstRepo, opts.Hosts, opts.Tags, opts.Paths, nil) {
|
||||||
|
if sn.Original != nil && !sn.Original.IsNull() {
|
||||||
|
dstSnapshotByOriginal[*sn.Original] = append(dstSnapshotByOriginal[*sn.Original], sn)
|
||||||
|
}
|
||||||
|
// also consider identical snapshot copies
|
||||||
|
dstSnapshotByOriginal[*sn.ID()] = append(dstSnapshotByOriginal[*sn.ID()], sn)
|
||||||
|
}
|
||||||
|
|
||||||
|
cloner := &treeCloner{
|
||||||
|
srcRepo: srcRepo,
|
||||||
|
dstRepo: dstRepo,
|
||||||
|
visitedTrees: restic.NewIDSet(),
|
||||||
|
buf: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
for sn := range FindFilteredSnapshots(ctx, srcRepo, opts.Hosts, opts.Tags, opts.Paths, args) {
|
||||||
|
Verbosef("\nsnapshot %s of %v at %s)\n", sn.ID().Str(), sn.Paths, sn.Time)
|
||||||
|
|
||||||
|
// check whether the destination has a snapshot with the same persistent ID which has similar snapshot fields
|
||||||
|
srcOriginal := *sn.ID()
|
||||||
|
if sn.Original != nil {
|
||||||
|
srcOriginal = *sn.Original
|
||||||
|
}
|
||||||
|
if originalSns, ok := dstSnapshotByOriginal[srcOriginal]; ok {
|
||||||
|
isCopy := false
|
||||||
|
for _, originalSn := range originalSns {
|
||||||
|
if similarSnapshots(originalSn, sn) {
|
||||||
|
Verbosef("skipping source snapshot %s, was already copied to snapshot %s\n", sn.ID().Str(), originalSn.ID().Str())
|
||||||
|
isCopy = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isCopy {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Verbosef(" copy started, this may take a while...\n")
|
||||||
|
|
||||||
|
if err := cloner.copyTree(ctx, *sn.Tree); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
debug.Log("tree copied")
|
||||||
|
|
||||||
|
if err = dstRepo.Flush(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
debug.Log("flushed packs and saved index")
|
||||||
|
|
||||||
|
// save snapshot
|
||||||
|
sn.Parent = nil // Parent does not have relevance in the new repo.
|
||||||
|
// Use Original as a persistent snapshot ID
|
||||||
|
if sn.Original == nil {
|
||||||
|
sn.Original = sn.ID()
|
||||||
|
}
|
||||||
|
newID, err := dstRepo.SaveJSONUnpacked(ctx, restic.SnapshotFile, sn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
Verbosef("snapshot %s saved\n", newID.Str())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func similarSnapshots(sna *restic.Snapshot, snb *restic.Snapshot) bool {
|
||||||
|
// everything except Parent and Original must match
|
||||||
|
if !sna.Time.Equal(snb.Time) || !sna.Tree.Equal(*snb.Tree) || sna.Hostname != snb.Hostname ||
|
||||||
|
sna.Username != snb.Username || sna.UID != snb.UID || sna.GID != snb.GID ||
|
||||||
|
len(sna.Paths) != len(snb.Paths) || len(sna.Excludes) != len(snb.Excludes) ||
|
||||||
|
len(sna.Tags) != len(snb.Tags) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !sna.HasPaths(snb.Paths) || !sna.HasTags(snb.Tags) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, a := range sna.Excludes {
|
||||||
|
if a != snb.Excludes[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type treeCloner struct {
|
||||||
|
srcRepo restic.Repository
|
||||||
|
dstRepo restic.Repository
|
||||||
|
visitedTrees restic.IDSet
|
||||||
|
buf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *treeCloner) copyTree(ctx context.Context, treeID restic.ID) error {
|
||||||
|
// We have already processed this tree
|
||||||
|
if t.visitedTrees.Has(treeID) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tree, err := t.srcRepo.LoadTree(ctx, treeID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("LoadTree(%v) returned error %v", treeID.Str(), err)
|
||||||
|
}
|
||||||
|
t.visitedTrees.Insert(treeID)
|
||||||
|
|
||||||
|
// Do we already have this tree blob?
|
||||||
|
if !t.dstRepo.Index().Has(treeID, restic.TreeBlob) {
|
||||||
|
newTreeID, err := t.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: 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 := t.copyTree(ctx, *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 t.dstRepo.Index().Has(blobID, restic.DataBlob) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
debug.Log("Copying blob %s\n", blobID.Str())
|
||||||
|
t.buf, err = t.srcRepo.LoadBlob(ctx, restic.DataBlob, blobID, t.buf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("LoadBlob(%v) returned error %v", blobID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = t.dstRepo.SaveBlob(ctx, restic.DataBlob, t.buf, blobID, false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SaveBlob(%v) returned error %v", blobID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -274,7 +274,7 @@ func Exitf(exitcode int, format string, args ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolvePassword determines the password to be used for opening the repository.
|
// resolvePassword determines the password to be used for opening the repository.
|
||||||
func resolvePassword(opts GlobalOptions) (string, error) {
|
func resolvePassword(opts GlobalOptions, envStr string) (string, error) {
|
||||||
if opts.PasswordFile != "" && opts.PasswordCommand != "" {
|
if opts.PasswordFile != "" && opts.PasswordCommand != "" {
|
||||||
return "", errors.Fatalf("Password file and command are mutually exclusive options")
|
return "", errors.Fatalf("Password file and command are mutually exclusive options")
|
||||||
}
|
}
|
||||||
@ -299,7 +299,7 @@ func resolvePassword(opts GlobalOptions) (string, error) {
|
|||||||
return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile")
|
return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile")
|
||||||
}
|
}
|
||||||
|
|
||||||
if pwd := os.Getenv("RESTIC_PASSWORD"); pwd != "" {
|
if pwd := os.Getenv(envStr); pwd != "" {
|
||||||
return pwd, nil
|
return pwd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -75,14 +76,13 @@ func sameModTime(fi1, fi2 os.FileInfo) bool {
|
|||||||
return fi1.ModTime().Equal(fi2.ModTime())
|
return fi1.ModTime().Equal(fi2.ModTime())
|
||||||
}
|
}
|
||||||
|
|
||||||
// directoriesEqualContents checks if both directories contain exactly the same
|
// directoriesContentsDiff returns a diff between both directories. If these
|
||||||
// contents.
|
// contain exactly the same contents, then the diff is an empty string.
|
||||||
func directoriesEqualContents(dir1, dir2 string) bool {
|
func directoriesContentsDiff(dir1, dir2 string) string {
|
||||||
|
var out bytes.Buffer
|
||||||
ch1 := walkDir(dir1)
|
ch1 := walkDir(dir1)
|
||||||
ch2 := walkDir(dir2)
|
ch2 := walkDir(dir2)
|
||||||
|
|
||||||
changes := false
|
|
||||||
|
|
||||||
var a, b *dirEntry
|
var a, b *dirEntry
|
||||||
for {
|
for {
|
||||||
var ok bool
|
var ok bool
|
||||||
@ -106,36 +106,27 @@ func directoriesEqualContents(dir1, dir2 string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ch1 == nil {
|
if ch1 == nil {
|
||||||
fmt.Printf("+%v\n", b.path)
|
fmt.Fprintf(&out, "+%v\n", b.path)
|
||||||
changes = true
|
|
||||||
} else if ch2 == nil {
|
} else if ch2 == nil {
|
||||||
fmt.Printf("-%v\n", a.path)
|
fmt.Fprintf(&out, "-%v\n", a.path)
|
||||||
changes = true
|
} else if !a.equals(&out, b) {
|
||||||
} else if !a.equals(b) {
|
|
||||||
if a.path < b.path {
|
if a.path < b.path {
|
||||||
fmt.Printf("-%v\n", a.path)
|
fmt.Fprintf(&out, "-%v\n", a.path)
|
||||||
changes = true
|
|
||||||
a = nil
|
a = nil
|
||||||
continue
|
continue
|
||||||
} else if a.path > b.path {
|
} else if a.path > b.path {
|
||||||
fmt.Printf("+%v\n", b.path)
|
fmt.Fprintf(&out, "+%v\n", b.path)
|
||||||
changes = true
|
|
||||||
b = nil
|
b = nil
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%%%v\n", a.path)
|
fmt.Fprintf(&out, "%%%v\n", a.path)
|
||||||
changes = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a, b = nil, nil
|
a, b = nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if changes {
|
return out.String()
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type dirStat struct {
|
type dirStat struct {
|
||||||
|
@ -4,25 +4,26 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *dirEntry) equals(other *dirEntry) bool {
|
func (e *dirEntry) equals(out io.Writer, other *dirEntry) bool {
|
||||||
if e.path != other.path {
|
if e.path != other.path {
|
||||||
fmt.Fprintf(os.Stderr, "%v: path does not match (%v != %v)\n", e.path, e.path, other.path)
|
fmt.Fprintf(out, "%v: path does not match (%v != %v)\n", e.path, e.path, other.path)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.fi.Mode() != other.fi.Mode() {
|
if e.fi.Mode() != other.fi.Mode() {
|
||||||
fmt.Fprintf(os.Stderr, "%v: mode does not match (%v != %v)\n", e.path, e.fi.Mode(), other.fi.Mode())
|
fmt.Fprintf(out, "%v: mode does not match (%v != %v)\n", e.path, e.fi.Mode(), other.fi.Mode())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !sameModTime(e.fi, other.fi) {
|
if !sameModTime(e.fi, other.fi) {
|
||||||
fmt.Fprintf(os.Stderr, "%v: ModTime does not match (%v != %v)\n", e.path, e.fi.ModTime(), other.fi.ModTime())
|
fmt.Fprintf(out, "%v: ModTime does not match (%v != %v)\n", e.path, e.fi.ModTime(), other.fi.ModTime())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,17 +31,17 @@ func (e *dirEntry) equals(other *dirEntry) bool {
|
|||||||
stat2, _ := other.fi.Sys().(*syscall.Stat_t)
|
stat2, _ := other.fi.Sys().(*syscall.Stat_t)
|
||||||
|
|
||||||
if stat.Uid != stat2.Uid {
|
if stat.Uid != stat2.Uid {
|
||||||
fmt.Fprintf(os.Stderr, "%v: UID does not match (%v != %v)\n", e.path, stat.Uid, stat2.Uid)
|
fmt.Fprintf(out, "%v: UID does not match (%v != %v)\n", e.path, stat.Uid, stat2.Uid)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if stat.Gid != stat2.Gid {
|
if stat.Gid != stat2.Gid {
|
||||||
fmt.Fprintf(os.Stderr, "%v: GID does not match (%v != %v)\n", e.path, stat.Gid, stat2.Gid)
|
fmt.Fprintf(out, "%v: GID does not match (%v != %v)\n", e.path, stat.Gid, stat2.Gid)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if stat.Nlink != stat2.Nlink {
|
if stat.Nlink != stat2.Nlink {
|
||||||
fmt.Fprintf(os.Stderr, "%v: Number of links do not match (%v != %v)\n", e.path, stat.Nlink, stat2.Nlink)
|
fmt.Fprintf(out, "%v: Number of links do not match (%v != %v)\n", e.path, stat.Nlink, stat2.Nlink)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,23 +4,24 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *dirEntry) equals(other *dirEntry) bool {
|
func (e *dirEntry) equals(out io.Writer, other *dirEntry) bool {
|
||||||
if e.path != other.path {
|
if e.path != other.path {
|
||||||
fmt.Fprintf(os.Stderr, "%v: path does not match (%v != %v)\n", e.path, e.path, other.path)
|
fmt.Fprintf(out, "%v: path does not match (%v != %v)\n", e.path, e.path, other.path)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.fi.Mode() != other.fi.Mode() {
|
if e.fi.Mode() != other.fi.Mode() {
|
||||||
fmt.Fprintf(os.Stderr, "%v: mode does not match (%v != %v)\n", e.path, e.fi.Mode(), other.fi.Mode())
|
fmt.Fprintf(out, "%v: mode does not match (%v != %v)\n", e.path, e.fi.Mode(), other.fi.Mode())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !sameModTime(e.fi, other.fi) {
|
if !sameModTime(e.fi, other.fi) {
|
||||||
fmt.Fprintf(os.Stderr, "%v: ModTime does not match (%v != %v)\n", e.path, e.fi.ModTime(), other.fi.ModTime())
|
fmt.Fprintf(out, "%v: ModTime does not match (%v != %v)\n", e.path, e.fi.ModTime(), other.fi.ModTime())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,22 +260,18 @@ func testRunPrune(t testing.TB, gopts GlobalOptions) {
|
|||||||
rtest.OK(t, runPrune(gopts))
|
rtest.OK(t, runPrune(gopts))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testSetupBackupData(t testing.TB, env *testEnvironment) string {
|
||||||
|
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
||||||
|
testRunInit(t, env.gopts)
|
||||||
|
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
||||||
|
return datafile
|
||||||
|
}
|
||||||
|
|
||||||
func TestBackup(t *testing.T) {
|
func TestBackup(t *testing.T) {
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
testSetupBackupData(t, env)
|
||||||
fd, err := os.Open(datafile)
|
|
||||||
if os.IsNotExist(errors.Cause(err)) {
|
|
||||||
t.Skipf("unable to find data file %q, skipping", datafile)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rtest.OK(t, err)
|
|
||||||
rtest.OK(t, fd.Close())
|
|
||||||
|
|
||||||
testRunInit(t, env.gopts)
|
|
||||||
|
|
||||||
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
|
||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
// first backup
|
// first backup
|
||||||
@ -317,9 +313,9 @@ func TestBackup(t *testing.T) {
|
|||||||
for i, snapshotID := range snapshotIDs {
|
for i, snapshotID := range snapshotIDs {
|
||||||
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||||
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
||||||
testRunRestore(t, env.gopts, restoredir, snapshotIDs[0])
|
testRunRestore(t, env.gopts, restoredir, snapshotID)
|
||||||
rtest.Assert(t, directoriesEqualContents(env.testdata, filepath.Join(restoredir, "testdata")),
|
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
|
||||||
"directories are not equal")
|
rtest.Assert(t, diff == "", "directories are not equal: %v", diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
@ -329,18 +325,7 @@ func TestBackupNonExistingFile(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
testSetupBackupData(t, env)
|
||||||
fd, err := os.Open(datafile)
|
|
||||||
if os.IsNotExist(errors.Cause(err)) {
|
|
||||||
t.Skipf("unable to find data file %q, skipping", datafile)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rtest.OK(t, err)
|
|
||||||
rtest.OK(t, fd.Close())
|
|
||||||
|
|
||||||
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
|
||||||
|
|
||||||
testRunInit(t, env.gopts)
|
|
||||||
globalOptions.stderr = ioutil.Discard
|
globalOptions.stderr = ioutil.Discard
|
||||||
defer func() {
|
defer func() {
|
||||||
globalOptions.stderr = os.Stderr
|
globalOptions.stderr = os.Stderr
|
||||||
@ -503,11 +488,7 @@ func TestBackupErrors(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
testSetupBackupData(t, env)
|
||||||
|
|
||||||
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
|
||||||
|
|
||||||
testRunInit(t, env.gopts)
|
|
||||||
|
|
||||||
// Assume failure
|
// Assume failure
|
||||||
inaccessibleFile := filepath.Join(env.testdata, "0", "0", "9", "0")
|
inaccessibleFile := filepath.Join(env.testdata, "0", "0", "9", "0")
|
||||||
@ -596,10 +577,7 @@ func TestBackupTags(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
testSetupBackupData(t, env)
|
||||||
testRunInit(t, env.gopts)
|
|
||||||
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
|
||||||
|
|
||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
@ -622,6 +600,121 @@ func TestBackupTags(t *testing.T) {
|
|||||||
"expected parent to be %v, got %v", parent.ID, newest.Parent)
|
"expected parent to be %v, got %v", parent.ID, newest.Parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testRunCopy(t testing.TB, srcGopts GlobalOptions, dstGopts GlobalOptions) {
|
||||||
|
copyOpts := CopyOptions{
|
||||||
|
Repo: dstGopts.Repo,
|
||||||
|
password: dstGopts.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
rtest.OK(t, runCopy(copyOpts, srcGopts, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCopy(t *testing.T) {
|
||||||
|
env, cleanup := withTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
env2, cleanup2 := withTestEnvironment(t)
|
||||||
|
defer cleanup2()
|
||||||
|
|
||||||
|
testSetupBackupData(t, env)
|
||||||
|
opts := BackupOptions{}
|
||||||
|
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts)
|
||||||
|
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "2")}, opts, env.gopts)
|
||||||
|
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "3")}, opts, env.gopts)
|
||||||
|
testRunCheck(t, env.gopts)
|
||||||
|
|
||||||
|
testRunInit(t, env2.gopts)
|
||||||
|
testRunCopy(t, env.gopts, env2.gopts)
|
||||||
|
|
||||||
|
snapshotIDs := testRunList(t, "snapshots", env.gopts)
|
||||||
|
copiedSnapshotIDs := testRunList(t, "snapshots", env2.gopts)
|
||||||
|
|
||||||
|
// Check that the copies size seems reasonable
|
||||||
|
rtest.Assert(t, len(snapshotIDs) == len(copiedSnapshotIDs), "expected %v snapshots, found %v",
|
||||||
|
len(snapshotIDs), len(copiedSnapshotIDs))
|
||||||
|
stat := dirStats(env.repo)
|
||||||
|
stat2 := dirStats(env2.repo)
|
||||||
|
sizeDiff := int64(stat.size) - int64(stat2.size)
|
||||||
|
if sizeDiff < 0 {
|
||||||
|
sizeDiff = -sizeDiff
|
||||||
|
}
|
||||||
|
rtest.Assert(t, sizeDiff < int64(stat.size)/50, "expected less than 2%% size difference: %v vs. %v",
|
||||||
|
stat.size, stat2.size)
|
||||||
|
|
||||||
|
// Check integrity of the copy
|
||||||
|
testRunCheck(t, env2.gopts)
|
||||||
|
|
||||||
|
// Check that the copied snapshots have the same tree contents as the old ones (= identical tree hash)
|
||||||
|
origRestores := make(map[string]struct{})
|
||||||
|
for i, snapshotID := range snapshotIDs {
|
||||||
|
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||||
|
origRestores[restoredir] = struct{}{}
|
||||||
|
testRunRestore(t, env.gopts, restoredir, snapshotID)
|
||||||
|
}
|
||||||
|
for i, snapshotID := range copiedSnapshotIDs {
|
||||||
|
restoredir := filepath.Join(env2.base, fmt.Sprintf("restore%d", i))
|
||||||
|
testRunRestore(t, env2.gopts, restoredir, snapshotID)
|
||||||
|
foundMatch := false
|
||||||
|
for cmpdir := range origRestores {
|
||||||
|
diff := directoriesContentsDiff(restoredir, cmpdir)
|
||||||
|
if diff == "" {
|
||||||
|
delete(origRestores, cmpdir)
|
||||||
|
foundMatch = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rtest.Assert(t, foundMatch, "found no counterpart for snapshot %v", snapshotID)
|
||||||
|
}
|
||||||
|
|
||||||
|
rtest.Assert(t, len(origRestores) == 0, "found not copied snapshots")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCopyIncremental(t *testing.T) {
|
||||||
|
env, cleanup := withTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
env2, cleanup2 := withTestEnvironment(t)
|
||||||
|
defer cleanup2()
|
||||||
|
|
||||||
|
testSetupBackupData(t, env)
|
||||||
|
opts := BackupOptions{}
|
||||||
|
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts)
|
||||||
|
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "2")}, opts, env.gopts)
|
||||||
|
testRunCheck(t, env.gopts)
|
||||||
|
|
||||||
|
testRunInit(t, env2.gopts)
|
||||||
|
testRunCopy(t, env.gopts, env2.gopts)
|
||||||
|
|
||||||
|
snapshotIDs := testRunList(t, "snapshots", env.gopts)
|
||||||
|
copiedSnapshotIDs := testRunList(t, "snapshots", env2.gopts)
|
||||||
|
|
||||||
|
// Check that the copies size seems reasonable
|
||||||
|
testRunCheck(t, env2.gopts)
|
||||||
|
rtest.Assert(t, len(snapshotIDs) == len(copiedSnapshotIDs), "expected %v snapshots, found %v",
|
||||||
|
len(snapshotIDs), len(copiedSnapshotIDs))
|
||||||
|
|
||||||
|
// check that no snapshots are copied, as there are no new ones
|
||||||
|
testRunCopy(t, env.gopts, env2.gopts)
|
||||||
|
testRunCheck(t, env2.gopts)
|
||||||
|
copiedSnapshotIDs = testRunList(t, "snapshots", env2.gopts)
|
||||||
|
rtest.Assert(t, len(snapshotIDs) == len(copiedSnapshotIDs), "still expected %v snapshots, found %v",
|
||||||
|
len(snapshotIDs), len(copiedSnapshotIDs))
|
||||||
|
|
||||||
|
// check that only new snapshots are copied
|
||||||
|
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "3")}, opts, env.gopts)
|
||||||
|
testRunCopy(t, env.gopts, env2.gopts)
|
||||||
|
testRunCheck(t, env2.gopts)
|
||||||
|
snapshotIDs = testRunList(t, "snapshots", env.gopts)
|
||||||
|
copiedSnapshotIDs = testRunList(t, "snapshots", env2.gopts)
|
||||||
|
rtest.Assert(t, len(snapshotIDs) == len(copiedSnapshotIDs), "still expected %v snapshots, found %v",
|
||||||
|
len(snapshotIDs), len(copiedSnapshotIDs))
|
||||||
|
|
||||||
|
// also test the reverse direction
|
||||||
|
testRunCopy(t, env2.gopts, env.gopts)
|
||||||
|
testRunCheck(t, env.gopts)
|
||||||
|
snapshotIDs = testRunList(t, "snapshots", env.gopts)
|
||||||
|
rtest.Assert(t, len(snapshotIDs) == len(copiedSnapshotIDs), "still expected %v snapshots, found %v",
|
||||||
|
len(copiedSnapshotIDs), len(snapshotIDs))
|
||||||
|
}
|
||||||
|
|
||||||
func testRunTag(t testing.TB, opts TagOptions, gopts GlobalOptions) {
|
func testRunTag(t testing.TB, opts TagOptions, gopts GlobalOptions) {
|
||||||
rtest.OK(t, runTag(opts, gopts, []string{}))
|
rtest.OK(t, runTag(opts, gopts, []string{}))
|
||||||
}
|
}
|
||||||
@ -630,10 +723,7 @@ func TestTag(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
testSetupBackupData(t, env)
|
||||||
testRunInit(t, env.gopts)
|
|
||||||
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
|
||||||
|
|
||||||
testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts)
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
newest, _ := testRunSnapshots(t, env.gopts)
|
newest, _ := testRunSnapshots(t, env.gopts)
|
||||||
@ -881,8 +971,8 @@ func TestRestore(t *testing.T) {
|
|||||||
restoredir := filepath.Join(env.base, "restore")
|
restoredir := filepath.Join(env.base, "restore")
|
||||||
testRunRestoreLatest(t, env.gopts, restoredir, nil, nil)
|
testRunRestoreLatest(t, env.gopts, restoredir, nil, nil)
|
||||||
|
|
||||||
rtest.Assert(t, directoriesEqualContents(env.testdata, filepath.Join(restoredir, filepath.Base(env.testdata))),
|
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, filepath.Base(env.testdata)))
|
||||||
"directories are not equal")
|
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRestoreLatest(t *testing.T) {
|
func TestRestoreLatest(t *testing.T) {
|
||||||
@ -1033,10 +1123,7 @@ func TestFind(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
datafile := testSetupBackupData(t, env)
|
||||||
testRunInit(t, env.gopts)
|
|
||||||
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
|
||||||
|
|
||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
@ -1073,10 +1160,7 @@ func TestFindJSON(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
datafile := testSetupBackupData(t, env)
|
||||||
testRunInit(t, env.gopts)
|
|
||||||
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
|
||||||
|
|
||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
@ -1199,18 +1283,7 @@ func TestPrune(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
testSetupBackupData(t, env)
|
||||||
fd, err := os.Open(datafile)
|
|
||||||
if os.IsNotExist(errors.Cause(err)) {
|
|
||||||
t.Skipf("unable to find data file %q, skipping", datafile)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rtest.OK(t, err)
|
|
||||||
rtest.OK(t, fd.Close())
|
|
||||||
|
|
||||||
testRunInit(t, env.gopts)
|
|
||||||
|
|
||||||
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
|
||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts)
|
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts)
|
||||||
@ -1317,9 +1390,9 @@ func TestHardLink(t *testing.T) {
|
|||||||
for i, snapshotID := range snapshotIDs {
|
for i, snapshotID := range snapshotIDs {
|
||||||
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||||
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
||||||
testRunRestore(t, env.gopts, restoredir, snapshotIDs[0])
|
testRunRestore(t, env.gopts, restoredir, snapshotID)
|
||||||
rtest.Assert(t, directoriesEqualContents(env.testdata, filepath.Join(restoredir, "testdata")),
|
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
|
||||||
"directories are not equal")
|
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
|
||||||
|
|
||||||
linkResults := createFileSetPerHardlink(filepath.Join(restoredir, "testdata"))
|
linkResults := createFileSetPerHardlink(filepath.Join(restoredir, "testdata"))
|
||||||
rtest.Assert(t, linksEqual(linkTests, linkResults),
|
rtest.Assert(t, linksEqual(linkTests, linkResults),
|
||||||
@ -1385,18 +1458,7 @@ func TestQuietBackup(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
testSetupBackupData(t, env)
|
||||||
fd, err := os.Open(datafile)
|
|
||||||
if os.IsNotExist(errors.Cause(err)) {
|
|
||||||
t.Skipf("unable to find data file %q, skipping", datafile)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rtest.OK(t, err)
|
|
||||||
rtest.OK(t, fd.Close())
|
|
||||||
|
|
||||||
testRunInit(t, env.gopts)
|
|
||||||
|
|
||||||
rtest.SetupTarTestFixture(t, env.testdata, datafile)
|
|
||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
env.gopts.Quiet = false
|
env.gopts.Quiet = false
|
||||||
|
@ -54,7 +54,7 @@ directories in an encrypted repository stored on different backends.
|
|||||||
if c.Name() == "version" {
|
if c.Name() == "version" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
pwd, err := resolvePassword(globalOptions)
|
pwd, err := resolvePassword(globalOptions, "RESTIC_PASSWORD")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Resolving password failed: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Resolving password failed: %v\n", err)
|
||||||
Exit(1)
|
Exit(1)
|
||||||
|
@ -82,6 +82,66 @@ Furthermore you can group the output by the same filters (host, paths, tags):
|
|||||||
1 snapshots
|
1 snapshots
|
||||||
|
|
||||||
|
|
||||||
|
Copying snapshots between repositories
|
||||||
|
======================================
|
||||||
|
|
||||||
|
In case you want to transfer snapshots between two repositories, for
|
||||||
|
example from a local to a remote repository, you can use the ``copy`` command:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ restic -r /srv/restic-repo copy --repo2 /srv/restic-repo-copy
|
||||||
|
repository d6504c63 opened successfully, password is correct
|
||||||
|
repository 3dd0878c opened successfully, password is correct
|
||||||
|
|
||||||
|
snapshot 410b18a2 of [/home/user/work] at 2020-06-09 23:15:57.305305 +0200 CEST)
|
||||||
|
copy started, this may take a while...
|
||||||
|
snapshot 7a746a07 saved
|
||||||
|
|
||||||
|
snapshot 4e5d5487 of [/home/user/work] at 2020-05-01 22:44:07.012113 +0200 CEST)
|
||||||
|
skipping snapshot 4e5d5487, was already copied to snapshot 50eb62b7
|
||||||
|
|
||||||
|
The example command copies all snapshots from the source repository
|
||||||
|
``/srv/restic-repo`` to the destination repository ``/srv/restic-repo-copy``.
|
||||||
|
Snapshots which have previously been copied between repositories will
|
||||||
|
be skipped by later copy runs.
|
||||||
|
|
||||||
|
.. note:: Note that this process will have to read (download) and write (upload) the
|
||||||
|
entire snapshot(s) due to the different encryption keys used in the source and
|
||||||
|
destination repository. Also, the transferred files are not re-chunked, which
|
||||||
|
may break deduplication between files already stored in the destination repo
|
||||||
|
and files copied there using this command.
|
||||||
|
|
||||||
|
For the destination repository ``--repo2`` the password can be read from
|
||||||
|
a file ``--password-file2`` or from a command ``--password-command2``.
|
||||||
|
Alternatively the environment variables ``$RESTIC_PASSWORD_COMMAND2`` and
|
||||||
|
``$RESTIC_PASSWORD_FILE2`` can be used. It is also possible to directly
|
||||||
|
pass the password via ``$RESTIC_PASSWORD2``. The key which should be used
|
||||||
|
for decryption can be selected by passing its ID via the flag ``--key-hint2``
|
||||||
|
or the environment variable ``$RESTIC_KEY_HINT2``.
|
||||||
|
|
||||||
|
In case the source and destination repository use the same backend, then
|
||||||
|
configuration options and environment variables to configure the backend
|
||||||
|
apply to both repositories. For example it might not be possible to specify
|
||||||
|
different accounts for the source and destination repository. You can
|
||||||
|
avoid this limitation by using the rclone backend along with remotes which
|
||||||
|
are configured in rclone.
|
||||||
|
|
||||||
|
The list of snapshots to copy can be filtered by host, path in the backup
|
||||||
|
and / or a comma-separated tag list:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ restic -r /srv/restic-repo copy --repo2 /srv/restic-repo-copy --host luigi --path /srv --tag foo,bar
|
||||||
|
|
||||||
|
It is also possible to explicitly specify the list of snapshots to copy, in
|
||||||
|
which case only these instead of all snapshots will be copied:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ restic -r /srv/restic-repo copy --repo2 /srv/restic-repo-copy 410b18a2 4e5d5487 latest
|
||||||
|
|
||||||
|
|
||||||
Checking integrity and consistency
|
Checking integrity and consistency
|
||||||
==================================
|
==================================
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ Usage help is available:
|
|||||||
cache Operate on local cache directories
|
cache Operate on local cache directories
|
||||||
cat Print internal objects to stdout
|
cat Print internal objects to stdout
|
||||||
check Check the repository for errors
|
check Check the repository for errors
|
||||||
|
copy Copy snapshots from one repository to another
|
||||||
diff Show differences between two snapshots
|
diff Show differences between two snapshots
|
||||||
dump Print a backed-up file to stdout
|
dump Print a backed-up file to stdout
|
||||||
find Find a file, a directory or restic IDs
|
find Find a file, a directory or restic IDs
|
||||||
@ -79,10 +80,9 @@ command:
|
|||||||
EXIT STATUS
|
EXIT STATUS
|
||||||
===========
|
===========
|
||||||
|
|
||||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
Exit status is 0 if the command was successful.
|
||||||
|
Exit status is 1 if there was a fatal error (no snapshot created).
|
||||||
Note that some issues such as unreadable or deleted files during backup
|
Exit status is 3 if some source data could not be read (incomplete snapshot created).
|
||||||
currently doesn't result in a non-zero error exit status.
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
restic backup [flags] FILE/DIR [FILE/DIR] ...
|
restic backup [flags] FILE/DIR [FILE/DIR] ...
|
||||||
|
Loading…
Reference in New Issue
Block a user