137 lines
3.5 KiB
Rust
137 lines
3.5 KiB
Rust
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<String>,
|
|
) -> ChannelResult<CdnStoredFile> {
|
|
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<CdnFileContent> {
|
|
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<Option<String>> {
|
|
self.storage.public_url(key).map_err(Into::into)
|
|
}
|
|
|
|
pub async fn presigned_url(&self, key: &str) -> ChannelResult<String> {
|
|
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::<String>()
|
|
.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<String>,
|
|
pub size: i64,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct CdnFileContent {
|
|
pub bytes: Vec<u8>,
|
|
pub content_type: Option<String>,
|
|
pub content_length: Option<i64>,
|
|
}
|