2014-03-23 14:13:23 +04:00
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// for www.gravatar.com image cache
2014-03-24 17:16:00 +04:00
/ *
It is recommend to use this way
cacheDir := "./cache"
defaultImg := "./default.jpg"
2014-03-26 17:26:31 +04:00
http . Handle ( "/avatar/" , avatar . CacheServer ( cacheDir , defaultImg ) )
2014-03-24 17:16:00 +04:00
* /
2014-03-23 08:24:09 +04:00
package avatar
import (
"crypto/md5"
"encoding/hex"
2014-03-23 11:55:27 +04:00
"errors"
2014-03-23 08:24:09 +04:00
"fmt"
2014-03-23 11:55:27 +04:00
"image"
"image/jpeg"
"image/png"
2014-03-23 08:24:09 +04:00
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
2014-03-23 11:55:27 +04:00
"github.com/nfnt/resize"
2014-03-25 20:12:27 +04:00
"github.com/gogits/gogs/modules/log"
2014-03-23 08:24:09 +04:00
)
2014-03-23 11:55:27 +04:00
var (
2014-03-23 14:13:23 +04:00
gravatar = "http://www.gravatar.com/avatar"
2014-03-23 11:55:27 +04:00
)
2014-03-23 08:24:09 +04:00
// hash email to md5 string
2014-03-23 14:13:23 +04:00
// keep this func in order to make this package indenpent
2014-03-23 08:24:09 +04:00
func HashEmail ( email string ) string {
h := md5 . New ( )
h . Write ( [ ] byte ( strings . ToLower ( email ) ) )
return hex . EncodeToString ( h . Sum ( nil ) )
}
2014-03-28 03:42:10 +04:00
// Avatar represents the avatar object.
2014-03-23 08:24:09 +04:00
type Avatar struct {
2014-03-23 11:55:27 +04:00
Hash string
2014-03-23 14:13:23 +04:00
AlterImage string // image path
2014-03-23 11:55:27 +04:00
cacheDir string // image save dir
reqParams string
imagePath string
expireDuration time . Duration
2014-03-23 08:24:09 +04:00
}
func New ( hash string , cacheDir string ) * Avatar {
return & Avatar {
2014-03-23 11:55:27 +04:00
Hash : hash ,
cacheDir : cacheDir ,
expireDuration : time . Minute * 10 ,
2014-03-23 08:24:09 +04:00
reqParams : url . Values {
"d" : { "retro" } ,
"size" : { "200" } ,
"r" : { "pg" } } . Encode ( ) ,
2014-03-23 11:55:27 +04:00
imagePath : filepath . Join ( cacheDir , hash + ".image" ) , //maybe png or jpeg
}
}
2014-03-23 14:13:23 +04:00
func ( this * Avatar ) HasCache ( ) bool {
2014-03-23 11:55:27 +04:00
fileInfo , err := os . Stat ( this . imagePath )
return err == nil && fileInfo . Mode ( ) . IsRegular ( )
}
func ( this * Avatar ) Modtime ( ) ( modtime time . Time , err error ) {
fileInfo , err := os . Stat ( this . imagePath )
if err != nil {
return
}
return fileInfo . ModTime ( ) , nil
}
func ( this * Avatar ) Expired ( ) bool {
2014-03-23 14:13:23 +04:00
modtime , err := this . Modtime ( )
return err != nil || time . Since ( modtime ) > this . expireDuration
2014-03-23 11:55:27 +04:00
}
// default image format: jpeg
func ( this * Avatar ) Encode ( wr io . Writer , size int ) ( err error ) {
var img image . Image
decodeImageFile := func ( file string ) ( img image . Image , err error ) {
fd , err := os . Open ( file )
if err != nil {
return
}
defer fd . Close ( )
2014-03-28 03:42:10 +04:00
if img , err = jpeg . Decode ( fd ) ; err != nil {
2014-03-23 11:55:27 +04:00
fd . Seek ( 0 , os . SEEK_SET )
img , err = png . Decode ( fd )
}
return
}
imgPath := this . imagePath
2014-03-23 14:13:23 +04:00
if ! this . HasCache ( ) {
if this . AlterImage == "" {
return errors . New ( "request image failed, and no alt image offered" )
}
imgPath = this . AlterImage
2014-03-23 11:55:27 +04:00
}
2014-03-28 03:42:10 +04:00
if img , err = decodeImageFile ( imgPath ) ; err != nil {
2014-03-23 11:55:27 +04:00
return
2014-03-23 08:24:09 +04:00
}
2014-03-23 11:55:27 +04:00
m := resize . Resize ( uint ( size ) , 0 , img , resize . Lanczos3 )
return jpeg . Encode ( wr , m , nil )
2014-03-23 08:24:09 +04:00
}
// get image from gravatar.com
func ( this * Avatar ) Update ( ) {
thunder . Fetch ( gravatar + "/" + this . Hash + "?" + this . reqParams ,
2014-03-23 11:55:27 +04:00
this . imagePath )
2014-03-23 08:24:09 +04:00
}
2014-03-28 03:42:10 +04:00
func ( this * Avatar ) UpdateTimeout ( timeout time . Duration ) ( err error ) {
2014-03-23 08:24:09 +04:00
select {
case <- time . After ( timeout ) :
2014-03-24 17:16:00 +04:00
err = fmt . Errorf ( "get gravatar image %s timeout" , this . Hash )
2014-03-23 11:55:27 +04:00
case err = <- thunder . GoFetch ( gravatar + "/" + this . Hash + "?" + this . reqParams ,
this . imagePath ) :
2014-03-23 08:24:09 +04:00
}
2014-03-23 11:55:27 +04:00
return err
}
2014-03-26 17:26:31 +04:00
type service struct {
2014-03-23 14:13:23 +04:00
cacheDir string
altImage string
2014-03-23 11:55:27 +04:00
}
2014-03-28 03:42:10 +04:00
func ( this * service ) mustInt ( r * http . Request , defaultValue int , keys ... string ) ( v int ) {
2014-03-23 14:13:23 +04:00
for _ , k := range keys {
if _ , err := fmt . Sscanf ( r . FormValue ( k ) , "%d" , & v ) ; err == nil {
defaultValue = v
2014-03-23 11:55:27 +04:00
}
}
2014-03-23 14:13:23 +04:00
return defaultValue
}
2014-03-23 11:55:27 +04:00
2014-03-26 17:26:31 +04:00
func ( this * service ) ServeHTTP ( w http . ResponseWriter , r * http . Request ) {
2014-03-23 14:13:23 +04:00
urlPath := r . URL . Path
hash := urlPath [ strings . LastIndex ( urlPath , "/" ) + 1 : ]
2014-03-24 17:16:00 +04:00
size := this . mustInt ( r , 80 , "s" , "size" ) // default size = 80*80
2014-03-23 14:13:23 +04:00
avatar := New ( hash , this . cacheDir )
avatar . AlterImage = this . altImage
if avatar . Expired ( ) {
2014-04-16 04:01:20 +04:00
if err := avatar . UpdateTimeout ( time . Millisecond * 1000 ) ; err != nil {
2014-03-24 17:16:00 +04:00
log . Trace ( "avatar update error: %v" , err )
2014-04-16 04:01:20 +04:00
return
2014-03-23 11:55:27 +04:00
}
}
2014-03-23 14:13:23 +04:00
if modtime , err := avatar . Modtime ( ) ; err == nil {
etag := fmt . Sprintf ( "size(%d)" , size )
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" ) {
h := w . Header ( )
delete ( h , "Content-Type" )
delete ( h , "Content-Length" )
w . WriteHeader ( http . StatusNotModified )
return
}
w . Header ( ) . Set ( "Last-Modified" , modtime . UTC ( ) . Format ( http . TimeFormat ) )
w . Header ( ) . Set ( "ETag" , etag )
}
w . Header ( ) . Set ( "Content-Type" , "image/jpeg" )
2014-03-28 03:42:10 +04:00
if err := avatar . Encode ( w , size ) ; err != nil {
2014-03-24 17:16:00 +04:00
log . Warn ( "avatar encode error: %v" , err )
2014-03-23 14:13:23 +04:00
w . WriteHeader ( 500 )
}
2014-03-23 11:55:27 +04:00
}
2014-03-26 17:26:31 +04:00
// http.Handle("/avatar/", avatar.CacheServer("./cache"))
func CacheServer ( cacheDir string , defaultImgPath string ) http . Handler {
return & service {
2014-03-23 14:13:23 +04:00
cacheDir : cacheDir ,
altImage : defaultImgPath ,
}
2014-03-23 08:24:09 +04:00
}
2014-03-23 14:13:23 +04:00
// thunder downloader
2014-03-23 08:24:09 +04:00
var thunder = & Thunder { QueueSize : 10 }
type Thunder struct {
QueueSize int // download queue size
q chan * thunderTask
once sync . Once
}
func ( t * Thunder ) init ( ) {
if t . QueueSize < 1 {
t . QueueSize = 1
}
t . q = make ( chan * thunderTask , t . QueueSize )
for i := 0 ; i < t . QueueSize ; i ++ {
go func ( ) {
for {
task := <- t . q
task . Fetch ( )
}
} ( )
}
}
func ( t * Thunder ) Fetch ( url string , saveFile string ) error {
t . once . Do ( t . init )
task := & thunderTask {
Url : url ,
SaveFile : saveFile ,
}
task . Add ( 1 )
t . q <- task
task . Wait ( )
return task . err
}
func ( t * Thunder ) GoFetch ( url , saveFile string ) chan error {
c := make ( chan error )
go func ( ) {
c <- t . Fetch ( url , saveFile )
} ( )
return c
}
// thunder download
type thunderTask struct {
Url string
SaveFile string
sync . WaitGroup
err error
}
func ( this * thunderTask ) Fetch ( ) {
this . err = this . fetch ( )
this . Done ( )
}
2014-03-23 11:55:27 +04:00
var client = & http . Client { }
2014-03-23 08:24:09 +04:00
func ( this * thunderTask ) fetch ( ) error {
2014-04-16 04:01:20 +04:00
log . Debug ( "avatar.fetch(fetch new avatar): %s" , this . Url )
2014-03-23 11:55:27 +04:00
req , _ := http . NewRequest ( "GET" , this . Url , nil )
2014-03-25 14:34:57 +04:00
req . Header . Set ( "Accept" , "text/html,application/xhtml+xml,application/xml;q=0.9,image/jpeg,image/png,*/*;q=0.8" )
req . Header . Set ( "Accept-Encoding" , "deflate,sdch" )
2014-03-23 11:55:27 +04:00
req . Header . Set ( "Accept-Language" , "zh-CN,zh;q=0.8" )
req . Header . Set ( "Cache-Control" , "no-cache" )
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" )
resp , err := client . Do ( req )
2014-03-23 08:24:09 +04:00
if err != nil {
return err
}
defer resp . Body . Close ( )
if resp . StatusCode != 200 {
return fmt . Errorf ( "status code: %d" , resp . StatusCode )
}
2014-03-23 11:55:27 +04:00
/ *
log . Println ( "headers:" , resp . Header )
switch resp . Header . Get ( "Content-Type" ) {
case "image/jpeg" :
this . SaveFile += ".jpeg"
case "image/png" :
this . SaveFile += ".png"
}
* /
/ *
imgType := resp . Header . Get ( "Content-Type" )
if imgType != "image/jpeg" && imgType != "image/png" {
return errors . New ( "not png or jpeg" )
}
* /
tmpFile := this . SaveFile + ".part" // mv to destination when finished
fd , err := os . Create ( tmpFile )
2014-03-23 08:24:09 +04:00
if err != nil {
return err
}
_ , err = io . Copy ( fd , resp . Body )
2014-03-23 11:55:27 +04:00
fd . Close ( )
2014-03-23 08:24:09 +04:00
if err != nil {
2014-03-23 11:55:27 +04:00
os . Remove ( tmpFile )
2014-03-23 08:24:09 +04:00
return err
}
2014-03-23 11:55:27 +04:00
return os . Rename ( tmpFile , this . SaveFile )
2014-03-23 08:24:09 +04:00
}