use config::AppConfig; use std::path::PathBuf; #[derive(Clone)] pub struct AppStorage { pub base_path: PathBuf, pub public_url_base: String, } impl AppStorage { pub fn new(config: &AppConfig) -> anyhow::Result { let base_path = config .env .get("STORAGE_PATH") .map(PathBuf::from) .unwrap_or_else(|| PathBuf::from("/data/files")); let public_url_base = config .env .get("STORAGE_PUBLIC_URL") .cloned() .unwrap_or_else(|| "/files".to_string()); Ok(Self { base_path, public_url_base, }) } /// Write data to a local path and return the public URL. pub async fn upload( &self, key: &str, data: Vec, ) -> anyhow::Result { let path = self.base_path.join(key); // Create parent directories if let Some(parent) = path.parent() { tokio::fs::create_dir_all(parent).await?; } tokio::fs::write(&path, &data).await?; let url = format!( "{}/{}", self.public_url_base.trim_end_matches('/'), key ); Ok(url) } pub async fn delete(&self, key: &str) -> anyhow::Result<()> { let path = self.base_path.join(key); if path.exists() { tokio::fs::remove_file(&path).await?; } Ok(()) } /// Read a file by key and return (bytes, content_type). pub async fn read(&self, key: &str) -> anyhow::Result<(Vec, String)> { let path = self.base_path.join(key); let data = tokio::fs::read(&path).await?; let content_type = mime_guess2::from_path(&path) .first_or_octet_stream() .to_string(); Ok((data, content_type)) } }