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.

307 lines
7.1 KiB

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