Skip to content

File Service API (document-service alias)

Baseline Endpoints

  • POST /v1/media-assets
  • GET /v1/media-assets
  • GET /v1/media-assets/{id}
  • GET /v1/media-assets/{id}/content
  • POST /v1/storage/presigned-upload
  • POST /v1/storage/media-assets
  • GET /v1/storage/media-assets/{id}
  • GET /v1/storage/media-assets/{id}/content
  • GET /v1/storage/objects/content?storageKey=...
  • DELETE /v1/storage/objects?storageKey=...
  • POST /v1/import-sources (Gateway candidate upload only)
  • GET /v1/internal/import-sources/{id}/content (trusted import-service only)
  • POST /v1/internal/media-assets (trusted producer only)
  • GET /v1/documents/search-projections
  • GET /v1/admin/documents/review-summary
  • POST /v1/admin/documents/{id}/archive
  • GET /healthz
  • GET /readyz

SVC-011 Storage Media Browser Proof

The bounded SVC-011-storage-media-browser-proof contract is recorded in Storage Media Browser Proof. Document-service tests cover upload metadata, object content, media variants, organization scoping, and archive lifecycle. The storage parity self-test and native route tables remain non-default; real frontend/browser upload/read evidence and rollback proof are still required before public promotion.

Current Behavior

The current implementation owns media asset metadata for the new Go platform and signs direct browser uploads to S3-compatible storage.

POST /v1/media-assets accepts JSON metadata for an object that already exists or will exist in MinIO/S3-compatible storage:

  • object_key is required.
  • content_type is required and must parse as a media type.
  • size_bytes must be greater than or equal to 0.
  • bucket defaults to hoctapaz-local.
  • organization_id and owner_user_id can be supplied by JSON or the X-Organization-Id and X-User-Id headers.

The service uses hoctapaz_document_db.media_assets when DATABASE_URL is reachable. It falls back to an in-memory store for local skeleton runs.

GET /v1/documents/search-projections is the native owner projection for search-service backfills. It reads canonical teaching document rows from hoctapaz_document_db.teaching_documents, returns copied DOCUMENT search documents, and keeps search-service on the document-service API boundary instead of reading the document database directly.

Projection query parameters:

  • organizationId, defaulting from X-Organization-Id when omitted.
  • allOrgs=1 or allOrgs=true clears organization scope for internal rebuilds.
  • status, visibility, subjectId, gradeLevel, q, page, limit.
  • includeArchived=1 includes archived rows when status is omitted.

Search Projection Events

With PostgreSQL, every teaching_documents insert/update/delete also commits a document-service-owned revision and redacted search.projection.changed.v1 outbox envelope. The source version is monotonic per document and the stored envelope is reused unchanged across retries. Archiving creates a DELETE tombstone, matching the default pull projection that excludes ARCHIVED rows.

Set DOCUMENT_SEARCH_PROJECTION_EVENT_TRANSPORT=nats explicitly to enable the owner publisher. It leases due rows, publishes to JetStream SEARCH_PROJECTIONS / search.projection.changed.v1, and records retry or terminal-dead state in the owner database. The default is disabled. Envelopes contain copied title/summary/taxonomy metadata only; storage keys, file URLs, and arbitrary source metadata are excluded.

This is an internal owner-to-Search transport. It neither adds a Gateway route nor changes public document, marketplace, purchase, quota, or entitlement behavior. Search's pull rebuild remains the reconciliation and rollback path.

GET /v1/admin/documents/review-summary is the document-service owner API for admin dashboard document totals and pending-review counts. It returns sourceService=document-service, total, status counts for DRAFT, PENDING_REVIEW, APPROVED, REJECTED, ARCHIVED, and generatedAt. It supports organizationId, X-Organization-Id, and allOrgs=1, reads only document-service-owned teaching_documents, and does not own purchase, entitlement, quota, or wallet decisions.

POST /v1/storage/presigned-upload mirrors the legacy request contract:

  • Request JSON: fileName, mimeType, size, optional purpose.
  • X-Organization-Id supplies the tenant scope while auth/tenant middleware is still pending.
  • Allowed MIME types and upload limits follow legacy StorageService.validateUpload: default max 50MB, classroom-video max 500MB.
  • Response JSON: bucket, storageKey, uploadUrl, publicUrl, expiresIn.
  • Storage key format follows legacy: {organizationId}/{purpose}/{timestamp}-{safeFileName}.

POST /v1/storage/media-assets accepts the legacy frontend payload (fileName, storageKey, mimeType, size, optional checksum, purpose, url) and returns camelCase media asset fields for compatibility.

POST /v1/internal/generated-artifacts is an internal raw-binary boundary for trusted producer services. It requires a matching X-Document-Artifact-Token, organization and owner headers, artifact file/purpose/source headers, and a supported binary Content-Type. document-service creates the storage key under {organizationId}/generated-artifacts/..., writes object bytes, and creates the matching media_assets metadata atomically from the producer's point of view. exam-service durable DOCX print export is the first producer. This is not a gateway or browser endpoint; frontends must wait for a separate, authenticated BFF/gateway artifact-read contract.

POST /v1/internal/media-assets is the equivalent trusted raw-binary boundary for extracted import media. It requires X-Document-Artifact-Token, organization and owner headers, X-Media-File-Name, X-Media-Purpose, and X-Media-Source-Service. document-service creates the object key, writes the image bytes, and creates media_assets metadata in one owner-local operation. docx-import-service uses it when DOCUMENT_ARTIFACT_SERVICE_TOKEN is configured, so container workers do not attempt to use browser-facing localhost:9902 presigned URLs. Production has no shared-token or legacy-presigned fallback.

GET /v1/internal/media-assets/{id}/content is the corresponding trusted read boundary for a producer that must package an already-owned image into a generated document. It requires X-Document-Artifact-Token and a non-empty X-Organization-Id exactly matching the media asset before object bytes are opened. It streams the stored bytes with Cache-Control: private, max-age=0; it does not accept an object key, does not expose a browser URL, and does not apply image variants. exam-service uses this route only while building a native DOCX from immutable snapshot mediaAssetId values.

POST /v1/import-sources is a separate owner-scoped source boundary for the non-default OCR, MathType, and Teacher DOCX Fast Gateway workflows. It accepts multipart file plus sourceKind (ocr-document, mathtype-equation, or docx-fast), derives organization and actor from Gateway headers, and returns only sourceAssetId, file metadata, and checksum. It does not return an object key, signed URL, or public URL. OCR accepts PDF/PNG/JPEG/WebP; MathType and DOCX Fast accept DOCX. In production the Gateway also injects X-Internal-Service: api-gateway and X-Document-Storage-Token; document-service verifies DOCUMENT_STORAGE_GATEWAY_TOKEN.

GET /v1/internal/import-sources/{id}/content is not a browser route. It requires X-Internal-Service-Token plus exact organization, actor, purpose, and source-kind match before docx-import-service can read the bytes for create or reprocess. The generic media read routes remain unchanged for existing image rendering compatibility.

GET /v1/storage/media-assets/{id}/content streams object bytes from the configured object store and sets Content-Type, Content-Length, and Cache-Control.

For image content, variant=formula trims white background and returns PNG. variant=trim trims white background, adds 8px white padding, and returns PNG. If the image cannot be decoded by the Go stdlib image decoders, the service falls back to the original object bytes and content type.

GET /v1/storage/objects/content?storageKey=... is a non-production compatibility adapter only. Production does not register this route and DOCX Fast rejects storageKey-only reads. Local compatibility reads require X-Organization-Id; the storage key must remain inside that organization prefix.

DELETE /v1/storage/objects?storageKey=... deletes object bytes through the document-service storage boundary. It requires X-Organization-Id, rejects storage keys outside that organization prefix, and intentionally does not accept the internal object-read token for destructive operations.

POST /v1/admin/documents/{id}/archive is the bounded native teaching document lifecycle mutation. It requires organization scope plus a gateway actor (X-User-Id and X-User-Role), allows the owner or ADMIN/STAFF actors in the same organization, sets status=ARCHIVED, and clears published_at and featured_at. Archived documents remain visible to admin summary and includeArchived=1 projections, but drop out of default search projections. Purchase, entitlement, wallet, quota, share, and public marketplace decisions remain outside this route.

Runtime storage selection:

  • If MINIO_ENDPOINT or S3_ENDPOINT is set, the service uses the S3-compatible adapter.
  • If no S3/MinIO endpoint is configured, tests and local skeleton runs use an in-memory object store.
  • Docker Compose config signs upload URLs against http://localhost:9902 and reads internally from http://minio:9000.
  • Trusted internal producers use /v1/internal/media-assets; they do not need a presigned URL and therefore keep the browser upload hostname separate from the container storage hostname.
  • scripts/test/storage-e2e.sh verifies signed PUT plus content read against MinIO/S3-compatible storage.
  • scripts/test/storage-media-parity-smoke.sh compares a real legacy media asset content response with native document-service content in read-only mode. It supports direct native asset-id reads, direct storage-key reads for pre-backfill object parity, and gateway native-route reads.

Legacy Evidence

  • Legacy import/editor media parity relies on /storage/presigned-upload and /storage/media-assets compatibility surfaces.
  • DOCX Fast temp drafts and imported images use shared object storage paths such as system/docx-fast-temp-drafts/<token>.json.
  • Real DOCX import validation expects extracted images to map back into MediaAsset instead of being silently dropped.
  • Legacy teaching documents use review states DRAFT, PENDING_REVIEW, APPROVED, REJECTED, and ARCHIVED; the native projection copies those states into search metadata without moving review ownership to search-service.
  • Legacy document archive sets teaching documents to ARCHIVED and clears publish/feature state; the native archive route preserves that bounded lifecycle behavior with explicit organization and actor guards.
  • Legacy uploaded-file deletion removes object bytes through storage service helpers; the native object delete route keeps this lifecycle action inside document-service instead of letting callers reach object storage directly.
  • Legacy admin overview displays total and pending teaching document counts; native admin/BFF dashboards must read those counts from document-service /v1/admin/documents/review-summary, not admin-service storage.

Pending Compatibility Work

  • HOCTAPAZ v2 Gateway rehearsal can use the strict deploy/gateway/routes.storage-native-example.json; it contains no legacy fallback and routes every browser write through the quota-accounted multipart control plane.
  • Multipart UploadPart URLs bind the exact expected part byte count through a signed Content-Length. Object Edge rejects an UploadPart presign that does not sign that header, while the browser client verifies the returned part offset and size before slicing the Blob.
  • Keep DOCX extracted image materialization on /v1/internal/media-assets and keep any legacy presigned compatibility surface outside the v2 production route table.
  • Add full teaching document write/review/share/purchase APIs in document-service before moving public /documents routes off legacy.
  • Add parity fixtures for legacy sharp outputs on real imported formula/image assets.
  • Add hard teaching document purge/share/public marketplace lifecycle only after purchase, entitlement, billing, wallet, and usage boundaries are proven.
  • Execute scripts/test/storage-media-parity-smoke.sh against representative imported image/formula assets before default route promotion.

Go-platform documentation is generated from repository Markdown.