mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
Adds PrecompressedStatic handler in internal/server/static_precompressed.go, wired by routes_static.go for /static/*filepath (bundled) and /c/static/*filepath (custom). When a .zst or .gz sibling is on disk and the client's Accept-Encoding allows it, the handler streams the sibling via http.ServeContent — preserving Last-Modified and If-Modified-Since revalidation, deriving Content-Type from the identity filename so service-worker registration and SRI continue to work, and setting an explicit Content-Length that http.ServeContent omits when Content-Encoding is in effect. Range requests always serve identity (the byte offsets in Content-Range correspond to identity bytes), and PHOTOPRISM_HTTP_COMPRESSION=none disables encoded-sibling selection so the operator switch stays consistent with the runtime middleware. Vary: Accept-Encoding is emitted only when the response could realistically vary by encoding. Both /static/* and /c/static/* are added to NewShouldCompressFn's exclusion list so the runtime middleware never re-encodes an already-encoded body.
123 lines
3.8 KiB
Go
123 lines
3.8 KiB
Go
package server
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/photoprism/photoprism/internal/config"
|
|
"github.com/photoprism/photoprism/pkg/http/proxy"
|
|
)
|
|
|
|
// excludedExtensions lists file extensions whose responses should never be
|
|
// HTTP-compressed. These formats are already compressed or typically served
|
|
// as large binary payloads where gzip/zstd add CPU cost without saving bytes.
|
|
var excludedExtensions = map[string]struct{}{
|
|
".png": {},
|
|
".gif": {},
|
|
".jpeg": {},
|
|
".jpg": {},
|
|
".webp": {},
|
|
".mp3": {},
|
|
".mp4": {},
|
|
".zip": {},
|
|
".gz": {},
|
|
}
|
|
|
|
// NewShouldCompressFn returns an encoding-agnostic predicate that decides
|
|
// whether a given request is eligible for HTTP response compression. The
|
|
// predicate inspects the request URL path, file extension, and matched Gin
|
|
// route pattern; it deliberately does not look at Accept-Encoding or
|
|
// Connection headers (the middleware owns those checks). When conf is nil it
|
|
// returns a predicate that always declines.
|
|
func NewShouldCompressFn(conf *config.Config) func(c *gin.Context) bool {
|
|
if conf == nil {
|
|
return func(*gin.Context) bool { return false }
|
|
}
|
|
|
|
apiBase := conf.BaseUri(config.ApiUri)
|
|
|
|
// Raw path fallbacks for dynamic exclusions in case FullPath is unavailable.
|
|
sharePrefix := conf.BaseUri("/s/")
|
|
photoDlPrefix := apiBase + "/photos/"
|
|
clusterThemePath := apiBase + "/cluster/theme"
|
|
|
|
// FullPath patterns (exact match) for dynamic routes that should bypass compression.
|
|
excludedFullPaths := map[string]struct{}{
|
|
apiBase + "/photos/:uid/dl": {},
|
|
apiBase + "/cluster/theme": {},
|
|
conf.BaseUri("/s/:token/:shared/preview"): {},
|
|
}
|
|
|
|
// Path prefixes that should bypass compression (prefix match on raw URL path).
|
|
excludedPrefixes := []string{
|
|
// Health endpoints are small and frequently polled; compression would add overhead.
|
|
conf.BaseUri("/livez"),
|
|
conf.BaseUri("/health"),
|
|
conf.BaseUri("/readyz"),
|
|
conf.BaseUri(config.ApiUri + "/t"),
|
|
conf.BaseUri(config.ApiUri + "/folders/t"),
|
|
conf.BaseUri(config.ApiUri + "/dl"),
|
|
conf.BaseUri(config.ApiUri + "/zip"),
|
|
conf.BaseUri(config.ApiUri + "/albums"),
|
|
conf.BaseUri(config.ApiUri + "/labels"),
|
|
conf.BaseUri(config.ApiUri + "/videos"),
|
|
conf.BaseUri(proxy.PathPrefix),
|
|
// Bundled and custom static assets are served with precompressed
|
|
// .zst / .gz siblings via PrecompressedStatic; bypass the runtime
|
|
// encoder so it never re-encodes an already-encoded body and so
|
|
// PHOTOPRISM_HTTP_COMPRESSION=none consistently disables every
|
|
// encoded code path on these routes.
|
|
conf.BaseUri(config.StaticUri),
|
|
conf.BaseUri(config.CustomStaticUri),
|
|
}
|
|
|
|
return func(c *gin.Context) bool {
|
|
if c == nil || c.Request == nil {
|
|
return false
|
|
}
|
|
|
|
path := c.Request.URL.Path
|
|
if path == "" {
|
|
return false
|
|
}
|
|
|
|
// Exclude known already-compressed/binary extensions.
|
|
if ext := strings.ToLower(filepath.Ext(path)); ext != "" {
|
|
if _, ok := excludedExtensions[ext]; ok {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Exclude configured prefix groups.
|
|
for _, prefix := range excludedPrefixes {
|
|
if prefix != "" && strings.HasPrefix(path, prefix) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Exclude matched route patterns for dynamic endpoints.
|
|
if full := c.FullPath(); full != "" {
|
|
if _, ok := excludedFullPaths[full]; ok {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Fallback exclusions using raw path checks for robustness.
|
|
// Note: Keep the prefix guard here (not just HasSuffix), as the frontend SPA
|
|
// wildcard route may include paths ending in "/preview" (HTML) that should
|
|
// remain compressible (e.g., "/library/.../preview").
|
|
if path == clusterThemePath {
|
|
return false
|
|
}
|
|
if strings.HasPrefix(path, photoDlPrefix) && strings.HasSuffix(path, "/dl") {
|
|
return false
|
|
}
|
|
if strings.HasPrefix(path, sharePrefix) && strings.HasSuffix(path, "/preview") {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
}
|