mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Config: Add options for HTTP cache control #3297
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -2,27 +2,16 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/get"
|
"github.com/photoprism/photoprism/internal/get"
|
||||||
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
"github.com/photoprism/photoprism/internal/thumb"
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MaxAge represents a cache TTL in seconds.
|
// CoverCacheTTL specifies the number of seconds to cache album covers.
|
||||||
type MaxAge int
|
var CoverCacheTTL thumb.MaxAge = 3600 // 1 hour
|
||||||
|
|
||||||
// String returns the cache TTL in seconds as string.
|
|
||||||
func (a MaxAge) String() string {
|
|
||||||
return strconv.Itoa(int(a))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default cache TTL times in seconds.
|
|
||||||
var (
|
|
||||||
CoverCacheTTL MaxAge = 3600 // 1 hour
|
|
||||||
ThumbCacheTTL MaxAge = 3600 * 24 * 90 // ~ 3 months
|
|
||||||
)
|
|
||||||
|
|
||||||
type ThumbCache struct {
|
type ThumbCache struct {
|
||||||
FileName string
|
FileName string
|
||||||
@@ -80,3 +69,26 @@ func FlushCoverCache() {
|
|||||||
|
|
||||||
log.Debugf("albums: flushed cover cache")
|
log.Debugf("albums: flushed cover cache")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddCacheHeader adds a cache control header to the response.
|
||||||
|
func AddCacheHeader(c *gin.Context, maxAge thumb.MaxAge, public bool) {
|
||||||
|
if public {
|
||||||
|
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%s, no-transform", maxAge.String()))
|
||||||
|
} else {
|
||||||
|
c.Header("Cache-Control", fmt.Sprintf("private, max-age=%s, no-transform", maxAge.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCoverCacheHeader adds cover image cache control headers to the response.
|
||||||
|
func AddCoverCacheHeader(c *gin.Context) {
|
||||||
|
AddCacheHeader(c, CoverCacheTTL, thumb.CachePublic)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddThumbCacheHeader adds thumbnail cache control headers to the response.
|
||||||
|
func AddThumbCacheHeader(c *gin.Context) {
|
||||||
|
if thumb.CachePublic {
|
||||||
|
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%s, no-transform, immutable", thumb.CacheTTL.String()))
|
||||||
|
} else {
|
||||||
|
c.Header("Cache-Control", fmt.Sprintf("private, max-age=%s, no-transform, immutable", thumb.CacheTTL.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,21 +14,6 @@ const (
|
|||||||
ContentTypeAvc = `video/mp4; codecs="avc1"`
|
ContentTypeAvc = `video/mp4; codecs="avc1"`
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddCacheHeader adds a cache control header to the response.
|
|
||||||
func AddCacheHeader(c *gin.Context, maxAge MaxAge) {
|
|
||||||
c.Header("Cache-Control", fmt.Sprintf("private, max-age=%s, no-transform", maxAge.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddCoverCacheHeader adds cover image cache control headers to the response.
|
|
||||||
func AddCoverCacheHeader(c *gin.Context) {
|
|
||||||
AddCacheHeader(c, CoverCacheTTL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddThumbCacheHeader adds thumbnail cache control headers to the response.
|
|
||||||
func AddThumbCacheHeader(c *gin.Context) {
|
|
||||||
c.Header("Cache-Control", fmt.Sprintf("private, max-age=%s, no-transform, immutable", ThumbCacheTTL.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddCountHeader adds the actual result count to the response.
|
// AddCountHeader adds the actual result count to the response.
|
||||||
func AddCountHeader(c *gin.Context, count int) {
|
func AddCountHeader(c *gin.Context, count int) {
|
||||||
c.Header("X-Count", strconv.Itoa(count))
|
c.Header("X-Count", strconv.Itoa(count))
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ func startAction(ctx *cli.Context) error {
|
|||||||
{"detach-server", fmt.Sprintf("%t", conf.DetachServer())},
|
{"detach-server", fmt.Sprintf("%t", conf.DetachServer())},
|
||||||
{"http-mode", conf.HttpMode()},
|
{"http-mode", conf.HttpMode()},
|
||||||
{"http-compression", conf.HttpCompression()},
|
{"http-compression", conf.HttpCompression()},
|
||||||
|
{"http-cache-ttl", fmt.Sprintf("%d", conf.HttpCacheTTL())},
|
||||||
|
{"http-cache-public", fmt.Sprintf("%t", conf.HttpCachePublic())},
|
||||||
{"http-host", conf.HttpHost()},
|
{"http-host", conf.HttpHost()},
|
||||||
{"http-port", fmt.Sprintf("%d", conf.HttpPort())},
|
{"http-port", fmt.Sprintf("%d", conf.HttpPort())},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ func (c *Config) Propagate() {
|
|||||||
thumb.SizeUncached = c.ThumbSizeUncached()
|
thumb.SizeUncached = c.ThumbSizeUncached()
|
||||||
thumb.Filter = c.ThumbFilter()
|
thumb.Filter = c.ThumbFilter()
|
||||||
thumb.JpegQuality = c.JpegQuality()
|
thumb.JpegQuality = c.JpegQuality()
|
||||||
|
thumb.CacheTTL = c.HttpCacheTTL()
|
||||||
|
thumb.CachePublic = c.HttpCachePublic()
|
||||||
|
|
||||||
// Set geocoding parameters.
|
// Set geocoding parameters.
|
||||||
places.UserAgent = c.UserAgent()
|
places.UserAgent = c.UserAgent()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/server/header"
|
"github.com/photoprism/photoprism/internal/server/header"
|
||||||
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -82,6 +83,25 @@ func (c *Config) HttpCompression() string {
|
|||||||
return strings.ToLower(strings.TrimSpace(c.options.HttpCompression))
|
return strings.ToLower(strings.TrimSpace(c.options.HttpCompression))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HttpCacheTTL returns the HTTP response cache time in seconds.
|
||||||
|
func (c *Config) HttpCacheTTL() thumb.MaxAge {
|
||||||
|
if c.options.HttpCacheTTL < 1 || c.options.HttpCacheTTL > 31536000 {
|
||||||
|
// Default to one month.
|
||||||
|
return 2630000
|
||||||
|
}
|
||||||
|
|
||||||
|
return thumb.MaxAge(c.options.HttpCacheTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HttpCachePublic checks whether HTTP responses may be cached publicly, e.g. by a CDN.
|
||||||
|
func (c *Config) HttpCachePublic() bool {
|
||||||
|
if c.options.HttpCachePublic {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.options.CdnUrl != ""
|
||||||
|
}
|
||||||
|
|
||||||
// HttpHost returns the built-in HTTP server host name or IP address (empty for all interfaces).
|
// HttpHost returns the built-in HTTP server host name or IP address (empty for all interfaces).
|
||||||
func (c *Config) HttpHost() string {
|
func (c *Config) HttpHost() string {
|
||||||
if c.options.HttpHost == "" {
|
if c.options.HttpHost == "" {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfig_HttpServerHost2(t *testing.T) {
|
func TestConfig_HttpServerHost2(t *testing.T) {
|
||||||
@@ -49,3 +51,27 @@ func TestConfig_HttpCompression(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, "", c.HttpCompression())
|
assert.Equal(t, "", c.HttpCompression())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfig_HttpCacheTTL(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
|
assert.Equal(t, thumb.MaxAge(2630000), c.HttpCacheTTL())
|
||||||
|
c.Options().HttpCacheTTL = 23
|
||||||
|
assert.Equal(t, thumb.MaxAge(23), c.HttpCacheTTL())
|
||||||
|
c.Options().HttpCacheTTL = 0
|
||||||
|
assert.Equal(t, thumb.MaxAge(2630000), c.HttpCacheTTL())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_HttpCachePublic(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
|
assert.False(t, c.HttpCachePublic())
|
||||||
|
c.Options().CdnUrl = "https://cdn.com/"
|
||||||
|
assert.True(t, c.HttpCachePublic())
|
||||||
|
c.Options().CdnUrl = ""
|
||||||
|
assert.False(t, c.HttpCachePublic())
|
||||||
|
c.Options().HttpCachePublic = true
|
||||||
|
assert.True(t, c.HttpCachePublic())
|
||||||
|
c.Options().HttpCachePublic = false
|
||||||
|
assert.False(t, c.HttpCachePublic())
|
||||||
|
}
|
||||||
|
|||||||
@@ -483,6 +483,17 @@ var Flags = CliFlags{
|
|||||||
Usage: "Web server compression `METHOD` (gzip, none)",
|
Usage: "Web server compression `METHOD` (gzip, none)",
|
||||||
EnvVar: EnvVar("HTTP_COMPRESSION"),
|
EnvVar: EnvVar("HTTP_COMPRESSION"),
|
||||||
}}, {
|
}}, {
|
||||||
|
Flag: cli.IntFlag{
|
||||||
|
Name: "http-cache-ttl",
|
||||||
|
Value: int(thumb.CacheTTL),
|
||||||
|
Usage: "number of `SECONDS` that a browser or CDN is allowed to cache HTTP responses",
|
||||||
|
EnvVar: EnvVar("HTTP_CACHE_TTL"),
|
||||||
|
}}, {
|
||||||
|
Flag: cli.BoolFlag{
|
||||||
|
Name: "http-cache-public",
|
||||||
|
Usage: "allow HTTP responses to be stored in a public cache, e.g. a CDN or caching proxy",
|
||||||
|
EnvVar: EnvVar("HTTP_CACHE_PUBLIC"),
|
||||||
|
}}, {
|
||||||
Flag: cli.StringFlag{
|
Flag: cli.StringFlag{
|
||||||
Name: "http-host, ip",
|
Name: "http-host, ip",
|
||||||
Usage: "Web server `IP` address",
|
Usage: "Web server `IP` address",
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ type Options struct {
|
|||||||
TLSKey string `yaml:"TLSKey" json:"TLSKey" flag:"tls-key"`
|
TLSKey string `yaml:"TLSKey" json:"TLSKey" flag:"tls-key"`
|
||||||
HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"`
|
HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"`
|
||||||
HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"`
|
HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"`
|
||||||
|
HttpCacheTTL int `yaml:"HttpCacheTTL" json:"HttpCacheTTL" flag:"http-cache-ttl"`
|
||||||
|
HttpCachePublic bool `yaml:"HttpCachePublic" json:"HttpCachePublic" flag:"http-cache-public"`
|
||||||
HttpHost string `yaml:"HttpHost" json:"-" flag:"http-host"`
|
HttpHost string `yaml:"HttpHost" json:"-" flag:"http-host"`
|
||||||
HttpPort int `yaml:"HttpPort" json:"-" flag:"http-port"`
|
HttpPort int `yaml:"HttpPort" json:"-" flag:"http-port"`
|
||||||
DatabaseDriver string `yaml:"DatabaseDriver" json:"-" flag:"database-driver"`
|
DatabaseDriver string `yaml:"DatabaseDriver" json:"-" flag:"database-driver"`
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||||||
{"tls-key", c.TLSKey()},
|
{"tls-key", c.TLSKey()},
|
||||||
{"http-mode", c.HttpMode()},
|
{"http-mode", c.HttpMode()},
|
||||||
{"http-compression", c.HttpCompression()},
|
{"http-compression", c.HttpCompression()},
|
||||||
|
{"http-cache-ttl", fmt.Sprintf("%d", c.HttpCacheTTL())},
|
||||||
|
{"http-cache-public", fmt.Sprintf("%t", c.HttpCachePublic())},
|
||||||
{"http-host", c.HttpHost()},
|
{"http-host", c.HttpHost()},
|
||||||
{"http-port", fmt.Sprintf("%d", c.HttpPort())},
|
{"http-port", fmt.Sprintf("%d", c.HttpPort())},
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import (
|
|||||||
gc "github.com/patrickmn/go-cache"
|
gc "github.com/patrickmn/go-cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cache = gc.New(time.Hour*4, 10*time.Minute)
|
var clientCache = gc.New(time.Hour*4, 10*time.Minute)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func FindLocation(id string) (result Location, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Location details cached?
|
// Location details cached?
|
||||||
if hit, ok := cache.Get(id); ok {
|
if hit, ok := clientCache.Get(id); ok {
|
||||||
log.Tracef("places: cache hit for lat %f, lng %f", lat, lng)
|
log.Tracef("places: cache hit for lat %f, lng %f", lat, lng)
|
||||||
cached := hit.(Location)
|
cached := hit.(Location)
|
||||||
cached.Cached = true
|
cached.Cached = true
|
||||||
@@ -106,7 +106,7 @@ func FindLocation(id string) (result Location, err error) {
|
|||||||
return result, fmt.Errorf("no result for %s", id)
|
return result, fmt.Errorf("no result for %s", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
cache.SetDefault(id, result)
|
clientCache.SetDefault(id, result)
|
||||||
log.Tracef("places: cached cell %s [%s]", clean.Log(id), time.Since(start))
|
log.Tracef("places: cached cell %s [%s]", clean.Log(id), time.Since(start))
|
||||||
|
|
||||||
result.Cached = false
|
result.Cached = false
|
||||||
|
|||||||
16
internal/thumb/cache.go
Normal file
16
internal/thumb/cache.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package thumb
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
// MaxAge represents a cache TTL in seconds.
|
||||||
|
type MaxAge int
|
||||||
|
|
||||||
|
// String returns the cache TTL in seconds as string.
|
||||||
|
func (a MaxAge) String() string {
|
||||||
|
return strconv.Itoa(int(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
CacheTTL MaxAge = 2630000
|
||||||
|
CachePublic = false
|
||||||
|
)
|
||||||
13
internal/thumb/cache_test.go
Normal file
13
internal/thumb/cache_test.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package thumb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMaxAge_String(t *testing.T) {
|
||||||
|
t.Run("Default", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "2630000", MaxAge(2630000).String())
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user