package app import ( "encoding/json" "fmt" "os" "strings" "time" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/fileops" ) type restoreMarker struct { StartedAt time.Time `json:"started_at"` } func writeRestoreMarker(paths artifacts.SessionPaths) error { marker := restoreMarker{StartedAt: nowUTC()} data, err := json.Marshal(marker) if err != nil { return fmt.Errorf("encode recovery marker: %w", err) } if err := fileops.WriteFileAtomic(paths.RestoreMarkerPath, data, fileops.WorkspaceFileMode); err != nil { return fmt.Errorf("write recovery marker: %w", err) } return nil } func clearRestoreMarker(paths artifacts.SessionPaths) error { if strings.TrimSpace(paths.RestoreMarkerPath) == "" { return fmt.Errorf("recovery marker path is required") } if err := os.Remove(paths.RestoreMarkerPath); err != nil && !os.IsNotExist(err) { return fmt.Errorf("remove recovery marker: %w", err) } return nil } func requireCompleteRestore(paths artifacts.SessionPaths) error { if strings.TrimSpace(paths.RestoreMarkerPath) == "" { return fmt.Errorf("restore recovery marker path is required") } info, err := os.Lstat(paths.RestoreMarkerPath) if os.IsNotExist(err) { return nil } if err != nil { return fmt.Errorf("check restore recovery marker: %w", err) } if !info.Mode().IsRegular() { return fmt.Errorf("restore recovery marker is not a regular file; rerun session restore") } data, err := os.ReadFile(paths.RestoreMarkerPath) if err != nil { return fmt.Errorf("read restore recovery marker: %w", err) } var marker restoreMarker if err := json.Unmarshal(data, &marker); err != nil || marker.StartedAt.IsZero() { return fmt.Errorf("restore recovery marker is invalid; rerun session restore") } return fmt.Errorf("restore is incomplete; rerun session restore before running pipeline stages") }