100 lines
3.3 KiB
Rust
100 lines
3.3 KiB
Rust
use async_trait::async_trait;
|
|
use bytes::Bytes;
|
|
use domain::errors::DomainError;
|
|
use domain::ports::{FileEntry, FileStoragePort};
|
|
use std::path::PathBuf;
|
|
|
|
pub struct LocalFileStorage {
|
|
base_path: PathBuf,
|
|
}
|
|
|
|
impl LocalFileStorage {
|
|
pub fn new(base_path: impl Into<PathBuf>) -> Self {
|
|
Self {
|
|
base_path: base_path.into(),
|
|
}
|
|
}
|
|
|
|
fn resolve(&self, path: &str) -> Result<PathBuf, DomainError> {
|
|
let full = self.base_path.join(path);
|
|
// Prevent path traversal
|
|
if !full.starts_with(&self.base_path) {
|
|
return Err(DomainError::Validation(
|
|
"Path traversal not allowed".to_string(),
|
|
));
|
|
}
|
|
Ok(full)
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl FileStoragePort for LocalFileStorage {
|
|
async fn store_file(&self, path: &str, data: Bytes) -> Result<(), DomainError> {
|
|
let full = self.resolve(path)?;
|
|
if let Some(parent) = full.parent() {
|
|
tokio::fs::create_dir_all(parent)
|
|
.await
|
|
.map_err(|e| DomainError::Internal(format!("Failed to create dirs: {e}")))?;
|
|
}
|
|
tokio::fs::write(&full, &data)
|
|
.await
|
|
.map_err(|e| DomainError::Internal(format!("Failed to write file: {e}")))?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn read_file(&self, path: &str) -> Result<Bytes, DomainError> {
|
|
let full = self.resolve(path)?;
|
|
let data = tokio::fs::read(&full).await.map_err(|e| match e.kind() {
|
|
std::io::ErrorKind::NotFound => DomainError::NotFound(path.to_string()),
|
|
_ => DomainError::Internal(format!("Failed to read file: {e}")),
|
|
})?;
|
|
Ok(Bytes::from(data))
|
|
}
|
|
|
|
async fn delete_file(&self, path: &str) -> Result<(), DomainError> {
|
|
let full = self.resolve(path)?;
|
|
match tokio::fs::remove_file(&full).await {
|
|
Ok(()) => Ok(()),
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
|
Err(e) => Err(DomainError::Internal(format!("Failed to delete file: {e}"))),
|
|
}
|
|
}
|
|
|
|
async fn list_directory(&self, path: &str) -> Result<Vec<FileEntry>, DomainError> {
|
|
let full = self.resolve(path)?;
|
|
let mut entries = Vec::new();
|
|
let mut read_dir = tokio::fs::read_dir(&full)
|
|
.await
|
|
.map_err(|e| match e.kind() {
|
|
std::io::ErrorKind::NotFound => DomainError::NotFound(path.to_string()),
|
|
_ => DomainError::Internal(format!("Failed to read dir: {e}")),
|
|
})?;
|
|
while let Some(entry) = read_dir
|
|
.next_entry()
|
|
.await
|
|
.map_err(|e| DomainError::Internal(e.to_string()))?
|
|
{
|
|
let meta = entry
|
|
.metadata()
|
|
.await
|
|
.map_err(|e| DomainError::Internal(e.to_string()))?;
|
|
entries.push(FileEntry {
|
|
path: entry.file_name().to_string_lossy().to_string(),
|
|
size_bytes: meta.len(),
|
|
is_directory: meta.is_dir(),
|
|
});
|
|
}
|
|
Ok(entries)
|
|
}
|
|
|
|
async fn file_exists(&self, path: &str) -> Result<bool, DomainError> {
|
|
let full = self.resolve(path)?;
|
|
Ok(full.exists())
|
|
}
|
|
|
|
async fn available_space(&self) -> Result<u64, DomainError> {
|
|
// Simple stub: return a large number
|
|
Ok(u64::MAX)
|
|
}
|
|
}
|