//! 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; /// 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>; /// 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>; } #[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" ); }