fix: copy through a temporary file so a cut-short copy is not kept
Build and publish container / build (pull_request) Successful in 6m57s
Build and publish container / build (pull_request) Successful in 6m57s
Copies of already-MP3 sources were written straight to their destination while encodes went via a temporary file and a rename. A copy interrupted by a full disk, a killed container or an I/O error therefore left a truncated MP3 in the mirror -- and because shutil.copy2 reproduces the source's mtime along with its bytes, staleness detection would read that fragment as up to date and never replace it. The damage is silent and permanent until someone plays the track. Give copy the same temporary-file-and-rename path encode already uses, so the destination either has the whole file or has nothing.
This commit is contained in:
+15
-3
@@ -264,19 +264,31 @@ def encode(source, mirror, quality_args, dry_run):
|
||||
|
||||
|
||||
def copy(source, mirror, dry_run):
|
||||
"""Copy an already-MP3 source into the mirror."""
|
||||
"""Copy an already-MP3 source into the mirror, atomically."""
|
||||
if dry_run:
|
||||
logger.info("would copy %s", source)
|
||||
return Result("copied", mirror)
|
||||
|
||||
mirror.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Through a temporary file and a rename, for the same reason encodes go
|
||||
# that way, and a sharper one: copy2 reproduces the source's mtime as well
|
||||
# as its bytes, so a copy cut short by a full disk or a killed container
|
||||
# would leave a truncated MP3 that every later pass reads as current.
|
||||
handle, temporary = tempfile.mkstemp(dir=mirror.parent, suffix=".mp3.part")
|
||||
os.close(handle)
|
||||
temporary = Path(temporary)
|
||||
|
||||
try:
|
||||
shutil.copy2(source, mirror)
|
||||
shutil.copy2(source, temporary)
|
||||
# copy2 brings the source's mode with it, and the source library is not
|
||||
# ours to have permissions opinions about.
|
||||
make_group_readable(mirror)
|
||||
make_group_readable(temporary)
|
||||
os.replace(temporary, mirror)
|
||||
except OSError as error:
|
||||
return Result("failed", source, str(error))
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
logger.info("copied %s", source)
|
||||
return Result("copied", mirror)
|
||||
|
||||
Reference in New Issue
Block a user