pub struct DiskArtifactStore { /* private fields */ }Expand description
On-disk content-addressed signed artifact store.
Format on disk for a single blob:
magic(16) || version(4) || content_hash(32) || zstd(payload) || hmac_tag(32)The HMAC covers everything except the trailing 32-byte tag itself. Verification uses constant-time comparison.
Implementations§
Source§impl DiskArtifactStore
impl DiskArtifactStore
Sourcepub fn new(dir: PathBuf, hmac_key: [u8; 32]) -> Self
pub fn new(dir: PathBuf, hmac_key: [u8; 32]) -> Self
Construct a disk store rooted at dir, signing with hmac_key.
The directory is created lazily on the first put.
This wraps hmac_key in a SingleKeyProvider internally, so the
store writes and reads under exactly that one key — identical to
the historical single-key behaviour. Use
Self::with_key_provider for rotation support.
Sourcepub fn with_key_provider(
dir: PathBuf,
key_provider: Arc<dyn KeyProvider>,
) -> Self
pub fn with_key_provider( dir: PathBuf, key_provider: Arc<dyn KeyProvider>, ) -> Self
Construct a disk store rooted at dir whose signing keys come from
key_provider.
New puts sign under KeyProvider::active_key; get and list
try every key in KeyProvider::read_keys, so a store built with
a RotatingKeyProvider can read blobs written under a retired
key after rotation. The directory is created lazily on the first
put.
Sourcepub fn put_with_metadata(
&self,
payload: &[u8],
metadata: &ArtifactMetadata,
) -> Result<ContentHash, ArtifactError>
pub fn put_with_metadata( &self, payload: &[u8], metadata: &ArtifactMetadata, ) -> Result<ContentHash, ArtifactError>
Insert payload (exactly like ArtifactStore::put) and write an
ArtifactMetadata sidecar next to the blob under the active key.
The blob and the sidecar are independent files: the blob is the
content-addressed envelope, the sidecar is a .meta.json serde
document. A later Self::metadata reads the sidecar back. Plain
put continues to write no sidecar, so metadata stays optional.
metadata.original_len is recorded as supplied by the caller (it
is provenance, not a re-derived fact); pass payload.len() for the
common “this is the blob I just stored” case.
Sourcepub fn metadata(
&self,
hash: &ContentHash,
) -> Result<ArtifactMetadata, ArtifactError>
pub fn metadata( &self, hash: &ContentHash, ) -> Result<ArtifactMetadata, ArtifactError>
Read the ArtifactMetadata sidecar for hash under whichever
accepted key (active or retired) the blob actually lives under.
Rotation-aware: this resolves the blob’s key with the same
resolve_read_key probe get / list use, then reads the
sidecar partitioned under that key’s fingerprint. A blob written
under a now-retired key (and its sidecar) is therefore still
readable here after rotation, matching what get exposes.
Returns ArtifactError::NotFound if no blob exists under any
accepted key, or if the blob exists but carries no sidecar (e.g. it
was written via plain put). A malformed sidecar surfaces as
ArtifactError::Metadata.
Trait Implementations§
Source§impl ArtifactStore for DiskArtifactStore
impl ArtifactStore for DiskArtifactStore
Source§fn put(&self, payload: &[u8]) -> Result<ContentHash, ArtifactError>
fn put(&self, payload: &[u8]) -> Result<ContentHash, ArtifactError>
payload into the store. The returned ContentHash is
blake3(payload); repeated calls with identical payloads return
identical hashes and overwrite the existing entry in place.Source§fn get(&self, hash: &ContentHash) -> Result<Vec<u8>, ArtifactError>
fn get(&self, hash: &ContentHash) -> Result<Vec<u8>, ArtifactError>
hash. Returns
ArtifactError::NotFound on a genuine miss; integrity-failure
variants (ArtifactError::BadMagic, ArtifactError::BadVersion,
ArtifactError::BadHmac, ArtifactError::HashMismatch) are
returned when a record was present but the format checks rejected
it.Source§fn get_to(
&self,
hash: &ContentHash,
out: &mut dyn Write,
) -> Result<u64, ArtifactError>
fn get_to( &self, hash: &ContentHash, out: &mut dyn Write, ) -> Result<u64, ArtifactError>
hash into out,
returning the number of bytes written. This completes the
streaming story Self::put already has on the write side: where
Self::get returns an owned Vec<u8> (up to
MAX_DECOMPRESSED_LEN resident),
get_to lets a caller pipe a large artifact straight into a file,
socket, or hashing sink without first materialising the whole
decoded payload as a return value. Read moreSource§fn list(&self) -> Result<Vec<ContentHash>, ArtifactError>
fn list(&self) -> Result<Vec<ContentHash>, ArtifactError>
Source§fn contains(&self, hash: &ContentHash) -> Result<bool, ArtifactError>
fn contains(&self, hash: &ContentHash) -> Result<bool, ArtifactError>
hash: returns true if an entry is
present, false otherwise. This is deliberately a stat-only
check — it does NOT decode, decompress, or HMAC-verify the
record, so it is dramatically cheaper than Self::get and
suitable for the GC/audit roadmap’s “is this content already
resident?” question. Read moreSource§fn remove(&self, hash: &ContentHash) -> Result<bool, ArtifactError>
fn remove(&self, hash: &ContentHash) -> Result<bool, ArtifactError>
hash. Returns true if a record
was removed, false if nothing was stored under hash (a
no-op delete is not an error — it mirrors HashMap::remove’s
“was it there?” boolean and the POSIX unlink-of-missing
convention GC callers expect). Read more