Videos: Allow setting a lower TTL for caching video content #3631

Adds the new "--http-video-maxage SECONDS" config option.

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2023-08-15 11:06:43 +02:00
parent 5da5ee72a7
commit a287830d1f
16 changed files with 193 additions and 46 deletions

View file

@ -5625,9 +5625,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.490",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.490.tgz",
"integrity": "sha512-6s7NVJz+sATdYnIwhdshx/N/9O6rvMxmhVoDSDFdj6iA45gHR8EQje70+RYsF4GeB+k0IeNSBnP7yG9ZXJFr7A=="
"version": "1.4.491",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.491.tgz",
"integrity": "sha512-ZzPqGKghdVzlQJ+qpfE+r6EB321zed7e5JsvHIlMM4zPFF8okXUkF5Of7h7F3l3cltPL0rG7YVmlp5Qro7RQLA=="
},
"node_modules/emoji-regex": {
"version": "8.0.0",

View file

@ -8,11 +8,9 @@ import (
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/internal/ttl"
)
// CoverMaxAge specifies the number of seconds to cache album covers.
var CoverMaxAge thumb.MaxAge = 3600 // 1 hour
type ThumbCache struct {
FileName string
ShareName string
@ -71,24 +69,41 @@ func FlushCoverCache() {
}
// 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()))
func AddCacheHeader(c *gin.Context, maxAge ttl.Duration, public bool) {
if c == nil {
return
} else if maxAge <= 0 {
c.Header("Cache-Control", "no-cache")
} else if public {
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%s", maxAge.String()))
} else {
c.Header("Cache-Control", fmt.Sprintf("private, max-age=%s, no-transform", maxAge.String()))
c.Header("Cache-Control", fmt.Sprintf("private, max-age=%s", maxAge.String()))
}
}
// AddCoverCacheHeader adds cover image cache control headers to the response.
func AddCoverCacheHeader(c *gin.Context) {
AddCacheHeader(c, CoverMaxAge, thumb.CachePublic)
AddCacheHeader(c, ttl.Cover, thumb.CachePublic)
}
// AddImmutableCacheHeader adds cache control headers to the response for immutable content like thumbnails.
func AddImmutableCacheHeader(c *gin.Context) {
if thumb.CachePublic {
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%s, no-transform, immutable", thumb.CacheMaxAge.String()))
if c == nil {
return
} else if thumb.CachePublic {
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%s, immutable", ttl.Default.String()))
} else {
c.Header("Cache-Control", fmt.Sprintf("private, max-age=%s, no-transform, immutable", thumb.CacheMaxAge.String()))
c.Header("Cache-Control", fmt.Sprintf("private, max-age=%s, immutable", ttl.Default.String()))
}
}
// AddVideoCacheHeader adds video cache control headers to the response.
func AddVideoCacheHeader(c *gin.Context, cdn bool) {
if c == nil {
return
} else if cdn || thumb.CachePublic {
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%s, immutable", ttl.Video.String()))
} else {
c.Header("Cache-Control", fmt.Sprintf("private, max-age=%s, immutable", ttl.Video.String()))
}
}

View file

@ -0,0 +1,29 @@
package api
import (
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestAddVideoCacheHeader(t *testing.T) {
t.Run("Public", func(t *testing.T) {
r := httptest.NewRecorder()
c, _ := gin.CreateTestContext(r)
AddVideoCacheHeader(c, true)
h := r.Header()
s := h["Cache-Control"][0]
assert.Equal(t, "public, max-age=21600, immutable", s)
})
t.Run("Private", func(t *testing.T) {
r := httptest.NewRecorder()
c, _ := gin.CreateTestContext(r)
AddVideoCacheHeader(c, false)
h := r.Header()
s := h["Cache-Control"][0]
assert.Equal(t, "private, max-age=21600, immutable", s)
})
}

View file

@ -14,7 +14,7 @@ import (
"github.com/photoprism/photoprism/pkg/video"
)
// GetVideo streams videos.
// GetVideo streams video content.
//
// GET /api/v1/videos/:hash/:token/:type
//
@ -111,7 +111,7 @@ func GetVideo(router *gin.RouterGroup) {
}
// Add HTTP cache header.
AddImmutableCacheHeader(c)
AddVideoCacheHeader(c, conf.CdnVideo())
// Return requested content.
if c.Query("download") != "" {

View file

@ -61,6 +61,7 @@ func startAction(ctx *cli.Context) error {
{"http-mode", conf.HttpMode()},
{"http-compression", conf.HttpCompression()},
{"http-cache-maxage", fmt.Sprintf("%d", conf.HttpCacheMaxAge())},
{"http-video-maxage", fmt.Sprintf("%d", conf.HttpVideoMaxAge())},
{"http-cache-public", fmt.Sprintf("%t", conf.HttpCachePublic())},
{"http-host", conf.HttpHost()},
{"http-port", fmt.Sprintf("%d", conf.HttpPort())},

View file

@ -33,6 +33,7 @@ import (
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/internal/ttl"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
@ -180,9 +181,12 @@ func (c *Config) Propagate() {
thumb.SizeUncached = c.ThumbSizeUncached()
thumb.Filter = c.ThumbFilter()
thumb.JpegQuality = c.JpegQuality()
thumb.CacheMaxAge = c.HttpCacheMaxAge()
thumb.CachePublic = c.HttpCachePublic()
// Set cache expiration defaults.
ttl.Default = c.HttpCacheMaxAge()
ttl.Video = c.HttpVideoMaxAge()
// Set geocoding parameters.
places.UserAgent = c.UserAgent()
entity.GeoApi = c.GeoApi()

View file

@ -6,7 +6,7 @@ import (
"strings"
"github.com/photoprism/photoprism/internal/server/header"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/internal/ttl"
"github.com/photoprism/photoprism/pkg/fs"
)
@ -84,13 +84,29 @@ func (c *Config) HttpCompression() string {
}
// HttpCacheMaxAge returns the time in seconds until cached content expires.
func (c *Config) HttpCacheMaxAge() thumb.MaxAge {
if c.options.HttpCacheMaxAge < 1 || c.options.HttpCacheMaxAge > 31536000 {
// Default to one month.
return thumb.CacheMaxAge
func (c *Config) HttpCacheMaxAge() ttl.Duration {
// Return default cache maxage?
if c.options.HttpCacheMaxAge < 1 {
return ttl.Default
} else if c.options.HttpCacheMaxAge > 31536000 {
return ttl.Duration(31536000)
}
return thumb.MaxAge(c.options.HttpCacheMaxAge)
// Return the configured cache expiration time.
return ttl.Duration(c.options.HttpCacheMaxAge)
}
// HttpVideoMaxAge returns the time in seconds until cached videos expire.
func (c *Config) HttpVideoMaxAge() ttl.Duration {
// Return default video maxage?
if c.options.HttpVideoMaxAge < 1 {
return ttl.Video
} else if c.options.HttpVideoMaxAge > 31536000 {
return ttl.Duration(31536000)
}
// Return the configured cache expiration time.
return ttl.Duration(c.options.HttpVideoMaxAge)
}
// HttpCachePublic checks whether static content may be cached by a CDN or caching proxy.

View file

@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/internal/ttl"
)
func TestConfig_HttpSocket(t *testing.T) {
@ -65,11 +65,25 @@ func TestConfig_HttpCompression(t *testing.T) {
func TestConfig_HttpCacheMaxAge(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, thumb.MaxAge(2592000), c.HttpCacheMaxAge())
assert.Equal(t, ttl.Duration(2592000), c.HttpCacheMaxAge())
c.Options().HttpCacheMaxAge = 23
assert.Equal(t, thumb.MaxAge(23), c.HttpCacheMaxAge())
assert.Equal(t, ttl.Duration(23), c.HttpCacheMaxAge())
c.Options().HttpCacheMaxAge = 41536000
assert.Equal(t, ttl.Limit, c.HttpCacheMaxAge())
c.Options().HttpCacheMaxAge = 0
assert.Equal(t, thumb.MaxAge(2592000), c.HttpCacheMaxAge())
assert.Equal(t, ttl.Duration(2592000), c.HttpCacheMaxAge())
}
func TestConfig_HttpVideoMaxAge(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, ttl.Video, c.HttpVideoMaxAge())
c.Options().HttpVideoMaxAge = 23
assert.Equal(t, ttl.Duration(23), c.HttpVideoMaxAge())
c.Options().HttpVideoMaxAge = 41536000
assert.Equal(t, ttl.Limit, c.HttpVideoMaxAge())
c.Options().HttpVideoMaxAge = 0
assert.Equal(t, ttl.Video, c.HttpVideoMaxAge())
}
func TestConfig_HttpCachePublic(t *testing.T) {

View file

@ -12,6 +12,7 @@ import (
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/server/header"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/internal/ttl"
"github.com/photoprism/photoprism/pkg/txt"
)
@ -492,10 +493,16 @@ var Flags = CliFlags{
}}, {
Flag: cli.IntFlag{
Name: "http-cache-maxage",
Value: int(thumb.CacheMaxAge),
Value: int(ttl.Default),
Usage: "time in `SECONDS` until cached content expires",
EnvVar: EnvVar("HTTP_CACHE_MAXAGE"),
}}, {
Flag: cli.IntFlag{
Name: "http-video-maxage",
Value: int(ttl.Video),
Usage: "time in `SECONDS` until cached videos expire",
EnvVar: EnvVar("HTTP_VIDEO_MAXAGE"),
}}, {
Flag: cli.BoolFlag{
Name: "http-cache-public",
Usage: "allow static content to be cached by a CDN or caching proxy",

View file

@ -114,6 +114,7 @@ type Options struct {
HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"`
HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"`
HttpCacheMaxAge int `yaml:"HttpCacheMaxAge" json:"HttpCacheMaxAge" flag:"http-cache-maxage"`
HttpVideoMaxAge int `yaml:"HttpVideoMaxAge" json:"HttpVideoMaxAge" flag:"http-video-maxage"`
HttpCachePublic bool `yaml:"HttpCachePublic" json:"HttpCachePublic" flag:"http-cache-public"`
HttpHost string `yaml:"HttpHost" json:"-" flag:"http-host"`
HttpPort int `yaml:"HttpPort" json:"-" flag:"http-port"`

View file

@ -162,6 +162,7 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"http-mode", c.HttpMode()},
{"http-compression", c.HttpCompression()},
{"http-cache-maxage", fmt.Sprintf("%d", c.HttpCacheMaxAge())},
{"http-video-maxage", fmt.Sprintf("%d", c.HttpVideoMaxAge())},
{"http-cache-public", fmt.Sprintf("%t", c.HttpCachePublic())},
{"http-host", c.HttpHost()},
{"http-port", fmt.Sprintf("%d", c.HttpPort())},

View file

@ -1,16 +1,5 @@
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 (
CacheMaxAge MaxAge = 2592000
CachePublic = false
CachePublic = false
)

View file

@ -6,11 +6,8 @@ import (
"github.com/stretchr/testify/assert"
)
func TestMaxAge_String(t *testing.T) {
t.Run("Hour", func(t *testing.T) {
assert.Equal(t, "3600", MaxAge(3600).String())
})
t.Run("Month", func(t *testing.T) {
assert.Equal(t, "2592000", MaxAge(2592000).String())
func TestCachePublic(t *testing.T) {
t.Run("Default", func(t *testing.T) {
assert.Equal(t, false, CachePublic)
})
}

16
internal/ttl/duration.go Normal file
View file

@ -0,0 +1,16 @@
package ttl
import "strconv"
// Duration represents a cache duration in seconds.
type Duration int
// Int returns the cache Duration in seconds as signed integer.
func (a Duration) Int() int {
return int(a)
}
// String returns the cache Duration in seconds as string.
func (a Duration) String() string {
return strconv.Itoa(int(a))
}

View file

@ -0,0 +1,25 @@
package ttl
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDuration_Int(t *testing.T) {
t.Run("Hour", func(t *testing.T) {
assert.Equal(t, 3600, Duration(3600).Int())
})
t.Run("Month", func(t *testing.T) {
assert.Equal(t, 2592000, Duration(2592000).Int())
})
}
func TestDuration_String(t *testing.T) {
t.Run("Hour", func(t *testing.T) {
assert.Equal(t, "3600", Duration(3600).String())
})
t.Run("Month", func(t *testing.T) {
assert.Equal(t, "2592000", Duration(2592000).String())
})
}

32
internal/ttl/maxage.go Normal file
View file

@ -0,0 +1,32 @@
/*
Package ttl provides cache expiration defaults and helper functions.
Copyright (c) 2018 - 2023 PhotoPrism UG. All rights reserved.
This program is free software: you can redistribute it and/or modify
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
<https://docs.photoprism.app/license/agpl>
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
The AGPL is supplemented by our Trademark and Brand Guidelines,
which describe how our Brand Assets may be used:
<https://www.photoprism.app/trademark>
Feel free to send an email to hello@photoprism.app if you have questions,
want to support our work, or just want to say hello.
Additional information can be found in our Developer Guide:
<https://docs.photoprism.app/developer-guide/>
*/
package ttl
var (
Limit Duration = 31536000 // 365 days
Default Duration = 2592000 // 30 days
Video Duration = 21600 // 6 hours
Cover Duration = 3600 // 1 hour
)