lust/src/routes.rs

306 lines
9.0 KiB
Rust

use std::fmt::Display;
use bytes::Bytes;
use poem_openapi::OpenApi;
use poem::{Body, Result};
use poem_openapi::{ApiResponse, Object};
use poem_openapi::param::{Header, Path, Query};
use poem_openapi::payload::{Binary, Json};
use futures::StreamExt;
use uuid::Uuid;
use crate::config::{config, ImageKind};
use crate::controller::{BucketController, get_bucket_by_name, UploadInfo};
use crate::pipelines::ProcessingMode;
#[derive(Debug, Object)]
pub struct Detail {
/// Additional information regarding the response.
detail: String,
}
#[derive(ApiResponse)]
pub enum UploadResponse {
#[oai(status = 200)]
Ok(Json<UploadInfo>),
/// Bucket not found
#[oai(status = 404)]
NotFound,
/// The image format was incorrect or the system was
/// unable to guess the format of the image.
#[oai(status = 400)]
InvalidImageFormat,
/// The upload exceeds the configured maximum file size.
#[oai(status = 413)]
TooBig,
#[allow(unused)]
/// You are not authorized to complete this action.
///
/// This normally means the `Authorization` bearer has been left out
/// of the request or is invalid.
#[oai(status = 401)]
Unauthorized,
}
#[derive(ApiResponse)]
pub enum DeleteResponse {
#[oai(status = 200)]
Ok,
#[allow(unused)]
/// You are not authorized to complete this action.
///
/// This normally means the `Authorization` bearer has been left out
/// of the request or is invalid.
#[oai(status = 401)]
Unauthorized,
/// Bucket does not exist.
#[oai(status = 404)]
NotFound,
}
#[derive(ApiResponse)]
pub enum FetchResponse {
#[oai(status = 200)]
Ok(
Binary<Vec<u8>>,
#[oai(header = "content-type")] String,
),
/// The request is invalid with the current configuration.
///
/// See the detail section for more info.
#[oai(status = 400)]
UnsupportedOperation(Json<Detail>),
/// Bucket does not exist or image does not exist.
///
/// See the detail section for more info.
#[oai(status = 404)]
NotFound(Json<Detail>),
}
impl FetchResponse {
fn bucket_not_found(bucket: &str) -> Self {
let detail = Detail {
detail: format!("The bucket {:?} does not exist.", bucket),
};
Self::NotFound(Json(detail))
}
fn image_not_found(image_id: Uuid) -> Self {
let detail = Detail {
detail: format!("The image {:?} does not exist in bucket.", image_id),
};
Self::NotFound(Json(detail))
}
fn bad_request(msg: impl Display) -> Self {
let detail = Detail {
detail: msg.to_string(),
};
Self::UnsupportedOperation(Json(detail))
}
}
pub struct LustApi ;
#[OpenApi(prefix_path = "/:bucket")]
impl LustApi {
/// Upload Image
///
/// Upload an image to the given bucket.
/// The `content-type` header must be provided as well
/// as the `content-length` header otherwise the request will be rejected.
///
/// The uploaded file must also not exceed the given `content-length`.
#[oai(path = "/", method = "post")]
pub async fn upload_image(
&self,
/// The bucket that the image should be uploaded.
bucket: Path<String>,
/// The total size of the image in bytes.
#[oai(name = "content-length")] content_length: Header<usize>,
/// The format that the uploaded image is encoded in.
///
/// If not provided, lust will guess the encoding.
format: Query<Option<ImageKind>>,
/// The raw binary data of the image.
file: Binary<Body>,
) -> Result<UploadResponse> {
let bucket = match get_bucket_by_name(&*bucket) {
None => return Ok(UploadResponse::NotFound),
Some(b) => b,
};
let length = if !config().valid_global_size(*content_length) {
return Ok(UploadResponse::TooBig)
} else {
let local_limit = bucket
.cfg()
.max_upload_size
.map(|v| (v * 1024) as usize)
.unwrap_or(u32::MAX as usize);
if *content_length > local_limit {
return Ok(UploadResponse::TooBig)
}
*content_length
};
let mut allocated_image = Vec::with_capacity(length);
let mut stream = file.0.into_bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk: Bytes = chunk.map_err(anyhow::Error::from)?;
allocated_image.extend(chunk.into_iter());
if allocated_image.len() > length {
return Ok(UploadResponse::TooBig)
}
}
let format = if let Some(format) = format.0 {
let validate = image::load_from_memory_with_format(&allocated_image, format.into());
if validate.is_err() {
return Ok(UploadResponse::InvalidImageFormat)
}
format
} else {
let maybe_guessed = image::guess_format(&allocated_image)
.map(ImageKind::from_guessed_format)
.map_err(anyhow::Error::from)?;
if let Some(guessed) = maybe_guessed {
guessed
} else {
return Ok(UploadResponse::InvalidImageFormat)
}
};
let info = bucket.upload(format, allocated_image).await?;
Ok(UploadResponse::Ok(Json(info)))
}
/// Fetch Image
///
/// Fetch the image from the storage backend and apply and additional affects
/// if required.
#[allow(clippy::too_many_arguments)]
#[oai(path = "/:image_id", method = "get")]
pub async fn fetch_image(
&self,
/// The bucket to try fetch the image from.
bucket: Path<String>,
/// The id of the image.
image_id: Path<Uuid>,
/// The encoding format that the image should be returned as.
format: Query<Option<ImageKind>>,
/// The size preset that should be used when returning the image.
size: Query<Option<String>>,
/// A custom width to resize the returned image to.
width: Query<Option<u32>>,
/// A custom height to resize the returned image to.
height: Query<Option<u32>>,
/// A set of `,` seperated content-types that could be sent as a response.
/// E.g. `image/png,image/webp,image/gif`
accept: Header<Option<String>>,
) -> Result<FetchResponse> {
let bucket = match get_bucket_by_name(&*bucket) {
None => return Ok(FetchResponse::bucket_not_found(&*bucket)),
Some(b) => b,
};
let kind = get_image_kind(format.0, accept.0, bucket);
let custom_sizing = match (width.0, height.0) {
(Some(w), Some(h)) => if bucket.cfg().mode != ProcessingMode::Realtime {
return Ok(FetchResponse::bad_request(
"Custom resizing can only be done when bucket set to 'realtime' processing mode",
))
} else {
Some((w, h))
},
(None, None) => None,
_ => return Ok(FetchResponse::bad_request(
"A custom size must include both the width and the height.",
))
};
let img = bucket.fetch(image_id.0, kind, size.0, custom_sizing).await?;
match img {
None => Ok(FetchResponse::image_not_found(image_id.0)),
Some(img) => Ok(FetchResponse::Ok(Binary(img.data.to_vec()), img.kind.as_content_type()))
}
}
/// Delete Image
///
/// Delete the given image.
/// This will purge all variants of the image including sizing presets and formats.
///
/// Images that do not exist already will be ignored and will not return a 404.
#[oai(path = "/:image_id", method = "delete")]
pub async fn delete_image(
&self,
/// The bucket to try delete the image from.
bucket: Path<String>,
/// The image to delete try delete.
image_id: Path<Uuid>,
) -> Result<DeleteResponse> {
let bucket = match get_bucket_by_name(&*bucket) {
None => return Ok(DeleteResponse::NotFound),
Some(b) => b,
};
bucket.delete(*image_id).await?;
Ok(DeleteResponse::Ok)
}
}
fn get_image_kind(direct_format: Option<ImageKind>, accept: Option<String>, bucket: &BucketController) -> ImageKind {
match direct_format {
Some(kind) => kind,
None => match accept {
Some(accept) => {
let parts = accept.split(',');
for accepted in parts {
if let Some(kind) = ImageKind::from_content_type(accepted) {
return kind;
}
}
bucket.cfg()
.default_serving_format
.unwrap_or_else(|| bucket.cfg().formats.first_enabled_format())
},
None => bucket.cfg()
.default_serving_format
.unwrap_or_else(|| bucket.cfg().formats.first_enabled_format())
},
}
}