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