Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
30523ab
fix(sab): honour nzbname and stop naming addurl jobs after the URL
xbmc4lyfe Oct 9, 2026
102a3ab
fix(web): serialise config read-modify-write to stop lost updates
xbmc4lyfe Oct 9, 2026
a593772
fix(watch): import .zip and .nzb.bz2 archives from the watch folder
xbmc4lyfe Oct 9, 2026
6d05f7a
fix(nntp): mark connection Error on STAT pipeline I/O failures
xbmc4lyfe Oct 9, 2026
bd2b41e
fix(sab): honour the pp post-processing override on addfile/addurl
xbmc4lyfe Oct 9, 2026
8a639b2
fix(api): return 404/400 instead of 500 for unknown ids and invalid i…
xbmc4lyfe Oct 9, 2026
56dee53
fix(watch): claim watched NZBs before enqueueing and wait for writes …
xbmc4lyfe Oct 9, 2026
87635fa
fix(sab): accept .nzb.gz, .nzb.bz2 and .zip on addfile/addurl
xbmc4lyfe Oct 9, 2026
5d505b2
fix(sab): give mode=server_stats SABnzbd's response shape
xbmc4lyfe Oct 9, 2026
ebfb91c
fix(sab): answer mode=warnings instead of "Unknown mode"
xbmc4lyfe Oct 9, 2026
098bf57
fix(dispatch): keep pipelined article errors off the circuit breaker
xbmc4lyfe Oct 9, 2026
03a1f85
fix(dispatch): apply speed-limit changes to parked bandwidth waiters
xbmc4lyfe Oct 9, 2026
b1536a2
fix(queue): give same-named jobs unique complete folders
xbmc4lyfe Oct 9, 2026
f8dac41
fix(queue): reclaim retained incomplete work dirs
xbmc4lyfe Oct 9, 2026
13932cb
style: cargo fmt after the xbmc batch 2 cherry-picks
thedancingdeveloper Oct 9, 2026
1b6009e
chore(desktop): refresh src-tauri lockfile for nzb-web 0.4.23
thedancingdeveloper Oct 9, 2026
54e49e0
fix(web): close the three security blockers from the batch 2 review
thedancingdeveloper Oct 9, 2026
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
6 changes: 3 additions & 3 deletions Cargo.lock

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

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,15 @@ regex = "1"
walkdir = "2"
tar = "0.4"
flate2 = "1"
zip = { version = "8", default-features = false, features = ["deflate"] }
bzip2 = "0.6"
tokio-socks = "0.5"
notify = "7"
feed-rs = "2"
unicode-normalization = "0.1"

# Shared NZB crates
nzb-web = { version = "0.4.22", path = "crates/nzb-web", features = ["groups-db"] }
nzb-web = { version = "0.4.23", path = "crates/nzb-web", features = ["groups-db"] }
nzb-nntp = { version = "0.2.24", path = "crates/nzb-nntp" }
nzb-core = { version = "0.2.18", path = "crates/nzb-core", features = ["groups-db"] }
nzb-decode = { version = "0.1.5", path = "crates/nzb-decode" }
Expand Down
2 changes: 0 additions & 2 deletions apps/rustnzb/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ hex = { workspace = true }
rust-embed = { version = "8", features = ["debug-embed", "interpolate-folder-path"] }
mime_guess = "2"
libc = "0.2"
zip = { version = "8", default-features = false, features = ["deflate"] }
flate2 = { workspace = true }
parking_lot = { workspace = true }
tokio-util = { version = "0.7", features = ["rt"] }

Expand Down
55 changes: 39 additions & 16 deletions apps/rustnzb/src/group_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,33 @@
use std::sync::Arc;

use axum::Json;
use axum::extract::{Path, Query, State};
use axum::extract::{FromRequestParts, Path, Query, State};
use axum::http::request::Parts;
use serde::Deserialize;

use nzb_web::error::ApiError;
use nzb_web::state::AppState;

/// `Path` extractor whose rejection is a JSON `ApiError` 400 instead of
/// axum's plain-text parser message (which leaks Rust type names such as
/// "Cannot parse `abc` to a `i64`").
pub struct IdPath<T>(pub T);

impl<S, T> FromRequestParts<S> for IdPath<T>
where
S: Send + Sync,
T: serde::de::DeserializeOwned + Send,
{
type Rejection = ApiError;

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
Path::<T>::from_request_parts(parts, state)
.await
.map(|Path(value)| IdPath(value))
.map_err(|_| ApiError::bad_request("Invalid id in request path"))
}
}

#[derive(Deserialize, Default)]
pub struct GroupListQuery {
pub subscribed: Option<bool>,
Expand Down Expand Up @@ -98,7 +119,7 @@ pub async fn h_group_refresh(
/// GET /api/groups/{id}
pub async fn h_group_get(
State(state): State<Arc<AppState>>,
Path(id): Path<i64>,
IdPath(id): IdPath<i64>,
) -> Result<Json<serde_json::Value>, ApiError> {
let group = state
.queue_manager
Expand All @@ -113,7 +134,7 @@ pub async fn h_group_get(
/// GET /api/groups/{id}/status
pub async fn h_group_status(
State(state): State<Arc<AppState>>,
Path(id): Path<i64>,
IdPath(id): IdPath<i64>,
) -> Result<Json<serde_json::Value>, ApiError> {
let qm = &state.queue_manager;
let group = qm
Expand All @@ -140,7 +161,7 @@ pub async fn h_group_status(
/// POST /api/groups/{id}/subscribe
pub async fn h_group_subscribe(
State(state): State<Arc<AppState>>,
Path(id): Path<i64>,
IdPath(id): IdPath<i64>,
) -> Result<Json<serde_json::Value>, ApiError> {
state
.queue_manager
Expand All @@ -152,7 +173,7 @@ pub async fn h_group_subscribe(
/// POST /api/groups/{id}/unsubscribe
pub async fn h_group_unsubscribe(
State(state): State<Arc<AppState>>,
Path(id): Path<i64>,
IdPath(id): IdPath<i64>,
) -> Result<Json<serde_json::Value>, ApiError> {
state
.queue_manager
Expand All @@ -164,7 +185,7 @@ pub async fn h_group_unsubscribe(
/// GET /api/groups/{id}/headers
pub async fn h_header_list(
State(state): State<Arc<AppState>>,
Path(group_id): Path<i64>,
IdPath(group_id): IdPath<i64>,
Query(q): Query<HeaderListQuery>,
) -> Result<Json<serde_json::Value>, ApiError> {
let limit = q.limit.unwrap_or(50);
Expand All @@ -186,7 +207,7 @@ pub async fn h_header_list(
/// POST /api/groups/{id}/headers/fetch — Background XOVER fetch.
pub async fn h_header_fetch(
State(state): State<Arc<AppState>>,
Path(group_id): Path<i64>,
IdPath(group_id): IdPath<i64>,
) -> Result<Json<serde_json::Value>, ApiError> {
use nzb_web::nzb_core::nzb_nntp::connection::NntpConnection;

Expand Down Expand Up @@ -292,7 +313,7 @@ pub async fn h_header_fetch(
/// GET /api/groups/{id}/threads
pub async fn h_thread_list(
State(state): State<Arc<AppState>>,
Path(group_id): Path<i64>,
IdPath(group_id): IdPath<i64>,
Query(q): Query<HeaderListQuery>,
) -> Result<Json<serde_json::Value>, ApiError> {
let limit = q.limit.unwrap_or(50);
Expand All @@ -311,7 +332,7 @@ pub async fn h_thread_list(
/// GET /api/groups/{gid}/threads/{root_msg_id}
pub async fn h_thread_get(
State(state): State<Arc<AppState>>,
Path((group_id, root_msg_id)): Path<(i64, String)>,
IdPath((group_id, root_msg_id)): IdPath<(i64, String)>,
) -> Result<Json<serde_json::Value>, ApiError> {
let articles = state
.queue_manager
Expand All @@ -326,7 +347,7 @@ pub async fn h_thread_get(
/// POST /api/groups/{id}/headers/mark-read
pub async fn h_header_mark_read(
State(state): State<Arc<AppState>>,
Path(_group_id): Path<i64>,
IdPath(_group_id): IdPath<i64>,
Json(input): Json<nzb_web::nzb_core::models::MarkReadInput>,
) -> Result<Json<serde_json::Value>, ApiError> {
let count = state
Expand All @@ -339,7 +360,7 @@ pub async fn h_header_mark_read(
/// POST /api/groups/{id}/headers/mark-all-read
pub async fn h_header_mark_all_read(
State(state): State<Arc<AppState>>,
Path(group_id): Path<i64>,
IdPath(group_id): IdPath<i64>,
) -> Result<Json<serde_json::Value>, ApiError> {
let count = state
.queue_manager
Expand Down Expand Up @@ -372,10 +393,12 @@ pub async fn h_article_get(
.await
.map_err(|e| ApiError::from(anyhow::anyhow!("Connect failed: {e}")))?;

let response = conn
.fetch_article(&message_id)
.await
.map_err(|e| ApiError::from(anyhow::anyhow!("ARTICLE failed: {e}")))?;
let response = conn.fetch_article(&message_id).await.map_err(|e| match e {
nzb_web::nzb_core::nzb_nntp::error::NntpError::ArticleNotFound(_) => {
ApiError::not_found("Article not found")
}
e => ApiError::from(anyhow::anyhow!("ARTICLE failed: {e}")),
})?;
let _ = conn.quit().await;

let body = response
Expand All @@ -392,7 +415,7 @@ pub async fn h_article_get(
/// POST /api/groups/{id}/headers/download — Download selected as NZB.
pub async fn h_header_download(
State(state): State<Arc<AppState>>,
Path(group_id): Path<i64>,
IdPath(group_id): IdPath<i64>,
Json(input): Json<nzb_web::nzb_core::models::DownloadSelectedInput>,
) -> Result<Json<serde_json::Value>, ApiError> {
let group = state
Expand Down
Loading
Loading