fix(worker): import_album recovers orphaned .old/.importing from an interrupted swap

Adds a crash-recovery preamble that restores `final` from `.old` when a
prior swap was interrupted after rename(final, old) but before
rename(tmp, final), and always drops stale `.old`/`.importing` orphans.

Also guards the swap: if nothing new was staged into tmp (empty/missing
staging) and `final` already exists, leave it untouched instead of
swapping in an empty copy. This is required beyond the literal preamble
because the pre-existing swap logic unconditionally replaced `final`
with `tmp` regardless of whether anything was actually imported, which
would otherwise destroy a just-recovered (or any existing) copy whenever
staging is empty.
This commit is contained in:
Jonathan
2026-07-13 00:46:40 +02:00
parent 44c1b1a62a
commit df7bf2fcf8
2 changed files with 37 additions and 0 deletions
+23
View File
@@ -55,6 +55,29 @@ def test_import_album_leaves_no_temp_siblings(tmp_path):
assert os.listdir(tmp_path / "Artist") == ["Album"] # no .importing / .old leftovers
def test_import_album_recovers_from_interrupted_swap(tmp_path):
final = str(tmp_path / "Artist" / "Album (2020)")
os.makedirs(final + ".old")
open(os.path.join(final + ".old", "01 Song.flac"), "w").close()
# `final` is missing (crash happened after rename(final, old), before rename(tmp, final))
staging = str(tmp_path / "staging") # empty/missing staging
import_album(staging, final)
# previous good copy restored to `final`; no leftover .old
assert os.path.isfile(os.path.join(final, "01 Song.flac"))
assert not os.path.isdir(final + ".old")
def test_import_album_drops_stale_old_when_final_present(tmp_path):
final = str(tmp_path / "Artist" / "Album (2020)")
os.makedirs(final)
open(os.path.join(final, "keep.flac"), "w").close()
os.makedirs(final + ".old") # leftover from a completed swap
open(os.path.join(final + ".old", "stale.flac"), "w").close()
import_album(str(tmp_path / "staging"), final)
assert not os.path.isdir(final + ".old") # stale .old removed
assert os.path.isfile(os.path.join(final, "keep.flac")) # existing copy untouched by preamble
def test_clear_staging_root_removes_jobs_but_keeps_root(tmp_path):
# Clears per-job dirs (and stray files) under the root, but leaves the root itself so a
# mounted staging volume stays mounted.