Skip to content

Fix rTorrent path evaluation for items without a subdirectory#548

Merged
Flaminel merged 2 commits into
mainfrom
fix_rtorrent_path
Apr 5, 2026
Merged

Fix rTorrent path evaluation for items without a subdirectory#548
Flaminel merged 2 commits into
mainfrom
fix_rtorrent_path

Conversation

@Flaminel

@Flaminel Flaminel commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Summary by Sourcery

Ensure correct path resolution for rTorrent torrents, especially single-file torrents without subdirectories, by distinguishing between base file path and containing directory.

Bug Fixes:

  • Fix incorrect file path construction for torrents whose base path points directly to a file by falling back to the torrent directory when combining paths.

Enhancements:

  • Extend the RTorrent torrent model and client to capture both the base path and the directory path reported by rTorrent for more accurate path handling.

@Flaminel

Flaminel commented Apr 5, 2026

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@qodo-code-review

qodo-code-review Bot commented Apr 5, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX Issues (0)

Grey Divider


Action required

1. No test for Directory path📘 Rule violation ☼ Reliability
Description
The bug fix changes file path construction in ChangeCategoryForNoHardLinksAsync to prefer
torrent.Info.Directory, but there is no unit test asserting the computed file path for the
single-file torrent scenario where BasePath is a full file path. This risks regressions because
current tests use It.IsAny() and never validate the argument passed to GetHardLinkCount.
Code

code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentServiceDC.cs[96]

+                    Path.Combine(torrent.Info.Directory ?? torrent.Info.BasePath ?? "", file.Path).Split(['\\', '/']));
Evidence
PR Compliance ID 225607 requires a unit test for bug fixes. The change introduces new behavior by
using torrent.Info.Directory (and a new Directory property) to build filePath, but the
existing unit test for this method does not set Directory or assert the computed filePath
argument (uses It.IsAny()), so it would not fail if the single-file path bug reappears.

Rule 225607: Require unit tests for all new features and bug fixes
code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentServiceDC.cs[93-98]
code/backend/Cleanuparr.Domain/Entities/RTorrent/Response/RTorrentTorrent.cs[63-73]
code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/RTorrentServiceDCTests.cs[515-541]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A bug fix changes path evaluation to prefer `torrent.Info.Directory` (from `d.directory`) over `torrent.Info.BasePath`, but there is no unit test that would fail if the single-file torrent case regresses (where `BasePath` is a full file path).
## Issue Context
For single-file torrents, `BasePath` may be the full file path; combining it with `file.Path` can produce an incorrect double-appended path. The fix uses `Directory` to ensure the combined path is correct, but tests currently don’t assert the computed `filePath` passed into `GetHardLinkCount`.
## Fix Focus Areas
- code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/RTorrentServiceDCTests.cs[321-550]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Silent torrent drops 🐞 Bug ≡ Correctness
Description
RTorrentClient.CreateTorrentFromValues now returns null unless at least 13 fields are present, and
ParseTorrentList silently skips null torrents. This can make torrents disappear from downstream
processing (e.g., seeding list / category changes) without any error signal.
Code

code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentClient.cs[331]

+        if (values.Length < 13) return null;
Evidence
The multicall field list was extended to include d.directory=, and CreateTorrentFromValues was
updated to require 13 values; any shorter row returns null. ParseTorrentList then drops these
torrents by only adding non-null results, with no logging—so callers receive an incomplete list
without knowing why.

code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentClient.cs[18-34]
code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentClient.cs[305-324]
code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentClient.cs[329-348]
code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentServiceDC.cs[12-22]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`CreateTorrentFromValues` now rejects any torrent row with fewer than 13 values, which causes torrents to be silently dropped from `ParseTorrentList`.
### Issue Context
`TorrentFields` was extended with `d.directory=`, but the parser should remain tolerant to rows that are missing the new field (or otherwise shorter) and still construct a `RTorrentTorrent` when the original required fields exist.
### Fix Focus Areas
- code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentClient.cs[18-34]
- code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentClient.cs[305-348]
### Suggested fix
- Change the minimum-length check back to the pre-existing required field count (12).
- Populate `Directory` only when the value exists:
- `Directory = values.Length > 12 ? values[12]?.ToString() : null;`
- (Optional but recommended) add a debug log when a row is shorter than expected, to avoid silent data loss.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Wrong BasePath fallback combine 🐞 Bug ≡ Correctness
Description
RTorrentServiceDC.ChangeCategoryForNoHardLinksAsync falls back to `Path.Combine(BasePath,
file.Path) when Directory is null, but BasePath` is documented as a full file path for
single-file torrents. In that case the combine produces an invalid path and hardlink checks may
treat existing files as missing.
Code

code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentServiceDC.cs[96]

+                    Path.Combine(torrent.Info.Directory ?? torrent.Info.BasePath ?? "", file.Path).Split(['\\', '/']));
Evidence
The domain model explicitly states BasePath can be a full file path (single-file torrents), while
Directory is intended to always be a directory. The new code still combines file.Path onto
BasePath when Directory is absent, which is inconsistent with the documented meaning of
BasePath and can yield incorrect paths for the hardlink check flow.

code/backend/Cleanuparr.Domain/Entities/RTorrent/Response/RTorrentTorrent.cs[63-73]
code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentServiceDC.cs[93-108]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `torrent.Info.Directory` is null, the code falls back to combining `torrent.Info.BasePath` with `file.Path`. But `BasePath` may already be a full file path for single-file torrents, so appending `file.Path` can produce an invalid path.
### Issue Context
The hardlink check relies on `filePath` being the real on-disk file path.
### Fix Focus Areas
- code/backend/Cleanuparr.Domain/Entities/RTorrent/Response/RTorrentTorrent.cs[63-73]
- code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentServiceDC.cs[93-99]
### Suggested fix
Update the fallback logic to distinguish directory vs file semantics when `Directory` is missing. For example:
- If `Directory` is non-empty: `Path.Combine(Directory, file.Path)` (current behavior)
- Else if `BasePath` is non-empty:
- If `Path.GetFileName(BasePath)` matches `Path.GetFileName(file.Path)` (or `file.Path` has no directory separators), treat `BasePath` as the full file path and use it directly.
- Otherwise treat `BasePath` as a directory and combine as before.
Keep the existing path-separator normalization (`Split(['\\','/'])` + `Join`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@Flaminel

Flaminel commented Apr 5, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai review

@sourcery-ai

sourcery-ai Bot commented Apr 5, 2026

Copy link
Copy Markdown
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Adds explicit directory handling for rTorrent torrents and updates path construction to correctly handle single-file torrents without subdirectories.

Class diagram for updated rTorrent torrent handling

classDiagram
class RTorrentTorrent {
    string~nullable~ BasePath
    string~nullable~ Directory
}

class RTorrentClient {
    - string[] TorrentFields
    + List~RTorrentTorrent~ ParseTorrentList(XElement value)
    + RTorrentTorrent CreateTorrentFromValues(object[] values)
}

class RTorrentServiceDC {
    + Task ChangeCategoryForNoHardLinksAsync(List~ITorrentItemWrapper~ torrentItems)
}

RTorrentClient --> RTorrentTorrent : creates
RTorrentServiceDC ..> RTorrentClient : uses
RTorrentServiceDC ..> RTorrentTorrent : reads Info.Directory and Info.BasePath
Loading

Flow diagram for rTorrent path evaluation logic

flowchart TD
    A["Start processing file in ChangeCategoryForNoHardLinksAsync"] --> B["Read torrent.Info.Directory"]
    B --> C{Directory is null?}
    C -- "No" --> D["BaseDir = torrent.Info.Directory"]
    C -- "Yes" --> E["BaseDir = torrent.Info.BasePath"]
    D --> F["Combine BaseDir with file.Path using Path.Combine"]
    E --> F
    F --> G["Normalize separators by splitting on / and \\ and joining with system separator"]
    G --> H["Apply PathHelper.RemapPath using DownloadDirectorySource and DownloadDirectoryTarget"]
    H --> I["Resulting filePath used for further processing"]
    I --> J["End"]
Loading

File-Level Changes

Change Details Files
Extend RTorrent torrent model and XML parsing to include the torrent directory alongside the base path.
  • Add a Directory property with documentation to the RTorrentTorrent record to distinguish between base path and directory.
  • Request d.directory in the RTorrentClient fields list returned from rTorrent.
  • Update CreateTorrentFromValues to expect the additional directory field and map it into the new Directory property.
code/backend/Cleanuparr.Domain/Entities/RTorrent/Response/RTorrentTorrent.cs
code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentClient.cs
Fix computed file path generation to use an explicit directory field when available, falling back to base path for backward compatibility.
  • Change filePath computation in ChangeCategoryForNoHardLinksAsync to use torrent.Info.Directory when present, otherwise fallback to BasePath.
  • Keep normalization of mixed path separators and application of PathHelper.RemapPath unchanged apart from new base directory selection.
code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentServiceDC.cs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The path construction in ChangeCategoryForNoHardLinksAsync (using Path.Combine(...).Split(['\\', '/'])) is a bit brittle; consider extracting this normalization logic into a dedicated helper that consistently handles mixed separators and trailing slashes across the codebase.
  • Now that BasePath may point to a file and Directory always points to a directory, consider renaming or adding small helper properties/methods on RTorrentTorrent (e.g., GetDataDirectory()) to make the distinction clearer and reduce the risk of future misuse of these fields.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The path construction in `ChangeCategoryForNoHardLinksAsync` (using `Path.Combine(...).Split(['\\', '/'])`) is a bit brittle; consider extracting this normalization logic into a dedicated helper that consistently handles mixed separators and trailing slashes across the codebase.
- Now that `BasePath` may point to a file and `Directory` always points to a directory, consider renaming or adding small helper properties/methods on `RTorrentTorrent` (e.g., `GetDataDirectory()`) to make the distinction clearer and reduce the risk of future misuse of these fields.

## Individual Comments

### Comment 1
<location path="code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentClient.cs" line_range="331" />
<code_context>
     private static RTorrentTorrent? CreateTorrentFromValues(object?[] values)
     {
-        if (values.Length < 12) return null;
+        if (values.Length < 13) return null;

         return new RTorrentTorrent
</code_context>
<issue_to_address>
**issue (bug_risk):** Consider allowing shorter responses and treating the new `Directory` field as optional to avoid dropping torrents.

With `values.Length >= 13`, any rTorrent instance that omits `d.directory` (or returns fewer fields) will cause that torrent to be skipped entirely. Since `Directory` is additive and non-critical, keep the `values.Length < 12` guard and only set `Directory` when `values.Length > 12`, so missing `d.directory` yields a null `Directory` instead of dropping the torrent.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@codecov

codecov Bot commented Apr 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 28.57143% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...Features/DownloadClient/RTorrent/RTorrentClient.cs 0.00% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Cleanuparr Cleanuparr deleted a comment from greptile-apps Bot Apr 5, 2026
@Cleanuparr Cleanuparr deleted a comment from qodo-code-review Bot Apr 5, 2026
@Flaminel Flaminel merged commit 7b80e03 into main Apr 5, 2026
11 of 12 checks passed
@Flaminel Flaminel deleted the fix_rtorrent_path branch April 5, 2026 19:11
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