diff --git a/go.mod b/go.mod index 7069238a2..0cde5ba97 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/go-chi/chi v4.0.2+incompatible github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/micro/cli/v2 v2.1.1 + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/ogier/pflag v0.0.1 // indirect github.com/oklog/run v1.0.0 github.com/openzipkin/zipkin-go v0.2.2 diff --git a/go.sum b/go.sum index bde3bff73..0767e8baf 100644 --- a/go.sum +++ b/go.sum @@ -522,6 +522,8 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/stan.go v0.5.0/go.mod h1:dYqB+vMN3C2F9pT1FRQpg9eHbjPj6mP0yYuyBNuXHZE= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nlopes/slack v0.6.0/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk= github.com/nlopes/slack v0.6.1-0.20191106133607-d06c2a2b3249/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk= github.com/nrdcg/auroradns v1.0.0/go.mod h1:6JPXKzIRzZzMqtTDgueIhTi6rFf1QvYE/HzqidhOhjw= diff --git a/pkg/service/v0/instrument.go b/pkg/service/v0/instrument.go index 5021bf1c9..9c3a6d999 100644 --- a/pkg/service/v0/instrument.go +++ b/pkg/service/v0/instrument.go @@ -25,6 +25,6 @@ func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // Dummy implements the Service interface. -func (i instrument) Dummy(w http.ResponseWriter, r *http.Request) { - i.next.Dummy(w, r) +func (i instrument) Thumbnails(w http.ResponseWriter, r *http.Request) { + i.next.Thumbnails(w, r) } diff --git a/pkg/service/v0/logging.go b/pkg/service/v0/logging.go index e0b10f8c2..a439f1efd 100644 --- a/pkg/service/v0/logging.go +++ b/pkg/service/v0/logging.go @@ -25,6 +25,6 @@ func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // Dummy implements the Service interface. -func (l logging) Dummy(w http.ResponseWriter, r *http.Request) { - l.next.Dummy(w, r) +func (l logging) Thumbnails(w http.ResponseWriter, r *http.Request) { + l.next.Thumbnails(w, r) } diff --git a/pkg/service/v0/service.go b/pkg/service/v0/service.go index a251734d0..e47c73c99 100644 --- a/pkg/service/v0/service.go +++ b/pkg/service/v0/service.go @@ -2,15 +2,18 @@ package svc import ( "net/http" + "strconv" "github.com/go-chi/chi" "github.com/owncloud/ocis-thumbnails/pkg/config" + "github.com/owncloud/ocis-thumbnails/pkg/thumbnails" + "github.com/owncloud/ocis-thumbnails/pkg/thumbnails/cache" ) // Service defines the extension handlers. type Service interface { ServeHTTP(http.ResponseWriter, *http.Request) - Dummy(http.ResponseWriter, *http.Request) + Thumbnails(http.ResponseWriter, *http.Request) } // NewService returns a service implementation for Service. @@ -23,10 +26,13 @@ func NewService(opts ...Option) Service { svc := Thumbnails{ config: options.Config, mux: m, + manager: thumbnails.SimpleManager{ + Cache: cache.NewInMemoryCache(), + }, } m.Route(options.Config.HTTP.Root, func(r chi.Router) { - r.Get("/", svc.Dummy) + r.Get("/thumbnails", svc.Thumbnails) }) return svc @@ -34,8 +40,9 @@ func NewService(opts ...Option) Service { // Thumbnails defines implements the business logic for Service. type Thumbnails struct { - config *config.Config - mux *chi.Mux + config *config.Config + mux *chi.Mux + manager thumbnails.Manager } // ServeHTTP implements the Service interface. @@ -43,10 +50,30 @@ func (g Thumbnails) ServeHTTP(w http.ResponseWriter, r *http.Request) { g.mux.ServeHTTP(w, r) } -// Dummy implements the Service interface. -func (g Thumbnails) Dummy(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusOK) +// Thumbnails provides the endpoint to retrieve a thumbnail for an image +func (g Thumbnails) Thumbnails(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + width, _ := strconv.Atoi(query.Get("w")) + height, _ := strconv.Atoi(query.Get("h")) + fileType := query.Get("type") + fileID := query.Get("file_id") - w.Write([]byte("Hello ocis-thumbnails!")) + encoder := thumbnails.EncoderForType(fileType) + if encoder == nil { + // TODO: better error responses + w.Write([]byte("can't encode that")) + return + } + ctx := thumbnails.ThumbnailContext{ + Width: width, + Height: height, + ImagePath: fileID, + Encoder: encoder, + } + thumbnail, err := g.manager.Get(ctx) + if err != nil { + w.Write([]byte(err.Error())) + } + + w.Write(thumbnail) } diff --git a/pkg/service/v0/tracing.go b/pkg/service/v0/tracing.go index 48da0b320..444afd3ec 100644 --- a/pkg/service/v0/tracing.go +++ b/pkg/service/v0/tracing.go @@ -21,6 +21,6 @@ func (t tracing) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // Dummy implements the Service interface. -func (t tracing) Dummy(w http.ResponseWriter, r *http.Request) { - t.next.Dummy(w, r) +func (t tracing) Thumbnails(w http.ResponseWriter, r *http.Request) { + t.next.Thumbnails(w, r) } diff --git a/pkg/thumbnails/cache/cache.go b/pkg/thumbnails/cache/cache.go new file mode 100644 index 000000000..71db593cd --- /dev/null +++ b/pkg/thumbnails/cache/cache.go @@ -0,0 +1,11 @@ +package cache + +import ( + "image" +) + +// Cache defines the interface for a thumbnail cache. +type Cache interface { + Get(key string) image.Image + Set(key string, thumbnail image.Image) (image.Image, error) +} diff --git a/pkg/thumbnails/cache/filesystem.go b/pkg/thumbnails/cache/filesystem.go new file mode 100644 index 000000000..b3e25a4d8 --- /dev/null +++ b/pkg/thumbnails/cache/filesystem.go @@ -0,0 +1,3 @@ +package cache + +// TODO: implement filesystem cache for longer persistence diff --git a/pkg/thumbnails/cache/inmemory.go b/pkg/thumbnails/cache/inmemory.go new file mode 100644 index 000000000..d9c45fb66 --- /dev/null +++ b/pkg/thumbnails/cache/inmemory.go @@ -0,0 +1,22 @@ +package cache + +import "image" + +func NewInMemoryCache() InMemoryCache { + return InMemoryCache{ + store: make(map[string]image.Image), + } +} + +type InMemoryCache struct { + store map[string]image.Image +} + +func (fsc InMemoryCache) Get(key string) image.Image { + return fsc.store[key] +} + +func (fsc InMemoryCache) Set(key string, thumbnail image.Image) (image.Image, error) { + fsc.store[key] = thumbnail + return thumbnail, nil +} diff --git a/pkg/thumbnails/encoding.go b/pkg/thumbnails/encoding.go new file mode 100644 index 000000000..60a02f59b --- /dev/null +++ b/pkg/thumbnails/encoding.go @@ -0,0 +1,47 @@ +package thumbnails + +import ( + "image" + "image/jpeg" + "image/png" + "io" + "strings" +) + +type Encoder interface { + Encode(io.Writer, image.Image) error + Types() []string +} + +type PngEncoder struct{} + +func (e PngEncoder) Encode(w io.Writer, i image.Image) error { + return png.Encode(w, i) +} + +func (e PngEncoder) Types() []string { + return []string{"png"} +} + +type JpegEncoder struct{} + +func (e JpegEncoder) Encode(w io.Writer, i image.Image) error { + return jpeg.Encode(w, i, nil) +} + +func (e JpegEncoder) Types() []string { + return []string{"jpeg", "jpg"} +} + +func EncoderForType(fileType string) Encoder { + switch strings.ToLower(fileType) { + case "png": + return PngEncoder{} + case "jpg": + fallthrough + case "jpeg": + return JpegEncoder{} + default: + return nil + } +} diff --git a/pkg/thumbnails/thumbnails.go b/pkg/thumbnails/thumbnails.go new file mode 100644 index 000000000..07d163e7f --- /dev/null +++ b/pkg/thumbnails/thumbnails.go @@ -0,0 +1,86 @@ +package thumbnails + +import ( + "bytes" + "image" + "os" + "strings" + "time" + + "github.com/nfnt/resize" + "github.com/owncloud/ocis-thumbnails/pkg/thumbnails/cache" +) + +// ThumbnailContext bundles information needed to generate a thumbnail for afile +type ThumbnailContext struct { + Width int + Height int + ImagePath string + + Encoder Encoder +} + +// Manager is responsible for generating thumbnails +type Manager interface { + // Get will return a thumbnail for a file + Get(ThumbnailContext) ([]byte, error) + GetCached(ThumbnailContext) []byte +} + +// SimpleManager is a simple implementation of Manager +type SimpleManager struct { + Cache cache.Cache +} + +// Get implements the Get Method of Manager +func (s SimpleManager) Get(ctx ThumbnailContext) ([]byte, error) { + key := buildCacheKey(ctx) + + cached := s.Cache.Get(key) + if cached == nil { + thumbnail := s.generate(ctx) + s.Cache.Set(key, thumbnail) + cached = thumbnail + } + + buf := new(bytes.Buffer) + err := ctx.Encoder.Encode(buf, cached) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// GetCached tries to get the cached thumbnail and return it. +// If there is no cached thumbnail it will return nil +func (s SimpleManager) GetCached(ctx ThumbnailContext) []byte { + key := buildCacheKey(ctx) + cached := s.Cache.Get(key) + if cached == nil { + return nil + } + buf := new(bytes.Buffer) + ctx.Encoder.Encode(buf, cached) + return buf.Bytes() +} + +func (s SimpleManager) generate(ctx ThumbnailContext) image.Image { + // TODO: remove, just for demo purposes + time.Sleep(time.Second * 2) + + // TODO: get file from reva + reader, _ := os.Open(ctx.ImagePath) + defer reader.Close() + m, _, _ := image.Decode(reader) + thumbnail := resize.Thumbnail(uint(ctx.Width), uint(ctx.Height), m, resize.Lanczos2) + return thumbnail +} + +func buildCacheKey(ctx ThumbnailContext) string { + parts := []string{ + ctx.ImagePath, + string(ctx.Width) + "x" + string(ctx.Height), + strings.Join(ctx.Encoder.Types(), ","), + } + return strings.Join(parts, "+") +}