opds error handling
This commit is contained in:
parent
faea154ff5
commit
93aeb80c56
@ -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))
|
||||||
|
@ -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)?;
|
||||||
|
|
||||||
|
@ -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)?;
|
||||||
|
@ -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 {
|
||||||
DataStoreError::NoResults(_) => StatusCode::NOT_FOUND,
|
HandlerError::OpdsError(e) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
HandlerError::DataError(e) => match e {
|
||||||
|
DataStoreError::NoResults(_) => StatusCode::NOT_FOUND,
|
||||||
|
_ => 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 {
|
||||||
DataStoreError::NoResults(_) => "item not found",
|
HandlerError::OpdsError(e) => "internal server error",
|
||||||
_ => "internal server error",
|
HandlerError::DataError(e) => match e {
|
||||||
|
DataStoreError::NoResults(_) => "item not found",
|
||||||
|
_ => "internal server error",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
error!("{id}: {internal_msg}");
|
error!("{id}: {internal_msg}");
|
||||||
|
|
||||||
|
@ -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"))
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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))
|
||||||
|
@ -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))
|
||||||
|
@ -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")
|
||||||
|
15
rusty-library/src/opds/error.rs
Normal file
15
rusty-library/src/opds/error.rs
Normal 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),
|
||||||
|
}
|
@ -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)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user