avatar.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. // for www.gravatar.com image cache
  5. package avatar
  6. import (
  7. "crypto/md5"
  8. "encoding/hex"
  9. "errors"
  10. "fmt"
  11. "image"
  12. "image/jpeg"
  13. "image/png"
  14. "io"
  15. "log"
  16. "net/http"
  17. "net/url"
  18. "os"
  19. "path/filepath"
  20. "strings"
  21. "sync"
  22. "time"
  23. "github.com/nfnt/resize"
  24. )
  25. var (
  26. gravatar = "http://www.gravatar.com/avatar"
  27. )
  28. func debug(a ...interface{}) {
  29. if true {
  30. log.Println(a...)
  31. }
  32. }
  33. // hash email to md5 string
  34. // keep this func in order to make this package indenpent
  35. func HashEmail(email string) string {
  36. h := md5.New()
  37. h.Write([]byte(strings.ToLower(email)))
  38. return hex.EncodeToString(h.Sum(nil))
  39. }
  40. type Avatar struct {
  41. Hash string
  42. AlterImage string // image path
  43. cacheDir string // image save dir
  44. reqParams string
  45. imagePath string
  46. expireDuration time.Duration
  47. }
  48. func New(hash string, cacheDir string) *Avatar {
  49. return &Avatar{
  50. Hash: hash,
  51. cacheDir: cacheDir,
  52. expireDuration: time.Minute * 10,
  53. reqParams: url.Values{
  54. "d": {"retro"},
  55. "size": {"200"},
  56. "r": {"pg"}}.Encode(),
  57. imagePath: filepath.Join(cacheDir, hash+".image"), //maybe png or jpeg
  58. }
  59. }
  60. func (this *Avatar) HasCache() bool {
  61. fileInfo, err := os.Stat(this.imagePath)
  62. return err == nil && fileInfo.Mode().IsRegular()
  63. }
  64. func (this *Avatar) Modtime() (modtime time.Time, err error) {
  65. fileInfo, err := os.Stat(this.imagePath)
  66. if err != nil {
  67. return
  68. }
  69. return fileInfo.ModTime(), nil
  70. }
  71. func (this *Avatar) Expired() bool {
  72. modtime, err := this.Modtime()
  73. return err != nil || time.Since(modtime) > this.expireDuration
  74. }
  75. // default image format: jpeg
  76. func (this *Avatar) Encode(wr io.Writer, size int) (err error) {
  77. var img image.Image
  78. decodeImageFile := func(file string) (img image.Image, err error) {
  79. fd, err := os.Open(file)
  80. if err != nil {
  81. return
  82. }
  83. defer fd.Close()
  84. img, err = jpeg.Decode(fd)
  85. if err != nil {
  86. fd.Seek(0, os.SEEK_SET)
  87. img, err = png.Decode(fd)
  88. }
  89. return
  90. }
  91. imgPath := this.imagePath
  92. if !this.HasCache() {
  93. if this.AlterImage == "" {
  94. return errors.New("request image failed, and no alt image offered")
  95. }
  96. imgPath = this.AlterImage
  97. }
  98. img, err = decodeImageFile(imgPath)
  99. if err != nil {
  100. return
  101. }
  102. m := resize.Resize(uint(size), 0, img, resize.Lanczos3)
  103. return jpeg.Encode(wr, m, nil)
  104. }
  105. // get image from gravatar.com
  106. func (this *Avatar) Update() {
  107. thunder.Fetch(gravatar+"/"+this.Hash+"?"+this.reqParams,
  108. this.imagePath)
  109. }
  110. func (this *Avatar) UpdateTimeout(timeout time.Duration) error {
  111. var err error
  112. select {
  113. case <-time.After(timeout):
  114. err = errors.New("get gravatar image timeout")
  115. case err = <-thunder.GoFetch(gravatar+"/"+this.Hash+"?"+this.reqParams,
  116. this.imagePath):
  117. }
  118. return err
  119. }
  120. type avatarHandler struct {
  121. cacheDir string
  122. altImage string
  123. }
  124. func (this *avatarHandler) mustInt(r *http.Request, defaultValue int, keys ...string) int {
  125. var v int
  126. for _, k := range keys {
  127. if _, err := fmt.Sscanf(r.FormValue(k), "%d", &v); err == nil {
  128. defaultValue = v
  129. }
  130. }
  131. return defaultValue
  132. }
  133. func (this *avatarHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  134. urlPath := r.URL.Path
  135. hash := urlPath[strings.LastIndex(urlPath, "/")+1:]
  136. //hash = HashEmail(hash)
  137. size := this.mustInt(r, 80, "s", "size") // size = 80*80
  138. avatar := New(hash, this.cacheDir)
  139. avatar.AlterImage = this.altImage
  140. if avatar.Expired() {
  141. err := avatar.UpdateTimeout(time.Millisecond * 500)
  142. if err != nil {
  143. debug(err)
  144. //log.Trace("avatar update error: %v", err)
  145. }
  146. }
  147. if modtime, err := avatar.Modtime(); err == nil {
  148. etag := fmt.Sprintf("size(%d)", size)
  149. 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") {
  150. h := w.Header()
  151. delete(h, "Content-Type")
  152. delete(h, "Content-Length")
  153. w.WriteHeader(http.StatusNotModified)
  154. return
  155. }
  156. w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
  157. w.Header().Set("ETag", etag)
  158. }
  159. w.Header().Set("Content-Type", "image/jpeg")
  160. err := avatar.Encode(w, size)
  161. if err != nil {
  162. //log.Warn("avatar encode error: %v", err) // will panic when err != nil
  163. debug(err)
  164. w.WriteHeader(500)
  165. }
  166. }
  167. // http.Handle("/avatar/", avatar.HttpHandler("./cache"))
  168. func HttpHandler(cacheDir string, defaultImgPath string) http.Handler {
  169. return &avatarHandler{
  170. cacheDir: cacheDir,
  171. altImage: defaultImgPath,
  172. }
  173. }
  174. // thunder downloader
  175. var thunder = &Thunder{QueueSize: 10}
  176. type Thunder struct {
  177. QueueSize int // download queue size
  178. q chan *thunderTask
  179. once sync.Once
  180. }
  181. func (t *Thunder) init() {
  182. if t.QueueSize < 1 {
  183. t.QueueSize = 1
  184. }
  185. t.q = make(chan *thunderTask, t.QueueSize)
  186. for i := 0; i < t.QueueSize; i++ {
  187. go func() {
  188. for {
  189. task := <-t.q
  190. task.Fetch()
  191. }
  192. }()
  193. }
  194. }
  195. func (t *Thunder) Fetch(url string, saveFile string) error {
  196. t.once.Do(t.init)
  197. task := &thunderTask{
  198. Url: url,
  199. SaveFile: saveFile,
  200. }
  201. task.Add(1)
  202. t.q <- task
  203. task.Wait()
  204. return task.err
  205. }
  206. func (t *Thunder) GoFetch(url, saveFile string) chan error {
  207. c := make(chan error)
  208. go func() {
  209. c <- t.Fetch(url, saveFile)
  210. }()
  211. return c
  212. }
  213. // thunder download
  214. type thunderTask struct {
  215. Url string
  216. SaveFile string
  217. sync.WaitGroup
  218. err error
  219. }
  220. func (this *thunderTask) Fetch() {
  221. this.err = this.fetch()
  222. this.Done()
  223. }
  224. var client = &http.Client{}
  225. func (this *thunderTask) fetch() error {
  226. //log.Println("thunder, fetch", this.Url)
  227. req, _ := http.NewRequest("GET", this.Url, nil)
  228. req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
  229. req.Header.Set("Accept-Encoding", "gzip,deflate,sdch")
  230. req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8")
  231. req.Header.Set("Cache-Control", "no-cache")
  232. 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")
  233. resp, err := client.Do(req)
  234. if err != nil {
  235. return err
  236. }
  237. defer resp.Body.Close()
  238. if resp.StatusCode != 200 {
  239. return fmt.Errorf("status code: %d", resp.StatusCode)
  240. }
  241. /*
  242. log.Println("headers:", resp.Header)
  243. switch resp.Header.Get("Content-Type") {
  244. case "image/jpeg":
  245. this.SaveFile += ".jpeg"
  246. case "image/png":
  247. this.SaveFile += ".png"
  248. }
  249. */
  250. /*
  251. imgType := resp.Header.Get("Content-Type")
  252. if imgType != "image/jpeg" && imgType != "image/png" {
  253. return errors.New("not png or jpeg")
  254. }
  255. */
  256. tmpFile := this.SaveFile + ".part" // mv to destination when finished
  257. fd, err := os.Create(tmpFile)
  258. if err != nil {
  259. return err
  260. }
  261. _, err = io.Copy(fd, resp.Body)
  262. fd.Close()
  263. if err != nil {
  264. os.Remove(tmpFile)
  265. return err
  266. }
  267. return os.Rename(tmpFile, this.SaveFile)
  268. }