Files
kgpz_web/server/server.go
2025-01-08 15:53:44 +01:00

253 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package server
import (
"sync"
"time"
"github.com/Theodor-Springmann-Stiftung/kgpz_web/app"
"github.com/Theodor-Springmann-Stiftung/kgpz_web/controllers"
"github.com/Theodor-Springmann-Stiftung/kgpz_web/helpers/logging"
"github.com/Theodor-Springmann-Stiftung/kgpz_web/providers"
"github.com/Theodor-Springmann-Stiftung/kgpz_web/templating"
"github.com/Theodor-Springmann-Stiftung/kgpz_web/views"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cache"
"github.com/gofiber/fiber/v2/middleware/etag"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/storage/memory/v2"
)
const (
// INFO: This timeout is stupid. Uploads can take a long time, other routes might not. It's messy.
REQUEST_TIMEOUT = 16 * time.Second
SERVER_TIMEOUT = 16 * time.Second
// INFO: Maybe this is too long/short?
CACHE_TIME = 24 * time.Hour
CACHE_GC_INTERVAL = 120 * time.Second
)
const (
ASSETS_URL_PREFIX = "/assets"
EDITION_URL = "/edition/"
PRIVACY_URL = "/datenschutz/"
CONTACT_URL = "/kontakt/"
CITATION_URL = "/zitation/"
INDEX_URL = "/1764"
YEAR_OVERVIEW_URL = "/:year"
PLACE_OVERVIEW_URL = "/ort/:place"
AGENTS_OVERVIEW_URL = "/akteure/:letterorid"
CATEGORY_OVERVIEW_URL = "/kategorie/:category"
ISSSUE_URL = "/:year/:issue/:page?"
ADDITIONS_URL = "/:year/:issue/beilage/:page?"
)
const (
STATIC_FILEPATH = "./views/assets"
ROUTES_FILEPATH = "./views/routes"
LAYOUT_FILEPATH = "./views/layouts"
)
// INFO: Server is a meta-package that handles the current router, which it starts in a goroutine.
// The router must be able to restart itself, if the data validation fails, so we subscribe to a channel on the app,
// which indicates that the data has changed
// On data change:
// - we invalidate all caches if data is valid
// - we reload all clients
// - if data validity catastrophically fails, we restart the router to map error pages.
type Server struct {
Config *providers.ConfigProvider
running chan bool
shutdown *sync.WaitGroup
cache *memory.Storage
engine *templating.Engine
mu sync.Mutex
kgpz *app.KGPZ
}
func Create(k *app.KGPZ, c *providers.ConfigProvider, e *templating.Engine) *Server {
if c == nil || k == nil {
logging.Error(nil, "Error creating server: Config or App is posssibly nil.")
return nil
}
return &Server{
Config: c,
kgpz: k,
engine: e,
}
}
func (s *Server) Engine(e *templating.Engine) {
s.Stop()
s.mu.Lock()
s.engine = e
s.mu.Unlock()
s.Start()
}
// TODO: There is no error handler
func (s *Server) Start() {
s.mu.Lock()
defer s.mu.Unlock()
if s.cache == nil {
s.cache = memory.New(memory.Config{
GCInterval: CACHE_GC_INTERVAL,
})
}
srv := fiber.New(fiber.Config{
AppName: s.Config.Address,
CaseSensitive: false,
// INFO: This is a bit of an issue, since this treats /foo and /foo/ as different routes:
// Maybe we turn that behavior permanently off and differentiate HTMX from "normal" reuqests only by headers.
StrictRouting: true,
// EnablePrintRoutes: s.Config.Debug,
// TODO: Error handler, which sadly, is global:
ErrorHandler: fiber.DefaultErrorHandler,
// WARNING: The app must be run in a console, since this uses environment variables:
// It is not trivial to turn this on, since we need to mark goroutines that must be started only once.
// Prefork: true,
StreamRequestBody: false,
WriteTimeout: REQUEST_TIMEOUT,
ReadTimeout: REQUEST_TIMEOUT,
PassLocalsToViews: true,
Views: s.engine,
ViewsLayout: templating.DEFAULT_LAYOUT_NAME,
})
if s.Config.Debug {
srv.Use(logger.New())
}
srv.Use(recover.New())
srv.Use(ASSETS_URL_PREFIX, etag.New())
srv.Use(ASSETS_URL_PREFIX, static(&views.StaticFS))
// TODO: Dont cache static assets, bc storage gets huge on images.
// -> Maybe fiber does this already, automatically?
if s.Config.Debug {
srv.Use(cache.New(cache.Config{
Next: CacheFunc,
Expiration: CACHE_TIME,
CacheControl: false,
Storage: s.cache,
}))
} else {
srv.Use(cache.New(cache.Config{
Next: CacheFunc,
Expiration: CACHE_TIME,
CacheControl: true,
Storage: s.cache,
}))
}
srv.Get("/", func(c *fiber.Ctx) error {
c.Redirect(INDEX_URL)
return nil
})
srv.Get(PLACE_OVERVIEW_URL, controllers.GetPlace(s.kgpz))
srv.Get(CATEGORY_OVERVIEW_URL, controllers.GetCategory(s.kgpz))
srv.Get(AGENTS_OVERVIEW_URL, controllers.GetAgents(s.kgpz))
// TODO: YEAR_OVERVIEW_URL being /:year is a bad idea, since it captures basically everything,
// probably creating problems with static files, and also in case we add a front page later.
// That's why we redirect to /1764 on "/ " above and don´t use an optional /:year? paramter.
// -> Check SEO requirements on index pages that are 301 forwarded.
// This applies to all paths with two or three segments without a static prefix:
// Prob better to do /ausgabe/:year/:issue/:page? and /jahrgang/:year? respectively.
srv.Get(YEAR_OVERVIEW_URL, controllers.GetYear(s.kgpz))
srv.Get(ISSSUE_URL, controllers.GetIssue(s.kgpz))
srv.Get(ADDITIONS_URL, controllers.GetIssue(s.kgpz))
srv.Get(EDITION_URL, controllers.Get(EDITION_URL))
srv.Get(PRIVACY_URL, controllers.Get(PRIVACY_URL))
srv.Get(CONTACT_URL, controllers.Get(CONTACT_URL))
srv.Get(CITATION_URL, controllers.Get(CITATION_URL))
s.runner(srv)
}
func (s *Server) Stop() {
if s.running == nil {
return
}
s.running <- true
s.shutdown.Wait()
}
func (s *Server) Kill() {
if s.running == nil {
return
}
s.running <- false
s.shutdown.Wait()
}
func (s *Server) Restart() {
s.Stop()
s.Start()
}
func (s *Server) runner(srv *fiber.App) {
s.running = make(chan bool)
s.shutdown = &sync.WaitGroup{}
s.shutdown.Add(1)
cleanup := sync.WaitGroup{}
cleanup.Add(1)
go func() {
defer s.shutdown.Done()
if err := srv.Listen(s.Config.Address + ":" + s.Config.Port); err != nil {
logging.Error(err, "Error starting server")
return
}
cleanup.Wait()
}()
go func() {
defer cleanup.Done()
clean := <-s.running
logging.Info("Server shutdown requested")
if clean {
if err := srv.ShutdownWithTimeout(SERVER_TIMEOUT); err != nil {
logging.Error(err, "Error closing server cleanly. Shutting server down by force.")
clean = false
}
s.cache.Reset()
}
if !clean {
if err := srv.ShutdownWithTimeout(0); err != nil {
logging.Error(err, "Error closing server by force.")
}
}
}()
}