diff --git a/cache/content/constructor.go b/cache/content/constructor.go index 21e7f67..6abe193 100644 --- a/cache/content/constructor.go +++ b/cache/content/constructor.go @@ -5,6 +5,7 @@ import ( "github.com/foomo/neosproxy/cache/content/store" "github.com/foomo/neosproxy/client/cms" + "golang.org/x/sync/singleflight" ) // New will return a newly created content cache @@ -15,7 +16,8 @@ func New(cacheLifetime time.Duration, store store.CacheStore, loader cms.Content store: store, // invalidationChannel: make(chan InvalidationRequest), - lifetime: cacheLifetime, + invalidationRequestGroup: &singleflight.Group{}, + lifetime: cacheLifetime, } return c } diff --git a/cache/content/getter.go b/cache/content/getter.go index 35c092a..1d34645 100644 --- a/cache/content/getter.go +++ b/cache/content/getter.go @@ -26,3 +26,11 @@ func (c *Cache) Len() int { } return counter } + +func (c *Cache) GetAllEtags(workspace string) (etags map[string]string) { + return c.store.GetAllEtags(workspace) +} + +func (c *Cache) GetEtag(hash string) (etag string, e error) { + return c.store.GetEtag(hash) +} diff --git a/cache/content/invalidator.go b/cache/content/invalidator.go index e5f1178..1256f32 100644 --- a/cache/content/invalidator.go +++ b/cache/content/invalidator.go @@ -1,6 +1,7 @@ package content import ( + "strings" "time" "github.com/foomo/neosproxy/cache/content/store" @@ -11,8 +12,7 @@ func (c *Cache) RemoveAll() (err error) { return c.store.RemoveAll() } -// Invalidate cache item -func (c *Cache) Invalidate(id, dimension, workspace string) (item store.CacheItem, err error) { +func (c *Cache) invalidator(id, dimension, workspace string) (item store.CacheItem, err error) { // timer start := time.Now() @@ -40,7 +40,23 @@ func (c *Cache) Invalidate(id, dimension, workspace string) (item store.CacheIte Duration: time.Since(start), }) - // done + return +} + +// Invalidate cache item +func (c *Cache) Invalidate(id, dimension, workspace string) (item store.CacheItem, err error) { + + groupName := strings.Join([]string{"invalidate", id, dimension, workspace}, "-") + itemInterfaced, errThrottled, _ := c.invalidationRequestGroup.Do(groupName, func() (i interface{}, e error) { + return c.invalidator(id, dimension, workspace) + }) + + if errThrottled != nil { + err = errThrottled + return + } + + item = itemInterfaced.(store.CacheItem) return } diff --git a/cache/content/store/cache.go b/cache/content/store/cache.go index 211a41e..d8d4575 100644 --- a/cache/content/store/cache.go +++ b/cache/content/store/cache.go @@ -7,6 +7,9 @@ type CacheStore interface { Get(hash string) (item CacheItem, e error) GetAll() (item []CacheItem, e error) + GetEtag(hash string) (etag string, e error) + GetAllEtags(workspace string) (etags map[string]string) + Count() (int, error) Remove(hash string) (e error) diff --git a/cache/content/store/fs/filesystem.go b/cache/content/store/fs/filesystem.go index bc7d649..eea2c48 100644 --- a/cache/content/store/fs/filesystem.go +++ b/cache/content/store/fs/filesystem.go @@ -6,6 +6,7 @@ import ( "os" "strings" "sync" + "time" "github.com/foomo/neosproxy/cache/content" "github.com/foomo/neosproxy/cache/content/store" @@ -23,6 +24,9 @@ type fsCacheStore struct { lock sync.Mutex rw map[string]*sync.RWMutex l logging.Entry + + lockEtags sync.RWMutex + etags map[string]string } //------------------------------------------------------------------ @@ -38,14 +42,21 @@ func NewCacheStore(cacheDir string) store.CacheStore { l.WithError(err).Fatal("failed creating cache directory") } - s := &fsCacheStore{ + f := &fsCacheStore{ CacheDir: cacheDir, - lock: sync.Mutex{}, - rw: make(map[string]*sync.RWMutex), - l: l, + + l: l, + + lock: sync.Mutex{}, + rw: make(map[string]*sync.RWMutex), + + lockEtags: sync.RWMutex{}, + etags: make(map[string]string), } - return s + go f.initEtagCache() + + return f } //------------------------------------------------------------------ @@ -56,6 +67,11 @@ func (f *fsCacheStore) Upsert(item store.CacheItem) (e error) { // key key := f.getItemKey(item) + // validate etag + if item.Etag == "" { + item.Etag = item.GetEtag() + } + // serialize bytes, errMarshall := json.Marshal(item) if errMarshall != nil { @@ -64,10 +80,54 @@ func (f *fsCacheStore) Upsert(item store.CacheItem) (e error) { // lock cacheFile := f.Lock(key) - defer f.Unlock(key) // write to file - return ioutil.WriteFile(cacheFile, bytes, 0644) + errWrite := ioutil.WriteFile(cacheFile, bytes, 0644) + if errWrite != nil { + f.Unlock(key) + e = errWrite + return + } + f.Unlock(key) + + // update etag + f.upsertEtag(item.Hash, item.Etag) + + return nil +} + +func (f *fsCacheStore) GetAllEtags(workspace string) (etags map[string]string) { + f.lockEtags.RLock() + etags = make(map[string]string) + for hash, etag := range f.etags { + if !strings.HasPrefix(hash, workspace) { + continue + } + etags[hash] = etag + } + f.lockEtags.RUnlock() + return +} + +func (f *fsCacheStore) GetEtag(hash string) (etag string, e error) { + f.lockEtags.RLock() + if value, ok := f.etags[hash]; ok { + etag = value + f.lockEtags.RUnlock() + return + } + f.lockEtags.RUnlock() + + item, errGet := f.Get(hash) + if errGet != nil { + e = errGet + return + } + + etag = item.GetEtag() + f.upsertEtag(hash, etag) + + return } func (f *fsCacheStore) Get(hash string) (item store.CacheItem, e error) { @@ -146,7 +206,17 @@ func (f *fsCacheStore) Remove(hash string) (e error) { cacheFile := f.Lock(key) defer f.Unlock(key) - return os.Remove(cacheFile) + errRemove := os.Remove(cacheFile) + if errRemove != nil { + e = errRemove + return + } + + f.lockEtags.Lock() + delete(f.etags, hash) + f.lockEtags.Unlock() + + return nil } func (f *fsCacheStore) createCacheDir() error { @@ -163,6 +233,10 @@ func (f *fsCacheStore) RemoveAll() (e error) { return errRemoveAll } + f.lockEtags.Lock() + f.etags = make(map[string]string) + f.lockEtags.Unlock() + errCreateCache := f.createCacheDir() if errCreateCache != nil { f.l.WithError(errCreateCache).Error("unable to re-create cache directory") @@ -176,6 +250,44 @@ func (f *fsCacheStore) RemoveAll() (e error) { // ~ PRIVATE METHODS //------------------------------------------------------------------ +func (f *fsCacheStore) initEtagCache() { + start := time.Now() + l := f.l.WithField(logging.FieldFunction, "initEtagCache") + files, errReadDir := ioutil.ReadDir(f.CacheDir) + if errReadDir != nil { + l.WithError(errReadDir).Error("failed reading cache dir") + return + } + + l.Debug("init etag cache") + counter := 0 + for _, file := range files { + if !file.IsDir() { + filename := file.Name() + index := strings.Index(filename, ".") + if index >= 0 { + filename = filename[0:index] + } + item, errGet := f.Get(filename) + if errGet != nil { + l.WithError(errGet).Warn("could not load cache item") + continue + } + counter++ + f.upsertEtag(item.Hash, item.GetEtag()) + } + } + l.WithField("len", counter).WithDuration(start).Debug("etag cache initialized") + + return +} + +func (f *fsCacheStore) upsertEtag(hash, etag string) { + f.lockEtags.Lock() + f.etags[hash] = etag + f.lockEtags.Unlock() +} + func (f *fsCacheStore) getItemKey(item store.CacheItem) string { return f.getKey(item.Hash) } diff --git a/cache/content/store/vo.go b/cache/content/store/vo.go index 6bdd59f..24367b5 100644 --- a/cache/content/store/vo.go +++ b/cache/content/store/vo.go @@ -1,6 +1,8 @@ package store import ( + "crypto/sha256" + "encoding/hex" "strings" "time" ) @@ -19,6 +21,7 @@ type CacheItem struct { validUntil time.Time HTML string + Etag string // hashed fingerprint of html content } // NewCacheItem will create a new cache item @@ -31,10 +34,25 @@ func NewCacheItem(id string, dimension string, workspace string, html string, va created: time.Now(), validUntil: validUntil, HTML: html, + Etag: generateFingerprint(html), } } +// GetEtag returns an etag +func (item *CacheItem) GetEtag() string { + if item.Etag != "" { + return item.Etag + } + return generateFingerprint(item.HTML) +} + // GetHash will return a cache item hash func GetHash(id, dimension, workspace string) string { return strings.Join([]string{workspace, dimension, id}, "_") } + +func generateFingerprint(data string) string { + sha := sha256.New() + sha.Write([]byte(data)) + return hex.EncodeToString(sha.Sum(nil)) +} diff --git a/cache/content/vo.go b/cache/content/vo.go index 6b2ee57..bac5b5d 100644 --- a/cache/content/vo.go +++ b/cache/content/vo.go @@ -5,14 +5,16 @@ import ( "github.com/foomo/neosproxy/cache/content/store" "github.com/foomo/neosproxy/client/cms" + "golang.org/x/sync/singleflight" ) // Cache workspace items type Cache struct { - observer Observer - loader cms.ContentLoader - store store.CacheStore - invalidationChannel chan InvalidationRequest + observer Observer + loader cms.ContentLoader + store store.CacheStore + invalidationChannel chan InvalidationRequest + invalidationRequestGroup *singleflight.Group lifetime time.Duration // time until an item must be re-invalidated (< 0 === never) } diff --git a/cache/invalidator.go b/cache/invalidator.go index 6e744d0..89c5d5f 100644 --- a/cache/invalidator.go +++ b/cache/invalidator.go @@ -5,9 +5,9 @@ import ( "os" "time" - "github.com/Sirupsen/logrus" "github.com/cloudfoundry/bytefmt" "github.com/foomo/neosproxy/logging" + "github.com/sirupsen/logrus" ) // Invalidate cache maybe invalidates cache, but skips requests if invalidation queue is already full diff --git a/cmd/neosproxy/main.go b/cmd/neosproxy/main.go index f4fdf73..d4ab4ad 100644 --- a/cmd/neosproxy/main.go +++ b/cmd/neosproxy/main.go @@ -5,7 +5,7 @@ import ( "path/filepath" "time" - "github.com/Sirupsen/logrus" + "github.com/sirupsen/logrus" "github.com/foomo/neosproxy/cache/content/store/fs" "github.com/foomo/neosproxy/client/cms" "github.com/foomo/neosproxy/config" diff --git a/glide.lock b/glide.lock deleted file mode 100644 index f641ea4..0000000 --- a/glide.lock +++ /dev/null @@ -1,54 +0,0 @@ -hash: c8774276cbc27688b992f05c04cf2e330655e27c5a22bc9b3cad5dae8fd66b60 -updated: 2018-12-24T02:41:18.010901+01:00 -imports: -- name: github.com/auth0/go-jwt-middleware - version: 5493cabe49f7bfa6e2ec444a09d334d90cd4e2bd -- name: github.com/cloudfoundry/bytefmt - version: 2aa6f33b730c79971cfc3c742f279195b0abc627 -- name: github.com/dgrijalva/jwt-go - version: 06ea1031745cb8b3dab3f6a236daf2b0aa468b7e -- name: github.com/foomo/shop - version: e55de455dd26d50c2033008174da3bff0125e218 - subpackages: - - persistence -- name: github.com/gorilla/context - version: 51ce91d2eaddeca0ef29a71d766bb3634dadf729 -- name: github.com/gorilla/mux - version: e3702bed27f0d39777b0b37b664b6280e8ef8fbf -- name: github.com/konsorten/go-windows-terminal-sequences - version: 5c8c8bd35d3832f5d134ae1e1e375b69a4d25242 -- name: github.com/pkg/errors - version: 059132a15dd08d6704c67711dae0cf35ab991756 -- name: github.com/Sirupsen/logrus - version: bcd833dfe83d3cebad139e4a29ed79cb2318bf95 -- name: github.com/stretchr/testify - version: f35b8ab0b5a2cef36673838d662e249dd9c94686 - subpackages: - - assert -- name: golang.org/x/crypto - version: 81e90905daefcd6fd217b62423c0908922eadb30 - subpackages: - - ssh/terminal -- name: golang.org/x/sys - version: b05ddf57801d2239d6ab0ee35f9d981e0420f4ac - subpackages: - - unix - - windows -- name: gopkg.in/mgo.v2 - version: 9856a29383ce1c59f308dd1cf0363a79b5bef6b5 - subpackages: - - bson - - internal/json - - internal/sasl - - internal/scram -- name: gopkg.in/yaml.v2 - version: 51d6538a90f86fe93ac480b35f37b2be17fef232 -testImports: -- name: github.com/davecgh/go-spew - version: d8f796af33cc11cb798c1aaeb27a4ebc5099927d - subpackages: - - spew -- name: github.com/pmezard/go-difflib - version: 792786c7400a136282c1664665ae0a8db921c6c2 - subpackages: - - difflib diff --git a/glide.yaml b/glide.yaml deleted file mode 100644 index ffe136b..0000000 --- a/glide.yaml +++ /dev/null @@ -1,22 +0,0 @@ -package: github.com/foomo/neosproxy -import: -- package: github.com/Sirupsen/logrus - version: ~1.2.0 -- package: gopkg.in/yaml.v2 - version: ~2.2.2 -- package: golang.org/x/crypto - subpackages: - - ssh/terminal -- package: golang.org/x/sys - subpackages: - - unix -- package: github.com/stretchr/testify - version: ~1.2.2 - subpackages: - - assert -- package: github.com/gorilla/mux - version: ~1.6.2 -- package: github.com/auth0/go-jwt-middleware -- package: github.com/dgrijalva/jwt-go - version: ~3.2.0 -- package: github.com/cloudfoundry/bytefmt diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ac61d94 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module github.com/foomo/neosproxy + +go 1.12 + +require ( + github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7 + github.com/cloudfoundry/bytefmt v0.0.0-20180906201452-2aa6f33b730c + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/foomo/shop v0.0.0-20190306093145-644b0b683ba1 + github.com/gorilla/mux v1.6.2 + github.com/pkg/errors v0.0.0-20181023235946-059132a15dd0 + github.com/sirupsen/logrus v1.2.0 + github.com/stretchr/testify v1.2.2 + golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc + golang.org/x/sync v0.0.0-20170517211232-f52d1811a629 + golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 + gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b822b8d --- /dev/null +++ b/go.sum @@ -0,0 +1,30 @@ +github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7 h1:irR1cO6eek3n5uquIVaRAsQmZnlsfPuHNz31cXo4eyk= +github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7/go.mod h1:LWMyo4iOLWXHGdBki7NIht1kHru/0wM179h+d3g8ATM= +github.com/cloudfoundry/bytefmt v0.0.0-20180906201452-2aa6f33b730c h1:zE9z4EZZwJTjOi9Q9WYM/81BuwOKyjhHagiNUDhDdnI= +github.com/cloudfoundry/bytefmt v0.0.0-20180906201452-2aa6f33b730c/go.mod h1:4oo6ExqTPaBVBwSm814h6UO5Fels1kN2KvpNscaCcS0= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/foomo/shop v0.0.0-20190306093145-644b0b683ba1/go.mod h1:+Y2nUdyvktarq9+F2B0tvk/BM7y+jwNJI1CW/KgM7as= +github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/pkg/errors v0.0.0-20181023235946-059132a15dd0 h1:R+lX9nKwNd1n7UE5SQAyoorREvRn3aLF6ZndXBoIWqY= +github.com/pkg/errors v0.0.0-20181023235946-059132a15dd0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc h1:F5tKCVGp+MUAHhKp5MZtGqAlGX3+oCsiL1Q629FL90M= +golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/sync v0.0.0-20170517211232-f52d1811a629 h1:wqoYUzeICxRnvJCvfHTh0OY0VQ6xern7nYq+ccc19e4= +golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20171031081856-95c657629925/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/logging/entry.go b/logging/entry.go index f87c158..496a9a1 100644 --- a/logging/entry.go +++ b/logging/entry.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/Sirupsen/logrus" + "github.com/sirupsen/logrus" "github.com/pkg/errors" ) diff --git a/logging/getter.go b/logging/getter.go index 2e4f309..4ef4446 100644 --- a/logging/getter.go +++ b/logging/getter.go @@ -3,7 +3,7 @@ package logging import ( "errors" - "github.com/Sirupsen/logrus" + "github.com/sirupsen/logrus" ) func GetDefaultLogEntry() Entry { diff --git a/logging/logger.go b/logging/logger.go index 1936ffd..0bb6c67 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -9,7 +9,7 @@ import ( "sync" "time" - "github.com/Sirupsen/logrus" + "github.com/sirupsen/logrus" ) var Logger *logrus.Logger diff --git a/notifier/broker.go b/notifier/broker.go index f7c9b4a..5f54be2 100644 --- a/notifier/broker.go +++ b/notifier/broker.go @@ -3,7 +3,7 @@ package notifier import ( "sync" - "github.com/Sirupsen/logrus" + "github.com/sirupsen/logrus" "github.com/foomo/neosproxy/cache" "github.com/foomo/neosproxy/logging" diff --git a/proxy/api.go b/proxy/api.go index 416cdf9..2c94259 100644 --- a/proxy/api.go +++ b/proxy/api.go @@ -7,7 +7,9 @@ import ( "strings" "time" - "github.com/Sirupsen/logrus" + "github.com/foomo/neosproxy/cache/content/store" + + "github.com/sirupsen/logrus" "github.com/cloudfoundry/bytefmt" "github.com/foomo/neosproxy/cache" "github.com/foomo/neosproxy/client/cms" @@ -50,6 +52,17 @@ func (p *Proxy) getContent(w http.ResponseWriter, r *http.Request) { logging.FieldID: id, }) + // etag cache handling + headerEtag := r.Header.Get("ETag") + if headerEtag != "" { + etag, errEtag := p.contentCache.GetEtag(store.GetHash(id, dimension, workspace)) + if errEtag == nil && etag != "" && etag == headerEtag { + w.WriteHeader(http.StatusNotModified) + log.WithDuration(start).Debug("content not modified") + return + } + } + // try cache hit, invalidate in case of item not found item, errCacheGet := p.contentCache.Get(id, dimension, workspace) if errCacheGet != nil { @@ -78,6 +91,8 @@ func (p *Proxy) getContent(w http.ResponseWriter, r *http.Request) { HTML: item.HTML, } + w.Header().Set("ETag", item.GetEtag()) + // stream json response encoder := json.NewEncoder(w) errEncode := encoder.Encode(data) @@ -88,7 +103,7 @@ func (p *Proxy) getContent(w http.ResponseWriter, r *http.Request) { } // done - log.WithDuration(start).Debug("served content") + log.WithDuration(start).Debug("content served") return } @@ -241,12 +256,87 @@ func (p *Proxy) streamStatus(w http.ResponseWriter, r *http.Request) { // error handling if errEncode != nil { log.WithError(errEncode).WithField("content-negotiation", contentNegotioation).Error("failed streaming status") - w.WriteHeader(http.StatusInternalServerError) + http.Error(w, "failed streaming status", http.StatusInternalServerError) return } } +func (p *Proxy) getAllEtags(w http.ResponseWriter, r *http.Request) { + + // extract request data + workspace := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("workspace"))) + + // validate workspace + if workspace == "" { + workspace = cms.WorkspaceLive + } + + // logger + log := p.setupLogger(r, "getAllEtags").WithField(logging.FieldWorkspace, workspace) + + etags := p.contentCache.GetAllEtags(workspace) + + w.Header().Set("Content-Type", string(mimeApplicationJSON)) + encoder := json.NewEncoder(w) + errEncode := encoder.Encode(etags) + + // error handling + if errEncode != nil { + log.WithError(errEncode).Error("failed encoding etags") + http.Error(w, "failed encoding etags", http.StatusInternalServerError) + return + } + + return +} + +func (p *Proxy) getEtag(w http.ResponseWriter, r *http.Request, hash string) { + // logger + log := p.setupLogger(r, "getEtag").WithField(logging.FieldID, hash) + + etag, errEtag := p.contentCache.GetEtag(hash) + + // error handling + if errEtag != nil { + log.WithError(errEtag).Error("failed getting etag") + http.Error(w, "failed getting etag", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", string(mimeTextPlain)) + w.Header().Set("ETag", etag) + w.Write([]byte(etag)) + + return +} + +func (p *Proxy) getEtagByID(w http.ResponseWriter, r *http.Request) { + // extract request data + workspace := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("workspace"))) + + // validate workspace + if workspace == "" { + workspace = cms.WorkspaceLive + } + + // extract request data + id := getRequestParameter(r, "id") + dimension := getRequestParameter(r, "dimension") + + hash := store.GetHash(id, dimension, workspace) + + p.getEtag(w, r, hash) +} + +func (p *Proxy) getEtagByHash(w http.ResponseWriter, r *http.Request) { + + // extract request data + hash := getRequestParameter(r, "hash") + + p.getEtag(w, r, hash) +} + // ------------------------------------------------------------------------------------------------ // ~ Private methods // ------------------------------------------------------------------------------------------------ diff --git a/proxy/routes.go b/proxy/routes.go index 0d94ea6..0fc59eb 100644 --- a/proxy/routes.go +++ b/proxy/routes.go @@ -16,12 +16,25 @@ const routeContentServerExport = "/contentserver/export" func (p *Proxy) setupRoutes() { // hijack content server export routes + + // content tree / sitemap p.router.HandleFunc(routeContentServerExport, p.streamCachedNeosContentServerExport) p.router.HandleFunc(routeContentServerExport, p.streamCachedNeosContentServerExport).Queries("workspace", "{workspace}") - // /contentserver/export/de/571fd1ae-c8e4-4d91-a708-d97025fb015c?workspace=stage - p.router.HandleFunc(routeContentServerExport+"/{dimension}/{id}", p.getContent) - p.router.HandleFunc(routeContentServerExport+"/{dimension}/{id}", p.getContent).Queries("workspace", "{workspace}") + // etag + p.router.HandleFunc(routeContentServerExport+"/etag/{dimension}/{id}", p.getEtagByID).Methods(http.MethodGet) + p.router.HandleFunc(routeContentServerExport+"/etag/{dimension}/{id}", p.getEtagByID).Methods(http.MethodGet).Queries("workspace", "{workspace}") + p.router.HandleFunc(routeContentServerExport+"/etag/{hash}", p.getEtagByHash).Methods(http.MethodGet) + + p.router.HandleFunc(routeContentServerExport+"/etags", p.getAllEtags).Methods(http.MethodGet) + p.router.HandleFunc(routeContentServerExport+"/etags", p.getAllEtags).Methods(http.MethodGet).Queries("workspace", "{workspace}") + + // documents => /contentserver/export/de/571fd1ae-c8e4-4d91-a708-d97025fb015c?workspace=stage + p.router.HandleFunc(routeContentServerExport+"/{dimension}/{id}", p.getContent).Methods(http.MethodGet) + p.router.HandleFunc(routeContentServerExport+"/{dimension}/{id}", p.getContent).Methods(http.MethodGet).Queries("workspace", "{workspace}") + + p.router.HandleFunc(routeContentServerExport+"/{dimension}/{id}", p.getEtagByID).Methods(http.MethodHead) + p.router.HandleFunc(routeContentServerExport+"/{dimension}/{id}", p.getEtagByID).Methods(http.MethodHead).Queries("workspace", "{workspace}") // api // neosproxy/cache/%s?workspace=%s