From 95fdd8b79f327395c962581fd9ecc62607e09c4d Mon Sep 17 00:00:00 2001 From: Will Norris Date: Wed, 4 Dec 2013 02:55:56 -0800 Subject: [PATCH] add basic caching support includes two implementations, a no-op NopCache and an in-memory MemoryCache. --- cache/cache.go | 17 ++++++++++ cache/memory.go | 27 +++++++++++++++ cache/nop.go | 12 +++++++ data/data.go | 22 ++++++++++-- imageproxy.go | 12 ++++++- proxy/proxy.go | 89 ++++++++++++++++++++++++++++++++++++++++++++----- 6 files changed, 167 insertions(+), 12 deletions(-) create mode 100644 cache/cache.go create mode 100644 cache/memory.go create mode 100644 cache/nop.go diff --git a/cache/cache.go b/cache/cache.go new file mode 100644 index 0000000..525a2a4 --- /dev/null +++ b/cache/cache.go @@ -0,0 +1,17 @@ +// Package cache implements a image cache. +package cache + +import "github.com/willnorris/go-imageproxy/data" + +// Cache provides a cache for image metadata and transformed variants of the +// image. +type Cache interface { + // Get retrieves the cached Image for the provided image URL. + Get(string) (image *data.Image, ok bool) + + // Put caches the provided Image. + Save(*data.Image) + + // Delete deletes the cached Image and all variants for the image at the specified URL. + Delete(string) +} diff --git a/cache/memory.go b/cache/memory.go new file mode 100644 index 0000000..4fca29d --- /dev/null +++ b/cache/memory.go @@ -0,0 +1,27 @@ +package cache + +import "github.com/willnorris/go-imageproxy/data" + +// MemoryCache provides an in-memory Cache implementation. +type MemoryCache struct { + images map[string]*data.Image +} + +func NewMemoryCache() *MemoryCache { + return &MemoryCache{ + make(map[string]*data.Image), + } +} + +func (c MemoryCache) Get(u string) (*data.Image, bool) { + image, ok := c.images[u] + return image, ok +} + +func (c MemoryCache) Save(image *data.Image) { + c.images[image.URL] = image +} + +func (c MemoryCache) Delete(u string) { + delete(c.images, u) +} diff --git a/cache/nop.go b/cache/nop.go new file mode 100644 index 0000000..73e13cf --- /dev/null +++ b/cache/nop.go @@ -0,0 +1,12 @@ +package cache + +import "github.com/willnorris/go-imageproxy/data" + +// NopCache provides a no-op cache implementation that doesn't actually cache anything. +var NopCache = new(nopCache) + +type nopCache struct{} + +func (c nopCache) Get(u string) (*data.Image, bool) { return nil, false } +func (c nopCache) Save(image *data.Image) {} +func (c nopCache) Delete(u string) {} diff --git a/data/data.go b/data/data.go index a3b52f1..f9a8fe1 100644 --- a/data/data.go +++ b/data/data.go @@ -7,13 +7,14 @@ import ( "net/url" "strconv" "strings" + "time" ) // Transform specifies transformations that can be performed on a // requested image. type Transform struct { - Width int `json:"width"` // requested width, in pixels - Height int `json:"height"` // requested height, in pixels + Width int // requested width, in pixels + Height int // requested height, in pixels } func (o Transform) String() string { @@ -53,3 +54,20 @@ type Request struct { URL *url.URL // URL of the image to proxy Transform *Transform // Image transformation to perform } + +// Image represents a remote image that is being proxied. It tracks where +// the image was originally retrieved from and how long the image can be cached. +type Image struct { + // URL of original remote image. + URL string + + // Expires is the cache expiration time for the original image, as + // returned by the remote server. + Expires time.Time + + // Etag returned from server when fetching image. + Etag string + + // Bytes contains the actual image. + Bytes []byte +} diff --git a/imageproxy.go b/imageproxy.go index 39cbecd..3bfc262 100644 --- a/imageproxy.go +++ b/imageproxy.go @@ -1,16 +1,26 @@ package main import ( + "flag" + "fmt" "log" "net/http" + "github.com/willnorris/go-imageproxy/cache" "github.com/willnorris/go-imageproxy/proxy" ) +var port = flag.Int("port", 8080, "port to listen on") + func main() { + flag.Parse() + + fmt.Printf("go-imageproxy listening on port %d\n", *port) + p := proxy.NewProxy(nil) + p.Cache = cache.NewMemoryCache() server := &http.Server{ - Addr: ":8080", + Addr: fmt.Sprintf(":%d", *port), Handler: p, } err := server.ListenAndServe() diff --git a/proxy/proxy.go b/proxy/proxy.go index 1aace03..3c4cd5c 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -2,12 +2,17 @@ package proxy import ( + "errors" "fmt" - "io" + "io/ioutil" "net/http" "net/url" + "strconv" "strings" + "time" + "github.com/golang/glog" + "github.com/willnorris/go-imageproxy/cache" "github.com/willnorris/go-imageproxy/data" ) @@ -60,6 +65,7 @@ func NewRequest(r *http.Request) (*data.Request, error) { // Proxy serves image requests. type Proxy struct { Client *http.Client // client used to fetch remote URLs + Cache cache.Cache } // NewProxy constructs a new proxy. The provided http Client will be used to @@ -68,7 +74,7 @@ func NewProxy(client *http.Client) *Proxy { if client == nil { client = http.DefaultClient } - return &Proxy{Client: client} + return &Proxy{Client: client, Cache: cache.NopCache} } // ServeHTTP handles image requests. @@ -78,20 +84,85 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprintf("invalid request URL: %v", err.Error()), http.StatusBadRequest) return } - resp, err := p.Client.Get(req.URL.String()) + + u := req.URL.String() + glog.Infof("request for image: %v", u) + + image, ok := p.Cache.Get(u) + if !ok { + glog.Infof("image not cached") + image, err = p.fetchRemoteImage(u, nil) + if err != nil { + glog.Errorf("errorf fetching remote image: %v", err) + } + p.Cache.Save(image) + } else if time.Now().After(image.Expires) { + glog.Infof("cached image expired") + image, err = p.fetchRemoteImage(u, image) + if err != nil { + glog.Errorf("errorf fetching remote image: %v", err) + } + p.Cache.Save(image) + } else { + glog.Infof("serving from cache") + } + + w.Header().Add("Content-Length", strconv.Itoa(len(image.Bytes))) + w.Header().Add("Expires", image.Expires.Format(time.RFC1123)) + w.Write(image.Bytes) +} + +func (p *Proxy) fetchRemoteImage(u string, cached *data.Image) (*data.Image, error) { + glog.Infof("fetching remote image: %s", u) + + req, err := http.NewRequest("GET", u, nil) if err != nil { - http.Error(w, fmt.Sprintf("error fetching remote image: %v", err.Error()), http.StatusInternalServerError) - return + return nil, err + } + + if cached != nil && cached.Etag != "" { + req.Header.Add("If-None-Match", cached.Etag) + } + + resp, err := p.Client.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode == http.StatusNotModified { + glog.Infof("remote image not modified (304 response)") + cached.Expires = parseExpires(resp) + return cached, nil } if resp.StatusCode != http.StatusOK { - http.Error(w, fmt.Sprintf("error fetching remote image: %v", resp.Status), resp.StatusCode) - return + return nil, errors.New(fmt.Sprintf("HTTP status not OK: %v", resp.Status)) } defer resp.Body.Close() - _, err = io.Copy(w, resp.Body) + b, err := ioutil.ReadAll(resp.Body) if err != nil { - http.Error(w, fmt.Sprintf("error fetching remote image: %v", err.Error()), http.StatusInternalServerError) + return nil, err } + + return &data.Image{ + URL: u, + Expires: parseExpires(resp), + Etag: resp.Header.Get("Etag"), + Bytes: b, + }, nil +} + +func parseExpires(resp *http.Response) time.Time { + exp := resp.Header.Get("Expires") + if exp == "" { + return time.Now() + } + + t, err := time.Parse(time.RFC1123, exp) + if err != nil { + return time.Now() + } + + return t }