use std::{fmt, io};
pub use dir::{Dir, DirEntry, Key, VERSIONS};
pub use file::{File, FileVersionRead, FileVersionWrite};
pub use hr_id::Id;
mod dir;
mod file;
pub enum Error {
Conflict(txn_lock::Error),
IO(io::Error),
NotFound(String),
Parse(hr_id::ParseError),
}
impl From<hr_id::ParseError> for Error {
fn from(cause: hr_id::ParseError) -> Self {
Self::Parse(cause)
}
}
impl From<io::Error> for Error {
fn from(cause: io::Error) -> Self {
Self::IO(cause)
}
}
impl From<txn_lock::Error> for Error {
fn from(cause: txn_lock::Error) -> Self {
Self::Conflict(cause)
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Conflict(cause) => cause.fmt(f),
Self::IO(cause) => cause.fmt(f),
Self::NotFound(locator) => write!(f, "not found: {locator}"),
Self::Parse(cause) => cause.fmt(f),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
impl std::error::Error for Error {}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use std::fmt;
use std::time::{SystemTime, UNIX_EPOCH};
use freqfs::Cache;
use freqfs::Name;
use get_size::GetSize;
use safecast::as_type;
use safecast::AsType;
use tokio::fs;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
struct TxnId(u64);
impl fmt::Display for TxnId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq<str> for TxnId {
fn eq(&self, other: &str) -> bool {
if let Ok(other) = other.parse() {
self.0 == other
} else {
false
}
}
}
impl PartialOrd<str> for TxnId {
fn partial_cmp(&self, other: &str) -> Option<Ordering> {
if let Ok(other) = other.parse() {
PartialOrd::partial_cmp(&self.0, &other)
} else {
None
}
}
}
impl Name for TxnId {
fn partial_cmp(&self, key: &str) -> Option<Ordering> {
Name::partial_cmp(&self.0, key)
}
}
#[derive(Clone)]
enum Entry {
Bin(Vec<u8>),
}
impl freqfs::FileLoad for Entry {
async fn load(
_path: &std::path::Path,
mut file: tokio::fs::File,
_metadata: std::fs::Metadata,
) -> std::io::Result<Self> {
let mut bytes = Vec::new();
file.read_to_end(&mut bytes).await?;
Ok(Self::Bin(bytes))
}
}
impl freqfs::FileSave for Entry {
async fn save(&self, file: &mut tokio::fs::File) -> std::io::Result<u64> {
match self {
Self::Bin(bytes) => {
file.write_all(bytes).await?;
Ok(bytes.len() as u64)
}
}
}
}
impl GetSize for Entry {
fn get_size(&self) -> usize {
match self {
Self::Bin(bytes) => bytes.get_size(),
}
}
}
impl AsType<Entry> for Entry {
fn as_type(&self) -> Option<&Entry> {
Some(self)
}
fn as_type_mut(&mut self) -> Option<&mut Entry> {
Some(self)
}
fn into_type(self) -> Option<Entry> {
Some(self)
}
}
as_type!(Entry, Bin, Vec<u8>);
#[test]
fn name_partial_cmp_accepts_str() {
let id = TxnId(5);
assert_eq!(Name::partial_cmp(&id, "5"), Some(Ordering::Equal));
assert_eq!(Name::partial_cmp(&id, "7"), Some(Ordering::Less));
assert_eq!(Name::partial_cmp(&id, "nope"), None);
}
#[tokio::test]
async fn file_roundtrip_persists_bytes() -> Result<(), Box<dyn std::error::Error>> {
let mut path = std::env::temp_dir();
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)?
.as_nanos();
path.push(format!("txfs_test_{}_{}", std::process::id(), unique));
fs::create_dir(&path).await?;
let cache = Cache::<Entry>::new(40, None);
let root = cache.load(path.clone())?;
let dir = super::Dir::load(TxnId(1), root).await?;
let name: super::Id = "file-one".parse()?;
let file = dir
.create_file(TxnId(1), name, Entry::Bin(vec![1u8, 2, 3]))
.await?;
let read = file.read::<Entry>(TxnId(1)).await?;
match &*read {
Entry::Bin(bytes) => assert_eq!(bytes.as_slice(), &[1u8, 2, 3]),
}
{
let mut write = file.write::<Entry>(TxnId(2)).await?;
*write = Entry::Bin(vec![9u8, 8]);
}
file.commit(TxnId(2)).await;
let read = file.read::<Entry>(TxnId(3)).await?;
match &*read {
Entry::Bin(bytes) => assert_eq!(bytes.as_slice(), &[9u8, 8]),
}
let _ = fs::remove_dir_all(&path).await;
Ok(())
}
}