From a287830d1f06ac090ac5ef7d6b1fd2385ba74acb Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Tue, 15 Aug 2023 11:06:43 +0200 Subject: [PATCH] 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 --- frontend/package-lock.json | 6 ++--- internal/api/cache.go | 37 +++++++++++++++++++-------- internal/api/cache_test.go | 29 +++++++++++++++++++++ internal/api/video.go | 4 +-- internal/commands/start.go | 1 + internal/config/config.go | 6 ++++- internal/config/config_server.go | 28 +++++++++++++++----- internal/config/config_server_test.go | 22 +++++++++++++--- internal/config/flags.go | 9 ++++++- internal/config/options.go | 1 + internal/config/report.go | 1 + internal/thumb/cache.go | 13 +--------- internal/thumb/cache_test.go | 9 +++---- internal/ttl/duration.go | 16 ++++++++++++ internal/ttl/duration_test.go | 25 ++++++++++++++++++ internal/ttl/maxage.go | 32 +++++++++++++++++++++++ 16 files changed, 193 insertions(+), 46 deletions(-) create mode 100644 internal/api/cache_test.go create mode 100644 internal/ttl/duration.go create mode 100644 internal/ttl/duration_test.go create mode 100644 internal/ttl/maxage.go diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a66b17a0e..c5f29496f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/internal/api/cache.go b/internal/api/cache.go index 389574f3f..0cdc01985 100644 --- a/internal/api/cache.go +++ b/internal/api/cache.go @@ -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())) } } diff --git a/internal/api/cache_test.go b/internal/api/cache_test.go new file mode 100644 index 000000000..fdc1b7055 --- /dev/null +++ b/internal/api/cache_test.go @@ -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) + }) +} diff --git a/internal/api/video.go b/internal/api/video.go index 103cdc9b0..0e235d633 100644 --- a/internal/api/video.go +++ b/internal/api/video.go @@ -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") != "" { diff --git a/internal/commands/start.go b/internal/commands/start.go index 963e221ac..a43271b64 100644 --- a/internal/commands/start.go +++ b/internal/commands/start.go @@ -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())}, diff --git a/internal/config/config.go b/internal/config/config.go index b83b14820..d50d09620 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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() diff --git a/internal/config/config_server.go b/internal/config/config_server.go index a67f05614..be16d5962 100644 --- a/internal/config/config_server.go +++ b/internal/config/config_server.go @@ -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. diff --git a/internal/config/config_server_test.go b/internal/config/config_server_test.go index 895ee8181..343b217bb 100644 --- a/internal/config/config_server_test.go +++ b/internal/config/config_server_test.go @@ -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) { diff --git a/internal/config/flags.go b/internal/config/flags.go index a15d6d833..70964dde2 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -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", diff --git a/internal/config/options.go b/internal/config/options.go index 76f9ff934..5596f7de0 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -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"` diff --git a/internal/config/report.go b/internal/config/report.go index a9b8acb52..e3f81bb16 100644 --- a/internal/config/report.go +++ b/internal/config/report.go @@ -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())}, diff --git a/internal/thumb/cache.go b/internal/thumb/cache.go index 932341327..e0b25939e 100644 --- a/internal/thumb/cache.go +++ b/internal/thumb/cache.go @@ -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 ) diff --git a/internal/thumb/cache_test.go b/internal/thumb/cache_test.go index d5953f9c5..dda1dd22f 100644 --- a/internal/thumb/cache_test.go +++ b/internal/thumb/cache_test.go @@ -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) }) } diff --git a/internal/ttl/duration.go b/internal/ttl/duration.go new file mode 100644 index 000000000..8d475d331 --- /dev/null +++ b/internal/ttl/duration.go @@ -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)) +} diff --git a/internal/ttl/duration_test.go b/internal/ttl/duration_test.go new file mode 100644 index 000000000..15af45de4 --- /dev/null +++ b/internal/ttl/duration_test.go @@ -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()) + }) +} diff --git a/internal/ttl/maxage.go b/internal/ttl/maxage.go new file mode 100644 index 000000000..b88c8b0a4 --- /dev/null +++ b/internal/ttl/maxage.go @@ -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"): + + + 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: + + +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: + +*/ +package ttl + +var ( + Limit Duration = 31536000 // 365 days + Default Duration = 2592000 // 30 days + Video Duration = 21600 // 6 hours + Cover Duration = 3600 // 1 hour +)