0fa7dc7982
Wires sync.LogChange into the library mutation sites so /api/library/sync reflects upserts and deletes. Architectural pivot: LogChange's signature is now (ctx, dbq.DBTX, ...) so it works with both *pgxpool.Pool and pgx.Tx. The scanner doesn't run mutations in explicit transactions, so it pool-binds; delete.go matches. Tx-bound callers (likes/playlists in subsequent commits) keep atomicity. Also: sync.FormatUUID centralizes the pgtype.UUID → canonical string conversion that both the scanner and the sync handler need; library_sync.go now uses it instead of a local copy. Best-effort logging on scanner failures (Warn, don't fail the scan): a LogChange error after a successful upsert is rare and self-healing — the next scan that touches the entity re-emits the change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
61 lines
2.0 KiB
Go
61 lines
2.0 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 recoverable: the next
|
|
// library scan reconciles missing files by removing their tracks rows. So
|
|
// the function is retry-safe rather than atomic, by design.
|
|
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
|
|
}
|