package artifacts import ( "context" "errors" "fmt" "os" "strings" "gitea.maximumdirect.net/eric/narratio/internal/adapters/storage" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) var ( ErrCurrentRunPointerMissing = errors.New("current run pointer missing") ErrCurrentManifestMissing = errors.New("current manifest missing") ) type CurrentRunPointerMissingError struct { Key string } func (e *CurrentRunPointerMissingError) Error() string { return fmt.Sprintf("%s: %q", ErrCurrentRunPointerMissing, e.Key) } func (e *CurrentRunPointerMissingError) Unwrap() error { return ErrCurrentRunPointerMissing } type CurrentManifestMissingError struct { Key string } func (e *CurrentManifestMissingError) Error() string { return fmt.Sprintf("%s: %q", ErrCurrentManifestMissing, e.Key) } func (e *CurrentManifestMissingError) Unwrap() error { return ErrCurrentManifestMissing } type CurrentState struct { SessionPrefix string CurrentRunIDKey string CurrentManifestKey string RunID string Manifest *manifest.Manifest } type CurrentStateValidation struct { ExpectedCampaign string ExpectedSessionID string ExpectedRunID string ValidateRunID bool } func LoadCurrentRunPointer(ctx context.Context, store storage.ObjectStore, currentRunIDKey string) (string, error) { if store == nil { return "", fmt.Errorf("object store is required") } key := strings.TrimSpace(currentRunIDKey) if key == "" { return "", fmt.Errorf("current run pointer key is required") } exists, err := store.Exists(ctx, key) if err != nil { return "", fmt.Errorf("check current run pointer %q: %w", key, err) } if !exists { return "", &CurrentRunPointerMissingError{Key: key} } localPath, err := storage.DownloadObjectToTemp(ctx, store, key, "narratio-current-run-id-*.txt") if err != nil { return "", fmt.Errorf("download current run pointer %q: %w", key, err) } defer func() { _ = os.Remove(localPath) }() data, err := os.ReadFile(localPath) if err != nil { return "", fmt.Errorf("read downloaded current run pointer %q: %w", key, err) } runID := strings.TrimSpace(string(data)) if runID == "" { return "", fmt.Errorf("current run pointer %q is empty", key) } return runID, nil } func LoadCurrentManifest(ctx context.Context, store storage.ObjectStore, currentManifestKey string) (*manifest.Manifest, error) { if store == nil { return nil, fmt.Errorf("object store is required") } key := strings.TrimSpace(currentManifestKey) if key == "" { return nil, fmt.Errorf("current manifest key is required") } exists, err := store.Exists(ctx, key) if err != nil { return nil, fmt.Errorf("check current manifest %q: %w", key, err) } if !exists { return nil, &CurrentManifestMissingError{Key: key} } localPath, err := storage.DownloadObjectToTemp(ctx, store, key, "narratio-current-manifest-*.json") if err != nil { return nil, fmt.Errorf("download current manifest %q: %w", key, err) } defer func() { _ = os.Remove(localPath) }() manifestStore := &manifest.LocalStore{} m, err := manifestStore.Load(ctx, localPath) if err != nil { return nil, fmt.Errorf("current manifest decode failed: %w", err) } return m, nil } func LoadCurrentState( ctx context.Context, store storage.ObjectStore, sessionPrefix string, validation CurrentStateValidation, ) (*CurrentState, error) { prefix := strings.TrimSpace(sessionPrefix) if prefix == "" { return nil, fmt.Errorf("session prefix is required") } currentManifestKey, currentRunIDKey := ResolveCurrentStateKeys(prefix) runID, err := LoadCurrentRunPointer(ctx, store, currentRunIDKey) if err != nil { return nil, err } m, err := LoadCurrentManifest(ctx, store, currentManifestKey) if err != nil { return nil, err } state := &CurrentState{ SessionPrefix: prefix, CurrentRunIDKey: currentRunIDKey, CurrentManifestKey: currentManifestKey, RunID: runID, Manifest: m, } if err := ValidateCurrentStateIdentity(state, validation); err != nil { return nil, err } return state, nil } func ValidateCurrentStateIdentity(state *CurrentState, validation CurrentStateValidation) error { if state == nil || state.Manifest == nil { return fmt.Errorf("current state with manifest is required") } expectedSessionID := strings.TrimSpace(validation.ExpectedSessionID) expectedCampaign := strings.TrimSpace(validation.ExpectedCampaign) expectedRunID := strings.TrimSpace(validation.ExpectedRunID) manifestSessionID := strings.TrimSpace(state.Manifest.SessionID) manifestCampaign := strings.TrimSpace(state.Manifest.Campaign) manifestRunID := strings.TrimSpace(state.Manifest.RunID) if expectedSessionID != "" && manifestSessionID != expectedSessionID { return fmt.Errorf( "current manifest session_id %q does not match expected session_id %q", manifestSessionID, expectedSessionID, ) } if expectedCampaign != "" { if manifestCampaign == "" { return fmt.Errorf("current manifest campaign is required") } if manifestCampaign != expectedCampaign { return fmt.Errorf( "current manifest campaign %q does not match expected campaign %q", manifestCampaign, expectedCampaign, ) } } if expectedRunID == "" && validation.ValidateRunID { expectedRunID = strings.TrimSpace(state.RunID) } if expectedRunID != "" { if manifestRunID == "" { return fmt.Errorf("current manifest run_id is required") } if manifestRunID != expectedRunID { return fmt.Errorf( "current run pointer %q references run %q but current manifest run_id is %q", state.CurrentRunIDKey, expectedRunID, manifestRunID, ) } } return nil }