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

avatar.go 8.2 kB

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