Files
kopia/cli/command_snapshot_copy_move_history.go
Jarek Kowalski d2288c443f cli: major refactoring (#1046)
cli: major refactoring of how CLI commands are registered

The goal is to eliminate flags as global variables to allow for better
testing. Each command and subcommand and most sets of flags are now
their own struct with 'setup()' methods that attached the flags or
subcommand to the provided parent.

This change is 94.3% mechanical, but is fully organic and hand-made.

* introduced cli.appServices interface which provides the environment in which commands run
* remove auto-maintenance global flag
* removed globals in memory_tracking.go
* removed globals from cli_progress.go
* removed globals from the update_check.go
* moved configPath into TheApp
* removed remaining globals from config.go
* refactored logfile to get rid of global variables
* removed 'app' global variable
* linter fixes
* fixed password_*.go build
* fixed BSD build
2021-05-03 10:28:00 -07:00

238 lines
8.1 KiB
Go

package cli
import (
"context"
"strings"
"github.com/alecthomas/kingpin"
"github.com/pkg/errors"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/snapshot"
)
type commandSnapshotCopyMoveHistory struct {
snapshotCopyOrMoveDryRun bool
snapshotCopyOrMoveSource string
snapshotCopyOrMoveDestination string
}
func (c *commandSnapshotCopyMoveHistory) setup(svc appServices, parent commandParent, isMove bool) {
var cmd *kingpin.CmdClause
if isMove {
cmd = parent.Command("move-history", snapshotCopyMoveHelp("move"))
} else {
cmd = parent.Command("copy-history", snapshotCopyMoveHelp("copy"))
}
cmd.Flag("dry-run", "Do not actually copy snapshots, only print what would happen").Short('n').BoolVar(&c.snapshotCopyOrMoveDryRun)
cmd.Arg("source", "Source (user@host or user@host:path)").Required().StringVar(&c.snapshotCopyOrMoveSource)
cmd.Arg("destination", "Destination (defaults to current user@host)").StringVar(&c.snapshotCopyOrMoveDestination)
cmd.Action(svc.repositoryWriterAction(func(ctx context.Context, rep repo.RepositoryWriter) error {
return c.run(ctx, rep, isMove)
}))
}
func snapshotCopyMoveHelp(verb string) string {
return strings.ReplaceAll(`Performs a VERB of the history of snapshots from another user or host.
This command will VERB snapshot manifests of the specified source to the respective destination.
This is typically used when renaming a host, switching username or moving directory
around to maintain snapshot history.
Both source and destination can be specified using user@host, @host or user@host:/path
where destination values override the corresponding parts of the source, so both targeted
and mass VERB is supported.
Source: Destination Behavior
---------------------------------------------------
@host1 @host2 VERB snapshots from all users of host1
@host1 user2@host2 (disallowed as it would potentially collapse users)
@host1 user2@host2:/path2 (disallowed as it would potentially collapse paths)
user1@host1 @host2 VERB all snapshots to user1@host2
user1@host1 user2@host2 VERB all snapshots to user2@host2
user1@host1 user2@host2:/path2 (disallowed as it would potentially collapse paths)
user1@host1:/path1 @host2 VERB to user1@host2:/path1
user1@host1:/path1 user2@host2 VERB to user2@host2:/path1
user1@host1:/path1 user2@host2:/path2 VERB snapshots from single path.
`, "VERB", verb)
}
// run copies snapshot manifests of the specified source
// to the respective destination. This is typically used when renaming a host,
// switching username or moving directory around to maintain snapshot history.
//
// Both source and destination can be specified using user@host, @host or user@host:/path
// where destination values override the corresponding parts of the source, so both targeted
// and mass copying is supported.
//
// Source: Destination Behavior
// ---------------------------------------------------
// @host1 @host2 copy snapshots from all users of host1
// @host1 user2@host2 (disallowed as it would potentially collapse users)
// @host1 user2@host2:/path2 (disallowed as it would potentially collapse paths)
//
// user1@host1 @host2 copy all snapshots to user1@host2
// user1@host1 user2@host2 copy all snapshots to user2@host2
// user1@host1 user2@host2:/path2 (disallowed as it would potentially collapse paths)
//
// user1@host1:/path1 @host2 copy to user1@host2:/path1
// user1@host1:/path1 user2@host2 copy to user2@host2:/path1
// user1@host1:/path1 user2@host2:/path2 copy snapshots from single path.
func (c *commandSnapshotCopyMoveHistory) run(ctx context.Context, rep repo.RepositoryWriter, isMoveCommand bool) error {
si, di, err := c.getCopySourceAndDestination(rep)
if err != nil {
return err
}
// At this point si and di are possibly incomplete snapshot.SourceInfo
// could be hostname, user@hostname or user@hostname:/path
srcSnapshots, err := snapshot.ListSnapshots(ctx, rep, si)
if err != nil {
return errors.Wrap(err, "error listing source snapshots")
}
dstSnapshots, err := snapshot.ListSnapshots(ctx, rep, di)
if err != nil {
return errors.Wrap(err, "error listing destination snapshots")
}
for _, manifest := range srcSnapshots {
dstSource := getCopyDestination(manifest.Source, di)
if dstSource == manifest.Source {
log(ctx).Debugf("%v is the same as destination, ignoring", dstSource)
continue
}
if snapshotExists(dstSnapshots, dstSource, manifest) {
if isMoveCommand && !c.snapshotCopyOrMoveDryRun {
log(ctx).Infof("%v (%v) already exists - deleting source", dstSource, formatTimestamp(manifest.StartTime))
if err := rep.DeleteManifest(ctx, manifest.ID); err != nil {
return errors.Wrap(err, "unable to delete source manifest")
}
} else {
log(ctx).Infof("%v (%v) already exists", dstSource, formatTimestamp(manifest.StartTime))
}
continue
}
srcID := manifest.ID
log(ctx).Infof("%v %v (%v) => %v", c.getCopySnapshotAction(isMoveCommand), manifest.Source, formatTimestamp(manifest.StartTime), dstSource)
if c.snapshotCopyOrMoveDryRun {
continue
}
manifest.ID = ""
manifest.Source = dstSource
if _, err := snapshot.SaveSnapshot(ctx, rep, manifest); err != nil {
return errors.Wrap(err, "unable to save snapshot")
}
if isMoveCommand {
if err := rep.DeleteManifest(ctx, srcID); err != nil {
return errors.Wrap(err, "unable to delete source manifest")
}
}
}
return nil
}
func (c *commandSnapshotCopyMoveHistory) getCopySnapshotAction(isMoveCommand bool) string {
action := "copying"
if isMoveCommand {
action = "moving"
}
if c.snapshotCopyOrMoveDryRun {
action += " (dry run)"
}
return action
}
func (c *commandSnapshotCopyMoveHistory) getCopySourceAndDestination(rep repo.RepositoryWriter) (si, di snapshot.SourceInfo, err error) {
si, err = snapshot.ParseSourceInfo(c.snapshotCopyOrMoveSource, rep.ClientOptions().Hostname, rep.ClientOptions().Username)
if err != nil {
return si, di, errors.Wrap(err, "invalid source")
}
if c.snapshotCopyOrMoveDestination == "" {
// no destination - assume current user@hostname
di.UserName = rep.ClientOptions().Username
di.Host = rep.ClientOptions().Hostname
} else {
di, err = snapshot.ParseSourceInfo(c.snapshotCopyOrMoveDestination, rep.ClientOptions().Hostname, rep.ClientOptions().Username)
if err != nil {
return si, di, errors.Wrap(err, "invalid destination")
}
}
if di.Path != "" && si.Path == "" {
// it is illegal to specify source without path, but destination with a path
// as it would result in multiple individual paths being squished together.
return si, di, errors.Errorf("path specified on destination but not source")
}
if di.UserName != "" && si.UserName == "" {
// it is illegal to specify source without username, but destination with a username
// as it would result in multiple individual paths being squished together.
return si, di, errors.Errorf("username specified on destination but not source")
}
return si, di, nil
}
func snapshotExists(snaps []*snapshot.Manifest, src snapshot.SourceInfo, srcManifest *snapshot.Manifest) bool {
for _, s := range snaps {
if src != s.Source {
continue
}
if sameSnapshot(srcManifest, s) {
return true
}
}
return false
}
// sameSnapshot returns true if snapshot manifests have the same start time and root object ID.
func sameSnapshot(a, b *snapshot.Manifest) bool {
if !a.StartTime.Equal(b.StartTime) {
return false
}
if a.RootObjectID() != b.RootObjectID() {
return false
}
return true
}
// getCopyDestination returns the source modified by applying non-empty fields specified in the overrides.
func getCopyDestination(source, overrides snapshot.SourceInfo) snapshot.SourceInfo {
dst := source
if overrides.Host != "" {
dst.Host = overrides.Host
}
if overrides.UserName != "" {
dst.UserName = overrides.UserName
}
if overrides.Path != "" {
dst.Path = overrides.Path
}
return dst
}