Skip to main content

bamboo_subagent/
error.rs

1//! Error model + atomic write helper shared by `store` and `mailbox`.
2
3use std::path::{Path, PathBuf};
4
5/// Errors from the persistent store / mailbox layer.
6///
7/// Invariant: authoritative data lives in `session.json` files; index and mailbox files are
8/// caches/queues — a corrupt one is recoverable (rebuild / quarantine), never fatal.
9#[derive(Debug, thiserror::Error)]
10pub enum StoreError {
11    #[error("io at {path}: {source}")]
12    Io {
13        path: PathBuf,
14        source: std::io::Error,
15    },
16    #[error("decode {path}: {source}")]
17    Decode {
18        path: PathBuf,
19        source: serde_json::Error,
20    },
21    #[error("corrupt index {path}, rebuild required")]
22    CorruptIndex { path: PathBuf },
23    #[error("not found: {0}")]
24    NotFound(String),
25}
26
27pub type Result<T> = std::result::Result<T, StoreError>;
28
29impl StoreError {
30    pub(crate) fn io(path: impl AsRef<Path>, source: std::io::Error) -> Self {
31        StoreError::Io {
32            path: path.as_ref().to_path_buf(),
33            source,
34        }
35    }
36    pub(crate) fn decode(path: impl AsRef<Path>, source: serde_json::Error) -> Self {
37        StoreError::Decode {
38            path: path.as_ref().to_path_buf(),
39            source,
40        }
41    }
42}
43
44/// Write `bytes` to `path` atomically: a temp file in the same directory + `rename`.
45///
46/// Readers never observe a half-written file. Parent directories are created as needed.
47/// The temp name is hidden (`.`-prefixed) and unique so concurrent writers and directory
48/// scanners (e.g. mailbox `drain`) skip it.
49pub(crate) async fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> {
50    use tokio::io::AsyncWriteExt;
51
52    let dir = path
53        .parent()
54        .ok_or_else(|| StoreError::NotFound(format!("no parent dir for {}", path.display())))?;
55    tokio::fs::create_dir_all(dir)
56        .await
57        .map_err(|e| StoreError::io(dir, e))?;
58
59    let stem = path.file_name().and_then(|s| s.to_str()).unwrap_or("file");
60    let tmp = dir.join(format!(".{stem}.tmp.{}", uuid::Uuid::new_v4()));
61
62    {
63        let mut f = tokio::fs::File::create(&tmp)
64            .await
65            .map_err(|e| StoreError::io(&tmp, e))?;
66        f.write_all(bytes)
67            .await
68            .map_err(|e| StoreError::io(&tmp, e))?;
69        f.sync_all().await.map_err(|e| StoreError::io(&tmp, e))?;
70    }
71
72    tokio::fs::rename(&tmp, path)
73        .await
74        .map_err(|e| StoreError::io(path, e))?;
75    Ok(())
76}