diff options
author | Lunny Xiao <xiaolunwen@gmail.com> | 2014-03-24 22:30:50 +0800 |
---|---|---|
committer | Lunny Xiao <xiaolunwen@gmail.com> | 2014-03-24 22:30:50 +0800 |
commit | 0e28dcdac402b3bfc8336fe250e3418939467208 (patch) | |
tree | f16788377cf724e7d6ea28301f2876632a34d3dc /modules | |
parent | 48ea9b12f65e21c8584eb89224bda4ad6c635847 (diff) | |
parent | c9e1eb0a0d9e6bdafa158442158c762b7f188177 (diff) |
Merge branch 'master' of github.com:gogits/gogs
Diffstat (limited to 'modules')
-rw-r--r-- | modules/avatar/avatar.go | 296 | ||||
-rw-r--r-- | modules/avatar/avatar_test.go | 61 | ||||
-rw-r--r-- | modules/base/conf.go | 48 | ||||
-rw-r--r-- | modules/base/tool.go | 4 | ||||
-rw-r--r-- | modules/log/log.go | 9 | ||||
-rw-r--r-- | modules/middleware/auth.go | 2 | ||||
-rw-r--r-- | modules/middleware/repo.go | 6 |
7 files changed, 408 insertions, 18 deletions
diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go new file mode 100644 index 00000000..0ba20294 --- /dev/null +++ b/modules/avatar/avatar.go @@ -0,0 +1,296 @@ +// 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 + +/* +It is recommend to use this way + + cacheDir := "./cache" + defaultImg := "./default.jpg" + http.Handle("/avatar/", avatar.HttpHandler(cacheDir, defaultImg)) +*/ +package avatar + +import ( + "crypto/md5" + "encoding/hex" + "errors" + "fmt" + "image" + "image/jpeg" + "image/png" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/gogits/gogs/modules/log" + "github.com/nfnt/resize" +) + +var ( + gravatar = "http://www.gravatar.com/avatar" +) + +// hash email to md5 string +// keep this func in order to make this package indenpent +func HashEmail(email string) string { + h := md5.New() + h.Write([]byte(strings.ToLower(email))) + return hex.EncodeToString(h.Sum(nil)) +} + +type Avatar struct { + Hash string + AlterImage string // image path + cacheDir string // image save dir + reqParams string + imagePath string + expireDuration time.Duration +} + +func New(hash string, cacheDir string) *Avatar { + return &Avatar{ + Hash: hash, + cacheDir: cacheDir, + expireDuration: time.Minute * 10, + reqParams: url.Values{ + "d": {"retro"}, + "size": {"200"}, + "r": {"pg"}}.Encode(), + imagePath: filepath.Join(cacheDir, hash+".image"), //maybe png or jpeg + } +} + +func (this *Avatar) HasCache() bool { + 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 { + modtime, err := this.Modtime() + return err != nil || time.Since(modtime) > this.expireDuration +} + +// 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() + img, err = jpeg.Decode(fd) + if err != nil { + fd.Seek(0, os.SEEK_SET) + img, err = png.Decode(fd) + } + return + } + imgPath := this.imagePath + if !this.HasCache() { + if this.AlterImage == "" { + return errors.New("request image failed, and no alt image offered") + } + imgPath = this.AlterImage + } + img, err = decodeImageFile(imgPath) + if err != nil { + return + } + m := resize.Resize(uint(size), 0, img, resize.Lanczos3) + return jpeg.Encode(wr, m, nil) +} + +// get image from gravatar.com +func (this *Avatar) Update() { + thunder.Fetch(gravatar+"/"+this.Hash+"?"+this.reqParams, + this.imagePath) +} + +func (this *Avatar) UpdateTimeout(timeout time.Duration) error { + var err error + select { + case <-time.After(timeout): + err = fmt.Errorf("get gravatar image %s timeout", this.Hash) + case err = <-thunder.GoFetch(gravatar+"/"+this.Hash+"?"+this.reqParams, + this.imagePath): + } + return err +} + +type avatarHandler struct { + cacheDir string + altImage string +} + +func (this *avatarHandler) mustInt(r *http.Request, defaultValue int, keys ...string) int { + var v int + for _, k := range keys { + if _, err := fmt.Sscanf(r.FormValue(k), "%d", &v); err == nil { + defaultValue = v + } + } + return defaultValue +} + +func (this *avatarHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + urlPath := r.URL.Path + hash := urlPath[strings.LastIndex(urlPath, "/")+1:] + size := this.mustInt(r, 80, "s", "size") // default size = 80*80 + + avatar := New(hash, this.cacheDir) + avatar.AlterImage = this.altImage + if avatar.Expired() { + err := avatar.UpdateTimeout(time.Millisecond * 500) + if err != nil { + log.Trace("avatar update error: %v", err) + } + } + 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") + err := avatar.Encode(w, size) + if err != nil { + log.Warn("avatar encode error: %v", err) + w.WriteHeader(500) + } +} + +// http.Handle("/avatar/", avatar.HttpHandler("./cache")) +func HttpHandler(cacheDir string, defaultImgPath string) http.Handler { + return &avatarHandler{ + cacheDir: cacheDir, + altImage: defaultImgPath, + } +} + +// thunder downloader +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() +} + +var client = &http.Client{} + +func (this *thunderTask) fetch() error { + req, _ := http.NewRequest("GET", this.Url, nil) + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8") + req.Header.Set("Accept-Encoding", "gzip,deflate,sdch") + 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) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return fmt.Errorf("status code: %d", resp.StatusCode) + } + + /* + 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) + if err != nil { + return err + } + _, err = io.Copy(fd, resp.Body) + fd.Close() + if err != nil { + os.Remove(tmpFile) + return err + } + return os.Rename(tmpFile, this.SaveFile) +} diff --git a/modules/avatar/avatar_test.go b/modules/avatar/avatar_test.go new file mode 100644 index 00000000..4656d6f0 --- /dev/null +++ b/modules/avatar/avatar_test.go @@ -0,0 +1,61 @@ +// 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. +package avatar_test + +import ( + "errors" + "os" + "strconv" + "testing" + "time" + + "github.com/gogits/gogs/modules/avatar" + "github.com/gogits/gogs/modules/log" +) + +const TMPDIR = "test-avatar" + +func TestFetch(t *testing.T) { + os.Mkdir(TMPDIR, 0755) + defer os.RemoveAll(TMPDIR) + + hash := avatar.HashEmail("ssx205@gmail.com") + a := avatar.New(hash, TMPDIR) + a.UpdateTimeout(time.Millisecond * 200) +} + +func TestFetchMany(t *testing.T) { + os.Mkdir(TMPDIR, 0755) + defer os.RemoveAll(TMPDIR) + + t.Log("start") + var n = 5 + ch := make(chan bool, n) + for i := 0; i < n; i++ { + go func(i int) { + hash := avatar.HashEmail(strconv.Itoa(i) + "ssx205@gmail.com") + a := avatar.New(hash, TMPDIR) + a.Update() + t.Log("finish", hash) + ch <- true + }(i) + } + for i := 0; i < n; i++ { + <-ch + } + t.Log("end") +} + +// cat +// wget http://www.artsjournal.com/artfulmanager/wp/wp-content/uploads/2013/12/200x200xmirror_cat.jpg.pagespeed.ic.GOZSv6v1_H.jpg -O default.jpg +/* +func TestHttp(t *testing.T) { + http.Handle("/", avatar.HttpHandler("./", "default.jpg")) + http.ListenAndServe(":8001", nil) +} +*/ + +func TestLogTrace(t *testing.T) { + log.Trace("%v", errors.New("console log test")) +} diff --git a/modules/base/conf.go b/modules/base/conf.go index 19f58707..b4e0de97 100644 --- a/modules/base/conf.go +++ b/modules/base/conf.go @@ -38,6 +38,8 @@ var ( RunUser string RepoRootPath string + EnableHttpsClone bool + LogInRememberDays int CookieUserName string CookieRememberName string @@ -56,8 +58,7 @@ var ( SessionConfig *session.Config SessionManager *session.Manager - PictureService string - PictureRootPath string + PictureService string ) var Service struct { @@ -65,6 +66,7 @@ var Service struct { DisenableRegisteration bool RequireSignInView bool EnableCacheAvatar bool + NotifyMail bool ActiveCodeLives int ResetPwdCodeLives int } @@ -98,7 +100,7 @@ func newService() { Service.EnableCacheAvatar = Cfg.MustBool("service", "ENABLE_CACHE_AVATAR", false) } -func newLogService() { +func NewLogService() { // Get and check log mode. LogMode = Cfg.MustValue("log", "MODE", "console") modeSec := "log." + LogMode @@ -123,7 +125,7 @@ func newLogService() { logPath := Cfg.MustValue(modeSec, "FILE_NAME", "log/gogs.log") os.MkdirAll(path.Dir(logPath), os.ModePerm) LogConfig = fmt.Sprintf( - `{"level":%s,"filename":%s,"rotate":%v,"maxlines":%d,"maxsize",%d,"daily":%v,"maxdays":%d}`, level, + `{"level":%s,"filename":"%s","rotate":%v,"maxlines":%d,"maxsize":%d,"daily":%v,"maxdays":%d}`, level, logPath, Cfg.MustBool(modeSec, "LOG_ROTATE", true), Cfg.MustInt(modeSec, "MAX_LINES", 1000000), @@ -131,20 +133,20 @@ func newLogService() { Cfg.MustBool(modeSec, "DAILY_ROTATE", true), Cfg.MustInt(modeSec, "MAX_DAYS", 7)) case "conn": - LogConfig = fmt.Sprintf(`{"level":%s,"reconnectOnMsg":%v,"reconnect":%v,"net":%s,"addr":%s}`, level, + LogConfig = fmt.Sprintf(`{"level":"%s","reconnectOnMsg":%v,"reconnect":%v,"net":"%s","addr":"%s"}`, level, Cfg.MustBool(modeSec, "RECONNECT_ON_MSG", false), Cfg.MustBool(modeSec, "RECONNECT", false), Cfg.MustValue(modeSec, "PROTOCOL", "tcp"), Cfg.MustValue(modeSec, "ADDR", ":7020")) case "smtp": - LogConfig = fmt.Sprintf(`{"level":%s,"username":%s,"password":%s,"host":%s,"sendTos":%s,"subject":%s}`, level, + LogConfig = fmt.Sprintf(`{"level":"%s","username":"%s","password":"%s","host":"%s","sendTos":"%s","subject":"%s"}`, level, Cfg.MustValue(modeSec, "USER", "example@example.com"), Cfg.MustValue(modeSec, "PASSWD", "******"), Cfg.MustValue(modeSec, "HOST", "127.0.0.1:25"), Cfg.MustValue(modeSec, "RECEIVERS", "[]"), Cfg.MustValue(modeSec, "SUBJECT", "Diagnostic message from serve")) case "database": - LogConfig = fmt.Sprintf(`{"level":%s,"driver":%s,"conn":%s}`, level, + LogConfig = fmt.Sprintf(`{"level":"%s","driver":"%s","conn":"%s"}`, level, Cfg.MustValue(modeSec, "Driver"), Cfg.MustValue(modeSec, "CONN")) } @@ -229,6 +231,17 @@ func newRegisterMailService() { log.Info("Register Mail Service Enabled") } +func newNotifyMailService() { + if !Cfg.MustBool("service", "ENABLE_NOTIFY_MAIL") { + return + } else if MailService == nil { + log.Warn("Notify Mail Service: Mail Service is not enabled") + return + } + Service.NotifyMail = true + log.Info("Notify Mail Service Enabled") +} + func NewConfigContext() { var err error workDir, err := exeDir() @@ -246,11 +259,16 @@ func NewConfigContext() { Cfg.BlockMode = false cfgPath = filepath.Join(workDir, "custom/conf/app.ini") - if com.IsFile(cfgPath) { - if err = Cfg.AppendFiles(cfgPath); err != nil { - fmt.Printf("Cannot load config file '%s'\n", cfgPath) - os.Exit(2) - } + if !com.IsFile(cfgPath) { + fmt.Println("Custom configuration not found(custom/conf/app.ini)\n" + + "Please create it and make your own configuration!") + os.Exit(2) + + } + + if err = Cfg.AppendFiles(cfgPath); err != nil { + fmt.Printf("Cannot load config file '%s'\n", cfgPath) + os.Exit(2) } AppName = Cfg.MustValue("", "APP_NAME", "Gogs: Go Git Service") @@ -260,12 +278,13 @@ func NewConfigContext() { SecretKey = Cfg.MustValue("security", "SECRET_KEY") RunUser = Cfg.MustValue("", "RUN_USER") + EnableHttpsClone = Cfg.MustBool("security", "ENABLE_HTTPS_CLONE", false) + LogInRememberDays = Cfg.MustInt("security", "LOGIN_REMEMBER_DAYS") CookieUserName = Cfg.MustValue("security", "COOKIE_USERNAME") CookieRememberName = Cfg.MustValue("security", "COOKIE_REMEMBER_NAME") PictureService = Cfg.MustValue("picture", "SERVICE") - PictureRootPath = Cfg.MustValue("picture", "PATH") // Determine and create root git reposiroty path. RepoRootPath = Cfg.MustValue("repository", "ROOT") @@ -277,9 +296,10 @@ func NewConfigContext() { func NewServices() { newService() - newLogService() + NewLogService() newCacheService() newSessionService() newMailService() newRegisterMailService() + newNotifyMailService() } diff --git a/modules/base/tool.go b/modules/base/tool.go index b48566f5..0dec7aa8 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -102,7 +102,7 @@ func CreateTimeLimitCode(data string, minutes int, startInf interface{}) string // AvatarLink returns avatar link by given e-mail. func AvatarLink(email string) string { - return "http://1.gravatar.com/avatar/" + EncodeMd5(email) + return "/avatar/" + EncodeMd5(email) } // Seconds-based time units @@ -519,7 +519,7 @@ func ActionDesc(act Actioner, avatarLink string) string { buf.WriteString(fmt.Sprintf(TPL_COMMIT_REPO_LI, avatarLink, actUserName, repoName, commit[0], commit[0][:7], commit[1]) + "\n") } if push.Len > 3 { - buf.WriteString(fmt.Sprintf(`<div><a href="/%s/%s/commits">%d other commits >></a></div>`, actUserName, repoName, push.Len)) + buf.WriteString(fmt.Sprintf(`<div><a href="/%s/%s/commits/%s">%d other commits >></a></div>`, actUserName, repoName, branch, push.Len)) } return fmt.Sprintf(TPL_COMMIT_REPO, actUserName, actUserName, actUserName, repoName, branch, branch, actUserName, repoName, actUserName, repoName, buf.String()) diff --git a/modules/log/log.go b/modules/log/log.go index 29782fb2..0c07c7c6 100644 --- a/modules/log/log.go +++ b/modules/log/log.go @@ -11,6 +11,11 @@ import ( var logger *logs.BeeLogger +func init() { + logger = logs.NewLogger(10000) + logger.SetLogger("console", `{"level": 0}`) +} + func NewLogger(bufLen int64, mode, config string) { logger = logs.NewLogger(bufLen) logger.SetLogger(mode, config) @@ -20,6 +25,10 @@ func Trace(format string, v ...interface{}) { logger.Trace(format, v...) } +func Debug(format string, v ...interface{}) { + logger.Debug(format, v...) +} + func Info(format string, v ...interface{}) { logger.Info(format, v...) } diff --git a/modules/middleware/auth.go b/modules/middleware/auth.go index 82c3367c..64f75d75 100644 --- a/modules/middleware/auth.go +++ b/modules/middleware/auth.go @@ -21,7 +21,7 @@ type ToggleOptions struct { func Toggle(options *ToggleOptions) martini.Handler { return func(ctx *Context) { - if options.SignOutRequire && ctx.IsSigned { + if options.SignOutRequire && ctx.IsSigned && ctx.Req.RequestURI != "/" { ctx.Redirect("/") return } diff --git a/modules/middleware/repo.go b/modules/middleware/repo.go index 3864caaf..eea2570c 100644 --- a/modules/middleware/repo.go +++ b/modules/middleware/repo.go @@ -69,8 +69,12 @@ func RepoAssignment(redirect bool) martini.Handler { ctx.Repo.IsWatching = models.IsWatching(ctx.User.Id, repo.Id) } ctx.Repo.Repository = repo + scheme := "http" + if base.EnableHttpsClone { + scheme = "https" + } ctx.Repo.CloneLink.SSH = fmt.Sprintf("git@%s:%s/%s.git", base.Domain, user.LowerName, repo.LowerName) - ctx.Repo.CloneLink.HTTPS = fmt.Sprintf("https://%s/%s/%s.git", base.Domain, user.LowerName, repo.LowerName) + ctx.Repo.CloneLink.HTTPS = fmt.Sprintf("%s://%s/%s/%s.git", scheme, base.Domain, user.LowerName, repo.LowerName) ctx.Data["IsRepositoryValid"] = true ctx.Data["Repository"] = repo |