use crate::blob::{self, Blob};
use crate::client::{Storage, PURPOSE_DISK_UPLOAD};
use crate::content_type;
use crate::signing::Disposition;
use axum::body::Bytes;
use axum::extract::{Path, State};
use axum::http::{header, StatusCode};
use axum::response::{IntoResponse, Redirect, Response};
use axum::routing::{get, post, put};
use axum::{Json, Router};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub fn routes(storage: Storage) -> Router {
let p = storage.prefix().to_string();
Router::new()
.route(
&format!("{p}/blobs/redirect/{{signed_id}}/{{filename}}"),
get(redirect_handler),
)
.route(
&format!("{p}/blobs/proxy/{{signed_id}}/{{filename}}"),
get(proxy_handler),
)
.route(&format!("{p}/disk/{{token}}"), put(disk_upload_handler))
.route(&format!("{p}/direct_uploads"), post(direct_uploads_handler))
.with_state(storage)
}
async fn resolve_blob(storage: &Storage, signed_id: &str) -> Option<Blob> {
let key = storage.verify_signed_id(signed_id).ok()?;
storage.find_blob(&key).await.ok().flatten()
}
async fn redirect_handler(
State(storage): State<Storage>,
Path((signed_id, _filename)): Path<(String, String)>,
) -> Response {
let Some(blob) = resolve_blob(&storage, &signed_id).await else {
return StatusCode::NOT_FOUND.into_response();
};
match storage.url_for(&blob, Disposition::Inline).await {
Ok(url) => Redirect::temporary(&url).into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
async fn proxy_handler(
State(storage): State<Storage>,
Path((signed_id, _filename)): Path<(String, String)>,
) -> Response {
let Some(blob) = resolve_blob(&storage, &signed_id).await else {
return StatusCode::NOT_FOUND.into_response();
};
let bytes = match storage.download(&blob.key).await {
Ok(b) => b,
Err(_) => return StatusCode::NOT_FOUND.into_response(),
};
let ct = blob
.content_type
.clone()
.unwrap_or_else(|| content_type::DEFAULT.to_string());
(
[
(header::CONTENT_TYPE, ct),
(
header::CONTENT_DISPOSITION,
Disposition::Inline.header(&blob.filename),
),
],
bytes,
)
.into_response()
}
async fn disk_upload_handler(
State(storage): State<Storage>,
Path(token): Path<String>,
body: Bytes,
) -> Response {
let key = match storage.signer().verify(&token, PURPOSE_DISK_UPLOAD) {
Ok(k) => k,
Err(_) => return StatusCode::FORBIDDEN.into_response(),
};
match storage.service().upload(&key, body.to_vec(), None).await {
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
#[derive(Debug, Deserialize)]
pub struct DirectUploadRequest {
pub filename: String,
#[serde(default)]
pub byte_size: Option<i64>,
#[serde(default)]
pub checksum: Option<String>,
#[serde(default)]
pub content_type: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct DirectUpload {
pub url: String,
pub headers: HashMap<String, String>,
}
#[derive(Debug, Serialize)]
pub struct DirectUploadResponse {
pub key: String,
pub signed_id: String,
pub direct_upload: DirectUpload,
}
async fn direct_uploads_handler(
State(storage): State<Storage>,
Json(req): Json<DirectUploadRequest>,
) -> Response {
let content_type = req
.content_type
.or_else(|| content_type::from_filename(&req.filename).map(str::to_string));
let blob = Blob {
key: blob::new_key(),
filename: req.filename.clone(),
content_type,
metadata: None,
service_name: storage.service().name().to_string(),
byte_size: req.byte_size.unwrap_or(0),
checksum: req.checksum,
created_at: chrono::Utc::now().to_rfc3339(),
};
if blob::insert(storage.conn(), &blob).await.is_err() {
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
let opts = crate::service::UrlOptions {
expires_in: storage.expires_in(),
disposition: Disposition::Inline,
filename: Some(blob.filename.clone()),
content_type: blob.content_type.clone(),
};
let url = match storage.service().presigned_put(&blob.key, &opts).await {
Ok(Some(u)) => u,
_ => {
let token =
storage
.signer()
.sign(&blob.key, PURPOSE_DISK_UPLOAD, Some(storage.expires_in()));
format!("{}/disk/{}", storage.prefix(), token)
}
};
let mut headers = HashMap::new();
if let Some(ct) = &blob.content_type {
headers.insert("Content-Type".to_string(), ct.clone());
}
Json(DirectUploadResponse {
signed_id: storage.signed_id(&blob.key),
key: blob.key,
direct_upload: DirectUpload { url, headers },
})
.into_response()
}