use std::fs::{self, File};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use serde::Serialize;
use serde::de::DeserializeOwned;
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("store io error: {0}")]
Io(#[from] io::Error),
#[error("store encode error: {0}")]
Encode(#[source] serde_json::Error),
#[error("corrupt store at {path}: {source}")]
Corrupt {
path: PathBuf,
quarantined: Option<PathBuf>,
#[source]
source: serde_json::Error,
},
}
static UNIQUE_COUNTER: AtomicU64 = AtomicU64::new(0);
fn unique_suffix() -> String {
let unique = UNIQUE_COUNTER.fetch_add(1, Ordering::Relaxed);
format!(
"{}-{}-{unique}",
std::process::id(),
crate::time::now_millis()
)
}
fn directory_of(path: &Path) -> PathBuf {
match path.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent.to_path_buf(),
_ => PathBuf::from("."),
}
}
fn file_name_of(path: &Path) -> &str {
path.file_name()
.and_then(|name| name.to_str())
.unwrap_or("store")
}
pub fn write_json_atomic<T: Serialize + ?Sized>(path: &Path, value: &T) -> Result<(), StoreError> {
let bytes = serde_json::to_vec_pretty(value).map_err(StoreError::Encode)?;
write_atomic(path, &bytes)?;
Ok(())
}
pub fn read_json<T: DeserializeOwned>(path: &Path) -> Result<Option<T>, StoreError> {
let bytes = match fs::read(path) {
Ok(bytes) => bytes,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(err) => return Err(StoreError::Io(err)),
};
match serde_json::from_slice::<T>(&bytes) {
Ok(value) => Ok(Some(value)),
Err(source) => Err(StoreError::Corrupt {
path: path.to_path_buf(),
quarantined: quarantine(path).ok(),
source,
}),
}
}
pub fn write_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
let directory = directory_of(path);
fs::create_dir_all(&directory)?;
let temp = temp_sibling(path);
if let Err(err) = write_temp_then_rename(&temp, path, bytes) {
let _ = fs::remove_file(&temp);
return Err(err);
}
let _ = File::open(&directory).and_then(|dir| dir.sync_all());
Ok(())
}
fn write_temp_then_rename(temp: &Path, path: &Path, bytes: &[u8]) -> io::Result<()> {
{
let mut file = File::create(temp)?;
file.write_all(bytes)?;
file.sync_all()?;
}
fs::rename(temp, path)
}
pub fn quarantine(path: &Path) -> io::Result<PathBuf> {
let target = directory_of(path).join(format!(
"{}.corrupt-{}",
file_name_of(path),
unique_suffix()
));
fs::rename(path, &target)?;
Ok(target)
}
fn temp_sibling(path: &Path) -> PathBuf {
directory_of(path).join(format!(".{}.tmp-{}", file_name_of(path), unique_suffix()))
}