From aa9cdf93cf6041b1e08f3faee03dab7548175fbf Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Fri, 7 Feb 2025 18:54:26 +0100 Subject: [PATCH] refactor persistent options to be applied via functions --- cmd/restic/global.go | 104 +++++++++++++++++------------------ cmd/restic/global_debug.go | 70 ++++++++++++++++------- cmd/restic/global_release.go | 8 +-- cmd/restic/main.go | 15 ++--- 4 files changed, 113 insertions(+), 84 deletions(-) diff --git a/cmd/restic/global.go b/cmd/restic/global.go index bea09837f..88502e18c 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -34,6 +34,7 @@ import ( "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/textfile" "github.com/restic/restic/internal/ui/termstatus" + "github.com/spf13/pflag" "github.com/restic/restic/internal/errors" @@ -95,6 +96,57 @@ type GlobalOptions struct { extended options.Options } +func (opts *GlobalOptions) AddFlags(f *pflag.FlagSet) { + f.StringVarP(&opts.Repo, "repo", "r", "", "`repository` to backup to or restore from (default: $RESTIC_REPOSITORY)") + f.StringVarP(&opts.RepositoryFile, "repository-file", "", "", "`file` to read the repository location from (default: $RESTIC_REPOSITORY_FILE)") + f.StringVarP(&opts.PasswordFile, "password-file", "p", "", "`file` to read the repository password from (default: $RESTIC_PASSWORD_FILE)") + f.StringVarP(&opts.KeyHint, "key-hint", "", "", "`key` ID of key to try decrypting first (default: $RESTIC_KEY_HINT)") + f.StringVarP(&opts.PasswordCommand, "password-command", "", "", "shell `command` to obtain the repository password from (default: $RESTIC_PASSWORD_COMMAND)") + f.BoolVarP(&opts.Quiet, "quiet", "q", false, "do not output comprehensive progress report") + // use empty parameter name as `-v, --verbose n` instead of the correct `--verbose=n` is confusing + f.CountVarP(&opts.Verbose, "verbose", "v", "be verbose (specify multiple times or a level using --verbose=n``, max level/times is 2)") + f.BoolVar(&opts.NoLock, "no-lock", false, "do not lock the repository, this allows some operations on read-only repositories") + f.DurationVar(&opts.RetryLock, "retry-lock", 0, "retry to lock the repository if it is already locked, takes a value like 5m or 2h (default: no retries)") + f.BoolVarP(&opts.JSON, "json", "", false, "set output mode to JSON for commands that support it") + f.StringVar(&opts.CacheDir, "cache-dir", "", "set the cache `directory`. (default: use system default cache directory)") + f.BoolVar(&opts.NoCache, "no-cache", false, "do not use a local cache") + f.StringSliceVar(&opts.RootCertFilenames, "cacert", nil, "`file` to load root certificates from (default: use system certificates or $RESTIC_CACERT)") + f.StringVar(&opts.TLSClientCertKeyFilename, "tls-client-cert", "", "path to a `file` containing PEM encoded TLS client certificate and private key (default: $RESTIC_TLS_CLIENT_CERT)") + f.BoolVar(&opts.InsecureNoPassword, "insecure-no-password", false, "use an empty password for the repository, must be passed to every restic command (insecure)") + f.BoolVar(&opts.InsecureTLS, "insecure-tls", false, "skip TLS certificate verification when connecting to the repository (insecure)") + f.BoolVar(&opts.CleanupCache, "cleanup-cache", false, "auto remove old cache directories") + f.Var(&opts.Compression, "compression", "compression mode (only available for repository format version 2), one of (auto|off|max) (default: $RESTIC_COMPRESSION)") + f.BoolVar(&opts.NoExtraVerify, "no-extra-verify", false, "skip additional verification of data before upload (see documentation)") + f.IntVar(&opts.Limits.UploadKb, "limit-upload", 0, "limits uploads to a maximum `rate` in KiB/s. (default: unlimited)") + f.IntVar(&opts.Limits.DownloadKb, "limit-download", 0, "limits downloads to a maximum `rate` in KiB/s. (default: unlimited)") + f.UintVar(&opts.PackSize, "pack-size", 0, "set target pack `size` in MiB, created pack files may be larger (default: $RESTIC_PACK_SIZE)") + f.StringSliceVarP(&opts.Options, "option", "o", []string{}, "set extended option (`key=value`, can be specified multiple times)") + f.StringVar(&opts.HTTPUserAgent, "http-user-agent", "", "set a http user agent for outgoing http requests") + f.DurationVar(&opts.StuckRequestTimeout, "stuck-request-timeout", 5*time.Minute, "`duration` after which to retry stuck requests") + + opts.Repo = os.Getenv("RESTIC_REPOSITORY") + opts.RepositoryFile = os.Getenv("RESTIC_REPOSITORY_FILE") + opts.PasswordFile = os.Getenv("RESTIC_PASSWORD_FILE") + opts.KeyHint = os.Getenv("RESTIC_KEY_HINT") + opts.PasswordCommand = os.Getenv("RESTIC_PASSWORD_COMMAND") + if os.Getenv("RESTIC_CACERT") != "" { + opts.RootCertFilenames = strings.Split(os.Getenv("RESTIC_CACERT"), ",") + } + opts.TLSClientCertKeyFilename = os.Getenv("RESTIC_TLS_CLIENT_CERT") + comp := os.Getenv("RESTIC_COMPRESSION") + if comp != "" { + // ignore error as there's no good way to handle it + _ = opts.Compression.Set(comp) + } + // parse target pack size from env, on error the default value will be used + targetPackSize, _ := strconv.ParseUint(os.Getenv("RESTIC_PACK_SIZE"), 10, 32) + opts.PackSize = uint(targetPackSize) + + if os.Getenv("RESTIC_HTTP_USER_AGENT") != "" { + opts.HTTPUserAgent = os.Getenv("RESTIC_HTTP_USER_AGENT") + } +} + var globalOptions = GlobalOptions{ stdout: os.Stdout, stderr: os.Stderr, @@ -112,58 +164,6 @@ func init() { backends.Register(sftp.NewFactory()) backends.Register(swift.NewFactory()) globalOptions.backends = 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 { diff --git a/cmd/restic/global_debug.go b/cmd/restic/global_debug.go index 502b2cf6e..1fe35146a 100644 --- a/cmd/restic/global_debug.go +++ b/cmd/restic/global_debug.go @@ -11,10 +11,44 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/repository" + "github.com/spf13/cobra" + "github.com/spf13/pflag" "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 { listen string memPath string @@ -24,19 +58,13 @@ type ProfileOptions struct { insecure bool } -var profileOpts ProfileOptions -var prof interface { - Stop() -} - -func init() { - f := cmdRoot.PersistentFlags() - 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") +func (opts *ProfileOptions) AddFlags(f *pflag.FlagSet) { + f.StringVar(&opts.listen, "listen-profile", "", "listen on this `address:port` for memory profiling") + 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`") + f.StringVar(&opts.blockPath, "block-profile", "", "write block profile to `dir`") + f.BoolVar(&opts.insecure, "insecure-kdf", false, "use insecure KDF settings") } type fakeTestingTB struct{} @@ -45,7 +73,7 @@ func (fakeTestingTB) Logf(msg string, args ...interface{}) { fmt.Fprintf(os.Stderr, msg, args...) } -func runDebug() error { +func (p *profiler) Start(profileOpts ProfileOptions) error { if profileOpts.listen != "" { fmt.Fprintf(os.Stderr, "running profile HTTP server on %v\n", profileOpts.listen) go func() { @@ -75,13 +103,13 @@ func runDebug() error { } 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 != "" { - 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 != "" { - 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 != "" { - 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 { @@ -91,8 +119,8 @@ func runDebug() error { return nil } -func stopDebug() { - if prof != nil { - prof.Stop() +func (p *profiler) Stop() { + if p.stop != nil { + p.stop.Stop() } } diff --git a/cmd/restic/global_release.go b/cmd/restic/global_release.go index 1dab5a293..63ad6e17d 100644 --- a/cmd/restic/global_release.go +++ b/cmd/restic/global_release.go @@ -3,8 +3,8 @@ package main -// runDebug is a noop without the debug tag. -func runDebug() error { return nil } +import "github.com/spf13/cobra" -// stopDebug is a noop without the debug tag. -func stopDebug() {} +func registerProfiling(cmd *cobra.Command) { + // No profiling in release mode +} diff --git a/cmd/restic/main.go b/cmd/restic/main.go index 096c5695c..49499a151 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -74,13 +74,7 @@ The full documentation can be found at https://restic.readthedocs.io/ . 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() + return nil }, } @@ -98,6 +92,13 @@ func init() { Title: "Advanced Options:", }, ) + + globalOptions.AddFlags(cmdRoot.PersistentFlags()) + + // Use our "generate" command instead of the cobra provided "completion" command + cmdRoot.CompletionOptions.DisableDefaultCmd = true + + registerProfiling(cmdRoot) } // Distinguish commands that need the password from those that work without,