package cli import ( "context" "fmt" "os" "git.eeqj.de/sneak/vaultik/internal/log" "git.eeqj.de/sneak/vaultik/internal/vaultik" "github.com/spf13/cobra" "go.uber.org/fx" ) // NewSnapshotCommand creates the snapshot command and subcommands func NewSnapshotCommand() *cobra.Command { cmd := &cobra.Command{ Use: "snapshot", Short: "Snapshot management commands", Long: "Commands for creating, listing, and managing snapshots", } // Add subcommands cmd.AddCommand(newSnapshotCreateCommand()) cmd.AddCommand(newSnapshotListCommand()) cmd.AddCommand(newSnapshotPurgeCommand()) cmd.AddCommand(newSnapshotVerifyCommand()) return cmd } // newSnapshotCreateCommand creates the 'snapshot create' subcommand func newSnapshotCreateCommand() *cobra.Command { opts := &vaultik.SnapshotCreateOptions{} cmd := &cobra.Command{ Use: "create", Short: "Create a new snapshot", Long: `Creates a new snapshot of the configured directories. Config is located at /etc/vaultik/config.yml by default, but can be overridden by specifying a path using --config or by setting VAULTIK_CONFIG to a path.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { // Use unified config resolution configPath, err := ResolveConfigPath() if err != nil { return err } // Use the backup functionality from cli package rootFlags := GetRootFlags() return RunWithApp(cmd.Context(), AppOptions{ ConfigPath: configPath, LogOptions: log.LogOptions{ Verbose: rootFlags.Verbose, Debug: rootFlags.Debug, Cron: opts.Cron, }, Modules: []fx.Option{}, Invokes: []fx.Option{ fx.Invoke(func(v *vaultik.Vaultik, lc fx.Lifecycle) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { // Start the snapshot creation in a goroutine go func() { // Run the snapshot creation if err := v.CreateSnapshot(opts); err != nil { if err != context.Canceled { log.Error("Snapshot creation failed", "error", err) } } // Shutdown the app when snapshot completes if err := v.Shutdowner.Shutdown(); err != nil { log.Error("Failed to shutdown", "error", err) } }() return nil }, OnStop: func(ctx context.Context) error { log.Debug("Stopping snapshot creation") // Cancel the Vaultik context v.Cancel() return nil }, }) }), }, }) }, } cmd.Flags().BoolVar(&opts.Daemon, "daemon", false, "Run in daemon mode with inotify monitoring") cmd.Flags().BoolVar(&opts.Cron, "cron", false, "Run in cron mode (silent unless error)") cmd.Flags().BoolVar(&opts.Prune, "prune", false, "Delete all previous snapshots and unreferenced blobs after backup") return cmd } // newSnapshotListCommand creates the 'snapshot list' subcommand func newSnapshotListCommand() *cobra.Command { var jsonOutput bool cmd := &cobra.Command{ Use: "list", Short: "List all snapshots", Long: "Lists all snapshots with their ID, timestamp, and compressed size", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { // Use unified config resolution configPath, err := ResolveConfigPath() if err != nil { return err } rootFlags := GetRootFlags() return RunWithApp(cmd.Context(), AppOptions{ ConfigPath: configPath, LogOptions: log.LogOptions{ Verbose: rootFlags.Verbose, Debug: rootFlags.Debug, }, Modules: []fx.Option{}, Invokes: []fx.Option{ fx.Invoke(func(v *vaultik.Vaultik, lc fx.Lifecycle) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { go func() { if err := v.ListSnapshots(jsonOutput); err != nil { if err != context.Canceled { log.Error("Failed to list snapshots", "error", err) os.Exit(1) } } if err := v.Shutdowner.Shutdown(); err != nil { log.Error("Failed to shutdown", "error", err) } }() return nil }, OnStop: func(ctx context.Context) error { v.Cancel() return nil }, }) }), }, }) }, } cmd.Flags().BoolVar(&jsonOutput, "json", false, "Output in JSON format") return cmd } // newSnapshotPurgeCommand creates the 'snapshot purge' subcommand func newSnapshotPurgeCommand() *cobra.Command { var keepLatest bool var olderThan string var force bool cmd := &cobra.Command{ Use: "purge", Short: "Purge old snapshots", Long: "Removes snapshots based on age or count criteria", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { // Validate flags if !keepLatest && olderThan == "" { return fmt.Errorf("must specify either --keep-latest or --older-than") } if keepLatest && olderThan != "" { return fmt.Errorf("cannot specify both --keep-latest and --older-than") } // Use unified config resolution configPath, err := ResolveConfigPath() if err != nil { return err } rootFlags := GetRootFlags() return RunWithApp(cmd.Context(), AppOptions{ ConfigPath: configPath, LogOptions: log.LogOptions{ Verbose: rootFlags.Verbose, Debug: rootFlags.Debug, }, Modules: []fx.Option{}, Invokes: []fx.Option{ fx.Invoke(func(v *vaultik.Vaultik, lc fx.Lifecycle) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { go func() { if err := v.PurgeSnapshots(keepLatest, olderThan, force); err != nil { if err != context.Canceled { log.Error("Failed to purge snapshots", "error", err) os.Exit(1) } } if err := v.Shutdowner.Shutdown(); err != nil { log.Error("Failed to shutdown", "error", err) } }() return nil }, OnStop: func(ctx context.Context) error { v.Cancel() return nil }, }) }), }, }) }, } cmd.Flags().BoolVar(&keepLatest, "keep-latest", false, "Keep only the latest snapshot") cmd.Flags().StringVar(&olderThan, "older-than", "", "Remove snapshots older than duration (e.g., 30d, 6m, 1y)") cmd.Flags().BoolVar(&force, "force", false, "Skip confirmation prompt") return cmd } // newSnapshotVerifyCommand creates the 'snapshot verify' subcommand func newSnapshotVerifyCommand() *cobra.Command { var deep bool cmd := &cobra.Command{ Use: "verify ", Short: "Verify snapshot integrity", Long: "Verifies that all blobs referenced in a snapshot exist", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { snapshotID := args[0] // Use unified config resolution configPath, err := ResolveConfigPath() if err != nil { return err } rootFlags := GetRootFlags() return RunWithApp(cmd.Context(), AppOptions{ ConfigPath: configPath, LogOptions: log.LogOptions{ Verbose: rootFlags.Verbose, Debug: rootFlags.Debug, }, Modules: []fx.Option{}, Invokes: []fx.Option{ fx.Invoke(func(v *vaultik.Vaultik, lc fx.Lifecycle) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { go func() { if err := v.VerifySnapshot(snapshotID, deep); err != nil { if err != context.Canceled { log.Error("Verification failed", "error", err) os.Exit(1) } } if err := v.Shutdowner.Shutdown(); err != nil { log.Error("Failed to shutdown", "error", err) } }() return nil }, OnStop: func(ctx context.Context) error { v.Cancel() return nil }, }) }), }, }) }, } cmd.Flags().BoolVar(&deep, "deep", false, "Download and verify blob hashes") return cmd }