feat: implement routes for serving images
parent
efb5b9ebea
commit
0c4fc0ef98
|
@ -351,11 +351,13 @@ dependencies = [
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"futures",
|
"futures",
|
||||||
"image",
|
"image",
|
||||||
|
"mime",
|
||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"tera",
|
"tera",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"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"] }
|
diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
image = { version = "0.25.5", default-features = false, features = ["gif", "jpeg", "png"] }
|
image = { version = "0.25.5", default-features = false, features = ["gif", "jpeg", "png"] }
|
||||||
|
mime = "0.3.17"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
# this dependency is needed soly because the r2d2_sqlite crate doesn't export
|
# this dependency is needed soly because the r2d2_sqlite crate doesn't export
|
||||||
# the 'chrono' feature flag
|
# the 'chrono' feature flag
|
||||||
|
@ -24,6 +25,7 @@ serde = { version = "1.0.217", features = ["derive"] }
|
||||||
tera = "1.20.0"
|
tera = "1.20.0"
|
||||||
tokio = { version = "1.42.0", features = ["full"] }
|
tokio = { version = "1.42.0", features = ["full"] }
|
||||||
tokio-util = { version = "0.7.13", features = ["io"] }
|
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"] }
|
tower-http = { version = "0.6.2", features = ["compression-br", "compression-gzip", "set-header", "fs"] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
|
|
|
@ -47,3 +47,13 @@ impl NewImage {
|
||||||
.get_result(&mut pool.get()?)?)
|
.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),
|
Tera(tera::Error),
|
||||||
Multipart(MultipartError),
|
Multipart(MultipartError),
|
||||||
IO(std::io::Error),
|
IO(std::io::Error),
|
||||||
|
Other(Box<dyn std::error::Error + 'static + Send + Sync>),
|
||||||
BadRequest,
|
BadRequest,
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
NotFound,
|
NotFound,
|
||||||
|
@ -22,6 +23,7 @@ impl fmt::Display for AppError {
|
||||||
Self::Tera(_) => write!(f, "error rendering template"),
|
Self::Tera(_) => write!(f, "error rendering template"),
|
||||||
Self::Multipart(_) => write!(f, "error processing multipart request"),
|
Self::Multipart(_) => write!(f, "error processing multipart request"),
|
||||||
Self::IO(_) => write!(f, "io error"),
|
Self::IO(_) => write!(f, "io error"),
|
||||||
|
Self::Other(_) => write!(f, "other error"),
|
||||||
Self::BadRequest => write!(f, "bad request"),
|
Self::BadRequest => write!(f, "bad request"),
|
||||||
Self::Unauthorized => write!(f, "unauthorized"),
|
Self::Unauthorized => write!(f, "unauthorized"),
|
||||||
Self::NotFound => write!(f, "not found"),
|
Self::NotFound => write!(f, "not found"),
|
||||||
|
@ -35,6 +37,7 @@ impl std::error::Error for AppError {
|
||||||
Self::Db(err) => Some(err),
|
Self::Db(err) => Some(err),
|
||||||
Self::Tera(err) => Some(err),
|
Self::Tera(err) => Some(err),
|
||||||
Self::IO(err) => Some(err),
|
Self::IO(err) => Some(err),
|
||||||
|
Self::Other(err) => Some(err.as_ref()),
|
||||||
Self::Multipart(err) => Some(err),
|
Self::Multipart(err) => Some(err),
|
||||||
Self::NotFound | Self::Unauthorized | Self::BadRequest => None,
|
Self::NotFound | Self::Unauthorized | Self::BadRequest => None,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{DefaultBodyLimit, Multipart, State},
|
extract::{DefaultBodyLimit, Multipart, Path, Request, State},
|
||||||
handler::Handler,
|
handler::Handler,
|
||||||
response::Html,
|
response::{Html, IntoResponse},
|
||||||
routing::post,
|
routing::{get, post},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
use image::{codecs::jpeg::JpegEncoder, ImageReader};
|
use image::{codecs::jpeg::JpegEncoder, ImageReader};
|
||||||
|
use mime::Mime;
|
||||||
use tokio_util::io::StreamReader;
|
use tokio_util::io::StreamReader;
|
||||||
|
use tower::ServiceExt;
|
||||||
|
use tower_http::services::ServeFile;
|
||||||
|
|
||||||
use std::{io::BufWriter, path::PathBuf};
|
use std::{io::BufWriter, path::PathBuf};
|
||||||
|
|
||||||
|
@ -21,10 +24,54 @@ use crate::{
|
||||||
const THUMBNAIL_EXT: &str = "thumb";
|
const THUMBNAIL_EXT: &str = "thumb";
|
||||||
|
|
||||||
pub fn app() -> axum::Router<crate::Context> {
|
pub fn app() -> axum::Router<crate::Context> {
|
||||||
Router::new().route(
|
Router::new()
|
||||||
|
.route(
|
||||||
"/",
|
"/",
|
||||||
post(post_image.layer(DefaultBodyLimit::max(1024 * 1024 * 20))),
|
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(
|
async fn post_image(
|
||||||
|
|
Loading…
Reference in New Issue