mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-05-05 19:09:56 -05:00
first prototype of the thumbnail service
Currently uses in memory caching and loads the file from the local filesystem.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Vendored
+11
@@ -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)
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
package cache
|
||||
|
||||
// TODO: implement filesystem cache for longer persistence
|
||||
Vendored
+22
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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, "+")
|
||||
}
|
||||
Reference in New Issue
Block a user