package app import ( "context" "errors" "flag" "fmt" "io" "gitea.maximumdirect.net/eric/narratio/internal/adapters/storage" "gitea.maximumdirect.net/eric/narratio/internal/config" ) var newObjectStoreFromConfigFn = storage.NewObjectStoreFromConfig var discoverRemoteCurrentStateFn = discoverRemoteCurrentState var buildRestorePlanFn = buildRestorePlan // Restore validates restore CLI/config inputs and storage preflight for future restore phases. func Restore(ctx context.Context, args []string, out io.Writer) error { fs := flag.NewFlagSet("restore", flag.ContinueOnError) fs.SetOutput(out) var pipelinePath string var sessionPath string var sessionID string var dryRun bool var force bool var includeAudio bool fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)") fs.StringVar(&sessionPath, "session", "", "path to session.yml") fs.StringVar(&sessionID, "session-id", "", "session identifier for session.yml templates") fs.BoolVar(&dryRun, "dry-run", false, "plan restore actions without writing local files") fs.BoolVar(&force, "force", false, "overwrite local conflicts with remote state") fs.BoolVar(&includeAudio, "include-audio", false, "include archived session-level audio objects") fs.Usage = func() { _, _ = fmt.Fprintln(out, "Usage: narratio restore [--config ] [--session ] [--session-id ] [--dry-run] [--force] [--include-audio]") _, _ = fmt.Fprintln(out) _, _ = fmt.Fprintln(out, "Flags:") fs.PrintDefaults() } if err := fs.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { return nil } return fmt.Errorf("restore: invalid flags: %w", err) } if fs.NArg() != 0 { return fmt.Errorf("restore: unexpected positional arguments") } resolvedPipelinePath, err := resolvePipelineConfigPath(pipelinePath) if err != nil { return fmt.Errorf("restore: %w", err) } resolvedSessionPath, err := resolveSessionConfigPath(sessionPath) if err != nil { return fmt.Errorf("restore: %w", err) } cfg, err := config.LoadWithSessionOptions(resolvedPipelinePath, resolvedSessionPath, config.SessionLoadOptions{ SessionID: sessionID, }) if err != nil { return fmt.Errorf("restore: %w", err) } if err := config.Validate(cfg); err != nil { return fmt.Errorf("restore: %w", err) } objectStore, err := newObjectStoreFromConfigFn(ctx, cfg) if err != nil { return fmt.Errorf("restore: %w", err) } current, err := discoverRemoteCurrentStateFn(ctx, cfg, objectStore) if err != nil { return fmt.Errorf("restore: %w", err) } plan, err := buildRestorePlanFn(ctx, cfg, current, objectStore, RestorePlanOptions{ IncludeAudio: includeAudio, Force: force, DryRun: dryRun, }) if err != nil { return fmt.Errorf("restore: %w", err) } if err := writeRestorePlan(out, current, plan, RestorePlanOptions{ IncludeAudio: includeAudio, Force: force, DryRun: dryRun, }); err != nil { return fmt.Errorf("restore: write plan output: %w", err) } if dryRun { return nil } if plan.ConflictCount > 0 && !force { return fmt.Errorf( "restore: plan has %d conflicting path(s); rerun with --force or resolve local conflicts", plan.ConflictCount, ) } return fmt.Errorf( "restore: planned remote restore for %s/%s (run %s); not yet implemented (phase 4: restore execution)", current.Campaign, current.SessionID, current.RunID, ) }