feat: implement routes for serving images

image-uploads
Jef Roosens 2025-01-17 13:45:28 +01:00
parent efb5b9ebea
commit 0c4fc0ef98
No known key found for this signature in database
GPG Key ID: 21FD3D77D56BAF49
5 changed files with 71 additions and 7 deletions

2
Cargo.lock generated
View File

@ -351,11 +351,13 @@ dependencies = [
"diesel_migrations",
"futures",
"image",
"mime",
"rand",
"serde",
"tera",
"tokio",
"tokio-util",
"tower",
"tower-http",
"tracing",
"tracing-subscriber",

View File

@ -17,6 +17,7 @@ diesel = { version = "2.2.6", features = ["sqlite", "returning_clauses_for_sqlit
diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
futures = "0.3.31"
image = { version = "0.25.5", default-features = false, features = ["gif", "jpeg", "png"] }
mime = "0.3.17"
rand = "0.8.5"
# this dependency is needed soly because the r2d2_sqlite crate doesn't export
# the 'chrono' feature flag
@ -24,6 +25,7 @@ serde = { version = "1.0.217", features = ["derive"] }
tera = "1.20.0"
tokio = { version = "1.42.0", features = ["full"] }
tokio-util = { version = "0.7.13", features = ["io"] }
tower = { version = "0.5.2", features = ["util"] }
tower-http = { version = "0.6.2", features = ["compression-br", "compression-gzip", "set-header", "fs"] }
tracing = "0.1.41"
tracing-subscriber = "0.3.19"

View File

@ -47,3 +47,13 @@ impl NewImage {
.get_result(&mut pool.get()?)?)
}
}
impl Image {
pub fn by_id(pool: &DbPool, id: i32) -> DbResult<Option<Self>> {
Ok(images::table
.find(id)
.select(Self::as_select())
.first(&mut pool.get()?)
.optional()?)
}
}

View File

@ -10,6 +10,7 @@ pub enum AppError {
Tera(tera::Error),
Multipart(MultipartError),
IO(std::io::Error),
Other(Box<dyn std::error::Error + 'static + Send + Sync>),
BadRequest,
Unauthorized,
NotFound,
@ -22,6 +23,7 @@ impl fmt::Display for AppError {
Self::Tera(_) => write!(f, "error rendering template"),
Self::Multipart(_) => write!(f, "error processing multipart request"),
Self::IO(_) => write!(f, "io error"),
Self::Other(_) => write!(f, "other error"),
Self::BadRequest => write!(f, "bad request"),
Self::Unauthorized => write!(f, "unauthorized"),
Self::NotFound => write!(f, "not found"),
@ -35,6 +37,7 @@ impl std::error::Error for AppError {
Self::Db(err) => Some(err),
Self::Tera(err) => Some(err),
Self::IO(err) => Some(err),
Self::Other(err) => Some(err.as_ref()),
Self::Multipart(err) => Some(err),
Self::NotFound | Self::Unauthorized | Self::BadRequest => None,
}

View File

@ -1,14 +1,17 @@
use axum::{
extract::{DefaultBodyLimit, Multipart, State},
extract::{DefaultBodyLimit, Multipart, Path, Request, State},
handler::Handler,
response::Html,
routing::post,
response::{Html, IntoResponse},
routing::{get, post},
Router,
};
use chrono::NaiveDate;
use futures::TryStreamExt;
use image::{codecs::jpeg::JpegEncoder, ImageReader};
use mime::Mime;
use tokio_util::io::StreamReader;
use tower::ServiceExt;
use tower_http::services::ServeFile;
use std::{io::BufWriter, path::PathBuf};
@ -21,10 +24,54 @@ use crate::{
const THUMBNAIL_EXT: &str = "thumb";
pub fn app() -> axum::Router<crate::Context> {
Router::new().route(
Router::new()
.route(
"/",
post(post_image.layer(DefaultBodyLimit::max(1024 * 1024 * 20))),
)
.route("/{id}/original", get(get_image_original))
.route("/{id}/thumb", get(get_image_thumb))
}
async fn get_image(
ctx: crate::Context,
id: i32,
req: Request,
thumb: bool,
) -> super::Result<impl IntoResponse> {
let image = tokio::task::spawn_blocking(move || Image::by_id(&ctx.pool, id))
.await
.unwrap()?
.ok_or(AppError::NotFound)?;
let mime: Mime = image
.mime_type
.parse()
.map_err(|err| AppError::Other(Box::new(err)))?;
let mut path = ctx.data_dir.join(crate::IMG_DIR).join(image.id.to_string());
if thumb {
path.set_extension(THUMBNAIL_EXT);
}
Ok(ServeFile::new_with_mime(path, &mime).oneshot(req).await)
}
async fn get_image_original(
State(ctx): State<crate::Context>,
Path(id): Path<i32>,
req: Request,
) -> super::Result<impl IntoResponse> {
get_image(ctx, id, req, false).await
}
async fn get_image_thumb(
State(ctx): State<crate::Context>,
Path(id): Path<i32>,
req: Request,
) -> super::Result<impl IntoResponse> {
get_image(ctx, id, req, true).await
}
async fn post_image(