fix(library): survive a music disk that is not mounted
ci / api / api (push) Successful in 29s
ci / db / db (push) Successful in 47s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 52s
release / release (версия и заметки) (push) Successful in 4s
release / android (APK в релиз) (push) Successful in 3m20s
ci / api / api (push) Successful in 29s
ci / db / db (push) Successful in 47s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 52s
release / release (версия и заметки) (push) Successful in 4s
release / android (APK в релиз) (push) Successful in 3m20s
On the test server the host music folder was not mounted, so every stream and fingerprint failed with no such file, and a scan of the empty folder would have marked the whole library missing and purged it after 30 days. - a scan of an empty source folder with known files aborts with a clear error - fingerprinting skips such a source with one warning instead of one per file - LIBRARY_HOST mounts use rslave, so a disk mounted after start shows up - update.sh refuses to deploy with an empty LIBRARY_HOST Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
421b1a3eb9
commit
3da82e902a
7 files changed
+66
-1
No files matched your search
@@ -103,7 +103,8 @@ func TestLibraryAndUploads(t *testing.T) {
|
||||
assert.True(t, src.AcceptsUploads)
|
||||
require.NotNil(t, src.WriteTemplate)
|
||||
|
||||
scanner := library.NewScanner(lib, fakeProber{}, library.NewProgressBoard(), log)
|
||||
board := library.NewProgressBoard()
|
||||
scanner := library.NewScanner(lib, fakeProber{}, board, log)
|
||||
var fileIDs []uuid.UUID
|
||||
for _, rel := range []string{"Кино/Группа крови/Кино - Группа крови.flac", "Кино/Группа крови/Кино - Закрой за мной дверь.flac"} {
|
||||
id, err := scanner.ScanFile(ctx, src, root, rel)
|
||||
@@ -114,6 +115,15 @@ func TestLibraryAndUploads(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fileIDs[0], again, "rescan keeps the file row")
|
||||
|
||||
// Пустой каталог при известных файлах (диск не смонтирован) — скан прерывается и ничего не помечает пропавшим.
|
||||
require.NoError(t, scanner.Start(src, t.TempDir(), false))
|
||||
require.Eventually(t, func() bool { p, ok := board.Get(src.ID); return ok && !p.Running }, 10*time.Second, 50*time.Millisecond)
|
||||
p, _ := board.Get(src.ID)
|
||||
assert.Contains(t, p.Error, "is the disk mounted")
|
||||
var missing int
|
||||
require.NoError(t, pool.QueryRow(ctx, `SELECT count(*) FROM media_files WHERE source_id = $1 AND missing_since IS NOT NULL`, src.ID).Scan(&missing))
|
||||
assert.Zero(t, missing)
|
||||
|
||||
// --- Отпечатки ---
|
||||
todo, err := lib.FilesToFingerprint(ctx, true, uuid.Nil, 10)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -198,6 +198,7 @@ func (fp *Fingerprinter) chromaprint() bool {
|
||||
func (fp *Fingerprinter) Run(ctx context.Context) {
|
||||
withFP := fp.chromaprint()
|
||||
roots := map[uuid.UUID]string{}
|
||||
unavailable := map[uuid.UUID]bool{} // каталог источника пуст или не открывается — диск не смонтирован
|
||||
var after uuid.UUID
|
||||
for ctx.Err() == nil {
|
||||
todo, err := fp.store.FilesToFingerprint(ctx, withFP, after, 200)
|
||||
@@ -213,6 +214,9 @@ func (fp *Fingerprinter) Run(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
after = f.ID
|
||||
if unavailable[f.SourceID] {
|
||||
continue
|
||||
}
|
||||
root, ok := roots[f.SourceID]
|
||||
if !ok {
|
||||
if root, err = fp.rootOf(ctx, f.SourceID); err != nil {
|
||||
@@ -220,12 +224,31 @@ func (fp *Fingerprinter) Run(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
roots[f.SourceID] = root
|
||||
if empty, err := dirEmpty(root); empty || err != nil {
|
||||
fp.log.Warn("fingerprints: source folder is empty or unreadable, skipped — is the disk mounted?",
|
||||
"source_id", f.SourceID, "root", root, "err", err)
|
||||
unavailable[f.SourceID] = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
fp.process(ctx, f, filepath.Join(root, filepath.FromSlash(f.RelPath)), withFP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dirEmpty(dir string) (bool, error) {
|
||||
d, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer func() { _ = d.Close() }()
|
||||
names, err := d.Readdirnames(1)
|
||||
if errors.Is(err, io.EOF) {
|
||||
return true, nil
|
||||
}
|
||||
return len(names) == 0, err
|
||||
}
|
||||
|
||||
func (fp *Fingerprinter) process(ctx context.Context, f FingerprintTodo, abs string, withFP bool) {
|
||||
sum := f.SHA256
|
||||
if sum == nil {
|
||||
|
||||
@@ -119,6 +119,15 @@ type walked struct {
|
||||
hash []byte
|
||||
}
|
||||
|
||||
func hasPresent(files map[string]FileRow) bool {
|
||||
for _, f := range files {
|
||||
if !f.Missing {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// job — файл, который нужно прочитать.
|
||||
type job struct {
|
||||
walked
|
||||
@@ -135,6 +144,11 @@ type probed struct {
|
||||
// ErrScanRunning — скан источника уже идёт.
|
||||
var ErrScanRunning = errors.New("library: scan already running")
|
||||
|
||||
// ErrSourceEmpty — в каталоге источника нет ни одного файла, а в библиотеке они есть: скорее всего, диск
|
||||
// не смонтирован. Скан прерывается, чтобы не пометить всю библиотеку отсутствующей (и через MissingRetention —
|
||||
// не удалить треки вместе с историей).
|
||||
var ErrSourceEmpty = errors.New("library: source folder is empty but the library has its files — is the disk mounted?")
|
||||
|
||||
// Start запускает скан в фоне. root — каталог источника в файловой системе api.
|
||||
func (sc *Scanner) Start(src Source, root string, full bool) error {
|
||||
if !sc.board.start(src.ID, full, sc.now()) {
|
||||
@@ -192,6 +206,9 @@ func (sc *Scanner) run(ctx context.Context, src Source, root string, full bool)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(found) == 0 && hasPresent(existing) {
|
||||
return ErrSourceEmpty
|
||||
}
|
||||
sc.board.update(src.ID, func(p *Progress) { p.Found = len(found) })
|
||||
|
||||
var (
|
||||
|
||||
Reference in new issue
Block a user