use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
pub trait DocStore {
fn exists(&self, path: &str) -> bool;
fn read(&self, path: &str) -> Result<Option<String>>;
fn write(&mut self, path: &str, bytes: &str) -> Result<()>;
fn rename(&mut self, from: &str, to: &str) -> Result<()>;
fn remove(&mut self, path: &str) -> Result<()>;
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MemDocStore {
files: BTreeMap<String, String>,
}
impl MemDocStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn files(&self) -> &BTreeMap<String, String> {
&self.files
}
}
impl DocStore for MemDocStore {
fn exists(&self, path: &str) -> bool {
self.files.contains_key(path)
}
fn read(&self, path: &str) -> Result<Option<String>> {
Ok(self.files.get(path).cloned())
}
fn write(&mut self, path: &str, bytes: &str) -> Result<()> {
self.files.insert(path.to_owned(), bytes.to_owned());
Ok(())
}
fn rename(&mut self, from: &str, to: &str) -> Result<()> {
let bytes = self
.files
.remove(from)
.ok_or_else(|| Error::Other(format!("rename: no file at {from}")))?;
self.files.insert(to.to_owned(), bytes);
Ok(())
}
fn remove(&mut self, path: &str) -> Result<()> {
self.files.remove(path);
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FsDocStore {
root: PathBuf,
}
impl FsDocStore {
#[must_use]
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
fn abs(&self, path: &str) -> PathBuf {
self.root.join(path)
}
}
fn io_err(what: &str, path: &Path, e: std::io::Error) -> Error {
Error::Other(format!("{what} {}: {e}", path.display()))
}
fn ensure_parent(abs: &Path) -> Result<()> {
if let Some(dir) = abs.parent().filter(|d| !d.as_os_str().is_empty()) {
fs::create_dir_all(dir).map_err(|e| io_err("cannot create", dir, e))?;
}
Ok(())
}
impl DocStore for FsDocStore {
fn exists(&self, path: &str) -> bool {
self.abs(path).exists()
}
fn read(&self, path: &str) -> Result<Option<String>> {
let abs = self.abs(path);
if !abs.exists() {
return Ok(None);
}
let bytes = fs::read(&abs).map_err(|e| io_err("cannot read", &abs, e))?;
Ok(Some(String::from_utf8_lossy(&bytes).into_owned()))
}
fn write(&mut self, path: &str, bytes: &str) -> Result<()> {
let abs = self.abs(path);
ensure_parent(&abs)?;
let mut tmp = abs.clone().into_os_string();
tmp.push(".omgtmp");
let tmp = PathBuf::from(tmp);
fs::write(&tmp, bytes).map_err(|e| io_err("cannot write", &tmp, e))?;
fs::rename(&tmp, &abs).map_err(|e| io_err("cannot rename into", &abs, e))
}
fn rename(&mut self, from: &str, to: &str) -> Result<()> {
let to_abs = self.abs(to);
ensure_parent(&to_abs)?;
fs::rename(self.abs(from), &to_abs).map_err(|e| io_err("cannot rename to", &to_abs, e))
}
fn remove(&mut self, path: &str) -> Result<()> {
let abs = self.abs(path);
if abs.exists() {
fs::remove_file(&abs).map_err(|e| io_err("cannot remove", &abs, e))?;
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct NullDocStore;
impl DocStore for NullDocStore {
fn exists(&self, _path: &str) -> bool {
false
}
fn read(&self, _path: &str) -> Result<Option<String>> {
Ok(None)
}
fn write(&mut self, _path: &str, _bytes: &str) -> Result<()> {
Ok(())
}
fn rename(&mut self, _from: &str, _to: &str) -> Result<()> {
Ok(())
}
fn remove(&mut self, _path: &str) -> Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mem_store_round_trips_and_renames() {
let mut m = MemDocStore::new();
assert!(!m.exists("a.md"));
m.write("a.md", "x\n").unwrap();
assert_eq!(m.read("a.md").unwrap().as_deref(), Some("x\n"));
m.rename("a.md", "dir/b.md").unwrap();
assert!(!m.exists("a.md") && m.exists("dir/b.md"));
assert!(m.rename("nope", "x").is_err());
m.remove("dir/b.md").unwrap();
m.remove("dir/b.md").unwrap();
assert!(m.files().is_empty());
}
#[test]
fn fs_store_writes_atomically_into_nested_dirs() {
let dir = std::env::temp_dir().join(format!(
"omgbase-fs-store-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
let mut s = FsDocStore::new(&dir);
assert_eq!(s.root(), dir.as_path());
s.write("a/b/c.md", "hello\n").unwrap();
assert!(s.exists("a/b/c.md"));
assert!(!dir.join("a/b/c.md.omgtmp").exists());
assert_eq!(s.read("a/b/c.md").unwrap().as_deref(), Some("hello\n"));
s.rename("a/b/c.md", "d/e.md").unwrap();
assert!(!s.exists("a/b/c.md"));
assert_eq!(s.read("d/e.md").unwrap().as_deref(), Some("hello\n"));
s.remove("d/e.md").unwrap();
assert_eq!(s.read("d/e.md").unwrap(), None);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn null_store_does_nothing() {
let mut n = NullDocStore;
n.write("a.md", "x").unwrap();
assert!(!n.exists("a.md"));
assert_eq!(n.read("a.md").unwrap(), None);
n.rename("a.md", "b.md").unwrap();
n.remove("b.md").unwrap();
}
}