fix(scanner): add nil guards to cursor wrapping (#5139)

* fix(persistence): add nil guards to cursor wrapping in folder and mediafile repos

Prevent SIGSEGV panic when queryWithStableResults yields a zero-value
struct on the rows.Err() path (e.g., "database is locked" during
concurrent scanning). Extract cursor wrapping into wrapFolderCursor and
wrapMediaFileCursor with nil checks matching the existing pattern in
album_repository.go.

Fixes #5138

* fix(persistence): wrap original cursor error in nil guard messages

Use %w to preserve the underlying error (e.g., "database is locked")
so callers can use errors.Is/As for root cause analysis. Tests now
verify the original error is accessible via errors.Is.

* fix(persistence): add nil guards and error wrapping in album, folder, and mediafile cursor functions

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão
2026-03-03 07:58:14 -05:00
committed by GitHub
parent c885766854
commit ed4c0ef432
6 changed files with 152 additions and 15 deletions
+10 -1
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"iter"
"maps"
"os"
"path/filepath"
@@ -218,13 +219,21 @@ func (r folderRepository) GetTouchedWithPlaylists() (model.FolderCursor, error)
if err != nil {
return nil, err
}
return wrapFolderCursor(cursor), nil
}
func wrapFolderCursor(cursor iter.Seq2[dbFolder, error]) model.FolderCursor {
return func(yield func(model.Folder, error) bool) {
for f, err := range cursor {
if f.Folder == nil {
yield(model.Folder{}, fmt.Errorf("unexpected nil folder (%v): %w", f, err))
return
}
if !yield(*f.Folder, err) || err != nil {
return
}
}
}, nil
}
}
func (r folderRepository) purgeEmpty(libraryIDs ...int) error {