Merge pull request #5241 from MichaelEischer/cleanup-cli

Refactor CLI command initialization to use less global state
This commit is contained in:
Michael Eischer 2025-02-16 18:28:48 +01:00 committed by GitHub
commit 8c12291f56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 1135 additions and 925 deletions

View File

@ -15,6 +15,7 @@ import (
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/restic/restic/internal/archiver" "github.com/restic/restic/internal/archiver"
@ -30,7 +31,10 @@ import (
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
) )
var cmdBackup = &cobra.Command{ func newBackupCommand() *cobra.Command {
var opts BackupOptions
cmd := &cobra.Command{
Use: "backup [flags] [FILE/DIR] ...", Use: "backup [flags] [FILE/DIR] ...",
Short: "Create a new backup of files and/or directories", Short: "Create a new backup of files and/or directories",
Long: ` Long: `
@ -48,13 +52,13 @@ Exit status is 11 if the repository is already locked.
Exit status is 12 if the password is incorrect. Exit status is 12 if the password is incorrect.
`, `,
PreRun: func(_ *cobra.Command, _ []string) { PreRun: func(_ *cobra.Command, _ []string) {
if backupOptions.Host == "" { if opts.Host == "" {
hostname, err := os.Hostname() hostname, err := os.Hostname()
if err != nil { if err != nil {
debug.Log("os.Hostname() returned err: %v", err) debug.Log("os.Hostname() returned err: %v", err)
return return
} }
backupOptions.Host = hostname opts.Host = hostname
} }
}, },
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
@ -62,8 +66,12 @@ Exit status is 12 if the password is incorrect.
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus() term, cancel := setupTermstatus()
defer cancel() defer cancel()
return runBackup(cmd.Context(), backupOptions, globalOptions, term, args) return runBackup(cmd.Context(), opts, globalOptions, term, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// BackupOptions bundles all options for the backup command. // BackupOptions bundles all options for the backup command.
@ -97,64 +105,60 @@ type BackupOptions struct {
SkipIfUnchanged bool SkipIfUnchanged bool
} }
var backupOptions BackupOptions func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) {
var backupFSTestHook func(fs fs.FS) fs.FS f.StringVar(&opts.Parent, "parent", "", "use this parent `snapshot` (default: latest snapshot in the group determined by --group-by and not newer than the timestamp determined by --time)")
opts.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true}
f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')")
f.BoolVarP(&opts.Force, "force", "f", false, `force re-reading the source files/directories (overrides the "parent" flag)`)
// ErrInvalidSourceData is used to report an incomplete backup opts.ExcludePatternOptions.Add(f)
var ErrInvalidSourceData = errors.New("at least one source file could not be read")
func init() { f.BoolVarP(&opts.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems, don't cross filesystem boundaries and subvolumes")
cmdRoot.AddCommand(cmdBackup) f.StringArrayVar(&opts.ExcludeIfPresent, "exclude-if-present", nil, "takes `filename[:header]`, exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)")
f.BoolVar(&opts.ExcludeCaches, "exclude-caches", false, `excludes cache directories that are marked with a CACHEDIR.TAG file. See https://bford.info/cachedir/ for the Cache Directory Tagging Standard`)
f := cmdBackup.Flags() f.StringVar(&opts.ExcludeLargerThan, "exclude-larger-than", "", "max `size` of the files to be backed up (allowed suffixes: k/K, m/M, g/G, t/T)")
f.StringVar(&backupOptions.Parent, "parent", "", "use this parent `snapshot` (default: latest snapshot in the group determined by --group-by and not newer than the timestamp determined by --time)") f.BoolVar(&opts.Stdin, "stdin", false, "read backup from stdin")
backupOptions.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true} f.StringVar(&opts.StdinFilename, "stdin-filename", "stdin", "`filename` to use when reading from stdin")
f.VarP(&backupOptions.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')") f.BoolVar(&opts.StdinCommand, "stdin-from-command", false, "interpret arguments as command to execute and store its stdout")
f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the source files/directories (overrides the "parent" flag)`) f.Var(&opts.Tags, "tag", "add `tags` for the new snapshot in the format `tag[,tag,...]` (can be specified multiple times)")
f.UintVar(&opts.ReadConcurrency, "read-concurrency", 0, "read `n` files concurrently (default: $RESTIC_READ_CONCURRENCY or 2)")
backupOptions.ExcludePatternOptions.Add(f) f.StringVarP(&opts.Host, "host", "H", "", "set the `hostname` for the snapshot manually (default: $RESTIC_HOST). To prevent an expensive rescan use the \"parent\" flag")
f.StringVar(&opts.Host, "hostname", "", "set the `hostname` for the snapshot manually")
f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems, don't cross filesystem boundaries and subvolumes")
f.StringArrayVar(&backupOptions.ExcludeIfPresent, "exclude-if-present", nil, "takes `filename[:header]`, exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)")
f.BoolVar(&backupOptions.ExcludeCaches, "exclude-caches", false, `excludes cache directories that are marked with a CACHEDIR.TAG file. See https://bford.info/cachedir/ for the Cache Directory Tagging Standard`)
f.StringVar(&backupOptions.ExcludeLargerThan, "exclude-larger-than", "", "max `size` of the files to be backed up (allowed suffixes: k/K, m/M, g/G, t/T)")
f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin")
f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "`filename` to use when reading from stdin")
f.BoolVar(&backupOptions.StdinCommand, "stdin-from-command", false, "interpret arguments as command to execute and store its stdout")
f.Var(&backupOptions.Tags, "tag", "add `tags` for the new snapshot in the format `tag[,tag,...]` (can be specified multiple times)")
f.UintVar(&backupOptions.ReadConcurrency, "read-concurrency", 0, "read `n` files concurrently (default: $RESTIC_READ_CONCURRENCY or 2)")
f.StringVarP(&backupOptions.Host, "host", "H", "", "set the `hostname` for the snapshot manually (default: $RESTIC_HOST). To prevent an expensive rescan use the \"parent\" flag")
f.StringVar(&backupOptions.Host, "hostname", "", "set the `hostname` for the snapshot manually")
err := f.MarkDeprecated("hostname", "use --host") err := f.MarkDeprecated("hostname", "use --host")
if err != nil { if err != nil {
// MarkDeprecated only returns an error when the flag could not be found // MarkDeprecated only returns an error when the flag could not be found
panic(err) panic(err)
} }
f.StringArrayVar(&backupOptions.FilesFrom, "files-from", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)") f.StringArrayVar(&opts.FilesFrom, "files-from", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)")
f.StringArrayVar(&backupOptions.FilesFromVerbatim, "files-from-verbatim", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)") f.StringArrayVar(&opts.FilesFromVerbatim, "files-from-verbatim", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)")
f.StringArrayVar(&backupOptions.FilesFromRaw, "files-from-raw", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)") f.StringArrayVar(&opts.FilesFromRaw, "files-from-raw", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)")
f.StringVar(&backupOptions.TimeStamp, "time", "", "`time` of the backup (ex. '2012-11-01 22:08:41') (default: now)") f.StringVar(&opts.TimeStamp, "time", "", "`time` of the backup (ex. '2012-11-01 22:08:41') (default: now)")
f.BoolVar(&backupOptions.WithAtime, "with-atime", false, "store the atime for all files and directories") f.BoolVar(&opts.WithAtime, "with-atime", false, "store the atime for all files and directories")
f.BoolVar(&backupOptions.IgnoreInode, "ignore-inode", false, "ignore inode number and ctime changes when checking for modified files") f.BoolVar(&opts.IgnoreInode, "ignore-inode", false, "ignore inode number and ctime changes when checking for modified files")
f.BoolVar(&backupOptions.IgnoreCtime, "ignore-ctime", false, "ignore ctime changes when checking for modified files") f.BoolVar(&opts.IgnoreCtime, "ignore-ctime", false, "ignore ctime changes when checking for modified files")
f.BoolVarP(&backupOptions.DryRun, "dry-run", "n", false, "do not upload or write any data, just show what would be done") f.BoolVarP(&opts.DryRun, "dry-run", "n", false, "do not upload or write any data, just show what would be done")
f.BoolVar(&backupOptions.NoScan, "no-scan", false, "do not run scanner to estimate size of backup") f.BoolVar(&opts.NoScan, "no-scan", false, "do not run scanner to estimate size of backup")
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
f.BoolVar(&backupOptions.UseFsSnapshot, "use-fs-snapshot", false, "use filesystem snapshot where possible (currently only Windows VSS)") f.BoolVar(&opts.UseFsSnapshot, "use-fs-snapshot", false, "use filesystem snapshot where possible (currently only Windows VSS)")
f.BoolVar(&backupOptions.ExcludeCloudFiles, "exclude-cloud-files", false, "excludes online-only cloud files (such as OneDrive Files On-Demand)") f.BoolVar(&opts.ExcludeCloudFiles, "exclude-cloud-files", false, "excludes online-only cloud files (such as OneDrive Files On-Demand)")
} }
f.BoolVar(&backupOptions.SkipIfUnchanged, "skip-if-unchanged", false, "skip snapshot creation if identical to parent snapshot") f.BoolVar(&opts.SkipIfUnchanged, "skip-if-unchanged", false, "skip snapshot creation if identical to parent snapshot")
// parse read concurrency from env, on error the default value will be used // parse read concurrency from env, on error the default value will be used
readConcurrency, _ := strconv.ParseUint(os.Getenv("RESTIC_READ_CONCURRENCY"), 10, 32) readConcurrency, _ := strconv.ParseUint(os.Getenv("RESTIC_READ_CONCURRENCY"), 10, 32)
backupOptions.ReadConcurrency = uint(readConcurrency) opts.ReadConcurrency = uint(readConcurrency)
// parse host from env, if not exists or empty the default value will be used // parse host from env, if not exists or empty the default value will be used
if host := os.Getenv("RESTIC_HOST"); host != "" { if host := os.Getenv("RESTIC_HOST"); host != "" {
backupOptions.Host = host opts.Host = host
} }
} }
var backupFSTestHook func(fs fs.FS) fs.FS
// ErrInvalidSourceData is used to report an incomplete backup
var ErrInvalidSourceData = errors.New("at least one source file could not be read")
// filterExisting returns a slice of all existing items, or an error if no // filterExisting returns a slice of all existing items, or an error if no
// items exist at all. // items exist at all.
func filterExisting(items []string) (result []string, err error) { func filterExisting(items []string) (result []string, err error) {

View File

@ -13,9 +13,13 @@ import (
"github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/table" "github.com/restic/restic/internal/ui/table"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var cmdCache = &cobra.Command{ func newCacheCommand() *cobra.Command {
var opts CacheOptions
cmd := &cobra.Command{
Use: "cache", Use: "cache",
Short: "Operate on local cache directories", Short: "Operate on local cache directories",
Long: ` Long: `
@ -30,8 +34,12 @@ Exit status is 1 if there was any error.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
return runCache(cacheOptions, globalOptions, args) return runCache(opts, globalOptions, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// CacheOptions bundles all options for the snapshots command. // CacheOptions bundles all options for the snapshots command.
@ -41,15 +49,10 @@ type CacheOptions struct {
NoSize bool NoSize bool
} }
var cacheOptions CacheOptions func (opts *CacheOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&opts.Cleanup, "cleanup", false, "remove old cache directories")
func init() { f.UintVar(&opts.MaxAge, "max-age", 30, "max age in `days` for cache directories to be considered old")
cmdRoot.AddCommand(cmdCache) f.BoolVar(&opts.NoSize, "no-size", false, "do not output the size of the cache directories")
f := cmdCache.Flags()
f.BoolVar(&cacheOptions.Cleanup, "cleanup", false, "remove old cache directories")
f.UintVar(&cacheOptions.MaxAge, "max-age", 30, "max age in `days` for cache directories to be considered old")
f.BoolVar(&cacheOptions.NoSize, "no-size", false, "do not output the size of the cache directories")
} }
func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error { func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {

View File

@ -14,7 +14,8 @@ import (
var catAllowedCmds = []string{"config", "index", "snapshot", "key", "masterkey", "lock", "pack", "blob", "tree"} var catAllowedCmds = []string{"config", "index", "snapshot", "key", "masterkey", "lock", "pack", "blob", "tree"}
var cmdCat = &cobra.Command{ func newCatCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "cat [flags] [masterkey|config|pack ID|blob ID|snapshot ID|index ID|key ID|lock ID|tree snapshot:subfolder]", Use: "cat [flags] [masterkey|config|pack ID|blob ID|snapshot ID|index ID|key ID|lock ID|tree snapshot:subfolder]",
Short: "Print internal objects to stdout", Short: "Print internal objects to stdout",
Long: ` Long: `
@ -35,10 +36,8 @@ Exit status is 12 if the password is incorrect.
return runCat(cmd.Context(), globalOptions, args) return runCat(cmd.Context(), globalOptions, args)
}, },
ValidArgs: catAllowedCmds, ValidArgs: catAllowedCmds,
} }
return cmd
func init() {
cmdRoot.AddCommand(cmdCat)
} }
func validateCatArgs(args []string) error { func validateCatArgs(args []string) error {

View File

@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/restic/restic/internal/backend/cache" "github.com/restic/restic/internal/backend/cache"
"github.com/restic/restic/internal/checker" "github.com/restic/restic/internal/checker"
@ -22,7 +23,9 @@ import (
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
) )
var cmdCheck = &cobra.Command{ func newCheckCommand() *cobra.Command {
var opts CheckOptions
cmd := &cobra.Command{
Use: "check [flags]", Use: "check [flags]",
Short: "Check the repository for errors", Short: "Check the repository for errors",
Long: ` Long: `
@ -46,7 +49,7 @@ Exit status is 12 if the password is incorrect.
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus() term, cancel := setupTermstatus()
defer cancel() defer cancel()
summary, err := runCheck(cmd.Context(), checkOptions, globalOptions, args, term) summary, err := runCheck(cmd.Context(), opts, globalOptions, args, term)
if globalOptions.JSON { if globalOptions.JSON {
if err != nil && summary.NumErrors == 0 { if err != nil && summary.NumErrors == 0 {
summary.NumErrors = 1 summary.NumErrors = 1
@ -56,8 +59,12 @@ Exit status is 12 if the password is incorrect.
return err return err
}, },
PreRunE: func(_ *cobra.Command, _ []string) error { PreRunE: func(_ *cobra.Command, _ []string) error {
return checkFlags(checkOptions) return checkFlags(opts)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// CheckOptions bundles all options for the 'check' command. // CheckOptions bundles all options for the 'check' command.
@ -68,14 +75,9 @@ type CheckOptions struct {
WithCache bool WithCache bool
} }
var checkOptions CheckOptions func (opts *CheckOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&opts.ReadData, "read-data", false, "read all data blobs")
func init() { f.StringVar(&opts.ReadDataSubset, "read-data-subset", "", "read a `subset` of data packs, specified as 'n/t' for specific part, or either 'x%' or 'x.y%' or a size in bytes with suffixes k/K, m/M, g/G, t/T for a random subset")
cmdRoot.AddCommand(cmdCheck)
f := cmdCheck.Flags()
f.BoolVar(&checkOptions.ReadData, "read-data", false, "read all data blobs")
f.StringVar(&checkOptions.ReadDataSubset, "read-data-subset", "", "read a `subset` of data packs, specified as 'n/t' for specific part, or either 'x%' or 'x.y%' or a size in bytes with suffixes k/K, m/M, g/G, t/T for a random subset")
var ignored bool var ignored bool
f.BoolVar(&ignored, "check-unused", false, "find unused blobs") f.BoolVar(&ignored, "check-unused", false, "find unused blobs")
err := f.MarkDeprecated("check-unused", "`--check-unused` is deprecated and will be ignored") err := f.MarkDeprecated("check-unused", "`--check-unused` is deprecated and will be ignored")
@ -83,7 +85,7 @@ func init() {
// MarkDeprecated only returns an error when the flag is not found // MarkDeprecated only returns an error when the flag is not found
panic(err) panic(err)
} }
f.BoolVar(&checkOptions.WithCache, "with-cache", false, "use existing cache, only read uncached data from repository") f.BoolVar(&opts.WithCache, "with-cache", false, "use existing cache, only read uncached data from repository")
} }
func checkFlags(opts CheckOptions) error { func checkFlags(opts CheckOptions) error {

View File

@ -11,9 +11,12 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var cmdCopy = &cobra.Command{ func newCopyCommand() *cobra.Command {
var opts CopyOptions
cmd := &cobra.Command{
Use: "copy [flags] [snapshotID ...]", Use: "copy [flags] [snapshotID ...]",
Short: "Copy snapshots from one repository to another", Short: "Copy snapshots from one repository to another",
Long: ` Long: `
@ -43,8 +46,12 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runCopy(cmd.Context(), copyOptions, globalOptions, args) return runCopy(cmd.Context(), opts, globalOptions, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// CopyOptions bundles all options for the copy command. // CopyOptions bundles all options for the copy command.
@ -53,14 +60,9 @@ type CopyOptions struct {
restic.SnapshotFilter restic.SnapshotFilter
} }
var copyOptions CopyOptions func (opts *CopyOptions) AddFlags(f *pflag.FlagSet) {
opts.secondaryRepoOptions.AddFlags(f, "destination", "to copy snapshots from")
func init() { initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
cmdRoot.AddCommand(cmdCopy)
f := cmdCopy.Flags()
initSecondaryRepoOptions(f, &copyOptions.secondaryRepoOptions, "destination", "to copy snapshots from")
initMultiSnapshotFilter(f, &copyOptions.SnapshotFilter, true)
} }
func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []string) error { func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []string) error {

View File

@ -18,6 +18,7 @@ import (
"github.com/klauspost/compress/zstd" "github.com/klauspost/compress/zstd"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/restic/restic/internal/crypto" "github.com/restic/restic/internal/crypto"
@ -28,14 +29,26 @@ import (
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
) )
var cmdDebug = &cobra.Command{ func registerDebugCommand(cmd *cobra.Command) {
cmd.AddCommand(
newDebugCommand(),
)
}
func newDebugCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "debug", Use: "debug",
Short: "Debug commands", Short: "Debug commands",
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
}
cmd.AddCommand(newDebugDumpCommand())
cmd.AddCommand(newDebugExamineCommand())
return cmd
} }
var cmdDebugDump = &cobra.Command{ func newDebugDumpCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "dump [indexes|snapshots|all|packs]", Use: "dump [indexes|snapshots|all|packs]",
Short: "Dump data structures", Short: "Dump data structures",
Long: ` Long: `
@ -55,6 +68,24 @@ Exit status is 12 if the password is incorrect.
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runDebugDump(cmd.Context(), globalOptions, args) return runDebugDump(cmd.Context(), globalOptions, args)
}, },
}
return cmd
}
func newDebugExamineCommand() *cobra.Command {
var opts DebugExamineOptions
cmd := &cobra.Command{
Use: "examine pack-ID...",
Short: "Examine a pack file",
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runDebugExamine(cmd.Context(), globalOptions, opts, args)
},
}
opts.AddFlags(cmd.Flags())
return cmd
} }
type DebugExamineOptions struct { type DebugExamineOptions struct {
@ -64,16 +95,11 @@ type DebugExamineOptions struct {
ReuploadBlobs bool ReuploadBlobs bool
} }
var debugExamineOpts DebugExamineOptions func (opts *DebugExamineOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&opts.ExtractPack, "extract-pack", false, "write blobs to the current directory")
func init() { f.BoolVar(&opts.ReuploadBlobs, "reupload-blobs", false, "reupload blobs to the repository")
cmdRoot.AddCommand(cmdDebug) f.BoolVar(&opts.TryRepair, "try-repair", false, "try to repair broken blobs with single bit flips")
cmdDebug.AddCommand(cmdDebugDump) f.BoolVar(&opts.RepairByte, "repair-byte", false, "try to repair broken blobs by trying bytes")
cmdDebug.AddCommand(cmdDebugExamine)
cmdDebugExamine.Flags().BoolVar(&debugExamineOpts.ExtractPack, "extract-pack", false, "write blobs to the current directory")
cmdDebugExamine.Flags().BoolVar(&debugExamineOpts.ReuploadBlobs, "reupload-blobs", false, "reupload blobs to the repository")
cmdDebugExamine.Flags().BoolVar(&debugExamineOpts.TryRepair, "try-repair", false, "try to repair broken blobs with single bit flips")
cmdDebugExamine.Flags().BoolVar(&debugExamineOpts.RepairByte, "repair-byte", false, "try to repair broken blobs by trying bytes")
} }
func prettyPrintJSON(wr io.Writer, item interface{}) error { func prettyPrintJSON(wr io.Writer, item interface{}) error {
@ -92,7 +118,9 @@ func debugPrintSnapshots(ctx context.Context, repo *repository.Repository, wr io
return err return err
} }
fmt.Fprintf(wr, "snapshot_id: %v\n", id) if _, err := fmt.Fprintf(wr, "snapshot_id: %v\n", id); err != nil {
return err
}
return prettyPrintJSON(wr, snapshot) return prettyPrintJSON(wr, snapshot)
}) })
@ -192,16 +220,7 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string) error
} }
} }
var cmdDebugExamine = &cobra.Command{ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
Use: "examine pack-ID...",
Short: "Examine a pack file",
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runDebugExamine(cmd.Context(), globalOptions, debugExamineOpts, args)
},
}
func tryRepairWithBitflip(ctx context.Context, key *crypto.Key, input []byte, bytewise bool) []byte {
if bytewise { if bytewise {
Printf(" trying to repair blob by finding a broken byte\n") Printf(" trying to repair blob by finding a broken byte\n")
} else { } else {
@ -300,7 +319,7 @@ func tryRepairWithBitflip(ctx context.Context, key *crypto.Key, input []byte, by
return fixed return fixed
} }
func decryptUnsigned(ctx context.Context, k *crypto.Key, buf []byte) []byte { func decryptUnsigned(k *crypto.Key, buf []byte) []byte {
// strip signature at the end // strip signature at the end
l := len(buf) l := len(buf)
nonce, ct := buf[:16], buf[16:l-16] nonce, ct := buf[:16], buf[16:l-16]
@ -351,13 +370,13 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
if err != nil { if err != nil {
Warnf("error decrypting blob: %v\n", err) Warnf("error decrypting blob: %v\n", err)
if opts.TryRepair || opts.RepairByte { if opts.TryRepair || opts.RepairByte {
plaintext = tryRepairWithBitflip(ctx, key, buf, opts.RepairByte) plaintext = tryRepairWithBitflip(key, buf, opts.RepairByte)
} }
if plaintext != nil { if plaintext != nil {
outputPrefix = "repaired " outputPrefix = "repaired "
filePrefix = "repaired-" filePrefix = "repaired-"
} else { } else {
plaintext = decryptUnsigned(ctx, key, buf) plaintext = decryptUnsigned(key, buf)
err = storePlainBlob(blob.ID, "damaged-", plaintext) err = storePlainBlob(blob.ID, "damaged-", plaintext)
if err != nil { if err != nil {
return err return err

View File

@ -0,0 +1,9 @@
//go:build !debug
package main
import "github.com/spf13/cobra"
func registerDebugCommand(_ *cobra.Command) {
// No commands to register in non-debug mode
}

View File

@ -12,9 +12,13 @@ import (
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var cmdDiff = &cobra.Command{ func newDiffCommand() *cobra.Command {
var opts DiffOptions
cmd := &cobra.Command{
Use: "diff [flags] snapshotID snapshotID", Use: "diff [flags] snapshotID snapshotID",
Short: "Show differences between two snapshots", Short: "Show differences between two snapshots",
Long: ` Long: `
@ -48,8 +52,12 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runDiff(cmd.Context(), diffOptions, globalOptions, args) return runDiff(cmd.Context(), opts, globalOptions, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// DiffOptions collects all options for the diff command. // DiffOptions collects all options for the diff command.
@ -57,13 +65,8 @@ type DiffOptions struct {
ShowMetadata bool ShowMetadata bool
} }
var diffOptions DiffOptions func (opts *DiffOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&opts.ShowMetadata, "metadata", false, "print changes in metadata")
func init() {
cmdRoot.AddCommand(cmdDiff)
f := cmdDiff.Flags()
f.BoolVar(&diffOptions.ShowMetadata, "metadata", false, "print changes in metadata")
} }
func loadSnapshot(ctx context.Context, be restic.Lister, repo restic.LoaderUnpacked, desc string) (*restic.Snapshot, string, error) { func loadSnapshot(ctx context.Context, be restic.Lister, repo restic.LoaderUnpacked, desc string) (*restic.Snapshot, string, error) {

View File

@ -13,9 +13,12 @@ import (
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var cmdDump = &cobra.Command{ func newDumpCommand() *cobra.Command {
var opts DumpOptions
cmd := &cobra.Command{
Use: "dump [flags] snapshotID file", Use: "dump [flags] snapshotID file",
Short: "Print a backed-up file to stdout", Short: "Print a backed-up file to stdout",
Long: ` Long: `
@ -43,8 +46,12 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runDump(cmd.Context(), dumpOptions, globalOptions, args) return runDump(cmd.Context(), opts, globalOptions, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// DumpOptions collects all options for the dump command. // DumpOptions collects all options for the dump command.
@ -54,15 +61,10 @@ type DumpOptions struct {
Target string Target string
} }
var dumpOptions DumpOptions func (opts *DumpOptions) AddFlags(f *pflag.FlagSet) {
initSingleSnapshotFilter(f, &opts.SnapshotFilter)
func init() { f.StringVarP(&opts.Archive, "archive", "a", "tar", "set archive `format` as \"tar\" or \"zip\"")
cmdRoot.AddCommand(cmdDump) f.StringVarP(&opts.Target, "target", "t", "", "write the output to target `path`")
flags := cmdDump.Flags()
initSingleSnapshotFilter(flags, &dumpOptions.SnapshotFilter)
flags.StringVarP(&dumpOptions.Archive, "archive", "a", "tar", "set archive `format` as \"tar\" or \"zip\"")
flags.StringVarP(&dumpOptions.Target, "target", "t", "", "write the output to target `path`")
} }
func splitPath(p string) []string { func splitPath(p string) []string {

View File

@ -10,7 +10,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var featuresCmd = &cobra.Command{ func newFeaturesCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "features", Use: "features",
Short: "Print list of feature flags", Short: "Print list of feature flags",
Long: ` Long: `
@ -52,8 +53,7 @@ Exit status is 1 if there was any error.
} }
return tab.Write(globalOptions.stdout) return tab.Write(globalOptions.stdout)
}, },
} }
func init() { return cmd
cmdRoot.AddCommand(featuresCmd)
} }

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
@ -16,7 +17,10 @@ import (
"github.com/restic/restic/internal/walker" "github.com/restic/restic/internal/walker"
) )
var cmdFind = &cobra.Command{ func newFindCommand() *cobra.Command {
var opts FindOptions
cmd := &cobra.Command{
Use: "find [flags] PATTERN...", Use: "find [flags] PATTERN...",
Short: "Find a file, a directory or restic IDs", Short: "Find a file, a directory or restic IDs",
Long: ` Long: `
@ -44,8 +48,12 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runFind(cmd.Context(), findOptions, globalOptions, args) return runFind(cmd.Context(), opts, globalOptions, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// FindOptions bundles all options for the find command. // FindOptions bundles all options for the find command.
@ -62,25 +70,20 @@ type FindOptions struct {
restic.SnapshotFilter restic.SnapshotFilter
} }
var findOptions FindOptions func (opts *FindOptions) AddFlags(f *pflag.FlagSet) {
f.StringVarP(&opts.Oldest, "oldest", "O", "", "oldest modification date/time")
f.StringVarP(&opts.Newest, "newest", "N", "", "newest modification date/time")
f.StringArrayVarP(&opts.Snapshots, "snapshot", "s", nil, "snapshot `id` to search in (can be given multiple times)")
f.BoolVar(&opts.BlobID, "blob", false, "pattern is a blob-ID")
f.BoolVar(&opts.TreeID, "tree", false, "pattern is a tree-ID")
f.BoolVar(&opts.PackID, "pack", false, "pattern is a pack-ID")
f.BoolVar(&opts.ShowPackID, "show-pack-id", false, "display the pack-ID the blobs belong to (with --blob or --tree)")
f.BoolVarP(&opts.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern")
f.BoolVarP(&opts.Reverse, "reverse", "R", false, "reverse sort order oldest to newest")
f.BoolVarP(&opts.ListLong, "long", "l", false, "use a long listing format showing size and mode")
f.BoolVar(&opts.HumanReadable, "human-readable", false, "print sizes in human readable format")
func init() { initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
cmdRoot.AddCommand(cmdFind)
f := cmdFind.Flags()
f.StringVarP(&findOptions.Oldest, "oldest", "O", "", "oldest modification date/time")
f.StringVarP(&findOptions.Newest, "newest", "N", "", "newest modification date/time")
f.StringArrayVarP(&findOptions.Snapshots, "snapshot", "s", nil, "snapshot `id` to search in (can be given multiple times)")
f.BoolVar(&findOptions.BlobID, "blob", false, "pattern is a blob-ID")
f.BoolVar(&findOptions.TreeID, "tree", false, "pattern is a tree-ID")
f.BoolVar(&findOptions.PackID, "pack", false, "pattern is a pack-ID")
f.BoolVar(&findOptions.ShowPackID, "show-pack-id", false, "display the pack-ID the blobs belong to (with --blob or --tree)")
f.BoolVarP(&findOptions.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern")
f.BoolVarP(&findOptions.Reverse, "reverse", "R", false, "reverse sort order oldest to newest")
f.BoolVarP(&findOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode")
f.BoolVar(&findOptions.HumanReadable, "human-readable", false, "print sizes in human readable format")
initMultiSnapshotFilter(f, &findOptions.SnapshotFilter, true)
} }
type findPattern struct { type findPattern struct {

View File

@ -11,9 +11,14 @@ import (
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var cmdForget = &cobra.Command{ func newForgetCommand() *cobra.Command {
var opts ForgetOptions
var pruneOpts PruneOptions
cmd := &cobra.Command{
Use: "forget [flags] [snapshot ID] [...]", Use: "forget [flags] [snapshot ID] [...]",
Short: "Remove snapshots from the repository", Short: "Remove snapshots from the repository",
Long: ` Long: `
@ -45,8 +50,13 @@ Exit status is 12 if the password is incorrect.
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus() term, cancel := setupTermstatus()
defer cancel() defer cancel()
return runForget(cmd.Context(), forgetOptions, forgetPruneOptions, globalOptions, term, args) return runForget(cmd.Context(), opts, pruneOpts, globalOptions, term, args)
}, },
}
opts.AddFlags(cmd.Flags())
pruneOpts.AddLimitedFlags(cmd.Flags())
return cmd
} }
type ForgetPolicyCount int type ForgetPolicyCount int
@ -111,44 +121,37 @@ type ForgetOptions struct {
Prune bool Prune bool
} }
var forgetOptions ForgetOptions func (opts *ForgetOptions) AddFlags(f *pflag.FlagSet) {
var forgetPruneOptions PruneOptions f.VarP(&opts.Last, "keep-last", "l", "keep the last `n` snapshots (use 'unlimited' to keep all snapshots)")
f.VarP(&opts.Hourly, "keep-hourly", "H", "keep the last `n` hourly snapshots (use 'unlimited' to keep all hourly snapshots)")
f.VarP(&opts.Daily, "keep-daily", "d", "keep the last `n` daily snapshots (use 'unlimited' to keep all daily snapshots)")
f.VarP(&opts.Weekly, "keep-weekly", "w", "keep the last `n` weekly snapshots (use 'unlimited' to keep all weekly snapshots)")
f.VarP(&opts.Monthly, "keep-monthly", "m", "keep the last `n` monthly snapshots (use 'unlimited' to keep all monthly snapshots)")
f.VarP(&opts.Yearly, "keep-yearly", "y", "keep the last `n` yearly snapshots (use 'unlimited' to keep all yearly snapshots)")
f.VarP(&opts.Within, "keep-within", "", "keep snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.VarP(&opts.WithinHourly, "keep-within-hourly", "", "keep hourly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.VarP(&opts.WithinDaily, "keep-within-daily", "", "keep daily snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.VarP(&opts.WithinWeekly, "keep-within-weekly", "", "keep weekly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.VarP(&opts.WithinMonthly, "keep-within-monthly", "", "keep monthly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.VarP(&opts.WithinYearly, "keep-within-yearly", "", "keep yearly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.Var(&opts.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)")
f.BoolVar(&opts.UnsafeAllowRemoveAll, "unsafe-allow-remove-all", false, "allow deleting all snapshots of a snapshot group")
func init() { initMultiSnapshotFilter(f, &opts.SnapshotFilter, false)
cmdRoot.AddCommand(cmdForget) f.StringArrayVar(&opts.Hosts, "hostname", nil, "only consider snapshots with the given `hostname` (can be specified multiple times)")
f := cmdForget.Flags()
f.VarP(&forgetOptions.Last, "keep-last", "l", "keep the last `n` snapshots (use 'unlimited' to keep all snapshots)")
f.VarP(&forgetOptions.Hourly, "keep-hourly", "H", "keep the last `n` hourly snapshots (use 'unlimited' to keep all hourly snapshots)")
f.VarP(&forgetOptions.Daily, "keep-daily", "d", "keep the last `n` daily snapshots (use 'unlimited' to keep all daily snapshots)")
f.VarP(&forgetOptions.Weekly, "keep-weekly", "w", "keep the last `n` weekly snapshots (use 'unlimited' to keep all weekly snapshots)")
f.VarP(&forgetOptions.Monthly, "keep-monthly", "m", "keep the last `n` monthly snapshots (use 'unlimited' to keep all monthly snapshots)")
f.VarP(&forgetOptions.Yearly, "keep-yearly", "y", "keep the last `n` yearly snapshots (use 'unlimited' to keep all yearly snapshots)")
f.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.VarP(&forgetOptions.WithinHourly, "keep-within-hourly", "", "keep hourly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.VarP(&forgetOptions.WithinDaily, "keep-within-daily", "", "keep daily snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.VarP(&forgetOptions.WithinWeekly, "keep-within-weekly", "", "keep weekly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.VarP(&forgetOptions.WithinMonthly, "keep-within-monthly", "", "keep monthly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.VarP(&forgetOptions.WithinYearly, "keep-within-yearly", "", "keep yearly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)")
f.BoolVar(&forgetOptions.UnsafeAllowRemoveAll, "unsafe-allow-remove-all", false, "allow deleting all snapshots of a snapshot group")
initMultiSnapshotFilter(f, &forgetOptions.SnapshotFilter, false)
f.StringArrayVar(&forgetOptions.Hosts, "hostname", nil, "only consider snapshots with the given `hostname` (can be specified multiple times)")
err := f.MarkDeprecated("hostname", "use --host") err := f.MarkDeprecated("hostname", "use --host")
if err != nil { if err != nil {
// MarkDeprecated only returns an error when the flag is not found // MarkDeprecated only returns an error when the flag is not found
panic(err) panic(err)
} }
f.BoolVarP(&forgetOptions.Compact, "compact", "c", false, "use compact output format") f.BoolVarP(&opts.Compact, "compact", "c", false, "use compact output format")
forgetOptions.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true} opts.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true}
f.VarP(&forgetOptions.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')") f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')")
f.BoolVarP(&forgetOptions.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done") f.BoolVarP(&opts.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done")
f.BoolVar(&forgetOptions.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed") f.BoolVar(&opts.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed")
f.SortFlags = false f.SortFlags = false
addPruneOptions(cmdForget, &forgetPruneOptions)
} }
func verifyForgetOptions(opts *ForgetOptions) error { func verifyForgetOptions(opts *ForgetOptions) error {

View File

@ -8,9 +8,13 @@ import (
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/cobra/doc" "github.com/spf13/cobra/doc"
"github.com/spf13/pflag"
) )
var cmdGenerate = &cobra.Command{ func newGenerateCommand() *cobra.Command {
var opts generateOptions
cmd := &cobra.Command{
Use: "generate [flags]", Use: "generate [flags]",
Short: "Generate manual pages and auto-completion files (bash, fish, zsh, powershell)", Short: "Generate manual pages and auto-completion files (bash, fish, zsh, powershell)",
Long: ` Long: `
@ -25,8 +29,11 @@ Exit status is 1 if there was any error.
`, `,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
return runGenerate(genOpts, args) return runGenerate(opts, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
type generateOptions struct { type generateOptions struct {
@ -37,19 +44,15 @@ type generateOptions struct {
PowerShellCompletionFile string PowerShellCompletionFile string
} }
var genOpts generateOptions func (opts *generateOptions) AddFlags(f *pflag.FlagSet) {
f.StringVar(&opts.ManDir, "man", "", "write man pages to `directory`")
func init() { f.StringVar(&opts.BashCompletionFile, "bash-completion", "", "write bash completion `file` (`-` for stdout)")
cmdRoot.AddCommand(cmdGenerate) f.StringVar(&opts.FishCompletionFile, "fish-completion", "", "write fish completion `file` (`-` for stdout)")
fs := cmdGenerate.Flags() f.StringVar(&opts.ZSHCompletionFile, "zsh-completion", "", "write zsh completion `file` (`-` for stdout)")
fs.StringVar(&genOpts.ManDir, "man", "", "write man pages to `directory`") f.StringVar(&opts.PowerShellCompletionFile, "powershell-completion", "", "write powershell completion `file` (`-` for stdout)")
fs.StringVar(&genOpts.BashCompletionFile, "bash-completion", "", "write bash completion `file` (`-` for stdout)")
fs.StringVar(&genOpts.FishCompletionFile, "fish-completion", "", "write fish completion `file` (`-` for stdout)")
fs.StringVar(&genOpts.ZSHCompletionFile, "zsh-completion", "", "write zsh completion `file` (`-` for stdout)")
fs.StringVar(&genOpts.PowerShellCompletionFile, "powershell-completion", "", "write powershell completion `file` (`-` for stdout)")
} }
func writeManpages(dir string) error { func writeManpages(root *cobra.Command, dir string) error {
// use a fixed date for the man pages so that generating them is deterministic // use a fixed date for the man pages so that generating them is deterministic
date, err := time.Parse("Jan 2006", "Jan 2017") date, err := time.Parse("Jan 2006", "Jan 2017")
if err != nil { if err != nil {
@ -64,7 +67,7 @@ func writeManpages(dir string) error {
} }
Verbosef("writing man pages to directory %v\n", dir) Verbosef("writing man pages to directory %v\n", dir)
return doc.GenManTree(cmdRoot, header, dir) return doc.GenManTree(root, header, dir)
} }
func writeCompletion(filename string, shell string, generate func(w io.Writer) error) (err error) { func writeCompletion(filename string, shell string, generate func(w io.Writer) error) (err error) {
@ -112,8 +115,10 @@ func runGenerate(opts generateOptions, args []string) error {
return errors.Fatal("the generate command expects no arguments, only options - please see `restic help generate` for usage and flags") return errors.Fatal("the generate command expects no arguments, only options - please see `restic help generate` for usage and flags")
} }
cmdRoot := newRootCommand()
if opts.ManDir != "" { if opts.ManDir != "" {
err := writeManpages(opts.ManDir) err := writeManpages(cmdRoot, opts.ManDir)
if err != nil { if err != nil {
return err return err
} }

View File

@ -12,9 +12,13 @@ import (
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var cmdInit = &cobra.Command{ func newInitCommand() *cobra.Command {
var opts InitOptions
cmd := &cobra.Command{
Use: "init", Use: "init",
Short: "Initialize a new repository", Short: "Initialize a new repository",
Long: ` Long: `
@ -29,8 +33,11 @@ Exit status is 1 if there was any error.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runInit(cmd.Context(), initOptions, globalOptions, args) return runInit(cmd.Context(), opts, globalOptions, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// InitOptions bundles all options for the init command. // InitOptions bundles all options for the init command.
@ -40,15 +47,10 @@ type InitOptions struct {
RepositoryVersion string RepositoryVersion string
} }
var initOptions InitOptions func (opts *InitOptions) AddFlags(f *pflag.FlagSet) {
opts.secondaryRepoOptions.AddFlags(f, "secondary", "to copy chunker parameters from")
func init() { f.BoolVar(&opts.CopyChunkerParameters, "copy-chunker-params", false, "copy chunker parameters from the secondary repository (useful with the copy command)")
cmdRoot.AddCommand(cmdInit) f.StringVar(&opts.RepositoryVersion, "repository-version", "stable", "repository format version to use, allowed values are a format version, 'latest' and 'stable'")
f := cmdInit.Flags()
initSecondaryRepoOptions(f, &initOptions.secondaryRepoOptions, "secondary", "to copy chunker parameters from")
f.BoolVar(&initOptions.CopyChunkerParameters, "copy-chunker-params", false, "copy chunker parameters from the secondary repository (useful with the copy command)")
f.StringVar(&initOptions.RepositoryVersion, "repository-version", "stable", "repository format version to use, allowed values are a format version, 'latest' and 'stable'")
} }
func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []string) error { func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []string) error {

View File

@ -4,7 +4,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var cmdKey = &cobra.Command{ func newKeyCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "key", Use: "key",
Short: "Manage keys (passwords)", Short: "Manage keys (passwords)",
Long: ` Long: `
@ -13,8 +14,13 @@ per repository.
`, `,
DisableAutoGenTag: true, DisableAutoGenTag: true,
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
} }
func init() { cmd.AddCommand(
cmdRoot.AddCommand(cmdKey) newKeyAddCommand(),
newKeyListCommand(),
newKeyPasswdCommand(),
newKeyRemoveCommand(),
)
return cmd
} }

View File

@ -10,7 +10,10 @@ import (
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
var cmdKeyAdd = &cobra.Command{ func newKeyAddCommand() *cobra.Command {
var opts KeyAddOptions
cmd := &cobra.Command{
Use: "add", Use: "add",
Short: "Add a new key (password) to the repository; returns the new key ID", Short: "Add a new key (password) to the repository; returns the new key ID",
Long: ` Long: `
@ -26,6 +29,13 @@ Exit status is 11 if the repository is already locked.
Exit status is 12 if the password is incorrect. Exit status is 12 if the password is incorrect.
`, `,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runKeyAdd(cmd.Context(), globalOptions, opts, args)
},
}
opts.Add(cmd.Flags())
return cmd
} }
type KeyAddOptions struct { type KeyAddOptions struct {
@ -42,16 +52,6 @@ func (opts *KeyAddOptions) Add(flags *pflag.FlagSet) {
flags.StringVarP(&opts.Hostname, "host", "", "", "the hostname for new key") flags.StringVarP(&opts.Hostname, "host", "", "", "the hostname for new key")
} }
func init() {
cmdKey.AddCommand(cmdKeyAdd)
var keyAddOpts KeyAddOptions
keyAddOpts.Add(cmdKeyAdd.Flags())
cmdKeyAdd.RunE = func(cmd *cobra.Command, args []string) error {
return runKeyAdd(cmd.Context(), globalOptions, keyAddOpts, args)
}
}
func runKeyAdd(ctx context.Context, gopts GlobalOptions, opts KeyAddOptions, args []string) error { func runKeyAdd(ctx context.Context, gopts GlobalOptions, opts KeyAddOptions, args []string) error {
if len(args) > 0 { if len(args) > 0 {
return fmt.Errorf("the key add command expects no arguments, only options - please see `restic help key add` for usage and flags") return fmt.Errorf("the key add command expects no arguments, only options - please see `restic help key add` for usage and flags")

View File

@ -12,7 +12,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var cmdKeyList = &cobra.Command{ func newKeyListCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list", Use: "list",
Short: "List keys (passwords)", Short: "List keys (passwords)",
Long: ` Long: `
@ -33,10 +34,8 @@ Exit status is 12 if the password is incorrect.
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runKeyList(cmd.Context(), globalOptions, args) return runKeyList(cmd.Context(), globalOptions, args)
}, },
} }
return cmd
func init() {
cmdKey.AddCommand(cmdKeyList)
} }
func runKeyList(ctx context.Context, gopts GlobalOptions, args []string) error { func runKeyList(ctx context.Context, gopts GlobalOptions, args []string) error {

View File

@ -7,9 +7,13 @@ import (
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var cmdKeyPasswd = &cobra.Command{ func newKeyPasswdCommand() *cobra.Command {
var opts KeyPasswdOptions
cmd := &cobra.Command{
Use: "passwd", Use: "passwd",
Short: "Change key (password); creates a new key ID and removes the old key ID, returns new key ID", Short: "Change key (password); creates a new key ID and removes the old key ID, returns new key ID",
Long: ` Long: `
@ -26,20 +30,21 @@ Exit status is 11 if the repository is already locked.
Exit status is 12 if the password is incorrect. Exit status is 12 if the password is incorrect.
`, `,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runKeyPasswd(cmd.Context(), globalOptions, opts, args)
},
}
opts.AddFlags(cmd.Flags())
return cmd
} }
type KeyPasswdOptions struct { type KeyPasswdOptions struct {
KeyAddOptions KeyAddOptions
} }
func init() { func (opts *KeyPasswdOptions) AddFlags(flags *pflag.FlagSet) {
cmdKey.AddCommand(cmdKeyPasswd) opts.KeyAddOptions.Add(flags)
var keyPasswdOpts KeyPasswdOptions
keyPasswdOpts.KeyAddOptions.Add(cmdKeyPasswd.Flags())
cmdKeyPasswd.RunE = func(cmd *cobra.Command, args []string) error {
return runKeyPasswd(cmd.Context(), globalOptions, keyPasswdOpts, args)
}
} }
func runKeyPasswd(ctx context.Context, gopts GlobalOptions, opts KeyPasswdOptions, args []string) error { func runKeyPasswd(ctx context.Context, gopts GlobalOptions, opts KeyPasswdOptions, args []string) error {

View File

@ -10,7 +10,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var cmdKeyRemove = &cobra.Command{ func newKeyRemoveCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "remove [ID]", Use: "remove [ID]",
Short: "Remove key ID (password) from the repository.", Short: "Remove key ID (password) from the repository.",
Long: ` Long: `
@ -30,10 +31,8 @@ Exit status is 12 if the password is incorrect.
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runKeyRemove(cmd.Context(), globalOptions, args) return runKeyRemove(cmd.Context(), globalOptions, args)
}, },
} }
return cmd
func init() {
cmdKey.AddCommand(cmdKeyRemove)
} }
func runKeyRemove(ctx context.Context, gopts GlobalOptions, args []string) error { func runKeyRemove(ctx context.Context, gopts GlobalOptions, args []string) error {

View File

@ -11,10 +11,11 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var listAllowedArgs = []string{"blobs", "packs", "index", "snapshots", "keys", "locks"} func newListCommand() *cobra.Command {
var listAllowedArgsUseString = strings.Join(listAllowedArgs, "|") var listAllowedArgs = []string{"blobs", "packs", "index", "snapshots", "keys", "locks"}
var listAllowedArgsUseString = strings.Join(listAllowedArgs, "|")
var cmdList = &cobra.Command{ cmd := &cobra.Command{
Use: "list [flags] [" + listAllowedArgsUseString + "]", Use: "list [flags] [" + listAllowedArgsUseString + "]",
Short: "List objects in the repository", Short: "List objects in the repository",
Long: ` Long: `
@ -36,10 +37,8 @@ Exit status is 12 if the password is incorrect.
}, },
ValidArgs: listAllowedArgs, ValidArgs: listAllowedArgs,
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
} }
return cmd
func init() {
cmdRoot.AddCommand(cmdList)
} }
func runList(ctx context.Context, gopts GlobalOptions, args []string) error { func runList(ctx context.Context, gopts GlobalOptions, args []string) error {

View File

@ -13,6 +13,7 @@ import (
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/fs"
@ -20,7 +21,10 @@ import (
"github.com/restic/restic/internal/walker" "github.com/restic/restic/internal/walker"
) )
var cmdLs = &cobra.Command{ func newLsCommand() *cobra.Command {
var opts LsOptions
cmd := &cobra.Command{
Use: "ls [flags] snapshotID [dir...]", Use: "ls [flags] snapshotID [dir...]",
Short: "List files in a snapshot", Short: "List files in a snapshot",
Long: ` Long: `
@ -55,8 +59,11 @@ Exit status is 12 if the password is incorrect.
DisableAutoGenTag: true, DisableAutoGenTag: true,
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runLs(cmd.Context(), lsOptions, globalOptions, args) return runLs(cmd.Context(), opts, globalOptions, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// LsOptions collects all options for the ls command. // LsOptions collects all options for the ls command.
@ -70,19 +77,14 @@ type LsOptions struct {
Reverse bool Reverse bool
} }
var lsOptions LsOptions func (opts *LsOptions) AddFlags(f *pflag.FlagSet) {
initSingleSnapshotFilter(f, &opts.SnapshotFilter)
func init() { f.BoolVarP(&opts.ListLong, "long", "l", false, "use a long listing format showing size and mode")
cmdRoot.AddCommand(cmdLs) f.BoolVar(&opts.Recursive, "recursive", false, "include files in subfolders of the listed directories")
f.BoolVar(&opts.HumanReadable, "human-readable", false, "print sizes in human readable format")
flags := cmdLs.Flags() f.BoolVar(&opts.Ncdu, "ncdu", false, "output NCDU export format (pipe into 'ncdu -f -')")
initSingleSnapshotFilter(flags, &lsOptions.SnapshotFilter) f.VarP(&opts.Sort, "sort", "s", "sort output by (name|size|time=mtime|atime|ctime|extension)")
flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode") f.BoolVar(&opts.Reverse, "reverse", false, "reverse sorted output")
flags.BoolVar(&lsOptions.Recursive, "recursive", false, "include files in subfolders of the listed directories")
flags.BoolVar(&lsOptions.HumanReadable, "human-readable", false, "print sizes in human readable format")
flags.BoolVar(&lsOptions.Ncdu, "ncdu", false, "output NCDU export format (pipe into 'ncdu -f -')")
flags.VarP(&lsOptions.Sort, "sort", "s", "sort output by (name|size|time=mtime|atime|ctime|extension)")
flags.BoolVar(&lsOptions.Reverse, "reverse", false, "reverse sorted output")
} }
type lsPrinter interface { type lsPrinter interface {

View File

@ -9,9 +9,13 @@ import (
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var cmdMigrate = &cobra.Command{ func newMigrateCommand() *cobra.Command {
var opts MigrateOptions
cmd := &cobra.Command{
Use: "migrate [flags] [migration name] [...]", Use: "migrate [flags] [migration name] [...]",
Short: "Apply migrations", Short: "Apply migrations",
Long: ` Long: `
@ -33,8 +37,12 @@ Exit status is 12 if the password is incorrect.
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus() term, cancel := setupTermstatus()
defer cancel() defer cancel()
return runMigrate(cmd.Context(), migrateOptions, globalOptions, args, term) return runMigrate(cmd.Context(), opts, globalOptions, args, term)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// MigrateOptions bundles all options for the 'check' command. // MigrateOptions bundles all options for the 'check' command.
@ -42,12 +50,8 @@ type MigrateOptions struct {
Force bool Force bool
} }
var migrateOptions MigrateOptions func (opts *MigrateOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVarP(&opts.Force, "force", "f", false, `apply a migration a second time`)
func init() {
cmdRoot.AddCommand(cmdMigrate)
f := cmdMigrate.Flags()
f.BoolVarP(&migrateOptions.Force, "force", "f", false, `apply a migration a second time`)
} }
func checkMigrations(ctx context.Context, repo restic.Repository, printer progress.Printer) error { func checkMigrations(ctx context.Context, repo restic.Repository, printer progress.Printer) error {

View File

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
@ -21,7 +22,14 @@ import (
"github.com/anacrolix/fuse/fs" "github.com/anacrolix/fuse/fs"
) )
var cmdMount = &cobra.Command{ func registerMountCommand(cmdRoot *cobra.Command) {
cmdRoot.AddCommand(newMountCommand())
}
func newMountCommand() *cobra.Command {
var opts MountOptions
cmd := &cobra.Command{
Use: "mount [flags] mountpoint", Use: "mount [flags] mountpoint",
Short: "Mount the repository", Short: "Mount the repository",
Long: ` Long: `
@ -72,8 +80,12 @@ Exit status is 12 if the password is incorrect.
DisableAutoGenTag: true, DisableAutoGenTag: true,
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runMount(cmd.Context(), mountOptions, globalOptions, args) return runMount(cmd.Context(), opts, globalOptions, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// MountOptions collects all options for the mount command. // MountOptions collects all options for the mount command.
@ -86,22 +98,17 @@ type MountOptions struct {
PathTemplates []string PathTemplates []string
} }
var mountOptions MountOptions func (opts *MountOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&opts.OwnerRoot, "owner-root", false, "use 'root' as the owner of files and dirs")
f.BoolVar(&opts.AllowOther, "allow-other", false, "allow other users to access the data in the mounted directory")
f.BoolVar(&opts.NoDefaultPermissions, "no-default-permissions", false, "for 'allow-other', ignore Unix permissions and allow users to read all snapshot files")
func init() { initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
cmdRoot.AddCommand(cmdMount)
mountFlags := cmdMount.Flags() f.StringArrayVar(&opts.PathTemplates, "path-template", nil, "set `template` for path names (can be specified multiple times)")
mountFlags.BoolVar(&mountOptions.OwnerRoot, "owner-root", false, "use 'root' as the owner of files and dirs") f.StringVar(&opts.TimeTemplate, "snapshot-template", time.RFC3339, "set `template` to use for snapshot dirs")
mountFlags.BoolVar(&mountOptions.AllowOther, "allow-other", false, "allow other users to access the data in the mounted directory") f.StringVar(&opts.TimeTemplate, "time-template", time.RFC3339, "set `template` to use for times")
mountFlags.BoolVar(&mountOptions.NoDefaultPermissions, "no-default-permissions", false, "for 'allow-other', ignore Unix permissions and allow users to read all snapshot files") _ = f.MarkDeprecated("snapshot-template", "use --time-template")
initMultiSnapshotFilter(mountFlags, &mountOptions.SnapshotFilter, true)
mountFlags.StringArrayVar(&mountOptions.PathTemplates, "path-template", nil, "set `template` for path names (can be specified multiple times)")
mountFlags.StringVar(&mountOptions.TimeTemplate, "snapshot-template", time.RFC3339, "set `template` to use for snapshot dirs")
mountFlags.StringVar(&mountOptions.TimeTemplate, "time-template", time.RFC3339, "set `template` to use for times")
_ = mountFlags.MarkDeprecated("snapshot-template", "use --time-template")
} }
func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args []string) error { func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args []string) error {

View File

@ -0,0 +1,10 @@
//go:build !darwin && !freebsd && !linux
// +build !darwin,!freebsd,!linux
package main
import "github.com/spf13/cobra"
func registerMountCommand(_ *cobra.Command) {
// Mount command not supported on these platforms
}

View File

@ -8,7 +8,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var optionsCmd = &cobra.Command{ func newOptionsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "options", Use: "options",
Short: "Print list of extended options", Short: "Print list of extended options",
Long: ` Long: `
@ -34,8 +35,6 @@ Exit status is 1 if there was any error.
fmt.Printf(" %*s %s\n", -maxLen, opt.Namespace+"."+opt.Name, opt.Text) fmt.Printf(" %*s %s\n", -maxLen, opt.Namespace+"."+opt.Name, opt.Text)
} }
}, },
} }
return cmd
func init() {
cmdRoot.AddCommand(optionsCmd)
} }

View File

@ -16,9 +16,13 @@ import (
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var cmdPrune = &cobra.Command{ func newPruneCommand() *cobra.Command {
var opts PruneOptions
cmd := &cobra.Command{
Use: "prune [flags]", Use: "prune [flags]",
Short: "Remove unneeded data from the repository", Short: "Remove unneeded data from the repository",
Long: ` Long: `
@ -39,8 +43,12 @@ Exit status is 12 if the password is incorrect.
RunE: func(cmd *cobra.Command, _ []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
term, cancel := setupTermstatus() term, cancel := setupTermstatus()
defer cancel() defer cancel()
return runPrune(cmd.Context(), pruneOptions, globalOptions, term) return runPrune(cmd.Context(), opts, globalOptions, term)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// PruneOptions collects all options for the cleanup command. // PruneOptions collects all options for the cleanup command.
@ -61,23 +69,18 @@ type PruneOptions struct {
RepackUncompressed bool RepackUncompressed bool
} }
var pruneOptions PruneOptions func (opts *PruneOptions) AddFlags(f *pflag.FlagSet) {
opts.AddLimitedFlags(f)
func init() { f.BoolVarP(&opts.DryRun, "dry-run", "n", false, "do not modify the repository, just print what would be done")
cmdRoot.AddCommand(cmdPrune) f.StringVarP(&opts.UnsafeNoSpaceRecovery, "unsafe-recover-no-free-space", "", "", "UNSAFE, READ THE DOCUMENTATION BEFORE USING! Try to recover a repository stuck with no free space. Do not use without trying out 'prune --max-repack-size 0' first.")
f := cmdPrune.Flags()
f.BoolVarP(&pruneOptions.DryRun, "dry-run", "n", false, "do not modify the repository, just print what would be done")
f.StringVarP(&pruneOptions.UnsafeNoSpaceRecovery, "unsafe-recover-no-free-space", "", "", "UNSAFE, READ THE DOCUMENTATION BEFORE USING! Try to recover a repository stuck with no free space. Do not use without trying out 'prune --max-repack-size 0' first.")
addPruneOptions(cmdPrune, &pruneOptions)
} }
func addPruneOptions(c *cobra.Command, pruneOptions *PruneOptions) { func (opts *PruneOptions) AddLimitedFlags(f *pflag.FlagSet) {
f := c.Flags() f.StringVar(&opts.MaxUnused, "max-unused", "5%", "tolerate given `limit` of unused data (absolute value in bytes with suffixes k/K, m/M, g/G, t/T, a value in % or the word 'unlimited')")
f.StringVar(&pruneOptions.MaxUnused, "max-unused", "5%", "tolerate given `limit` of unused data (absolute value in bytes with suffixes k/K, m/M, g/G, t/T, a value in % or the word 'unlimited')") f.StringVar(&opts.MaxRepackSize, "max-repack-size", "", "stop after repacking this much data in total (allowed suffixes for `size`: k/K, m/M, g/G, t/T)")
f.StringVar(&pruneOptions.MaxRepackSize, "max-repack-size", "", "stop after repacking this much data in total (allowed suffixes for `size`: k/K, m/M, g/G, t/T)") f.BoolVar(&opts.RepackCacheableOnly, "repack-cacheable-only", false, "only repack packs which are cacheable")
f.BoolVar(&pruneOptions.RepackCacheableOnly, "repack-cacheable-only", false, "only repack packs which are cacheable") f.BoolVar(&opts.RepackSmall, "repack-small", false, "repack pack files below 80% of target pack size")
f.BoolVar(&pruneOptions.RepackSmall, "repack-small", false, "repack pack files below 80% of target pack size") f.BoolVar(&opts.RepackUncompressed, "repack-uncompressed", false, "repack all uncompressed data")
f.BoolVar(&pruneOptions.RepackUncompressed, "repack-uncompressed", false, "repack all uncompressed data")
} }
func verifyPruneOptions(opts *PruneOptions) error { func verifyPruneOptions(opts *PruneOptions) error {

View File

@ -11,7 +11,8 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
var cmdRecover = &cobra.Command{ func newRecoverCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "recover [flags]", Use: "recover [flags]",
Short: "Recover data from the repository not referenced by snapshots", Short: "Recover data from the repository not referenced by snapshots",
Long: ` Long: `
@ -33,10 +34,8 @@ Exit status is 12 if the password is incorrect.
RunE: func(cmd *cobra.Command, _ []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
return runRecover(cmd.Context(), globalOptions) return runRecover(cmd.Context(), globalOptions)
}, },
} }
return cmd
func init() {
cmdRoot.AddCommand(cmdRecover)
} }
func runRecover(ctx context.Context, gopts GlobalOptions) error { func runRecover(ctx context.Context, gopts GlobalOptions) error {

View File

@ -4,13 +4,18 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var cmdRepair = &cobra.Command{ func newRepairCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "repair", Use: "repair",
Short: "Repair the repository", Short: "Repair the repository",
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
} }
func init() { cmd.AddCommand(
cmdRoot.AddCommand(cmdRepair) newRepairIndexCommand(),
newRepairPacksCommand(),
newRepairSnapshotsCommand(),
)
return cmd
} }

View File

@ -9,7 +9,10 @@ import (
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
var cmdRepairIndex = &cobra.Command{ func newRepairIndexCommand() *cobra.Command {
var opts RepairIndexOptions
cmd := &cobra.Command{
Use: "index [flags]", Use: "index [flags]",
Short: "Build a new index", Short: "Build a new index",
Long: ` Long: `
@ -29,17 +32,12 @@ Exit status is 12 if the password is incorrect.
RunE: func(cmd *cobra.Command, _ []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
term, cancel := setupTermstatus() term, cancel := setupTermstatus()
defer cancel() defer cancel()
return runRebuildIndex(cmd.Context(), repairIndexOptions, globalOptions, term) return runRebuildIndex(cmd.Context(), opts, globalOptions, term)
}, },
} }
var cmdRebuildIndex = &cobra.Command{ opts.AddFlags(cmd.Flags())
Use: "rebuild-index [flags]", return cmd
Short: cmdRepairIndex.Short,
Long: cmdRepairIndex.Long,
Deprecated: `Use "repair index" instead`,
DisableAutoGenTag: true,
RunE: cmdRepairIndex.RunE,
} }
// RepairIndexOptions collects all options for the repair index command. // RepairIndexOptions collects all options for the repair index command.
@ -47,16 +45,31 @@ type RepairIndexOptions struct {
ReadAllPacks bool ReadAllPacks bool
} }
var repairIndexOptions RepairIndexOptions func (opts *RepairIndexOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&opts.ReadAllPacks, "read-all-packs", false, "read all pack files to generate new index from scratch")
}
func init() { func newRebuildIndexCommand() *cobra.Command {
cmdRepair.AddCommand(cmdRepairIndex) var opts RepairIndexOptions
// add alias for old name
cmdRoot.AddCommand(cmdRebuildIndex)
for _, f := range []*pflag.FlagSet{cmdRepairIndex.Flags(), cmdRebuildIndex.Flags()} { replacement := newRepairIndexCommand()
f.BoolVar(&repairIndexOptions.ReadAllPacks, "read-all-packs", false, "read all pack files to generate new index from scratch") cmd := &cobra.Command{
Use: "rebuild-index [flags]",
Short: replacement.Short,
Long: replacement.Long,
Deprecated: `Use "repair index" instead`,
DisableAutoGenTag: true,
// must create a new instance of the run function as it captures opts
// by reference
RunE: func(cmd *cobra.Command, _ []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runRebuildIndex(cmd.Context(), opts, globalOptions, term)
},
} }
opts.AddFlags(cmd.Flags())
return cmd
} }
func runRebuildIndex(ctx context.Context, opts RepairIndexOptions, gopts GlobalOptions, term *termstatus.Terminal) error { func runRebuildIndex(ctx context.Context, opts RepairIndexOptions, gopts GlobalOptions, term *termstatus.Terminal) error {

View File

@ -13,7 +13,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var cmdRepairPacks = &cobra.Command{ func newRepairPacksCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "packs [packIDs...]", Use: "packs [packIDs...]",
Short: "Salvage damaged pack files", Short: "Salvage damaged pack files",
Long: ` Long: `
@ -35,10 +36,8 @@ Exit status is 12 if the password is incorrect.
defer cancel() defer cancel()
return runRepairPacks(cmd.Context(), globalOptions, term, args) return runRepairPacks(cmd.Context(), globalOptions, term, args)
}, },
} }
return cmd
func init() {
cmdRepair.AddCommand(cmdRepairPacks)
} }
func runRepairPacks(ctx context.Context, gopts GlobalOptions, term *termstatus.Terminal, args []string) error { func runRepairPacks(ctx context.Context, gopts GlobalOptions, term *termstatus.Terminal, args []string) error {

View File

@ -8,9 +8,13 @@ import (
"github.com/restic/restic/internal/walker" "github.com/restic/restic/internal/walker"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var cmdRepairSnapshots = &cobra.Command{ func newRepairSnapshotsCommand() *cobra.Command {
var opts RepairOptions
cmd := &cobra.Command{
Use: "snapshots [flags] [snapshot ID] [...]", Use: "snapshots [flags] [snapshot ID] [...]",
Short: "Repair snapshots", Short: "Repair snapshots",
Long: ` Long: `
@ -45,8 +49,12 @@ Exit status is 12 if the password is incorrect.
`, `,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runRepairSnapshots(cmd.Context(), globalOptions, repairSnapshotOptions, args) return runRepairSnapshots(cmd.Context(), globalOptions, opts, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// RepairOptions collects all options for the repair command. // RepairOptions collects all options for the repair command.
@ -57,16 +65,11 @@ type RepairOptions struct {
restic.SnapshotFilter restic.SnapshotFilter
} }
var repairSnapshotOptions RepairOptions func (opts *RepairOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVarP(&opts.DryRun, "dry-run", "n", false, "do not do anything, just print what would be done")
f.BoolVarP(&opts.Forget, "forget", "", false, "remove original snapshots after creating new ones")
func init() { initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
cmdRepair.AddCommand(cmdRepairSnapshots)
flags := cmdRepairSnapshots.Flags()
flags.BoolVarP(&repairSnapshotOptions.DryRun, "dry-run", "n", false, "do not do anything, just print what would be done")
flags.BoolVarP(&repairSnapshotOptions.Forget, "forget", "", false, "remove original snapshots after creating new ones")
initMultiSnapshotFilter(flags, &repairSnapshotOptions.SnapshotFilter, true)
} }
func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOptions, args []string) error { func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOptions, args []string) error {

View File

@ -15,9 +15,13 @@ import (
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var cmdRestore = &cobra.Command{ func newRestoreCommand() *cobra.Command {
var opts RestoreOptions
cmd := &cobra.Command{
Use: "restore [flags] snapshotID", Use: "restore [flags] snapshotID",
Short: "Extract the data from a snapshot", Short: "Extract the data from a snapshot",
Long: ` Long: `
@ -44,8 +48,12 @@ Exit status is 12 if the password is incorrect.
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus() term, cancel := setupTermstatus()
defer cancel() defer cancel()
return runRestore(cmd.Context(), restoreOptions, globalOptions, term, args) return runRestore(cmd.Context(), opts, globalOptions, term, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// RestoreOptions collects all options for the restore command. // RestoreOptions collects all options for the restore command.
@ -63,26 +71,21 @@ type RestoreOptions struct {
IncludeXattrPattern []string IncludeXattrPattern []string
} }
var restoreOptions RestoreOptions func (opts *RestoreOptions) AddFlags(f *pflag.FlagSet) {
f.StringVarP(&opts.Target, "target", "t", "", "directory to extract data to")
func init() { opts.ExcludePatternOptions.Add(f)
cmdRoot.AddCommand(cmdRestore) opts.IncludePatternOptions.Add(f)
flags := cmdRestore.Flags() f.StringArrayVar(&opts.ExcludeXattrPattern, "exclude-xattr", nil, "exclude xattr by `pattern` (can be specified multiple times)")
flags.StringVarP(&restoreOptions.Target, "target", "t", "", "directory to extract data to") f.StringArrayVar(&opts.IncludeXattrPattern, "include-xattr", nil, "include xattr by `pattern` (can be specified multiple times)")
restoreOptions.ExcludePatternOptions.Add(flags) initSingleSnapshotFilter(f, &opts.SnapshotFilter)
restoreOptions.IncludePatternOptions.Add(flags) f.BoolVar(&opts.DryRun, "dry-run", false, "do not write any data, just show what would be done")
f.BoolVar(&opts.Sparse, "sparse", false, "restore files as sparse")
flags.StringArrayVar(&restoreOptions.ExcludeXattrPattern, "exclude-xattr", nil, "exclude xattr by `pattern` (can be specified multiple times)") f.BoolVar(&opts.Verify, "verify", false, "verify restored files content")
flags.StringArrayVar(&restoreOptions.IncludeXattrPattern, "include-xattr", nil, "include xattr by `pattern` (can be specified multiple times)") f.Var(&opts.Overwrite, "overwrite", "overwrite behavior, one of (always|if-changed|if-newer|never) (default: always)")
f.BoolVar(&opts.Delete, "delete", false, "delete files from target directory if they do not exist in snapshot. Use '--dry-run -vv' to check what would be deleted")
initSingleSnapshotFilter(flags, &restoreOptions.SnapshotFilter)
flags.BoolVar(&restoreOptions.DryRun, "dry-run", false, "do not write any data, just show what would be done")
flags.BoolVar(&restoreOptions.Sparse, "sparse", false, "restore files as sparse")
flags.BoolVar(&restoreOptions.Verify, "verify", false, "verify restored files content")
flags.Var(&restoreOptions.Overwrite, "overwrite", "overwrite behavior, one of (always|if-changed|if-newer|never) (default: always)")
flags.BoolVar(&restoreOptions.Delete, "delete", false, "delete files from target directory if they do not exist in snapshot. Use '--dry-run -vv' to check what would be deleted")
} }
func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,

View File

@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
@ -15,7 +16,10 @@ import (
"github.com/restic/restic/internal/walker" "github.com/restic/restic/internal/walker"
) )
var cmdRewrite = &cobra.Command{ func newRewriteCommand() *cobra.Command {
var opts RewriteOptions
cmd := &cobra.Command{
Use: "rewrite [flags] [snapshotID ...]", Use: "rewrite [flags] [snapshotID ...]",
Short: "Rewrite snapshots to exclude unwanted files", Short: "Rewrite snapshots to exclude unwanted files",
Long: ` Long: `
@ -54,8 +58,12 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runRewrite(cmd.Context(), rewriteOptions, globalOptions, args) return runRewrite(cmd.Context(), opts, globalOptions, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
type snapshotMetadata struct { type snapshotMetadata struct {
@ -99,20 +107,15 @@ type RewriteOptions struct {
filter.ExcludePatternOptions filter.ExcludePatternOptions
} }
var rewriteOptions RewriteOptions func (opts *RewriteOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVarP(&opts.Forget, "forget", "", false, "remove original snapshots after creating new ones")
f.BoolVarP(&opts.DryRun, "dry-run", "n", false, "do not do anything, just print what would be done")
f.StringVar(&opts.Metadata.Hostname, "new-host", "", "replace hostname")
f.StringVar(&opts.Metadata.Time, "new-time", "", "replace time of the backup")
f.BoolVarP(&opts.SnapshotSummary, "snapshot-summary", "s", false, "create snapshot summary record if it does not exist")
func init() { initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
cmdRoot.AddCommand(cmdRewrite) opts.ExcludePatternOptions.Add(f)
f := cmdRewrite.Flags()
f.BoolVarP(&rewriteOptions.Forget, "forget", "", false, "remove original snapshots after creating new ones")
f.BoolVarP(&rewriteOptions.DryRun, "dry-run", "n", false, "do not do anything, just print what would be done")
f.StringVar(&rewriteOptions.Metadata.Hostname, "new-host", "", "replace hostname")
f.StringVar(&rewriteOptions.Metadata.Time, "new-time", "", "replace time of the backup")
f.BoolVarP(&rewriteOptions.SnapshotSummary, "snapshot-summary", "s", false, "create snapshot summary record if it does not exist")
initMultiSnapshotFilter(f, &rewriteOptions.SnapshotFilter, true)
rewriteOptions.ExcludePatternOptions.Add(f)
} }
// rewriteFilterFunc returns the filtered tree ID or an error. If a snapshot summary is returned, the snapshot will // rewriteFilterFunc returns the filtered tree ID or an error. If a snapshot summary is returned, the snapshot will

View File

@ -10,9 +10,19 @@ import (
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/selfupdate" "github.com/restic/restic/internal/selfupdate"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var cmdSelfUpdate = &cobra.Command{ func registerSelfUpdateCommand(cmd *cobra.Command) {
cmd.AddCommand(
newSelfUpdateCommand(),
)
}
func newSelfUpdateCommand() *cobra.Command {
var opts SelfUpdateOptions
cmd := &cobra.Command{
Use: "self-update [flags]", Use: "self-update [flags]",
Short: "Update the restic binary", Short: "Update the restic binary",
Long: ` Long: `
@ -32,8 +42,12 @@ Exit status is 12 if the password is incorrect.
`, `,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runSelfUpdate(cmd.Context(), selfUpdateOptions, globalOptions, args) return runSelfUpdate(cmd.Context(), opts, globalOptions, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// SelfUpdateOptions collects all options for the update-restic command. // SelfUpdateOptions collects all options for the update-restic command.
@ -41,13 +55,8 @@ type SelfUpdateOptions struct {
Output string Output string
} }
var selfUpdateOptions SelfUpdateOptions func (opts *SelfUpdateOptions) AddFlags(f *pflag.FlagSet) {
f.StringVar(&opts.Output, "output", "", "Save the downloaded file as `filename` (default: running binary itself)")
func init() {
cmdRoot.AddCommand(cmdSelfUpdate)
flags := cmdSelfUpdate.Flags()
flags.StringVar(&selfUpdateOptions.Output, "output", "", "Save the downloaded file as `filename` (default: running binary itself)")
} }
func runSelfUpdate(ctx context.Context, opts SelfUpdateOptions, gopts GlobalOptions, args []string) error { func runSelfUpdate(ctx context.Context, opts SelfUpdateOptions, gopts GlobalOptions, args []string) error {

View File

@ -0,0 +1,9 @@
//go:build !selfupdate
package main
import "github.com/spf13/cobra"
func registerSelfUpdateCommand(_ *cobra.Command) {
// No commands to register in non-selfupdate mode
}

View File

@ -12,9 +12,13 @@ import (
"github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/table" "github.com/restic/restic/internal/ui/table"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var cmdSnapshots = &cobra.Command{ func newSnapshotsCommand() *cobra.Command {
var opts SnapshotOptions
cmd := &cobra.Command{
Use: "snapshots [flags] [snapshotID ...]", Use: "snapshots [flags] [snapshotID ...]",
Short: "List all snapshots", Short: "List all snapshots",
Long: ` Long: `
@ -32,8 +36,12 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runSnapshots(cmd.Context(), snapshotOptions, globalOptions, args) return runSnapshots(cmd.Context(), opts, globalOptions, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// SnapshotOptions bundles all options for the snapshots command. // SnapshotOptions bundles all options for the snapshots command.
@ -45,22 +53,17 @@ type SnapshotOptions struct {
GroupBy restic.SnapshotGroupByOptions GroupBy restic.SnapshotGroupByOptions
} }
var snapshotOptions SnapshotOptions func (opts *SnapshotOptions) AddFlags(f *pflag.FlagSet) {
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
func init() { f.BoolVarP(&opts.Compact, "compact", "c", false, "use compact output format")
cmdRoot.AddCommand(cmdSnapshots) f.BoolVar(&opts.Last, "last", false, "only show the last snapshot for each host and path")
f := cmdSnapshots.Flags()
initMultiSnapshotFilter(f, &snapshotOptions.SnapshotFilter, true)
f.BoolVarP(&snapshotOptions.Compact, "compact", "c", false, "use compact output format")
f.BoolVar(&snapshotOptions.Last, "last", false, "only show the last snapshot for each host and path")
err := f.MarkDeprecated("last", "use --latest 1") err := f.MarkDeprecated("last", "use --latest 1")
if err != nil { if err != nil {
// MarkDeprecated only returns an error when the flag is not found // MarkDeprecated only returns an error when the flag is not found
panic(err) panic(err)
} }
f.IntVar(&snapshotOptions.Latest, "latest", 0, "only show the last `n` snapshots for each host and path") f.IntVar(&opts.Latest, "latest", 0, "only show the last `n` snapshots for each host and path")
f.VarP(&snapshotOptions.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma") f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma")
} }
func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions, args []string) error { func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions, args []string) error {

View File

@ -18,9 +18,13 @@ import (
"github.com/restic/restic/internal/walker" "github.com/restic/restic/internal/walker"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var cmdStats = &cobra.Command{ func newStatsCommand() *cobra.Command {
var opts StatsOptions
cmd := &cobra.Command{
Use: "stats [flags] [snapshot ID] [...]", Use: "stats [flags] [snapshot ID] [...]",
Short: "Scan the repository and show basic statistics", Short: "Scan the repository and show basic statistics",
Long: ` Long: `
@ -58,8 +62,15 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runStats(cmd.Context(), statsOptions, globalOptions, args) return runStats(cmd.Context(), opts, globalOptions, args)
}, },
}
opts.AddFlags(cmd.Flags())
must(cmd.RegisterFlagCompletionFunc("mode", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{countModeRestoreSize, countModeUniqueFilesByContents, countModeBlobsPerFile, countModeRawData}, cobra.ShellCompDirectiveDefault
}))
return cmd
} }
// StatsOptions collects all options for the stats command. // StatsOptions collects all options for the stats command.
@ -70,7 +81,10 @@ type StatsOptions struct {
restic.SnapshotFilter restic.SnapshotFilter
} }
var statsOptions StatsOptions func (opts *StatsOptions) AddFlags(f *pflag.FlagSet) {
f.StringVar(&opts.countMode, "mode", countModeRestoreSize, "counting mode: restore-size (default), files-by-contents, blobs-per-file or raw-data")
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
}
func must(err error) { func must(err error) {
if err != nil { if err != nil {
@ -78,17 +92,6 @@ func must(err error) {
} }
} }
func init() {
cmdRoot.AddCommand(cmdStats)
f := cmdStats.Flags()
f.StringVar(&statsOptions.countMode, "mode", countModeRestoreSize, "counting mode: restore-size (default), files-by-contents, blobs-per-file or raw-data")
must(cmdStats.RegisterFlagCompletionFunc("mode", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{countModeRestoreSize, countModeUniqueFilesByContents, countModeBlobsPerFile, countModeRawData}, cobra.ShellCompDirectiveDefault
}))
initMultiSnapshotFilter(f, &statsOptions.SnapshotFilter, true)
}
func runStats(ctx context.Context, opts StatsOptions, gopts GlobalOptions, args []string) error { func runStats(ctx context.Context, opts StatsOptions, gopts GlobalOptions, args []string) error {
err := verifyStatsInput(opts) err := verifyStatsInput(opts)
if err != nil { if err != nil {

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
@ -13,7 +14,10 @@ import (
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
) )
var cmdTag = &cobra.Command{ func newTagCommand() *cobra.Command {
var opts TagOptions
cmd := &cobra.Command{
Use: "tag [flags] [snapshotID ...]", Use: "tag [flags] [snapshotID ...]",
Short: "Modify tags on snapshots", Short: "Modify tags on snapshots",
Long: ` Long: `
@ -38,8 +42,12 @@ Exit status is 12 if the password is incorrect.
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus() term, cancel := setupTermstatus()
defer cancel() defer cancel()
return runTag(cmd.Context(), tagOptions, globalOptions, term, args) return runTag(cmd.Context(), opts, globalOptions, term, args)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// TagOptions bundles all options for the 'tag' command. // TagOptions bundles all options for the 'tag' command.
@ -50,16 +58,11 @@ type TagOptions struct {
RemoveTags restic.TagLists RemoveTags restic.TagLists
} }
var tagOptions TagOptions func (opts *TagOptions) AddFlags(f *pflag.FlagSet) {
f.Var(&opts.SetTags, "set", "`tags` which will replace the existing tags in the format `tag[,tag,...]` (can be given multiple times)")
func init() { f.Var(&opts.AddTags, "add", "`tags` which will be added to the existing tags in the format `tag[,tag,...]` (can be given multiple times)")
cmdRoot.AddCommand(cmdTag) f.Var(&opts.RemoveTags, "remove", "`tags` which will be removed from the existing tags in the format `tag[,tag,...]` (can be given multiple times)")
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
tagFlags := cmdTag.Flags()
tagFlags.Var(&tagOptions.SetTags, "set", "`tags` which will replace the existing tags in the format `tag[,tag,...]` (can be given multiple times)")
tagFlags.Var(&tagOptions.AddTags, "add", "`tags` which will be added to the existing tags in the format `tag[,tag,...]` (can be given multiple times)")
tagFlags.Var(&tagOptions.RemoveTags, "remove", "`tags` which will be removed from the existing tags in the format `tag[,tag,...]` (can be given multiple times)")
initMultiSnapshotFilter(tagFlags, &tagOptions.SnapshotFilter, true)
} }
type changedSnapshot struct { type changedSnapshot struct {

View File

@ -5,9 +5,13 @@ import (
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var unlockCmd = &cobra.Command{ func newUnlockCommand() *cobra.Command {
var opts UnlockOptions
cmd := &cobra.Command{
Use: "unlock", Use: "unlock",
Short: "Remove locks other processes created", Short: "Remove locks other processes created",
Long: ` Long: `
@ -22,8 +26,11 @@ Exit status is 1 if there was any error.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
return runUnlock(cmd.Context(), unlockOptions, globalOptions) return runUnlock(cmd.Context(), opts, globalOptions)
}, },
}
opts.AddFlags(cmd.Flags())
return cmd
} }
// UnlockOptions collects all options for the unlock command. // UnlockOptions collects all options for the unlock command.
@ -31,12 +38,8 @@ type UnlockOptions struct {
RemoveAll bool RemoveAll bool
} }
var unlockOptions UnlockOptions func (opts *UnlockOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&opts.RemoveAll, "remove-all", false, "remove all locks, even non-stale ones")
func init() {
cmdRoot.AddCommand(unlockCmd)
unlockCmd.Flags().BoolVar(&unlockOptions.RemoveAll, "remove-all", false, "remove all locks, even non-stale ones")
} }
func runUnlock(ctx context.Context, opts UnlockOptions, gopts GlobalOptions) error { func runUnlock(ctx context.Context, opts UnlockOptions, gopts GlobalOptions) error {

View File

@ -8,7 +8,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var versionCmd = &cobra.Command{ func newVersionCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "version", Use: "version",
Short: "Print version information", Short: "Print version information",
Long: ` Long: `
@ -51,8 +52,6 @@ Exit status is 1 if there was any error.
} }
}, },
} }
return cmd
func init() {
cmdRoot.AddCommand(versionCmd)
} }

View File

@ -8,7 +8,7 @@ import (
// TestFlags checks for double defined flags, the commands will panic on // TestFlags checks for double defined flags, the commands will panic on
// ParseFlags() when a shorthand flag is defined twice. // ParseFlags() when a shorthand flag is defined twice.
func TestFlags(t *testing.T) { func TestFlags(t *testing.T) {
for _, cmd := range cmdRoot.Commands() { for _, cmd := range newRootCommand().Commands() {
t.Run(cmd.Name(), func(t *testing.T) { t.Run(cmd.Name(), func(t *testing.T) {
cmd.Flags().SetOutput(io.Discard) cmd.Flags().SetOutput(io.Discard)
err := cmd.ParseFlags([]string{"--help"}) err := cmd.ParseFlags([]string{"--help"})

View File

@ -34,6 +34,7 @@ import (
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/textfile" "github.com/restic/restic/internal/textfile"
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
"github.com/spf13/pflag"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
@ -95,12 +96,97 @@ type GlobalOptions struct {
extended options.Options extended options.Options
} }
func (opts *GlobalOptions) AddFlags(f *pflag.FlagSet) {
f.StringVarP(&opts.Repo, "repo", "r", "", "`repository` to backup to or restore from (default: $RESTIC_REPOSITORY)")
f.StringVarP(&opts.RepositoryFile, "repository-file", "", "", "`file` to read the repository location from (default: $RESTIC_REPOSITORY_FILE)")
f.StringVarP(&opts.PasswordFile, "password-file", "p", "", "`file` to read the repository password from (default: $RESTIC_PASSWORD_FILE)")
f.StringVarP(&opts.KeyHint, "key-hint", "", "", "`key` ID of key to try decrypting first (default: $RESTIC_KEY_HINT)")
f.StringVarP(&opts.PasswordCommand, "password-command", "", "", "shell `command` to obtain the repository password from (default: $RESTIC_PASSWORD_COMMAND)")
f.BoolVarP(&opts.Quiet, "quiet", "q", false, "do not output comprehensive progress report")
// use empty parameter name as `-v, --verbose n` instead of the correct `--verbose=n` is confusing
f.CountVarP(&opts.Verbose, "verbose", "v", "be verbose (specify multiple times or a level using --verbose=n``, max level/times is 2)")
f.BoolVar(&opts.NoLock, "no-lock", false, "do not lock the repository, this allows some operations on read-only repositories")
f.DurationVar(&opts.RetryLock, "retry-lock", 0, "retry to lock the repository if it is already locked, takes a value like 5m or 2h (default: no retries)")
f.BoolVarP(&opts.JSON, "json", "", false, "set output mode to JSON for commands that support it")
f.StringVar(&opts.CacheDir, "cache-dir", "", "set the cache `directory`. (default: use system default cache directory)")
f.BoolVar(&opts.NoCache, "no-cache", false, "do not use a local cache")
f.StringSliceVar(&opts.RootCertFilenames, "cacert", nil, "`file` to load root certificates from (default: use system certificates or $RESTIC_CACERT)")
f.StringVar(&opts.TLSClientCertKeyFilename, "tls-client-cert", "", "path to a `file` containing PEM encoded TLS client certificate and private key (default: $RESTIC_TLS_CLIENT_CERT)")
f.BoolVar(&opts.InsecureNoPassword, "insecure-no-password", false, "use an empty password for the repository, must be passed to every restic command (insecure)")
f.BoolVar(&opts.InsecureTLS, "insecure-tls", false, "skip TLS certificate verification when connecting to the repository (insecure)")
f.BoolVar(&opts.CleanupCache, "cleanup-cache", false, "auto remove old cache directories")
f.Var(&opts.Compression, "compression", "compression mode (only available for repository format version 2), one of (auto|off|max) (default: $RESTIC_COMPRESSION)")
f.BoolVar(&opts.NoExtraVerify, "no-extra-verify", false, "skip additional verification of data before upload (see documentation)")
f.IntVar(&opts.Limits.UploadKb, "limit-upload", 0, "limits uploads to a maximum `rate` in KiB/s. (default: unlimited)")
f.IntVar(&opts.Limits.DownloadKb, "limit-download", 0, "limits downloads to a maximum `rate` in KiB/s. (default: unlimited)")
f.UintVar(&opts.PackSize, "pack-size", 0, "set target pack `size` in MiB, created pack files may be larger (default: $RESTIC_PACK_SIZE)")
f.StringSliceVarP(&opts.Options, "option", "o", []string{}, "set extended option (`key=value`, can be specified multiple times)")
f.StringVar(&opts.HTTPUserAgent, "http-user-agent", "", "set a http user agent for outgoing http requests")
f.DurationVar(&opts.StuckRequestTimeout, "stuck-request-timeout", 5*time.Minute, "`duration` after which to retry stuck requests")
opts.Repo = os.Getenv("RESTIC_REPOSITORY")
opts.RepositoryFile = os.Getenv("RESTIC_REPOSITORY_FILE")
opts.PasswordFile = os.Getenv("RESTIC_PASSWORD_FILE")
opts.KeyHint = os.Getenv("RESTIC_KEY_HINT")
opts.PasswordCommand = os.Getenv("RESTIC_PASSWORD_COMMAND")
if os.Getenv("RESTIC_CACERT") != "" {
opts.RootCertFilenames = strings.Split(os.Getenv("RESTIC_CACERT"), ",")
}
opts.TLSClientCertKeyFilename = os.Getenv("RESTIC_TLS_CLIENT_CERT")
comp := os.Getenv("RESTIC_COMPRESSION")
if comp != "" {
// ignore error as there's no good way to handle it
_ = opts.Compression.Set(comp)
}
// parse target pack size from env, on error the default value will be used
targetPackSize, _ := strconv.ParseUint(os.Getenv("RESTIC_PACK_SIZE"), 10, 32)
opts.PackSize = uint(targetPackSize)
if os.Getenv("RESTIC_HTTP_USER_AGENT") != "" {
opts.HTTPUserAgent = os.Getenv("RESTIC_HTTP_USER_AGENT")
}
}
func (opts *GlobalOptions) PreRun(needsPassword bool) error {
// set verbosity, default is one
opts.verbosity = 1
if opts.Quiet && opts.Verbose > 0 {
return errors.Fatal("--quiet and --verbose cannot be specified at the same time")
}
switch {
case opts.Verbose >= 2:
opts.verbosity = 3
case opts.Verbose > 0:
opts.verbosity = 2
case opts.Quiet:
opts.verbosity = 0
}
// parse extended options
extendedOpts, err := options.Parse(opts.Options)
if err != nil {
return err
}
opts.extended = extendedOpts
if !needsPassword {
return nil
}
pwd, err := resolvePassword(opts, "RESTIC_PASSWORD")
if err != nil {
return errors.Fatal(fmt.Sprintf("Resolving password failed: %v\n", err))
}
opts.password = pwd
return nil
}
var globalOptions = GlobalOptions{ var globalOptions = GlobalOptions{
stdout: os.Stdout, stdout: os.Stdout,
stderr: os.Stderr, stderr: os.Stderr,
backends: collectBackends(),
} }
func init() { func collectBackends() *location.Registry {
backends := location.NewRegistry() backends := location.NewRegistry()
backends.Register(azure.NewFactory()) backends.Register(azure.NewFactory())
backends.Register(b2.NewFactory()) backends.Register(b2.NewFactory())
@ -111,59 +197,7 @@ func init() {
backends.Register(s3.NewFactory()) backends.Register(s3.NewFactory())
backends.Register(sftp.NewFactory()) backends.Register(sftp.NewFactory())
backends.Register(swift.NewFactory()) backends.Register(swift.NewFactory())
globalOptions.backends = backends return backends
f := cmdRoot.PersistentFlags()
f.StringVarP(&globalOptions.Repo, "repo", "r", "", "`repository` to backup to or restore from (default: $RESTIC_REPOSITORY)")
f.StringVarP(&globalOptions.RepositoryFile, "repository-file", "", "", "`file` to read the repository location from (default: $RESTIC_REPOSITORY_FILE)")
f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", "", "`file` to read the repository password from (default: $RESTIC_PASSWORD_FILE)")
f.StringVarP(&globalOptions.KeyHint, "key-hint", "", "", "`key` ID of key to try decrypting first (default: $RESTIC_KEY_HINT)")
f.StringVarP(&globalOptions.PasswordCommand, "password-command", "", "", "shell `command` to obtain the repository password from (default: $RESTIC_PASSWORD_COMMAND)")
f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not output comprehensive progress report")
// use empty parameter name as `-v, --verbose n` instead of the correct `--verbose=n` is confusing
f.CountVarP(&globalOptions.Verbose, "verbose", "v", "be verbose (specify multiple times or a level using --verbose=n``, max level/times is 2)")
f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repository, this allows some operations on read-only repositories")
f.DurationVar(&globalOptions.RetryLock, "retry-lock", 0, "retry to lock the repository if it is already locked, takes a value like 5m or 2h (default: no retries)")
f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it")
f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache `directory`. (default: use system default cache directory)")
f.BoolVar(&globalOptions.NoCache, "no-cache", false, "do not use a local cache")
f.StringSliceVar(&globalOptions.RootCertFilenames, "cacert", nil, "`file` to load root certificates from (default: use system certificates or $RESTIC_CACERT)")
f.StringVar(&globalOptions.TLSClientCertKeyFilename, "tls-client-cert", "", "path to a `file` containing PEM encoded TLS client certificate and private key (default: $RESTIC_TLS_CLIENT_CERT)")
f.BoolVar(&globalOptions.InsecureNoPassword, "insecure-no-password", false, "use an empty password for the repository, must be passed to every restic command (insecure)")
f.BoolVar(&globalOptions.InsecureTLS, "insecure-tls", false, "skip TLS certificate verification when connecting to the repository (insecure)")
f.BoolVar(&globalOptions.CleanupCache, "cleanup-cache", false, "auto remove old cache directories")
f.Var(&globalOptions.Compression, "compression", "compression mode (only available for repository format version 2), one of (auto|off|max) (default: $RESTIC_COMPRESSION)")
f.BoolVar(&globalOptions.NoExtraVerify, "no-extra-verify", false, "skip additional verification of data before upload (see documentation)")
f.IntVar(&globalOptions.Limits.UploadKb, "limit-upload", 0, "limits uploads to a maximum `rate` in KiB/s. (default: unlimited)")
f.IntVar(&globalOptions.Limits.DownloadKb, "limit-download", 0, "limits downloads to a maximum `rate` in KiB/s. (default: unlimited)")
f.UintVar(&globalOptions.PackSize, "pack-size", 0, "set target pack `size` in MiB, created pack files may be larger (default: $RESTIC_PACK_SIZE)")
f.StringSliceVarP(&globalOptions.Options, "option", "o", []string{}, "set extended option (`key=value`, can be specified multiple times)")
f.StringVar(&globalOptions.HTTPUserAgent, "http-user-agent", "", "set a http user agent for outgoing http requests")
f.DurationVar(&globalOptions.StuckRequestTimeout, "stuck-request-timeout", 5*time.Minute, "`duration` after which to retry stuck requests")
// Use our "generate" command instead of the cobra provided "completion" command
cmdRoot.CompletionOptions.DisableDefaultCmd = true
globalOptions.Repo = os.Getenv("RESTIC_REPOSITORY")
globalOptions.RepositoryFile = os.Getenv("RESTIC_REPOSITORY_FILE")
globalOptions.PasswordFile = os.Getenv("RESTIC_PASSWORD_FILE")
globalOptions.KeyHint = os.Getenv("RESTIC_KEY_HINT")
globalOptions.PasswordCommand = os.Getenv("RESTIC_PASSWORD_COMMAND")
if os.Getenv("RESTIC_CACERT") != "" {
globalOptions.RootCertFilenames = strings.Split(os.Getenv("RESTIC_CACERT"), ",")
}
globalOptions.TLSClientCertKeyFilename = os.Getenv("RESTIC_TLS_CLIENT_CERT")
comp := os.Getenv("RESTIC_COMPRESSION")
if comp != "" {
// ignore error as there's no good way to handle it
_ = globalOptions.Compression.Set(comp)
}
// parse target pack size from env, on error the default value will be used
targetPackSize, _ := strconv.ParseUint(os.Getenv("RESTIC_PACK_SIZE"), 10, 32)
globalOptions.PackSize = uint(targetPackSize)
if os.Getenv("RESTIC_HTTP_USER_AGENT") != "" {
globalOptions.HTTPUserAgent = os.Getenv("RESTIC_HTTP_USER_AGENT")
}
} }
func stdinIsTerminal() bool { func stdinIsTerminal() bool {
@ -254,7 +288,7 @@ func Warnf(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, envStr string) (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")
} }

View File

@ -11,10 +11,44 @@ import (
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/pkg/profile" "github.com/pkg/profile"
) )
func registerProfiling(cmd *cobra.Command) {
var profiler profiler
origPreRun := cmd.PersistentPreRunE
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if origPreRun != nil {
if err := origPreRun(cmd, args); err != nil {
return err
}
}
return profiler.Start(profiler.opts)
}
origPostRun := cmd.PersistentPostRunE
cmd.PersistentPostRunE = func(cmd *cobra.Command, args []string) error {
profiler.Stop()
if origPostRun != nil {
return origPostRun(cmd, args)
}
return nil
}
profiler.opts.AddFlags(cmd.PersistentFlags())
}
type profiler struct {
opts ProfileOptions
stop interface {
Stop()
}
}
type ProfileOptions struct { type ProfileOptions struct {
listen string listen string
memPath string memPath string
@ -24,19 +58,13 @@ type ProfileOptions struct {
insecure bool insecure bool
} }
var profileOpts ProfileOptions func (opts *ProfileOptions) AddFlags(f *pflag.FlagSet) {
var prof interface { f.StringVar(&opts.listen, "listen-profile", "", "listen on this `address:port` for memory profiling")
Stop() f.StringVar(&opts.memPath, "mem-profile", "", "write memory profile to `dir`")
} f.StringVar(&opts.cpuPath, "cpu-profile", "", "write cpu profile to `dir`")
f.StringVar(&opts.tracePath, "trace-profile", "", "write trace to `dir`")
func init() { f.StringVar(&opts.blockPath, "block-profile", "", "write block profile to `dir`")
f := cmdRoot.PersistentFlags() f.BoolVar(&opts.insecure, "insecure-kdf", false, "use insecure KDF settings")
f.StringVar(&profileOpts.listen, "listen-profile", "", "listen on this `address:port` for memory profiling")
f.StringVar(&profileOpts.memPath, "mem-profile", "", "write memory profile to `dir`")
f.StringVar(&profileOpts.cpuPath, "cpu-profile", "", "write cpu profile to `dir`")
f.StringVar(&profileOpts.tracePath, "trace-profile", "", "write trace to `dir`")
f.StringVar(&profileOpts.blockPath, "block-profile", "", "write block profile to `dir`")
f.BoolVar(&profileOpts.insecure, "insecure-kdf", false, "use insecure KDF settings")
} }
type fakeTestingTB struct{} type fakeTestingTB struct{}
@ -45,7 +73,7 @@ func (fakeTestingTB) Logf(msg string, args ...interface{}) {
fmt.Fprintf(os.Stderr, msg, args...) fmt.Fprintf(os.Stderr, msg, args...)
} }
func runDebug() error { func (p *profiler) Start(profileOpts ProfileOptions) error {
if profileOpts.listen != "" { if profileOpts.listen != "" {
fmt.Fprintf(os.Stderr, "running profile HTTP server on %v\n", profileOpts.listen) fmt.Fprintf(os.Stderr, "running profile HTTP server on %v\n", profileOpts.listen)
go func() { go func() {
@ -75,13 +103,13 @@ func runDebug() error {
} }
if profileOpts.memPath != "" { if profileOpts.memPath != "" {
prof = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.MemProfile, profile.ProfilePath(profileOpts.memPath)) p.stop = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.MemProfile, profile.ProfilePath(profileOpts.memPath))
} else if profileOpts.cpuPath != "" { } else if profileOpts.cpuPath != "" {
prof = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.CPUProfile, profile.ProfilePath(profileOpts.cpuPath)) p.stop = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.CPUProfile, profile.ProfilePath(profileOpts.cpuPath))
} else if profileOpts.tracePath != "" { } else if profileOpts.tracePath != "" {
prof = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.TraceProfile, profile.ProfilePath(profileOpts.tracePath)) p.stop = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.TraceProfile, profile.ProfilePath(profileOpts.tracePath))
} else if profileOpts.blockPath != "" { } else if profileOpts.blockPath != "" {
prof = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.BlockProfile, profile.ProfilePath(profileOpts.blockPath)) p.stop = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.BlockProfile, profile.ProfilePath(profileOpts.blockPath))
} }
if profileOpts.insecure { if profileOpts.insecure {
@ -91,8 +119,8 @@ func runDebug() error {
return nil return nil
} }
func stopDebug() { func (p *profiler) Stop() {
if prof != nil { if p.stop != nil {
prof.Stop() p.stop.Stop()
} }
} }

View File

@ -3,8 +3,8 @@
package main package main
// runDebug is a noop without the debug tag. import "github.com/spf13/cobra"
func runDebug() error { return nil }
// stopDebug is a noop without the debug tag. func registerProfiling(_ *cobra.Command) {
func stopDebug() {} // No profiling in release mode
}

View File

@ -17,7 +17,6 @@ import (
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/feature" "github.com/restic/restic/internal/feature"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
) )
@ -29,8 +28,11 @@ func init() {
var ErrOK = errors.New("ok") var ErrOK = errors.New("ok")
// cmdRoot is the base command when no other command has been specified. var cmdGroupDefault = "default"
var cmdRoot = &cobra.Command{ var cmdGroupAdvanced = "advanced"
func newRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "restic", Use: "restic",
Short: "Backup and restore files", Short: "Backup and restore files",
Long: ` Long: `
@ -44,51 +46,11 @@ The full documentation can be found at https://restic.readthedocs.io/ .
DisableAutoGenTag: true, DisableAutoGenTag: true,
PersistentPreRunE: func(c *cobra.Command, _ []string) error { PersistentPreRunE: func(c *cobra.Command, _ []string) error {
// set verbosity, default is one return globalOptions.PreRun(needsPassword(c.Name()))
globalOptions.verbosity = 1
if globalOptions.Quiet && globalOptions.Verbose > 0 {
return errors.Fatal("--quiet and --verbose cannot be specified at the same time")
}
switch {
case globalOptions.Verbose >= 2:
globalOptions.verbosity = 3
case globalOptions.Verbose > 0:
globalOptions.verbosity = 2
case globalOptions.Quiet:
globalOptions.verbosity = 0
}
// parse extended options
opts, err := options.Parse(globalOptions.Options)
if err != nil {
return err
}
globalOptions.extended = opts
if !needsPassword(c.Name()) {
return nil
}
pwd, err := resolvePassword(globalOptions, "RESTIC_PASSWORD")
if err != nil {
fmt.Fprintf(os.Stderr, "Resolving password failed: %v\n", err)
Exit(1)
}
globalOptions.password = pwd
// run the debug functions for all subcommands (if build tag "debug" is
// enabled)
return runDebug()
}, },
PersistentPostRun: func(_ *cobra.Command, _ []string) { }
stopDebug()
},
}
var cmdGroupDefault = "default" cmd.AddGroup(
var cmdGroupAdvanced = "advanced"
func init() {
cmdRoot.AddGroup(
&cobra.Group{ &cobra.Group{
ID: cmdGroupDefault, ID: cmdGroupDefault,
Title: "Available Commands:", Title: "Available Commands:",
@ -98,6 +60,49 @@ func init() {
Title: "Advanced Options:", Title: "Advanced Options:",
}, },
) )
globalOptions.AddFlags(cmd.PersistentFlags())
// Use our "generate" command instead of the cobra provided "completion" command
cmd.CompletionOptions.DisableDefaultCmd = true
cmd.AddCommand(
newBackupCommand(),
newCacheCommand(),
newCatCommand(),
newCheckCommand(),
newCopyCommand(),
newDiffCommand(),
newDumpCommand(),
newFeaturesCommand(),
newFindCommand(),
newForgetCommand(),
newGenerateCommand(),
newInitCommand(),
newKeyCommand(),
newListCommand(),
newLsCommand(),
newMigrateCommand(),
newOptionsCommand(),
newPruneCommand(),
newRebuildIndexCommand(),
newRecoverCommand(),
newRepairCommand(),
newRestoreCommand(),
newRewriteCommand(),
newSnapshotsCommand(),
newStatsCommand(),
newTagCommand(),
newUnlockCommand(),
newVersionCommand(),
)
registerDebugCommand(cmd)
registerMountCommand(cmd)
registerSelfUpdateCommand(cmd)
registerProfiling(cmd)
return cmd
} }
// Distinguish commands that need the password from those that work without, // Distinguish commands that need the password from those that work without,
@ -164,7 +169,7 @@ func main() {
version, runtime.Version(), runtime.GOOS, runtime.GOARCH) version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
ctx := createGlobalContext() ctx := createGlobalContext()
err = cmdRoot.ExecuteContext(ctx) err = newRootCommand().ExecuteContext(ctx)
if err == nil { if err == nil {
err = ctx.Err() err = ctx.Err()

View File

@ -25,7 +25,7 @@ type secondaryRepoOptions struct {
LegacyKeyHint string LegacyKeyHint string
} }
func initSecondaryRepoOptions(f *pflag.FlagSet, opts *secondaryRepoOptions, repoPrefix string, repoUsage string) { func (opts *secondaryRepoOptions) AddFlags(f *pflag.FlagSet, repoPrefix string, repoUsage string) {
f.StringVarP(&opts.LegacyRepo, "repo2", "", "", repoPrefix+" `repository` "+repoUsage+" (default: $RESTIC_REPOSITORY2)") f.StringVarP(&opts.LegacyRepo, "repo2", "", "", repoPrefix+" `repository` "+repoUsage+" (default: $RESTIC_REPOSITORY2)")
f.StringVarP(&opts.LegacyRepositoryFile, "repository-file2", "", "", "`file` from which to read the "+repoPrefix+" repository location "+repoUsage+" (default: $RESTIC_REPOSITORY_FILE2)") f.StringVarP(&opts.LegacyRepositoryFile, "repository-file2", "", "", "`file` from which to read the "+repoPrefix+" repository location "+repoUsage+" (default: $RESTIC_REPOSITORY_FILE2)")
f.StringVarP(&opts.LegacyPasswordFile, "password-file2", "", "", "`file` to read the "+repoPrefix+" repository password from (default: $RESTIC_PASSWORD_FILE2)") f.StringVarP(&opts.LegacyPasswordFile, "password-file2", "", "", "`file` to read the "+repoPrefix+" repository password from (default: $RESTIC_PASSWORD_FILE2)")
@ -110,7 +110,7 @@ func fillSecondaryGlobalOpts(ctx context.Context, opts secondaryRepoOptions, gop
if opts.password != "" { if opts.password != "" {
dstGopts.password = opts.password dstGopts.password = opts.password
} else { } else {
dstGopts.password, err = resolvePassword(dstGopts, pwdEnv) dstGopts.password, err = resolvePassword(&dstGopts, pwdEnv)
if err != nil { if err != nil {
return GlobalOptions{}, false, err return GlobalOptions{}, false, err
} }