refactor(scanner): make poll interval dynamic from database

- Add GetPollInterval() method to MediaScanner to read from database
- Add GetAutoScanEnabled() method to check if auto-scan is enabled
- Remove ScanPollIntervalSeconds from config (now DB-driven)
- Update NewMediaScanner signature to not require interval parameter
- Remove SCAN_POLL_INTERVAL_SECONDS from docker-compose env var
This commit is contained in:
2026-02-28 12:57:06 -05:00
parent 4d0d86838a
commit ce72781ec0
4 changed files with 72 additions and 44 deletions
-1
View File
@@ -10,7 +10,6 @@ services:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${DBPASS} POSTGRES_PASSWORD: ${DBPASS}
COOKIE_SECURE: false # make true in production with HTTPS COOKIE_SECURE: false # make true in production with HTTPS
SCAN_POLL_INTERVAL_SECONDS: 30
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
- ./database/schema:/docker-entrypoint-initdb.d - ./database/schema:/docker-entrypoint-initdb.d
+24 -26
View File
@@ -7,37 +7,35 @@ import (
) )
type Config struct { type Config struct {
ServerPort string ServerPort string
BaseURL string BaseURL string
JWTSecret string JWTSecret string
UploadPath string UploadPath string
DatabaseHost string DatabaseHost string
DatabasePort string DatabasePort string
DatabaseUser string DatabaseUser string
DatabasePassword string DatabasePassword string
DatabaseName string DatabaseName string
TestMode bool TestMode bool
RateLimitEnabled bool RateLimitEnabled bool
RequestsPerMinute int RequestsPerMinute int
ScanPollIntervalSeconds int `env:"SCAN_POLL_INTERVAL_SECONDS" default:"30"`
} }
func LoadConfig() *Config { func LoadConfig() *Config {
port := getEnv("SERVER_PORT", "8765") port := getEnv("SERVER_PORT", "8765")
return &Config{ return &Config{
ServerPort: port, ServerPort: port,
BaseURL: getEnv("BASE_URL", "http://localhost:"+port), BaseURL: getEnv("BASE_URL", "http://localhost:"+port),
DatabaseHost: getEnv("DATABASE_HOST", "localhost"), DatabaseHost: getEnv("DATABASE_HOST", "localhost"),
DatabasePort: getEnv("DATABASE_PORT", "5432"), DatabasePort: getEnv("DATABASE_PORT", "5432"),
DatabaseUser: getEnv("DATABASE_USER", "postgres"), DatabaseUser: getEnv("DATABASE_USER", "postgres"),
DatabasePassword: getEnv("DATABASE_PASSWORD", "password"), DatabasePassword: getEnv("DATABASE_PASSWORD", "password"),
DatabaseName: getEnv("DATABASE_NAME", "bookhoard"), DatabaseName: getEnv("DATABASE_NAME", "bookhoard"),
JWTSecret: getEnv("JWT_SECRET", "your-secret-key"), JWTSecret: getEnv("JWT_SECRET", "your-secret-key"),
UploadPath: getEnv("UPLOAD_PATH", "./uploads"), UploadPath: getEnv("UPLOAD_PATH", "./uploads"),
TestMode: getEnvBool("TEST_MODE", false), TestMode: getEnvBool("TEST_MODE", false),
RateLimitEnabled: getEnvBool("RATE_LIMIT_ENABLED", true), RateLimitEnabled: getEnvBool("RATE_LIMIT_ENABLED", true),
RequestsPerMinute: getEnvInt("REQUESTS_PER_MINUTE", 10), RequestsPerMinute: getEnvInt("REQUESTS_PER_MINUTE", 10),
ScanPollIntervalSeconds: getEnvInt("SCAN_POLL_INTERVAL_SECONDS", 30),
} }
} }
+1 -4
View File
@@ -16,7 +16,6 @@ type Handler struct {
db *database.Queries db *database.Queries
scanner *services.MediaScanner scanner *services.MediaScanner
worker *services.Worker worker *services.Worker
scheduler *services.Scheduler
queueProcessor *wsync.SyncQueueProcessor queueProcessor *wsync.SyncQueueProcessor
queueCtx context.Context queueCtx context.Context
queueCancel context.CancelFunc queueCancel context.CancelFunc
@@ -34,7 +33,6 @@ type Handler struct {
func NewHandler(db *database.Queries, connManager *wsync.ConnectionManager, queueProcessor *wsync.SyncQueueProcessor, cfg *config.Config) *Handler { func NewHandler(db *database.Queries, connManager *wsync.ConnectionManager, queueProcessor *wsync.SyncQueueProcessor, cfg *config.Config) *Handler {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
worker := services.NewWorker(3) worker := services.NewWorker(3)
scheduler := services.NewScheduler(worker, db)
queueCtx, queueCancel := context.WithCancel(context.Background()) queueCtx, queueCancel := context.WithCancel(context.Background())
@@ -42,9 +40,8 @@ func NewHandler(db *database.Queries, connManager *wsync.ConnectionManager, queu
return &Handler{ return &Handler{
db: db, db: db,
scanner: services.NewMediaScanner(db, cfg.ScanPollIntervalSeconds), scanner: services.NewMediaScanner(db),
worker: worker, worker: worker,
scheduler: scheduler,
queueProcessor: queueProcessor, queueProcessor: queueProcessor,
queueCtx: queueCtx, queueCtx: queueCtx,
queueCancel: queueCancel, queueCancel: queueCancel,
+47 -13
View File
@@ -90,18 +90,12 @@ type MediaScanner struct {
} }
// NewMediaScanner creates a new media scanner instance // NewMediaScanner creates a new media scanner instance
func NewMediaScanner(db *database.Queries, pollIntervalSeconds int) *MediaScanner { func NewMediaScanner(db *database.Queries) *MediaScanner {
watcher, err := fsnotify.NewWatcher() watcher, err := fsnotify.NewWatcher()
if err != nil { if err != nil {
panic(fmt.Sprintf("Failed to create file watcher: %v", err)) panic(fmt.Sprintf("Failed to create file watcher: %v", err))
} }
pollInterval := 30 * time.Second
if pollInterval > 0 {
pollInterval = time.Duration(pollIntervalSeconds) * time.Second
}
return &MediaScanner{ return &MediaScanner{
db: db, db: db,
watcher: watcher, watcher: watcher,
@@ -111,10 +105,44 @@ func NewMediaScanner(db *database.Queries, pollIntervalSeconds int) *MediaScanne
libraryTypes: make(map[string][]string), libraryTypes: make(map[string][]string),
logger: NewScannerLogger(), logger: NewScannerLogger(),
eventQueue: make(chan string, 500), eventQueue: make(chan string, 500),
pollInterval: pollInterval,
} }
} }
func (s *MediaScanner) GetPollInterval() time.Duration {
if s.db == nil {
return 30 * time.Second
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
setting, err := s.db.GetSystemSetting(ctx, "scan_poll_interval_seconds")
if err != nil || setting == "" {
return 30 * time.Second
}
seconds, err := strconv.Atoi(setting)
if err != nil || seconds < 1 {
return 30 * time.Second
}
return time.Duration(seconds) * time.Second
}
func (s *MediaScanner) GetAutoScanEnabled() bool {
if s.db == nil {
return true // default to enabled
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
setting, err := s.db.GetSystemSetting(ctx, "auto_scan_enabled")
if err != nil || setting == "" {
return true
}
enabled, err := strconv.ParseBool(setting)
if err != nil {
return true
}
return enabled
}
func (s *MediaScanner) SetAdminID(adminID pgtype.UUID) { func (s *MediaScanner) SetAdminID(adminID pgtype.UUID) {
s.adminID = adminID s.adminID = adminID
} }
@@ -1654,26 +1682,32 @@ func (s *MediaScanner) handleFileDelete(ctx context.Context, filePath string) {
} }
func (s *MediaScanner) StartPolling(ctx context.Context) { func (s *MediaScanner) StartPolling(ctx context.Context) {
if s.pollInterval <= 0 { interval := s.GetPollInterval()
if interval <= 0 {
fmt.Println("Polling fallback disabled (interval = 0") fmt.Println("Polling fallback disabled (interval = 0")
return return
} }
ticker := time.NewTicker(s.pollInterval) fmt.Printf("Polling fallback started with interval: %v\n", interval)
defer ticker.Stop()
fmt.Printf("Polling fallback started with interval: %v\n", s.pollInterval)
for { for {
ticker := time.NewTicker(interval)
defer ticker.Stop()
select { select {
case <-ctx.Done(): case <-ctx.Done():
fmt.Println("Polling fallback stopped") fmt.Println("Polling fallback stopped")
return return
case <-ticker.C: case <-ticker.C:
fmt.Println("Running polling fallback sync...") //Re-read interval each tick for dynamic updates
interval = s.GetPollInterval()
fmt.Printf("Running polling fallback sync (interval: %v)...\n", interval)
if err := s.SyncFilesystemWithDatabase(ctx); err != nil { if err := s.SyncFilesystemWithDatabase(ctx); err != nil {
fmt.Printf("Polling sync error: %v\n", err) fmt.Printf("Polling sync error: %v\n", err)
} }
} }
} }
} }
func (s *MediaScanner) SyncFilesystemWithDatabase(ctx context.Context) error { func (s *MediaScanner) SyncFilesystemWithDatabase(ctx context.Context) error {
fmt.Println("[POLL-SYNC] Starting filesystem sync with database") fmt.Println("[POLL-SYNC] Starting filesystem sync with database")
for _, folder := range s.folders { for _, folder := range s.folders {