Files
bookhoard/cmd/server/main.go
T
john-okeefe 209e9f2a3c feat: implement relative path storage and URL resolution for media files
- Add libraryService dependency to CollectionHandler and OPDSHandler for centralized path resolution
- Create internal/utils/mediaurl.go with ResolveMediaURL() function as single source of truth
- Update GetMediaItem and ListMediaItems handlers to return resolved URLs in API responses
- Update collection handlers (GetCollection, TestRules, PreviewCollection) to use resolved cover URLs
- Update progress handler (GetAllProgress) to use resolved cover URLs
- Add library_id to GetCollectionItems SQL query to enable URL resolution
- Refactor media scanner to store relative paths instead of absolute filesystem paths
- Add ResolveMediaPath() to LibraryService for resolving relative paths to absolute paths
- Add ServeFile endpoint at /uploads/library-:id/* for authenticated file serving
- Add MimeTypes map to library_service.go for consistent MIME type handling
- Update DownloadBook handler to use resolved filesystem paths
- Add getRelativePath() helper to MediaScanner for converting absolute to relative paths
- Use strings.EqualFold for case-insensitive path comparisons in zip extraction

This change enables the application to work with relative paths stored in the
database, making it portable across different server environments while
maintaining backward compatibility with existing absolute paths.
2026-02-27 16:51:44 -05:00

211 lines
6.9 KiB
Go

package main
import (
"bookhoard/internal/app"
"bookhoard/internal/config"
"bookhoard/internal/database"
"bookhoard/internal/handlers"
"bookhoard/internal/middleware"
ratelimit "bookhoard/internal/middleware"
"bookhoard/internal/router"
"bookhoard/internal/services"
"bookhoard/internal/sync"
"context"
"log"
"net/http"
"time"
"github.com/go-playground/validator/v10"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/labstack/echo/v4"
echomiddleware "github.com/labstack/echo/v4/middleware"
)
// CustomValidator wraps the go-playground validator
type CustomValidator struct {
validator *validator.Validate
}
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.validator.Struct(i)
}
// TODO: templates package was removed during router refactor
// This function is unused and should be removed or updated
/*
func getTemplateUserWithTheme(c echo.Context, queries *database.Queries) (templates.User, error) {
// Get the user from context
userID := c.Get("user_id")
if userID == nil {
return templates.User{}, fmt.Errorf("user not authenticated")
}
// Convert to UUID
userUUID, err := uuid.Parse(userID.(string))
if err != nil {
return templates.User{}, fmt.Errorf("invalid user ID: %w", err)
}
// Fetch user from database
userDB, err := queries.GetUser(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
if err != nil {
return templates.User{}, err
}
userTheme := "tokyo-night"
if userDB.Theme.Valid {
userTheme = userDB.Theme.String
}
return templates.User{
ID: userID,
Email: userEmail,
Username: userUsername,
Role: userRole,
Theme: userTheme,
}, nil
}
*/
func main() {
cfg := config.LoadConfig()
dbPool, err := pgxpool.New(context.Background(), cfg.DatabaseURL())
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer dbPool.Close()
queries := database.New(dbPool)
// Initialize database schema
log.Println("🔧 Ensuring database schema is initialized...")
ctx := context.Background()
if err := database.Initialize(ctx, dbPool); err != nil {
log.Fatal("❌ Database schema initialization failed:", err)
}
log.Println("✅ Database schema initialized and verified, starting server...")
// Create login attempt tracker: 5 failed attempts = 15 minute lockout
loginAttemptTracker := ratelimit.NewLoginAttemptTracker(5, 15*time.Minute, 5*time.Minute)
authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret, loginAttemptTracker)
systemSettingsHandler := handlers.NewSystemSettingsHandler(queries)
libraryHandler := handlers.NewLibraryHandler(queries)
deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg)
deviceAuthMiddleware := middleware.NewDeviceAuthMiddleware(queries)
// Create WebSocket connection manager
connManager := sync.NewConnectionManager()
// Create sync queue processor
queueProcessor := sync.NewSyncQueueProcessor(queries)
// Create library service
libraryService := services.NewLibraryService(queries)
// Create worker for background tasks
worker := services.NewWorker(3)
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor)
wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware)
conflictHandler := handlers.NewConflictHandler(queries, connManager)
analyticsHandler := handlers.NewAnalyticsHandler(queries)
queueHandler := handlers.NewQueueHandler(queries, queueProcessor)
// Create conversion service for EPUB→KEPUB conversion
conversionService := services.NewConversionService(queries, "/var/bookhoard/cache/kepub")
opdsHandler := handlers.NewOPDSHandler(queries, libraryService, conversionService)
// NEW: Create refactored handlers
collectionHandler := handlers.NewCollectionHandler(queries, libraryService, connManager)
dashboardService := services.NewDashboardService(queries)
dashboardHandler := handlers.NewDashboardHandler(queries)
mediaHandler := handlers.NewMediaHandler(queries, libraryService, worker)
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
e := echo.New()
// Set up validator
v := validator.New()
// Register custom password complexity validator
if err := ratelimit.RegisterPasswordValidation(v); err != nil {
log.Fatal("Failed to register password validator:", err)
}
e.Validator = &CustomValidator{validator: v}
// Middleware
e.Use(echomiddleware.Logger())
e.Use(echomiddleware.Recover())
e.Use(echomiddleware.CORS())
e.Use(ratelimit.RequestTracingMiddleware(cfg))
// Rate limiter for auth endpoints
// rateLimiterConfig := ratelimit.RateLimiterConfig{
// Enabled: cfg.RateLimitEnabled,
// RequestsPerMinute: cfg.RequestsPerMinute,
// CleanupInterval: 5 * time.Minute,
// }
// rateLimiter := ratelimit.NewRateLimiter(rateLimiterConfig)
// rateLimitMiddleware := ratelimit.RateLimiterMiddleware(rateLimiter) // Now in router/auth.go
// ========================================================================
// ROUTER REGISTRATION - Migrate routes to internal/router/ package
// ========================================================================
routerConfig := &router.Config{
Echo: e,
Queries: queries,
Cfg: cfg,
DBPool: dbPool,
AuthHandler: authHandler,
LibraryHandler: libraryHandler,
DeviceHandler: deviceHandler,
MediaHandler: mediaHandler,
MatchingHandler: matchingHandler,
KOReaderHandler: koreaderHandler,
WSHandler: wsHandler,
ConflictHandler: conflictHandler,
AnalyticsHandler: analyticsHandler,
QueueHandler: queueHandler,
CollectionHandler: collectionHandler,
DashboardHandler: dashboardHandler,
DashboardService: dashboardService,
OPDSHandler: opdsHandler,
SystemSettingsHandler: systemSettingsHandler,
ConnManager: connManager,
QueueProcessor: queueProcessor,
DeviceAuthMiddleware: deviceAuthMiddleware,
LoginTracker: loginAttemptTracker,
}
// Register all routes and get ebook handler
ebookHandler := router.RegisterRoutes(routerConfig)
// ========================================================================
// APPLICATION LIFECYCLE MANAGEMENT
// ========================================================================
// Create app with lifecycle management
application := app.New(e, ebookHandler)
// ========================================================================
// START SERVER (managed by app lifecycle)
// ========================================================================
log.Printf("Starting server on port %s", cfg.ServerPort)
// Start HTTP server in background
go func() {
if err := e.Start(":" + cfg.ServerPort); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed to start: %v", err)
}
}()
// Start application (blocks until shutdown signal)
if err := application.Start(); err != nil {
log.Fatalf("Application error: %v", err)
}
}