feat(scanner): ScanFolders honors its context; cancelling never archives
ScanFolders never checked its context, so a cancelled job's walk ran to completion no matter what the worker's cancel did. - ctx checks at every stage boundary (file-counting pass, per folder, before and during the archive pass) and inside the walk callback (fs.SkipAll): the file currently being processed finishes, then the scan aborts with context.Canceled, which processJob classifies as a cancelled job rather than a failed one - the archive/purge pass is skipped entirely when the scan was cancelled. It only visits what the walk visited, so after a partial walk the unvisited (but on-disk) files would be treated as missing, archived, and eventually purged. Cancelling is a UI convenience and must never destroy library data.
This commit is contained in:
1 parent
b2220756e2
commit
30ecdd4bf3
1 file changed
+32
@@ -419,6 +419,14 @@ func (s *MediaScanner) enqueueLibraryScan(rootFolder string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *MediaScanner) ScanFolders(ctx context.Context) error {
|
func (s *MediaScanner) ScanFolders(ctx context.Context) error {
|
||||||
|
// Cancellation is checked at every stage boundary and inside the
|
||||||
|
// walk callback: the in-flight file finishes, then the scan aborts
|
||||||
|
// and reports context.Canceled (processJob classifies that as a
|
||||||
|
// cancelled job, not a failed one).
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
if len(s.folders) == 0 {
|
if len(s.folders) == 0 {
|
||||||
return fmt.Errorf("no folders set")
|
return fmt.Errorf("no folders set")
|
||||||
}
|
}
|
||||||
@@ -428,6 +436,9 @@ func (s *MediaScanner) ScanFolders(ctx context.Context) error {
|
|||||||
s.errors = 0
|
s.errors = 0
|
||||||
|
|
||||||
for _, folder := range s.folders {
|
for _, folder := range s.folders {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
if err := filepath.WalkDir(folder, func(path string, d fs.DirEntry, err error) error {
|
if err := filepath.WalkDir(folder, func(path string, d fs.DirEntry, err error) error {
|
||||||
if !d.IsDir() && s.isScannableFile(path) {
|
if !d.IsDir() && s.isScannableFile(path) {
|
||||||
s.totalFiles++
|
s.totalFiles++
|
||||||
@@ -445,6 +456,10 @@ func (s *MediaScanner) ScanFolders(ctx context.Context) error {
|
|||||||
mediaFiles := 0
|
mediaFiles := 0
|
||||||
|
|
||||||
for _, folder := range s.folders {
|
for _, folder := range s.folders {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("Scanning folder: %s\n", folder)
|
fmt.Printf("Scanning folder: %s\n", folder)
|
||||||
|
|
||||||
if _, err := os.Stat(folder); os.IsNotExist(err) {
|
if _, err := os.Stat(folder); os.IsNotExist(err) {
|
||||||
@@ -454,6 +469,10 @@ func (s *MediaScanner) ScanFolders(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := filepath.WalkDir(folder, func(path string, d fs.DirEntry, err error) error {
|
err := filepath.WalkDir(folder, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return fs.SkipAll
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error accessing path %s: %v\n", path, err)
|
fmt.Printf("Error accessing path %s: %v\n", path, err)
|
||||||
s.errors++
|
s.errors++
|
||||||
@@ -502,6 +521,14 @@ func (s *MediaScanner) ScanFolders(ctx context.Context) error {
|
|||||||
fmt.Printf("Scan completed: %d total files scanned, %d media files found, %d new items, %d errors\n",
|
fmt.Printf("Scan completed: %d total files scanned, %d media files found, %d new items, %d errors\n",
|
||||||
processedFiles, mediaFiles, s.newItems, s.errors)
|
processedFiles, mediaFiles, s.newItems, s.errors)
|
||||||
|
|
||||||
|
// A cancelled scan must not run the archive lifecycle pass below:
|
||||||
|
// it never visited every folder, so unvisited files would be
|
||||||
|
// treated as missing and archived (and eventually purged). Abort
|
||||||
|
// instead — cancelling never destroys library data.
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
// Archive lifecycle pass. Items whose files vanished from disk are
|
// Archive lifecycle pass. Items whose files vanished from disk are
|
||||||
// archived after two consecutive missing scans (reading history kept,
|
// archived after two consecutive missing scans (reading history kept,
|
||||||
// item hidden), and purged for good once archived older than the
|
// item hidden), and purged for good once archived older than the
|
||||||
@@ -509,6 +536,11 @@ func (s *MediaScanner) ScanFolders(ctx context.Context) error {
|
|||||||
// Every branch logs - the previous hard-delete cleanup failed silently
|
// Every branch logs - the previous hard-delete cleanup failed silently
|
||||||
// and left orphaned rows undetected.
|
// and left orphaned rows undetected.
|
||||||
for _, folder := range s.folders {
|
for _, folder := range s.folders {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
fmt.Printf("[ARCHIVE] Scan cancelled, stopping archive pass\n")
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
lib, err := s.db.GetLibraryByFolder(ctx, folder)
|
lib, err := s.db.GetLibraryByFolder(ctx, folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[ARCHIVE] Warning: no library found for folder %s, skipping archive pass: %v\n", folder, err)
|
fmt.Printf("[ARCHIVE] Warning: no library found for folder %s, skipping archive pass: %v\n", folder, err)
|
||||||
|
|||||||
Reference in new issue
Block a user