avatar.go 8.1 KB

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