feat(scanner): add debounced file watching with polling fallback
- Implement event queue with 3-second debouncing for file system events - Add configurable polling fallback (default 3 min) via SCAN_POLL_INTERVAL_MINUTES - Add SyncFilesystemWithDatabase to detect orphaned DB entries and new files - Integrate utils.ResolveMediaURL for consistent media file path resolution - Add COOKIE_SECURE env var with SameSite=LaxMode for session cookies - Update media handler to properly decode URL paths for file serving - Refactor scanner initialization to accept poll interval configuration
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -29,6 +30,8 @@ const (
|
||||
// Note: This is computed from SessionDuration to avoid magic numbers
|
||||
var SessionDurationSec = int(SessionDuration.Seconds())
|
||||
|
||||
var secure = os.Getenv("COOKIE_SECURE")
|
||||
|
||||
type AuthHandler struct {
|
||||
db *database.Queries
|
||||
jwtKey []byte
|
||||
@@ -249,7 +252,8 @@ func (h *AuthHandler) Register(c echo.Context) error {
|
||||
Value: accessToken,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: false, // TODO: Set to true in production with HTTPS
|
||||
Secure: secure == "true", // TODO: Set to true in production with HTTPS
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: SessionDurationSec,
|
||||
}
|
||||
c.SetCookie(cookie)
|
||||
@@ -394,7 +398,8 @@ func (h *AuthHandler) Login(c echo.Context) error {
|
||||
Value: accessToken,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: false, // TODO: Set to true in production with HTTPS
|
||||
Secure: secure == "true", // TODO: Set to true in production with HTTPS
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: SessionDurationSec,
|
||||
}
|
||||
c.SetCookie(cookie)
|
||||
|
||||
@@ -928,10 +928,10 @@ func mediaItemsToListMediaItemsRow(item database.MediaItems) database.ListMediaI
|
||||
Author: item.Author,
|
||||
Isbn: item.Isbn,
|
||||
Description: item.Description,
|
||||
FilePath: item.FilePath,
|
||||
FilePath: utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: true}),
|
||||
FileSize: item.FileSize,
|
||||
MimeType: item.MimeType,
|
||||
CoverImagePath: item.CoverImagePath,
|
||||
CoverImagePath: pgtype.Text{String: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), Valid: true},
|
||||
Series: item.Series,
|
||||
SeriesNumber: item.SeriesNumber,
|
||||
Tags: item.Tags,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bookhoard/internal/config"
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/services"
|
||||
wsync "bookhoard/internal/sync"
|
||||
@@ -27,9 +28,10 @@ type Handler struct {
|
||||
watchingLibraries map[string]bool
|
||||
connManager *wsync.ConnectionManager
|
||||
cleanupTaskCancel context.CancelFunc
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewHandler(db *database.Queries, connManager *wsync.ConnectionManager, queueProcessor *wsync.SyncQueueProcessor) *Handler {
|
||||
func NewHandler(db *database.Queries, connManager *wsync.ConnectionManager, queueProcessor *wsync.SyncQueueProcessor, cfg *config.Config) *Handler {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
worker := services.NewWorker(3)
|
||||
scheduler := services.NewScheduler(worker, db)
|
||||
@@ -40,7 +42,7 @@ func NewHandler(db *database.Queries, connManager *wsync.ConnectionManager, queu
|
||||
|
||||
return &Handler{
|
||||
db: db,
|
||||
scanner: services.NewMediaScanner(db),
|
||||
scanner: services.NewMediaScanner(db, cfg.ScanPollIntervalMinutes),
|
||||
worker: worker,
|
||||
scheduler: scheduler,
|
||||
queueProcessor: queueProcessor,
|
||||
@@ -66,6 +68,6 @@ func parseDate(dateStr string) time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func SetupRoutes(g *echo.Group, db *database.Queries, connManager *wsync.ConnectionManager, queueProcessor *wsync.SyncQueueProcessor) *Handler {
|
||||
return NewHandler(db, connManager, queueProcessor)
|
||||
func SetupRoutes(g *echo.Group, db *database.Queries, connManager *wsync.ConnectionManager, queueProcessor *wsync.SyncQueueProcessor, cfg *config.Config) *Handler {
|
||||
return NewHandler(db, connManager, queueProcessor, cfg)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/services"
|
||||
"bookhoard/internal/utils"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -151,7 +152,7 @@ func BuildSections(sections []services.DashboardSection) []SectionData {
|
||||
MediaItemID: itemUUID.String(),
|
||||
Title: item.Title,
|
||||
Author: textToString(item.Author),
|
||||
CoverImagePath: textToString(item.CoverImagePath),
|
||||
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-10
@@ -9,6 +9,7 @@ import (
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -1490,21 +1491,23 @@ func (mh *MediaHandler) getFullFilePath(ctx context.Context, libraryID pgtype.UU
|
||||
// Requires JWT authentication
|
||||
func (mh *MediaHandler) ServeFile(c echo.Context) error {
|
||||
// URL format: /uploads/library-{libraryID}/{relativePath}
|
||||
path := c.Param("*") // Gets everything after /uploads/library-{id}/
|
||||
|
||||
// Extract library ID from path
|
||||
parts := strings.SplitN(path, "/", 2)
|
||||
if len(parts) < 2 {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||
}
|
||||
|
||||
libraryIDStr := strings.TrimPrefix(parts[0], "library-")
|
||||
// Get library ID directly from route parameter
|
||||
libraryIDStr := c.Param("id")
|
||||
libraryUUID, err := uuid.Parse(libraryIDStr)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library ID"})
|
||||
}
|
||||
|
||||
relativePath := parts[1]
|
||||
// Get remaining path from URL
|
||||
rawPath := c.Param("*")
|
||||
relativePath, err := url.QueryUnescape(rawPath)
|
||||
if err != nil {
|
||||
relativePath = rawPath
|
||||
}
|
||||
|
||||
if relativePath == "" {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||
}
|
||||
|
||||
// Resolve using service
|
||||
fullPath, err := mh.getFullFilePath(c.Request().Context(), pgtype.UUID{Bytes: libraryUUID, Valid: true}, relativePath)
|
||||
|
||||
@@ -197,7 +197,7 @@ func (h *Handler) StartWatchModeForLibrary(ctx context.Context, libraryID pgtype
|
||||
folderPaths[i] = folder.FolderPath
|
||||
}
|
||||
|
||||
scanner := services.NewMediaScanner(h.db)
|
||||
scanner := services.NewMediaScanner(h.db, 3)
|
||||
if err := scanner.SetFolders(folderPaths); err != nil {
|
||||
return fmt.Errorf("failed to set scanner folders: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user