mirror of
https://github.com/willnorris/imageproxy.git
synced 2026-04-29 06:46:23 +02:00
allow overriding cache directives in responses
Add a new `-minCacheDuration` flag to specify a minimum duration to cache images for. Updates #28 Updates #144 Fixes #207 Fixes #208
This commit is contained in:
parent
c45e01c551
commit
cf0bc8469a
6 changed files with 181 additions and 4 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.cache
|
||||
imageproxy
|
||||
12
README.md
12
README.md
|
|
@ -184,6 +184,18 @@ first check an in-memory cache for an image, followed by a gcs bucket:
|
|||
|
||||
[tiered fashion]: https://godoc.org/github.com/die-net/lrucache/twotier
|
||||
|
||||
#### Cache Duration
|
||||
|
||||
By default, images are cached for the duration specified in response headers.
|
||||
If an image has no cache directives, or an explicit `Cache-Control: no-cache` header,
|
||||
then the response is not cached.
|
||||
|
||||
To override the response cache directives, set a minimum time that response should be cached for.
|
||||
This will ignore `no-cache` and `no-store` directives, and will set `max-age`
|
||||
to the specified value if it is greater than the original `max-age` value.
|
||||
|
||||
imageproxy -cache /tmp/imageproxy -minCacheDuration 5m
|
||||
|
||||
### Allowed Referrer List
|
||||
|
||||
You can limit images to only be accessible for certain hosts in the HTTP
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ var verbose = flag.Bool("verbose", false, "print verbose logging messages")
|
|||
var _ = flag.Bool("version", false, "Deprecated: this flag does nothing")
|
||||
var contentTypes = flag.String("contentTypes", "image/*", "comma separated list of allowed content types")
|
||||
var userAgent = flag.String("userAgent", "willnorris/imageproxy", "specify the user-agent used by imageproxy when fetching images from origin website")
|
||||
var minCacheDuration = flag.Duration("minCacheDuration", 0, "minimum duration to cache remote images")
|
||||
|
||||
func init() {
|
||||
flag.Var(&cache, "cache", "location to cache images (see https://github.com/willnorris/imageproxy#cache)")
|
||||
|
|
@ -87,6 +88,7 @@ func main() {
|
|||
p.ScaleUp = *scaleUp
|
||||
p.Verbose = *verbose
|
||||
p.UserAgent = *userAgent
|
||||
p.MinimumCacheDuration = *minCacheDuration
|
||||
|
||||
server := &http.Server{
|
||||
Addr: *addr,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import (
|
|||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
tphttp "willnorris.com/go/imageproxy/third_party/http"
|
||||
tphc "willnorris.com/go/imageproxy/third_party/httpcache"
|
||||
)
|
||||
|
||||
// Maximum number of redirection-followings allowed.
|
||||
|
|
@ -91,6 +92,10 @@ type Proxy struct {
|
|||
// PassRequestHeaders identifies HTTP headers to pass from inbound
|
||||
// requests to the proxied server.
|
||||
PassRequestHeaders []string
|
||||
|
||||
// MinimumCacheDuration is the minimum duration to cache remote images.
|
||||
// This will override cache-control instructions from the remote server.
|
||||
MinimumCacheDuration time.Duration
|
||||
}
|
||||
|
||||
// NewProxy constructs a new proxy. The provided http RoundTripper will be
|
||||
|
|
@ -118,6 +123,7 @@ func NewProxy(transport http.RoundTripper, cache Cache) *Proxy {
|
|||
proxy.logf(format, v...)
|
||||
}
|
||||
},
|
||||
updateCacheHeaders: proxy.updateCacheHeaders,
|
||||
},
|
||||
Cache: cache,
|
||||
MarkCachedResponses: true,
|
||||
|
|
@ -128,6 +134,39 @@ func NewProxy(transport http.RoundTripper, cache Cache) *Proxy {
|
|||
return proxy
|
||||
}
|
||||
|
||||
// updateCacheHeaders updates the cache-control headers in the provided headers.
|
||||
// It sets the cache-control max-age value to the maximum of the minimum cache
|
||||
// duration, the expires header, and the max-age header. It also removes the
|
||||
// expires header.
|
||||
func (p *Proxy) updateCacheHeaders(hdr http.Header) {
|
||||
if p.MinimumCacheDuration == 0 {
|
||||
return
|
||||
}
|
||||
cc := tphc.ParseCacheControl(hdr)
|
||||
|
||||
var expiresDuration time.Duration
|
||||
var maxAgeDuration time.Duration
|
||||
|
||||
if maxAge, ok := cc["max-age"]; ok {
|
||||
maxAgeDuration, _ = time.ParseDuration(maxAge + "s")
|
||||
}
|
||||
if date, err := httpcache.Date(hdr); err == nil {
|
||||
if expiresHeader := hdr.Get("Expires"); expiresHeader != "" {
|
||||
if expires, err := time.Parse(time.RFC1123, expiresHeader); err == nil {
|
||||
expiresDuration = expires.Sub(date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maxAge := max(p.MinimumCacheDuration, expiresDuration, maxAgeDuration)
|
||||
cc["max-age"] = fmt.Sprintf("%d", int(maxAge.Seconds()))
|
||||
delete(cc, "no-cache")
|
||||
delete(cc, "no-store")
|
||||
|
||||
hdr.Set("Cache-Control", cc.String())
|
||||
hdr.Del("Expires")
|
||||
}
|
||||
|
||||
// ServeHTTP handles incoming requests.
|
||||
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/favicon.ico" {
|
||||
|
|
@ -475,6 +514,8 @@ type TransformingTransport struct {
|
|||
CachingClient *http.Client
|
||||
|
||||
log func(format string, v ...any)
|
||||
|
||||
updateCacheHeaders func(hdr http.Header)
|
||||
}
|
||||
|
||||
// RoundTrip implements the http.RoundTripper interface.
|
||||
|
|
@ -484,7 +525,11 @@ func (t *TransformingTransport) RoundTrip(req *http.Request) (*http.Response, er
|
|||
if t.log != nil {
|
||||
t.log("fetching remote URL: %v", req.URL)
|
||||
}
|
||||
return t.Transport.RoundTrip(req)
|
||||
resp, err := t.Transport.RoundTrip(req)
|
||||
if err == nil && t.updateCacheHeaders != nil {
|
||||
t.updateCacheHeaders(resp.Header)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
f := req.URL.Fragment
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"image"
|
||||
"image/png"
|
||||
"log"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
|
@ -21,6 +22,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPeekContentType(t *testing.T) {
|
||||
|
|
@ -368,6 +370,108 @@ func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||
return http.ReadResponse(buf, req)
|
||||
}
|
||||
|
||||
func TestProxy_UpdateCacheHeaders(t *testing.T) {
|
||||
date := "Mon, 02 Jan 2006 15:04:05 MST"
|
||||
exp := "Mon, 02 Jan 2006 16:04:05 MST"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
minDuration time.Duration
|
||||
headers http.Header
|
||||
want http.Header
|
||||
}{
|
||||
{
|
||||
name: "zero",
|
||||
headers: http.Header{},
|
||||
want: http.Header{},
|
||||
},
|
||||
{
|
||||
name: "no min duration",
|
||||
headers: http.Header{
|
||||
"Date": {date},
|
||||
"Expires": {exp},
|
||||
"Cache-Control": {"max-age=600"},
|
||||
},
|
||||
want: http.Header{
|
||||
"Date": {date},
|
||||
"Expires": {exp},
|
||||
"Cache-Control": {"max-age=600"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cache control exceeds min duration",
|
||||
minDuration: 30 * time.Second,
|
||||
headers: http.Header{
|
||||
"Cache-Control": {"max-age=600"},
|
||||
},
|
||||
want: http.Header{
|
||||
"Cache-Control": {"max-age=600"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cache control exceeds min duration, expires",
|
||||
minDuration: 30 * time.Second,
|
||||
headers: http.Header{
|
||||
"Date": {date},
|
||||
"Expires": {exp},
|
||||
"Cache-Control": {"max-age=86400"},
|
||||
},
|
||||
want: http.Header{
|
||||
"Date": {date},
|
||||
"Cache-Control": {"max-age=86400"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "min duration exceeds cache control",
|
||||
minDuration: 1 * time.Hour,
|
||||
headers: http.Header{
|
||||
"Cache-Control": {"max-age=600"},
|
||||
},
|
||||
want: http.Header{
|
||||
"Cache-Control": {"max-age=3600"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "min duration exceeds cache control, expires",
|
||||
minDuration: 2 * time.Hour,
|
||||
headers: http.Header{
|
||||
"Date": {date},
|
||||
"Expires": {exp},
|
||||
"Cache-Control": {"max-age=600"},
|
||||
},
|
||||
want: http.Header{
|
||||
"Date": {date},
|
||||
"Cache-Control": {"max-age=7200"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "expires exceeds min duration, cache control",
|
||||
minDuration: 30 * time.Minute,
|
||||
headers: http.Header{
|
||||
"Date": {date},
|
||||
"Expires": {exp},
|
||||
"Cache-Control": {"max-age=600"},
|
||||
},
|
||||
want: http.Header{
|
||||
"Date": {date},
|
||||
"Cache-Control": {"max-age=3600"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &Proxy{MinimumCacheDuration: tt.minDuration}
|
||||
hdr := maps.Clone(tt.headers)
|
||||
p.updateCacheHeaders(hdr)
|
||||
|
||||
if !reflect.DeepEqual(hdr, tt.want) {
|
||||
t.Errorf("updateCacheHeaders(%v) returned %v, want %v", tt.headers, hdr, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxy_ServeHTTP(t *testing.T) {
|
||||
p := &Proxy{
|
||||
Client: &http.Client{
|
||||
|
|
|
|||
18
third_party/httpcache/httpcache.go
vendored
18
third_party/httpcache/httpcache.go
vendored
|
|
@ -5,10 +5,10 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
type cacheControl map[string]string
|
||||
type CacheControl map[string]string
|
||||
|
||||
func parseCacheControl(headers http.Header) cacheControl {
|
||||
cc := cacheControl{}
|
||||
func ParseCacheControl(headers http.Header) CacheControl {
|
||||
cc := CacheControl{}
|
||||
ccHeader := headers.Get("Cache-Control")
|
||||
for _, part := range strings.Split(ccHeader, ",") {
|
||||
part = strings.Trim(part, " ")
|
||||
|
|
@ -24,3 +24,15 @@ func parseCacheControl(headers http.Header) cacheControl {
|
|||
}
|
||||
return cc
|
||||
}
|
||||
|
||||
func (cc CacheControl) String() string {
|
||||
parts := make([]string, 0, len(cc))
|
||||
for k, v := range cc {
|
||||
if v == "" {
|
||||
parts = append(parts, k)
|
||||
} else {
|
||||
parts = append(parts, k+"="+v)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue