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.

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