Files
homarr/apps/workshop/main.go
T
+12 fcd7c979df feat!: release v2 (#6545)
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com>
Co-authored-by: arturict <arturict@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
Co-authored-by: Danura30082 <danura30082+github@gmail.com>
Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com>
Co-authored-by: ajnart <thomas@ajnart.fr>
Co-authored-by: ajnart <thomas@ajnart.dev>
Co-authored-by: chenjun <chenjun@foxmail.com>
Co-authored-by: Wikibear <wikibear@users.noreply.github.com>
Co-authored-by: Bergasha <bergasha@gmail.com>
Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com>
Co-authored-by: Lucas Araujo <lucas.lra@gmail.com>
Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com>
Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social>
Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com>
Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Co-authored-by: thrawnx <tight.bulb9384@fastmail.com>
Co-authored-by: Chris <58918656+nordada@users.noreply.github.com>
Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com>
Co-authored-by: Yanis <naamaneyanis@gmail.com>
Co-authored-by: Wikibear <me@sej.de>
Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
2026-10-02 21:00:14 +02:00

188 lines
5.9 KiB
Go

package main
import (
"compress/gzip"
"errors"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/plugins/jsvm"
"github.com/pocketbase/pocketbase/plugins/migratecmd"
"github.com/pocketbase/pocketbase/tools/hook"
"github.com/pocketbase/pocketbase/tools/osutils"
"github.com/pocketbase/pocketbase/tools/router"
)
func main() {
app := pocketbase.New()
var hooksDir string
app.RootCmd.PersistentFlags().StringVar(&hooksDir, "hooksDir", "", "the directory with the JS app hooks")
var hooksWatch bool
app.RootCmd.PersistentFlags().BoolVar(&hooksWatch, "hooksWatch", true, "auto restart on JS hook changes")
var hooksPool int
app.RootCmd.PersistentFlags().IntVar(&hooksPool, "hooksPool", 15, "the number of prewarmed JS runtimes")
var migrationsDir string
app.RootCmd.PersistentFlags().StringVar(&migrationsDir, "migrationsDir", "", "the directory with JS migrations")
var automigrate bool
app.RootCmd.PersistentFlags().BoolVar(&automigrate, "automigrate", true, "enable automatic migrations")
var publicDir string
app.RootCmd.PersistentFlags().StringVar(&publicDir, "publicDir", defaultPublicDir(), "the static files directory")
var indexFallback bool
app.RootCmd.PersistentFlags().BoolVar(&indexFallback, "indexFallback", false, "serve index.html for missing paths")
_ = app.RootCmd.ParseFlags(os.Args[1:])
jsvm.MustRegister(app, jsvm.Config{
MigrationsDir: migrationsDir,
HooksDir: hooksDir,
HooksWatch: hooksWatch,
HooksPoolSize: hooksPool,
})
migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{
TemplateLang: migratecmd.TemplateLangJS,
Automigrate: automigrate,
Dir: migrationsDir,
})
registerHomarrProvider(app)
app.OnServe().Bind(&hook.Handler[*core.ServeEvent]{
Func: func(event *core.ServeEvent) error {
if !event.Router.HasRoute(http.MethodGet, "/{path...}") {
event.Router.GET("/{path...}", staticWebsite(os.DirFS(publicDir), indexFallback)).Bind(
staticCompression(),
)
}
return event.Next()
},
Priority: 999,
})
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
func staticCompression() *hook.Handler[*core.RequestEvent] {
compression := apis.GzipWithConfig(apis.GzipConfig{Level: gzip.BestSpeed, MinLength: 1024})
compress := compression.Func
compression.Func = func(event *core.RequestEvent) error {
// A compressed 206 response no longer matches its Content-Range and cannot be cached as a video range.
if event.Request.Header.Get("Range") != "" || isCompressedAsset(event.Request.URL.Path) {
return event.Next()
}
// PocketBase's middleware matches "gzip" without checking its quality value.
// Respect clients that explicitly refuse it before delegating compression.
for _, encoding := range strings.Split(event.Request.Header.Get("Accept-Encoding"), ",") {
name, parameters, _ := strings.Cut(encoding, ";")
if !strings.EqualFold(strings.TrimSpace(name), "gzip") {
continue
}
for _, parameter := range strings.Split(parameters, ";") {
key, value, found := strings.Cut(parameter, "=")
if !found || !strings.EqualFold(strings.TrimSpace(key), "q") {
continue
}
quality, err := strconv.ParseFloat(strings.TrimSpace(value), 64)
if err != nil || quality <= 0 {
event.Response.Header().Add("Vary", "Accept-Encoding")
return event.Next()
}
}
}
return compress(event)
}
return compression
}
func isCompressedAsset(path string) bool {
switch strings.ToLower(filepath.Ext(path)) {
case ".avif", ".gif", ".ico", ".jpeg", ".jpg", ".mp4", ".png", ".webm", ".webp", ".woff", ".woff2":
return true
default:
return false
}
}
func staticWebsite(fsys fs.FS, indexFallback bool) func(*core.RequestEvent) error {
serve := apis.Static(fsys, indexFallback)
return func(event *core.RequestEvent) error {
event.Response.Header().Set("Cache-Control", staticCacheControl(event.Request.URL.Path))
if event.Request.URL.RawQuery != "" {
// PocketBase's canonical file/directory redirects omit the request query.
event.Response = &staticRedirectResponse{event.Response, event.Request.URL.RawQuery}
}
err := serve(event)
if err != nil {
event.Response.Header().Set("Cache-Control", "no-store")
}
if !errors.Is(err, router.ErrFileNotFound) || strings.HasPrefix(event.Request.URL.Path, "/api/") {
return err
}
page, readErr := fs.ReadFile(fsys, "404.html")
if readErr != nil {
return err
}
event.Response.Header().Set("Cache-Control", "no-store")
return event.HTML(http.StatusNotFound, string(page))
}
}
func staticCacheControl(path string) string {
if path == "/workshop-runtime-config.js" {
return "no-store"
}
for _, prefix := range []string{"/workshop", "/marketplace"} {
if path == prefix || strings.HasPrefix(path, prefix+"/") {
return "no-store"
}
}
if strings.HasPrefix(path, "/_next/static/") {
return "public, max-age=31536000, immutable"
}
for _, prefix := range []string{"/img/", "/media/", "/videos/", "/data/", "/custom-widgets/"} {
if strings.HasPrefix(path, prefix) {
return "public, max-age=600, s-maxage=86400"
}
}
switch strings.ToLower(filepath.Ext(path)) {
case ".avif", ".gif", ".ico", ".jpeg", ".jpg", ".mp4", ".png", ".svg", ".ttf", ".webm", ".webp", ".woff", ".woff2":
return "public, max-age=600, s-maxage=86400"
default:
return "public, max-age=60, s-maxage=300"
}
}
type staticRedirectResponse struct {
http.ResponseWriter
query string
}
func (response *staticRedirectResponse) WriteHeader(status int) {
if location := response.Header().Get("Location"); status == http.StatusMovedPermanently && location != "" {
response.Header().Set("Location", location+"?"+response.query)
}
response.ResponseWriter.WriteHeader(status)
}
func defaultPublicDir() string {
if osutils.IsProbablyGoRun() {
return "./pb_public"
}
return filepath.Join(os.Args[0], "../pb_public")
}