Two output-style fixes plus a quiet-mode correction.
Banner: a manual scan of os.Args in CLIEntry decides whether to suppress
the banner (--quiet/-q/--cron), then prints it before cobra parses any
arguments. This makes the banner appear even when cobra rejects bad args
("requires at least 2 arg(s)") and on --help — paths that previously
skipped PersistentPreRun entirely. The cobra-side hook plumbing (sync.Once,
PersistentPreRun, custom HelpFunc) is removed.
Errors: rootCmd.SilenceErrors = true so cobra no longer prints its own
"Error: <msg>" line. Any error returned from Execute() goes through
ui.New(os.Stderr).Error(...), giving the documented "🛑 ERROR: <msg>"
format. A new helper cli.ReportError() formats errors from goroutine
paths that can't return through cobra's normal return chain; every
CLI command's fx-goroutine error path now calls it alongside the
existing structured log.Error so both channels record the failure.
Quiet mode: previously --quiet/--cron swapped Vaultik.UI to io.Discard,
which silenced Warning and Error messages too — contradicting the
documented "suppresses non-error output" semantics. ui.Writer now has
a SetQuiet flag that drops Begin/Complete/Info/Notice/Detail/Progress/
Banner only; Warning and Error always emit.
Also folds in restore.go cleanups the audit flagged: the hardcoded
"WARNING:" prefix on the failed-files block now uses ui.Warning +
ui.Detail, the post-restore "Restored N files" line uses ui.Complete,
and the "No files found to restore" branch emits both log.Warn and
ui.Warning so structured logs continue to capture it under --verbose.
156 lines
4.4 KiB
Go
156 lines
4.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
|
|
"github.com/spf13/cobra"
|
|
"go.uber.org/fx"
|
|
"sneak.berlin/go/vaultik/internal/config"
|
|
"sneak.berlin/go/vaultik/internal/globals"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/storage"
|
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
|
)
|
|
|
|
// RestoreOptions contains options for the restore command
|
|
type RestoreOptions struct {
|
|
TargetDir string
|
|
Paths []string // Optional paths to restore (empty = all)
|
|
Verify bool // Verify restored files after restore
|
|
}
|
|
|
|
// RestoreApp contains all dependencies needed for restore
|
|
type RestoreApp struct {
|
|
Globals *globals.Globals
|
|
Config *config.Config
|
|
Storage storage.Storer
|
|
Vaultik *vaultik.Vaultik
|
|
Shutdowner fx.Shutdowner
|
|
}
|
|
|
|
// newSnapshotRestoreCommand creates the 'snapshot restore' subcommand
|
|
func newSnapshotRestoreCommand() *cobra.Command {
|
|
opts := &RestoreOptions{}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "restore <snapshot-id> <target-dir> [paths...]",
|
|
Short: "Restore files from a snapshot",
|
|
Long: `Download and decrypt files from a backup snapshot.
|
|
|
|
This command will restore files from the specified snapshot to the target directory.
|
|
If no paths are specified, all files are restored.
|
|
If paths are specified, only matching files/directories are restored.
|
|
|
|
Requires the VAULTIK_AGE_SECRET_KEY environment variable to be set with the age private key.
|
|
|
|
Examples:
|
|
# Restore entire snapshot
|
|
vaultik snapshot restore myhost_docs_2025-01-01T12:00:00Z /restore
|
|
|
|
# Restore specific file
|
|
vaultik snapshot restore myhost_docs_2025-01-01T12:00:00Z /restore /home/user/important.txt
|
|
|
|
# Restore specific directory
|
|
vaultik snapshot restore myhost_docs_2025-01-01T12:00:00Z /restore /home/user/documents/
|
|
|
|
# Restore and verify all files
|
|
vaultik snapshot restore --verify myhost_docs_2025-01-01T12:00:00Z /restore`,
|
|
Args: cobra.MinimumNArgs(2),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runRestore(cmd, args, opts)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolVar(&opts.Verify, "verify", false, "Verify restored files by checking chunk hashes")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// runRestore parses arguments and runs the restore operation through the app framework
|
|
func runRestore(cmd *cobra.Command, args []string, opts *RestoreOptions) error {
|
|
snapshotID := args[0]
|
|
opts.TargetDir = args[1]
|
|
if len(args) > 2 {
|
|
opts.Paths = args[2:]
|
|
}
|
|
|
|
// Use unified config resolution
|
|
configPath, err := ResolveConfigPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Use the app framework like other commands
|
|
rootFlags := GetRootFlags()
|
|
return RunWithApp(cmd.Context(), AppOptions{
|
|
ConfigPath: configPath,
|
|
LogOptions: log.LogOptions{
|
|
Verbose: rootFlags.Verbose,
|
|
Debug: rootFlags.Debug,
|
|
Quiet: rootFlags.Quiet,
|
|
},
|
|
Modules: buildRestoreModules(),
|
|
Invokes: buildRestoreInvokes(snapshotID, opts),
|
|
})
|
|
}
|
|
|
|
// buildRestoreModules returns the fx.Options for dependency injection in restore
|
|
func buildRestoreModules() []fx.Option {
|
|
return []fx.Option{
|
|
fx.Provide(fx.Annotate(
|
|
func(g *globals.Globals, cfg *config.Config,
|
|
storer storage.Storer, v *vaultik.Vaultik, shutdowner fx.Shutdowner) *RestoreApp {
|
|
return &RestoreApp{
|
|
Globals: g,
|
|
Config: cfg,
|
|
Storage: storer,
|
|
Vaultik: v,
|
|
Shutdowner: shutdowner,
|
|
}
|
|
},
|
|
)),
|
|
}
|
|
}
|
|
|
|
// buildRestoreInvokes returns the fx.Options that wire up the restore lifecycle
|
|
func buildRestoreInvokes(snapshotID string, opts *RestoreOptions) []fx.Option {
|
|
return []fx.Option{
|
|
fx.Invoke(func(app *RestoreApp, lc fx.Lifecycle) {
|
|
lc.Append(fx.Hook{
|
|
OnStart: func(ctx context.Context) error {
|
|
// Start the restore operation in a goroutine
|
|
go func() {
|
|
// Run the restore operation
|
|
restoreOpts := &vaultik.RestoreOptions{
|
|
SnapshotID: snapshotID,
|
|
TargetDir: opts.TargetDir,
|
|
Paths: opts.Paths,
|
|
Verify: opts.Verify,
|
|
SkipErrors: GetRootFlags().SkipErrors,
|
|
}
|
|
if err := app.Vaultik.Restore(restoreOpts); err != nil {
|
|
if err != context.Canceled {
|
|
log.Error("Restore operation failed", "error", err)
|
|
ReportError("Restore failed: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// Shutdown the app when restore completes
|
|
if err := app.Shutdowner.Shutdown(); err != nil {
|
|
log.Error("Failed to shutdown", "error", err)
|
|
}
|
|
}()
|
|
return nil
|
|
},
|
|
OnStop: func(ctx context.Context) error {
|
|
log.Debug("Stopping restore operation")
|
|
app.Vaultik.Cancel()
|
|
return nil
|
|
},
|
|
})
|
|
}),
|
|
}
|
|
}
|