diff --git a/README.md b/README.md index fde1d64f..dc52a514 100644 --- a/README.md +++ b/README.md @@ -238,3 +238,4 @@ _all calls require auth and admin_ - User can vote for the comment multiple times but only to change his/her vote. Double-voting not allowed. - User can edit comments in 5 mins window after creation. - User ID prefixed by oauth provider name in order to avoid collisions and potential abuse. +- All avatars cached locally in order to prevent rate limiters from google/github/facebook. diff --git a/app/rest/auth/auth.go b/app/rest/auth/auth.go index ce8acdb5..eb34bd09 100644 --- a/app/rest/auth/auth.go +++ b/app/rest/auth/auth.go @@ -32,9 +32,9 @@ type Provider struct { Endpoint oauth2.Endpoint Scopes []string MapUser func(userData, []byte) store.User - AvatarProxy *avatar.Proxy - conf *oauth2.Config + avatarProxy *avatar.Proxy + conf *oauth2.Config } // Params to make initialized and ready to use provider @@ -69,7 +69,7 @@ func initProvider(p Params, provider Provider) Provider { provider.conf = &conf provider.Store = p.SessionStore - provider.AvatarProxy = p.AvatarProxy + provider.avatarProxy = p.AvatarProxy return provider } @@ -166,8 +166,8 @@ func (p Provider) authHandler(w http.ResponseWriter, r *http.Request) { log.Printf("[DEBUG] got raw user info %+v", jData) u := p.MapUser(jData, data) - if p.AvatarProxy != nil { - if avatarURL, e := p.AvatarProxy.Put(u); e == nil { + if p.avatarProxy != nil { + if avatarURL, e := p.avatarProxy.Put(u); e == nil { u.Picture = avatarURL } else { log.Printf("[WARN] failed to proxy avatar, %s", e) diff --git a/app/rest/avatar/avatar.go b/app/rest/avatar/avatar.go index ff185972..40eaef32 100644 --- a/app/rest/avatar/avatar.go +++ b/app/rest/avatar/avatar.go @@ -29,6 +29,8 @@ type Proxy struct { RoutePath string } +const imgSfx = ".image" + // Put gets original avatar url from user info and returns proxied url func (p *Proxy) Put(u store.User) (avatarURL string, err error) { @@ -39,6 +41,7 @@ func (p *Proxy) Put(u store.User) (avatarURL string, err error) { return "", errors.Errorf("no picture for %s", u.ID) } + // load avatar from remote location client := http.Client{Timeout: 10 * time.Second} resp, err := client.Get(u.Picture) if err != nil { @@ -49,13 +52,18 @@ func (p *Proxy) Put(u store.User) (avatarURL string, err error) { log.Printf("[WARN] can't close response body, %s", e) } }() + + // get ID and location of locally cached avatar encID := p.encodeID(u.ID) - location := p.location(encID) - if err = os.Mkdir(location, 0700); err != nil && !strings.Contains(err.Error(), "file exists") { - return "", errors.Wrapf(err, "failed to make avatar location %s", location) + location := p.location(encID) // location adds partion to path + + if _, err = os.Stat(location); os.IsNotExist(err) { + if e := os.Mkdir(location, 0700); e != nil { + return "", errors.Wrapf(e, "failed to mkdir avatar location %s", location) + } } - avFile := path.Join(location, encID+".image") + avFile := path.Join(location, encID+imgSfx) fh, err := os.Create(avFile) if err != nil { return "", errors.Wrapf(err, "can't create file %s", avFile) @@ -71,15 +79,17 @@ func (p *Proxy) Put(u store.User) (avatarURL string, err error) { } log.Printf("[DEBUG] saved avatar from %s to %s, user %q", u.Picture, avFile, u.Name) - return p.RoutePath + "/" + encID + ".image", nil + return p.RoutePath + "/" + encID + imgSfx, nil } // Routes returns auth routes for given provider func (p *Proxy) Routes() chi.Router { router := chi.NewRouter() + + // GET /123456789.image router.Get("/{avatar}", func(w http.ResponseWriter, r *http.Request) { avatar := chi.URLParam(r, "avatar") - location := p.location(strings.TrimSuffix(avatar, ".image")) + location := p.location(strings.TrimSuffix(avatar, imgSfx)) avFile := path.Join(location, avatar) fh, err := os.Open(avFile) if err != nil { @@ -111,6 +121,7 @@ func (p *Proxy) Routes() chi.Router { return router } +// encodeID hashes user id to sha1 func (p *Proxy) encodeID(id string) string { h := sha1.New() _, err := h.Write([]byte(id)) @@ -121,7 +132,7 @@ func (p *Proxy) encodeID(id string) string { } // get location for user id by adding partion to final path -// the end result is a full path like this - /tmp/avatars.test/992 +// the end result is a full path like this - /tmp/avatars.test/92 func (p *Proxy) location(id string) string { checksum64 := crc64.Checksum([]byte(id), crc64.MakeTable(crc64.ECMA)) partition := checksum64 % 100