use crate::persistence::error::{PersistenceError, PersistenceResult};
use std::io::{Read, Write};
use std::path::PathBuf;
pub trait Directory: Send + Sync {
fn create_file(&self, path: &str) -> PersistenceResult<Box<dyn Write>>;
fn open_file(&self, path: &str) -> PersistenceResult<Box<dyn Read>>;
fn exists(&self, path: &str) -> bool;
fn delete(&self, path: &str) -> PersistenceResult<()>;
fn atomic_rename(&self, from: &str, to: &str) -> PersistenceResult<()>;
fn create_dir_all(&self, path: &str) -> PersistenceResult<()>;
fn list_dir(&self, path: &str) -> PersistenceResult<Vec<String>>;
fn append_file(&self, path: &str) -> PersistenceResult<Box<dyn Write>>;
fn atomic_write(&self, path: &str, data: &[u8]) -> PersistenceResult<()>;
fn file_path(&self, path: &str) -> Option<PathBuf>;
}
pub struct FsDirectory {
root: PathBuf,
}
impl FsDirectory {
pub fn new<P: Into<PathBuf>>(root: P) -> PersistenceResult<Self> {
let root = root.into();
std::fs::create_dir_all(&root)?;
Ok(Self { root })
}
pub fn root(&self) -> &PathBuf {
&self.root
}
fn resolve_path(&self, path: &str) -> PathBuf {
self.root.join(path)
}
}
impl Directory for FsDirectory {
fn create_file(&self, path: &str) -> PersistenceResult<Box<dyn Write>> {
let full_path = self.resolve_path(path);
if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::File::create(full_path)?;
Ok(Box::new(file))
}
fn open_file(&self, path: &str) -> PersistenceResult<Box<dyn Read>> {
let full_path = self.resolve_path(path);
let file = std::fs::File::open(full_path)?;
Ok(Box::new(file))
}
fn exists(&self, path: &str) -> bool {
self.resolve_path(path).exists()
}
fn delete(&self, path: &str) -> PersistenceResult<()> {
let full_path = self.resolve_path(path);
if full_path.is_dir() {
std::fs::remove_dir_all(full_path)?;
} else {
std::fs::remove_file(full_path)?;
}
Ok(())
}
fn atomic_rename(&self, from: &str, to: &str) -> PersistenceResult<()> {
let from_path = self.resolve_path(from);
let to_path = self.resolve_path(to);
if let Some(parent) = to_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::rename(from_path, to_path)?;
Ok(())
}
fn create_dir_all(&self, path: &str) -> PersistenceResult<()> {
let full_path = self.resolve_path(path);
std::fs::create_dir_all(full_path)?;
Ok(())
}
fn list_dir(&self, path: &str) -> PersistenceResult<Vec<String>> {
let full_path = self.resolve_path(path);
let entries = std::fs::read_dir(full_path)?;
let mut paths = Vec::new();
for entry in entries {
let entry = entry?;
let file_name = entry.file_name();
paths.push(file_name.to_string_lossy().to_string());
}
Ok(paths)
}
fn append_file(&self, path: &str) -> PersistenceResult<Box<dyn Write>> {
let full_path = self.resolve_path(path);
if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(full_path)?;
Ok(Box::new(file))
}
fn atomic_write(&self, path: &str, data: &[u8]) -> PersistenceResult<()> {
let temp_path = format!("{}.tmp", path);
let full_temp_path = self.resolve_path(&temp_path);
if let Some(parent) = full_temp_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut temp_file = std::fs::File::create(&full_temp_path)?;
temp_file.write_all(data)?;
temp_file.sync_all()?;
let full_path = self.resolve_path(path);
std::fs::rename(&full_temp_path, &full_path)?;
if let Some(parent) = full_path.parent() {
if let Ok(parent_file) = std::fs::File::open(parent) {
let _ = parent_file.sync_all(); }
}
Ok(())
}
fn file_path(&self, path: &str) -> Option<PathBuf> {
Some(self.resolve_path(path))
}
}
#[derive(Clone)]
pub struct MemoryDirectory {
files: std::sync::Arc<std::sync::RwLock<std::collections::HashMap<String, Vec<u8>>>>,
}
impl MemoryDirectory {
pub fn new() -> Self {
Self {
files: std::sync::Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())),
}
}
}
impl Default for MemoryDirectory {
fn default() -> Self {
Self::new()
}
}
impl Directory for MemoryDirectory {
fn create_file(&self, path: &str) -> PersistenceResult<Box<dyn Write>> {
let files = self.files.clone();
let path = path.to_string();
Ok(Box::new(MemoryWriter {
files,
path,
buffer: Vec::new(),
}))
}
fn open_file(&self, path: &str) -> PersistenceResult<Box<dyn Read>> {
let files = self
.files
.read()
.map_err(|_| PersistenceError::LockFailed {
resource: "memory files".into(),
reason: "lock poisoned".into(),
})?;
let data = files
.get(path)
.ok_or_else(|| PersistenceError::NotFound(path.to_string()))?
.clone();
Ok(Box::new(std::io::Cursor::new(data)))
}
fn exists(&self, path: &str) -> bool {
self.files
.read()
.map(|f| f.contains_key(path))
.unwrap_or(false)
}
fn delete(&self, path: &str) -> PersistenceResult<()> {
self.files
.write()
.map_err(|_| PersistenceError::LockFailed {
resource: "memory files".into(),
reason: "lock poisoned".into(),
})?
.remove(path);
Ok(())
}
fn atomic_rename(&self, from: &str, to: &str) -> PersistenceResult<()> {
let mut files = self
.files
.write()
.map_err(|_| PersistenceError::LockFailed {
resource: "memory files".into(),
reason: "lock poisoned".into(),
})?;
if let Some(data) = files.remove(from) {
files.insert(to.to_string(), data);
}
Ok(())
}
fn create_dir_all(&self, _path: &str) -> PersistenceResult<()> {
Ok(())
}
fn list_dir(&self, path: &str) -> PersistenceResult<Vec<String>> {
let files = self
.files
.read()
.map_err(|_| PersistenceError::LockFailed {
resource: "memory files".into(),
reason: "lock poisoned".into(),
})?;
let prefix = if path.is_empty() {
"".to_string()
} else {
format!("{}/", path)
};
let mut result: Vec<String> = files
.keys()
.filter(|k| k.starts_with(&prefix))
.map(|k| {
k.strip_prefix(&prefix).unwrap_or(k).to_string()
})
.collect();
result.sort();
Ok(result)
}
fn append_file(&self, path: &str) -> PersistenceResult<Box<dyn Write>> {
let files = self.files.clone();
let path = path.to_string();
let existing = self
.files
.read()
.map_err(|_| PersistenceError::LockFailed {
resource: "memory files".into(),
reason: "lock poisoned".into(),
})?
.get(&path)
.cloned()
.unwrap_or_default();
Ok(Box::new(MemoryAppendWriter {
files,
path,
buffer: existing,
}))
}
fn atomic_write(&self, path: &str, data: &[u8]) -> PersistenceResult<()> {
let mut files = self
.files
.write()
.map_err(|_| PersistenceError::LockFailed {
resource: "memory files".into(),
reason: "lock poisoned".into(),
})?;
files.insert(path.to_string(), data.to_vec());
Ok(())
}
fn file_path(&self, _path: &str) -> Option<PathBuf> {
None
}
}
struct MemoryWriter {
files: std::sync::Arc<std::sync::RwLock<std::collections::HashMap<String, Vec<u8>>>>,
path: String,
buffer: Vec<u8>,
}
impl Write for MemoryWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.buffer.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
let mut files = self
.files
.write()
.map_err(|_| std::io::Error::other("lock poisoned"))?;
files.insert(self.path.clone(), self.buffer.clone());
Ok(())
}
}
impl Drop for MemoryWriter {
fn drop(&mut self) {
let _ = self.flush();
}
}
struct MemoryAppendWriter {
files: std::sync::Arc<std::sync::RwLock<std::collections::HashMap<String, Vec<u8>>>>,
path: String,
buffer: Vec<u8>,
}
impl Write for MemoryAppendWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.buffer.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
let mut files = self
.files
.write()
.map_err(|_| std::io::Error::other("lock poisoned"))?;
files.insert(self.path.clone(), self.buffer.clone());
Ok(())
}
}
impl Drop for MemoryAppendWriter {
fn drop(&mut self) {
let _ = self.flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fs_directory() {
let temp_dir = std::env::temp_dir().join(format!("jin_test_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&temp_dir);
let dir = FsDirectory::new(&temp_dir).unwrap();
let mut file = dir.create_file("test.txt").unwrap();
file.write_all(b"hello").unwrap();
drop(file);
let mut file = dir.open_file("test.txt").unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
assert_eq!(contents, "hello");
assert!(dir.exists("test.txt"));
dir.atomic_rename("test.txt", "renamed.txt").unwrap();
assert!(!dir.exists("test.txt"));
assert!(dir.exists("renamed.txt"));
dir.delete("renamed.txt").unwrap();
assert!(!dir.exists("renamed.txt"));
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_memory_directory() {
let dir = MemoryDirectory::new();
let mut file = dir.create_file("test.txt").unwrap();
file.write_all(b"hello").unwrap();
file.flush().unwrap();
let mut file = dir.open_file("test.txt").unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
assert_eq!(contents, "hello");
assert!(dir.exists("test.txt"));
dir.atomic_rename("test.txt", "renamed.txt").unwrap();
assert!(!dir.exists("test.txt"));
assert!(dir.exists("renamed.txt"));
dir.delete("renamed.txt").unwrap();
assert!(!dir.exists("renamed.txt"));
}
}