A little app to serve my photography from my personal website

basic 404 handling

ericwood.org 72477b96 996fde45

Waiting for spindle ...
+42 -22
+1
Cargo.lock
··· 1136 1136 "serde", 1137 1137 "serde_html_form", 1138 1138 "sqlx", 1139 + "thiserror", 1139 1140 "tokio", 1140 1141 "tower-http", 1141 1142 "urlencoding",
+1
Cargo.toml
··· 14 14 serde = "1.0.228" 15 15 serde_html_form = "0.2.8" 16 16 sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] } 17 + thiserror = "2.0.17" 17 18 tokio = { version = "1.47.1", features = ["rt-multi-thread"] } 18 19 tower-http = { version = "0.6.6", features = ["fs"] } 19 20 urlencoding = "2.1.3"
+21 -15
src/app_error.rs
··· 1 1 use axum::{ 2 2 http::StatusCode, 3 - response::{IntoResponse, Response}, 3 + response::{Html, IntoResponse, Response}, 4 4 }; 5 5 6 - pub struct AppError(anyhow::Error); 6 + #[derive(thiserror::Error, Debug)] 7 + pub enum AppError { 8 + #[error("not found")] 9 + NotFound, 7 10 8 - impl IntoResponse for AppError { 9 - fn into_response(self) -> Response { 10 - ( 11 - StatusCode::INTERNAL_SERVER_ERROR, 12 - format!("Something went wrong: {}", self.0), 13 - ) 14 - .into_response() 11 + #[error("internal server error")] 12 + DbError(#[from] sqlx::Error), 13 + 14 + #[error("internal server error")] 15 + Anyhow(#[from] anyhow::Error), 16 + } 17 + 18 + impl AppError { 19 + pub fn status_code(&self) -> StatusCode { 20 + match self { 21 + Self::NotFound => StatusCode::NOT_FOUND, 22 + Self::DbError(_) | Self::Anyhow(_) => StatusCode::INTERNAL_SERVER_ERROR, 23 + } 15 24 } 16 25 } 17 26 18 - impl<E> From<E> for AppError 19 - where 20 - E: Into<anyhow::Error>, 21 - { 22 - fn from(err: E) -> Self { 23 - Self(err.into()) 27 + impl IntoResponse for AppError { 28 + fn into_response(self) -> Response { 29 + (self.status_code(), Html(self.to_string())).into_response() 24 30 } 25 31 }
+5 -2
src/db.rs
··· 78 78 pub tags: Vec<String>, 79 79 } 80 80 81 - pub async fn get_photos(pool: &SqlitePool, pq: PhotoQuery) -> anyhow::Result<(u32, Vec<Photo>)> { 81 + pub async fn get_photos( 82 + pool: &SqlitePool, 83 + pq: PhotoQuery, 84 + ) -> Result<(u32, Vec<Photo>), sqlx::Error> { 82 85 let photos = build_photo_query(&pq) 83 86 .build_query_as() 84 87 .fetch_all(pool) ··· 144 147 query 145 148 } 146 149 147 - pub async fn get_photo(pool: &SqlitePool, id: &String) -> anyhow::Result<Photo> { 150 + pub async fn get_photo(pool: &SqlitePool, id: &String) -> Result<Photo, sqlx::Error> { 148 151 let photo: Photo = sqlx::query_as("SELECT * FROM photos WHERE id = ?") 149 152 .bind(id) 150 153 .fetch_one(pool)
+9 -4
src/main.rs
··· 1 - use axum::{Router, routing::get}; 1 + use axum::{Router, response::Html, routing::get}; 2 2 use dotenv::dotenv; 3 3 use minijinja_autoreload::AutoReloader; 4 4 use std::{env, sync::Arc}; ··· 29 29 let reloader = load_templates_dyn(); 30 30 let app_state = Arc::new(AppState { reloader, pool }); 31 31 let app = Router::new() 32 - .route("/", get(routes::photos::index)) 33 - .route("/{id}", get(routes::photos::show)) 32 + .route("/photos", get(routes::photos::index)) 33 + .route("/photos/{id}", get(routes::photos::show)) 34 34 .with_state(app_state) 35 35 .nest_service("/assets", ServeDir::new(&env::var("ASSETS_PATH")?)) 36 36 .nest_service("/thumbnails", ServeDir::new(&env::var("THUMBNAIL_PATH")?)) 37 - .nest_service("/images", ServeDir::new(&env::var("IMAGE_PATH")?)); 37 + .nest_service("/images", ServeDir::new(&env::var("IMAGE_PATH")?)) 38 + .fallback(handler_404); 38 39 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); 39 40 axum::serve(listener, app).await?; 40 41 41 42 Ok(()) 42 43 } 44 + 45 + async fn handler_404() -> anyhow::Result<Html<String>, app_error::AppError> { 46 + Err(AppError::NotFound) 47 + }
+5 -1
src/routes/photos/show.rs
··· 10 10 Path(id): Path<String>, 11 11 State(state): State<Arc<AppState>>, 12 12 ) -> Result<Html<String>, AppError> { 13 - let photo = db::get_photo(&state.pool, &id).await?; 13 + let photo = db::get_photo(&state.pool, &id).await.map_err(|e| match e { 14 + sqlx::Error::RowNotFound => AppError::NotFound, 15 + _ => AppError::DbError(e), 16 + })?; 17 + 14 18 let tags = db::get_photo_tags(&state.pool, &id).await?; 15 19 let aperture = format!("{:.1}", photo.aperture); 16 20 let focal_length = format!("{:.0}", photo.focal_length);