Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package imagor

import (
"context"
"errors"
"sync"
)

type imagorContextKey struct{}

type imagorContextRef struct {
funcs []func()
l sync.Mutex
Cache sync.Map
}

func (r *imagorContextRef) Defer(fn func()) {
r.l.Lock()
r.funcs = append(r.funcs, fn)
r.l.Unlock()
}

func (r *imagorContextRef) Done() {
r.l.Lock()
for _, fn := range r.funcs {
fn()
}
r.funcs = nil
r.l.Unlock()
}

// WithContext context with imagor defer handling and cache
func WithContext(ctx context.Context) context.Context {
r := &imagorContextRef{}
ctx = context.WithValue(ctx, imagorContextKey{}, r)
go func() {
<-ctx.Done()
r.Done()
}()
return ctx
}

func mustContextValue(ctx context.Context) *imagorContextRef {
if r, ok := ctx.Value(imagorContextKey{}).(*imagorContextRef); ok && r != nil {
return r
}
panic(errors.New("not imagor context"))
}

// Defer add func to context, defer called at the end of request
func Defer(ctx context.Context, fn func()) {
mustContextValue(ctx).Defer(fn)
}

// ContextCachePut put cache within the imagor request context lifetime
func ContextCachePut(ctx context.Context, key any, val any) {
if r, ok := ctx.Value(imagorContextKey{}).(*imagorContextRef); ok && r != nil {
r.Cache.Store(key, val)
}
}

// ContextCacheGet get cache within the imagor request context lifetime
func ContextCacheGet(ctx context.Context, key any) (any, bool) {
if r, ok := ctx.Value(imagorContextKey{}).(*imagorContextRef); ok && r != nil {
return r.Cache.Load(key)
}
return nil, false
}
17 changes: 16 additions & 1 deletion defer_test.go → context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestDefer(t *testing.T) {
t.Fatal("should not call")
})
})
ctx = DeferContext(ctx)
ctx = WithContext(ctx)
Defer(ctx, func() {
called++
})
Expand All @@ -30,3 +30,18 @@ func TestDefer(t *testing.T) {
})
assert.Equal(t, 2, called, "should count all defers before cancel")
}

func TestContextCache(t *testing.T) {
ctx := context.Background()
assert.NotPanics(t, func() {
ContextCachePut(ctx, "foo", "bar")
})
ctx = WithContext(ctx)
s, ok := ContextCacheGet(ctx, "foo")
assert.False(t, ok)
assert.Nil(t, s)
ContextCachePut(ctx, "foo", "bar")
s, ok = ContextCacheGet(ctx, "foo")
assert.True(t, ok)
assert.Equal(t, "bar", s)
}
49 changes: 0 additions & 49 deletions defer.go

This file was deleted.

16 changes: 8 additions & 8 deletions detach.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,36 @@ import (
"time"
)

type contextKey struct {
type detachContextKey struct {
name string
}

var detachedCtxKey = &contextKey{"Detached"}
var detachedCtxKey = &detachContextKey{"Detached"}

type detached struct {
type detachedContext struct {
ctx context.Context
}

func (detached) Deadline() (time.Time, bool) {
func (detachedContext) Deadline() (time.Time, bool) {
return time.Time{}, false
}

func (detached) Done() <-chan struct{} {
func (detachedContext) Done() <-chan struct{} {
return nil
}

func (detached) Err() error {
func (detachedContext) Err() error {
return nil
}

func (d detached) Value(key interface{}) interface{} {
func (d detachedContext) Value(key interface{}) interface{} {
return d.ctx.Value(key)
}

// DetachContext returns a context that keeps all the values of its parent context
// but detaches from cancellation and timeout
func DetachContext(ctx context.Context) context.Context {
return context.WithValue(detached{ctx: ctx}, detachedCtxKey, true)
return context.WithValue(detachedContext{ctx: ctx}, detachedCtxKey, true)
}

// IsDetached returns if context is detached
Expand Down
2 changes: 1 addition & 1 deletion imagor.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func (app *Imagor) ServeHTTP(w http.ResponseWriter, r *http.Request) {

// Do executes Imagor operations
func (app *Imagor) Do(r *http.Request, p imagorpath.Params) (blob *Blob, err error) {
var ctx = DeferContext(r.Context())
var ctx = WithContext(r.Context())
var cancel func()
if app.RequestTimeout > 0 {
ctx, cancel = context.WithTimeout(ctx, app.RequestTimeout)
Expand Down
27 changes: 20 additions & 7 deletions storage/filestorage/filestorage.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import (

var dotFileRegex = regexp.MustCompile("/\\.")

type statKey struct {
Key string
}

type FileStorage struct {
BaseDir string
PathPrefix string
Expand Down Expand Up @@ -56,13 +60,17 @@ func (s *FileStorage) Path(image string) (string, bool) {
return filepath.Join(s.BaseDir, strings.TrimPrefix(image, s.PathPrefix)), true
}

func (s *FileStorage) Get(_ *http.Request, image string) (*imagor.Blob, error) {
func (s *FileStorage) Get(r *http.Request, image string) (*imagor.Blob, error) {
image, ok := s.Path(image)
if !ok {
return nil, imagor.ErrInvalid
}
return imagor.NewBlobFromFile(image, func(stats os.FileInfo) error {
if s.Expiration > 0 && time.Now().Sub(stats.ModTime()) > s.Expiration {
return imagor.NewBlobFromFile(image, func(stat os.FileInfo) error {
imagor.ContextCachePut(r.Context(), statKey{image}, imagor.Stat{
Size: stat.Size(),
ModifiedTime: stat.ModTime(),
})
if s.Expiration > 0 && time.Now().Sub(stat.ModTime()) > s.Expiration {
return imagor.ErrExpired
}
return nil
Expand Down Expand Up @@ -109,20 +117,25 @@ func (s *FileStorage) Delete(_ context.Context, image string) error {
return os.Remove(image)
}

func (s *FileStorage) Stat(_ context.Context, image string) (stat *imagor.Stat, err error) {
func (s *FileStorage) Stat(ctx context.Context, image string) (stat *imagor.Stat, err error) {
image, ok := s.Path(image)
if !ok {
return nil, imagor.ErrInvalid
}
stats, err := os.Stat(image)
if s, ok := imagor.ContextCacheGet(ctx, statKey{image}); ok && s != nil {
if stat, ok2 := s.(imagor.Stat); ok2 {
return &stat, nil
}
}
osStat, err := os.Stat(image)
if err != nil {
if os.IsNotExist(err) {
return nil, imagor.ErrNotFound
}
return nil, err
}
return &imagor.Stat{
Size: stats.Size(),
ModifiedTime: stats.ModTime(),
Size: osStat.Size(),
ModifiedTime: osStat.ModTime(),
}, nil
}
35 changes: 20 additions & 15 deletions storage/filestorage/filestorage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,53 +121,58 @@ func TestFileStore_Path(t *testing.T) {
}

func TestFileStorage_Load_Save(t *testing.T) {
ctx := context.Background()
ctx := imagor.WithContext(context.Background())
r := (&http.Request{}).WithContext(ctx)
dir, err := ioutil.TempDir("", "imagor-test")
require.NoError(t, err)

t.Run("blacklisted path", func(t *testing.T) {
s := New(dir)
_, err = s.Get(&http.Request{}, "/abc/.git")
_, err = s.Get(r, "/abc/.git")
assert.Equal(t, imagor.ErrInvalid, err)
assert.Equal(t, imagor.ErrInvalid, s.Put(ctx, "/abc/.git", imagor.NewBlobFromBytes([]byte("boo"))))
})
t.Run("CRUD", func(t *testing.T) {
s := New(dir, WithPathPrefix("/foo"), WithMkdirPermission("0755"), WithWritePermission("0666"))

_, err := checkBlob(s.Get(&http.Request{}, "/bar/fooo/asdf"))
_, err := checkBlob(s.Get(r, "/bar/fooo/asdf"))
assert.Equal(t, imagor.ErrInvalid, err)

_, err = s.Stat(context.Background(), "/bar/fooo/asdf")
_, err = s.Stat(ctx, "/bar/fooo/asdf")
assert.Equal(t, imagor.ErrInvalid, err)

_, err = checkBlob(s.Get(&http.Request{}, "/foo/fooo/asdf"))
_, err = checkBlob(s.Get(r, "/foo/fooo/asdf"))
assert.Equal(t, imagor.ErrNotFound, err)

_, err = s.Stat(context.Background(), "/foo/fooo/asdf")
_, err = s.Stat(ctx, "/foo/fooo/asdf")
assert.Equal(t, imagor.ErrNotFound, err)

assert.ErrorIs(t, s.Put(ctx, "/bar/fooo/asdf", imagor.NewBlobFromBytes([]byte("bar"))), imagor.ErrInvalid)

assert.Equal(t, imagor.ErrInvalid, s.Delete(context.Background(), "/bar/fooo/asdf"))
assert.Equal(t, imagor.ErrInvalid, s.Delete(ctx, "/bar/fooo/asdf"))

blob := imagor.NewBlobFromBytes([]byte("bar"))

require.NoError(t, s.Put(ctx, "/foo/fooo/asdf", blob))

b, err := checkBlob(s.Get(&http.Request{}, "/foo/fooo/asdf"))
stat, err := s.Stat(ctx, "/foo/fooo/asdf")
require.NoError(t, err)
assert.True(t, stat.ModifiedTime.Before(time.Now()))

b, err := checkBlob(s.Get(r, "/foo/fooo/asdf"))
require.NoError(t, err)
buf, err := b.ReadAll()
require.NoError(t, err)
assert.Equal(t, "bar", string(buf))

stat, err := s.Stat(context.Background(), "/foo/fooo/asdf")
stat, err = s.Stat(ctx, "/foo/fooo/asdf")
require.NoError(t, err)
assert.True(t, stat.ModifiedTime.Before(time.Now()))

err = s.Delete(context.Background(), "/foo/fooo/asdf")
err = s.Delete(ctx, "/foo/fooo/asdf")
require.NoError(t, err)

b, err = checkBlob(s.Get(&http.Request{}, "/foo/fooo/asdf"))
b, err = checkBlob(s.Get(r, "/foo/fooo/asdf"))
assert.Equal(t, imagor.ErrNotFound, err)

})
Expand All @@ -176,7 +181,7 @@ func TestFileStorage_Load_Save(t *testing.T) {
s := New(dir, WithSaveErrIfExists(true))
require.NoError(t, s.Put(ctx, "/foo/tar/asdf", imagor.NewBlobFromBytes([]byte("bar"))))
assert.Error(t, s.Put(ctx, "/foo/tar/asdf", imagor.NewBlobFromBytes([]byte("boo"))))
b, err := checkBlob(s.Get(&http.Request{}, "/foo/tar/asdf"))
b, err := checkBlob(s.Get(r, "/foo/tar/asdf"))
require.NoError(t, err)
buf, err := b.ReadAll()
require.NoError(t, err)
Expand All @@ -187,18 +192,18 @@ func TestFileStorage_Load_Save(t *testing.T) {
s := New(dir, WithExpiration(time.Millisecond*10))
var err error

_, err = checkBlob(s.Get(&http.Request{}, "/foo/bar/asdf"))
_, err = checkBlob(s.Get(r, "/foo/bar/asdf"))
assert.Equal(t, imagor.ErrNotFound, err)
blob := imagor.NewBlobFromBytes([]byte("bar"))
require.NoError(t, s.Put(ctx, "/foo/bar/asdf", blob))
b, err := checkBlob(s.Get(&http.Request{}, "/foo/bar/asdf"))
b, err := checkBlob(s.Get(r, "/foo/bar/asdf"))
require.NoError(t, err)
buf, err := b.ReadAll()
require.NoError(t, err)
assert.Equal(t, "bar", string(buf))

time.Sleep(time.Second)
_, err = checkBlob(s.Get(&http.Request{}, "/foo/bar/asdf"))
_, err = checkBlob(s.Get(r, "/foo/bar/asdf"))
require.ErrorIs(t, err, imagor.ErrExpired)
})
}
Expand Down
Loading