1use crate::blob::{self, Blob};
14use crate::client::{Storage, PURPOSE_DISK_UPLOAD};
15use crate::content_type;
16use crate::signing::Disposition;
17use axum::body::Bytes;
18use axum::extract::{Path, State};
19use axum::http::{header, StatusCode};
20use axum::response::{IntoResponse, Redirect, Response};
21use axum::routing::{get, post, put};
22use axum::{Json, Router};
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25
26pub fn routes(storage: Storage) -> Router {
28 let p = storage.prefix().to_string();
29 Router::new()
30 .route(
31 &format!("{p}/blobs/redirect/{{signed_id}}/{{filename}}"),
32 get(redirect_handler),
33 )
34 .route(
35 &format!("{p}/blobs/proxy/{{signed_id}}/{{filename}}"),
36 get(proxy_handler),
37 )
38 .route(&format!("{p}/disk/{{token}}"), put(disk_upload_handler))
39 .route(&format!("{p}/direct_uploads"), post(direct_uploads_handler))
40 .with_state(storage)
41}
42
43async fn resolve_blob(storage: &Storage, signed_id: &str) -> Option<Blob> {
44 let key = storage.verify_signed_id(signed_id).ok()?;
45 storage.find_blob(&key).await.ok().flatten()
46}
47
48async fn redirect_handler(
49 State(storage): State<Storage>,
50 Path((signed_id, _filename)): Path<(String, String)>,
51) -> Response {
52 let Some(blob) = resolve_blob(&storage, &signed_id).await else {
53 return StatusCode::NOT_FOUND.into_response();
54 };
55 match storage.url_for(&blob, Disposition::Inline).await {
56 Ok(url) => Redirect::temporary(&url).into_response(),
57 Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
58 }
59}
60
61async fn proxy_handler(
62 State(storage): State<Storage>,
63 Path((signed_id, _filename)): Path<(String, String)>,
64) -> Response {
65 let Some(blob) = resolve_blob(&storage, &signed_id).await else {
66 return StatusCode::NOT_FOUND.into_response();
67 };
68 let bytes = match storage.download(&blob.key).await {
69 Ok(b) => b,
70 Err(_) => return StatusCode::NOT_FOUND.into_response(),
71 };
72 let ct = blob
73 .content_type
74 .clone()
75 .unwrap_or_else(|| content_type::DEFAULT.to_string());
76 (
77 [
78 (header::CONTENT_TYPE, ct),
79 (
80 header::CONTENT_DISPOSITION,
81 Disposition::Inline.header(&blob.filename),
82 ),
83 ],
84 bytes,
85 )
86 .into_response()
87}
88
89async fn disk_upload_handler(
90 State(storage): State<Storage>,
91 Path(token): Path<String>,
92 body: Bytes,
93) -> Response {
94 let key = match storage.signer().verify(&token, PURPOSE_DISK_UPLOAD) {
95 Ok(k) => k,
96 Err(_) => return StatusCode::FORBIDDEN.into_response(),
97 };
98 match storage.service().upload(&key, body.to_vec(), None).await {
99 Ok(()) => StatusCode::NO_CONTENT.into_response(),
100 Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
101 }
102}
103
104#[derive(Debug, Deserialize)]
106pub struct DirectUploadRequest {
107 pub filename: String,
109 #[serde(default)]
111 pub byte_size: Option<i64>,
112 #[serde(default)]
114 pub checksum: Option<String>,
115 #[serde(default)]
117 pub content_type: Option<String>,
118}
119
120#[derive(Debug, Serialize)]
122pub struct DirectUpload {
123 pub url: String,
125 pub headers: HashMap<String, String>,
127}
128
129#[derive(Debug, Serialize)]
131pub struct DirectUploadResponse {
132 pub key: String,
134 pub signed_id: String,
136 pub direct_upload: DirectUpload,
138}
139
140async fn direct_uploads_handler(
141 State(storage): State<Storage>,
142 Json(req): Json<DirectUploadRequest>,
143) -> Response {
144 let content_type = req
145 .content_type
146 .or_else(|| content_type::from_filename(&req.filename).map(str::to_string));
147
148 let blob = Blob {
149 key: blob::new_key(),
150 filename: req.filename.clone(),
151 content_type,
152 metadata: None,
153 service_name: storage.service().name().to_string(),
154 byte_size: req.byte_size.unwrap_or(0),
155 checksum: req.checksum,
156 created_at: chrono::Utc::now().to_rfc3339(),
157 };
158
159 if blob::insert(storage.conn(), &blob).await.is_err() {
160 return StatusCode::INTERNAL_SERVER_ERROR.into_response();
161 }
162
163 let opts = crate::service::UrlOptions {
164 expires_in: storage.expires_in(),
165 disposition: Disposition::Inline,
166 filename: Some(blob.filename.clone()),
167 content_type: blob.content_type.clone(),
168 };
169
170 let url = match storage.service().presigned_put(&blob.key, &opts).await {
172 Ok(Some(u)) => u,
173 _ => {
174 let token =
175 storage
176 .signer()
177 .sign(&blob.key, PURPOSE_DISK_UPLOAD, Some(storage.expires_in()));
178 format!("{}/disk/{}", storage.prefix(), token)
179 }
180 };
181
182 let mut headers = HashMap::new();
183 if let Some(ct) = &blob.content_type {
184 headers.insert("Content-Type".to_string(), ct.clone());
185 }
186
187 Json(DirectUploadResponse {
188 signed_id: storage.signed_id(&blob.key),
189 key: blob.key,
190 direct_upload: DirectUpload { url, headers },
191 })
192 .into_response()
193}