diff --git a/Cargo.lock b/Cargo.lock index 0733513..4e42bd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -351,11 +351,13 @@ dependencies = [ "diesel_migrations", "futures", "image", + "mime", "rand", "serde", "tera", "tokio", "tokio-util", + "tower", "tower-http", "tracing", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index 0efdb25..d26e04f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/db/models/image.rs b/src/db/models/image.rs index 48e2825..c7e6309 100644 --- a/src/db/models/image.rs +++ b/src/db/models/image.rs @@ -47,3 +47,13 @@ impl NewImage { .get_result(&mut pool.get()?)?) } } + +impl Image { + pub fn by_id(pool: &DbPool, id: i32) -> DbResult> { + Ok(images::table + .find(id) + .select(Self::as_select()) + .first(&mut pool.get()?) + .optional()?) + } +} diff --git a/src/server/error.rs b/src/server/error.rs index 79370ac..ffde0a2 100644 --- a/src/server/error.rs +++ b/src/server/error.rs @@ -10,6 +10,7 @@ pub enum AppError { Tera(tera::Error), Multipart(MultipartError), IO(std::io::Error), + Other(Box), 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, } diff --git a/src/server/images.rs b/src/server/images.rs index 414b931..3f1a1e9 100644 --- a/src/server/images.rs +++ b/src/server/images.rs @@ -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 { - Router::new().route( - "/", - post(post_image.layer(DefaultBodyLimit::max(1024 * 1024 * 20))), - ) + 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 { + 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, + Path(id): Path, + req: Request, +) -> super::Result { + get_image(ctx, id, req, false).await +} + +async fn get_image_thumb( + State(ctx): State, + Path(id): Path, + req: Request, +) -> super::Result { + get_image(ctx, id, req, true).await } async fn post_image(