opds error handling

This commit is contained in:
Sebastian Hugentobler 2024-05-09 08:39:46 +02:00
parent faea154ff5
commit 93aeb80c56
Signed by: shu
GPG Key ID: BB32CF3CA052C2F0
12 changed files with 94 additions and 47 deletions

View File

@ -9,7 +9,7 @@ use poem::{
use tera::Context; use tera::Context;
use crate::{ use crate::{
app_state::AppState, data::book::Book, handlers::error::SqliteError, templates::TEMPLATES, app_state::AppState, data::book::Book, handlers::error::HandlerError, templates::TEMPLATES,
}; };
#[handler] #[handler]
@ -17,11 +17,14 @@ pub async fn handler(
id: Path<u64>, id: Path<u64>,
state: Data<&Arc<AppState>>, state: Data<&Arc<AppState>>,
) -> Result<Html<String>, poem::Error> { ) -> Result<Html<String>, poem::Error> {
let author = state.calibre.scalar_author(*id).map_err(SqliteError)?; let author = state
.calibre
.scalar_author(*id)
.map_err(HandlerError::DataError)?;
let books = state let books = state
.calibre .calibre
.author_books(*id, u32::MAX.into(), None, SortOrder::ASC) .author_books(*id, u32::MAX.into(), None, SortOrder::ASC)
.map_err(SqliteError)?; .map_err(HandlerError::DataError)?;
let books = books let books = books
.iter() .iter()
.filter_map(|x| Book::full_book(x, &state)) .filter_map(|x| Book::full_book(x, &state))

View File

@ -7,14 +7,17 @@ use poem::{
IntoResponse, IntoResponse,
}; };
use crate::{app_state::AppState, handlers::error::SqliteError}; use crate::{app_state::AppState, handlers::error::HandlerError};
#[handler] #[handler]
pub async fn handler( pub async fn handler(
id: Path<u64>, id: Path<u64>,
state: Data<&Arc<AppState>>, state: Data<&Arc<AppState>>,
) -> Result<WithContentType<Vec<u8>>, poem::Error> { ) -> Result<WithContentType<Vec<u8>>, poem::Error> {
let book = state.calibre.scalar_book(*id).map_err(SqliteError)?; let book = state
.calibre
.scalar_book(*id)
.map_err(HandlerError::DataError)?;
let cover_path = state.config.library_path.join(book.path).join("cover.jpg"); let cover_path = state.config.library_path.join(book.path).join("cover.jpg");
let mut cover = File::open(cover_path).map_err(|_| NotFoundError)?; let mut cover = File::open(cover_path).map_err(|_| NotFoundError)?;

View File

@ -7,14 +7,17 @@ use poem::{
IntoResponse, IntoResponse,
}; };
use crate::{app_state::AppState, data::book::Book, handlers::error::SqliteError}; use crate::{app_state::AppState, data::book::Book, handlers::error::HandlerError};
#[handler] #[handler]
pub async fn handler( pub async fn handler(
Path((id, format)): Path<(u64, String)>, Path((id, format)): Path<(u64, String)>,
state: Data<&Arc<AppState>>, state: Data<&Arc<AppState>>,
) -> Result<WithHeader<WithContentType<Vec<u8>>>, poem::Error> { ) -> Result<WithHeader<WithContentType<Vec<u8>>>, poem::Error> {
let book = state.calibre.scalar_book(id).map_err(SqliteError)?; let book = state
.calibre
.scalar_book(id)
.map_err(HandlerError::DataError)?;
let book = Book::full_book(&book, &state).ok_or(NotFoundError)?; let book = Book::full_book(&book, &state).ok_or(NotFoundError)?;
let format: &str = format.as_str(); let format: &str = format.as_str();
let file_name = book.formats.get(format).ok_or(NotFoundError)?; let file_name = book.formats.get(format).ok_or(NotFoundError)?;

View File

@ -1,32 +1,40 @@
use calibre_db::data::error::DataStoreError; use calibre_db::data::error::DataStoreError;
use poem::{error::ResponseError, http::StatusCode, Body, Response}; use poem::{error::ResponseError, http::StatusCode, Body, Response};
use thiserror::Error;
use tracing::error; use tracing::error;
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug, thiserror::Error)] use crate::opds::error::OpdsError;
#[error("sqlite error")]
pub struct SqliteError(pub DataStoreError);
impl From<DataStoreError> for SqliteError { #[derive(Error, Debug)]
fn from(item: DataStoreError) -> Self { #[error("opds error")]
SqliteError(item) pub enum HandlerError {
} #[error("opds error")]
OpdsError(#[from] OpdsError),
#[error("data error")]
DataError(#[from] DataStoreError),
} }
impl ResponseError for SqliteError { impl ResponseError for HandlerError {
fn status(&self) -> StatusCode { fn status(&self) -> StatusCode {
match &self.0 { match &self {
HandlerError::OpdsError(e) => StatusCode::INTERNAL_SERVER_ERROR,
HandlerError::DataError(e) => match e {
DataStoreError::NoResults(_) => StatusCode::NOT_FOUND, DataStoreError::NoResults(_) => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
},
} }
} }
fn as_response(&self) -> Response { fn as_response(&self) -> Response {
let id = Uuid::new_v4(); let id = Uuid::new_v4();
let internal_msg = format!("{:?}", self); let internal_msg = format!("{:?}", self);
let external_msg = match &self.0 { let external_msg = match &self {
HandlerError::OpdsError(e) => "internal server error",
HandlerError::DataError(e) => match e {
DataStoreError::NoResults(_) => "item not found", DataStoreError::NoResults(_) => "item not found",
_ => "internal server error", _ => "internal server error",
},
}; };
error!("{id}: {internal_msg}"); error!("{id}: {internal_msg}");

View File

@ -2,14 +2,14 @@ use std::sync::Arc;
use poem::{ use poem::{
handler, handler,
web::{headers::ContentType, Data, WithContentType}, web::{Data, WithContentType},
IntoResponse, IntoResponse,
}; };
use quick_xml::se::to_string; use time::OffsetDateTime;
use time::macros::datetime;
use crate::{ use crate::{
app_state::AppState, app_state::AppState,
handlers::error::HandlerError,
opds::{ opds::{
author::Author, content::Content, entry::Entry, feed::Feed, link::Link, author::Author, content::Content, entry::Entry, feed::Feed, link::Link,
media_type::MediaType, relation::Relation, media_type::MediaType, relation::Relation,
@ -18,6 +18,8 @@ use crate::{
#[handler] #[handler]
pub async fn handler(state: Data<&Arc<AppState>>) -> Result<WithContentType<String>, poem::Error> { pub async fn handler(state: Data<&Arc<AppState>>) -> Result<WithContentType<String>, poem::Error> {
let now = OffsetDateTime::now_utc();
let author = Author { let author = Author {
name: "Thallian".to_string(), name: "Thallian".to_string(),
uri: "https://code.vanwa.ch/shu/rusty-library".to_string(), uri: "https://code.vanwa.ch/shu/rusty-library".to_string(),
@ -40,7 +42,7 @@ pub async fn handler(state: Data<&Arc<AppState>>) -> Result<WithContentType<Stri
let books_entry = Entry { let books_entry = Entry {
title: "Books".to_string(), title: "Books".to_string(),
id: "rusty:books".to_string(), id: "rusty:books".to_string(),
updated: datetime!(2024-05-06 19:14:54 UTC), updated: now,
content: Content { content: Content {
media_type: MediaType::Text, media_type: MediaType::Text,
content: "Index of all books".to_string(), content: "Index of all books".to_string(),
@ -57,12 +59,13 @@ pub async fn handler(state: Data<&Arc<AppState>>) -> Result<WithContentType<Stri
let feed = Feed { let feed = Feed {
title: "rusty-library".to_string(), title: "rusty-library".to_string(),
id: "rusty:catalog".to_string(), id: "rusty:catalog".to_string(),
updated: datetime!(2024-05-06 19:14:54 UTC), updated: now,
icon: "favicon.ico".to_string(), icon: "favicon.ico".to_string(),
author, author,
links: vec![home_link, self_link], links: vec![home_link, self_link],
entries: vec![books_entry], entries: vec![books_entry],
}; };
let xml = feed.as_xml(); let xml = feed.as_xml().map_err(HandlerError::OpdsError)?;
Ok(xml.with_content_type("application/atom+xml")) Ok(xml.with_content_type("application/atom+xml"))
} }

View File

@ -7,7 +7,7 @@ use tera::Context;
use crate::templates::TEMPLATES; use crate::templates::TEMPLATES;
use super::error::SqliteError; use super::error::HandlerError;
pub fn render<T: Serialize + Debug, F, S, P, M>( pub fn render<T: Serialize + Debug, F, S, P, M>(
template: &str, template: &str,
@ -22,7 +22,7 @@ where
P: Fn(&str) -> Result<bool, DataStoreError>, P: Fn(&str) -> Result<bool, DataStoreError>,
M: Fn(&str) -> Result<bool, DataStoreError>, M: Fn(&str) -> Result<bool, DataStoreError>,
{ {
let items = fetcher().map_err(SqliteError)?; let items = fetcher().map_err(HandlerError::DataError)?;
let mut context = Context::new(); let mut context = Context::new();
@ -32,8 +32,8 @@ where
let (backward_cursor, forward_cursor) = (sort_field(first_item), sort_field(last_item)); let (backward_cursor, forward_cursor) = (sort_field(first_item), sort_field(last_item));
let has_previous = has_previous(&backward_cursor).map_err(SqliteError)?; let has_previous = has_previous(&backward_cursor).map_err(HandlerError::DataError)?;
let has_more = has_more(&forward_cursor).map_err(SqliteError)?; let has_more = has_more(&forward_cursor).map_err(HandlerError::DataError)?;
context.insert("has_previous", &has_previous); context.insert("has_previous", &has_previous);
context.insert("has_more", &has_more); context.insert("has_more", &has_more);

View File

@ -8,12 +8,15 @@ use poem::{
use tera::Context; use tera::Context;
use crate::{ use crate::{
app_state::AppState, data::book::Book, handlers::error::SqliteError, templates::TEMPLATES, app_state::AppState, data::book::Book, handlers::error::HandlerError, templates::TEMPLATES,
}; };
#[handler] #[handler]
pub async fn handler(state: Data<&Arc<AppState>>) -> Result<Html<String>, poem::Error> { pub async fn handler(state: Data<&Arc<AppState>>) -> Result<Html<String>, poem::Error> {
let recent_books = state.calibre.recent_books(25).map_err(SqliteError)?; let recent_books = state
.calibre
.recent_books(25)
.map_err(HandlerError::DataError)?;
let recent_books = recent_books let recent_books = recent_books
.iter() .iter()
.filter_map(|x| Book::full_book(x, &state)) .filter_map(|x| Book::full_book(x, &state))

View File

@ -8,7 +8,7 @@ use poem::{
use tera::Context; use tera::Context;
use crate::{ use crate::{
app_state::AppState, data::book::Book, handlers::error::SqliteError, templates::TEMPLATES, app_state::AppState, data::book::Book, handlers::error::HandlerError, templates::TEMPLATES,
}; };
#[handler] #[handler]
@ -16,8 +16,14 @@ pub async fn handler(
id: Path<u64>, id: Path<u64>,
state: Data<&Arc<AppState>>, state: Data<&Arc<AppState>>,
) -> Result<Html<String>, poem::Error> { ) -> Result<Html<String>, poem::Error> {
let series = state.calibre.scalar_series(*id).map_err(SqliteError)?; let series = state
let books = state.calibre.series_books(*id).map_err(SqliteError)?; .calibre
.scalar_series(*id)
.map_err(HandlerError::DataError)?;
let books = state
.calibre
.series_books(*id)
.map_err(HandlerError::DataError)?;
let books = books let books = books
.iter() .iter()
.filter_map(|x| Book::full_book(x, &state)) .filter_map(|x| Book::full_book(x, &state))

View File

@ -6,7 +6,8 @@ use clap::Parser;
use cli::Cli; use cli::Cli;
use config::Config; use config::Config;
use poem::{ use poem::{
endpoint::EmbeddedFilesEndpoint, get, listener::TcpListener, EndpointExt, Route, Server, endpoint::EmbeddedFilesEndpoint, get, listener::TcpListener, middleware::Tracing, EndpointExt,
Route, Server,
}; };
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
@ -70,7 +71,8 @@ async fn main() -> Result<(), std::io::Error> {
.at("/cover/:id", get(handlers::cover::handler)) .at("/cover/:id", get(handlers::cover::handler))
.at("/book/:id/:format", get(handlers::download::handler)) .at("/book/:id/:format", get(handlers::download::handler))
.nest("/static", EmbeddedFilesEndpoint::<Files>::new()) .nest("/static", EmbeddedFilesEndpoint::<Files>::new())
.data(app_state); .data(app_state)
.with(Tracing);
Server::new(TcpListener::bind("[::]:3000")) Server::new(TcpListener::bind("[::]:3000"))
.name("cops-web") .name("cops-web")

View File

@ -0,0 +1,15 @@
use std::string::FromUtf8Error;
use quick_xml::DeError;
use thiserror::Error;
#[derive(Error, Debug)]
#[error("opds error")]
pub enum OpdsError {
#[error("failed to serialize struct")]
SerializingError(#[from] DeError),
#[error("xml failure")]
XmlError(#[from] quick_xml::Error),
#[error("failed to decode as utf-8")]
Utf8Error(#[from] FromUtf8Error),
}

View File

@ -8,7 +8,7 @@ use quick_xml::{
use serde::Serialize; use serde::Serialize;
use time::OffsetDateTime; use time::OffsetDateTime;
use super::{author::Author, entry::Entry, link::Link}; use super::{author::Author, entry::Entry, error::OpdsError, link::Link};
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename = "feed")] #[serde(rename = "feed")]
@ -26,14 +26,14 @@ pub struct Feed {
} }
impl Feed { impl Feed {
pub fn as_xml(&self) -> String { pub fn as_xml(&self) -> Result<String, OpdsError> {
let xml = to_string(&self).unwrap(); let xml = to_string(&self)?;
let mut reader = Reader::from_str(&xml); let mut reader = Reader::from_str(&xml);
reader.trim_text(true); reader.trim_text(true);
let declaration = BytesDecl::new("1.0", Some("UTF-8"), None); let declaration = BytesDecl::new("1.0", Some("UTF-8"), None);
let mut writer = Writer::new(Cursor::new(Vec::new())); let mut writer = Writer::new(Cursor::new(Vec::new()));
writer.write_event(Event::Decl(declaration)).unwrap(); writer.write_event(Event::Decl(declaration))?;
let mut feed_start = BytesStart::new("feed"); let mut feed_start = BytesStart::new("feed");
feed_start.push_attribute(("xmlns", "http://www.w3.org/2005/Atom")); feed_start.push_attribute(("xmlns", "http://www.w3.org/2005/Atom"));
@ -45,15 +45,15 @@ impl Feed {
loop { loop {
match reader.read_event() { match reader.read_event() {
Ok(Event::Start(e)) if e.name().as_ref() == b"feed" => writer Ok(Event::Start(e)) if e.name().as_ref() == b"feed" => {
.write_event(Event::Start(feed_start.clone())) writer.write_event(Event::Start(feed_start.clone()))?
.unwrap(), }
Ok(Event::Eof) => break, Ok(Event::Eof) => break,
Ok(e) => writer.write_event(e).unwrap(), Ok(e) => writer.write_event(e)?,
Err(e) => (), Err(e) => (),
} }
} }
let result = writer.into_inner().into_inner(); let result = writer.into_inner().into_inner();
String::from_utf8(result).unwrap() Ok(String::from_utf8(result)?)
} }
} }

View File

@ -1,6 +1,7 @@
pub mod author; pub mod author;
pub mod content; pub mod content;
pub mod entry; pub mod entry;
pub mod error;
pub mod feed; pub mod feed;
pub mod link; pub mod link;
pub mod media_type; pub mod media_type;