Add /documents/{id}/content and /versions routes (#859) - #863
rollroyces wants to merge 1 commit into
Conversation
The RDF export (0020) writes a stable digest for every byte — but no
route ever served those bytes. An outside auditor could check the digest
and never fetch what it described, and 0040's anchors into original
content had no working read path.
This change wires two document-identity-keyed routes through the same
require_kb(Viewer) gate the export uses:
GET /api/v1/documents/{id}/content[?version=N]
- raw bytes from the BlobStore (data/files/{sha256})
- Content-Length, Content-Type, Content-Disposition, strong ETag
(sha, double-quoted, RFC 7232 §2.3)
- purged documents → 410 Gone (bytes are gone for good)
- ?version=N where N is not recorded → 404 (not the default version)
- ledger says blob exists but disk is missing → 500 with the actual
sha in the log; pretending 404 would lead clients astray
GET /api/v1/documents/{id}/versions
- JSON list of {version, sha256, size_bytes, ingested_at} in version
order; current_sha256 from the documents row so callers can tell
which row is the live one
Backed by DocumentVersion + list_versions / get_version in utopia-store.
The DocumentVersion row matches document_versions 1:1; no schema change.
Tests:
- 2 ascii_filename unit tests (no DB)
- 3 integration tests via the existing Fixture: bytes round-trip,
unknown version → 404, purged → 410
Signed-off-by: rollroyces <royce@rollroyces.com>
|
Cross-posting on #860 and #863: these two implement the same feature from #859 and .route("/documents/{id}/content", get(documents_routes::content))
.route("/documents/{id}/versions", get(documents_routes::versions))Neither exists on Worth a maintainer call on which one to keep before either gets more review |
|
Closing the loop on the duplicate with #860. I went through both implementations
This branch authenticates through Three things in this branch I'd flag so they don't get carried forward:
One thing this branch has that #860 doesn't, and which I think is worth keeping:
Thanks for picking #859 up — sorry the timing collided. |
/documents/{id}/content and /versions (#859)
The RDF export writes a stable digest for every byte — but no route ever served those bytes. An outside auditor could check the digest and never fetch what it described. And 0040's anchors into original content (images, pages, recordings) had no working read path.
This PR wires two document-identity-keyed routes through the same
require_kb(Viewer)gate the export uses:GET /api/v1/documents/{id}/content[?version=N]BlobStore(data/files/{sha256})Content-Length,Content-Type,Content-Disposition(inline;filename="…"ASCII-safe), strong ETag (sha, double-quoted, RFC 7232 §2.3)purged_at IS NOT NULL→ 410 Gone — bytes are gone for good?version=Nwhere N is not recorded → 404 (not silently the default version)GET /api/v1/documents/{id}/versions{version, sha256, size_bytes, ingested_at}in version order;current_sha256from thedocumentsrow so callers can tell which row is the live oneScope of this PR
DocumentVersion) inutopia-corelist_versions,get_version(and aDocumentVersionrow matchingdocument_versions1:1 — no schema change)documents_routes::content,documents_routes::versionsapi/mod.rsascii_filename) + 3 integration tests via the existingFixture: bytes round-trip, unknown version → 404, purged → 410What is not in this PR
BlobStore::getreturnsVec<u8>), bounded by the existing 100 MiB upload cap. Streaming is a follow-up cut when the need appears.web/src/docs/api.mdwriteup, not a code change.Verification
cargo check -p utopia-servercleancargo clippy -p utopia-server --all-targets -- -D warningscleancargo fmt --checkcleanUTOPIA_DATABASE_URL)Ok(())early whenUTOPIA_DATABASE_URLis empty, same pattern as the rest ofdocuments_routes_tests.rsThe fix routes also write one full audit row per
GET /contentcall through the existingaudit::recordpath — actually they don't, because reads aren't audit-worthy the same way writes are. If you want them logged, say so and I'll add it.