118 lines
No EOL
3.3 KiB
Rust
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,
|
|
}))
|
|
} |