Skip to content

Move Dioxus routes and extract web API#13

Open
StirlingMouse wants to merge 1 commit intodioxus-uifrom
dioxus-switch
Open

Move Dioxus routes and extract web API#13
StirlingMouse wants to merge 1 commit intodioxus-uifrom
dioxus-switch

Conversation

@StirlingMouse
Copy link
Owner

@StirlingMouse StirlingMouse commented Mar 12, 2026

Summary by CodeRabbit

  • New Features

    • Added REST API endpoints for torrent search, retrieval, and file downloads.
  • Refactor

    • Reorganized routing: legacy interface routes now use /old/ prefix.
    • Simplified frontend paths by removing framework-specific prefixes.
    • Separated API functionality into a dedicated module.

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8183a8c0-255a-4aee-a7c2-f0ef889a5d63

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

A new mlm_web_api crate is introduced as an Axum-based HTTP API layer, providing endpoints for torrent search, metadata retrieval, and file downloads. Simultaneously, existing web frontends in mlm_web_askama and mlm_web_dioxus undergo route refactoring—shifting old paths to /old/ prefixes while SSE endpoints are simplified. The server router is updated to integrate the new API layer.

Changes

Cohort / File(s) Summary
New API Crate Setup
Cargo.toml, mlm_web_api/Cargo.toml
Crate added to workspace with dependencies: Axum, Tokio, native_db, qBittorrent API, and local crate references (mlm_core, mlm_db, mlm_mam).
API Error Handling
mlm_web_api/src/error.rs
Centralized AppError enum with automatic From conversions (native_db, mlm_mam, qbit, serde, anyhow) and IntoResponse impl mapping NotFound to 404 and others to 500.
API Handlers
mlm_web_api/src/lib.rs, mlm_web_api/src/download.rs, mlm_web_api/src/search.rs, mlm_web_api/src/torrent.rs
Four modules implementing HTTP handlers: router builder with caching middleware, file streaming (GET /torrents/{id}/{filename}), search endpoints (GET/POST /api/search with TOML query support and optional JSON persistence), and torrent metadata endpoint (GET /api/torrents/{id} with dual ID/mam_id dispatch).
Askama Route Refactoring
mlm_web_askama/src/lib.rs, mlm_web_askama/src/pages/torrent.rs, mlm_web_askama/src/pages/torrent_edit.rs
Routes prefixed to /old/* (e.g., /old/torrents), torrent_file handler removed, item_path methods updated, and nested /assets serving removed.
Askama Template Updates
mlm_web_askama/templates/base.html, mlm_web_askama/templates/pages/*
Navigation links updated from /torrents, /events, /search, /lists, etc. to /old/torrents, /old/events, /old/search, /old/lists, etc. across all templates.
Dioxus Route Refactoring
mlm_web_dioxus/src/app.rs, mlm_web_dioxus/src/sse.rs, mlm_web_dioxus/src/ssr.rs
Routes changed from /dioxus/... to / (flattened), SSE endpoints simplified (e.g., /dioxus-stats-updates → /stats-updates).
Dioxus Component Updates
mlm_web_dioxus/src/components/filter_link.rs, mlm_web_dioxus/src/components/search_row.rs, mlm_web_dioxus/src/errors/components.rs, mlm_web_dioxus/src/events/components.rs, mlm_web_dioxus/src/list.rs, mlm_web_dioxus/src/torrent_detail/components.rs, mlm_web_dioxus/src/torrent_edit.rs
Hyperlink hrefs updated from /dioxus/torrents and /dioxus/search to /torrents and /search; no logic changes.
Server Integration
server/Cargo.toml, server/src/main.rs
Dependencies added for mlm_web_api and tower_http; router updated to merge api_router alongside dioxus and askama routers.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Axum as Axum Router
    participant DB as Database
    participant QBit as qBittorrent API
    participant FS as File System

    Client->>Axum: GET /torrents/{id}/{filename}
    Axum->>DB: Fetch Torrent by ID
    alt Torrent found
        DB-->>Axum: Torrent metadata
        alt Has library_path + library_files
            Axum->>FS: Resolve file path
        else Query fallback
            Axum->>QBit: Fetch qBittorrent data + files
            QBit-->>Axum: Torrent + file list
            Axum->>Axum: Apply path mapping
        end
        
        alt File resolved
            Axum->>FS: Open file async
            FS-->>Axum: File handle
            Axum->>Axum: Wrap in ReaderStream
            Axum-->>Client: 200 + Content-Type: text/toml + file stream
        else File not found
            Axum-->>Client: 404 NotFound
        end
    else Torrent not found
        Axum-->>Client: 404 NotFound
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 New API pathways branch and grow,
With handlers stacked in ordered flow,
The old routes now wear /old/ names with care,
While search and torrents fill the API air,
Refactored links dance everywhere,
A schema born from developer's flair! 🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: extracting a new web API module (mlm_web_api) and moving Dioxus routes from /dioxus/* to /* paths, with corresponding route prefix updates throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dioxus-switch
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@StirlingMouse
Copy link
Owner Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Line 5: Add a matching dev-profile entry for the workspace crate "mlm_web_api"
by adding a [profile.dev.package.mlm_web_api] table with opt-level = 0 to your
Cargo.toml (the same workspace manifest where "mlm_web_api" is listed) so the
dev profile is consistent with the other workspace crates; ensure the table key
exactly matches mlm_web_api.

In `@mlm_web_api/Cargo.toml`:
- Around line 13-14: The Cargo manifest uses floating/branch git dependencies
(native_db and qbit) which can drift; update each dependency declaration (e.g.,
native_db and qbit) to pin an immutable commit by replacing the branch/omitted
revision with a rev = "<commit-sha>" field (use the specific commit SHA you want
to lock to), and remove the branch field if present; apply the same change
pattern to other manifests that list these git dependencies (e.g.,
server/Cargo.toml, mlm_core/Cargo.toml) so all git dependencies use rev pins for
reproducible builds and supply-chain safety.

In `@mlm_web_api/src/download.rs`:
- Around line 52-61: The headers block incorrectly hardcodes "text/toml;
charset=utf-8" and uses an unsanitized filename; update the code that builds the
headers (the `headers` array where `CONTENT_TYPE` and `CONTENT_DISPOSITION` are
set) to: 1) derive the MIME type from the file name/path using a library such as
mime_guess (e.g.
mime_guess::from_path(filename).first_or_octet_stream().to_string()) and use
that value for `CONTENT_TYPE` (include charset only for text types if desired),
and 2) sanitize/escape the `filename` used in `CONTENT_DISPOSITION` to prevent
header injection (strip or replace newlines and quotes and/or use the RFC5987
encoding form `filename*=` with percent-encoding for UTF-8); ensure you fall
back to "application/octet-stream" if no MIME is found.
- Around line 45-48: When opening the file with tokio::fs::File::open(...) in
this function, the Err arm currently discards the error and returns
AppError::NotFound; change the match to capture the error (e.g., Err(e)) and log
the error details before returning, so you still return AppError::NotFound but
record the underlying I/O error (use the project’s logger/tracing facility) to
aid debugging while preserving the existing error mapping.

In `@mlm_web_api/src/error.rs`:
- Around line 24-31: The into_response implementation on AppError currently
returns self.to_string() which can leak internal details; change into_response
(impl IntoResponse for AppError) to map AppError::NotFound to
StatusCode::NOT_FOUND with a safe user-facing message, but for all other
variants (StatusCode::INTERNAL_SERVER_ERROR) return a generic error message like
"Internal server error" to clients while logging the full error
string/server-side (use your existing logging facility) so internal details are
not sent in the HTTP response.

In `@mlm_web_api/src/lib.rs`:
- Line 28: MaMState is double-wrapping the MaM in Arc
(Arc<Result<Arc<MaM<'static>>>>); if context.mam() already returns
Result<Arc<MaM<'static>>> remove the outer Arc and redefine MaMState as
Result<Arc<MaM<'static>>> so callers get the same type as context.mam();
alternatively, if you intended a shared, thread-safe container for the Result
itself, explicitly document that and use Arc<Result<MaM<'static>>> — update the
MaMState type alias and any code that constructs or clones it to match the
chosen single-Arc shape (refer to MaMState, MaM, Arc, Result and context.mam()).
- Around line 33-44: The GET and POST routes for "/api/search" use different
state types causing inconsistent handler extraction: GET uses
Arc::new(context.mam()) while POST uses context.clone(), forcing different State
types for search_api vs search_api_post; make them consistent by choosing one
shared state type (preferably context.clone() so both routes call
.with_state(context.clone())) and update the handler signature of search_api to
accept State<Context> (or alternatively convert POST to use
Arc::new(context.mam()) and change search_api_post to accept the same MaMState);
adjust any uses of MaMState/Context inside search_api/search_api_post
accordingly.

In `@mlm_web_api/src/search.rs`:
- Line 75: The code currently constructs a hardcoded PathBuf via
PathBuf::from("/data/torrents"); replace this with a configurable value (for
example add a field like torrents_path or derive it from an existing
Config.data_dir) and use that instead (e.g., Config::torrents_path or
config.data_dir.join("torrents")). Update the code that constructs the path in
search.rs to read the path from the Config instance passed to the function (or
fall back to an environment variable/default relative path), and replace
references to PathBuf::from("/data/torrents") with
PathBuf::from(config.torrents_path) or config.data_dir.join("torrents") so
deployments can override the location.
- Around line 71-78: The path construction has an off-by-one: third is extracted
with id_str.get(3..4) which skips index 2; change the slice to id_str.get(2..3)
so third corresponds to the third character of torrent.id (keep using id_str,
first, second, third and PathBuf::from(...).join(...) as-is); ensure
unwrap_or_default() remains to handle short IDs.
- Around line 81-82: The code uses blocking std::fs::File::create and
serde_json::to_writer which will block the async runtime; replace the blocking
call with tokio::fs::File::create(...).await and write the JSON asynchronously
by serializing first (e.g. serde_json::to_vec or to_string on torrent) and then
calling tokio::io::AsyncWriteExt::write_all on the tokio file (remember to
.await and map errors into anyhow::Error as before). Update the code around
File::create, serde_json::to_writer, file_path and torrent to use the async
tokio APIs and ensure the surrounding function is async.

In `@mlm_web_api/src/torrent.rs`:
- Around line 14-23: The current router in torrent_api uses a parse-to-u64
heuristic to decide between MAM and torrent-id handling; update it to use the
explicit id_is_hash flag (or at minimum document the heuristic). Replace the
parse-based dispatch inside torrent_api so it looks up the record via Context
(using the Torrent model/DB access) and checks the Torrent.id_is_hash boolean to
decide whether to call torrent_api_mam_id or torrent_api_id (using the same
State(context) and Path value), and if you keep the heuristic add a clarifying
comment above torrent_api noting “numeric-only → MAM id” and reference the
Torrent.id_is_hash field for future clarity; ensure any DB lookup handles “not
found” and maps errors to AppError consistently.

In `@mlm_web_askama/templates/pages/errors.html`:
- Around line 40-41: Both anchor tags that use target=_blank (the link to
"/old/torrents/{{ mam_id }}" and the link to "https://www.myanonamouse.net/t/{{
mam_id }}") must include rel="noopener noreferrer" to prevent exposing
window.opener; update those <a> elements in the errors.html template to add
rel="noopener noreferrer" alongside target=_blank.

In `@server/src/main.rs`:
- Line 38: Remove the unused import statement "use
tower_http::services::ServeDir;" from main.rs: locate the top-level use/import
block where ServeDir is declared and delete that line (ServeDir is actually used
in mlm_web_api::lib, not here), then run cargo check to verify no remaining
unused imports.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 422d35b3-49cd-4b1c-a822-ac61de567235

📥 Commits

Reviewing files that changed from the base of the PR and between 9401250 and bca5e5a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (31)
  • Cargo.toml
  • mlm_web_api/Cargo.toml
  • mlm_web_api/src/download.rs
  • mlm_web_api/src/error.rs
  • mlm_web_api/src/lib.rs
  • mlm_web_api/src/search.rs
  • mlm_web_api/src/torrent.rs
  • mlm_web_askama/src/lib.rs
  • mlm_web_askama/src/pages/torrent.rs
  • mlm_web_askama/src/pages/torrent_edit.rs
  • mlm_web_askama/templates/base.html
  • mlm_web_askama/templates/pages/duplicate.html
  • mlm_web_askama/templates/pages/errors.html
  • mlm_web_askama/templates/pages/list.html
  • mlm_web_askama/templates/pages/lists.html
  • mlm_web_askama/templates/pages/replaced.html
  • mlm_web_askama/templates/pages/torrent.html
  • mlm_web_askama/templates/pages/torrents.html
  • mlm_web_askama/templates/partials/mam_torrents.html
  • mlm_web_dioxus/src/app.rs
  • mlm_web_dioxus/src/components/filter_link.rs
  • mlm_web_dioxus/src/components/search_row.rs
  • mlm_web_dioxus/src/errors/components.rs
  • mlm_web_dioxus/src/events/components.rs
  • mlm_web_dioxus/src/list.rs
  • mlm_web_dioxus/src/sse.rs
  • mlm_web_dioxus/src/ssr.rs
  • mlm_web_dioxus/src/torrent_detail/components.rs
  • mlm_web_dioxus/src/torrent_edit.rs
  • server/Cargo.toml
  • server/src/main.rs

resolver = "3"
members = [
"server",
"mlm_web_api",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding a matching dev-profile entry for mlm_web_api.

Line 5 correctly adds the crate to the workspace. For consistency with other workspace crates, you may also add [profile.dev.package.mlm_web_api] opt-level = 0.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 5, Add a matching dev-profile entry for the workspace
crate "mlm_web_api" by adding a [profile.dev.package.mlm_web_api] table with
opt-level = 0 to your Cargo.toml (the same workspace manifest where
"mlm_web_api" is listed) so the dev profile is consistent with the other
workspace crates; ensure the table key exactly matches mlm_web_api.

Comment on lines +13 to +14
native_db = { git = "https://github.com/StirlingMouse/native_db.git", branch = "0.8.x" }
qbit = { git = "https://github.com/StirlingMouse/qbittorrent-webui-api.git" }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Branch/floating git dependencies in Cargo.toml files =="
rg -n 'git\s*=\s*"[^"]+"|branch\s*=' --glob '**/Cargo.toml' -C1

echo
if [ -f Cargo.lock ]; then
  echo "== Git sources in Cargo.lock (native_db/qbit) =="
  rg -n 'name = "(native_db|qbit)"|source = "git\+' Cargo.lock -C2
else
  echo "Cargo.lock not found."
fi

Repository: StirlingMouse/MLM

Length of output: 4185


🏁 Script executed:

git ls-files | grep -E "^Cargo\.lock$"

Repository: StirlingMouse/MLM

Length of output: 71


Pin git dependencies to immutable commits instead of floating branches.

Lines 13–14 use branch-based and floating git dependencies, which allow upstream movement. Even with Cargo.lock pinning current builds, manifests should explicitly specify rev to prevent unintended drift during lockfile updates and strengthen supply-chain transparency.

Suggested manifest change
-native_db = { git = "https://github.com/StirlingMouse/native_db.git", branch = "0.8.x" }
-qbit = { git = "https://github.com/StirlingMouse/qbittorrent-webui-api.git" }
+native_db = { git = "https://github.com/StirlingMouse/native_db.git", rev = "cddaafe0f6863146fd0879cae88fdb613bc50b47" }
+qbit = { git = "https://github.com/StirlingMouse/qbittorrent-webui-api.git", rev = "1038c6000e749b6b0d55fe2108618e82beeaebfe" }

Note: This pattern also appears in server/Cargo.toml, mlm_core/Cargo.toml, and other manifests.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
native_db = { git = "https://github.com/StirlingMouse/native_db.git", branch = "0.8.x" }
qbit = { git = "https://github.com/StirlingMouse/qbittorrent-webui-api.git" }
native_db = { git = "https://github.com/StirlingMouse/native_db.git", rev = "cddaafe0f6863146fd0879cae88fdb613bc50b47" }
qbit = { git = "https://github.com/StirlingMouse/qbittorrent-webui-api.git", rev = "1038c6000e749b6b0d55fe2108618e82beeaebfe" }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlm_web_api/Cargo.toml` around lines 13 - 14, The Cargo manifest uses
floating/branch git dependencies (native_db and qbit) which can drift; update
each dependency declaration (e.g., native_db and qbit) to pin an immutable
commit by replacing the branch/omitted revision with a rev = "<commit-sha>"
field (use the specific commit SHA you want to lock to), and remove the branch
field if present; apply the same change pattern to other manifests that list
these git dependencies (e.g., server/Cargo.toml, mlm_core/Cargo.toml) so all git
dependencies use rev pins for reproducible builds and supply-chain safety.

Comment on lines +45 to +48
let file = match tokio::fs::File::open(path).await {
Ok(file) => file,
Err(_) => return Err(AppError::NotFound),
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider logging the file open error for debugging.

The actual error is discarded when the file cannot be opened. This loses diagnostic information (e.g., permission denied vs. file not found) that could be useful for troubleshooting.

♻️ Proposed fix
     let file = match tokio::fs::File::open(path).await {
         Ok(file) => file,
-        Err(_) => return Err(AppError::NotFound),
+        Err(e) => {
+            tracing::warn!("Failed to open file: {e}");
+            return Err(AppError::NotFound);
+        }
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let file = match tokio::fs::File::open(path).await {
Ok(file) => file,
Err(_) => return Err(AppError::NotFound),
};
let file = match tokio::fs::File::open(path).await {
Ok(file) => file,
Err(e) => {
tracing::warn!("Failed to open file: {e}");
return Err(AppError::NotFound);
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlm_web_api/src/download.rs` around lines 45 - 48, When opening the file with
tokio::fs::File::open(...) in this function, the Err arm currently discards the
error and returns AppError::NotFound; change the match to capture the error
(e.g., Err(e)) and log the error details before returning, so you still return
AppError::NotFound but record the underlying I/O error (use the project’s
logger/tracing facility) to aid debugging while preserving the existing error
mapping.

Comment on lines +52 to +61
let headers = [
(
axum::http::header::CONTENT_TYPE,
"text/toml; charset=utf-8".to_string(),
),
(
axum::http::header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{}\"", filename),
),
];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Incorrect Content-Type for file downloads.

The Content-Type is hardcoded to text/toml; charset=utf-8, but this endpoint serves arbitrary torrent-related files (audio files, etc.), not TOML configuration files. This will cause browsers to misinterpret the file type.

Additionally, the filename in Content-Disposition should be sanitized to prevent header injection with characters like " or newlines.

🐛 Proposed fix
     let headers = [
         (
             axum::http::header::CONTENT_TYPE,
-            "text/toml; charset=utf-8".to_string(),
+            "application/octet-stream".to_string(),
         ),
         (
             axum::http::header::CONTENT_DISPOSITION,
-            format!("attachment; filename=\"{}\"", filename),
+            format!(
+                "attachment; filename=\"{}\"",
+                filename.replace('"', "\\\"").replace('\n', "").replace('\r', "")
+            ),
         ),
     ];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let headers = [
(
axum::http::header::CONTENT_TYPE,
"text/toml; charset=utf-8".to_string(),
),
(
axum::http::header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{}\"", filename),
),
];
let headers = [
(
axum::http::header::CONTENT_TYPE,
"application/octet-stream".to_string(),
),
(
axum::http::header::CONTENT_DISPOSITION,
format!(
"attachment; filename=\"{}\"",
filename.replace('"', "\\\"").replace('\n', "").replace('\r', "")
),
),
];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlm_web_api/src/download.rs` around lines 52 - 61, The headers block
incorrectly hardcodes "text/toml; charset=utf-8" and uses an unsanitized
filename; update the code that builds the headers (the `headers` array where
`CONTENT_TYPE` and `CONTENT_DISPOSITION` are set) to: 1) derive the MIME type
from the file name/path using a library such as mime_guess (e.g.
mime_guess::from_path(filename).first_or_octet_stream().to_string()) and use
that value for `CONTENT_TYPE` (include charset only for text types if desired),
and 2) sanitize/escape the `filename` used in `CONTENT_DISPOSITION` to prevent
header injection (strip or replace newlines and quotes and/or use the RFC5987
encoding form `filename*=` with percent-encoding for UTF-8); ensure you fall
back to "application/octet-stream" if no MIME is found.

Comment on lines +24 to +31
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let status = match self {
AppError::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, self.to_string()).into_response()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider sanitizing error messages in responses to avoid information leakage.

The current implementation returns self.to_string() as the response body for all errors, which may expose internal details like database error messages, file paths, or stack traces to API clients. This could be a security concern in production.

Consider returning generic messages for 500 errors while logging the actual error server-side.

🛡️ Proposed fix
 impl IntoResponse for AppError {
     fn into_response(self) -> Response {
         let status = match self {
             AppError::NotFound => StatusCode::NOT_FOUND,
             _ => StatusCode::INTERNAL_SERVER_ERROR,
         };
-        (status, self.to_string()).into_response()
+        let message = match &self {
+            AppError::NotFound => "Not Found".to_string(),
+            _ => {
+                tracing::error!("API error: {self:?}");
+                "Internal Server Error".to_string()
+            }
+        };
+        (status, message).into_response()
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlm_web_api/src/error.rs` around lines 24 - 31, The into_response
implementation on AppError currently returns self.to_string() which can leak
internal details; change into_response (impl IntoResponse for AppError) to map
AppError::NotFound to StatusCode::NOT_FOUND with a safe user-facing message, but
for all other variants (StatusCode::INTERNAL_SERVER_ERROR) return a generic
error message like "Internal server error" to clients while logging the full
error string/server-side (use your existing logging facility) so internal
details are not sent in the HTTP response.

let first = id_str.get(0..1).unwrap_or_default();
let second = id_str.get(1..2).unwrap_or_default();
let third = id_str.get(3..4).unwrap_or_default();
let path = PathBuf::from("/data/torrents")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded absolute path /data/torrents is not configurable.

The path /data/torrents is hardcoded, which assumes a specific deployment environment and may not work across different setups (development, Docker, bare metal, etc.). Consider making this configurable via the Config struct or using a path relative to the data directory.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlm_web_api/src/search.rs` at line 75, The code currently constructs a
hardcoded PathBuf via PathBuf::from("/data/torrents"); replace this with a
configurable value (for example add a field like torrents_path or derive it from
an existing Config.data_dir) and use that instead (e.g., Config::torrents_path
or config.data_dir.join("torrents")). Update the code that constructs the path
in search.rs to read the path from the Config instance passed to the function
(or fall back to an environment variable/default relative path), and replace
references to PathBuf::from("/data/torrents") with
PathBuf::from(config.torrents_path) or config.data_dir.join("torrents") so
deployments can override the location.

Comment on lines +81 to +82
let file = File::create(file_path).map_err(anyhow::Error::new)?;
serde_json::to_writer(file, torrent)?;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Blocking file I/O in async context.

std::fs::File::create is a blocking operation that will block the async runtime's thread. This should use tokio::fs::File::create for non-blocking I/O, especially since tokio::fs::create_dir_all is already used on line 79.

🐛 Proposed fix
 use std::{fs::File, path::PathBuf};
+use std::io::Write;

-use anyhow::Result;
+use anyhow::Result;

And update the file writing:

             create_dir_all(&path).await.map_err(anyhow::Error::new)?;
             let file_path = path.join(format!("{}.json", torrent.id));
-            let file = File::create(file_path).map_err(anyhow::Error::new)?;
-            serde_json::to_writer(file, torrent)?;
+            let json = serde_json::to_vec(torrent)?;
+            tokio::fs::write(file_path, json).await.map_err(anyhow::Error::new)?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let file = File::create(file_path).map_err(anyhow::Error::new)?;
serde_json::to_writer(file, torrent)?;
use std::path::PathBuf;
use std::io::Write;
use anyhow::Result;
create_dir_all(&path).await.map_err(anyhow::Error::new)?;
let file_path = path.join(format!("{}.json", torrent.id));
let json = serde_json::to_vec(torrent)?;
tokio::fs::write(file_path, json).await.map_err(anyhow::Error::new)?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlm_web_api/src/search.rs` around lines 81 - 82, The code uses blocking
std::fs::File::create and serde_json::to_writer which will block the async
runtime; replace the blocking call with tokio::fs::File::create(...).await and
write the JSON asynchronously by serializing first (e.g. serde_json::to_vec or
to_string on torrent) and then calling tokio::io::AsyncWriteExt::write_all on
the tokio file (remember to .await and map errors into anyhow::Error as before).
Update the code around File::create, serde_json::to_writer, file_path and
torrent to use the async tokio APIs and ensure the surrounding function is
async.

Comment on lines +14 to +23
pub async fn torrent_api(
State(context): State<Context>,
Path(id_or_mam_id): Path<String>,
) -> std::result::Result<Json<serde_json::Value>, AppError> {
if let Ok(id) = id_or_mam_id.parse() {
torrent_api_mam_id(State(context), Path(id)).await
} else {
torrent_api_id(State(context), Path(id_or_mam_id)).await
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if torrent IDs can be purely numeric by examining the codebase
rg -n "id_is_hash|torrent\.id" --type rust -C2 | head -50

Repository: StirlingMouse/MLM

Length of output: 3320


🏁 Script executed:

cat -n mlm_web_api/src/torrent.rs | head -80

Repository: StirlingMouse/MLM

Length of output: 2773


🏁 Script executed:

# Find the Torrent struct definition
rg -n "struct Torrent" --type rust -A 10

Repository: StirlingMouse/MLM

Length of output: 42498


🏁 Script executed:

# Check if there's any id_is_hash usage in the API layer
rg -n "id_is_hash" mlm_web_api/ --type rust -C 3

Repository: StirlingMouse/MLM

Length of output: 489


Consider documenting the ID parsing heuristic or using the explicit id_is_hash field.

The dispatch logic relies on attempting to parse the input as u64 to differentiate between MAM IDs and torrent IDs. While this works in practice because torrent hashes are typically alphanumeric, the code doesn't validate this assumption. The Torrent struct already tracks an id_is_hash field in the database schema; consider either:

  1. Adding a comment explaining that numeric-only IDs are assumed to be MAM IDs, or
  2. Refactoring to check the database's id_is_hash flag directly for clarity
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlm_web_api/src/torrent.rs` around lines 14 - 23, The current router in
torrent_api uses a parse-to-u64 heuristic to decide between MAM and torrent-id
handling; update it to use the explicit id_is_hash flag (or at minimum document
the heuristic). Replace the parse-based dispatch inside torrent_api so it looks
up the record via Context (using the Torrent model/DB access) and checks the
Torrent.id_is_hash boolean to decide whether to call torrent_api_mam_id or
torrent_api_id (using the same State(context) and Path value), and if you keep
the heuristic add a clarifying comment above torrent_api noting “numeric-only →
MAM id” and reference the Torrent.id_is_hash field for future clarity; ensure
any DB lookup handles “not found” and maps errors to AppError consistently.

Comment on lines +40 to 41
<a href="/old/torrents/{{ mam_id }}" target=_blank>open</a>
<a href="https://www.myanonamouse.net/t/{{ mam_id }}" target=_blank>MaM</a>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add rel="noopener noreferrer" to _blank links.

Line 40 (and the adjacent Line 41) open a new tab without rel, which leaves window.opener exposed.

🔒 Suggested fix
-        <a href="/old/torrents/{{ mam_id }}" target=_blank>open</a>
-        <a href="https://www.myanonamouse.net/t/{{ mam_id }}" target=_blank>MaM</a>
+        <a href="/old/torrents/{{ mam_id }}" target="_blank" rel="noopener noreferrer">open</a>
+        <a href="https://www.myanonamouse.net/t/{{ mam_id }}" target="_blank" rel="noopener noreferrer">MaM</a>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<a href="/old/torrents/{{ mam_id }}" target=_blank>open</a>
<a href="https://www.myanonamouse.net/t/{{ mam_id }}" target=_blank>MaM</a>
<a href="/old/torrents/{{ mam_id }}" target="_blank" rel="noopener noreferrer">open</a>
<a href="https://www.myanonamouse.net/t/{{ mam_id }}" target="_blank" rel="noopener noreferrer">MaM</a>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlm_web_askama/templates/pages/errors.html` around lines 40 - 41, Both anchor
tags that use target=_blank (the link to "/old/torrents/{{ mam_id }}" and the
link to "https://www.myanonamouse.net/t/{{ mam_id }}") must include
rel="noopener noreferrer" to prevent exposing window.opener; update those <a>
elements in the errors.html template to add rel="noopener noreferrer" alongside
target=_blank.

use mlm_web_api::router as api_router;
use mlm_web_askama::router as askama_router;
use mlm_web_dioxus::ssr::router as dioxus_router;
use tower_http::services::ServeDir;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Unused import: ServeDir is not used in this file.

The ServeDir import appears to be unused in server/src/main.rs. The actual ServeDir usage is in mlm_web_api/src/lib.rs. Consider removing this import to keep the code clean.

🧹 Proposed fix
 use mlm_web_api::router as api_router;
 use mlm_web_askama::router as askama_router;
 use mlm_web_dioxus::ssr::router as dioxus_router;
-use tower_http::services::ServeDir;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/main.rs` at line 38, Remove the unused import statement "use
tower_http::services::ServeDir;" from main.rs: locate the top-level use/import
block where ServeDir is declared and delete that line (ServeDir is actually used
in mlm_web_api::lib, not here), then run cargo check to verify no remaining
unused imports.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant