Files
bvandeusen bda0896d82
test-go / test (push) Successful in 28s
test-go / integration (push) Has been cancelled
docs(server): drift #572 — delete.go honest about missing reconcile
The docstring claimed "the next library scan reconciles missing
files by removing their tracks rows" — but scanner.go only does
filepath.WalkDir + UpsertTrack; it never enumerates existing rows
to check file_path presence, and it never DELETEs orphan rows. The
audit verified this — repo-wide grep finds no orphan-sweep code.

The lie is load-bearing: lidarrquarantine/service.go:270 leans on
this guarantee, so downstream code thinks the orphan case heals
itself. Fix the comment to state reality (admin re-trigger or
manual cleanup) and reference the open follow-up for adding a real
sweep. The actual reconcile pass is a separate piece of work
(needs scanrun integration + retention semantics + tests) and
stays in the Scribe audit queue.
2026-06-02 18:26:45 -04:00

64 lines
2.2 KiB
Go

package library
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
syncpkg "git.fabledsword.com/bvandeusen/minstrel/internal/sync"
)
// ErrTrackNotFound is returned when DeleteTrackFile is called with an id
// that has no row in tracks.
var ErrTrackNotFound = errors.New("library: track not found")
// DeleteTrackFile removes a track file from disk and its row from the
// tracks table. Album and artist rows are left untouched.
//
// Steps:
// 1. Look up the track to get its file_path.
// 2. Remove the file from disk. fs.ErrNotExist is OK — already gone.
// 3. Delete the tracks row.
//
// Order matters: file first, then DB. If the file delete fails (permission,
// I/O error), we leave the DB row alone so the admin can retry. The reverse
// failure mode — file gone, DB row still present — is currently NOT
// auto-reconciled (drift #572 audit found the misleading prior claim
// that a scan would clean it up — the scanner only walks + upserts;
// it does not enumerate orphan rows). An admin must re-trigger
// DeleteTrackFile or delete the row manually. A scanrun orphan-row
// sweep is tracked as future work in the audit queue.
func DeleteTrackFile(ctx context.Context, pool *pgxpool.Pool, trackID pgtype.UUID) error {
q := dbq.New(pool)
track, err := q.GetTrackByID(ctx, trackID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrTrackNotFound
}
return fmt.Errorf("get track: %w", err)
}
if err := os.Remove(track.FilePath); err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("remove file: %w", err)
}
if _, err := pool.Exec(ctx, "DELETE FROM tracks WHERE id = $1", trackID); err != nil {
return fmt.Errorf("delete row: %w", err)
}
// Log the change after the delete succeeds. Best-effort: a Warn-level
// failure here would leave the cache index orphaned on offline clients
// until the next scan touches the surrounding album.
if err := syncpkg.LogChange(ctx, pool, syncpkg.EntityTrack,
syncpkg.FormatUUID(trackID), syncpkg.OpDelete); err != nil {
return fmt.Errorf("log change: %w", err)
}
return nil
}