tangled
alpha
login
or
join now
ericwood.org
/
photos-site
1
fork
atom
A little app to serve my photography from my personal website
1
fork
atom
overview
issues
pulls
pipelines
basic 404 handling
ericwood.org
4 months ago
72477b96
996fde45
0/0
Waiting for spindle ...
+42
-22
6 changed files
expand all
collapse all
unified
split
Cargo.lock
Cargo.toml
src
app_error.rs
db.rs
main.rs
routes
photos
show.rs
+1
Cargo.lock
···
1136
1136
"serde",
1137
1137
"serde_html_form",
1138
1138
"sqlx",
1139
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
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
3
-
response::{IntoResponse, Response},
3
3
+
response::{Html, IntoResponse, Response},
4
4
};
5
5
6
6
-
pub struct AppError(anyhow::Error);
6
6
+
#[derive(thiserror::Error, Debug)]
7
7
+
pub enum AppError {
8
8
+
#[error("not found")]
9
9
+
NotFound,
7
10
8
8
-
impl IntoResponse for AppError {
9
9
-
fn into_response(self) -> Response {
10
10
-
(
11
11
-
StatusCode::INTERNAL_SERVER_ERROR,
12
12
-
format!("Something went wrong: {}", self.0),
13
13
-
)
14
14
-
.into_response()
11
11
+
#[error("internal server error")]
12
12
+
DbError(#[from] sqlx::Error),
13
13
+
14
14
+
#[error("internal server error")]
15
15
+
Anyhow(#[from] anyhow::Error),
16
16
+
}
17
17
+
18
18
+
impl AppError {
19
19
+
pub fn status_code(&self) -> StatusCode {
20
20
+
match self {
21
21
+
Self::NotFound => StatusCode::NOT_FOUND,
22
22
+
Self::DbError(_) | Self::Anyhow(_) => StatusCode::INTERNAL_SERVER_ERROR,
23
23
+
}
15
24
}
16
25
}
17
26
18
18
-
impl<E> From<E> for AppError
19
19
-
where
20
20
-
E: Into<anyhow::Error>,
21
21
-
{
22
22
-
fn from(err: E) -> Self {
23
23
-
Self(err.into())
27
27
+
impl IntoResponse for AppError {
28
28
+
fn into_response(self) -> Response {
29
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
81
-
pub async fn get_photos(pool: &SqlitePool, pq: PhotoQuery) -> anyhow::Result<(u32, Vec<Photo>)> {
81
81
+
pub async fn get_photos(
82
82
+
pool: &SqlitePool,
83
83
+
pq: PhotoQuery,
84
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
147
-
pub async fn get_photo(pool: &SqlitePool, id: &String) -> anyhow::Result<Photo> {
150
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
1
-
use axum::{Router, routing::get};
1
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
32
-
.route("/", get(routes::photos::index))
33
33
-
.route("/{id}", get(routes::photos::show))
32
32
+
.route("/photos", get(routes::photos::index))
33
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
37
-
.nest_service("/images", ServeDir::new(&env::var("IMAGE_PATH")?));
37
37
+
.nest_service("/images", ServeDir::new(&env::var("IMAGE_PATH")?))
38
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
44
+
45
45
+
async fn handler_404() -> anyhow::Result<Html<String>, app_error::AppError> {
46
46
+
Err(AppError::NotFound)
47
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
13
-
let photo = db::get_photo(&state.pool, &id).await?;
13
13
+
let photo = db::get_photo(&state.pool, &id).await.map_err(|e| match e {
14
14
+
sqlx::Error::RowNotFound => AppError::NotFound,
15
15
+
_ => AppError::DbError(e),
16
16
+
})?;
17
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);