Diffstat (limited to 'internal/router/router.go')
| -rw-r--r-- | internal/router/router.go | 171 |
1 files changed, 136 insertions, 35 deletions
diff --git a/internal/router/router.go b/internal/router/router.go index c0556f0..65e35a4 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -2,16 +2,21 @@ package router import ( "database/sql" + "encoding/json" "errors" "fmt" + "html/template" "log/slog" + "net" "net/url" + "os" "slices" + "strconv" "strings" "time" "github.com/SayaAndy/saya-today-web/config" - "github.com/SayaAndy/saya-today-web/internal/b2" + "github.com/SayaAndy/saya-today-web/internal/blog" "github.com/SayaAndy/saya-today-web/internal/blogtrigger" "github.com/SayaAndy/saya-today-web/internal/factgiver" "github.com/SayaAndy/saya-today-web/internal/glightbox" @@ -21,7 +26,9 @@ import ( "github.com/SayaAndy/saya-today-web/locale" "github.com/dgraph-io/ristretto/v2" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/compress" "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/etag" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database/sqlite3" "github.com/yuin/goldmark" @@ -52,6 +59,19 @@ var ( Routes = make([]Route, 0) ) +type SitemapInfo struct { + Loc string + LastModified time.Time + ChangeFreq string + Priority float32 +} + +type MetaField struct { + Name string + Property string + Content string +} + type Route interface { Filter() (method string, path string) IsTemplated() bool @@ -59,7 +79,12 @@ type Route interface { CacheDuration() time.Duration ToValidateLang() LangSetting TemplatesToInject() []string + SitemapInfo(supplements *Supplements) []SitemapInfo + ContentType() string + RateLimiter() *fiber.Handler Render(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) + AddMeta(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (meta []MetaField, err error) + AddLinkedData(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (ld map[string]any, err error) RenderHeader(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) RenderBody(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) RenderFooter(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) @@ -69,7 +94,7 @@ type Route interface { type Supplements struct { DB *sql.DB - B2Client *b2.B2Client + BlogClient blog.Client Localization map[string]*locale.LocaleConfig AvailableLanguages []config.AvailableLanguageConfig ClientCache *ClientCache @@ -79,6 +104,9 @@ type Supplements struct { BlogTrigger *blogtrigger.BlogTriggerScheduler TemplateManager *templatemanager.TemplateManager MarkdownRenderer goldmark.Markdown + Meta config.MetaConfig + PhotoStorage config.PhotoStorageConfig + StaticStorage config.PhotoTypeConfig } type Router struct { @@ -86,6 +114,8 @@ type Router struct { app *fiber.App templatedRoutes map[string]map[string]Route templatedPathMatcher *PathMatcher + canonicalEndpoint string + endpoint config.EndpointConfig } func NewRouter(cfg *config.Config) (*Router, error) { @@ -117,7 +147,7 @@ func NewRouter(cfg *config.Config) (*Router, error) { } slog.Debug("successfully applied migrations") - supplements.B2Client, err = b2.NewB2Client(&cfg.BlogPages.Storage.Config) + supplements.BlogClient, err = blog.NewClientMap[cfg.BlogPages.Storage.Type](&cfg.BlogPages.Storage) if err != nil { return nil, fmt.Errorf("fail to initialize b2 client: %w", err) } @@ -133,7 +163,7 @@ func NewRouter(cfg *config.Config) (*Router, error) { supplements.MarkdownRenderer = goldmark.New( goldmark.WithExtensions( - glightbox.NewGLightboxExtension(), + glightbox.NewGLightboxExtension(cfg.PhotoStorage), tailwind.NewTailwindExtension(), ), goldmark.WithParserOptions( @@ -175,8 +205,8 @@ func NewRouter(cfg *config.Config) (*Router, error) { return nil, fmt.Errorf("fail to initialize mailer: %w", err) } - supplements.BlogTrigger, err = blogtrigger.NewBlogTriggerScheduler(supplements.B2Client, cfg.AvailableLanguages, cfg.Mail.Trigger.OnNewPost, - func(bp []*b2.BlogPage) error { + supplements.BlogTrigger, err = blogtrigger.NewBlogTriggerScheduler(supplements.BlogClient, cfg.AvailableLanguages, cfg.Mail.Trigger.OnNewPost, + func(bp []*blog.Page) error { for _, post := range bp { if err := supplements.Mailer.NewPost(post); err != nil { return err @@ -193,6 +223,10 @@ func NewRouter(cfg *config.Config) (*Router, error) { return nil, fmt.Errorf("fail to initialize template manager: %w", err) } + supplements.Meta = cfg.Meta + supplements.PhotoStorage = cfg.PhotoStorage + supplements.StaticStorage = cfg.StaticStorage + enablePrintRoutes := false if cfg.LogLevel <= slog.LevelDebug { enablePrintRoutes = true @@ -204,15 +238,29 @@ func NewRouter(cfg *config.Config) (*Router, error) { }) app.Use(cors.New(cors.Config{ - AllowOrigins: "https://f003.backblazeb2.com", + AllowOrigins: strings.Join(cfg.AllowOrigins, ","), AllowMethods: "GET,POST,OPTIONS", AllowHeaders: "Origin, Content-Type, Accept", })) + app.Use(compress.New(compress.Config{ + Level: compress.LevelBestSpeed, + })) + + app.Use(etag.New()) + + app.Use(func(c *fiber.Ctx) error { + path := c.Path() + if len(path) > 1 && path[len(path)-1] == '/' { + return c.Redirect(strings.TrimRight(path, "/"), fiber.StatusMovedPermanently) + } + return c.Next() + }) + templatedRoutes := make(map[string]map[string]Route) templatedPathMatcher := NewPathMatcher() - return &Router{supplements, app, templatedRoutes, templatedPathMatcher}, nil + return &Router{supplements, app, templatedRoutes, templatedPathMatcher, cfg.CanonicalEndpoint, cfg.Endpoint}, nil } func (r *Router) InitRoutes() (err error) { @@ -224,6 +272,10 @@ func (r *Router) InitRoutes() (err error) { return fmt.Errorf("failed to add '%s %s' route into template manager: %w", method, match, err) } + if rateLimiter := route.RateLimiter(); rateLimiter != nil { + r.app.Use(match, *rateLimiter) + } + if route.IsTemplated() { if _, ok := r.templatedRoutes[method]; !ok { r.templatedRoutes[method] = make(map[string]Route) @@ -257,13 +309,18 @@ func (r *Router) InitRoutes() (err error) { cacheKey = fmt.Sprintf("%s.full-page.%s", method, trimmedPath) case ByUrlAndQuery: cacheKey = fmt.Sprintf("%s.full-page.%s.%s", method, trimmedPath, queryString) + case Disabled: + c.Set("Cache-Control", "no-store, no-cache, must-revalidate") } defaultMap := fiber.Map{ - "L": r.supplements.Localization[lang], - "Lang": lang, - "Path": trimmedPath, - "QueryString": queryString, + "L": r.supplements.Localization[lang], + "Lang": lang, + "Path": trimmedPath, + "QueryString": queryString, + "CanonicalEndpoint": r.canonicalEndpoint, + "StaticStorageBaseUrl": r.supplements.StaticStorage.BaseUrl, + "ThumbnailBaseUrl": r.supplements.PhotoStorage.Thumbnail320p.BaseUrl, } statusCode, err := currentRoute.Render(c, r.supplements, lang, defaultMap) @@ -302,8 +359,8 @@ func (r *Router) InitRoutes() (err error) { go r.supplements.PageCache.SetWithTTL(cacheKey, content, int64(len(content)), route.CacheDuration()) } - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(statusCode).Type("html").Send(content) + c.Set(fiber.HeaderContentType, route.ContentType()) + return c.Status(statusCode).Send(content) }) } } @@ -316,6 +373,7 @@ func (r *Router) InitRoutes() (err error) { c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) return c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("unknown segment '%s'", part)) } + c.Set("Cache-Control", "no-store, no-cache, must-revalidate") err := r.generalPageSegment(c, part) return err }) @@ -326,15 +384,35 @@ func (r *Router) InitRoutes() (err error) { r.app.Static("/", "./static") - Routes = make([]Route, 0) + // Routes = make([]Route, 0) return nil } -func (r *Router) Listen(endpoint string) error { - if err := r.app.Listen(endpoint); err != nil { - return fmt.Errorf("error while running fiber server: %w", err) +func (r *Router) Listen() error { + switch r.endpoint.Type { + case "unix": + unixConfig := r.endpoint.Config.(*config.UnixConfig) + endpoint, _ := strings.CutPrefix(unixConfig.Path, "unix://") + ln, err := net.Listen("unix", endpoint) + if err != nil { + return fmt.Errorf("error while initializing unix listener: %w", err) + } + chmod, _ := strconv.ParseUint(unixConfig.Chmod[1:], 8, 32) + os.Chmod(unixConfig.Path, os.FileMode(chmod)) + if err := r.app.Listener(ln); err != nil { + return fmt.Errorf("error while running fiber server: %w", err) + } + case "http": + httpConfig := r.endpoint.Config.(*config.HttpConfig) + fmt.Print(httpConfig) + if err := r.app.Listen(httpConfig.ListenOn); err != nil { + return fmt.Errorf("error while running fiber server: %w", err) + } + default: + return fmt.Errorf("error with initializing fiber server: invalid endpoint type (supported are unix and http)") } + return nil } @@ -358,6 +436,7 @@ func (r *Router) Close() (err error) { } slog.Debug("closing page cache") r.supplements.PageCache.Close() + return errors.Join(allErrors...) } @@ -378,23 +457,42 @@ func (r *Router) generalPage(c *fiber.Ctx, route Route, lang string) error { } if val, ok := r.supplements.PageCache.Get(cacheKey); val != nil && ok { - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(fiber.StatusOK).Type("html").Send(val) + c.Set(fiber.HeaderContentType, route.ContentType()) + return c.Status(fiber.StatusOK).Send(val) } - content, err := r.supplements.TemplateManager.Render("general-page", fiber.Map{ - "L": r.supplements.Localization[lang], - "Lang": lang, - "QueryString": queryString, - }) + valueMap := fiber.Map{ + "L": r.supplements.Localization[lang], + "Lang": lang, + "QueryString": queryString, + "CanonicalEndpoint": r.canonicalEndpoint, + "StaticStorageBaseUrl": r.supplements.StaticStorage.BaseUrl, + "ThumbnailBaseUrl": r.supplements.PhotoStorage.Thumbnail320p.BaseUrl, + } + + var err error + if valueMap["Meta"], err = route.AddMeta(c, r.supplements, lang, valueMap); err != nil { + slog.Warn("failed to receive meta", slog.String("path", path), slog.String("error", err.Error())) + } + + var ld map[string]any + if ld, err = route.AddLinkedData(c, r.supplements, lang, valueMap); err != nil { + slog.Warn("failed to receive linked data", slog.String("path", path), slog.String("error", err.Error())) + } + if ld != nil { + ldBytes, _ := json.Marshal(ld) + valueMap["LinkedData"] = template.JS(ldBytes) + } + + content, err := r.supplements.TemplateManager.Render("general-page", valueMap) if err != nil { slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") } go r.supplements.PageCache.SetWithTTL(cacheKey, content, int64(len(content)), route.CacheDuration()) - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(fiber.StatusOK).Type("html").Send(content) + c.Set(fiber.HeaderContentType, route.ContentType()) + return c.Status(fiber.StatusOK).Send(content) } func (r *Router) generalPageSegment(c *fiber.Ctx, part string) error { @@ -439,17 +537,20 @@ func (r *Router) generalPageSegment(c *fiber.Ctx, part string) error { if route.ToCache() != Disabled { if val, ok := r.supplements.PageCache.Get(cacheKey); val != nil && ok { - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(fiber.StatusOK).Type("html").Send(val) + c.Set(fiber.HeaderContentType, route.ContentType()) + return c.Status(fiber.StatusOK).Send(val) } } var statusCode int defaultMap := fiber.Map{ - "L": r.supplements.Localization[lang], - "Lang": lang, - "Path": strings.Trim(path, "/"), - "QueryString": queryString, + "L": r.supplements.Localization[lang], + "Lang": lang, + "Path": strings.Trim(path, "/"), + "QueryString": queryString, + "CanonicalEndpoint": r.canonicalEndpoint, + "StaticStorageBaseUrl": r.supplements.StaticStorage.BaseUrl, + "ThumbnailBaseUrl": r.supplements.PhotoStorage.Thumbnail320p.BaseUrl, } switch part { @@ -497,8 +598,8 @@ func (r *Router) generalPageSegment(c *fiber.Ctx, part string) error { go r.supplements.PageCache.SetWithTTL(cacheKey, content, int64(len(content)), route.CacheDuration()) } - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(statusCode).Type("html").Send(content) + c.Set(fiber.HeaderContentType, route.ContentType()) + return c.Status(statusCode).Send(content) } func (r *Router) getAndValidateLang(c *fiber.Ctx, langSetting LangSetting, defaultLang ...string) (string, error) { |