From 211c5ea9631f502b69667a3fe485792801bb5621 Mon Sep 17 00:00:00 2001 From: Umputun Date: Wed, 14 Feb 2018 14:02:56 -0600 Subject: [PATCH] basic avatar proxy --- app/main.go | 7 ++- app/rest/avatar/avatar.go | 113 ++++++++++++++++++++++++++++++++++++++ app/rest/server.go | 14 +++-- remark.rest | 2 +- 4 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 app/rest/avatar/avatar.go diff --git a/app/main.go b/app/main.go index c2431deb..27029536 100644 --- a/app/main.go +++ b/app/main.go @@ -15,6 +15,7 @@ import ( "github.com/umputun/remark/app/migrator" "github.com/umputun/remark/app/rest" "github.com/umputun/remark/app/rest/auth" + "github.com/umputun/remark/app/rest/avatar" "github.com/umputun/remark/app/rest/common" "github.com/umputun/remark/app/store" ) @@ -42,7 +43,8 @@ var opts struct { FacebookCID string `long:"facebook-cid" env:"REMARK_FACEBOOK_CID" description:"Facebook OAuth client ID"` FacebookCSEC string `long:"facebook-csec" env:"REMARK_FACEBOOK_CSEC" description:"Facebook OAuth client secret"` - Port int `long:"port" env:"REMARK_PORT" default:"8080" description:"port"` + AvatarStore string `long:"avatars" env:"SESSION_STORE" default:"./var/avatars" description:"path to avatars directory"` + Port int `long:"port" env:"REMARK_PORT" default:"8080" description:"port"` } `command:"server" description:"run server"` ImportCommand struct { @@ -64,7 +66,7 @@ func main() { setupLog(opts.Dbg) log.Print("[INFO] started remark") - if err := makeDirs(opts.BoltPath, opts.ServerCommand.SessionStore, opts.BackupLocation); err != nil { + if err := makeDirs(opts.BoltPath, opts.ServerCommand.SessionStore, opts.BackupLocation, opts.ServerCommand.AvatarStore); err != nil { log.Fatalf("[ERROR] can't create directories, %+v", err) } @@ -104,6 +106,7 @@ func main() { Exporter: &exporter, AuthProviders: makeAuthProviders(sessionStore), Cache: common.NewLoadingCache(4*time.Hour, 15*time.Minute, postFlushFn), + AvatarProxy: avatar.Proxy{StorePath: opts.ServerCommand.AvatarStore, RoutePath: "/api/v1/avatar"}, } if opts.DevMode { diff --git a/app/rest/avatar/avatar.go b/app/rest/avatar/avatar.go new file mode 100644 index 00000000..377f255d --- /dev/null +++ b/app/rest/avatar/avatar.go @@ -0,0 +1,113 @@ +// Package avatar provides cached proxy for user pictures/avatars +// refreshed by login and kept in local store + +package avatar + +import ( + "net/http" + "time" + + "os" + "path" + + "io" + + "log" + + "bytes" + "image" + "image/png" + + "fmt" + "hash/crc64" + + "strings" + + "github.com/go-chi/chi" + "github.com/go-chi/render" + "github.com/pkg/errors" + "github.com/umputun/remark/app/rest/common" + "github.com/umputun/remark/app/store" +) + +// Proxy provides avatar store and http handler for avatars +type Proxy struct { + StorePath string + DefaultAvatar string + RoutePath string +} + +// Put gets original avatar url from user info and returns proxied +func (p *Proxy) Put(u store.User) (avatarURL string, err error) { + if u.Picture == "" { + return "", errors.Errorf("no picture for %s", u.ID) + } + + client := http.Client{Timeout: 10 * time.Second} + resp, err := client.Get(u.Picture) + if err != nil { + return "", errors.Wrapf(err, "failed to get avatar for user %s from %s", u.ID, u.Picture) + } + defer resp.Body.Close() + + pngWr := &bytes.Buffer{} + if err = p.convertToPng(resp.Body, pngWr); err != nil { + return "", err + } + + location := p.location(u.ID) + os.Mkdir(location, 0700) + avFile := path.Join(location, u.ID+".png") + fh, err := os.Create(avFile) + if err != nil { + return "", errors.Wrapf(err, "can't create file %s", avFile) + } + defer fh.Close() + if _, err = io.Copy(fh, pngWr); err != nil { + return "", errors.Wrapf(err, "can't save file %s", avFile) + } + return p.RoutePath + "/" + u.ID + ".png", nil +} + +// Routes returns auth routes for given provider +func (p *Proxy) Routes() chi.Router { + router := chi.NewRouter() + router.Get("/{avatar}", func(w http.ResponseWriter, r *http.Request) { + avatar := chi.URLParam(r, "avatar") + location := p.location(strings.TrimSuffix(avatar, ".png")) + avFile := path.Join(location, avatar) + fh, err := os.Open(avFile) + if err != nil { + common.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't load avatar") + return + } + defer fh.Close() + w.Header().Set("Content-Type", "image/png") + if status, ok := r.Context().Value(render.StatusCtxKey).(int); ok { + w.WriteHeader(status) + } + if _, err = io.Copy(w, fh); err != nil { + log.Printf("[WARN] can't send response to %s, %s", r.RemoteAddr, err) + } + }) + + return router +} + +func (p *Proxy) convertToPng(r io.Reader, w io.Writer) error { + imageData, _, err := image.Decode(r) + if err != nil { + return errors.Wrap(err, "can't decode image") + } + + if err = png.Encode(w, imageData); err != nil { + return errors.Wrap(err, "can't encode png image") + } + return nil +} + +func (p *Proxy) location(id string) string { + checksum64 := crc64.Checksum([]byte(id), crc64.MakeTable(crc64.ECMA)) + partition := checksum64 % 1000 + return path.Join(p.StorePath, fmt.Sprintf("%03d", partition)) +} diff --git a/app/rest/server.go b/app/rest/server.go index 8bba5e85..8620358f 100644 --- a/app/rest/server.go +++ b/app/rest/server.go @@ -23,6 +23,7 @@ import ( "github.com/umputun/remark/app/migrator" "github.com/umputun/remark/app/rest/auth" + "github.com/umputun/remark/app/rest/avatar" "github.com/umputun/remark/app/rest/common" "github.com/umputun/remark/app/rest/format" "github.com/umputun/remark/app/store" @@ -38,6 +39,7 @@ type Server struct { Exporter migrator.Exporter Cache common.LoadingCache DevMode bool + AvatarProxy avatar.Proxy httpServer *http.Server mod admin @@ -80,9 +82,10 @@ func (s *Server) Run(port int) { // shortcut, can be any of providers, all logouts do the same - removes cookie r.Get("/logout", s.AuthProviders[0].LogoutHandler) } - r.Get("/avatar/{id}", s.avatarHandler) }) + router.Mount(s.AvatarProxy.RoutePath, s.AvatarProxy.Routes()) + // api routes router.Route("/api/v1", func(rapi chi.Router) { @@ -150,6 +153,12 @@ func (s *Server) createCommentCtrl(w http.ResponseWriter, r *http.Request) { // render markdown comment.Text = string(blackfriday.Run([]byte(comment.Text), blackfriday.WithNoExtensions())) + if avatarUrl, err := s.AvatarProxy.Put(user); err == nil { + comment.User.Picture = avatarUrl + } else { + log.Printf("[WARN] failed to proxy avatar, %s", err) + } + log.Printf("[DEBUG] create comment %+v", comment) // check if user blocked @@ -407,9 +416,6 @@ func (s *Server) voteCtrl(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, JSON{"id": comment.ID, "score": comment.Score}) } -func (s *Server) avatarHandler(w http.ResponseWriter, r *http.Request) { -} - // serves static files from /web func (s *Server) addFileServer(r chi.Router, path string, root http.FileSystem) { log.Printf("[INFO] run file server for %s", root) diff --git a/remark.rest b/remark.rest index a4935076..425dcfcb 100644 --- a/remark.rest +++ b/remark.rest @@ -9,7 +9,7 @@ GET {{host}}/api/v1/find?site=remark&sort=time&format=plain&url=https://radio-t. GET {{host}}/api/v1/last/50?site=remark ### create comment -POST {{host}}/api/v1/comment +POST 127.0.0.1:8080/api/v1/comment Content-Type: application/json {