NextNVR/server.go

108 lines
3 KiB
Go

// NextNVR v0.1.0 — HTTP server and routing
// Serves the embedded SPA and API endpoints.
package main
import (
"embed"
"encoding/json"
"io/fs"
"log"
"net/http"
"strings"
)
//go:embed public/*
var publicFiles embed.FS
// Server wraps the HTTP server and application state.
type Server struct {
config Config
http *http.Server
mux *http.ServeMux
appConfig *Config // mutable config reference for hot-reload
}
// NewServer creates and configures the HTTP server.
func NewServer(cfg Config) (*Server, error) {
s := &Server{
config: cfg,
appConfig: &cfg,
mux: http.NewServeMux(),
}
s.registerRoutes()
s.http = &http.Server{
Addr: cfg.Server.BindHost + cfg.Server.Port,
Handler: s.middleware(s.mux),
}
return s, nil
}
// ListenAndServe starts the HTTP server.
func (s *Server) ListenAndServe() error {
log.Printf("HTTP server listening on %s", s.http.Addr)
return s.http.ListenAndServe()
}
// Close shuts down the HTTP server.
func (s *Server) Close() {
if s.http != nil {
s.http.Close()
}
}
// registerRoutes sets up all API and static file routes.
func (s *Server) registerRoutes() {
// API endpoints.
s.mux.HandleFunc("/api/cameras", s.handleCameras)
s.mux.HandleFunc("/api/cameras/", s.handleCameraByID)
s.mux.HandleFunc("/api/config", s.handleConfig)
s.mux.HandleFunc("/api/scan", s.handleScan)
s.mux.HandleFunc("/api/status", s.handleStatus)
s.mux.HandleFunc("/api/recordings", s.handleRecordings)
// Live MJPEG stream proxy — proxied through NextNVR to avoid cross-origin issues.
s.mux.HandleFunc("/stream/", s.handleStream)
// Static file server for recordings (actual video files on disk).
s.mux.Handle("/recordings/", http.StripPrefix("/recordings/",
http.FileServer(http.Dir(s.config.Storage.RecordingsPath))))
// Static file server for embedded SPA.
publicFS, _ := fs.Sub(publicFiles, "public")
s.mux.Handle("/", http.FileServer(http.FS(publicFS)))
}
// middleware wraps handlers with CORS, no-cache, and logging.
func (s *Server) middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
// Prevent caching of static files during development.
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
// API logging.
if strings.HasPrefix(r.URL.Path, "/api/") {
log.Printf("%s %s", r.Method, r.URL.Path)
}
next.ServeHTTP(w, r)
})
}
// jsonResponse writes a JSON response with the given status code.
func jsonResponse(w http.ResponseWriter, status int, resp APIResponse) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(resp)
}