feat: implement routes for serving images
							parent
							
								
									efb5b9ebea
								
							
						
					
					
						commit
						0c4fc0ef98
					
				|  | @ -351,11 +351,13 @@ dependencies = [ | |||
|  "diesel_migrations", | ||||
|  "futures", | ||||
|  "image", | ||||
|  "mime", | ||||
|  "rand", | ||||
|  "serde", | ||||
|  "tera", | ||||
|  "tokio", | ||||
|  "tokio-util", | ||||
|  "tower", | ||||
|  "tower-http", | ||||
|  "tracing", | ||||
|  "tracing-subscriber", | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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()?) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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, | ||||
|         } | ||||
|  |  | |||
|  | @ -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( | ||||
|         "/", | ||||
|         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<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( | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue