server.go
⎇
Raw
1package server
2
3import (
4 "fmt"
5 "net/http"
6 "path/filepath"
7 "runtime"
8
9 "github.com/go-chi/chi/v5"
10 "github.com/go-chi/chi/v5/middleware"
11
12 "vidarchive/internal/config"
13 "vidarchive/internal/handler"
14 "vidarchive/internal/service"
15)
16
17type Server struct {
18 router *chi.Mux
19 handler *handler.Handler
20 cfg *config.Config
21}
22
23func New(cfg *config.Config, h *handler.Handler) *Server {
24 s := &Server{
25 router: chi.NewRouter(),
26 handler: h,
27 cfg: cfg,
28 }
29 s.setupRoutes()
30 return s
31}
32
33func (s *Server) setupRoutes() {
34 _, file, _, ok := runtime.Caller(0)
35 staticRoot := "web/static"
36 if ok {
37 root := filepath.Join(filepath.Dir(file), "..", "..")
38 staticRoot = filepath.Join(root, "web", "static")
39 }
40
41 s.router.Use(middleware.Logger)
42 s.router.Use(middleware.Recoverer)
43 s.router.Use(s.securityHeaders)
44 s.router.Use(itemCacheMiddleware)
45
46 s.router.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir(staticRoot))))
47
48 s.router.Get("/media/item/*", s.handler.ServeMediaItem)
49
50 s.router.Get("/", func(w http.ResponseWriter, r *http.Request) {
51 http.Redirect(w, r, "/library", http.StatusSeeOther)
52 })
53 s.router.Get("/library", s.handler.Library)
54 s.router.Get("/library/item/*", s.handler.LibraryItem)
55 s.router.Post("/library/item/*", s.handler.LibraryItem)
56
57 s.router.Get("/queue", s.handler.Downloads)
58 s.router.Get("/queue/{id}", s.handler.DownloadDetail)
59 s.router.Post("/queue/{id}/delete", s.handler.DeleteDownload)
60 s.router.Post("/queue/clear", s.handler.ClearAllDownloads)
61
62 s.router.Get("/download", s.handler.DownloadForm)
63 s.router.Post("/download", s.handler.CreateDownload)
64
65 s.router.Get("/settings", s.handler.Settings)
66 s.router.Post("/settings/presets", s.handler.CreatePreset)
67 s.router.Post("/settings/presets/{id}", s.handler.UpdatePreset)
68 s.router.Post("/settings/presets/{id}/delete", s.handler.DeletePreset)
69 s.router.Post("/settings", s.handler.UpdateSettings)
70
71 s.router.Post("/theme", s.handler.Theme)
72
73 s.router.Get("/api/presets/{id}/flags", s.handler.GetPresetFlags)
74}
75
76// itemCacheMiddleware attaches a fresh per-request library item cache so a
77// single request that looks up the same item multiple times only scans it once.
78func itemCacheMiddleware(next http.Handler) http.Handler {
79 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
80 next.ServeHTTP(w, r.WithContext(service.WithItemCache(r.Context())))
81 })
82}
83
84func (s *Server) securityHeaders(next http.Handler) http.Handler {
85 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
86 w.Header().Set("X-Content-Type-Options", "nosniff")
87 w.Header().Set("X-Frame-Options", "DENY")
88 w.Header().Set("X-XSS-Protection", "1; mode=block")
89 w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
90
91 if s.cfg.IsHTTPS() {
92 w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
93 w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; media-src 'self' blob:;")
94 }
95
96 next.ServeHTTP(w, r)
97 })
98}
99
100func (s *Server) Router() http.Handler {
101 return s.router
102}
103
104func (s *Server) Start() error {
105 addr := fmt.Sprintf(":%d", s.cfg.Port)
106 fmt.Printf("Starting server on %s\n", addr)
107 if s.cfg.BaseURL != "" {
108 fmt.Printf("Base URL: %s\n", s.cfg.BaseURL)
109 fmt.Printf("HTTPS mode: %v\n", s.cfg.IsHTTPS())
110 }
111 return http.ListenAndServe(addr, s.router)
112}
113