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
10 years ago
11 years ago
10 years ago
11 years ago
10 years ago
11 years ago
11 years ago
10 years ago
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. }