You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

297 lines
6.8 KiB

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