Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

.nornir/raw-repo-design.md

Generic / raw repository — design

Class: design · Function: raw (uses registry for the RepositoryBackendTrait list()/fetch()/put()/delete_artifact() seam and the HTTP door's handle_http2_request dispatch). The whole-system view is design.md; the parity gap this closes is feature-complete-gaps.md §1 (Generic / Raw writable repo — previously ❌, "the single most-requested misc bucket and the cheapest missing backend") and artifactory-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:

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

  1. 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. No ArtifactId and no HTTP path can climb out of base_dir. (The sibling loose-file backends keep the same per-segment guard privately; promote::sanitize_repo collapses to a single segment, which would destroy the nested paths a raw bucket exists to hold — so it is intentionally not reused here.)

  2. 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 the expected_content_id the planner computed (the same sha256_hex as holger-server-lib and the other loose-file backends — all of which now consume the ONE shared nornir_hash::sha256_hex leaf 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.

  3. Integrity at the door, for free. A raw PUT inherits the existing checksum-deploy gate: an advertised X-Checksum-Sha256/-Sha512 is verified against the received bytes at the HTTP gateway before RawBackend sees 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.