Fix every finding surfaced by the canonical .golangci.yml with golangci-lint v2.12.2 (refs #61), behavior-preserving throughout: - err113: dynamic errors replaced with package-level sentinels and %w wrapping; direct comparisons converted to errors.Is - goprintffuncname: printf-style helpers renamed with an f suffix (ui.Writer message methods, cli.ReportErrorf, database.Fatalf, vaultik stdoutf) and all call sites updated - revive: stuttering type names renamed (blob.Handler, blob.WithReader, blob.ChunkPosition, storage.URL, storage.Info), doc comments added, unused parameters blanked, package comments added - contextcheck/noctx: ctx threaded through blob.Packer (AddChunk/Flush/FinalizeBlob/PackChunks) and scanner call sites; context-aware exec and sql variants used - funlen/cyclop/gocognit/nestif/dupl: oversized or duplicated functions split into focused helpers across production and test code - paralleltest/tparallel/thelper/usetesting/testpackage: tests parallelized where safe (global log.Initialize kept in the serial phase), helpers marked, t.TempDir adopted, external test packages where only exported API is used - gosec: integer conversions clamped or justified, header timeouts added, remaining findings suppressed with per-site justifications - mnd/goconst/lll/wsl_v5/nlreturn/noinlineerr/errcheck and other mechanical findings fixed directly Remove the deprecated log.LogOptions alias (callers migrated to log.Options). make check is green.
173 lines
4.7 KiB
Go
173 lines
4.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"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"
|
|
)
|
|
|
|
// restoreMinArgs is the minimum positional argument count of
|
|
// `snapshot restore <snapshot-id> <target-dir> [paths...]`.
|
|
const restoreMinArgs = 2
|
|
|
|
// 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(restoreMinArgs),
|
|
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) > restoreMinArgs {
|
|
opts.Paths = args[restoreMinArgs:]
|
|
}
|
|
|
|
// 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.Options{
|
|
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(_ 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,
|
|
}
|
|
|
|
err := app.Vaultik.Restore(restoreOpts)
|
|
if err != nil {
|
|
if !errors.Is(err, context.Canceled) {
|
|
log.Error("Restore operation failed", "error", err)
|
|
ReportErrorf("Restore failed: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// Shutdown the app when restore completes
|
|
err = app.Shutdowner.Shutdown()
|
|
if err != nil {
|
|
log.Error("Failed to shutdown", "error", err)
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
},
|
|
OnStop: func(_ context.Context) error {
|
|
log.Debug("Stopping restore operation")
|
|
app.Vaultik.Cancel()
|
|
|
|
return nil
|
|
},
|
|
})
|
|
}),
|
|
}
|
|
}
|