use cgcx_config::StoragePaths; use cgcx_core::{ContentId, Result, CgcxError}; use std::path::{Path, PathBuf}; use tokio::fs; #[derive(Clone)] pub struct Storage { paths: StoragePaths, } impl Storage { pub fn new(paths: StoragePaths) -> Self { Self { paths } } pub async fn ensure_dirs(&self) -> Result<()> { for dir in [&self.paths.media, &self.paths.documents, &self.paths.text, &self.paths.temp] { fs::create_dir_all(dir).await.map_err(|e| CgcxError::Storage(format!("create dir {:?}: {}", dir, e)))?; } Ok(()) } pub fn media_dir(&self) -> &Path { &self.paths.media } pub fn documents_dir(&self) -> &Path { &self.paths.documents } pub fn text_dir(&self) -> &Path { &self.paths.text } pub fn temp_dir(&self) -> &Path { &self.paths.temp } pub fn content_dir(&self, content_id: &ContentId, mime_type: &str) -> PathBuf { let base = if mime_type.starts_with("image/") || mime_type.starts_with("video/") || mime_type.starts_with("audio/") { &self.paths.media } else if mime_type.starts_with("text/") { &self.paths.text } else { &self.paths.documents }; base.join(content_id.as_str()) } pub fn file_path(&self, content_id: &ContentId, file_index: u32, mime_type: &str) -> Result { let base = if mime_type.starts_with("image/") || mime_type.starts_with("video/") || mime_type.starts_with("audio/") { &self.paths.media } else if mime_type.starts_with("text/") { &self.paths.text } else { &self.paths.documents }; let dir = base.join(content_id.as_str()); let file_name = format!("{}_{:04}.enc", content_id.as_str(), file_index); let path = dir.join(file_name); if !path.starts_with(base) { return Err(CgcxError::Storage("path traversal detected".into())); } Ok(path) } pub fn temp_file(&self) -> Result { tempfile::NamedTempFile::new_in(&self.paths.temp) .map_err(|e| CgcxError::Storage(format!("create temp file: {}", e))) } pub async fn delete_content_files(&self, content_id: &ContentId, mime_type: &str) -> Result<()> { let dir = self.content_dir(content_id, mime_type); if dir.exists() { fs::remove_dir_all(&dir).await.map_err(|e| CgcxError::Storage(format!("remove dir {:?}: {}", dir, e)))?; } Ok(()) } pub async fn file_size(&self, path: &Path) -> Result { let meta = fs::metadata(path).await.map_err(|e| CgcxError::Storage(format!("metadata {:?}: {}", path, e)))?; Ok(meta.len()) } }