docs: library comments say what the scanner and resolver do, not how they came to be (#6277)
release / govulncheck (push) Successful in 20s
release / web (push) Successful in 1m20s
release / go (push) Successful in 1m37s
release / integration (push) Successful in 5m15s
release / android (push) Successful in 5m26s
release / Build signed APK (releases and dev) (push) Successful in 5m45s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 16s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 20s
release / web (push) Successful in 1m20s
release / go (push) Successful in 1m37s
release / integration (push) Successful in 5m15s
release / android (push) Successful in 5m26s
release / Build signed APK (releases and dev) (push) Successful in 5m45s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 16s
release / Verify release artifacts (tag releases only) (push) Skipped
Rewrites the internal/library comment blocks to rule 32. Task, milestone, decision and rule ids are gone; the incidents behind them are restated as the constraint they left (why a file is deleted before its row, why the missing mark is a mark, why duplicates block on duration). Counts and album names from one library are dropped from production comments; test comments keep the names their fixtures use. The tagReadVersion log keeps each version's before and after, without the measurements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+25
-23
@@ -30,12 +30,12 @@ var ErrTrackNotFound = errors.New("library: track not found")
|
||||
var removeFile = os.Remove
|
||||
|
||||
// FileRemoveError reports that a track's file exists but could not be removed.
|
||||
// When DeleteTrackFile returns one, NOTHING was deleted: the row, its likes, its
|
||||
// play history and its playlist memberships are all intact.
|
||||
// When DeleteTrackFile returns one, NOTHING was deleted: the row, its likes,
|
||||
// its play history and its playlist memberships are all intact.
|
||||
type FileRemoveError struct {
|
||||
Path string
|
||||
// UID and GID are the identity the server process runs as — the half of a
|
||||
// permission problem the operator cannot see from the host side.
|
||||
// permission problem an admin cannot see from the host side.
|
||||
UID, GID int
|
||||
Err error
|
||||
}
|
||||
@@ -44,15 +44,15 @@ func (e *FileRemoveError) Error() string { return fmt.Sprintf("remove track file
|
||||
|
||||
func (e *FileRemoveError) Unwrap() error { return e.Err }
|
||||
|
||||
// Dir is the directory removal needs write access to. Unlinking a file writes to
|
||||
// its PARENT, so a world-writable file inside a read-only directory still cannot
|
||||
// be removed — naming the file's own permissions would send the operator to the
|
||||
// wrong place.
|
||||
// Dir is the directory removal needs write access to. Unlinking a file writes
|
||||
// to its PARENT, so a world-writable file inside a read-only directory still
|
||||
// cannot be removed — naming the file's own permissions would send an admin to
|
||||
// the wrong place.
|
||||
func (e *FileRemoveError) Dir() string { return filepath.Dir(e.Path) }
|
||||
|
||||
// NotWritable reports whether the library is unwritable for this process — a
|
||||
// read-only mount or a permission denial — rather than an I/O fault. It is the
|
||||
// case the operator can fix, so callers answer it differently.
|
||||
// case an admin can fix, so callers answer it differently.
|
||||
func (e *FileRemoveError) NotWritable() bool {
|
||||
return errors.Is(e.Err, fs.ErrPermission) || errors.Is(e.Err, syscall.EROFS)
|
||||
}
|
||||
@@ -69,36 +69,38 @@ func (e *FileRemoveError) Reason() string {
|
||||
|
||||
// DeletedTrack reports what a delete tidied away beyond the track itself.
|
||||
type DeletedTrack struct {
|
||||
// AlbumID is set when the track was its album's last, so the album went too.
|
||||
// AlbumID is set when the track was its album's last, so the album went
|
||||
// too.
|
||||
AlbumID *pgtype.UUID
|
||||
// ArtistID is set when that album was its artist's last, so the artist went too.
|
||||
// ArtistID is set when that album was its artist's last, so the artist went
|
||||
// too.
|
||||
ArtistID *pgtype.UUID
|
||||
}
|
||||
|
||||
// DeleteTrackFile removes a track's file from disk and then its row, tidying
|
||||
// away an album or artist the delete leaves empty. It is the ONLY path that
|
||||
// deletes a track file: the admin remove-track endpoint and quarantine's Delete
|
||||
// file both come through here (#3918).
|
||||
// file both come through here.
|
||||
//
|
||||
// Order is the whole contract. The file goes first, and if it cannot go — a
|
||||
// read-only mount, a permission denial, an I/O error — nothing else happens and
|
||||
// a *FileRemoveError comes back. Proceeding past that failure is how #3918 lost
|
||||
// a *FileRemoveError comes back. Proceeding past that failure would lose
|
||||
// history: tracks CASCADEs to play_events, general_likes, contextual_likes,
|
||||
// playlist_tracks, track_tags and playback_errors, so the row and everything
|
||||
// hanging off it were destroyed while the file survived, and the next scan
|
||||
// re-imported it as a brand-new track with none of it.
|
||||
// hanging off it would be destroyed while the file survived, and the next scan
|
||||
// would re-import it as a brand-new track with none of it.
|
||||
//
|
||||
// A file that is already gone (fs.ErrNotExist) is not a failure; the row is
|
||||
// removed as asked.
|
||||
//
|
||||
// This is NOT the missing-file path. That lifecycle is deliberately
|
||||
// non-destructive: reconcile stamps missing_since (#2523), selection paths
|
||||
// filter on it, and a returning file is un-marked or adopted (#2528). This is the
|
||||
// explicit, irreversible "remove this recording", never the way to tidy up a row
|
||||
// whose file merely went away.
|
||||
// non-destructive: reconcile stamps missing_since, selection paths filter on
|
||||
// it, and a returning file is un-marked or adopted. This is the explicit,
|
||||
// irreversible "remove this recording", never the way to tidy up a row whose
|
||||
// file merely went away.
|
||||
//
|
||||
// dataDir, when set, also clears the cached art of an artist the delete removed.
|
||||
// logger may be nil.
|
||||
// dataDir, when set, also clears the cached art of an artist the delete
|
||||
// removed. logger may be nil.
|
||||
func DeleteTrackFile(
|
||||
ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger, dataDir string, trackID pgtype.UUID,
|
||||
) (DeletedTrack, error) {
|
||||
@@ -163,9 +165,9 @@ func DeleteTrackFile(
|
||||
}
|
||||
|
||||
// removeTrackFileOnDisk is the one rule for removing a track's file, shared by
|
||||
// DeleteTrackFile and the duplicate merge. A file already gone is fine; anything
|
||||
// else comes back as a *FileRemoveError, and the caller must then change nothing
|
||||
// in the database (#3918).
|
||||
// DeleteTrackFile and the duplicate merge. A file already gone is fine;
|
||||
// anything else comes back as a *FileRemoveError, and the caller must then
|
||||
// change nothing in the database.
|
||||
func removeTrackFileOnDisk(path string) error {
|
||||
if err := removeFile(path); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return &FileRemoveError{Path: path, UID: os.Getuid(), GID: os.Getgid(), Err: err}
|
||||
|
||||
Reference in New Issue
Block a user