use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
pub const BLOB_SIZE_CAP: u64 = 10 * 1024 * 1024;
pub struct BlobStore {
root: PathBuf,
}
impl BlobStore {
pub fn new(root: impl Into<PathBuf>) -> Self {
BlobStore { root: root.into() }
}
pub fn blob_path(&self, hash: &str) -> PathBuf {
let (prefix, rest) = hash.split_at(2);
self.root.join(prefix).join(format!("{rest}.blob"))
}
pub fn has(&self, hash: &str) -> bool {
self.blob_path(hash).exists()
}
pub fn write(&self, hash: &str, content: &[u8]) -> Result<()> {
let dest = self.blob_path(hash);
if dest.exists() {
return Ok(());
}
let dir = dest.parent().expect("blob path always has a parent dir");
std::fs::create_dir_all(dir)
.with_context(|| format!("create blob dir {}", dir.display()))?;
let tmp_path = dir.join(format!(".tmp-{hash}"));
std::fs::write(&tmp_path, content)
.with_context(|| format!("write tmp blob {}", tmp_path.display()))?;
std::fs::rename(&tmp_path, &dest)
.with_context(|| format!("rename blob into place {}", dest.display()))?;
Ok(())
}
pub fn read(&self, hash: &str) -> Result<Vec<u8>> {
let path = self.blob_path(hash);
std::fs::read(&path).with_context(|| format!("read blob {} ({})", hash, path.display()))
}
pub fn read_string(&self, hash: &str) -> Result<String> {
let bytes = self.read(hash)?;
String::from_utf8(bytes).with_context(|| format!("blob {hash} is not valid UTF-8"))
}
}
pub fn open(objects_root: &Path) -> BlobStore {
BlobStore::new(objects_root)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn temp_store() -> (TempDir, BlobStore) {
let dir = TempDir::new().unwrap();
let store = BlobStore::new(dir.path().join("objects"));
(dir, store)
}
#[test]
fn fanout_path_splits_at_two() {
let store = BlobStore::new("/tmp/obj");
let hash = "aabbcc0011223344556677889900aabbcc0011223344556677889900aabbcc00";
let p = store.blob_path(hash);
assert_eq!(
p,
PathBuf::from(
"/tmp/obj/aa/bbcc0011223344556677889900aabbcc0011223344556677889900aabbcc00.blob"
)
);
}
#[test]
fn write_creates_file_in_fanout_dir() {
let (_dir, store) = temp_store();
let hash = "ffee00112233445566778899aabbccdd00112233445566778899aabbccdd0011";
store.write(hash, b"content").unwrap();
assert!(store.blob_path(hash).exists());
assert!(store.blob_path(hash).parent().unwrap().exists());
}
#[test]
fn write_then_read_roundtrip_unit() {
let (_dir, store) = temp_store();
let data = b"unit roundtrip";
let hash = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcde0";
store.write(hash, data).unwrap();
assert_eq!(store.read(hash).unwrap(), data);
}
}