Files
kopia/cli/app.go
Jarek Kowalski ad4b222939 cli: added support for copying (or moving) snapshot history (#703)
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.

Supported combinations are:

Source:             Destination         Behavior
---------------------------------------------------
@host1              @host2              copy snapshots from all users of host1
user1@host1         @host2              copy all snapshots to user1@host2
user1@host1         user2@host2         copy all snapshots to user2@host2
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

When --move is specified, the matching source snapshots are also deleted.

* cli: upgraded kingpin to latest version (not tagged)

This allows using `EnableFileExpansion` to disable treating
arguments prefixed with "@" as file includes.
2020-12-04 16:34:55 -08:00

204 lines
6.4 KiB
Go

// Package cli implements command-line commands for the Kopia.
package cli
import (
"context"
"net/http"
"os"
"github.com/alecthomas/kingpin"
"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/apiclient"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/blob"
"github.com/kopia/kopia/repo/content"
"github.com/kopia/kopia/repo/logging"
"github.com/kopia/kopia/repo/maintenance"
"github.com/kopia/kopia/snapshot/snapshotmaintenance"
)
var log = logging.GetContextLoggerFunc("kopia/cli")
var (
defaultColor = color.New()
warningColor = color.New(color.FgYellow)
errorColor = color.New(color.FgHiRed)
)
var (
app = kingpin.New("kopia", "Kopia - Online Backup").Author("http://kopia.github.io/")
enableAutomaticMaintenance = app.Flag("auto-maintenance", "Automatic maintenance").Default("true").Hidden().Bool()
_ = app.Flag("help-full", "Show help for all commands, including hidden").Action(helpFullAction).Bool()
repositoryCommands = app.Command("repository", "Commands to manipulate repository.").Alias("repo")
cacheCommands = app.Command("cache", "Commands to manipulate local cache").Hidden()
snapshotCommands = app.Command("snapshot", "Commands to manipulate snapshots.").Alias("snap")
policyCommands = app.Command("policy", "Commands to manipulate snapshotting policies.").Alias("policies")
serverCommands = app.Command("server", "Commands to control HTTP API server.")
manifestCommands = app.Command("manifest", "Low-level commands to manipulate manifest items.").Hidden()
contentCommands = app.Command("content", "Commands to manipulate content in repository.").Alias("contents").Hidden()
blobCommands = app.Command("blob", "Commands to manipulate BLOBs.").Hidden()
indexCommands = app.Command("index", "Commands to manipulate content index.").Hidden()
benchmarkCommands = app.Command("benchmark", "Commands to test performance of algorithms.").Hidden()
maintenanceCommands = app.Command("maintenance", "Maintenance commands.").Hidden().Alias("gc")
)
func helpFullAction(ctx *kingpin.ParseContext) error {
_ = app.UsageForContextWithTemplate(ctx, 0, kingpin.DefaultUsageTemplate)
os.Exit(0)
return nil
}
func noRepositoryAction(act func(ctx context.Context) error) func(ctx *kingpin.ParseContext) error {
return func(_ *kingpin.ParseContext) error {
return act(rootContext())
}
}
func serverAction(act func(ctx context.Context, cli *apiclient.KopiaAPIClient) error) func(ctx *kingpin.ParseContext) error {
return func(_ *kingpin.ParseContext) error {
opts, err := serverAPIClientOptions()
if err != nil {
return errors.Wrap(err, "unable to create API client options")
}
apiClient, err := apiclient.NewKopiaAPIClient(opts)
if err != nil {
return errors.Wrap(err, "unable to create API client")
}
return act(rootContext(), apiClient)
}
}
func assertDirectRepository(act func(ctx context.Context, rep *repo.DirectRepository) error) func(ctx context.Context, rep repo.Repository) error {
return func(ctx context.Context, rep repo.Repository) error {
if rep == nil {
return act(ctx, nil)
}
// right now this assertion never fails,
// but will fail in the future when we have remote repository implementation
lr, ok := rep.(*repo.DirectRepository)
if !ok {
return errors.Errorf("operation supported only on direct repository")
}
return act(ctx, lr)
}
}
func directRepositoryAction(act func(ctx context.Context, rep *repo.DirectRepository) error) func(ctx *kingpin.ParseContext) error {
return maybeRepositoryAction(assertDirectRepository(act), true)
}
func optionalRepositoryAction(act func(ctx context.Context, rep repo.Repository) error) func(ctx *kingpin.ParseContext) error {
return maybeRepositoryAction(act, false)
}
func repositoryAction(act func(ctx context.Context, rep repo.Repository) error) func(ctx *kingpin.ParseContext) error {
return maybeRepositoryAction(act, true)
}
func rootContext() context.Context {
ctx := context.Background()
ctx = content.UsingContentCache(ctx, *enableCaching)
ctx = content.UsingListCache(ctx, *enableListCaching)
ctx = blob.WithUploadProgressCallback(ctx, func(desc string, bytesSent, totalBytes int64) {
if bytesSent >= totalBytes {
log(ctx).Debugf("Uploaded %v %v %v", desc, bytesSent, totalBytes)
progress.UploadedBytes(totalBytes)
}
})
return ctx
}
func maybeRepositoryAction(act func(ctx context.Context, rep repo.Repository) error, required bool) func(ctx *kingpin.ParseContext) error {
return func(kpc *kingpin.ParseContext) error {
return withProfiling(func() error {
ctx := rootContext()
startMemoryTracking(ctx)
defer finishMemoryTracking(ctx)
if *metricsListenAddr != "" {
mux := http.NewServeMux()
if err := initPrometheus(mux); err != nil {
return errors.Wrap(err, "unable to initialize prometheus.")
}
log(ctx).Infof("starting prometheus metrics on %v", *metricsListenAddr)
go http.ListenAndServe(*metricsListenAddr, mux) // nolint:errcheck
}
rep, err := openRepository(ctx, nil, required)
if err != nil && required {
return errors.Wrap(err, "open repository")
}
err = act(ctx, rep)
if rep != nil {
if merr := maybeRunMaintenance(ctx, rep); merr != nil {
log(ctx).Warningf("error running maintenance: %v", merr)
}
}
if rep != nil && required {
if cerr := rep.Close(ctx); cerr != nil {
return errors.Wrap(cerr, "unable to close repository")
}
}
return err
})
}
}
func maybeRunMaintenance(ctx context.Context, rep repo.Repository) error {
if !*enableAutomaticMaintenance {
return nil
}
if rep.ClientOptions().ReadOnly {
return nil
}
err := snapshotmaintenance.Run(ctx, rep, maintenance.ModeAuto, false)
if err == nil {
return nil
}
if _, ok := err.(maintenance.NotOwnedError); ok {
// do not report the NotOwnedError to the user since this is automatic maintenance.
return nil
}
return err
}
func advancedCommand(ctx context.Context) {
if os.Getenv("KOPIA_ADVANCED_COMMANDS") != "enabled" {
log(ctx).Errorf(`
This command could be dangerous or lead to repository corruption when used improperly.
Running this command is not needed for using Kopia. Instead, most users should rely on periodic repository maintenance. See https://kopia.io/docs/maintenance/ for more information.
To run this command despite the warning, set KOPIA_ADVANCED_COMMANDS=enabled
`)
os.Exit(1)
}
}
// App returns an instance of command-line application object.
func App() *kingpin.Application {
return app
}