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:
@@ -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
@@ -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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user