basic avatar proxy

This commit is contained in:
Umputun
2018-02-14 14:02:56 -06:00
parent e589e4aa70
commit 211c5ea963
4 changed files with 129 additions and 7 deletions
+5 -2
View File
@@ -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 {
+113
View File
@@ -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))
}
+10 -4
View File
@@ -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)
+1 -1
View File
@@ -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
{