package cli import ( "context" "fmt" "os" "git.eeqj.de/sneak/vaultik/internal/backup" "git.eeqj.de/sneak/vaultik/internal/config" "git.eeqj.de/sneak/vaultik/internal/database" "git.eeqj.de/sneak/vaultik/internal/globals" "git.eeqj.de/sneak/vaultik/internal/log" "git.eeqj.de/sneak/vaultik/internal/s3" "github.com/spf13/cobra" "go.uber.org/fx" ) // PruneOptions contains options for the prune command type PruneOptions struct { DryRun bool } // PruneApp contains all dependencies needed for pruning type PruneApp struct { Globals *globals.Globals Config *config.Config Repositories *database.Repositories S3Client *s3.Client DB *database.DB Shutdowner fx.Shutdowner } // NewPruneCommand creates the prune command func NewPruneCommand() *cobra.Command { opts := &PruneOptions{} cmd := &cobra.Command{ Use: "prune", Short: "Remove unreferenced blobs", Long: `Delete blobs that are no longer referenced by any snapshot. This command will: 1. Download all snapshot metadata from S3 2. Build a list of all referenced blobs 3. List all blobs in S3 4. Delete any blobs not referenced by any snapshot 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 { // Check for private key if os.Getenv("VAULTIK_PRIVATE_KEY") == "" { return fmt.Errorf("VAULTIK_PRIVATE_KEY environment variable must be set") } // 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, }, Modules: []fx.Option{ backup.Module, s3.Module, fx.Provide(fx.Annotate( func(g *globals.Globals, cfg *config.Config, repos *database.Repositories, s3Client *s3.Client, db *database.DB, shutdowner fx.Shutdowner) *PruneApp { return &PruneApp{ Globals: g, Config: cfg, Repositories: repos, S3Client: s3Client, DB: db, Shutdowner: shutdowner, } }, )), }, Invokes: []fx.Option{ fx.Invoke(func(app *PruneApp, lc fx.Lifecycle) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { // Start the prune operation in a goroutine go func() { // Run the prune operation if err := app.runPrune(ctx, opts); err != nil { if err != context.Canceled { log.Error("Prune operation failed", "error", err) } } // Shutdown the app when prune 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 prune operation") return nil }, }) }), }, }) }, } cmd.Flags().BoolVar(&opts.DryRun, "dry-run", false, "Show what would be deleted without actually deleting") return cmd } // runPrune executes the prune operation func (app *PruneApp) runPrune(ctx context.Context, opts *PruneOptions) error { log.Info("Starting prune operation", "bucket", app.Config.S3.Bucket, "prefix", app.Config.S3.Prefix, "dry_run", opts.DryRun, ) // TODO: Implement the actual prune logic // 1. Download all snapshot metadata // 2. Build set of referenced blobs // 3. List all blobs in S3 // 4. Delete unreferenced blobs fmt.Printf("Pruning bucket %s with prefix %s\n", app.Config.S3.Bucket, app.Config.S3.Prefix) if opts.DryRun { fmt.Println("Running in dry-run mode") } // For now, just show we're using the config properly log.Info("Prune operation completed successfully") return nil }