Skip to content

Commit

Permalink
Adds etags for web static cached results (#136)
Browse files Browse the repository at this point in the history
* adding tests

* updates

* removing superfluous error checking

* refactor

* tweaks

* double clutch
  • Loading branch information
wcharczuk committed Nov 6, 2019
1 parent c946f8d commit 9b8efca
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 33 deletions.
2 changes: 1 addition & 1 deletion COVERAGE
Original file line number Diff line number Diff line change
@@ -1 +1 @@
67.84
67.90
6 changes: 6 additions & 0 deletions web/cached_static_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@ import (
"bytes"
"net/http"
"time"

"github.com/blend/go-sdk/webutil"
)

// CachedStaticFile is a memory mapped static file.
type CachedStaticFile struct {
Path string
Size int
ETag string
ModTime time.Time
Contents *bytes.Reader
}

// Render implements Result.
func (csf CachedStaticFile) Render(ctx *Ctx) error {
if csf.ETag != "" {
ctx.Response.Header().Set(webutil.HeaderETag, csf.ETag)
}
http.ServeContent(ctx.Response, ctx.Request, csf.Path, csf.ModTime, csf.Contents)
return nil
}
58 changes: 27 additions & 31 deletions web/static_file_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"os"
"regexp"
"sync"

"github.com/blend/go-sdk/webutil"
)

// NewStaticFileServer returns a new static file cache.
Expand Down Expand Up @@ -43,8 +45,9 @@ func OptStaticFileServerCacheDisabled(cacheDisabled bool) StaticFileserverOption
}

// StaticFileServer is a cache of static files.
// It can operate in cached mode, or with `CacheDisabled` it will read from
// disk for each request.
// It can operate in cached mode, or with `CacheDisabled` set to `true`
// it will read from disk for each request.
// In cached mode, it automatically adds etags for files it caches.
type StaticFileServer struct {
sync.RWMutex

Expand Down Expand Up @@ -110,28 +113,13 @@ func (sc *StaticFileServer) Action(r *Ctx) Result {
func (sc *StaticFileServer) ServeFile(r *Ctx, filePath string) Result {
f, err := sc.ResolveFile(filePath)
if err != nil {
if os.IsNotExist(err) {
if r.DefaultProvider != nil {
return r.DefaultProvider.NotFound()
}
http.NotFound(r.Response, r.Request)
return nil
}
if r.DefaultProvider != nil {
return r.DefaultProvider.InternalError(err)
}
http.Error(r.Response, err.Error(), http.StatusInternalServerError)
return nil
return sc.fileError(r, err)
}
defer f.Close()

finfo, err := f.Stat()
if err != nil {
if r.DefaultProvider != nil {
return r.DefaultProvider.InternalError(err)
}
http.Error(r.Response, err.Error(), http.StatusInternalServerError)
return nil
return sc.fileError(r, err)
}
http.ServeContent(r.Response, r.Request, filePath, finfo.ModTime(), f)
return nil
Expand All @@ -142,18 +130,10 @@ func (sc *StaticFileServer) ServeFile(r *Ctx, filePath string) Result {
func (sc *StaticFileServer) ServeCachedFile(r *Ctx, filepath string) Result {
file, err := sc.ResolveCachedFile(filepath)
if err != nil {
if os.IsNotExist(err) {
if r.DefaultProvider != nil {
return r.DefaultProvider.NotFound()
}
http.NotFound(r.Response, r.Request)
return nil
}
if r.DefaultProvider != nil {
return r.DefaultProvider.InternalError(err)
}
http.Error(r.Response, err.Error(), http.StatusInternalServerError)
return nil
return sc.fileError(r, err)
}
if file.ETag != "" {
r.Response.Header().Set(webutil.HeaderETag, file.ETag)
}
http.ServeContent(r.Response, r.Request, filepath, file.ModTime, file.Contents)
return nil
Expand Down Expand Up @@ -232,9 +212,25 @@ func (sc *StaticFileServer) ResolveCachedFile(filepath string) (*CachedStaticFil
Path: filepath,
Contents: bytes.NewReader(contents),
ModTime: finfo.ModTime(),
ETag: webutil.ETag(contents),
Size: len(contents),
}

sc.Cache[filepath] = file
return file, nil
}

func (sc *StaticFileServer) fileError(r *Ctx, err error) Result {
if os.IsNotExist(err) {
if r.DefaultProvider != nil {
return r.DefaultProvider.NotFound()
}
http.NotFound(r.Response, r.Request)
return nil
}
if r.DefaultProvider != nil {
return r.DefaultProvider.InternalError(err)
}
http.Error(r.Response, err.Error(), http.StatusInternalServerError)
return nil
}
22 changes: 21 additions & 1 deletion web/static_file_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ func TestStaticFileserverLiveNotFound(t *testing.T) {

cfs := NewStaticFileServer(
OptStaticFileServerSearchPaths(http.Dir("testdata")),
OptStaticFileServerCacheDisabled(true),
)
cfs.CacheDisabled = true
buffer := new(bytes.Buffer)
res := webutil.NewMockResponse(buffer)
req := webutil.NewMockRequest("GET", "/"+uuid.V4().String())
Expand All @@ -149,3 +149,23 @@ func TestStaticFileserverLiveNotFound(t *testing.T) {
assert.Equal(http.StatusNotFound, res.StatusCode())
assert.NotEmpty(buffer.Bytes())
}

func TestStaticFileserverAddsETag(t *testing.T) {
assert := assert.New(t)

cfs := NewStaticFileServer(
OptStaticFileServerSearchPaths(http.Dir("testdata")),
OptStaticFileServerCacheDisabled(false),
)
buffer := new(bytes.Buffer)
res := webutil.NewMockResponse(buffer)
req := webutil.NewMockRequest("GET", "/test_file.html")
result := cfs.Action(NewCtx(res, req, OptCtxRouteParams(RouteParameters{
RouteTokenFilepath: req.URL.Path,
})))

assert.Nil(result)
assert.Equal(http.StatusOK, res.StatusCode())
assert.NotEmpty(buffer.Bytes())
assert.NotEmpty(res.Header().Get(webutil.HeaderETag))
}
1 change: 1 addition & 0 deletions webutil/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var (
HeaderSetCookie = http.CanonicalHeaderKey("Set-Cookie")
HeaderCookie = http.CanonicalHeaderKey("Cookie")
HeaderDate = http.CanonicalHeaderKey("Date")
HeaderETag = http.CanonicalHeaderKey("etag")
HeaderCacheControl = http.CanonicalHeaderKey("Cache-Control")
HeaderConnection = http.CanonicalHeaderKey("Connection")
HeaderContentEncoding = http.CanonicalHeaderKey("Content-Encoding")
Expand Down
13 changes: 13 additions & 0 deletions webutil/etag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package webutil

import (
"crypto/md5"
"encoding/hex"
)

// ETag creates an etag for a given blob.
func ETag(contents []byte) string {
hash := md5.New()
_, _ = hash.Write(contents)
return hex.EncodeToString(hash.Sum(nil))
}
18 changes: 18 additions & 0 deletions webutil/etag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package webutil

import (
"testing"

"github.com/blend/go-sdk/assert"
)

func TestETag(t *testing.T) {
assert := assert.New(t)

etag := ETag([]byte("a quick brown fox jumps over the something cool"))
assert.Equal("4743a94a6030d34968f838c94cf4a6fd", etag)

etag = ETag([]byte("something else that is really cool"))
assert.Equal("a8c90c3202be46c1d766b2c63d38332b", etag)

}

0 comments on commit 9b8efca

Please sign in to comment.