use std::path::{Path, PathBuf};
use auths_id::ports::registry::RegistryError;
pub trait Vfs: Send + Sync {
fn read_file(&self, path: &Path) -> Result<Vec<u8>, RegistryError>;
fn atomic_write(&self, path: &Path, contents: &[u8]) -> Result<(), RegistryError>;
fn delete_file(&self, path: &Path) -> Result<(), RegistryError>;
fn exists(&self, path: &Path) -> bool;
}
pub struct OsVfs;
impl Vfs for OsVfs {
fn read_file(&self, path: &Path) -> Result<Vec<u8>, RegistryError> {
std::fs::read(path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
RegistryError::NotFound {
entity_type: "file".into(),
id: path.display().to_string(),
}
} else {
RegistryError::storage(e)
}
})
}
fn atomic_write(&self, path: &Path, contents: &[u8]) -> Result<(), RegistryError> {
let dir = path.parent().unwrap_or_else(|| Path::new("."));
write_atomically(dir, path, contents)
}
fn delete_file(&self, path: &Path) -> Result<(), RegistryError> {
std::fs::remove_file(path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
RegistryError::NotFound {
entity_type: "file".into(),
id: path.display().to_string(),
}
} else {
RegistryError::storage(e)
}
})
}
fn exists(&self, path: &Path) -> bool {
path.exists()
}
}
fn write_atomically(dir: &Path, final_path: &Path, contents: &[u8]) -> Result<(), RegistryError> {
use std::io::Write as _;
use tempfile::Builder;
let mut tmp = Builder::new()
.tempfile_in(dir)
.map_err(RegistryError::storage)?;
tmp.write_all(contents).map_err(RegistryError::storage)?;
tmp.flush().map_err(RegistryError::storage)?;
let (_, tmp_path) = tmp.keep().map_err(|e| RegistryError::storage(e.error))?;
persist_temp_file(&tmp_path, final_path)
}
fn persist_temp_file(tmp_path: &PathBuf, final_path: &Path) -> Result<(), RegistryError> {
match std::fs::rename(tmp_path, final_path) {
Ok(()) => Ok(()),
#[cfg(windows)]
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
std::fs::copy(tmp_path, final_path).map_err(RegistryError::storage)?;
let _ = std::fs::remove_file(tmp_path);
Ok(())
}
Err(e) => Err(RegistryError::storage(e)),
}
}
pub struct FixedClock {
at: chrono::DateTime<chrono::Utc>,
}
impl FixedClock {
pub fn new(at: chrono::DateTime<chrono::Utc>) -> Self {
Self { at }
}
}
impl auths_verifier::clock::ClockProvider for FixedClock {
fn now(&self) -> chrono::DateTime<chrono::Utc> {
self.at
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn os_vfs_round_trip() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test.json");
let vfs = OsVfs;
assert!(!vfs.exists(&path));
vfs.atomic_write(&path, b"{\"ok\":true}").unwrap();
assert!(vfs.exists(&path));
let bytes = vfs.read_file(&path).unwrap();
assert_eq!(bytes, b"{\"ok\":true}");
vfs.delete_file(&path).unwrap();
assert!(!vfs.exists(&path));
}
#[test]
fn atomic_write_is_idempotent() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("data.json");
let vfs = OsVfs;
vfs.atomic_write(&path, b"first").unwrap();
vfs.atomic_write(&path, b"second").unwrap();
let bytes = vfs.read_file(&path).unwrap();
assert_eq!(bytes, b"second");
}
#[test]
fn read_file_returns_not_found() {
let vfs = OsVfs;
let err = vfs
.read_file(Path::new("/nonexistent/path.json"))
.unwrap_err();
assert!(matches!(err, RegistryError::NotFound { .. }));
}
}