first prototype of the thumbnail service

Currently uses in memory caching and loads the file from the local
filesystem.
This commit is contained in:
David Christofas
2020-03-03 16:40:02 +01:00
parent 883778d883
commit a98df0b11f
11 changed files with 214 additions and 15 deletions
+1
View File
@@ -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
+2
View File
@@ -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=
+2 -2
View File
@@ -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)
}
+2 -2
View File
@@ -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)
}
+36 -9
View File
@@ -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)
}
+2 -2
View File
@@ -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)
}
+11
View File
@@ -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)
}
+3
View File
@@ -0,0 +1,3 @@
package cache
// TODO: implement filesystem cache for longer persistence
+22
View File
@@ -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
}
+47
View File
@@ -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
}
}
+86
View File
@@ -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, "+")
}