use std::time::Duration; use uuid::Uuid; use storage::ObjectStorage; use crate::{ChannelError, ChannelResult}; const ATTACHMENT_KEY_PREFIX: &str = "attachments"; const DEFAULT_PRESIGNED_TTL: Duration = Duration::from_secs(3600); const DEFAULT_MAX_FILE_SIZE: u64 = 50 * 1024 * 1024; // 50 MB #[derive(Clone)] pub struct CdnManager { storage: storage::AppStorage, presigned_ttl: Duration, max_file_size: u64, } impl CdnManager { pub fn new(storage: storage::AppStorage) -> Self { Self { storage, presigned_ttl: DEFAULT_PRESIGNED_TTL, max_file_size: DEFAULT_MAX_FILE_SIZE, } } pub fn with_config( storage: storage::AppStorage, presigned_ttl: Duration, max_file_size: u64, ) -> Self { Self { storage, presigned_ttl, max_file_size, } } pub fn max_file_size(&self) -> u64 { self.max_file_size } pub async fn upload_file( &self, room_id: Uuid, message_id: Uuid, file_data: &[u8], filename: &str, content_type: Option, ) -> ChannelResult { if file_data.len() as u64 > self.max_file_size { return Err(ChannelError::Internal( "file exceeds max size".to_string(), )); } let filename = sanitize_filename(filename); let key = format!( "{}/{}/{}/{}", ATTACHMENT_KEY_PREFIX, room_id, message_id, filename ); let options = storage::PutObjectOptions { content_type, content_length: Some(file_data.len() as i64), cache_control: Some("public, max-age=86400".to_string()), }; let stored = self .storage .put_bytes(&key, file_data.to_vec(), options) .await?; Ok(CdnStoredFile { key: stored.key, url: stored.url, e_tag: stored.e_tag, size: file_data.len() as i64, }) } pub async fn get_file(&self, key: &str) -> ChannelResult { let object = self.storage.get_bytes(key).await?; Ok(CdnFileContent { bytes: object.bytes, content_type: object.content_type, content_length: object.content_length, }) } pub async fn delete_file(&self, key: &str) -> ChannelResult<()> { self.storage.delete(key).await?; Ok(()) } pub fn public_url(&self, key: &str) -> ChannelResult> { self.storage.public_url(key).map_err(Into::into) } pub async fn presigned_url(&self, key: &str) -> ChannelResult { Ok(self .storage .presigned_get_url(key, self.presigned_ttl) .await?) } } fn sanitize_filename(name: &str) -> String { let cleaned: String = name .chars() .filter(|&c| c.is_ascii_graphic() && c != '/' && c != '\\' && c != '\0') .take(255) .collect::() .trim() .to_owned(); if cleaned.is_empty() { uuid::Uuid::new_v4().to_string() } else { cleaned } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct CdnStoredFile { pub key: String, pub url: String, pub e_tag: Option, pub size: i64, } #[derive(Debug)] pub struct CdnFileContent { pub bytes: Vec, pub content_type: Option, pub content_length: Option, }