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 }