package cli import ( "context" "fmt" "os" "github.com/spf13/cobra" "go.uber.org/fx" "sneak.berlin/go/vaultik/internal/log" "sneak.berlin/go/vaultik/internal/vaultik" ) // NewRemoteCommand creates the remote command and subcommands func NewRemoteCommand() *cobra.Command { cmd := &cobra.Command{ Use: "remote", Short: "Remote storage management commands", Long: "Commands for inspecting and managing remote storage", } // Add subcommands cmd.AddCommand(newRemoteInfoCommand()) cmd.AddCommand(newRemoteNukeCommand()) return cmd } // newRemoteNukeCommand creates the 'remote nuke' subcommand. func newRemoteNukeCommand() *cobra.Command { var force bool cmd := &cobra.Command{ Use: "nuke", Short: "Delete ALL snapshot metadata and blobs from the backup destination store", Long: `Removes every snapshot's metadata and every blob from remote storage. After this command completes successfully the bucket prefix is empty and the next backup starts from scratch. This is destructive and irreversible. Requires --force.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { if !force { return fmt.Errorf("remote nuke requires --force (this deletes ALL remote snapshots and blobs)") } 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.NukeRemote(true); err != nil { if err != context.Canceled { log.Error("Remote nuke 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(&force, "force", false, "Required: confirm destruction of ALL remote data") return cmd } // newRemoteInfoCommand creates the 'remote info' subcommand func newRemoteInfoCommand() *cobra.Command { var jsonOutput bool cmd := &cobra.Command{ Use: "info", Short: "Display remote storage information", Long: `Shows detailed information about remote storage, including: - Size of all snapshot metadata (per snapshot and total) - Count and total size of all blobs - Count and size of referenced blobs (from all manifests) - Count and size of orphaned blobs (not referenced by any manifest)`, 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 || jsonOutput, }, 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.RemoteInfo(jsonOutput); err != nil { if err != context.Canceled { if !jsonOutput { log.Error("Failed to get remote info", "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 }