1use std::path::{Path, PathBuf};
4
5#[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
44pub(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}