Full-depth rename per the approved plan; the 'claw' product vocabulary (claws, /claws routes, clawId, Claw Chat) stays — it is now the brand. - Display brand: Clawmates (manifest, titles, hero, login/rail logo 'clawmates'); default host app.clawmates.work; registry ghcr.io/clawmates - Crates tc-* -> cm-* (16 crates + all imports); binaries clawmates-server/broker/bundler; images clawmates/*; env prefix CLAWMATES_* (+ CM_TEST_DATABASE_URL / CM_LIVE_LLM); config clawmates.toml; helm chart deploy/helm/clawmates with clawmates-* resources; db names clawmates*; sockets /run/clawmates; cookie cm_session; kind cluster clawmates-test; seccomp node profile clawmates-agent-profile.json - All 9 Playwright brand assertions updated in lockstep; historical spec document left untouched as the only remaining 'TeamClaw' - Local env migrated: dev pg clawmates-dev-pg/clawmates_dev, shared test server clawmates-test-pg, kind cluster recreated with image + profile, compose images rebuilt under clawmates/* Verified end to end: 161 Rust + 68 frontend tests, 29 Playwright journeys, 4 live kind tests, helm/install/LOC/placeholder gates, and the clean-room install rehearsal serving the clawmates login page from a signed bundle of the rebuilt images. Co-Authored-By: Claude Fable 5 <[email protected]>
87 lines
2.7 KiB
Rust
87 lines
2.7 KiB
Rust
//! Blob storage behind the three file drives (spec §7.4). The local
|
|
//! filesystem implementation serves dev and the air-gapped target; an
|
|
//! S3-compatible implementation slots in behind the same trait for cloud.
|
|
|
|
mod s3;
|
|
|
|
pub use s3::S3BlobStore;
|
|
|
|
use std::path::{Component, Path, PathBuf};
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum BlobError {
|
|
#[error("blob not found")]
|
|
NotFound,
|
|
#[error("invalid blob key: {0}")]
|
|
InvalidKey(String),
|
|
#[error("storage io: {0}")]
|
|
Io(String),
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
pub trait BlobStore: Send + Sync {
|
|
async fn put(&self, key: &str, bytes: &[u8]) -> Result<(), BlobError>;
|
|
async fn get(&self, key: &str) -> Result<Vec<u8>, BlobError>;
|
|
async fn delete(&self, key: &str) -> Result<(), BlobError>;
|
|
}
|
|
|
|
/// Filesystem-backed store rooted at a data directory.
|
|
pub struct LocalBlobStore {
|
|
root: PathBuf,
|
|
}
|
|
|
|
impl LocalBlobStore {
|
|
pub fn new(root: PathBuf) -> LocalBlobStore {
|
|
LocalBlobStore { root }
|
|
}
|
|
|
|
/// Resolves a key strictly below the root: rejects absolute paths and
|
|
/// any `..`/`.` components so keys can never escape the data dir.
|
|
fn resolve(&self, key: &str) -> Result<PathBuf, BlobError> {
|
|
let path = Path::new(key);
|
|
if path.is_absolute() || key.is_empty() {
|
|
return Err(BlobError::InvalidKey(key.to_owned()));
|
|
}
|
|
for component in path.components() {
|
|
match component {
|
|
Component::Normal(_) => {}
|
|
_ => return Err(BlobError::InvalidKey(key.to_owned())),
|
|
}
|
|
}
|
|
Ok(self.root.join(path))
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl BlobStore for LocalBlobStore {
|
|
async fn put(&self, key: &str, bytes: &[u8]) -> Result<(), BlobError> {
|
|
let path = self.resolve(key)?;
|
|
if let Some(parent) = path.parent() {
|
|
tokio::fs::create_dir_all(parent)
|
|
.await
|
|
.map_err(|e| BlobError::Io(e.to_string()))?;
|
|
}
|
|
tokio::fs::write(&path, bytes)
|
|
.await
|
|
.map_err(|e| BlobError::Io(e.to_string()))
|
|
}
|
|
|
|
async fn get(&self, key: &str) -> Result<Vec<u8>, BlobError> {
|
|
let path = self.resolve(key)?;
|
|
match tokio::fs::read(&path).await {
|
|
Ok(bytes) => Ok(bytes),
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(BlobError::NotFound),
|
|
Err(e) => Err(BlobError::Io(e.to_string())),
|
|
}
|
|
}
|
|
|
|
async fn delete(&self, key: &str) -> Result<(), BlobError> {
|
|
let path = self.resolve(key)?;
|
|
match tokio::fs::remove_file(&path).await {
|
|
Ok(()) => Ok(()),
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(BlobError::NotFound),
|
|
Err(e) => Err(BlobError::Io(e.to_string())),
|
|
}
|
|
}
|
|
}
|