Appearance
File Service API (document-service alias)
Baseline Endpoints
POST /v1/media-assetsGET /v1/media-assetsGET /v1/media-assets/{id}GET /v1/media-assets/{id}/contentPOST /v1/storage/presigned-uploadPOST /v1/storage/media-assetsGET /v1/storage/media-assets/{id}GET /v1/storage/media-assets/{id}/contentGET /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-projectionsGET /v1/admin/documents/review-summaryPOST /v1/admin/documents/{id}/archiveGET /healthzGET /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_keyis required.content_typeis required and must parse as a media type.size_bytesmust be greater than or equal to0.bucketdefaults tohoctapaz-local.organization_idandowner_user_idcan be supplied by JSON or theX-Organization-IdandX-User-Idheaders.
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 fromX-Organization-Idwhen omitted.allOrgs=1orallOrgs=trueclears organization scope for internal rebuilds.status,visibility,subjectId,gradeLevel,q,page,limit.includeArchived=1includes archived rows whenstatusis 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, optionalpurpose. X-Organization-Idsupplies the tenant scope while auth/tenant middleware is still pending.- Allowed MIME types and upload limits follow legacy
StorageService.validateUpload: default max50MB,classroom-videomax500MB. - 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_ENDPOINTorS3_ENDPOINTis 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:9902and reads internally fromhttp://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.shverifies signed PUT plus content read against MinIO/S3-compatible storage.scripts/test/storage-media-parity-smoke.shcompares 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-uploadand/storage/media-assetscompatibility 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
MediaAssetinstead of being silently dropped. - Legacy teaching documents use review states
DRAFT,PENDING_REVIEW,APPROVED,REJECTED, andARCHIVED; the native projection copies those states into search metadata without moving review ownership to search-service. - Legacy document archive sets teaching documents to
ARCHIVEDand 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-assetsand 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
/documentsroutes off legacy. - Add parity fixtures for legacy
sharpoutputs 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.shagainst representative imported image/formula assets before default route promotion.