Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Basic server semver version check and ⬆️ Update epub-rs dep #270

Merged
merged 2 commits into from
Feb 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion apps/server/src/routers/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ mod tests {

use super::v1::{
auth::*, book_club::*, epub::*, job::*, library::*, media::*, metadata::*,
series::*, smart_list::*, user::*, ClaimResponse, StumpVersion,
series::*, smart_list::*, user::*, ClaimResponse, StumpVersion, UpdateCheck,
};

#[allow(dead_code)]
Expand Down Expand Up @@ -55,6 +55,7 @@ mod tests {
file.write_all(b"// SERVER TYPE GENERATION\n\n")?;

file.write_all(format!("{}\n\n", ts_export::<StumpVersion>()?).as_bytes())?;
file.write_all(format!("{}\n\n", ts_export::<UpdateCheck>()?).as_bytes())?;
file.write_all(
format!("{}\n\n", ts_export::<LoginOrRegisterArgs>()?).as_bytes(),
)?;
Expand Down
63 changes: 62 additions & 1 deletion apps/server/src/routers/api/v1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ use axum::{
routing::{get, post},
Json, Router,
};
use hyper::header::USER_AGENT;
use serde::{Deserialize, Serialize};
use specta::Type;
use utoipa::ToSchema;

use crate::{config::state::AppState, errors::ApiResult};
use crate::{
config::state::AppState,
errors::{ApiError, ApiResult},
};

pub(crate) mod auth;
pub(crate) mod book_club;
Expand Down Expand Up @@ -44,7 +48,9 @@ pub(crate) fn mount(app_state: AppState) -> Router<AppState> {
.merge(book_club::mount(app_state))
.route("/claim", get(claim))
.route("/ping", get(ping))
// TODO: should /version or /check-for-updates be behind any auth reqs?
.route("/version", post(version))
.route("/check-for-update", get(check_for_updates))
}

#[derive(Serialize, Type, ToSchema)]
Expand Down Expand Up @@ -102,3 +108,58 @@ async fn version() -> ApiResult<Json<StumpVersion>> {
compile_time: env!("STATIC_BUILD_DATE").to_string(),
}))
}

#[derive(Serialize, Deserialize, Type, ToSchema)]
pub struct UpdateCheck {
current_semver: String,
latest_semver: String,
has_update_available: bool,
}

#[utoipa::path(
get,
path = "/api/v1/check-for-update",
tag = "util",
responses(
(status = 200, description = "Check for updates", body = UpdateCheck)
)
)]
async fn check_for_updates() -> ApiResult<Json<UpdateCheck>> {
let current_semver = env!("CARGO_PKG_VERSION").to_string();

let client = reqwest::Client::new();
let github_response = client
.get("https://api.github.com/repos/stumpapp/stump/releases/latest")
.header(USER_AGENT, "stumpapp/stump")
.send()
.await?;

if github_response.status().is_success() {
let github_json: serde_json::Value = github_response.json().await?;

let latest_semver = github_json["tag_name"].as_str().ok_or_else(|| {
ApiError::InternalServerError(
"Failed to parse latest release tag name".to_string(),
)
})?;
let has_update_available = latest_semver != current_semver;

Ok(Json(UpdateCheck {
current_semver,
latest_semver: latest_semver.to_string(),
has_update_available,
}))
} else {
match github_response.status().as_u16() {
404 => Ok(Json(UpdateCheck {
current_semver,
latest_semver: "unknown".to_string(),
has_update_available: false,
})),
_ => Err(ApiError::InternalServerError(format!(
"Failed to fetch latest release: {}",
github_response.status()
))),
}
}
}
3 changes: 1 addition & 2 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ infer = "0.15.0"
image = "0.24.7"
webp = "0.2.6"
zip = "0.6.6"
epub = "1.2.4"
# unrar = { git = "https://github.com/stumpapp/unrar.rs", branch = "feature/typestate" }
epub = { git = "https://github.com/stumpapp/epub-rs", rev = "38e091abe96875952556ab7dec195022d0230e14" }
unrar = { version = "0.5.2" }
# pdf = "0.8.1"
pdf = { git = "https://github.com/pdf-rs/pdf", rev = "3bc9e636d31b1846e51b58c7429914e640866f53" } # TODO: revert back to crates.io once fix(es) release
Expand Down
123 changes: 56 additions & 67 deletions core/src/filesystem/media/epub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ use crate::{
filesystem::{content_type::ContentType, error::FileError, hash},
};
use epub::doc::EpubDoc;
use tracing::{debug, error, trace, warn};

use super::process::{FileProcessor, FileProcessorOptions, ProcessedFile};

// TODO: lots of smells in this file, needs a touch up :)

/// A file processor for EPUB files.
pub struct EpubProcessor;

Expand All @@ -29,14 +30,14 @@ impl FileProcessor for EpubProcessor {
}

if i > 0 {
epub_file
.set_current_page(i)
.map_err(|e| FileError::EpubReadError(e.to_string()))?;
epub_file.set_current_page(i);
}

let chapter_buffer = epub_file
.get_current()
.map_err(|e| FileError::EpubReadError(e.to_string()))?;
let (chapter_buffer, _) = epub_file.get_current().ok_or_else(|| {
FileError::EpubReadError(
"Failed to get chapter from epub file".to_string(),
)
})?;
let chapter_size = chapter_buffer.len() as u64;

sample_size += chapter_size;
Expand All @@ -52,7 +53,7 @@ impl FileProcessor for EpubProcessor {
match hash::generate(path, sample) {
Ok(digest) => Some(digest),
Err(e) => {
debug!(error = ?e, path, "Failed to digest epub file");
tracing::debug!(error = ?e, path, "Failed to digest epub file");
None
},
}
Expand All @@ -66,7 +67,7 @@ impl FileProcessor for EpubProcessor {
_: FileProcessorOptions,
_: &StumpConfig,
) -> Result<ProcessedFile, FileError> {
debug!(?path, "processing epub");
tracing::debug!(?path, "processing epub");

let path_buf = PathBuf::from(path);
let epub_file = Self::open(path)?;
Expand Down Expand Up @@ -112,18 +113,19 @@ impl FileProcessor for EpubProcessor {
continue;
}

epub_file.set_current_page(chapter as usize).map_err(|e| {
error!("Failed to get chapter from epub file: {}", e);
FileError::EpubReadError(e.to_string())
})?;
if !epub_file.set_current_page(chapter as usize) {
tracing::error!(path, chapter, "Failed to get chapter from epub file!");
return Err(FileError::EpubReadError(
"Failed to get chapter from epub file".to_string(),
));
}

let content_type = match epub_file.get_current_mime() {
Ok(mime) => ContentType::from(mime.as_str()),
Err(e) => {
error!(
error = ?e,
Some(mime) => ContentType::from(mime.as_str()),
None => {
tracing::error!(
chapter_path = ?path,
"Failed to get explicit resource mime for chapter. Returning default.",
"Failed to get explicit resource mime for chapter. Returning XHTML",
);

ContentType::XHTML
Expand Down Expand Up @@ -152,24 +154,20 @@ impl EpubProcessor {
/// returned.
pub fn get_cover(path: &str) -> Result<(ContentType, Vec<u8>), FileError> {
let mut epub_file = EpubDoc::new(path).map_err(|e| {
error!("Failed to open epub file: {}", e);
tracing::error!("Failed to open epub file: {}", e);
FileError::EpubOpenError(e.to_string())
})?;

let cover_id = epub_file.get_cover_id().unwrap_or_else(|_| {
debug!("Epub file does not contain cover metadata");
let cover_id = epub_file.get_cover_id().unwrap_or_else(|| {
tracing::debug!("Epub file does not contain cover metadata");
DEFAULT_EPUB_COVER_ID.to_string()
});

if let Ok(cover) = epub_file.get_resource(&cover_id) {
let mime = epub_file
.get_resource_mime(&cover_id)
.unwrap_or_else(|_| "image/png".to_string());

return Ok((ContentType::from(mime.as_str()), cover));
if let Some((buf, mime)) = epub_file.get_resource(&cover_id) {
return Ok((ContentType::from(mime.as_str()), buf));
}

debug!(
tracing::debug!(
"Explicit cover image could not be found, falling back to searching for best match..."
);
// FIXME: this is hack, i do NOT want to clone this entire hashmap...
Expand All @@ -182,7 +180,7 @@ impl EpubProcessor {
.any(|accepted_mime| accepted_mime == mime)
})
.map(|(id, (path, _))| {
trace!(name = ?path, "Found possible cover image");
tracing::trace!(name = ?path, "Found possible cover image");
// I want to weight the results based on how likely they are to be the cover.
// For example, if the cover is named "cover.jpg", it's probably the cover.
// TODO: this is SUPER naive, and should be improved at some point...
Expand All @@ -196,16 +194,12 @@ impl EpubProcessor {
.max_by_key(|(weight, _)| *weight);

if let Some((_, id)) = search_result {
if let Ok(c) = epub_file.get_resource(id) {
let mime = epub_file
.get_resource_mime(id)
.unwrap_or_else(|_| "image/png".to_string());

return Ok((ContentType::from(mime.as_str()), c));
if let Some((buf, mime)) = epub_file.get_resource(id) {
return Ok((ContentType::from(mime.as_str()), buf));
}
}

error!("Failed to find cover for epub file");
tracing::error!("Failed to find cover for epub file");
Err(FileError::EpubReadError(
"Failed to find cover for epub file".to_string(),
))
Expand All @@ -217,23 +211,24 @@ impl EpubProcessor {
) -> Result<(ContentType, Vec<u8>), FileError> {
let mut epub_file = Self::open(path)?;

epub_file.set_current_page(chapter).map_err(|e| {
error!("Failed to get chapter from epub file: {}", e);
FileError::EpubReadError(e.to_string())
})?;
if !epub_file.set_current_page(chapter) {
tracing::error!(path, chapter, "Failed to get chapter from epub file!");
return Err(FileError::EpubReadError(
"Failed to get chapter from epub file".to_string(),
));
}

let content = epub_file.get_current_with_epub_uris().map_err(|e| {
error!("Failed to get chapter from epub file: {}", e);
tracing::error!("Failed to get chapter from epub file: {}", e);
FileError::EpubReadError(e.to_string())
})?;

let content_type = match epub_file.get_current_mime() {
Ok(mime) => ContentType::from(mime.as_str()),
Err(e) => {
error!(
error = ?e,
Some(mime) => ContentType::from(mime.as_str()),
None => {
tracing::error!(
chapter_path = ?path,
"Failed to get explicit resource mime for chapter. Returning default.",
"Failed to get explicit resource mime for chapter. Returning XHTML",
);

ContentType::XHTML
Expand All @@ -249,17 +244,12 @@ impl EpubProcessor {
) -> Result<(ContentType, Vec<u8>), FileError> {
let mut epub_file = Self::open(path)?;

let contents = epub_file.get_resource(resource_id).map_err(|e| {
error!("Failed to get resource: {}", e);
FileError::EpubReadError(e.to_string())
let (buf, mime) = epub_file.get_resource(resource_id).ok_or_else(|| {
tracing::error!("Failed to get resource: {}", resource_id);
FileError::EpubReadError("Failed to get resource".to_string())
})?;

let content_type = epub_file.get_resource_mime(resource_id).map_err(|e| {
error!("Failed to get resource mime: {}", e);
FileError::EpubReadError(e.to_string())
})?;

Ok((ContentType::from(content_type.as_str()), contents))
Ok((ContentType::from(mime.as_str()), buf))
}

pub fn get_resource_by_path(
Expand All @@ -273,22 +263,21 @@ impl EpubProcessor {

let contents = epub_file
.get_resource_by_path(adjusted_path.as_path())
.map_err(|e| {
error!("Failed to get resource: {}", e);
FileError::EpubReadError(e.to_string())
.ok_or_else(|| {
tracing::error!(?adjusted_path, "Failed to get resource!");
FileError::EpubReadError("Failed to get resource".to_string())
})?;

// Note: If the resource does not have an entry in the `resources` map, then loading the content
// type will fail. This seems to only happen when loading the root file (e.g. container.xml,
// package.opf, etc.).
let content_type =
match epub_file.get_resource_mime_by_path(adjusted_path.as_path()) {
Ok(mime) => ContentType::from(mime.as_str()),
Err(e) => {
warn!(
"Failed to get explicit definition of resource mime for {}: {}",
adjusted_path.as_path().to_str().unwrap(),
e
Some(mime) => ContentType::from(mime.as_str()),
None => {
tracing::warn!(
?adjusted_path,
"Failed to get explicit definition of resource mime",
);

ContentType::from_path(adjusted_path.as_path())
Expand Down Expand Up @@ -318,7 +307,7 @@ impl EpubProcessor {
// 6. convert back to bytes

let content_str = String::from_utf8(content).map_err(|e| {
error!(error = ?e, "Failed to HTML buffer content to string");
tracing::error!(error = ?e, "Failed to HTML buffer content to string");
FileError::EpubReadError(e.to_string())
})?;

Expand All @@ -341,13 +330,13 @@ pub(crate) fn normalize_resource_path(path: PathBuf, root: &str) -> PathBuf {

// This below won't work since these paths are INSIDE the epub file >:(
// adjusted_path = adjusted_path.canonicalize().unwrap_or_else(|err| {
// // warn!(
// // tracing::warn!(
// // "Failed to safely canonicalize path {}: {}",
// // adjusted_path.display(),
// // err
// // );

// warn!(
// tracing::warn!(
// "Failed to safely canonicalize path {}: {}",
// adjusted_path.display(),
// err
Expand Down
6 changes: 5 additions & 1 deletion interface/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,11 @@
"helmet": "General server settings",
"title": "General settings",
"description": "General settings related to your Stump server instance",
"sections": {}
"sections": {
"updateAvailable": {
"message": "Your server is not up to date. Please update to the latest version!"
}
}
},
"server/jobs": {
"helmet": "Jobs",
Expand Down
Loading
Loading