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) -> Self { Self { base_path: base_path.into(), } } fn resolve(&self, path: &str) -> Result { 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 { 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, 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 { let full = self.resolve(path)?; Ok(full.exists()) } async fn available_space(&self) -> Result { // Simple stub: return a large number Ok(u64::MAX) } }