use std::{
fs::File,
io::{
self,
Read,
Write,
},
path::PathBuf,
};
use automerge::{
Change,
ChangeHash,
};
use tempfile::NamedTempFile;
use thiserror::Error;
use crate::ChangeStore;
pub const FILE_EXTENSION: &str = "am.change";
#[derive(Debug, Error)]
pub enum Error {
#[error("invalid change")]
Change(#[from] automerge::LoadChangeError),
#[error(transparent)]
Tmp(#[from] tempfile::PersistError),
#[error(transparent)]
Io(#[from] io::Error),
}
#[derive(Clone, Debug)]
pub struct FileStore {
root: PathBuf,
}
impl FileStore {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
fn filename(&self, hash: &ChangeHash) -> PathBuf {
let mut buf = self.root.clone();
let hex = hex::encode(hash.0);
let (x, y) = hex.split_at(2);
buf.push(x);
buf.push(y);
buf.set_extension(FILE_EXTENSION);
buf
}
}
impl ChangeStore for FileStore {
type Error = Error;
fn write_change(&self, mut change: Change) -> Result<(), Self::Error> {
let hash = change.hash();
let compressed = change.bytes();
let file = self.filename(&hash);
let dir = file.parent().unwrap();
std::fs::create_dir_all(dir)?;
let mut tmp = NamedTempFile::new_in(dir)?;
tmp.write_all(&compressed)?;
tmp.persist(&file)?;
Ok(())
}
fn get_change(&self, hash: &ChangeHash) -> Result<Option<Change>, Self::Error> {
match File::open(self.filename(hash)) {
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
Ok(mut f) => {
let mut buf = Vec::new();
f.read_to_end(&mut buf)?;
Ok(Change::from_bytes(buf).map(Some)?)
},
}
}
fn has_change(&self, hash: &ChangeHash) -> Result<bool, Self::Error> {
Ok(self.filename(hash).try_exists()?)
}
}