use crate::Result;
#[derive(Clone)]
pub enum UploadBackend {
Local {
directory: std::path::PathBuf,
},
R2 {
client: reqwest::Client,
account_id: String,
bucket: String,
api_token: String,
public_url: String,
},
}
impl UploadBackend {
fn validate_filename(filename: &str) -> Result<()> {
if filename.is_empty()
|| filename.contains("..")
|| filename.contains('/')
|| filename.contains('\\')
|| filename.contains('\0')
{
return Err(crate::Error::Upload(format!(
"Invalid filename: {:?}",
filename
)));
}
Ok(())
}
pub async fn put(&self, filename: &str, data: &[u8], content_type: &str) -> Result<String> {
Self::validate_filename(filename)?;
match self {
Self::Local { directory } => {
let path = directory.join(filename);
tokio::fs::write(&path, data).await?;
Ok(format!("/uploads/{}", filename))
}
Self::R2 {
client,
account_id,
bucket,
api_token,
public_url,
} => {
let url = format!(
"https://api.cloudflare.com/client/v4/accounts/{}/r2/buckets/{}/objects/{}",
account_id, bucket, filename
);
let resp = client
.put(&url)
.bearer_auth(api_token)
.header("Content-Type", content_type)
.body(data.to_vec())
.send()
.await
.map_err(|e| crate::Error::Upload(format!("R2 upload failed: {}", e)))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(crate::Error::Upload(format!(
"R2 upload error ({}): {}",
status, body
)));
}
Ok(format!("{}/{}", public_url.trim_end_matches('/'), filename))
}
}
}
pub async fn delete(&self, filename: &str) -> Result<()> {
Self::validate_filename(filename)?;
match self {
Self::Local { directory } => {
let path = directory.join(filename);
if path.exists() {
tokio::fs::remove_file(&path).await?;
}
Ok(())
}
Self::R2 {
client,
account_id,
bucket,
api_token,
..
} => {
let url = format!(
"https://api.cloudflare.com/client/v4/accounts/{}/r2/buckets/{}/objects/{}",
account_id, bucket, filename
);
let resp = client
.delete(&url)
.bearer_auth(api_token)
.send()
.await
.map_err(|e| crate::Error::Upload(format!("R2 delete failed: {}", e)))?;
if !resp.status().is_success() && resp.status().as_u16() != 404 {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(crate::Error::Upload(format!(
"R2 delete error ({}): {}",
status, body
)));
}
Ok(())
}
}
}
}