The recorded path was output_folder.join(handle.name()). The name is the publisher's: librqbit checks it for traversal only when it chooses the subfolder itself, and ipx passes output_folder, so a multi-file torrent named "/config" or ".." pointed Delete, the reaper and /media at that path. With output_folder set, librqbit also puts a multi-file torrent's files straight into it, not under the name, so the joined path was not where the files were even for an honest torrent. Each enclosure now gets torrent-<id> inside its feed's folder, and that folder is the path: playable() already picks the largest file under a folder and remove() deletes one (#158). The same folder keeps overwrite: true from replacing another enclosure's files of the same name. A torrent half-downloaded before this restarts in its new folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- A WordPress.com site that answers its feed with a browser check (HTTP 403, "Checking your browser...") is read from WordPress.com's API instead, so Pelgrane Press's feed no longer fails for hours at a time. The items are the ones the feed gave: same ids (#317).
|
||||
|
||||
### Security
|
||||
|
||||
- An episode that comes as a torrent downloads into a folder of its own, `torrent-<id>` in its podcast's folder. A torrent could name a path anywhere on the server for Delete, the cleanup and playback to act on, and could overwrite other episodes' files of the same name (#318, #331).
|
||||
|
||||
## [1.4.0] - 2026-10-19
|
||||
|
||||
### Added
|
||||
|
||||
@@ -2401,7 +2401,7 @@ async fn torrent_one(
|
||||
let cfg = ctx.cfg();
|
||||
ctx.torrents()
|
||||
.await?
|
||||
.fetch(&cfg, url, dest_dir, |done, total| {
|
||||
.fetch(&cfg, url, &torrent::own_folder(dest_dir, enclosure), |done, total| {
|
||||
if let Some(pct) = (done * 100).checked_div(total).map(|p| p as i64)
|
||||
&& pct > last_pct
|
||||
{
|
||||
|
||||
@@ -39,7 +39,8 @@ impl Torrents {
|
||||
}
|
||||
|
||||
/// Downloads a torrent (a .torrent URL or a magnet link) straight into `dest_dir`,
|
||||
/// seeds it up to the configured limit, then stops.
|
||||
/// seeds it up to the configured limit, then stops. `dest_dir` is the torrent's own
|
||||
/// folder (`own_folder`), and it is the path returned: a file of several or one.
|
||||
///
|
||||
/// Downloading in place rather than into a staging dir is deliberate: seeding serves
|
||||
/// the files it downloaded, so moving them first would break it.
|
||||
@@ -103,8 +104,10 @@ impl Torrents {
|
||||
}
|
||||
}
|
||||
|
||||
let name = handle.name().unwrap_or_else(|| "torrent".to_owned());
|
||||
let path = handle.output_folder().join(&name);
|
||||
// The torrent's own name is the publisher's to choose, and librqbit checks it only when it
|
||||
// picks the folder itself: joined on, "/config" or ".." pointed Delete, the reaper and
|
||||
// /media at anything the daemon can reach (#318). The folder is ours, so it is the path.
|
||||
let path = dest_dir.to_owned();
|
||||
let size = handle.stats().total_bytes;
|
||||
|
||||
self.seed(cfg, &handle).await;
|
||||
@@ -134,6 +137,13 @@ impl Torrents {
|
||||
}
|
||||
}
|
||||
|
||||
/// The folder an enclosure's torrent downloads into, one per enclosure inside the feed's folder.
|
||||
/// With `overwrite` on, a torrent written straight into the feed's folder could replace another
|
||||
/// enclosure's files of the same name, another feed's too when they share a folder (#331).
|
||||
pub fn own_folder(feed_dir: &Path, enclosure: i64) -> PathBuf {
|
||||
feed_dir.join(format!("torrent-{enclosure}"))
|
||||
}
|
||||
|
||||
/// Uploaded over total. A zero-byte torrent counts as fully seeded rather than dividing by zero.
|
||||
pub fn ratio(uploaded: u64, total: u64) -> f64 {
|
||||
if total == 0 {
|
||||
@@ -154,6 +164,15 @@ mod tests {
|
||||
assert!(ratio(0, 0).is_infinite(), "must not divide by zero or seed forever");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn own_folder_is_one_folder_inside_the_feeds() {
|
||||
let feed = Path::new("/downloads/Show");
|
||||
let dir = own_folder(feed, 42);
|
||||
assert_eq!(dir.parent(), Some(feed));
|
||||
assert!(matches!(dir.components().next_back(), Some(std::path::Component::Normal(_))));
|
||||
assert_ne!(own_folder(feed, 43), dir, "two enclosures never share a folder");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stall_limit_is_never_zero() {
|
||||
let mut cfg = Config::default();
|
||||
|
||||
Reference in New Issue
Block a user