103 lines
3.7 KiB
Rust
103 lines
3.7 KiB
Rust
|
|
//! Pluggable blob-storage backend trait (D23).
|
||
|
|
//!
|
||
|
|
//! The runtime is parametric over any type implementing [`BlobBackend`].
|
||
|
|
//! [`LocalFilesystemBlobs`](crate::store::LocalFilesystemBlobs) is the
|
||
|
|
//! default impl shipped with the substrate — content-addressed files on
|
||
|
|
//! the local disk, no external dependencies. Alternative backends
|
||
|
|
//! (`S3Blobs`, `OciBlobs`) are placeholder modules behind feature flags;
|
||
|
|
//! their real implementations are deferred per the trigger-based
|
||
|
|
//! discipline (D23).
|
||
|
|
|
||
|
|
use crate::error::Result;
|
||
|
|
use crate::store::BlobId;
|
||
|
|
|
||
|
|
/// Pluggable content-addressed blob store.
|
||
|
|
///
|
||
|
|
/// Contract any backend MUST honour:
|
||
|
|
///
|
||
|
|
/// 1. `put(bytes)` returns the address `BlobId::from_bytes(bytes)` —
|
||
|
|
/// addressing is by blake3, identical across backends.
|
||
|
|
/// 2. `get(id)` returns the exact bytes that produced `id`, or an
|
||
|
|
/// error. Corrupted bytes MUST be rejected with
|
||
|
|
/// [`crate::Error::Corrupt`].
|
||
|
|
/// 3. `exists(id)` is consistent with `get(id)`: it returns true iff
|
||
|
|
/// a subsequent `get(id)` would not return `NotFound`. It MUST NOT
|
||
|
|
/// perform an integrity check (use `get` for that).
|
||
|
|
/// 4. `list_prefix(prefix_hex)` returns every id whose hex
|
||
|
|
/// representation starts with `prefix_hex`. An empty `prefix_hex`
|
||
|
|
/// returns every id known to the backend.
|
||
|
|
pub trait BlobBackend {
|
||
|
|
/// Store `bytes` and return its content address.
|
||
|
|
///
|
||
|
|
/// # Errors
|
||
|
|
///
|
||
|
|
/// Backend-specific. The local-filesystem default returns IO errors
|
||
|
|
/// on write failure.
|
||
|
|
fn put(&self, bytes: &[u8]) -> Result<BlobId>;
|
||
|
|
|
||
|
|
/// Retrieve bytes by content address.
|
||
|
|
///
|
||
|
|
/// # Errors
|
||
|
|
///
|
||
|
|
/// - [`crate::Error::NotFound`] when the blob is absent.
|
||
|
|
/// - [`crate::Error::Corrupt`] when stored bytes do not hash to `id`.
|
||
|
|
/// - [`crate::Error::Io`] on transport failure.
|
||
|
|
fn get(&self, id: &BlobId) -> Result<Vec<u8>>;
|
||
|
|
|
||
|
|
/// Whether `id` is reachable (without integrity check).
|
||
|
|
fn exists(&self, id: &BlobId) -> bool;
|
||
|
|
|
||
|
|
/// List every known id whose hex representation starts with
|
||
|
|
/// `prefix_hex`.
|
||
|
|
///
|
||
|
|
/// # Errors
|
||
|
|
///
|
||
|
|
/// Backend-specific IO errors.
|
||
|
|
fn list_prefix(&self, prefix_hex: &str) -> Result<Vec<BlobId>>;
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
#[test]
|
||
|
|
fn local_filesystem() {
|
||
|
|
use crate::store::LocalFilesystemBlobs;
|
||
|
|
|
||
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
||
|
|
let backend: LocalFilesystemBlobs =
|
||
|
|
LocalFilesystemBlobs::open(tmp.path()).expect("open backend");
|
||
|
|
|
||
|
|
// put / get round trip
|
||
|
|
let bytes = b"hello blobs".to_vec();
|
||
|
|
let id = BlobBackend::put(&backend, &bytes).expect("put ok");
|
||
|
|
let fetched = BlobBackend::get(&backend, &id).expect("get ok");
|
||
|
|
assert_eq!(fetched, bytes);
|
||
|
|
|
||
|
|
// exists is true after put, false for an unknown id
|
||
|
|
assert!(BlobBackend::exists(&backend, &id));
|
||
|
|
let unknown = BlobId::from_bytes(b"never stored");
|
||
|
|
assert!(!BlobBackend::exists(&backend, &unknown));
|
||
|
|
|
||
|
|
// put is idempotent
|
||
|
|
let id_again = BlobBackend::put(&backend, &bytes).expect("re-put ok");
|
||
|
|
assert_eq!(id, id_again);
|
||
|
|
|
||
|
|
// list_prefix finds the blob by its hex prefix
|
||
|
|
let hex = id.to_hex();
|
||
|
|
let prefix = &hex[..4];
|
||
|
|
let found = BlobBackend::list_prefix(&backend, prefix).expect("list ok");
|
||
|
|
assert!(
|
||
|
|
found.iter().any(|x| x == &id),
|
||
|
|
"list_prefix({prefix}) must include the stored blob"
|
||
|
|
);
|
||
|
|
|
||
|
|
// empty prefix returns at least the stored blob
|
||
|
|
let all = BlobBackend::list_prefix(&backend, "").expect("list all ok");
|
||
|
|
assert!(all.iter().any(|x| x == &id));
|
||
|
|
|
||
|
|
// unknown prefix returns empty
|
||
|
|
let none = BlobBackend::list_prefix(&backend, "ffffffffff").expect("list ok");
|
||
|
|
assert!(
|
||
|
|
!none.iter().any(|x| x == &id),
|
||
|
|
"id with a different prefix must not be returned"
|
||
|
|
);
|
||
|
|
}
|