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:
parent
5da5ee72a7
commit
a287830d1f
6
frontend/package-lock.json
generated
6
frontend/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
29
internal/api/cache_test.go
Normal file
29
internal/api/cache_test.go
Normal 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)
|
||||
})
|
||||
}
|
|
@ -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") != "" {
|
||||
|
|
|
@ -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())},
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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())},
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
16
internal/ttl/duration.go
Normal 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))
|
||||
}
|
25
internal/ttl/duration_test.go
Normal file
25
internal/ttl/duration_test.go
Normal 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
32
internal/ttl/maxage.go
Normal 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
|
||||
)
|
Loading…
Reference in a new issue