1use axum::{
9 body::Body,
10 extract::{Path, State},
11 http::{header, StatusCode},
12 response::{IntoResponse, Response},
13 routing::{get, post},
14 Router,
15};
16use serde::Serialize;
17use std::collections::HashMap;
18use std::sync::{Arc, RwLock};
19
20const MAX_BLOB_SIZE: usize = 10 * 1024 * 1024;
22
23#[derive(Clone)]
25pub struct BlobStorage {
26 blobs: Arc<RwLock<HashMap<String, BlobData>>>,
27}
28
29#[derive(Clone)]
31pub struct BlobData {
32 data: Vec<u8>,
33 content_type: String,
34}
35
36impl BlobStorage {
37 pub fn new() -> Self {
39 Self {
40 blobs: Arc::new(RwLock::new(HashMap::new())),
41 }
42 }
43
44 pub fn store(&self, blob_id: String, data: Vec<u8>, content_type: String) {
46 let blob_data = BlobData { data, content_type };
47 if let Ok(mut blobs) = self.blobs.write() {
48 blobs.insert(blob_id, blob_data);
49 }
50 }
51
52 pub fn get(&self, blob_id: &str) -> Option<BlobData> {
54 self.blobs
55 .read()
56 .ok()
57 .and_then(|blobs| blobs.get(blob_id).cloned())
58 }
59
60 pub fn size(&self, blob_id: &str) -> Option<usize> {
62 self.blobs
63 .read()
64 .ok()
65 .and_then(|blobs| blobs.get(blob_id).map(|b| b.data.len()))
66 }
67}
68
69impl Default for BlobStorage {
70 fn default() -> Self {
71 Self::new()
72 }
73}
74
75#[derive(Debug, Serialize)]
77#[serde(rename_all = "camelCase")]
78pub struct UploadResponse {
79 pub account_id: String,
80 pub blob_id: String,
81 #[serde(rename = "type")]
82 pub content_type: String,
83 pub size: usize,
84}
85
86#[derive(Debug, Serialize)]
88#[serde(rename_all = "camelCase")]
89pub struct UploadError {
90 #[serde(rename = "type")]
91 pub error_type: String,
92 pub status: u16,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub detail: Option<String>,
95}
96
97pub fn blob_routes() -> Router<BlobStorage> {
99 Router::new()
100 .route("/download/:account/:blob/:name", get(download_blob))
101 .route("/upload/:account", post(upload_blob))
102}
103
104async fn download_blob(
106 Path((account, blob_id, name)): Path<(String, String, String)>,
107 State(storage): State<BlobStorage>,
108) -> Response {
109 if account.is_empty() {
111 return (StatusCode::BAD_REQUEST, "Invalid account ID".to_string()).into_response();
112 }
113
114 match storage.get(&blob_id) {
116 Some(blob_data) => {
117 match Response::builder()
119 .status(StatusCode::OK)
120 .header(header::CONTENT_TYPE, blob_data.content_type)
121 .header(
122 header::CONTENT_DISPOSITION,
123 format!("attachment; filename=\"{}\"", name),
124 )
125 .header(header::CONTENT_LENGTH, blob_data.data.len())
126 .body(Body::from(blob_data.data))
127 {
128 Ok(response) => response,
129 Err(e) => (
130 StatusCode::INTERNAL_SERVER_ERROR,
131 format!("Failed to build response: {}", e),
132 )
133 .into_response(),
134 }
135 }
136 None => (StatusCode::NOT_FOUND, "Blob not found".to_string()).into_response(),
137 }
138}
139
140async fn upload_blob(
142 Path(account): Path<String>,
143 State(storage): State<BlobStorage>,
144 headers: axum::http::HeaderMap,
145 body: axum::body::Bytes,
146) -> Response {
147 if account.is_empty() {
149 let error = UploadError {
150 error_type: "urn:ietf:params:jmap:error:invalidArguments".to_string(),
151 status: 400,
152 detail: Some("Invalid account ID".to_string()),
153 };
154 return (StatusCode::BAD_REQUEST, axum::Json(error)).into_response();
155 }
156
157 if body.len() > MAX_BLOB_SIZE {
159 let error = UploadError {
160 error_type: "urn:ietf:params:jmap:error:tooLarge".to_string(),
161 status: 413,
162 detail: Some(format!(
163 "Blob size {} exceeds maximum of {}",
164 body.len(),
165 MAX_BLOB_SIZE
166 )),
167 };
168 return (StatusCode::PAYLOAD_TOO_LARGE, axum::Json(error)).into_response();
169 }
170
171 let content_type = headers
173 .get(header::CONTENT_TYPE)
174 .and_then(|v| v.to_str().ok())
175 .unwrap_or("application/octet-stream")
176 .to_string();
177
178 let blob_id = generate_blob_id(&body);
180
181 let data = body.to_vec();
183 let size = data.len();
184 storage.store(blob_id.clone(), data, content_type.clone());
185
186 let response = UploadResponse {
188 account_id: account,
189 blob_id,
190 content_type,
191 size,
192 };
193
194 (StatusCode::CREATED, axum::Json(response)).into_response()
195}
196
197fn generate_blob_id(data: &[u8]) -> String {
199 use sha2::{Digest, Sha256};
200
201 let mut hasher = Sha256::new();
202 hasher.update(data);
203 let result = hasher.finalize();
204 format!("G{:x}", result)
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_blob_storage_store_and_get() {
213 let storage = BlobStorage::new();
214 let data = b"test data".to_vec();
215 let blob_id = "blob123".to_string();
216
217 storage.store(blob_id.clone(), data.clone(), "text/plain".to_string());
218
219 let retrieved = storage.get(&blob_id).unwrap();
220 assert_eq!(retrieved.data, data);
221 assert_eq!(retrieved.content_type, "text/plain");
222 }
223
224 #[test]
225 fn test_blob_storage_size() {
226 let storage = BlobStorage::new();
227 let data = b"test data".to_vec();
228 let blob_id = "blob123".to_string();
229
230 storage.store(blob_id.clone(), data.clone(), "text/plain".to_string());
231
232 assert_eq!(storage.size(&blob_id), Some(9));
233 }
234
235 #[test]
236 fn test_blob_storage_get_nonexistent() {
237 let storage = BlobStorage::new();
238 assert!(storage.get("nonexistent").is_none());
239 }
240
241 #[test]
242 fn test_generate_blob_id() {
243 let data1 = b"test data";
244 let data2 = b"test data";
245 let data3 = b"different data";
246
247 let id1 = generate_blob_id(data1);
248 let id2 = generate_blob_id(data2);
249 let id3 = generate_blob_id(data3);
250
251 assert_eq!(id1, id2);
253
254 assert_ne!(id1, id3);
256
257 assert!(id1.starts_with('G'));
259 }
260
261 #[test]
262 fn test_blob_id_length() {
263 let data = b"test data";
264 let blob_id = generate_blob_id(data);
265
266 assert_eq!(blob_id.len(), 65);
268 }
269
270 #[test]
271 fn test_blob_storage_multiple_blobs() {
272 let storage = BlobStorage::new();
273
274 for i in 0..10 {
275 let data = format!("data{}", i).into_bytes();
276 let blob_id = format!("blob{}", i);
277 storage.store(blob_id.clone(), data.clone(), "text/plain".to_string());
278 }
279
280 for i in 0..10 {
282 let blob_id = format!("blob{}", i);
283 assert!(storage.get(&blob_id).is_some());
284 }
285 }
286
287 #[test]
288 fn test_blob_storage_overwrite() {
289 let storage = BlobStorage::new();
290 let blob_id = "blob123".to_string();
291
292 storage.store(
293 blob_id.clone(),
294 b"original".to_vec(),
295 "text/plain".to_string(),
296 );
297 storage.store(
298 blob_id.clone(),
299 b"updated".to_vec(),
300 "text/html".to_string(),
301 );
302
303 let retrieved = storage.get(&blob_id).unwrap();
304 assert_eq!(retrieved.data, b"updated");
305 assert_eq!(retrieved.content_type, "text/html");
306 }
307
308 #[test]
309 fn test_blob_storage_empty_data() {
310 let storage = BlobStorage::new();
311 let blob_id = "empty".to_string();
312
313 storage.store(
314 blob_id.clone(),
315 vec![],
316 "application/octet-stream".to_string(),
317 );
318
319 let retrieved = storage.get(&blob_id).unwrap();
320 assert_eq!(retrieved.data.len(), 0);
321 }
322
323 #[test]
324 fn test_blob_storage_large_data() {
325 let storage = BlobStorage::new();
326 let data = vec![0u8; 1024 * 1024]; let blob_id = "large".to_string();
328
329 storage.store(
330 blob_id.clone(),
331 data.clone(),
332 "application/octet-stream".to_string(),
333 );
334
335 assert_eq!(storage.size(&blob_id), Some(1024 * 1024));
336 }
337
338 #[test]
339 fn test_upload_response_serialization() {
340 let response = UploadResponse {
341 account_id: "acc1".to_string(),
342 blob_id: "blob123".to_string(),
343 content_type: "image/png".to_string(),
344 size: 1024,
345 };
346
347 let json = serde_json::to_string(&response).unwrap();
348 assert!(json.contains("blob123"));
349 assert!(json.contains("image/png"));
350 }
351
352 #[test]
353 fn test_upload_error_serialization() {
354 let error = UploadError {
355 error_type: "urn:ietf:params:jmap:error:tooLarge".to_string(),
356 status: 413,
357 detail: Some("Too large".to_string()),
358 };
359
360 let json = serde_json::to_string(&error).unwrap();
361 assert!(json.contains("tooLarge"));
362 assert!(json.contains("413"));
363 }
364
365 #[test]
366 fn test_max_blob_size_constant() {
367 assert_eq!(MAX_BLOB_SIZE, 10 * 1024 * 1024);
368 }
369
370 #[test]
371 fn test_blob_id_deterministic() {
372 let data = b"consistent data";
373 let id1 = generate_blob_id(data);
374 let id2 = generate_blob_id(data);
375 let id3 = generate_blob_id(data);
376
377 assert_eq!(id1, id2);
378 assert_eq!(id2, id3);
379 }
380
381 #[test]
382 fn test_blob_storage_clone() {
383 let storage1 = BlobStorage::new();
384 storage1.store(
385 "blob1".to_string(),
386 b"data".to_vec(),
387 "text/plain".to_string(),
388 );
389
390 let storage2 = storage1.clone();
391 assert!(storage2.get("blob1").is_some());
392 }
393
394 #[test]
395 fn test_blob_data_clone() {
396 let data1 = BlobData {
397 data: b"test".to_vec(),
398 content_type: "text/plain".to_string(),
399 };
400
401 let data2 = data1.clone();
402 assert_eq!(data1.data, data2.data);
403 assert_eq!(data1.content_type, data2.content_type);
404 }
405
406 #[test]
407 fn test_blob_storage_default() {
408 let storage = BlobStorage::default();
409 assert!(storage.get("any").is_none());
410 }
411
412 #[test]
413 fn test_blob_id_uniqueness() {
414 let mut ids = std::collections::HashSet::new();
415
416 for i in 0..100 {
417 let data = format!("unique data {}", i).into_bytes();
418 let id = generate_blob_id(&data);
419 assert!(ids.insert(id), "Duplicate blob ID generated");
420 }
421 }
422
423 #[test]
424 fn test_blob_storage_concurrent_access() {
425 let storage = BlobStorage::new();
426
427 storage.store(
429 "blob1".to_string(),
430 b"data1".to_vec(),
431 "text/plain".to_string(),
432 );
433
434 let storage2 = storage.clone();
436 storage2.store(
437 "blob2".to_string(),
438 b"data2".to_vec(),
439 "text/html".to_string(),
440 );
441
442 assert!(storage.get("blob1").is_some());
444 assert!(storage.get("blob2").is_some());
445 assert!(storage2.get("blob1").is_some());
446 assert!(storage2.get("blob2").is_some());
447 }
448
449 #[test]
450 fn test_blob_storage_size_nonexistent() {
451 let storage = BlobStorage::new();
452 assert_eq!(storage.size("nonexistent"), None);
453 }
454
455 #[test]
456 fn test_blob_id_format() {
457 let data = b"test";
458 let blob_id = generate_blob_id(data);
459
460 assert!(blob_id.chars().skip(1).all(|c| c.is_ascii_hexdigit()));
462 }
463
464 #[test]
465 fn test_upload_error_without_detail() {
466 let error = UploadError {
467 error_type: "urn:ietf:params:jmap:error:serverFail".to_string(),
468 status: 500,
469 detail: None,
470 };
471
472 let json = serde_json::to_string(&error).unwrap();
473 assert!(!json.contains("detail"));
474 }
475}