diff --git a/internal/services/media_scanner.go b/internal/services/media_scanner.go index a6101b3c..cd0bf8c9 100644 --- a/internal/services/media_scanner.go +++ b/internal/services/media_scanner.go @@ -419,6 +419,14 @@ func (s *MediaScanner) enqueueLibraryScan(rootFolder string) { } 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 { return fmt.Errorf("no folders set") } @@ -428,6 +436,9 @@ func (s *MediaScanner) ScanFolders(ctx context.Context) error { s.errors = 0 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 !d.IsDir() && s.isScannableFile(path) { s.totalFiles++ @@ -445,6 +456,10 @@ func (s *MediaScanner) ScanFolders(ctx context.Context) error { mediaFiles := 0 for _, folder := range s.folders { + if ctx.Err() != nil { + return ctx.Err() + } + fmt.Printf("Scanning folder: %s\n", folder) 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 { + if ctx.Err() != nil { + return fs.SkipAll + } + if err != nil { fmt.Printf("Error accessing path %s: %v\n", path, err) 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", 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 // archived after two consecutive missing scans (reading history kept, // 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 // and left orphaned rows undetected. 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) if err != nil { fmt.Printf("[ARCHIVE] Warning: no library found for folder %s, skipping archive pass: %v\n", folder, err)