Files
chronological/internal/server/server.go
2025-12-10 11:41:18 +01:00

220 lines
5.4 KiB
Go

// Package server sets up and runs the HTTP server.
package server
import (
"context"
"embed"
"fmt"
"html/template"
"io/fs"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"luxick/chronological/internal/config"
"luxick/chronological/internal/handlers"
"luxick/chronological/internal/storage"
)
// TemplatesFS and StaticFS are set from main package where embed works
var (
TemplatesFS embed.FS
StaticFS embed.FS
)
// Server represents the HTTP server and its dependencies.
type Server struct {
config *config.Config
httpServer *http.Server
templates *template.Template
icsStore *storage.ICSStore
photoStore *storage.PhotoStore
caldavStore *storage.CalDAVStore
}
// New creates a new server with the given configuration.
func New(cfg *config.Config) (*Server, error) {
s := &Server{
config: cfg,
icsStore: storage.NewICSStore(cfg.Calendar.LocalICS, cfg.Diary.ICSFile),
photoStore: storage.NewPhotoStore(cfg.Photos.RootFolder),
}
// Initialize CalDAV store if enabled
if cfg.CalDAV.Enabled {
s.caldavStore = storage.NewCalDAVStore(cfg.CalDAV.URL, cfg.CalDAV.Username, cfg.CalDAV.Password)
}
// Parse templates
if err := s.parseTemplates(); err != nil {
return nil, fmt.Errorf("failed to parse templates: %w", err)
}
return s, nil
}
func (s *Server) parseTemplates() error {
funcMap := template.FuncMap{
"formatDate": formatDate,
"formatTime": formatTime,
"formatDateTime": formatDateTime,
"formatMonth": formatMonth,
"daysInMonth": daysInMonth,
"weekday": weekday,
"add": add,
"sub": sub,
"seq": seq,
"isToday": isToday,
"isSameMonth": isSameMonth,
}
// Get the templates subdirectory
templatesDir, err := fs.Sub(TemplatesFS, "templates")
if err != nil {
return fmt.Errorf("failed to get templates subdirectory: %w", err)
}
tmpl, err := template.New("").Funcs(funcMap).ParseFS(templatesDir,
"layouts/*.html",
"partials/*.html",
"pages/*.html",
)
if err != nil {
return fmt.Errorf("failed to parse templates: %w", err)
}
s.templates = tmpl
return nil
}
// Run starts the HTTP server and blocks until shutdown.
func (s *Server) Run() error {
// Connect to CalDAV if enabled
if s.caldavStore != nil {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := s.caldavStore.Connect(ctx); err != nil {
log.Printf("Warning: failed to connect to CalDAV server: %v", err)
}
}
// Create handlers
h := handlers.New(s.templates, s.icsStore, s.photoStore, s.caldavStore)
// Set up routes
mux := http.NewServeMux()
// Static files
staticDir, err := fs.Sub(StaticFS, "static")
if err != nil {
return fmt.Errorf("failed to get static subdirectory: %w", err)
}
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticDir))))
// Photo files (served from configured directory)
if s.config.Photos.RootFolder != "" {
mux.Handle("GET /photos/", http.StripPrefix("/photos/", http.FileServer(http.Dir(s.config.Photos.RootFolder))))
}
// Page routes
mux.HandleFunc("GET /", h.HandleIndex)
mux.HandleFunc("GET /calendar/{year}/{month}", h.HandleCalendar)
mux.HandleFunc("GET /timeline/{year}/{month}", h.HandleTimeline)
mux.HandleFunc("GET /day/{year}/{month}/{day}", h.HandleDay)
// Diary routes
mux.HandleFunc("GET /diary/{date}", h.HandleGetDiary)
mux.HandleFunc("POST /diary/{date}", h.HandleSaveDiary)
mux.HandleFunc("DELETE /diary/{date}", h.HandleDeleteDiary)
// Create HTTP server
s.httpServer = &http.Server{
Addr: fmt.Sprintf(":%d", s.config.Server.Port),
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Start server in goroutine
go func() {
log.Printf("Starting server on http://localhost:%d", s.config.Server.Port)
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
// Wait for shutdown signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := s.httpServer.Shutdown(ctx); err != nil {
return fmt.Errorf("server shutdown error: %w", err)
}
log.Println("Server stopped")
return nil
}
// Template helper functions
func formatDate(t time.Time) string {
return t.Format("2006-01-02")
}
func formatTime(t time.Time) string {
return t.Format("15:04")
}
func formatDateTime(t time.Time) string {
return t.Format("2006-01-02 15:04")
}
func formatMonth(t time.Time) string {
return t.Format("January 2006")
}
func daysInMonth(year int, month time.Month) int {
return time.Date(year, month+1, 0, 0, 0, 0, 0, time.Local).Day()
}
func weekday(year int, month time.Month, day int) int {
return int(time.Date(year, month, day, 0, 0, 0, 0, time.Local).Weekday())
}
func add(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
func seq(start, end int) []int {
s := make([]int, end-start+1)
for i := range s {
s[i] = start + i
}
return s
}
func isToday(year int, month time.Month, day int) bool {
now := time.Now()
return now.Year() == year && now.Month() == month && now.Day() == day
}
func isSameMonth(year int, month time.Month) bool {
now := time.Now()
return now.Year() == year && now.Month() == month
}