mirror of
https://github.com/willnorris/imageproxy.git
synced 2026-04-29 23:06:24 +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
|
[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
|
### Allowed Referrer List
|
||||||
|
|
||||||
You can limit images to only be accessible for certain hosts in the HTTP
|
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 _ = flag.Bool("version", false, "Deprecated: this flag does nothing")
|
||||||
var contentTypes = flag.String("contentTypes", "image/*", "comma separated list of allowed content types")
|
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 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() {
|
func init() {
|
||||||
flag.Var(&cache, "cache", "location to cache images (see https://github.com/willnorris/imageproxy#cache)")
|
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.ScaleUp = *scaleUp
|
||||||
p.Verbose = *verbose
|
p.Verbose = *verbose
|
||||||
p.UserAgent = *userAgent
|
p.UserAgent = *userAgent
|
||||||
|
p.MinimumCacheDuration = *minCacheDuration
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: *addr,
|
Addr: *addr,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import (
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
tphttp "willnorris.com/go/imageproxy/third_party/http"
|
tphttp "willnorris.com/go/imageproxy/third_party/http"
|
||||||
|
tphc "willnorris.com/go/imageproxy/third_party/httpcache"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Maximum number of redirection-followings allowed.
|
// Maximum number of redirection-followings allowed.
|
||||||
|
|
@ -91,6 +92,10 @@ type Proxy struct {
|
||||||
// PassRequestHeaders identifies HTTP headers to pass from inbound
|
// PassRequestHeaders identifies HTTP headers to pass from inbound
|
||||||
// requests to the proxied server.
|
// requests to the proxied server.
|
||||||
PassRequestHeaders []string
|
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
|
// 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...)
|
proxy.logf(format, v...)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateCacheHeaders: proxy.updateCacheHeaders,
|
||||||
},
|
},
|
||||||
Cache: cache,
|
Cache: cache,
|
||||||
MarkCachedResponses: true,
|
MarkCachedResponses: true,
|
||||||
|
|
@ -128,6 +134,39 @@ func NewProxy(transport http.RoundTripper, cache Cache) *Proxy {
|
||||||
return 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.
|
// ServeHTTP handles incoming requests.
|
||||||
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path == "/favicon.ico" {
|
if r.URL.Path == "/favicon.ico" {
|
||||||
|
|
@ -475,6 +514,8 @@ type TransformingTransport struct {
|
||||||
CachingClient *http.Client
|
CachingClient *http.Client
|
||||||
|
|
||||||
log func(format string, v ...any)
|
log func(format string, v ...any)
|
||||||
|
|
||||||
|
updateCacheHeaders func(hdr http.Header)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoundTrip implements the http.RoundTripper interface.
|
// RoundTrip implements the http.RoundTripper interface.
|
||||||
|
|
@ -484,7 +525,11 @@ func (t *TransformingTransport) RoundTrip(req *http.Request) (*http.Response, er
|
||||||
if t.log != nil {
|
if t.log != nil {
|
||||||
t.log("fetching remote URL: %v", req.URL)
|
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
|
f := req.URL.Fragment
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"image"
|
"image"
|
||||||
"image/png"
|
"image/png"
|
||||||
"log"
|
"log"
|
||||||
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
@ -21,6 +22,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPeekContentType(t *testing.T) {
|
func TestPeekContentType(t *testing.T) {
|
||||||
|
|
@ -368,6 +370,108 @@ func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
return http.ReadResponse(buf, req)
|
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) {
|
func TestProxy_ServeHTTP(t *testing.T) {
|
||||||
p := &Proxy{
|
p := &Proxy{
|
||||||
Client: &http.Client{
|
Client: &http.Client{
|
||||||
|
|
|
||||||
18
third_party/httpcache/httpcache.go
vendored
18
third_party/httpcache/httpcache.go
vendored
|
|
@ -5,10 +5,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cacheControl map[string]string
|
type CacheControl map[string]string
|
||||||
|
|
||||||
func parseCacheControl(headers http.Header) cacheControl {
|
func ParseCacheControl(headers http.Header) CacheControl {
|
||||||
cc := cacheControl{}
|
cc := CacheControl{}
|
||||||
ccHeader := headers.Get("Cache-Control")
|
ccHeader := headers.Get("Cache-Control")
|
||||||
for _, part := range strings.Split(ccHeader, ",") {
|
for _, part := range strings.Split(ccHeader, ",") {
|
||||||
part = strings.Trim(part, " ")
|
part = strings.Trim(part, " ")
|
||||||
|
|
@ -24,3 +24,15 @@ func parseCacheControl(headers http.Header) cacheControl {
|
||||||
}
|
}
|
||||||
return cc
|
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