package cli import ( "context" "git.eeqj.de/sneak/vaultik/internal/config" "git.eeqj.de/sneak/vaultik/internal/globals" "git.eeqj.de/sneak/vaultik/internal/log" "git.eeqj.de/sneak/vaultik/internal/storage" "git.eeqj.de/sneak/vaultik/internal/vaultik" "github.com/spf13/cobra" "go.uber.org/fx" ) // 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 } // NewRestoreCommand creates the restore command func NewRestoreCommand() *cobra.Command { opts := &RestoreOptions{} cmd := &cobra.Command{ Use: "restore [paths...]", Short: "Restore files from backup", 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 restore myhost_docs_2025-01-01T12:00:00Z /restore # Restore specific file vaultik restore myhost_docs_2025-01-01T12:00:00Z /restore /home/user/important.txt # Restore specific directory vaultik restore myhost_docs_2025-01-01T12:00:00Z /restore /home/user/documents/ # Restore and verify all files vaultik restore --verify myhost_docs_2025-01-01T12:00:00Z /restore`, Args: cobra.MinimumNArgs(2), RunE: func(cmd *cobra.Command, args []string) 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: []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, } }, )), }, Invokes: []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, } if err := app.Vaultik.Restore(restoreOpts); err != nil { if err != context.Canceled { log.Error("Restore operation failed", "error", err) } } // 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 }, }) }), }, }) }, } cmd.Flags().BoolVar(&opts.Verify, "verify", false, "Verify restored files by checking chunk hashes") return cmd }