use super::types::{AsyncFileSystem, AsyncFileSystemConfig};
use crate::error::{Error, FileSystemError, Result};
use async_trait::async_trait;
use std::{
path::{Path, PathBuf},
time::Duration,
};
use tokio::{fs, time::timeout};
#[derive(Debug, Clone)]
pub struct FileSystemManager {
config: AsyncFileSystemConfig,
}
impl Default for FileSystemManager {
fn default() -> Self {
Self::new()
}
}
impl FileSystemManager {
#[must_use]
pub fn new() -> Self {
Self { config: AsyncFileSystemConfig::default() }
}
#[must_use]
pub fn with_config(config: AsyncFileSystemConfig) -> Self {
Self { config }
}
#[must_use]
pub fn with_standard_config(fs_config: &crate::config::FilesystemConfig) -> Self {
Self { config: AsyncFileSystemConfig::from(fs_config) }
}
#[must_use]
pub fn with_async_io_config(async_io_config: &crate::config::standard::AsyncIoConfig) -> Self {
Self { config: AsyncFileSystemConfig::from(async_io_config) }
}
#[must_use]
pub fn config(&self) -> &AsyncFileSystemConfig {
&self.config
}
async fn validate_path(&self, path: &Path) -> Result<()> {
if !self.exists(path).await {
return Err(Error::FileSystem(FileSystemError::NotFound { path: path.to_path_buf() }));
}
Ok(())
}
async fn with_timeout<T, F>(&self, operation: F, timeout_duration: Duration) -> Result<T>
where
F: std::future::Future<Output = Result<T>>,
{
match timeout(timeout_duration, operation).await {
Ok(result) => result,
Err(_) => Err(Error::FileSystem(FileSystemError::Operation(format!(
"Operation timed out after {timeout_duration:?}"
)))),
}
}
}
#[async_trait]
impl AsyncFileSystem for FileSystemManager {
async fn read_file(&self, path: &Path) -> Result<Vec<u8>> {
let operation = async {
self.validate_path(path).await?;
fs::read(path).await.map_err(|e| Error::FileSystem(FileSystemError::from_io(e, path)))
};
self.with_timeout(operation, self.config.read_timeout).await
}
async fn write_file(&self, path: &Path, contents: &[u8]) -> Result<()> {
let operation = async {
if let Some(parent) = path.parent()
&& !self.exists(parent).await
{
self.create_dir_all(parent).await?;
}
fs::write(path, contents)
.await
.map_err(|e| Error::FileSystem(FileSystemError::from_io(e, path)))?;
Ok(())
};
self.with_timeout(operation, self.config.write_timeout).await
}
async fn read_file_string(&self, path: &Path) -> Result<String> {
let operation = async {
self.validate_path(path).await?;
fs::read_to_string(path)
.await
.map_err(|e| Error::FileSystem(FileSystemError::from_io(e, path)))
};
self.with_timeout(operation, self.config.read_timeout).await
}
async fn write_file_string(&self, path: &Path, contents: &str) -> Result<()> {
let operation = async {
if let Some(parent) = path.parent()
&& !self.exists(parent).await
{
self.create_dir_all(parent).await?;
}
fs::write(path, contents)
.await
.map_err(|e| Error::FileSystem(FileSystemError::from_io(e, path)))?;
Ok(())
};
self.with_timeout(operation, self.config.write_timeout).await
}
async fn create_dir_all(&self, path: &Path) -> Result<()> {
let operation = async {
fs::create_dir_all(path)
.await
.map_err(|e| Error::FileSystem(FileSystemError::from_io(e, path)))?;
Ok(())
};
self.with_timeout(operation, self.config.operation_timeout).await
}
async fn remove(&self, path: &Path) -> Result<()> {
let operation = async {
self.validate_path(path).await?;
let metadata = fs::metadata(path)
.await
.map_err(|e| Error::FileSystem(FileSystemError::from_io(e, path)))?;
if metadata.is_dir() {
fs::remove_dir_all(path)
.await
.map_err(|e| Error::FileSystem(FileSystemError::from_io(e, path)))?;
} else {
fs::remove_file(path)
.await
.map_err(|e| Error::FileSystem(FileSystemError::from_io(e, path)))?;
}
Ok(())
};
self.with_timeout(operation, self.config.operation_timeout).await
}
async fn exists(&self, path: &Path) -> bool {
match fs::try_exists(path).await {
Ok(exists) => exists,
Err(e) => {
log::warn!(
"Failed to check existence of path {}: {}. Treating as non-existent.",
path.display(),
e
);
false
}
}
}
async fn read_dir(&self, path: &Path) -> Result<Vec<PathBuf>> {
let operation = async {
self.validate_path(path).await?;
let metadata = fs::metadata(path)
.await
.map_err(|e| Error::FileSystem(FileSystemError::from_io(e, path)))?;
if !metadata.is_dir() {
return Err(Error::FileSystem(FileSystemError::NotADirectory {
path: path.to_path_buf(),
}));
}
let mut entries = Vec::new();
let mut read_dir = fs::read_dir(path)
.await
.map_err(|e| Error::FileSystem(FileSystemError::from_io(e, path)))?;
while let Some(entry) = read_dir
.next_entry()
.await
.map_err(|e| Error::FileSystem(FileSystemError::from_io(e, path)))?
{
entries.push(entry.path());
}
entries.sort();
Ok(entries)
};
self.with_timeout(operation, self.config.operation_timeout).await
}
async fn walk_dir(&self, path: &Path) -> Result<Vec<PathBuf>> {
let operation = async {
self.validate_path(path).await?;
let mut paths = Vec::new();
Self::walk_recursive(path, &mut paths, self).await?;
paths.sort();
Ok(paths)
};
self.with_timeout(operation, self.config.operation_timeout).await
}
async fn metadata(&self, path: &Path) -> Result<std::fs::Metadata> {
let operation = async {
self.validate_path(path).await?;
fs::metadata(path)
.await
.map_err(|e| Error::FileSystem(FileSystemError::from_io(e, path)))
};
self.with_timeout(operation, self.config.operation_timeout).await
}
}
impl FileSystemManager {
fn walk_recursive<'a>(
path: &'a Path,
paths: &'a mut Vec<PathBuf>,
fs_manager: &'a FileSystemManager,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
let entries = fs_manager.read_dir(path).await?;
for entry in entries {
paths.push(entry.clone());
let metadata = fs::metadata(&entry)
.await
.map_err(|e| Error::FileSystem(FileSystemError::from_io(e, &entry)))?;
if metadata.is_dir() {
Self::walk_recursive(&entry, paths, fs_manager).await?;
}
}
Ok(())
})
}
}