reap --dry-run says what would go, not that it went (#461)

A dry run printed "deleted <path> (N MB)" for every file and "deleted N
old file(s)" at the end, word for word a real reap, of files it kept:
Reaped and ReapDone carried no flag to say otherwise. Both now carry
dry_run (serde default false, so an older message still reads), and the
words are "would delete".

Found writing the browser suite's first test of retention, which ran on
the daemon's own clock and nowhere in the suite: over a tiny quota, a dry
reap lists the downloads and leaves them, and a reap with no limits
deletes nothing and only tidies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-14 03:59:02 +00:00
parent 61c38bacc1
commit f9f50fcf07
5 changed files with 50 additions and 9 deletions

View File

@@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- `ipx reap --dry-run` says "would delete" of each file and of the total, rather than "deleted", word for word a real reap, of files it kept (#461).
- Subscribing to a feed someone here already reads puts its recent items to your Signals at once, rather than at the judge's next pass, up to five minutes later (#415).
- A download a restart stopped, a deploy's for one, is taken up again as the daemon starts, rather than waiting for its feed's next check, an hour or a day away (#410).
- A YouTube channel no longer misses most days' videos: YouTube answers some feed requests with a 404 or 500 it does not mean, and ipx now asks again, twice, a second apart, before calling the feed failing (#382), and then every hour for a day where it would have waited a day, since YouTube's failures run for hours; and its YouTube feeds are read one at a time, two seconds apart, since YouTube refuses many asked together (#386).

View File

@@ -83,6 +83,9 @@ pub enum Event {
Reaped {
path: String,
bytes: u64,
/// Only what would go: `reap --dry-run` said "deleted" of files it kept (#461).
#[serde(default)]
dry_run: bool,
},
/// Terminal: a client that asked for work stops reading here.
ScanDone {
@@ -97,6 +100,8 @@ pub enum Event {
ReapDone {
files: usize,
bytes: u64,
#[serde(default)]
dry_run: bool,
},
Status {
feeds: usize,
@@ -165,11 +170,11 @@ impl Event {
},
Event::DownloadDone { path, .. } => format!(" saved {path}"),
Event::DownloadError { url, msg, .. } => format!(" failed {url}: {msg}"),
Event::Reaped { path, bytes } => {
format!("deleted {path} ({:.1} MB)", *bytes as f64 / 1_048_576.0)
Event::Reaped { path, bytes, dry_run } => {
format!("{} {path} ({:.1} MB)", deleted(*dry_run), *bytes as f64 / 1_048_576.0)
}
Event::ReapDone { files, bytes } => {
format!("deleted {files} old file(s), {:.1} MB", *bytes as f64 / 1_048_576.0)
Event::ReapDone { files, bytes, dry_run } => {
format!("{} {files} old file(s), {:.1} MB", deleted(*dry_run), *bytes as f64 / 1_048_576.0)
}
Event::Status { feeds, pending, downloaded } => {
format!("{feeds} feeds, {pending} queued to download, {downloaded} downloaded")
@@ -288,6 +293,10 @@ impl Event {
}
}
const fn deleted(dry_run: bool) -> &'static str {
if dry_run { "would delete" } else { "deleted" }
}
/// An event, logged once (#91): its words as the message, and `ev`, `feed`, `new` and the rest as
/// fields, so Loki reads them without parsing the message. A failure also gets `error.type` and,
/// from an HTTP error, `http.response.status_code`, so failures group by kind without a regex.
@@ -516,7 +525,9 @@ mod tests {
assert!(Event::ScanDone { feeds: 3, only: None, forced: false }.is_for(&me, &mine));
assert!(Event::SignalHits { user: 1, hits: 2 }.is_for(&me, &mine));
assert!(!Event::SignalHits { user: 3, hits: 2 }.is_for(&me, &mine));
assert!(!Event::Reaped { path: "/downloads/Theirs/ep.mp3".into(), bytes: 1 }.is_for(&me, &mine));
assert!(
!Event::Reaped { path: "/downloads/Theirs/ep.mp3".into(), bytes: 1, dry_run: false }.is_for(&me, &mine)
);
}
#[test]
@@ -627,7 +638,7 @@ mod tests {
#[test]
fn only_completion_events_end_a_client_session() {
assert!(Event::ScanDone { feeds: 1, only: None, forced: false }.is_terminal());
assert!(Event::ReapDone { files: 0, bytes: 0 }.is_terminal());
assert!(Event::ReapDone { files: 0, bytes: 0, dry_run: false }.is_terminal());
assert!(!Event::FeedDone { feed: "a".into(), new: 0, downloaded: 0, failed: 0, torrents: 0 }.is_terminal());
}

View File

@@ -142,10 +142,10 @@ pub(crate) async fn reap(ctx: &Ctx, dry_run: bool, standalone: bool) -> Result<(
}
let removed = r.aged_out.iter().chain(r.over_quota.iter());
for c in removed {
ctx.out.emit(Event::Reaped { path: c.path.clone(), bytes: c.bytes.max(0) as u64 });
ctx.out.emit(Event::Reaped { path: c.path.clone(), bytes: c.bytes.max(0) as u64, dry_run });
}
if standalone {
ctx.out.emit(Event::ReapDone { files: r.aged_out.len() + r.over_quota.len(), bytes: r.bytes_freed });
ctx.out.emit(Event::ReapDone { files: r.aged_out.len() + r.over_quota.len(), bytes: r.bytes_freed, dry_run });
}
Ok(())
}

View File

@@ -2518,7 +2518,11 @@ async fn delete_file(
state.ctx.db.mark_reaped(id).await?;
state
.events
.send(Event::Reaped { path: enc.path.unwrap_or_default(), bytes: enc.length.unwrap_or(0).max(0) as u64 })
.send(Event::Reaped {
path: enc.path.unwrap_or_default(),
bytes: enc.length.unwrap_or(0).max(0) as u64,
dry_run: false,
})
.ok();
Ok(StatusCode::NO_CONTENT)
}