Two ways the mirror ends up wrong, both invisible until someone tries to read
or play a track.
Nothing written into the mirror is group-readable, so the account that
serves it cannot read it. Encoded tracks go through tempfile.mkstemp, which
creates 0600 whatever the umask, and os.replace carries that mode into the
mirror unchanged. Copied MP3s go through shutil.copy2, which reproduces the
mode of a source file in a library this tool does not own.
Copies are not atomic. Encodes are written to a temporary file and renamed
into place; copies were written straight to their destination. A copy cut short
by a full disk, a killed container or an I/O error leaves a truncated MP3 in
the mirror -- and copy2 brings the source's mtime across with the bytes, so
staleness detection reads that fragment as up to date and never replaces it.
Change
The group-read bit is added to the temporary file before the rename, so a
mirror file is never visible without it, and to a copy before it lands.
Directories are covered by clearing the group bits from the process umask
once at startup rather than chmod'ing each one -- a file the group cannot
reach is no better than one it cannot read. Files cannot be done that way
because mkstemp and copy2 both set a mode outright.
Only the group bits are touched. World bits and ownership stay with the
umask, as before.
An existing mirror is repaired on the next pass: those files have correct
mtimes, so nothing else would revisit them, and the fix is a stat and a chmod, not a re-encode.
copy() now writes to a temporary file in the destination directory and
renames it into place, so the destination has either the whole file or
nothing.
Testing
Five new tests, each failing on main and passing here: encoded file, copied
file from a 0600 source, directory traversability under a 0077 umask,
repair-without-re-encode, and an interrupted copy (a patched copy2 that
writes 64 bytes and raises) leaving neither a mirror file nor a .part
straggler. A sixth asserts --dry-run still changes nothing. 27 passed
locally and in docker build --target test, which is what CI runs.
## Problem
Two ways the mirror ends up wrong, both invisible until someone tries to read
or play a track.
**Nothing written into the mirror is group-readable**, so the account that
serves it cannot read it. Encoded tracks go through `tempfile.mkstemp`, which
creates `0600` whatever the umask, and `os.replace` carries that mode into the
mirror unchanged. Copied MP3s go through `shutil.copy2`, which reproduces the
mode of a source file in a library this tool does not own.
**Copies are not atomic.** Encodes are written to a temporary file and renamed
into place; copies were written straight to their destination. A copy cut short
by a full disk, a killed container or an I/O error leaves a truncated MP3 in
the mirror -- and `copy2` brings the source's mtime across with the bytes, so
staleness detection reads that fragment as up to date and never replaces it.
## Change
- The group-read bit is added to the temporary file *before* the rename, so a
mirror file is never visible without it, and to a copy before it lands.
- Directories are covered by clearing the group bits from the process umask
once at startup rather than chmod'ing each one -- a file the group cannot
reach is no better than one it cannot read. Files cannot be done that way
because `mkstemp` and `copy2` both set a mode outright.
- Only the group bits are touched. World bits and ownership stay with the
umask, as before.
- An existing mirror is repaired on the next pass: those files have correct
mtimes, so nothing else would revisit them, and the fix is a `stat` and a
`chmod`, not a re-encode.
- `copy()` now writes to a temporary file in the destination directory and
renames it into place, so the destination has either the whole file or
nothing.
## Testing
Five new tests, each failing on `main` and passing here: encoded file, copied
file from a `0600` source, directory traversability under a `0077` umask,
repair-without-re-encode, and an interrupted copy (a patched `copy2` that
writes 64 bytes and raises) leaving neither a mirror file nor a `.part`
straggler. A sixth asserts `--dry-run` still changes nothing. 27 passed
locally and in `docker build --target test`, which is what CI runs.
The mirror is written by one account and read by another -- an SMB share, or
whatever else serves it -- but nothing here produced a group-readable file.
Encodes go through `tempfile.mkstemp`, which creates 0600 regardless of the
umask and keeps that mode through the rename into place, so every encoded
track landed unreadable. Copies of existing MP3s inherit the mode of a source
file in a library this tool does not own, which may be no better.
Add the group-read bit explicitly: to the temporary file before it is renamed,
so a mirror file is never visible without it, and to a copy once it has landed.
Directories are handled by clearing the group bits from the process umask
rather than chmod'ing each one, since a file the group cannot reach is no more
useful than one it cannot read. Only the group bits are touched; the world bits
and ownership stay with the umask as before.
Mirror files written before this are repaired on the next pass. Their mtimes
are correct, so no other part of the pass would revisit them, and topping up
the mode costs a stat rather than a re-encode.
Explain why the group bits are set explicitly rather than left to the umask,
what is deliberately not touched, and that an existing mirror is repaired in
place rather than re-encoded.
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.
The umask handling added for group access cleared only the group bits and left
owner and other to the environment. A container whose umask carries 0400 then
produces mirror directories of mode 0300: writable and enterable, unreadable to
the very run that created them, and unreadable to anything serving the share.
Clear the owner read and execute bits from the umask as well. The `other` bits
stay where the environment puts them, because whether the mirror is
world-readable is a real policy question; being able to read a directory the
process itself just created is not.
Files were never exposed to this: mkstemp sets 0600 outright and copy2 takes
the source file's mode, both ignoring the umask.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
Two ways the mirror ends up wrong, both invisible until someone tries to read
or play a track.
Nothing written into the mirror is group-readable, so the account that
serves it cannot read it. Encoded tracks go through
tempfile.mkstemp, whichcreates
0600whatever the umask, andos.replacecarries that mode into themirror unchanged. Copied MP3s go through
shutil.copy2, which reproduces themode of a source file in a library this tool does not own.
Copies are not atomic. Encodes are written to a temporary file and renamed
into place; copies were written straight to their destination. A copy cut short
by a full disk, a killed container or an I/O error leaves a truncated MP3 in
the mirror -- and
copy2brings the source's mtime across with the bytes, sostaleness detection reads that fragment as up to date and never replaces it.
Change
mirror file is never visible without it, and to a copy before it lands.
once at startup rather than chmod'ing each one -- a file the group cannot
reach is no better than one it cannot read. Files cannot be done that way
because
mkstempandcopy2both set a mode outright.umask, as before.
mtimes, so nothing else would revisit them, and the fix is a
statand achmod, not a re-encode.copy()now writes to a temporary file in the destination directory andrenames it into place, so the destination has either the whole file or
nothing.
Testing
Five new tests, each failing on
mainand passing here: encoded file, copiedfile from a
0600source, directory traversability under a0077umask,repair-without-re-encode, and an interrupted copy (a patched
copy2thatwrites 64 bytes and raises) leaving neither a mirror file nor a
.partstraggler. A sixth asserts
--dry-runstill changes nothing. 27 passedlocally and in
docker build --target test, which is what CI runs.67f99e6531toa1382185a7