use std::sync::Mutex;
use object_store::path::Path;
use object_store::{ObjectStore, ObjectStoreExt, PutPayload};
use url::Url;
use crate::crypto;
use crate::error::{Error, Result};
use super::CasStore;
#[derive(Debug)]
pub struct S3Cas {
store: Box<dyn ObjectStore>,
prefix: Path,
rt: Mutex<tokio::runtime::Runtime>,
}
impl S3Cas {
pub fn from_spec(spec: &str) -> Result<Self> {
let url = Url::parse(spec).map_err(|e| Error::InvalidArg {
arg: "--casdir",
reason: format!("malformed S3 CAS spec '{spec}': {e}"),
})?;
if url.scheme() != "s3" {
return Err(Error::InvalidArg {
arg: "--casdir",
reason: format!("S3Cas cannot serve scheme '{}'", url.scheme()),
});
}
let store = object_store::aws::AmazonS3Builder::from_env()
.with_url(url.clone())
.build()
.map_err(|e| Error::InvalidArg {
arg: "--casdir",
reason: format!("cannot open S3 CAS '{spec}': {e}"),
})?;
let store: Box<dyn ObjectStore> = Box::new(store);
let prefix = Path::from(url.path().trim_start_matches('/'));
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| Error::InvalidArg {
arg: "--casdir",
reason: format!("cannot start the object-store runtime: {e}"),
})?;
Ok(Self {
store,
prefix,
rt: Mutex::new(rt),
})
}
fn block_on<F: std::future::Future>(&self, fut: F) -> F::Output {
self.rt
.lock()
.expect("object-store runtime mutex poisoned")
.block_on(fut)
}
fn key(&self, hash: &str) -> Path {
self.prefix.clone().join(hash)
}
fn store_error(op: &'static str, e: object_store::Error) -> Error {
Error::CasBackend {
backend: "s3",
op,
detail: e.to_string(),
}
}
}
fn hash_of(blob: &[u8], policy: &dyn crypto::CryptoPolicy) -> Result<String> {
crypto::hexdigest("sha3-256", blob, policy)
}
impl CasStore for S3Cas {
fn name(&self) -> &'static str {
"s3"
}
fn save(&self, blob: &[u8], policy: &dyn crypto::CryptoPolicy) -> Result<String> {
let hash = hash_of(blob, policy)?;
let key = self.key(&hash);
let payload = PutPayload::from_bytes(blob.to_vec().into());
let res = self.block_on(self.store.put(&key, payload));
res.map_err(|e| Self::store_error("put", e))?;
Ok(hash)
}
fn load(&self, hash: &str, policy: &dyn crypto::CryptoPolicy) -> Result<Vec<u8>> {
super::validate_cas_hash(hash)?;
let key = self.key(hash);
let res = self.block_on(self.store.get(&key));
let result = res.map_err(|e| match e {
object_store::Error::NotFound { .. } => Error::CasNotFound { hash: hash.into() },
other => Self::store_error("get", other),
})?;
let bytes = self
.block_on(result.bytes())
.map_err(|e| Self::store_error("get (read body)", e))?;
let actual = hash_of(&bytes, policy)?;
if actual != hash {
return Err(Error::CasHashMismatch {
expected: hash.into(),
actual,
});
}
Ok(bytes.to_vec())
}
fn contains(&self, hash: &str, _policy: &dyn crypto::CryptoPolicy) -> Result<bool> {
super::validate_cas_hash(hash)?;
let key = self.key(hash);
match self.block_on(self.store.head(&key)) {
Ok(_) => Ok(true),
Err(object_store::Error::NotFound { .. }) => Ok(false),
Err(e) => Err(Self::store_error("head", e)),
}
}
fn list(&self) -> Result<Vec<String>> {
let mut hashes = Vec::new();
let mut stream = self.store.list(Some(&self.prefix));
use futures::StreamExt;
loop {
let next = self.block_on(stream.next());
match next {
Some(Ok(meta)) => {
let name = meta.location.filename().unwrap_or_default().to_string();
if name.len() == 64 && name.chars().all(|c| c.is_ascii_hexdigit()) {
hashes.push(name);
}
}
Some(Err(e)) => return Err(Self::store_error("list", e)),
None => break,
}
}
hashes.sort();
Ok(hashes)
}
fn delete(&self, hash: &str) -> Result<()> {
super::validate_cas_hash(hash)?;
let key = self.key(hash);
self.block_on(self.store.delete(&key))
.map_err(|e| Self::store_error("delete", e))
}
}