package artifacts import ( "context" "fmt" "os" "strings" "gitea.maximumdirect.net/eric/narratio/internal/adapters/storage" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) func loadCommittedCurrentState( ctx context.Context, store storage.ObjectStore, sessionPrefix string, pointerKey string, validation CurrentStateValidation, ) (*CurrentState, error) { pointerData, err := downloadRemoteObject(ctx, store, pointerKey, "narratio-current-commit-pointer-*.json") if err != nil { return nil, fmt.Errorf("download current commit pointer %q: %w", pointerKey, err) } pointer, err := DecodeCurrentCommitPointer(pointerData) if err != nil { return nil, err } if err := pointer.ValidateForSessionPrefix(sessionPrefix); err != nil { return nil, err } commitData, err := readVerifiedRemoteObject(ctx, store, pointer.CommitKey, pointer.CommitSHA256, pointer.CommitSize, pointer.CommitGeneration, "narratio-remote-commit-*.json") if err != nil { return nil, fmt.Errorf("read selected remote commit %q: %w", pointer.CommitKey, err) } commit, err := DecodeRemoteCommitManifest(commitData) if err != nil { return nil, err } if err := commit.ValidateForSessionPrefix(sessionPrefix); err != nil { return nil, err } if err := validatePointerCommitIdentity(pointer, commit); err != nil { return nil, err } sessionManifest, ok := commit.Artifact(RemoteArtifactTypeSessionManifest) if !ok { return nil, fmt.Errorf("remote commit must declare exactly one session manifest artifact") } manifestData, err := readVerifiedRemoteObject(ctx, store, sessionManifest.DestinationKey, sessionManifest.SHA256, sessionManifest.Size, sessionManifest.Generation, "narratio-remote-session-manifest-*.json") if err != nil { return nil, fmt.Errorf("read committed session manifest %q: %w", sessionManifest.DestinationKey, err) } m, err := decodeCommittedManifest(ctx, manifestData) if err != nil { return nil, err } if err := validateCommitManifestIdentity(commit, m); err != nil { return nil, err } state := &CurrentState{ SessionPrefix: sessionPrefix, CurrentRunIDKey: pointerKey, CurrentPointerKey: pointerKey, CurrentManifestKey: sessionManifest.DestinationKey, RunID: commit.RunID, Manifest: m, Commit: commit, Pointer: pointer, } if err := ValidateCurrentStateIdentity(state, validation); err != nil { return nil, err } return state, nil } func validatePointerCommitIdentity(pointer *CurrentCommitPointer, commit *RemoteCommitManifest) error { if pointer == nil || commit == nil { return fmt.Errorf("current commit pointer and remote commit are required") } if pointer.Campaign != commit.Campaign || pointer.SessionID != commit.SessionID || pointer.RunID != commit.RunID { return fmt.Errorf("current commit pointer identity does not match selected remote commit") } return nil } func validateCommitManifestIdentity(commit *RemoteCommitManifest, m *manifest.Manifest) error { if commit == nil || m == nil { return fmt.Errorf("remote commit and committed session manifest are required") } if commit.Campaign != strings.TrimSpace(m.Campaign) || commit.SessionID != strings.TrimSpace(m.SessionID) || commit.RunID != strings.TrimSpace(m.RunID) { return fmt.Errorf("selected remote commit identity does not match committed session manifest") } return nil } func readVerifiedRemoteObject( ctx context.Context, store storage.ObjectStore, key string, wantSHA256 string, wantSize int64, wantGeneration string, tempPattern string, ) ([]byte, error) { data, err := downloadRemoteObject(ctx, store, key, tempPattern) if err != nil { return nil, err } if int64(len(data)) != wantSize { return nil, fmt.Errorf("size mismatch: got %d, want %d", len(data), wantSize) } if checksum := remoteObjectSHA256(data); checksum != wantSHA256 { return nil, fmt.Errorf("checksum mismatch: got %s, want %s", checksum, wantSHA256) } info, err := remoteObjectInfo(ctx, store, key) if err != nil { return nil, err } if info.Size != wantSize { return nil, fmt.Errorf("storage size mismatch: got %d, want %d", info.Size, wantSize) } if strings.TrimSpace(info.ETag) != wantGeneration { return nil, fmt.Errorf("generation mismatch: got %q, want %q", info.ETag, wantGeneration) } return data, nil } func downloadRemoteObject(ctx context.Context, store storage.ObjectStore, key, tempPattern string) ([]byte, error) { localPath, err := storage.DownloadObjectToTemp(ctx, store, key, tempPattern) if err != nil { return nil, err } defer func() { _ = os.Remove(localPath) }() data, err := os.ReadFile(localPath) if err != nil { return nil, fmt.Errorf("read downloaded object %q: %w", key, err) } return data, nil } func remoteObjectInfo(ctx context.Context, store storage.ObjectStore, key string) (storage.ObjectInfo, error) { objects, err := store.List(ctx, key) if err != nil { return storage.ObjectInfo{}, fmt.Errorf("list remote object %q: %w", key, err) } var found *storage.ObjectInfo for _, object := range objects { if object.Key != key { continue } if found != nil { return storage.ObjectInfo{}, fmt.Errorf("remote object %q is ambiguous", key) } copy := object found = © } if found == nil { return storage.ObjectInfo{}, fmt.Errorf("remote object %q is missing", key) } return *found, nil } func decodeCommittedManifest(ctx context.Context, data []byte) (*manifest.Manifest, error) { file, err := os.CreateTemp("", "narratio-committed-session-manifest-*.json") if err != nil { return nil, fmt.Errorf("create committed manifest file: %w", err) } path := file.Name() defer func() { _ = os.Remove(path) }() if _, err := file.Write(data); err != nil { _ = file.Close() return nil, fmt.Errorf("write committed manifest file: %w", err) } if err := file.Close(); err != nil { return nil, fmt.Errorf("close committed manifest file: %w", err) } m, err := (&manifest.LocalStore{}).Load(ctx, path) if err != nil { return nil, fmt.Errorf("committed session manifest decode failed: %w", err) } return m, nil }