Generic / raw repository — design
Class:
design· Function:raw(usesregistryfor theRepositoryBackendTraitlist()/fetch()/put()/delete_artifact()seam and the HTTP door'shandle_http2_requestdispatch). The whole-system view isdesign.md; the parity gap this closes isfeature-complete-gaps.md§1 (Generic / Raw writable repo — previously ❌, "the single most-requested misc bucket and the cheapest missing backend") andartifactory-parity-design.md§A.
What this is
A raw (a.k.a. generic) repository is a writable bucket for arbitrary blobs addressed by path — the Nexus/Artifactory "raw hosted repository": the misc bucket for tarballs, installers, firmware images, reports, licences, any payload that has no ecosystem protocol of its own. A client PUTs bytes at an arbitrary path and GETs them back byte-identical; the path is the address.
It ships as server/lib/src/raw.rs — RawBackend, wired by ron_repo_type: "raw" (ron_data_dir its only requirement, defaulting under /var/lib/holger/<name>). It is deliberately the leanest backend in the tree: no archive, no index synthesis, no metadata generation, no ecosystem parsing — just a safe, content-verifiable loose-file store.
The one rule: a coordinate ↔ path bijection
holger has two entry points into every backend — the gRPC / in-process side addresses a blob by an ArtifactId {namespace, name, version}; the HTTP door addresses it by a raw path. RawBackend makes them meet at one deterministic mapping so both see the same bytes:
id → relpath= the/-join of the non-empty parts of[namespace, name, version]. A caller that leavesnamespace/versionempty simply addresses byname.list()reports every stored file asArtifactId { namespace: None, name: <relpath>, version: "" }— the whole relative path carried inname.
The property that makes the reuse work: id_to_relpath(listed_id) == <relpath> round-trips exactly. So a listing fed back into fetch/delete_artifact — which is precisely what search, retention/GC and replication all do — resolves to the very same file. The raw backend gets those four features for free by implementing the trait, twinning nothing (Law #5).
The three invariants
-
Traversal-closed. Every path segment is validated by
safe_relpath: an empty segment,.,.., a backslash or a NUL is refused, on both the write and the read path and for both entry points. NoArtifactIdand no HTTP path can climb out ofbase_dir. (The sibling loose-file backends keep the same per-segment guard privately;promote::sanitize_repocollapses to a single segment, which would destroy the nested paths a raw bucket exists to hold — so it is intentionally not reused here.) -
Content-verified deletes.
delete_artifact— the retention/GC executor's write side — reads the on-disk bytes, recomputes their SHA-256, and unlinks only if it matches theexpected_content_idthe planner computed (the samesha256_hexasholger-server-liband the other loose-file backends — all of which now consume the ONE sharednornir_hash::sha256_hexleaf from edda, LAW #5, so every recompute is byte-identical by construction). A mismatch (a re-upload, a swap under the plan) is refused; a missing blob is an error, never a silent success. -
Integrity at the door, for free. A raw
PUTinherits the existing checksum-deploy gate: an advertisedX-Checksum-Sha256/-Sha512is verified against the received bytes at the HTTP gateway beforeRawBackendsees them. Search-by-checksum (search.rs's fetch-and-hash axis) then finds a raw blob by its digest. "Checksum-addressed" is thus satisfied by reusing seams that already exist, not by a new CAS in this backend.
The HTTP surface
RawBackend::handle_http2_request (the repo-name-stripped path after the gateway routes by first segment):
| Method | Path | Effect | Status |
PUT / POST |
/<repo>/<path> |
atomic write-then-rename | 201 |
GET |
/<repo>/<path> |
serve bytes | 200 / 404 |
HEAD |
/<repo>/<path> |
existence, no body | 200 / 404 |
DELETE |
/<repo>/<path> |
unlink (admin-gated at the door) | 204 / 404 |
| other | — | not a raw verb | 405 + Allow |
Writes are already write-gated (is_writable() == true, RBAC + read-only checks run at the door before the body is collected); DELETE requires admin per the gateway's role model.
Observability (Law #4)
Every HTTP surface emits a #[cfg(feature="testmatrix")] functional_status("holger-raw/http", …) cell (raw_put / raw_get / raw_get_miss / raw_delete), so nornir test --features testmatrix sees each live surface. The HTTP gateway's single exit point already bumps the shared metrics registry (Put/Get/Delete), so a raw repo shows up in /metrics with no new wiring. 11 unit tests cover the round-trip, path-traversal refusal (put/fetch/HTTP), the nested-listing round-trip into fetch, substring/limit listing, and the RED-when-broken content-id-verified delete (a wrong id must refuse to unlink and the blob must survive).
What is deliberately out of scope
No per-blob custom properties (the deferred search axis, tracked in §8), no whole-directory listing REST endpoint beyond the trait list(), and no cross-blob dedup CAS at rest (promote's content-addressed PROD store already covers the immutable case). Those stay follow-ups; this backend is the cheap, complete misc bucket.