use super::Metadata;
use crate::infrastructure::{FileError, FileSystem};
use async_trait::async_trait;
use std::{
fs::{exists, metadata, read_to_string},
io,
path::Path,
};
use tokio::{
fs::{create_dir_all, read, remove_file},
sync::Semaphore,
};
#[derive(Debug)]
pub struct OsFileSystem {
semaphore: Semaphore,
}
impl OsFileSystem {
pub fn new(open_file_limit: usize) -> Self {
Self {
semaphore: Semaphore::new(open_file_limit.min(Semaphore::MAX_PERMITS)),
}
}
fn error(error: io::Error, path: &Path) -> FileError {
FileError::new(format!("{}: {}", error, path.display()))
}
}
#[async_trait]
impl FileSystem for OsFileSystem {
async fn read_file(&self, path: &Path) -> Result<Vec<u8>, FileError> {
let _permit = self.semaphore.acquire().await?;
read(path).await.map_err(|error| Self::error(error, path))
}
async fn read_file_to_string(&self, path: &Path) -> Result<String, FileError> {
read_to_string(path).map_err(|error| Self::error(error, path))
}
async fn exists(&self, path: &Path) -> Result<bool, FileError> {
exists(path).map_err(|error| Self::error(error, path))
}
async fn metadata(&self, path: &Path) -> Result<Option<Metadata>, FileError> {
match metadata(path) {
Ok(metadata) => Ok(Some(metadata.try_into()?)),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(Self::error(error, path)),
}
}
async fn create_directory(&self, path: &Path) -> Result<(), FileError> {
create_dir_all(path)
.await
.map_err(|error| Self::error(error, path))?;
Ok(())
}
async fn remove_file(&self, path: &Path) -> Result<(), FileError> {
remove_file(path)
.await
.map_err(|error| Self::error(error, path))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::fs::{metadata, write};
use tempfile::tempdir;
#[test]
fn limit_open_files() {
assert_eq!(OsFileSystem::new(42).semaphore.available_permits(), 42);
}
#[test]
fn limit_open_files_to_maximum_permits() {
assert_eq!(
OsFileSystem::new(usize::MAX).semaphore.available_permits(),
Semaphore::MAX_PERMITS
);
}
#[tokio::test]
async fn read_file() {
let directory = tempdir().unwrap();
let path = directory.path().join("foo");
write(&path, "bar").unwrap();
assert_eq!(OsFileSystem::new(1).read_file(&path).await.unwrap(), b"bar");
}
#[tokio::test]
async fn fail_to_read_missing_file() {
let directory = tempdir().unwrap();
let path = directory.path().join("foo");
assert!(
OsFileSystem::new(1)
.read_file(&path)
.await
.unwrap_err()
.to_string()
.contains(&path.display().to_string())
);
}
#[tokio::test]
async fn read_file_to_string() {
let directory = tempdir().unwrap();
let path = directory.path().join("foo");
write(&path, "bar").unwrap();
assert_eq!(
OsFileSystem::new(1)
.read_file_to_string(&path)
.await
.unwrap(),
"bar"
);
}
#[tokio::test]
async fn fail_to_read_missing_file_to_string() {
let directory = tempdir().unwrap();
let path = directory.path().join("foo");
assert!(
OsFileSystem::new(1)
.read_file_to_string(&path)
.await
.unwrap_err()
.to_string()
.contains(&path.display().to_string())
);
}
#[tokio::test]
async fn check_file_existence() {
let directory = tempdir().unwrap();
let path = directory.path().join("foo");
let file_system = OsFileSystem::new(1);
assert!(!file_system.exists(&path).await.unwrap());
write(&path, "").unwrap();
assert!(file_system.exists(&path).await.unwrap());
}
#[tokio::test]
async fn get_metadata_of_file() {
let directory = tempdir().unwrap();
let path = directory.path().join("foo");
write(&path, "").unwrap();
assert_eq!(
OsFileSystem::new(1).metadata(&path).await.unwrap(),
Some(Metadata::new(
metadata(&path).unwrap().modified().unwrap(),
false
))
);
}
#[tokio::test]
async fn get_metadata_of_directory() {
let directory = tempdir().unwrap();
assert_eq!(
OsFileSystem::new(1)
.metadata(directory.path())
.await
.unwrap(),
Some(Metadata::new(
metadata(directory.path()).unwrap().modified().unwrap(),
true
))
);
}
#[tokio::test]
async fn get_metadata_of_missing_file() {
let directory = tempdir().unwrap();
assert_eq!(
OsFileSystem::new(1)
.metadata(&directory.path().join("foo"))
.await,
Ok(None)
);
}
#[cfg(unix)]
#[tokio::test]
async fn fail_to_get_metadata_under_file() {
let directory = tempdir().unwrap();
let path = directory.path().join("foo");
write(&path, "").unwrap();
assert!(
OsFileSystem::new(1)
.metadata(&path.join("bar"))
.await
.unwrap_err()
.to_string()
.contains(&path.join("bar").display().to_string())
);
}
#[tokio::test]
async fn create_directory() {
let directory = tempdir().unwrap();
let path = directory.path().join("foo").join("bar");
OsFileSystem::new(1).create_directory(&path).await.unwrap();
assert!(metadata(&path).unwrap().is_dir());
}
#[tokio::test]
async fn remove_file() {
let directory = tempdir().unwrap();
let path = directory.path().join("foo");
write(&path, "").unwrap();
OsFileSystem::new(1).remove_file(&path).await.unwrap();
assert!(!path.try_exists().unwrap());
}
}