use std::fmt::Write as _;
use std::fs;
use std::io::{self, Read};
use std::os::unix::fs::DirBuilderExt;
use std::path::{Path, PathBuf};
use chrono::prelude::*;
use nix::errno::Errno;
use tiny_keccak::{Hasher, Sha3};
use crate::support::error::Error;
pub struct MessageStore {
root: PathBuf,
}
impl MessageStore {
pub fn new(root: PathBuf) -> Self {
Self { root }
}
pub fn canonical_path(mut data: impl Read) -> io::Result<PathBuf> {
let mut buf = [0u8; 4096];
let mut sha3 = Sha3::v256();
loop {
let nread = data.read(&mut buf)?;
if 0 == nread {
break;
}
sha3.update(&buf[..nread]);
}
let mut hash = [0u8; 32];
sha3.finalize(&mut hash);
let mut path = String::with_capacity(2 * hash.len() + 1);
let _ = write!(path, "{:02x}/", hash[0]);
for &b in &hash[1..] {
let _ = write!(path, "{:02x}", b);
}
Ok(path.into())
}
pub fn insert(&self, src: &Path, dst: &Path) -> Result<(), Error> {
let dst = self.root.join(dst);
if let Some(parent) = dst.parent() {
fs::DirBuilder::new()
.recursive(true)
.mode(0o770)
.create(parent)?;
}
match nix::unistd::linkat(
None,
src,
None,
&dst,
nix::unistd::LinkatFlags::SymlinkFollow,
) {
Ok(()) | Err(Errno::EEXIST) => Ok(()),
Err(e) => Err(e.into()),
}
}
pub fn open(&self, path: &Path) -> io::Result<fs::File> {
if path.is_absolute() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"unexpected absolute path",
));
}
fs::File::open(self.root.join(path))
}
pub fn delete(&self, path: &Path) -> io::Result<()> {
if path.is_absolute() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"unexpected absolute path",
));
}
fs::remove_file(self.root.join(path))
}
pub fn list(
&self,
modified_before: Option<DateTime<Utc>>,
) -> impl Iterator<Item = PathBuf> + '_ {
walkdir::WalkDir::new(&self.root)
.into_iter()
.filter_map(|e| e.ok())
.filter(move |e| {
e.file_type().is_file()
&& modified_before.is_none_or(|modified_before| {
e.metadata()
.ok()
.and_then(|md| md.modified().ok())
.is_some_and(|mt| {
DateTime::<Utc>::from(mt) < modified_before
})
})
})
.map(move |e| e.path().strip_prefix(&self.root).unwrap().to_owned())
}
}
#[cfg(test)]
mod test {
use std::time::Duration;
use super::*;
use tempfile::TempDir;
#[test]
fn test_message_store() {
let root = TempDir::new().unwrap();
let store = MessageStore::new(root.path().join("messages"));
let tmp = root.path().join("tmp");
fs::create_dir(&tmp).unwrap();
let foo_path = tmp.join("foo");
fs::write(&foo_path, b"foo").unwrap();
let bar_path = tmp.join("bar");
fs::write(&bar_path, b"bar").unwrap();
let baz_path = tmp.join("baz");
fs::write(&baz_path, b"baz").unwrap();
let foo_canonical =
MessageStore::canonical_path(fs::File::open(&foo_path).unwrap())
.unwrap();
let bar_canonical =
MessageStore::canonical_path(fs::File::open(&bar_path).unwrap())
.unwrap();
let baz_canonical =
MessageStore::canonical_path(fs::File::open(&baz_path).unwrap())
.unwrap();
store.insert(&foo_path, &foo_canonical).unwrap();
store.insert(&bar_path, &bar_canonical).unwrap();
store.insert(&baz_path, &baz_canonical).unwrap();
let mut buf = Vec::<u8>::new();
store
.open(&foo_canonical)
.unwrap()
.read_to_end(&mut buf)
.unwrap();
assert_eq!(b"foo", &buf[..]);
buf.clear();
store
.open(&bar_canonical)
.unwrap()
.read_to_end(&mut buf)
.unwrap();
assert_eq!(b"bar", &buf[..]);
store.insert(&baz_path, &foo_canonical).unwrap();
buf.clear();
store
.open(&foo_canonical)
.unwrap()
.read_to_end(&mut buf)
.unwrap();
assert_eq!(b"foo", &buf[..]);
let mut listed = store
.list(Some(Utc::now() + Duration::from_secs(5)))
.collect::<Vec<_>>();
assert_eq!(3, listed.len());
assert!(listed.contains(&foo_canonical));
assert!(listed.contains(&bar_canonical));
assert!(listed.contains(&baz_canonical));
assert_eq!(3, store.list(None).count());
listed = store
.list(Some(DateTime::from_timestamp(0, 0).unwrap()))
.collect::<Vec<_>>();
assert_eq!(0, listed.len());
store.delete(&bar_canonical).unwrap();
assert!(store.open(&bar_canonical).is_err());
listed = store
.list(Some(Utc::now() + Duration::from_secs(5)))
.collect::<Vec<_>>();
assert_eq!(2, listed.len());
assert!(listed.contains(&foo_canonical));
assert!(listed.contains(&baz_canonical));
}
}