package cli import ( "context" "fmt" "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" "git.eeqj.de/sneak/vaultik/internal/snapshot" "github.com/spf13/cobra" "go.uber.org/fx" ) // FetchOptions contains options for the fetch command type FetchOptions struct { } // FetchApp contains all dependencies needed for fetch type FetchApp struct { Globals *globals.Globals Config *config.Config Repositories *database.Repositories S3Client *s3.Client DB *database.DB Shutdowner fx.Shutdowner } // NewFetchCommand creates the fetch command func NewFetchCommand() *cobra.Command { opts := &FetchOptions{} cmd := &cobra.Command{ Use: "fetch ", Short: "Extract single file from backup", Long: `Download and decrypt a single file from a backup snapshot. This command extracts a specific file from the snapshot and saves it to the target path. The age_secret_key must be configured in the config file for decryption.`, Args: cobra.ExactArgs(3), RunE: func(cmd *cobra.Command, args []string) error { snapshotID := args[0] filePath := args[1] targetPath := 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, }, Modules: []fx.Option{ snapshot.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) *FetchApp { return &FetchApp{ Globals: g, Config: cfg, Repositories: repos, S3Client: s3Client, DB: db, Shutdowner: shutdowner, } }, )), }, Invokes: []fx.Option{ fx.Invoke(func(app *FetchApp, lc fx.Lifecycle) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { // Start the fetch operation in a goroutine go func() { // Run the fetch operation if err := app.runFetch(ctx, snapshotID, filePath, targetPath, opts); err != nil { if err != context.Canceled { log.Error("Fetch operation failed", "error", err) } } // Shutdown the app when fetch 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 fetch operation") return nil }, }) }), }, }) }, } return cmd } // runFetch executes the fetch operation func (app *FetchApp) runFetch(ctx context.Context, snapshotID, filePath, targetPath string, opts *FetchOptions) error { // Check for age_secret_key if app.Config.AgeSecretKey == "" { return fmt.Errorf("age_secret_key missing from config - required for fetch") } log.Info("Starting fetch operation", "snapshot_id", snapshotID, "file_path", filePath, "target_path", targetPath, "bucket", app.Config.S3.Bucket, "prefix", app.Config.S3.Prefix, ) // TODO: Implement fetch logic // 1. Download and decrypt database from S3 // 2. Find the file metadata and chunk list // 3. Download and decrypt only the necessary blobs // 4. Reconstruct the file from chunks // 5. Write file to target path with proper metadata fmt.Printf("Fetching %s from snapshot %s to %s\n", filePath, snapshotID, targetPath) fmt.Println("TODO: Implement fetch logic") return nil }