1use crate::Result;
4
5#[derive(Clone)]
7pub enum UploadBackend {
8 Local {
9 directory: std::path::PathBuf,
10 },
11 R2 {
12 client: reqwest::Client,
13 account_id: String,
14 bucket: String,
15 api_token: String,
16 public_url: String,
17 },
18}
19
20impl UploadBackend {
21 fn validate_filename(filename: &str) -> Result<()> {
23 if filename.is_empty()
24 || filename.contains("..")
25 || filename.contains('/')
26 || filename.contains('\\')
27 || filename.contains('\0')
28 {
29 return Err(crate::Error::Upload(format!(
30 "Invalid filename: {:?}",
31 filename
32 )));
33 }
34 Ok(())
35 }
36
37 pub async fn put(&self, filename: &str, data: &[u8], content_type: &str) -> Result<String> {
39 Self::validate_filename(filename)?;
40 match self {
41 Self::Local { directory } => {
42 let path = directory.join(filename);
43 tokio::fs::write(&path, data).await?;
44 Ok(format!("/uploads/{}", filename))
45 }
46 Self::R2 {
47 client,
48 account_id,
49 bucket,
50 api_token,
51 public_url,
52 } => {
53 let url = format!(
54 "https://api.cloudflare.com/client/v4/accounts/{}/r2/buckets/{}/objects/{}",
55 account_id, bucket, filename
56 );
57
58 let resp = client
59 .put(&url)
60 .bearer_auth(api_token)
61 .header("Content-Type", content_type)
62 .body(data.to_vec())
63 .send()
64 .await
65 .map_err(|e| crate::Error::Upload(format!("R2 upload failed: {}", e)))?;
66
67 if !resp.status().is_success() {
68 let status = resp.status();
69 let body = resp.text().await.unwrap_or_default();
70 return Err(crate::Error::Upload(format!(
71 "R2 upload error ({}): {}",
72 status, body
73 )));
74 }
75
76 Ok(format!("{}/{}", public_url.trim_end_matches('/'), filename))
77 }
78 }
79 }
80
81 pub async fn delete(&self, filename: &str) -> Result<()> {
83 Self::validate_filename(filename)?;
84 match self {
85 Self::Local { directory } => {
86 let path = directory.join(filename);
87 if path.exists() {
88 tokio::fs::remove_file(&path).await?;
89 }
90 Ok(())
91 }
92 Self::R2 {
93 client,
94 account_id,
95 bucket,
96 api_token,
97 ..
98 } => {
99 let url = format!(
100 "https://api.cloudflare.com/client/v4/accounts/{}/r2/buckets/{}/objects/{}",
101 account_id, bucket, filename
102 );
103
104 let resp = client
105 .delete(&url)
106 .bearer_auth(api_token)
107 .send()
108 .await
109 .map_err(|e| crate::Error::Upload(format!("R2 delete failed: {}", e)))?;
110
111 if !resp.status().is_success() && resp.status().as_u16() != 404 {
112 let status = resp.status();
113 let body = resp.text().await.unwrap_or_default();
114 return Err(crate::Error::Upload(format!(
115 "R2 delete error ({}): {}",
116 status, body
117 )));
118 }
119
120 Ok(())
121 }
122 }
123 }
124}