220 lines
5.4 KiB
Go
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
|
|
}
|