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,10 +31,13 @@ import (
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
) )
var cmdBackup = &cobra.Command{ func newBackupCommand() *cobra.Command {
Use: "backup [flags] [FILE/DIR] ...", var opts BackupOptions
Short: "Create a new backup of files and/or directories",
Long: ` cmd := &cobra.Command{
Use: "backup [flags] [FILE/DIR] ...",
Short: "Create a new backup of files and/or directories",
Long: `
The "backup" command creates a new snapshot and saves the files and directories The "backup" command creates a new snapshot and saves the files and directories
given as the arguments. given as the arguments.
@ -47,23 +51,27 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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
}
opts.Host = hostname
} }
backupOptions.Host = hostname },
} GroupID: cmdGroupDefault,
}, DisableAutoGenTag: true,
GroupID: cmdGroupDefault, RunE: func(cmd *cobra.Command, args []string) error {
DisableAutoGenTag: true, term, cancel := setupTermstatus()
RunE: func(cmd *cobra.Command, args []string) error { defer cancel()
term, cancel := setupTermstatus() return runBackup(cmd.Context(), opts, globalOptions, term, args)
defer cancel() },
return runBackup(cmd.Context(), backupOptions, 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,12 +13,16 @@ 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 {
Use: "cache", var opts CacheOptions
Short: "Operate on local cache directories",
Long: ` cmd := &cobra.Command{
Use: "cache",
Short: "Operate on local cache directories",
Long: `
The "cache" command allows listing and cleaning local cache directories. The "cache" command allows listing and cleaning local cache directories.
EXIT STATUS EXIT STATUS
@ -27,11 +31,15 @@ EXIT STATUS
Exit status is 0 if the command was successful. Exit status is 0 if the command was successful.
Exit status is 1 if there was any error. 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,10 +14,11 @@ 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 {
Use: "cat [flags] [masterkey|config|pack ID|blob ID|snapshot ID|index ID|key ID|lock ID|tree snapshot:subfolder]", cmd := &cobra.Command{
Short: "Print internal objects to stdout", Use: "cat [flags] [masterkey|config|pack ID|blob ID|snapshot ID|index ID|key ID|lock ID|tree snapshot:subfolder]",
Long: ` Short: "Print internal objects to stdout",
Long: `
The "cat" command is used to print internal objects to stdout. The "cat" command is used to print internal objects to stdout.
EXIT STATUS EXIT STATUS
@ -29,16 +30,14 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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.
`, `,
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 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,10 +23,12 @@ import (
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
) )
var cmdCheck = &cobra.Command{ func newCheckCommand() *cobra.Command {
Use: "check [flags]", var opts CheckOptions
Short: "Check the repository for errors", cmd := &cobra.Command{
Long: ` Use: "check [flags]",
Short: "Check the repository for errors",
Long: `
The "check" command tests the repository for errors and reports any errors it The "check" command tests the repository for errors and reports any errors it
finds. It can also be used to read all data and therefore simulate a restore. finds. It can also be used to read all data and therefore simulate a restore.
@ -41,23 +44,27 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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.
`, `,
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
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
}
term.Print(ui.ToJSONString(summary))
} }
term.Print(ui.ToJSONString(summary)) return err
} },
return err PreRunE: func(_ *cobra.Command, _ []string) error {
}, return checkFlags(opts)
PreRunE: func(_ *cobra.Command, _ []string) error { },
return checkFlags(checkOptions) }
},
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,12 +11,15 @@ 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 {
Use: "copy [flags] [snapshotID ...]", var opts CopyOptions
Short: "Copy snapshots from one repository to another", cmd := &cobra.Command{
Long: ` Use: "copy [flags] [snapshotID ...]",
Short: "Copy snapshots from one repository to another",
Long: `
The "copy" command copies one or more snapshots from one repository to another. The "copy" command copies one or more snapshots from one repository to another.
NOTE: This process will have to both download (read) and upload (write) the NOTE: This process will have to both download (read) and upload (write) the
@ -40,11 +43,15 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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.
`, `,
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,17 +29,29 @@ import (
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
) )
var cmdDebug = &cobra.Command{ func registerDebugCommand(cmd *cobra.Command) {
Use: "debug", cmd.AddCommand(
Short: "Debug commands", newDebugCommand(),
GroupID: cmdGroupDefault, )
DisableAutoGenTag: true,
} }
var cmdDebugDump = &cobra.Command{ func newDebugCommand() *cobra.Command {
Use: "dump [indexes|snapshots|all|packs]", cmd := &cobra.Command{
Short: "Dump data structures", Use: "debug",
Long: ` Short: "Debug commands",
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
}
cmd.AddCommand(newDebugDumpCommand())
cmd.AddCommand(newDebugExamineCommand())
return cmd
}
func newDebugDumpCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "dump [indexes|snapshots|all|packs]",
Short: "Dump data structures",
Long: `
The "dump" command dumps data structures from the repository as JSON objects. It The "dump" command dumps data structures from the repository as JSON objects. It
is used for debugging purposes only. is used for debugging purposes only.
@ -51,10 +64,28 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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 { 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,12 +12,16 @@ 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 {
Use: "diff [flags] snapshotID snapshotID", var opts DiffOptions
Short: "Show differences between two snapshots",
Long: ` cmd := &cobra.Command{
Use: "diff [flags] snapshotID snapshotID",
Short: "Show differences between two snapshots",
Long: `
The "diff" command shows differences from the first to the second snapshot. The The "diff" command shows differences from the first to the second snapshot. The
first characters in each line display what has happened to a particular file or first characters in each line display what has happened to a particular file or
directory: directory:
@ -45,11 +49,15 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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.
`, `,
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,12 +13,15 @@ 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 {
Use: "dump [flags] snapshotID file", var opts DumpOptions
Short: "Print a backed-up file to stdout", cmd := &cobra.Command{
Long: ` Use: "dump [flags] snapshotID file",
Short: "Print a backed-up file to stdout",
Long: `
The "dump" command extracts files from a snapshot from the repository. If a The "dump" command extracts files from a snapshot from the repository. If a
single file is selected, it prints its contents to stdout. Folders are output single file is selected, it prints its contents to stdout. Folders are output
as a tar (default) or zip file containing the contents of the specified folder. as a tar (default) or zip file containing the contents of the specified folder.
@ -40,11 +43,15 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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.
`, `,
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,10 +10,11 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var featuresCmd = &cobra.Command{ func newFeaturesCommand() *cobra.Command {
Use: "features", cmd := &cobra.Command{
Short: "Print list of feature flags", Use: "features",
Long: ` Short: "Print list of feature flags",
Long: `
The "features" command prints a list of supported feature flags. The "features" command prints a list of supported feature flags.
To pass feature flags to restic, set the RESTIC_FEATURES environment variable To pass feature flags to restic, set the RESTIC_FEATURES environment variable
@ -31,29 +32,28 @@ EXIT STATUS
Exit status is 0 if the command was successful. Exit status is 0 if the command was successful.
Exit status is 1 if there was any error. Exit status is 1 if there was any error.
`, `,
GroupID: cmdGroupAdvanced, GroupID: cmdGroupAdvanced,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
if len(args) != 0 { if len(args) != 0 {
return errors.Fatal("the feature command expects no arguments") return errors.Fatal("the feature command expects no arguments")
} }
fmt.Printf("All Feature Flags:\n") fmt.Printf("All Feature Flags:\n")
flags := feature.Flag.List() flags := feature.Flag.List()
tab := table.New() tab := table.New()
tab.AddColumn("Name", "{{ .Name }}") tab.AddColumn("Name", "{{ .Name }}")
tab.AddColumn("Type", "{{ .Type }}") tab.AddColumn("Type", "{{ .Type }}")
tab.AddColumn("Default", "{{ .Default }}") tab.AddColumn("Default", "{{ .Default }}")
tab.AddColumn("Description", "{{ .Description }}") tab.AddColumn("Description", "{{ .Description }}")
for _, flag := range flags { for _, flag := range flags {
tab.AddRow(flag) tab.AddRow(flag)
} }
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,16 +17,19 @@ import (
"github.com/restic/restic/internal/walker" "github.com/restic/restic/internal/walker"
) )
var cmdFind = &cobra.Command{ func newFindCommand() *cobra.Command {
Use: "find [flags] PATTERN...", var opts FindOptions
Short: "Find a file, a directory or restic IDs",
Long: ` cmd := &cobra.Command{
Use: "find [flags] PATTERN...",
Short: "Find a file, a directory or restic IDs",
Long: `
The "find" command searches for files or directories in snapshots stored in the The "find" command searches for files or directories in snapshots stored in the
repo. repo.
It can also be used to search for restic blobs or trees for troubleshooting. It can also be used to search for restic blobs or trees for troubleshooting.
The default sort option for the snapshots is youngest to oldest. To sort the The default sort option for the snapshots is youngest to oldest. To sort the
output from oldest to youngest specify --reverse.`, output from oldest to youngest specify --reverse.`,
Example: `restic find config.json Example: `restic find config.json
restic find --json "*.yml" "*.json" restic find --json "*.yml" "*.json"
restic find --json --blob 420f620f b46ebe8a ddd38656 restic find --json --blob 420f620f b46ebe8a ddd38656
restic find --show-pack-id --blob 420f620f restic find --show-pack-id --blob 420f620f
@ -41,11 +45,15 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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.
`, `,
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,12 +11,17 @@ 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 {
Use: "forget [flags] [snapshot ID] [...]", var opts ForgetOptions
Short: "Remove snapshots from the repository", var pruneOpts PruneOptions
Long: `
cmd := &cobra.Command{
Use: "forget [flags] [snapshot ID] [...]",
Short: "Remove snapshots from the repository",
Long: `
The "forget" command removes snapshots according to a policy. All snapshots are The "forget" command removes snapshots according to a policy. All snapshots are
first divided into groups according to "--group-by", and after that the policy first divided into groups according to "--group-by", and after that the policy
specified by the "--keep-*" options is applied to each group individually. specified by the "--keep-*" options is applied to each group individually.
@ -40,13 +45,18 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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.
`, `,
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
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,12 +8,16 @@ 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 {
Use: "generate [flags]", var opts generateOptions
Short: "Generate manual pages and auto-completion files (bash, fish, zsh, powershell)",
Long: ` cmd := &cobra.Command{
Use: "generate [flags]",
Short: "Generate manual pages and auto-completion files (bash, fish, zsh, powershell)",
Long: `
The "generate" command writes automatically generated files (like the man pages The "generate" command writes automatically generated files (like the man pages
and the auto-completion files for bash, fish and zsh). and the auto-completion files for bash, fish and zsh).
@ -23,10 +27,13 @@ EXIT STATUS
Exit status is 0 if the command was successful. Exit status is 0 if the command was successful.
Exit status is 1 if there was any error. 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,12 +12,16 @@ 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 {
Use: "init", var opts InitOptions
Short: "Initialize a new repository",
Long: ` cmd := &cobra.Command{
Use: "init",
Short: "Initialize a new repository",
Long: `
The "init" command initializes a new repository. The "init" command initializes a new repository.
EXIT STATUS EXIT STATUS
@ -26,11 +30,14 @@ EXIT STATUS
Exit status is 0 if the command was successful. Exit status is 0 if the command was successful.
Exit status is 1 if there was any error. 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,17 +4,23 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var cmdKey = &cobra.Command{ func newKeyCommand() *cobra.Command {
Use: "key", cmd := &cobra.Command{
Short: "Manage keys (passwords)", Use: "key",
Long: ` Short: "Manage keys (passwords)",
Long: `
The "key" command allows you to set multiple access keys or passwords The "key" command allows you to set multiple access keys or passwords
per repository. 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,10 +10,13 @@ import (
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
var cmdKeyAdd = &cobra.Command{ func newKeyAddCommand() *cobra.Command {
Use: "add", var opts KeyAddOptions
Short: "Add a new key (password) to the repository; returns the new key ID",
Long: ` cmd := &cobra.Command{
Use: "add",
Short: "Add a new key (password) to the repository; returns the new key ID",
Long: `
The "add" sub-command creates a new key and validates the key. Returns the new key ID. The "add" sub-command creates a new key and validates the key. Returns the new key ID.
EXIT STATUS EXIT STATUS
@ -25,7 +28,14 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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,10 +12,11 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var cmdKeyList = &cobra.Command{ func newKeyListCommand() *cobra.Command {
Use: "list", cmd := &cobra.Command{
Short: "List keys (passwords)", Use: "list",
Long: ` Short: "List keys (passwords)",
Long: `
The "list" sub-command lists all the keys (passwords) associated with the repository. The "list" sub-command lists all the keys (passwords) associated with the repository.
Returns the key ID, username, hostname, created time and if it's the current key being Returns the key ID, username, hostname, created time and if it's the current key being
used to access the repository. used to access the repository.
@ -29,14 +30,12 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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 { 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,12 +7,16 @@ 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 {
Use: "passwd", var opts KeyPasswdOptions
Short: "Change key (password); creates a new key ID and removes the old key ID, returns new key ID",
Long: ` cmd := &cobra.Command{
Use: "passwd",
Short: "Change key (password); creates a new key ID and removes the old key ID, returns new key ID",
Long: `
The "passwd" sub-command creates a new key, validates the key and remove the old key ID. The "passwd" sub-command creates a new key, validates the key and remove the old key ID.
Returns the new key ID. Returns the new key ID.
@ -25,21 +29,22 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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,10 +10,11 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var cmdKeyRemove = &cobra.Command{ func newKeyRemoveCommand() *cobra.Command {
Use: "remove [ID]", cmd := &cobra.Command{
Short: "Remove key ID (password) from the repository.", Use: "remove [ID]",
Long: ` Short: "Remove key ID (password) from the repository.",
Long: `
The "remove" sub-command removes the selected key ID. The "remove" command does not allow The "remove" sub-command removes the selected key ID. The "remove" command does not allow
removing the current key being used to access the repository. removing the current key being used to access the repository.
@ -26,14 +27,12 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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 { 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,13 +11,14 @@ 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: `
The "list" command allows listing objects in the repository based on type. The "list" command allows listing objects in the repository based on type.
EXIT STATUS EXIT STATUS
@ -29,17 +30,15 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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,
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runList(cmd.Context(), globalOptions, args) return runList(cmd.Context(), globalOptions, args)
}, },
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,10 +21,13 @@ import (
"github.com/restic/restic/internal/walker" "github.com/restic/restic/internal/walker"
) )
var cmdLs = &cobra.Command{ func newLsCommand() *cobra.Command {
Use: "ls [flags] snapshotID [dir...]", var opts LsOptions
Short: "List files in a snapshot",
Long: ` cmd := &cobra.Command{
Use: "ls [flags] snapshotID [dir...]",
Short: "List files in a snapshot",
Long: `
The "ls" command lists files and directories in a snapshot. The "ls" command lists files and directories in a snapshot.
The special snapshot ID "latest" can be used to list files and The special snapshot ID "latest" can be used to list files and
@ -52,11 +56,14 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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,
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,12 +9,16 @@ 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 {
Use: "migrate [flags] [migration name] [...]", var opts MigrateOptions
Short: "Apply migrations",
Long: ` cmd := &cobra.Command{
Use: "migrate [flags] [migration name] [...]",
Short: "Apply migrations",
Long: `
The "migrate" command checks which migrations can be applied for a repository The "migrate" command checks which migrations can be applied for a repository
and prints a list with available migration names. If one or more migration and prints a list with available migration names. If one or more migration
names are specified, these migrations are applied. names are specified, these migrations are applied.
@ -28,13 +32,17 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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,
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
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,10 +22,17 @@ import (
"github.com/anacrolix/fuse/fs" "github.com/anacrolix/fuse/fs"
) )
var cmdMount = &cobra.Command{ func registerMountCommand(cmdRoot *cobra.Command) {
Use: "mount [flags] mountpoint", cmdRoot.AddCommand(newMountCommand())
Short: "Mount the repository", }
Long: `
func newMountCommand() *cobra.Command {
var opts MountOptions
cmd := &cobra.Command{
Use: "mount [flags] mountpoint",
Short: "Mount the repository",
Long: `
The "mount" command mounts the repository via fuse to a directory. This is a The "mount" command mounts the repository via fuse to a directory. This is a
read-only mount. read-only mount.
@ -69,11 +77,15 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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,
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,10 +8,11 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var optionsCmd = &cobra.Command{ func newOptionsCommand() *cobra.Command {
Use: "options", cmd := &cobra.Command{
Short: "Print list of extended options", Use: "options",
Long: ` Short: "Print list of extended options",
Long: `
The "options" command prints a list of extended options. The "options" command prints a list of extended options.
EXIT STATUS EXIT STATUS
@ -20,22 +21,20 @@ EXIT STATUS
Exit status is 0 if the command was successful. Exit status is 0 if the command was successful.
Exit status is 1 if there was any error. Exit status is 1 if there was any error.
`, `,
GroupID: cmdGroupAdvanced, GroupID: cmdGroupAdvanced,
DisableAutoGenTag: true, DisableAutoGenTag: true,
Run: func(_ *cobra.Command, _ []string) { Run: func(_ *cobra.Command, _ []string) {
fmt.Printf("All Extended Options:\n") fmt.Printf("All Extended Options:\n")
var maxLen int var maxLen int
for _, opt := range options.List() { for _, opt := range options.List() {
if l := len(opt.Namespace + "." + opt.Name); l > maxLen { if l := len(opt.Namespace + "." + opt.Name); l > maxLen {
maxLen = l maxLen = l
}
} }
} for _, opt := range options.List() {
for _, opt := range options.List() { 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,12 +16,16 @@ 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 {
Use: "prune [flags]", var opts PruneOptions
Short: "Remove unneeded data from the repository",
Long: ` cmd := &cobra.Command{
Use: "prune [flags]",
Short: "Remove unneeded data from the repository",
Long: `
The "prune" command checks the repository and removes data that is not The "prune" command checks the repository and removes data that is not
referenced and therefore not needed any more. referenced and therefore not needed any more.
@ -34,13 +38,17 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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.
`, `,
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
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,10 +11,11 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
var cmdRecover = &cobra.Command{ func newRecoverCommand() *cobra.Command {
Use: "recover [flags]", cmd := &cobra.Command{
Short: "Recover data from the repository not referenced by snapshots", Use: "recover [flags]",
Long: ` Short: "Recover data from the repository not referenced by snapshots",
Long: `
The "recover" command builds a new snapshot from all directories it can find in The "recover" command builds a new snapshot from all directories it can find in
the raw data of the repository which are not referenced in an existing snapshot. the raw data of the repository which are not referenced in an existing snapshot.
It can be used if, for example, a snapshot has been removed by accident with "forget". It can be used if, for example, a snapshot has been removed by accident with "forget".
@ -28,15 +29,13 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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.
`, `,
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
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 {
Use: "repair", cmd := &cobra.Command{
Short: "Repair the repository", Use: "repair",
GroupID: cmdGroupDefault, Short: "Repair the repository",
DisableAutoGenTag: true, GroupID: cmdGroupDefault,
} DisableAutoGenTag: true,
}
func init() { cmd.AddCommand(
cmdRoot.AddCommand(cmdRepair) newRepairIndexCommand(),
newRepairPacksCommand(),
newRepairSnapshotsCommand(),
)
return cmd
} }

View File

@ -9,10 +9,13 @@ import (
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
var cmdRepairIndex = &cobra.Command{ func newRepairIndexCommand() *cobra.Command {
Use: "index [flags]", var opts RepairIndexOptions
Short: "Build a new index",
Long: ` cmd := &cobra.Command{
Use: "index [flags]",
Short: "Build a new index",
Long: `
The "repair index" command creates a new index based on the pack files in the The "repair index" command creates a new index based on the pack files in the
repository. repository.
@ -25,21 +28,16 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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, _ []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,10 +13,11 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var cmdRepairPacks = &cobra.Command{ func newRepairPacksCommand() *cobra.Command {
Use: "packs [packIDs...]", cmd := &cobra.Command{
Short: "Salvage damaged pack files", Use: "packs [packIDs...]",
Long: ` Short: "Salvage damaged pack files",
Long: `
The "repair packs" command extracts intact blobs from the specified pack files, rebuilds The "repair packs" command extracts intact blobs from the specified pack files, rebuilds
the index to remove the damaged pack files and removes the pack files from the repository. the index to remove the damaged pack files and removes the pack files from the repository.
@ -29,16 +30,14 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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 { RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus() term, cancel := setupTermstatus()
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,12 +8,16 @@ 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 {
Use: "snapshots [flags] [snapshot ID] [...]", var opts RepairOptions
Short: "Repair snapshots",
Long: ` cmd := &cobra.Command{
Use: "snapshots [flags] [snapshot ID] [...]",
Short: "Repair snapshots",
Long: `
The "repair snapshots" command repairs broken snapshots. It scans the given The "repair snapshots" command repairs broken snapshots. It scans the given
snapshots and generates new ones with damaged directories and file contents snapshots and generates new ones with damaged directories and file contents
removed. If the broken snapshots are deleted, a prune run will be able to removed. If the broken snapshots are deleted, a prune run will be able to
@ -43,10 +47,14 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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 { 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,12 +15,16 @@ 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 {
Use: "restore [flags] snapshotID", var opts RestoreOptions
Short: "Extract the data from a snapshot",
Long: ` cmd := &cobra.Command{
Use: "restore [flags] snapshotID",
Short: "Extract the data from a snapshot",
Long: `
The "restore" command extracts the data from a snapshot from the repository to The "restore" command extracts the data from a snapshot from the repository to
a directory. a directory.
@ -39,13 +43,17 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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.
`, `,
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
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,10 +16,13 @@ import (
"github.com/restic/restic/internal/walker" "github.com/restic/restic/internal/walker"
) )
var cmdRewrite = &cobra.Command{ func newRewriteCommand() *cobra.Command {
Use: "rewrite [flags] [snapshotID ...]", var opts RewriteOptions
Short: "Rewrite snapshots to exclude unwanted files",
Long: ` cmd := &cobra.Command{
Use: "rewrite [flags] [snapshotID ...]",
Short: "Rewrite snapshots to exclude unwanted files",
Long: `
The "rewrite" command excludes files from existing snapshots. It creates new The "rewrite" command excludes files from existing snapshots. It creates new
snapshots containing the same data as the original ones, but without the files snapshots containing the same data as the original ones, but without the files
you specify to exclude. All metadata (time, host, tags) will be preserved. you specify to exclude. All metadata (time, host, tags) will be preserved.
@ -51,11 +55,15 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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.
`, `,
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,12 +10,22 @@ 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) {
Use: "self-update [flags]", cmd.AddCommand(
Short: "Update the restic binary", newSelfUpdateCommand(),
Long: ` )
}
func newSelfUpdateCommand() *cobra.Command {
var opts SelfUpdateOptions
cmd := &cobra.Command{
Use: "self-update [flags]",
Short: "Update the restic binary",
Long: `
The command "self-update" downloads the latest stable release of restic from The command "self-update" downloads the latest stable release of restic from
GitHub and replaces the currently running binary. After download, the GitHub and replaces the currently running binary. After download, the
authenticity of the binary is verified using the GPG signature on the release authenticity of the binary is verified using the GPG signature on the release
@ -30,10 +40,14 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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 { 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,12 +12,16 @@ 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 {
Use: "snapshots [flags] [snapshotID ...]", var opts SnapshotOptions
Short: "List all snapshots",
Long: ` cmd := &cobra.Command{
Use: "snapshots [flags] [snapshotID ...]",
Short: "List all snapshots",
Long: `
The "snapshots" command lists all snapshots stored in the repository. The "snapshots" command lists all snapshots stored in the repository.
EXIT STATUS EXIT STATUS
@ -29,11 +33,15 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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.
`, `,
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,12 +18,16 @@ 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 {
Use: "stats [flags] [snapshot ID] [...]", var opts StatsOptions
Short: "Scan the repository and show basic statistics",
Long: ` cmd := &cobra.Command{
Use: "stats [flags] [snapshot ID] [...]",
Short: "Scan the repository and show basic statistics",
Long: `
The "stats" command walks one or multiple snapshots in a repository The "stats" command walks one or multiple snapshots in a repository
and accumulates statistics about the data stored therein. It reports and accumulates statistics about the data stored therein. It reports
on the number of unique files and their sizes, according to one of on the number of unique files and their sizes, according to one of
@ -55,11 +59,18 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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.
`, `,
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,10 +14,13 @@ import (
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
) )
var cmdTag = &cobra.Command{ func newTagCommand() *cobra.Command {
Use: "tag [flags] [snapshotID ...]", var opts TagOptions
Short: "Modify tags on snapshots",
Long: ` cmd := &cobra.Command{
Use: "tag [flags] [snapshotID ...]",
Short: "Modify tags on snapshots",
Long: `
The "tag" command allows you to modify tags on exiting snapshots. The "tag" command allows you to modify tags on exiting snapshots.
You can either set/replace the entire set of tags on a snapshot, or You can either set/replace the entire set of tags on a snapshot, or
@ -33,13 +37,17 @@ Exit status is 10 if the repository does not exist.
Exit status is 11 if the repository is already locked. 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.
`, `,
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
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,12 +5,16 @@ 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 {
Use: "unlock", var opts UnlockOptions
Short: "Remove locks other processes created",
Long: ` cmd := &cobra.Command{
Use: "unlock",
Short: "Remove locks other processes created",
Long: `
The "unlock" command removes stale locks that have been created by other restic processes. The "unlock" command removes stale locks that have been created by other restic processes.
EXIT STATUS EXIT STATUS
@ -19,11 +23,14 @@ EXIT STATUS
Exit status is 0 if the command was successful. Exit status is 0 if the command was successful.
Exit status is 1 if there was any error. 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,10 +8,11 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var versionCmd = &cobra.Command{ func newVersionCommand() *cobra.Command {
Use: "version", cmd := &cobra.Command{
Short: "Print version information", Use: "version",
Long: ` Short: "Print version information",
Long: `
The "version" command prints detailed information about the build environment The "version" command prints detailed information about the build environment
and the version of this software. and the version of this software.
@ -21,38 +22,36 @@ EXIT STATUS
Exit status is 0 if the command was successful. Exit status is 0 if the command was successful.
Exit status is 1 if there was any error. Exit status is 1 if there was any error.
`, `,
DisableAutoGenTag: true, DisableAutoGenTag: true,
Run: func(_ *cobra.Command, _ []string) { Run: func(_ *cobra.Command, _ []string) {
if globalOptions.JSON { if globalOptions.JSON {
type jsonVersion struct { type jsonVersion struct {
MessageType string `json:"message_type"` // version MessageType string `json:"message_type"` // version
Version string `json:"version"` Version string `json:"version"`
GoVersion string `json:"go_version"` GoVersion string `json:"go_version"`
GoOS string `json:"go_os"` GoOS string `json:"go_os"`
GoArch string `json:"go_arch"` GoArch string `json:"go_arch"`
}
jsonS := jsonVersion{
MessageType: "version",
Version: version,
GoVersion: runtime.Version(),
GoOS: runtime.GOOS,
GoArch: runtime.GOARCH,
}
err := json.NewEncoder(globalOptions.stdout).Encode(jsonS)
if err != nil {
Warnf("JSON encode failed: %v\n", err)
return
}
} else {
fmt.Printf("restic %s compiled with %v on %v/%v\n",
version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
} }
jsonS := jsonVersion{ },
MessageType: "version", }
Version: version, return cmd
GoVersion: runtime.Version(),
GoOS: runtime.GOOS,
GoArch: runtime.GOARCH,
}
err := json.NewEncoder(globalOptions.stdout).Encode(jsonS)
if err != nil {
Warnf("JSON encode failed: %v\n", err)
return
}
} else {
fmt.Printf("restic %s compiled with %v on %v/%v\n",
version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
}
},
}
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
} }
var globalOptions = GlobalOptions{ func (opts *GlobalOptions) AddFlags(f *pflag.FlagSet) {
stdout: os.Stdout, f.StringVarP(&opts.Repo, "repo", "r", "", "`repository` to backup to or restore from (default: $RESTIC_REPOSITORY)")
stderr: os.Stderr, 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 init() { 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{
stdout: os.Stdout,
stderr: os.Stderr,
backends: collectBackends(),
}
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,66 +28,29 @@ 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"
Use: "restic",
Short: "Backup and restore files", func newRootCommand() *cobra.Command {
Long: ` cmd := &cobra.Command{
Use: "restic",
Short: "Backup and restore files",
Long: `
restic is a backup program which allows saving multiple revisions of files and restic is a backup program which allows saving multiple revisions of files and
directories in an encrypted repository stored on different backends. directories in an encrypted repository stored on different backends.
The full documentation can be found at https://restic.readthedocs.io/ . The full documentation can be found at https://restic.readthedocs.io/ .
`, `,
SilenceErrors: true, SilenceErrors: true,
SilenceUsage: true, SilenceUsage: true,
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 { cmd.AddGroup(
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"
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
} }