+12








fcd7c979df
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>
188 lines
5.9 KiB
Go
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")
|
|
}
|