Просмотр исходного кода

Merge pull request #32 from shxsun/master

finish gravatar module
无闻 11 лет назад
Родитель
Сommit
8b43bd1c37
7 измененных файлов с 361 добавлено и 3 удалено
  1. 1 1
      conf/gitignore/C++
  2. 1 1
      models/user.go
  3. 298 0
      modules/avatar/avatar.go
  4. 56 0
      modules/avatar/avatar_test.go
  5. 1 1
      modules/base/tool.go
  6. BIN
      public/img/avatar/default.jpg
  7. 4 0
      web.go

+ 1 - 1
conf/gitignore/C++

@@ -10,4 +10,4 @@
 # Compiled Static libraries
 *.lai
 *.la
-*.a
+*.a

+ 1 - 1
models/user.go

@@ -72,7 +72,7 @@ func (user *User) HomeLink() string {
 
 // AvatarLink returns the user gravatar link.
 func (user *User) AvatarLink() string {
-	return "http://1.gravatar.com/avatar/" + user.Avatar
+	return "/avatar/" + user.Avatar
 }
 
 // NewGitSig generates and returns the signature of given user.

+ 298 - 0
modules/avatar/avatar.go

@@ -0,0 +1,298 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// for www.gravatar.com image cache
+package avatar
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"image"
+	"image/jpeg"
+	"image/png"
+	"io"
+	"log"
+	"net/http"
+	"net/url"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/nfnt/resize"
+)
+
+var (
+	gravatar = "http://www.gravatar.com/avatar"
+)
+
+func debug(a ...interface{}) {
+	if true {
+		log.Println(a...)
+	}
+}
+
+// hash email to md5 string
+// keep this func in order to make this package indenpent
+func HashEmail(email string) string {
+	h := md5.New()
+	h.Write([]byte(strings.ToLower(email)))
+	return hex.EncodeToString(h.Sum(nil))
+}
+
+type Avatar struct {
+	Hash           string
+	AlterImage     string // image path
+	cacheDir       string // image save dir
+	reqParams      string
+	imagePath      string
+	expireDuration time.Duration
+}
+
+func New(hash string, cacheDir string) *Avatar {
+	return &Avatar{
+		Hash:           hash,
+		cacheDir:       cacheDir,
+		expireDuration: time.Minute * 10,
+		reqParams: url.Values{
+			"d":    {"retro"},
+			"size": {"200"},
+			"r":    {"pg"}}.Encode(),
+		imagePath: filepath.Join(cacheDir, hash+".image"), //maybe png or jpeg
+	}
+}
+
+func (this *Avatar) HasCache() bool {
+	fileInfo, err := os.Stat(this.imagePath)
+	return err == nil && fileInfo.Mode().IsRegular()
+}
+
+func (this *Avatar) Modtime() (modtime time.Time, err error) {
+	fileInfo, err := os.Stat(this.imagePath)
+	if err != nil {
+		return
+	}
+	return fileInfo.ModTime(), nil
+}
+
+func (this *Avatar) Expired() bool {
+	modtime, err := this.Modtime()
+	return err != nil || time.Since(modtime) > this.expireDuration
+}
+
+// default image format: jpeg
+func (this *Avatar) Encode(wr io.Writer, size int) (err error) {
+	var img image.Image
+	decodeImageFile := func(file string) (img image.Image, err error) {
+		fd, err := os.Open(file)
+		if err != nil {
+			return
+		}
+		defer fd.Close()
+		img, err = jpeg.Decode(fd)
+		if err != nil {
+			fd.Seek(0, os.SEEK_SET)
+			img, err = png.Decode(fd)
+		}
+		return
+	}
+	imgPath := this.imagePath
+	if !this.HasCache() {
+		if this.AlterImage == "" {
+			return errors.New("request image failed, and no alt image offered")
+		}
+		imgPath = this.AlterImage
+	}
+	img, err = decodeImageFile(imgPath)
+	if err != nil {
+		return
+	}
+	m := resize.Resize(uint(size), 0, img, resize.Lanczos3)
+	return jpeg.Encode(wr, m, nil)
+}
+
+// get image from gravatar.com
+func (this *Avatar) Update() {
+	thunder.Fetch(gravatar+"/"+this.Hash+"?"+this.reqParams,
+		this.imagePath)
+}
+
+func (this *Avatar) UpdateTimeout(timeout time.Duration) error {
+	var err error
+	select {
+	case <-time.After(timeout):
+		err = errors.New("get gravatar image timeout")
+	case err = <-thunder.GoFetch(gravatar+"/"+this.Hash+"?"+this.reqParams,
+		this.imagePath):
+	}
+	return err
+}
+
+type avatarHandler struct {
+	cacheDir string
+	altImage string
+}
+
+func (this *avatarHandler) mustInt(r *http.Request, defaultValue int, keys ...string) int {
+	var v int
+	for _, k := range keys {
+		if _, err := fmt.Sscanf(r.FormValue(k), "%d", &v); err == nil {
+			defaultValue = v
+		}
+	}
+	return defaultValue
+}
+
+func (this *avatarHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	urlPath := r.URL.Path
+	hash := urlPath[strings.LastIndex(urlPath, "/")+1:]
+	//hash = HashEmail(hash)
+	size := this.mustInt(r, 80, "s", "size") // size = 80*80
+
+	avatar := New(hash, this.cacheDir)
+	avatar.AlterImage = this.altImage
+	if avatar.Expired() {
+		err := avatar.UpdateTimeout(time.Millisecond * 500)
+		if err != nil {
+			debug(err)
+			//log.Trace("avatar update error: %v", err)
+		}
+	}
+	if modtime, err := avatar.Modtime(); err == nil {
+		etag := fmt.Sprintf("size(%d)", size)
+		if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) && etag == r.Header.Get("If-None-Match") {
+			h := w.Header()
+			delete(h, "Content-Type")
+			delete(h, "Content-Length")
+			w.WriteHeader(http.StatusNotModified)
+			return
+		}
+		w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
+		w.Header().Set("ETag", etag)
+	}
+	w.Header().Set("Content-Type", "image/jpeg")
+	err := avatar.Encode(w, size)
+	if err != nil {
+		//log.Warn("avatar encode error: %v", err) // will panic when err != nil
+		debug(err)
+		w.WriteHeader(500)
+	}
+}
+
+// http.Handle("/avatar/", avatar.HttpHandler("./cache"))
+func HttpHandler(cacheDir string, defaultImgPath string) http.Handler {
+	return &avatarHandler{
+		cacheDir: cacheDir,
+		altImage: defaultImgPath,
+	}
+}
+
+// thunder downloader
+var thunder = &Thunder{QueueSize: 10}
+
+type Thunder struct {
+	QueueSize int // download queue size
+	q         chan *thunderTask
+	once      sync.Once
+}
+
+func (t *Thunder) init() {
+	if t.QueueSize < 1 {
+		t.QueueSize = 1
+	}
+	t.q = make(chan *thunderTask, t.QueueSize)
+	for i := 0; i < t.QueueSize; i++ {
+		go func() {
+			for {
+				task := <-t.q
+				task.Fetch()
+			}
+		}()
+	}
+}
+
+func (t *Thunder) Fetch(url string, saveFile string) error {
+	t.once.Do(t.init)
+	task := &thunderTask{
+		Url:      url,
+		SaveFile: saveFile,
+	}
+	task.Add(1)
+	t.q <- task
+	task.Wait()
+	return task.err
+}
+
+func (t *Thunder) GoFetch(url, saveFile string) chan error {
+	c := make(chan error)
+	go func() {
+		c <- t.Fetch(url, saveFile)
+	}()
+	return c
+}
+
+// thunder download
+type thunderTask struct {
+	Url      string
+	SaveFile string
+	sync.WaitGroup
+	err error
+}
+
+func (this *thunderTask) Fetch() {
+	this.err = this.fetch()
+	this.Done()
+}
+
+var client = &http.Client{}
+
+func (this *thunderTask) fetch() error {
+	//log.Println("thunder, fetch", this.Url)
+	req, _ := http.NewRequest("GET", this.Url, nil)
+	req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
+	req.Header.Set("Accept-Encoding", "gzip,deflate,sdch")
+	req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8")
+	req.Header.Set("Cache-Control", "no-cache")
+	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.154 Safari/537.36")
+	resp, err := client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != 200 {
+		return fmt.Errorf("status code: %d", resp.StatusCode)
+	}
+
+	/*
+		log.Println("headers:", resp.Header)
+		switch resp.Header.Get("Content-Type") {
+		case "image/jpeg":
+			this.SaveFile += ".jpeg"
+		case "image/png":
+			this.SaveFile += ".png"
+		}
+	*/
+	/*
+		imgType := resp.Header.Get("Content-Type")
+		if imgType != "image/jpeg" && imgType != "image/png" {
+			return errors.New("not png or jpeg")
+		}
+	*/
+
+	tmpFile := this.SaveFile + ".part" // mv to destination when finished
+	fd, err := os.Create(tmpFile)
+	if err != nil {
+		return err
+	}
+	_, err = io.Copy(fd, resp.Body)
+	fd.Close()
+	if err != nil {
+		os.Remove(tmpFile)
+		return err
+	}
+	return os.Rename(tmpFile, this.SaveFile)
+}

+ 56 - 0
modules/avatar/avatar_test.go

@@ -0,0 +1,56 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+package avatar_test
+
+import (
+	"log"
+	"os"
+	"strconv"
+	"testing"
+	"time"
+
+	"github.com/gogits/gogs/modules/avatar"
+)
+
+const TMPDIR = "test-avatar"
+
+func TestFetch(t *testing.T) {
+	os.Mkdir(TMPDIR, 0755)
+	defer os.RemoveAll(TMPDIR)
+
+	hash := avatar.HashEmail("[email protected]")
+	a := avatar.New(hash, TMPDIR)
+	a.UpdateTimeout(time.Millisecond * 200)
+}
+
+func TestFetchMany(t *testing.T) {
+	os.Mkdir(TMPDIR, 0755)
+	defer os.RemoveAll(TMPDIR)
+
+	log.Println("start")
+	var n = 5
+	ch := make(chan bool, n)
+	for i := 0; i < n; i++ {
+		go func(i int) {
+			hash := avatar.HashEmail(strconv.Itoa(i) + "[email protected]")
+			a := avatar.New(hash, TMPDIR)
+			a.Update()
+			log.Println("finish", hash)
+			ch <- true
+		}(i)
+	}
+	for i := 0; i < n; i++ {
+		<-ch
+	}
+	log.Println("end")
+}
+
+// cat
+// wget http://www.artsjournal.com/artfulmanager/wp/wp-content/uploads/2013/12/200x200xmirror_cat.jpg.pagespeed.ic.GOZSv6v1_H.jpg -O default.jpg
+/*
+func TestHttp(t *testing.T) {
+	http.Handle("/", avatar.HttpHandler("./", "default.jpg"))
+	http.ListenAndServe(":8001", nil)
+}
+*/

+ 1 - 1
modules/base/tool.go

@@ -102,7 +102,7 @@ func CreateTimeLimitCode(data string, minutes int, startInf interface{}) string
 
 // AvatarLink returns avatar link by given e-mail.
 func AvatarLink(email string) string {
-	return "http://1.gravatar.com/avatar/" + EncodeMd5(email)
+	return "/avatar/" + EncodeMd5(email)
 }
 
 // Seconds-based time units

BIN
public/img/avatar/default.jpg


+ 4 - 0
web.go

@@ -17,6 +17,7 @@ import (
 
 	"github.com/gogits/gogs/models"
 	"github.com/gogits/gogs/modules/auth"
+	"github.com/gogits/gogs/modules/avatar"
 	"github.com/gogits/gogs/modules/base"
 	"github.com/gogits/gogs/modules/log"
 	"github.com/gogits/gogs/modules/mailer"
@@ -93,6 +94,9 @@ func runWeb(*cli.Context) {
 	m.Get("/stars", reqSignIn, user.Stars)
 	m.Get("/help", routers.Help)
 
+	avatarCache := avatar.HttpHandler("public/img/avatar/", "public/img/avatar/default.jpg")
+	m.Get("/avatar/:hash", avatarCache.ServeHTTP)
+
 	m.Group("/user", func(r martini.Router) {
 		r.Any("/login", binding.BindIgnErr(auth.LogInForm{}), user.SignIn)
 		r.Any("/sign_up", binding.BindIgnErr(auth.RegisterForm{}), user.SignUp)