Catalog and protocol requests from the studio¶
Status: proposed (2026-11-07). Nothing here is implemented upstream.
The studio publisher (evilflowers_books_digitalizer/studio/adapters/catalog/) needs two
things that evilflowers-protocol and the EvilFlowers catalog do not offer today. It
works around the first and refuses the second.
Sources checked: evilflowers-protocol-python at rev 62c05d7 (the pinned one; main
at 04ab5cd adds only CI) and EvilFlowersCatalog/EvilFlowersCatalog master.
A. evilflowers-protocol: acquisition delete / replace wrappers¶
Today. CatalogClient (evilflowers_protocol/client.py) wraps ping,
resolve_catalog, create_entry, update_entry, get_entry, find_entries,
upload_acquisition and introspect. It cannot remove an acquisition, although the
catalog can: DELETE /api/v1/acquisitions/{acquisition_id} exists, and
PUT /api/v1/acquisitions/{id} updates only the acquisition's meta.
Workaround in the studio. studio/adapters/catalog/client.py::delete_acquisition
sends client.transport.request("DELETE", f"acquisitions/{id}") and treats 404 as
already deleted. The transport retries 5xx and timeouts, so a DELETE whose response got
lost comes back as 404 on the retry. The republish is upload-then-delete: upload the new
file, re-read the entry, delete every other acquisition of that MIME type. Re-reading
also catches the duplicate that a retried upload POST can attach (the protocol's own
docstring warns about this).
Request. Add to CatalogClient:
def delete_acquisition(self, acquisition_id: UUID, *, missing_ok: bool = True) -> None:
"""DELETE acquisitions/{id}; 404 is success when missing_ok."""
def replace_acquisition(self, catalog_id: UUID, entry_id: UUID, path: Path, *,
mime: str, relation: str = "open-access") -> AcquisitionRef:
"""Upload ``path``, then delete the entry's other acquisitions of ``mime``.
Upload first, so a failure never leaves the entry without the format."""
Once these are released, the studio drops delete_acquisition and TransportDeleter
from client.py and calls the client directly. No other studio change is needed.
B. EvilFlowers catalog: create URL-backed acquisitions over REST¶
Today. The Acquisition model (apps/core/models/acquisition.py) already has
file_url and storage_backend (local | external_url). No REST endpoint sets them:
POST /api/v1/catalogs/{catalog_id}/entries/{entry_id}(the acquisition upload) takes multipartcontentplus ametadataJSON validated byAcquisitionMetaForm, which has onlyrelationandprices.POST /api/v1/acquisitionsreturns 501 Not implemented.- Only the internal Dataverse sync (
apps/dataverse/services/sync.py) createsexternal_urlacquisitions.
Why the studio wants it. The studio writes every book to the NFS volume
(/data/digitized/studio/<project>/<item_id>/), which gateway nginx can serve as
https://elvira.stuba.sk/digitized/... (deploy/k8s/README.md, prerequisite 5). With
URL-backed acquisitions a publish sends a few hundred bytes instead of the PDF and EPUB,
nothing is stored twice, and a republish is an overwrite on the volume.
EvilFlowersPublisher.external_url() already builds these URLs:
files_base_url + "/" + <path below artifacts_root>.
Request. On POST /api/v1/catalogs/{catalog_id}/entries/{entry_id}, also accept a
JSON body (no file part) validated by an extended AcquisitionMetaForm:
{
"relation": "open-access",
"mime": "application/pdf",
"storage_backend": "external_url",
"file_url": "https://elvira.stuba.sk/digitized/stu/stu-fei-vektory/stu-fei-vektory.pdf"
}
Rules worth agreeing on:
file_urlis required whenstorage_backend = external_url.mimeis required because there is no upload to sniff it from.- Allow-list the URL hosts in settings, because readers' browsers will be sent there.
- Return the same acquisition payload as an upload (
id,relation,mime,url). - Serving the acquisition follows whatever the catalog already does for Dataverse
external_urlacquisitions (redirect or proxy tofile_url; not checked here).
The protocol would then add link_acquisition(catalog_id, entry_id, url, *, mime,
relation). The studio would replace its files_base_url refusal
(EvilFlowersPublisher._refuse_external_url) with a call to it, and the
upload-then-delete replace logic stays the same.
Until then a project that sets publish.files_base_url fails at publish time,
before any catalog request, with an error that points here. The publisher does not fall
back to uploading, because the setting exists to avoid uploads, and silently uploading
gigabytes would go against what the operator configured. To publish now, remove
files_base_url from the project's [publish] table.