proper error handling
This commit is contained in:
parent
687c33829f
commit
ac7b0e7e88
15 changed files with 245 additions and 25 deletions
|
@ -9,7 +9,11 @@ clap = { version = "4.5.4", features = ["derive"] }
|
|||
once_cell = "1.19.0"
|
||||
poem = { version = "3.0.0", features = ["embed", "static-files"] }
|
||||
rust-embed = "8.3.0"
|
||||
serde = { workspace = true }
|
||||
serde_json = "1.0.116"
|
||||
tera = "1.19.1"
|
||||
thiserror = { workspace = true }
|
||||
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
uuid = { version = "1.8.0", features = ["v4", "fast-rng"] }
|
||||
|
|
29
cops-web/src/data/book.rs
Normal file
29
cops-web/src/data/book.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use calibre_db::data::{book::Book as DbBook, series::Series as DbSeries};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Book {
|
||||
pub id: u64,
|
||||
pub title: String,
|
||||
pub sort: String,
|
||||
pub path: String,
|
||||
pub author: String,
|
||||
pub series: Option<(String, f64)>,
|
||||
}
|
||||
|
||||
impl Book {
|
||||
pub fn from_db_book(
|
||||
db_book: &DbBook,
|
||||
db_series: Option<(DbSeries, f64)>,
|
||||
author: &str,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: db_book.id,
|
||||
title: db_book.title.clone(),
|
||||
sort: db_book.sort.clone(),
|
||||
path: db_book.path.clone(),
|
||||
author: author.to_string(),
|
||||
series: db_series.map(|x| (x.0.name, x.1)),
|
||||
}
|
||||
}
|
||||
}
|
40
cops-web/src/handlers/error.rs
Normal file
40
cops-web/src/handlers/error.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use calibre_db::data::error::DataStoreError;
|
||||
use poem::{error::ResponseError, http::StatusCode, Body, Response};
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("sqlite error")]
|
||||
pub struct SqliteError(pub DataStoreError);
|
||||
|
||||
impl From<DataStoreError> for SqliteError {
|
||||
fn from(item: DataStoreError) -> Self {
|
||||
SqliteError(item)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for SqliteError {
|
||||
fn status(&self) -> StatusCode {
|
||||
match &self.0 {
|
||||
DataStoreError::NoResults(_) => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::BAD_GATEWAY,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_response(&self) -> Response {
|
||||
let id = Uuid::new_v4();
|
||||
let internal_msg = self.to_string();
|
||||
let external_msg = match &self.0 {
|
||||
DataStoreError::NoResults(_) => "item not found",
|
||||
_ => "internal server error",
|
||||
};
|
||||
error!("{id}: {internal_msg}");
|
||||
|
||||
let body = Body::from_json(serde_json::json!({
|
||||
"id": id.to_string(),
|
||||
"message": external_msg,
|
||||
}))
|
||||
.unwrap();
|
||||
Response::builder().status(self.status()).body(body)
|
||||
}
|
||||
}
|
|
@ -7,11 +7,21 @@ use poem::{
|
|||
};
|
||||
use tera::Context;
|
||||
|
||||
use crate::{app_state::AppState, templates::TEMPLATES};
|
||||
use crate::{
|
||||
app_state::AppState, data::book::Book, handlers::error::SqliteError, templates::TEMPLATES,
|
||||
};
|
||||
|
||||
#[handler]
|
||||
pub async fn handler(state: Data<&Arc<AppState>>) -> Result<Html<String>, poem::Error> {
|
||||
let recent_books = state.calibre.recent_books(50).unwrap();
|
||||
let recent_books = state.calibre.recent_books(50).map_err(SqliteError)?;
|
||||
let recent_books = recent_books
|
||||
.iter()
|
||||
.filter_map(|x| {
|
||||
let author = state.calibre.book_author(x.id).ok()?;
|
||||
let series = state.calibre.book_series(x.id).ok()?;
|
||||
Some(Book::from_db_book(x, series, &author.name))
|
||||
})
|
||||
.collect::<Vec<Book>>();
|
||||
|
||||
let mut context = Context::new();
|
||||
context.insert("books", &recent_books);
|
||||
|
|
|
@ -14,8 +14,12 @@ mod app_state;
|
|||
mod basic_auth;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod data {
|
||||
pub mod book;
|
||||
}
|
||||
mod handlers {
|
||||
pub mod cover;
|
||||
pub mod error;
|
||||
pub mod recents;
|
||||
}
|
||||
mod templates;
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.book-card hgroup {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
|
|
|
@ -3,10 +3,30 @@
|
|||
<h1>Recent Books</h1>
|
||||
<div class="grid-container">
|
||||
{% for book in books %}
|
||||
<article class="book-card grid-item">
|
||||
<header>{{ book.title }}</header>
|
||||
<article class="book-card">
|
||||
<header class="grid-item">
|
||||
<hgroup>
|
||||
<h5>{{ book.title }}</h5>
|
||||
<!-- {% if book.series %}<p>{{ book.series.0 }} ({{ book.series.1 }})</p>{% endif %} -->
|
||||
<p>{{ book.author }}</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
<img class="cover" src="/cover/{{ book.id }}" alt="book cover">
|
||||
<!-- <footer>{{ book.title }}</footer> -->
|
||||
<footer>
|
||||
<form>
|
||||
<fieldset role="group">
|
||||
<details class="dropdown">
|
||||
<summary role="button" class="outline">
|
||||
Download
|
||||
</summary>
|
||||
<ul>
|
||||
<li><a href="#">Epub</a></li>
|
||||
<li><a href="#">Pdf</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</fieldset>
|
||||
</form>
|
||||
</footer>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue