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.

311 lines
7.3 KiB

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