Config: Add options for HTTP cache control #3297

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2023-03-20 11:40:46 +01:00
parent ff3f9b8537
commit 286f06d894
13 changed files with 124 additions and 33 deletions

View File

@@ -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()))
}
}

View File

@@ -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))

View File

@@ -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())},
} }

View File

@@ -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()

View File

@@ -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 == "" {

View File

@@ -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())
}

View File

@@ -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",

View File

@@ -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"`

View File

@@ -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())},

View File

@@ -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)

View File

@@ -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
View 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
)

View 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())
})
}