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()) cmd.AddCommand(newSnapshotRemoveCommand()) cmd.AddCommand(newSnapshotPruneCommand()) return cmd } // newSnapshotCreateCommand creates the 'snapshot create' subcommand func newSnapshotCreateCommand() *cobra.Command { opts := &vaultik.SnapshotCreateOptions{} cmd := &cobra.Command{ Use: "create [snapshot-names...]", Short: "Create new snapshots", Long: `Creates new snapshots of the configured directories. If snapshot names are provided, only those snapshots are created. If no names are provided, all configured snapshots are created. 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.ArbitraryArgs, RunE: func(cmd *cobra.Command, args []string) error { // Pass snapshot names from args opts.Snapshots = args // 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, Quiet: rootFlags.Quiet, }, 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") cmd.Flags().BoolVar(&opts.SkipErrors, "skip-errors", false, "Skip file read errors (log them loudly but continue)") 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, Quiet: rootFlags.Quiet, }, 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, Quiet: rootFlags.Quiet, }, 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 { opts := &vaultik.VerifyOptions{} 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, Quiet: rootFlags.Quiet || opts.JSON, }, 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.VerifySnapshotWithOptions(snapshotID, opts); err != nil { if err != context.Canceled { if !opts.JSON { 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(&opts.Deep, "deep", false, "Download and verify blob hashes") cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output verification results as JSON") return cmd } // newSnapshotRemoveCommand creates the 'snapshot remove' subcommand func newSnapshotRemoveCommand() *cobra.Command { opts := &vaultik.RemoveOptions{} cmd := &cobra.Command{ Use: "remove ", Aliases: []string{"rm"}, Short: "Remove a snapshot and its orphaned blobs", Long: `Removes a snapshot and any blobs that are no longer referenced by other snapshots. This command downloads manifests from all other snapshots to determine which blobs are still in use, then deletes any blobs that would become orphaned.`, 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, Quiet: rootFlags.Quiet || opts.JSON, }, 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.RemoveSnapshot(snapshotID, opts); err != nil { if err != context.Canceled { if !opts.JSON { log.Error("Failed to remove snapshot", "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().BoolVarP(&opts.Force, "force", "f", false, "Skip confirmation prompt") cmd.Flags().BoolVar(&opts.DryRun, "dry-run", false, "Show what would be deleted without deleting") cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output deletion stats as JSON") return cmd } // newSnapshotPruneCommand creates the 'snapshot prune' subcommand func newSnapshotPruneCommand() *cobra.Command { cmd := &cobra.Command{ Use: "prune", Short: "Remove orphaned data from local database", Long: `Removes orphaned files, chunks, and blobs from the local database. This cleans up data that is no longer referenced by any snapshot, which can accumulate from incomplete backups or deleted snapshots.`, 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, Quiet: rootFlags.Quiet, }, 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.PruneDatabase(); err != nil { if err != context.Canceled { log.Error("Failed to prune database", "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 }, }) }), }, }) }, } return cmd }