kanzashi-cdn/src/routes/upload.rs
2026-07-24 01:36:58 +02:00

118 lines
No EOL
3.3 KiB
Rust

use aws_sdk_s3::Client;
use rocket::form::{Form, FromForm, Lenient};
use rocket::fs::TempFile;
use rocket::http::Status;
use rocket::serde::json::Json;
use rocket::State;
use serde::Serialize;
use std::path::Path;
use uuid::Uuid;
use crate::guards::AsymmetricAuth;
use crate::AllowedExtensions;
#[derive(Serialize)]
pub struct UploadResponse {
pub message: String,
pub key: String,
}
#[derive(FromForm)]
pub struct UploadForm<'r> {
pub file: TempFile<'r>,
}
#[post("/upload", data = "<form>")]
pub async fn upload_asset(
form: Form<Lenient<UploadForm<'_>>>,
_auth: AsymmetricAuth,
allowed_bucket: &State<String>,
allowed_exts: &State<AllowedExtensions>,
s3_client: &State<Client>,
) -> Result<Json<UploadResponse>, Status> {
let mut upload = form.into_inner();
let file = &mut upload.file;
let raw_filename = file
.raw_name()
.and_then(|n| n.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| Uuid::new_v4().to_string());
let mut ext = Path::new(&raw_filename)
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_lowercase())
.unwrap_or_default();
if ext.is_empty() {
if let Some(ct) = file.content_type() {
ext = match (ct.top().as_str(), ct.sub().as_str()) {
("image", "jpeg") => "jpg".to_string(),
("image", "png") => "png".to_string(),
("image", "gif") => "gif".to_string(),
("image", "webp") => "webp".to_string(),
_ => ct.sub().as_str().to_lowercase(),
};
} else {
eprintln!("missing file extension and Content-Type header");
return Err(Status::BadRequest);
}
}
if !allowed_exts.0.contains(&ext) {
eprintln!(
"Rejected upload '{}' with forbidden extension: '.{}'",
raw_filename, ext
);
return Err(Status::UnprocessableEntity);
}
let final_key = if Path::new(&raw_filename).extension().is_none() {
format!("{}.{}", raw_filename, ext)
} else {
raw_filename
};
let content_type_str = file
.content_type()
.map(|ct| ct.to_string())
.unwrap_or_else(|| format!("image/{}", if ext == "jpg" { "jpeg" } else { &ext }));
let body_bytes = match file.path() {
Some(path) => tokio::fs::read(path)
.await
.map_err(|_| Status::InternalServerError)?,
None => {
use tokio::io::AsyncReadExt;
let mut buf = Vec::new();
file.open()
.await
.map_err(|_| Status::InternalServerError)?
.read_to_end(&mut buf)
.await
.map_err(|_| Status::InternalServerError)?;
buf
}
};
let body = aws_sdk_s3::primitives::ByteStream::from(body_bytes);
s3_client
.put_object()
.bucket(allowed_bucket.inner())
.key(&final_key)
.content_type(content_type_str)
.body(body)
.send()
.await
.map_err(|e| {
eprintln!("S3 Upload Error: {:?}", e);
Status::InternalServerError
})?;
Ok(Json(UploadResponse {
message: "Upload successful".to_string(),
key: final_key,
}))
}