Compare commits

..

3 Commits

Author SHA1 Message Date
f4ee1c19ec
make screenshot in readme smaller
Some checks failed
Build Multiarch Container Image / call-reusable-workflow (push) Failing after 1s
2024-05-11 09:30:30 +02:00
ee764ca4ca
common download handler 2024-05-11 09:27:33 +02:00
13aae44163
stream covers (chunked encoding) 2024-05-11 09:06:08 +02:00
8 changed files with 58 additions and 68 deletions

View File

@ -12,7 +12,7 @@ insist on writing my own containers). _How hard can it be_ I thought and went on
hacking something together. The result does at most one tenth of what cops can hacking something together. The result does at most one tenth of what cops can
do but luckily enough it is the part I need for myself. do but luckily enough it is the part I need for myself.
![Screenshot](screenshot.png) ![Screenshot](screenshot.jpg)
# Building # Building

View File

@ -4,12 +4,20 @@ use std::sync::Arc;
use calibre_db::data::pagination::SortOrder; use calibre_db::data::pagination::SortOrder;
use poem::{ use poem::{
error::NotFoundError,
handler, handler,
web::{Data, Path}, web::{Data, Path},
Response, Response,
}; };
use tokio::fs::File;
use crate::{app_state::AppState, Accept}; use crate::{
app_state::AppState,
data::book::{Book, Format},
handlers::error::HandlerError,
opds::media_type::MediaType,
Accept,
};
/// Handle a request for multiple books, starting at the first. /// Handle a request for multiple books, starting at the first.
#[handler] #[handler]
@ -31,6 +39,31 @@ pub async fn handler(
books(&accept, &state, Some(&cursor), &sort_order).await books(&accept, &state, Some(&cursor), &sort_order).await
} }
/// Handle a request for a book with id `id` in format `format`.
#[handler]
pub async fn handler_download(
Path((id, format)): Path<(u64, String)>,
state: Data<&Arc<AppState>>,
) -> Result<Response, poem::Error> {
let book = state
.calibre
.scalar_book(id)
.map_err(HandlerError::DataError)?;
let book = Book::full_book(&book, &state).ok_or(NotFoundError)?;
let format = Format(format);
let file_name = book.formats.get(&format).ok_or(NotFoundError)?;
let file_path = state
.config
.library_path
.join(book.data.path)
.join(file_name);
let mut file = File::open(file_path).await.map_err(|_| NotFoundError)?;
let content_type: MediaType = format.into();
let content_type = format!("{content_type}");
crate::handlers::download::handler(file_name, file, &content_type).await
}
async fn books( async fn books(
accept: &Accept, accept: &Accept,
state: &Arc<AppState>, state: &Arc<AppState>,

View File

@ -1,31 +1,24 @@
//! Handle requests for cover images. //! Handle requests for cover images.
use std::{fs::File, io::Read, sync::Arc}; use std::sync::Arc;
use crate::{app_state::AppState, handlers::error::HandlerError};
use poem::{ use poem::{
error::NotFoundError, error::NotFoundError,
handler, handler,
web::{headers::ContentType, Data, Path, WithContentType}, web::{headers::ContentType, Data, Path},
IntoResponse, Response,
}; };
use tokio::fs::File;
use crate::{app_state::AppState, handlers::error::HandlerError};
/// Handle a request for the cover image of book with id `id`. /// Handle a request for the cover image of book with id `id`.
#[handler] #[handler]
pub async fn handler( pub async fn handler(id: Path<u64>, state: Data<&Arc<AppState>>) -> Result<Response, poem::Error> {
id: Path<u64>,
state: Data<&Arc<AppState>>,
) -> Result<WithContentType<Vec<u8>>, poem::Error> {
let book = state let book = state
.calibre .calibre
.scalar_book(*id) .scalar_book(*id)
.map_err(HandlerError::DataError)?; .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).await.map_err(|_| NotFoundError)?;
crate::handlers::download::handler("cover.jpg", cover, &ContentType::jpeg().to_string()).await
let mut data = Vec::new();
cover.read_to_end(&mut data).map_err(|_| NotFoundError)?;
Ok(data.with_content_type(ContentType::jpeg().to_string()))
} }

View File

@ -1,50 +1,23 @@
//! Handle requests for specific formats of a book. //! Handle requests for specific formats of a book.
use std::sync::Arc; use tokio::io::AsyncRead;
use tokio::fs::File; use poem::{Body, IntoResponse, Response};
use poem::{
error::NotFoundError,
handler,
web::{Data, Path},
Body, IntoResponse, Response,
};
use tokio_util::io::ReaderStream; use tokio_util::io::ReaderStream;
use crate::{ /// Handle a request for file.
app_state::AppState, ///
data::book::{Book, Format}, /// Must not be used directly from a route as that makes it vulnerable to path traversal attacks.
handlers::error::HandlerError, pub async fn handler<A: AsyncRead + Send + 'static>(
opds::media_type::MediaType, file_name: &str,
}; reader: A,
content_type: &str,
/// Handle a request for a book with id `id` in format `format`.
#[handler]
pub async fn handler(
Path((id, format)): Path<(u64, String)>,
state: Data<&Arc<AppState>>,
) -> Result<Response, poem::Error> { ) -> Result<Response, poem::Error> {
let book = state let stream = ReaderStream::new(reader);
.calibre
.scalar_book(id)
.map_err(HandlerError::DataError)?;
let book = Book::full_book(&book, &state).ok_or(NotFoundError)?;
let format = Format(format);
let file_name = book.formats.get(&format).ok_or(NotFoundError)?;
let file_path = state
.config
.library_path
.join(book.data.path)
.join(file_name);
let mut file = File::open(file_path).await.map_err(|_| NotFoundError)?;
let stream = ReaderStream::new(file);
let body = Body::from_bytes_stream(stream); let body = Body::from_bytes_stream(stream);
let content_type: MediaType = format.into();
Ok(body Ok(body
.with_content_type(format!("{content_type}")) .with_content_type(content_type)
.with_header("Content-Disposition", format!("filename=\"{file_name}\"")) .with_header("Content-Disposition", format!("filename=\"{file_name}\""))
.into_response()) .into_response())
} }

View File

@ -1,20 +1,11 @@
use crate::{APP_NAME, VERSION}; use crate::{APP_NAME, VERSION};
use poem::{handler, Body, IntoResponse, Response}; use poem::{handler, Response};
use tokio_util::io::ReaderStream;
const SOURCE_ARCHIVE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/archive.zip")); const SOURCE_ARCHIVE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/archive.zip"));
/// Handle a request for source code of the server.. /// Handle a request for source code of the server..
#[handler] #[handler]
pub async fn handler() -> Result<Response, poem::Error> { pub async fn handler() -> Result<Response, poem::Error> {
let stream = ReaderStream::new(SOURCE_ARCHIVE); let file_name = format!("{APP_NAME}-{VERSION}.zip");
let body = Body::from_bytes_stream(stream); crate::handlers::download::handler(&file_name, SOURCE_ARCHIVE, "application/zip").await
Ok(body
.with_content_type("application/zip")
.with_header(
"Content-Disposition",
format!("filename=\"{APP_NAME}-{VERSION}.zip\""),
)
.into_response())
} }

View File

@ -109,7 +109,7 @@ pub async fn run(config: Config) -> Result<(), std::io::Error> {
get(handlers::authors::handler), get(handlers::authors::handler),
) )
.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::books::handler_download))
.at("/archive", get(handlers::source_archive::handler)) .at("/archive", get(handlers::source_archive::handler))
.nest("/static", EmbeddedFilesEndpoint::<Files>::new()) .nest("/static", EmbeddedFilesEndpoint::<Files>::new())
.data(Accept::Html); .data(Accept::Html);

BIN
screenshot.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 675 KiB