avatar.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. package avatar
  2. import (
  3. "crypto/md5"
  4. "encoding/hex"
  5. "errors"
  6. "fmt"
  7. "image"
  8. "image/jpeg"
  9. "image/png"
  10. "io"
  11. "log"
  12. "net/http"
  13. "net/url"
  14. "os"
  15. "path/filepath"
  16. "strings"
  17. "sync"
  18. "time"
  19. "github.com/nfnt/resize"
  20. )
  21. var (
  22. gravatar = "http://www.gravatar.com/avatar"
  23. defaultImagePath = "./default.jpg"
  24. )
  25. // hash email to md5 string
  26. func HashEmail(email string) string {
  27. h := md5.New()
  28. h.Write([]byte(strings.ToLower(email)))
  29. return hex.EncodeToString(h.Sum(nil))
  30. }
  31. type Avatar struct {
  32. Hash string
  33. cacheDir string // image save dir
  34. reqParams string
  35. imagePath string
  36. expireDuration time.Duration
  37. }
  38. func New(hash string, cacheDir string) *Avatar {
  39. return &Avatar{
  40. Hash: hash,
  41. cacheDir: cacheDir,
  42. expireDuration: time.Minute * 10,
  43. reqParams: url.Values{
  44. "d": {"retro"},
  45. "size": {"200"},
  46. "r": {"pg"}}.Encode(),
  47. imagePath: filepath.Join(cacheDir, hash+".image"), //maybe png or jpeg
  48. }
  49. }
  50. func (this *Avatar) InCache() bool {
  51. fileInfo, err := os.Stat(this.imagePath)
  52. return err == nil && fileInfo.Mode().IsRegular()
  53. }
  54. func (this *Avatar) Modtime() (modtime time.Time, err error) {
  55. fileInfo, err := os.Stat(this.imagePath)
  56. if err != nil {
  57. return
  58. }
  59. return fileInfo.ModTime(), nil
  60. }
  61. func (this *Avatar) Expired() bool {
  62. if !this.InCache() {
  63. return true
  64. }
  65. fileInfo, err := os.Stat(this.imagePath)
  66. return err != nil || time.Since(fileInfo.ModTime()) > this.expireDuration
  67. }
  68. // default image format: jpeg
  69. func (this *Avatar) Encode(wr io.Writer, size int) (err error) {
  70. var img image.Image
  71. decodeImageFile := func(file string) (img image.Image, err error) {
  72. fd, err := os.Open(file)
  73. if err != nil {
  74. return
  75. }
  76. defer fd.Close()
  77. img, err = jpeg.Decode(fd)
  78. if err != nil {
  79. fd.Seek(0, os.SEEK_SET)
  80. img, err = png.Decode(fd)
  81. }
  82. return
  83. }
  84. imgPath := this.imagePath
  85. if !this.InCache() {
  86. imgPath = defaultImagePath
  87. }
  88. img, err = decodeImageFile(imgPath)
  89. if err != nil {
  90. return
  91. }
  92. m := resize.Resize(uint(size), 0, img, resize.Lanczos3)
  93. return jpeg.Encode(wr, m, nil)
  94. }
  95. // get image from gravatar.com
  96. func (this *Avatar) Update() {
  97. thunder.Fetch(gravatar+"/"+this.Hash+"?"+this.reqParams,
  98. this.imagePath)
  99. }
  100. func (this *Avatar) UpdateTimeout(timeout time.Duration) error {
  101. var err error
  102. select {
  103. case <-time.After(timeout):
  104. err = errors.New("get gravatar image timeout")
  105. case err = <-thunder.GoFetch(gravatar+"/"+this.Hash+"?"+this.reqParams,
  106. this.imagePath):
  107. }
  108. return err
  109. }
  110. func init() {
  111. log.SetFlags(log.Lshortfile | log.LstdFlags)
  112. }
  113. // http.Handle("/avatar/", avatar.HttpHandler("./cache"))
  114. func HttpHandler(cacheDir string) func(w http.ResponseWriter, r *http.Request) {
  115. MustInt := func(r *http.Request, defaultValue int, keys ...string) int {
  116. var v int
  117. for _, k := range keys {
  118. if _, err := fmt.Sscanf(r.FormValue(k), "%d", &v); err == nil {
  119. defaultValue = v
  120. }
  121. }
  122. return defaultValue
  123. }
  124. return func(w http.ResponseWriter, r *http.Request) {
  125. urlPath := r.URL.Path
  126. hash := urlPath[strings.LastIndex(urlPath, "/")+1:]
  127. hash = HashEmail(hash)
  128. size := MustInt(r, 80, "s", "size") // size = 80*80
  129. avatar := New(hash, cacheDir)
  130. if avatar.Expired() {
  131. err := avatar.UpdateTimeout(time.Millisecond * 500)
  132. if err != nil {
  133. log.Println(err)
  134. }
  135. }
  136. if modtime, err := avatar.Modtime(); err == nil {
  137. etag := fmt.Sprintf("size(%d)", size)
  138. 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") {
  139. h := w.Header()
  140. delete(h, "Content-Type")
  141. delete(h, "Content-Length")
  142. w.WriteHeader(http.StatusNotModified)
  143. return
  144. }
  145. w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
  146. w.Header().Set("ETag", etag)
  147. }
  148. w.Header().Set("Content-Type", "image/jpeg")
  149. err := avatar.Encode(w, size)
  150. if err != nil {
  151. log.Println(err)
  152. w.WriteHeader(500)
  153. }
  154. }
  155. }
  156. func init() {
  157. http.HandleFunc("/", HttpHandler("./"))
  158. log.Fatal(http.ListenAndServe(":8001", nil))
  159. }
  160. var thunder = &Thunder{QueueSize: 10}
  161. type Thunder struct {
  162. QueueSize int // download queue size
  163. q chan *thunderTask
  164. once sync.Once
  165. }
  166. func (t *Thunder) init() {
  167. if t.QueueSize < 1 {
  168. t.QueueSize = 1
  169. }
  170. t.q = make(chan *thunderTask, t.QueueSize)
  171. for i := 0; i < t.QueueSize; i++ {
  172. go func() {
  173. for {
  174. task := <-t.q
  175. task.Fetch()
  176. }
  177. }()
  178. }
  179. }
  180. func (t *Thunder) Fetch(url string, saveFile string) error {
  181. t.once.Do(t.init)
  182. task := &thunderTask{
  183. Url: url,
  184. SaveFile: saveFile,
  185. }
  186. task.Add(1)
  187. t.q <- task
  188. task.Wait()
  189. return task.err
  190. }
  191. func (t *Thunder) GoFetch(url, saveFile string) chan error {
  192. c := make(chan error)
  193. go func() {
  194. c <- t.Fetch(url, saveFile)
  195. }()
  196. return c
  197. }
  198. // thunder download
  199. type thunderTask struct {
  200. Url string
  201. SaveFile string
  202. sync.WaitGroup
  203. err error
  204. }
  205. func (this *thunderTask) Fetch() {
  206. this.err = this.fetch()
  207. this.Done()
  208. }
  209. var client = &http.Client{}
  210. func (this *thunderTask) fetch() error {
  211. log.Println("thunder, fetch", this.Url)
  212. req, _ := http.NewRequest("GET", this.Url, nil)
  213. req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
  214. req.Header.Set("Accept-Encoding", "gzip,deflate,sdch")
  215. req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8")
  216. req.Header.Set("Cache-Control", "no-cache")
  217. 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")
  218. resp, err := client.Do(req)
  219. if err != nil {
  220. return err
  221. }
  222. defer resp.Body.Close()
  223. if resp.StatusCode != 200 {
  224. return fmt.Errorf("status code: %d", resp.StatusCode)
  225. }
  226. /*
  227. log.Println("headers:", resp.Header)
  228. switch resp.Header.Get("Content-Type") {
  229. case "image/jpeg":
  230. this.SaveFile += ".jpeg"
  231. case "image/png":
  232. this.SaveFile += ".png"
  233. }
  234. */
  235. /*
  236. imgType := resp.Header.Get("Content-Type")
  237. if imgType != "image/jpeg" && imgType != "image/png" {
  238. return errors.New("not png or jpeg")
  239. }
  240. */
  241. tmpFile := this.SaveFile + ".part" // mv to destination when finished
  242. fd, err := os.Create(tmpFile)
  243. if err != nil {
  244. return err
  245. }
  246. _, err = io.Copy(fd, resp.Body)
  247. fd.Close()
  248. if err != nil {
  249. os.Remove(tmpFile)
  250. return err
  251. }
  252. return os.Rename(tmpFile, this.SaveFile)
  253. }