Fix 1 - File modification time for created_at: - Get file.ModTime() in processMediaFile and pass to CreateMediaItem - Modified SQL INSERT to include created_at column Fix 2 - Force rescan UPDATE instead of DELETE+INSERT: - Changed force rescan logic to call updateMediaItem instead of delete + create - Preserves created_at timestamp on force rescan Fix 3 - GetMediaItemByFilePath filters by library_id: - Added library_id to WHERE clause in SQL query - Created GetMediaItemByFilePathAnyLibrary for cross-library lookups (KOReader) - Added SetLibraryID method to MediaScanner - Updated handler to call SetLibraryID for watch mode Fix 4 - File deletion handling with persistent logging: - Added fsnotify.Remove handler in WatchChanges - Added orphan cleanup in ScanFolders after scan completes - Created scanner_logger.go with daily log rotation (7 days) - Logs to /app/logs/scanner-deletes-YYYY-MM-DD.log and scanner-errors-YYYY-MM-DD.log - Individual deletes with enhanced safety logging Note: Integration tests can now safely scan /app/uploads because GetMediaItemByFilePath now filters by library_id, preventing cross-library interference.
108 lines
2.4 KiB
Go
108 lines
2.4 KiB
Go
package services
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
logDir = "/app/logs"
|
|
maxLogAgeDays = 7
|
|
)
|
|
|
|
type ScannerLogger struct {
|
|
deletesFile *os.File
|
|
errorsFile *os.File
|
|
currentDate string
|
|
}
|
|
|
|
func NewScannerLogger() *ScannerLogger {
|
|
return &ScannerLogger{}
|
|
}
|
|
|
|
func (l *ScannerLogger) ensureLogFiles() error {
|
|
today := time.Now().Format("2006-01-02")
|
|
|
|
if l.currentDate == today && l.deletesFile != nil {
|
|
return nil
|
|
}
|
|
|
|
if l.deletesFile != nil {
|
|
l.deletesFile.Close()
|
|
}
|
|
if l.errorsFile != nil {
|
|
l.errorsFile.Close()
|
|
}
|
|
|
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create log directory: %v", err)
|
|
}
|
|
|
|
deletesPath := filepath.Join(logDir, fmt.Sprintf("scanner-deletes-%s.log", today))
|
|
errorsPath := filepath.Join(logDir, fmt.Sprintf("scanner-errors-%s.log", today))
|
|
|
|
deletesFile, err := os.OpenFile(deletesPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open deletes log file: %v", err)
|
|
}
|
|
|
|
errorsFile, err := os.OpenFile(errorsPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
deletesFile.Close()
|
|
return fmt.Errorf("failed to open errors log file: %v", err)
|
|
}
|
|
|
|
l.deletesFile = deletesFile
|
|
l.errorsFile = errorsFile
|
|
l.currentDate = today
|
|
|
|
l.cleanupOldLogs()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (l *ScannerLogger) cleanupOldLogs() {
|
|
cutoff := time.Now().AddDate(0, 0, -maxLogAgeDays)
|
|
|
|
filepath.Walk(logDir, func(path string, info os.FileInfo, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if !info.IsDir() && info.ModTime().Before(cutoff) {
|
|
os.Remove(path)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (l *ScannerLogger) LogDelete(message string) {
|
|
if err := l.ensureLogFiles(); err != nil {
|
|
fmt.Printf("ERROR: Failed to ensure log files: %v\n", err)
|
|
return
|
|
}
|
|
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
|
logLine := fmt.Sprintf("[%s] %s\n", timestamp, message)
|
|
l.deletesFile.WriteString(logLine)
|
|
}
|
|
|
|
func (l *ScannerLogger) LogError(message string) {
|
|
if err := l.ensureLogFiles(); err != nil {
|
|
fmt.Printf("ERROR: Failed to ensure log files: %v\n", err)
|
|
return
|
|
}
|
|
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
|
logLine := fmt.Sprintf("[%s] %s\n", timestamp, message)
|
|
l.errorsFile.WriteString(logLine)
|
|
}
|
|
|
|
func (l *ScannerLogger) Close() {
|
|
if l.deletesFile != nil {
|
|
l.deletesFile.Close()
|
|
}
|
|
if l.errorsFile != nil {
|
|
l.errorsFile.Close()
|
|
}
|
|
}
|