A clean-up with the downloads folder gone forgets no file (#293)

missing_files took every path that did not exist() as deleted by hand.
With /downloads out of reach (the array stopped, shfs dropped) that is
every file, and each would have been marked reaped at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 18:12:52 +00:00
parent c1d8983ca1
commit 7af6153d18

View File

@@ -45,9 +45,17 @@ pub fn aged(candidates: &[Candidate], cutoff: i64) -> Vec<Candidate> {
pub async fn run(cfg: &Config, db: &Db, dry_run: bool) -> Result<Report> {
let mut report = Report::default();
// Someone may have deleted a file by hand; the row must stop claiming it exists.
// Someone may have deleted a file by hand; the row must stop claiming it exists. Not while
// the folder itself is gone (the array stopped, shfs dropped): every file would look deleted
// and the whole library be forgotten at once (#293).
// Each step is a span of the reap's trace, so a slow one says which part was slow.
for (id, path) in db.missing_files().instrument(step("missing_files")).await? {
let missing = if cfg.general.download_dir.is_dir() {
db.missing_files().instrument(step("missing_files")).await?
} else {
tracing::warn!(dir = %cfg.general.download_dir.display(), "the downloads folder is not there; left every file as it was");
vec![]
};
for (id, path) in missing {
if !dry_run {
db.mark_reaped(id).await?;
}
@@ -204,6 +212,23 @@ mod tests {
assert_eq!(db.prune_entries(500).await.unwrap(), 1, "the old item goes, a leaver's pin or not");
}
#[tokio::test]
async fn a_downloads_folder_that_is_gone_forgets_no_file() {
let db = Db::memory().await.unwrap();
db.exec_for_test(
"INSERT INTO enclosures (id, feed_id, guid, url, path, state) VALUES
(1, 'f', 'a', 'u1', '/nowhere/ipx-test/a.mp3', 'done');",
)
.await
.unwrap();
let mut cfg = Config::default();
cfg.general.download_dir = "/nowhere/ipx-test".into();
assert_eq!(run(&cfg, &db, false).await.unwrap().reconciled, 0, "the folder is gone, not the file");
cfg.general.download_dir = std::env::temp_dir();
assert_eq!(run(&cfg, &db, false).await.unwrap().reconciled, 1, "the folder is there and the file is not");
}
#[tokio::test]
async fn prune_keeps_entries_that_still_have_a_file() {
let db = Db::memory().await.unwrap();