Browse Source

Merge branch 'master' of github.com:shxsun/gogs

skyblue 11 years ago
parent
commit
d945d0911b

+ 1 - 2
.gopmfile

@@ -9,13 +9,12 @@ github.com/Unknwon/com=
 github.com/Unknwon/cae=
 github.com/Unknwon/goconfig=
 github.com/dchest/scrypt=
-github.com/go-sql-driver/mysql=
-github.com/lib/pq=
 github.com/lunny/xorm=
 github.com/gogits/logs=
 github.com/gogits/binding=
 github.com/gogits/git=
 github.com/gogits/gfm=
+github.com/gogits/cache=
 
 [res]
 include=templates|public|conf

+ 11 - 0
models/repo.go

@@ -262,6 +262,17 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep
 		return err
 	}
 
+	// hook/post-update
+	pu2, err := os.OpenFile(filepath.Join(repoPath, "hooks", "post-receive"), os.O_CREATE|os.O_WRONLY, 0777)
+	if err != nil {
+		return err
+	}
+	defer pu2.Close()
+	// TODO: Windows .bat
+	if _, err = pu2.WriteString("#!/usr/bin/env bash\ngit update-server-info\n"); err != nil {
+		return err
+	}
+
 	// Initialize repository according to user's choice.
 	fileName := map[string]string{}
 	if initReadme {

+ 1 - 1
models/user.go

@@ -72,7 +72,7 @@ func (user *User) HomeLink() string {
 
 // AvatarLink returns the user gravatar link.
 func (user *User) AvatarLink() string {
-	return "http://1.gravatar.com/avatar/" + user.Avatar
+	return "/avatar/" + user.Avatar
 }
 
 // NewGitSig generates and returns the signature of given user.

+ 298 - 0
modules/avatar/avatar.go

@@ -0,0 +1,298 @@
+// 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
+package avatar
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"image"
+	"image/jpeg"
+	"image/png"
+	"io"
+	"log"
+	"net/http"
+	"net/url"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/nfnt/resize"
+)
+
+var (
+	gravatar = "http://www.gravatar.com/avatar"
+)
+
+func debug(a ...interface{}) {
+	if true {
+		log.Println(a...)
+	}
+}
+
+// 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 = errors.New("get gravatar image timeout")
+	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:]
+	//hash = HashEmail(hash)
+	size := this.mustInt(r, 80, "s", "size") // size = 80*80
+
+	avatar := New(hash, this.cacheDir)
+	avatar.AlterImage = this.altImage
+	if avatar.Expired() {
+		err := avatar.UpdateTimeout(time.Millisecond * 500)
+		if err != nil {
+			debug(err)
+			//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) // will panic when err != nil
+		debug(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 {
+	//log.Println("thunder, fetch", this.Url)
+	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)
+}

+ 56 - 0
modules/avatar/avatar_test.go

@@ -0,0 +1,56 @@
+// 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 (
+	"log"
+	"os"
+	"strconv"
+	"testing"
+	"time"
+
+	"github.com/gogits/gogs/modules/avatar"
+)
+
+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)
+
+	log.Println("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()
+			log.Println("finish", hash)
+			ch <- true
+		}(i)
+	}
+	for i := 0; i < n; i++ {
+		<-ch
+	}
+	log.Println("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)
+}
+*/

+ 14 - 12
modules/base/conf.go

@@ -43,6 +43,9 @@ var (
 	Cache        cache.Cache
 	CacheAdapter string
 	CacheConfig  string
+
+	LogMode   string
+	LogConfig string
 )
 
 var Service struct {
@@ -83,15 +86,15 @@ func newService() {
 
 func newLogService() {
 	// Get and check log mode.
-	mode := Cfg.MustValue("log", "MODE", "console")
-	modeSec := "log." + mode
+	LogMode = Cfg.MustValue("log", "MODE", "console")
+	modeSec := "log." + LogMode
 	if _, err := Cfg.GetSection(modeSec); err != nil {
-		fmt.Printf("Unknown log mode: %s\n", mode)
+		fmt.Printf("Unknown log mode: %s\n", LogMode)
 		os.Exit(2)
 	}
 
 	// Log level.
-	levelName := Cfg.MustValue("log."+mode, "LEVEL", "Trace")
+	levelName := Cfg.MustValue("log."+LogMode, "LEVEL", "Trace")
 	level, ok := logLevels[levelName]
 	if !ok {
 		fmt.Printf("Unknown log level: %s\n", levelName)
@@ -99,14 +102,13 @@ func newLogService() {
 	}
 
 	// Generate log configuration.
-	var config string
-	switch mode {
+	switch LogMode {
 	case "console":
-		config = fmt.Sprintf(`{"level":%s}`, level)
+		LogConfig = fmt.Sprintf(`{"level":%s}`, level)
 	case "file":
 		logPath := Cfg.MustValue(modeSec, "FILE_NAME", "log/gogs.log")
 		os.MkdirAll(path.Dir(logPath), os.ModePerm)
-		config = fmt.Sprintf(
+		LogConfig = fmt.Sprintf(
 			`{"level":%s,"filename":%s,"rotate":%v,"maxlines":%d,"maxsize",%d,"daily":%v,"maxdays":%d}`, level,
 			logPath,
 			Cfg.MustBool(modeSec, "LOG_ROTATE", true),
@@ -115,13 +117,13 @@ func newLogService() {
 			Cfg.MustBool(modeSec, "DAILY_ROTATE", true),
 			Cfg.MustInt(modeSec, "MAX_DAYS", 7))
 	case "conn":
-		config = 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":
-		config = 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"),
@@ -129,8 +131,8 @@ func newLogService() {
 			Cfg.MustValue(modeSec, "SUBJECT", "Diagnostic message from serve"))
 	}
 
-	log.NewLogger(Cfg.MustInt64("log", "BUFFER_LEN", 10000), mode, config)
-	log.Info("Log Mode: %s(%s)", strings.Title(mode), levelName)
+	log.NewLogger(Cfg.MustInt64("log", "BUFFER_LEN", 10000), LogMode, LogConfig)
+	log.Info("Log Mode: %s(%s)", strings.Title(LogMode), levelName)
 }
 
 func newCacheService() {

+ 1 - 1
modules/base/tool.go

@@ -98,7 +98,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

BIN
public/img/avatar/default.jpg


+ 3 - 0
routers/admin/admin.go

@@ -70,5 +70,8 @@ func Config(ctx *middleware.Context) {
 	ctx.Data["CacheAdapter"] = base.CacheAdapter
 	ctx.Data["CacheConfig"] = base.CacheConfig
 
+	ctx.Data["LogMode"] = base.LogMode
+	ctx.Data["LogConfig"] = base.LogConfig
+
 	ctx.HTML(200, "admin/config")
 }

+ 24 - 0
routers/repo/single.go

@@ -11,6 +11,7 @@ import (
 	"github.com/codegangsta/martini"
 
 	"github.com/gogits/git"
+	"github.com/gogits/webdav"
 
 	"github.com/gogits/gogs/models"
 	"github.com/gogits/gogs/modules/base"
@@ -192,6 +193,29 @@ func Single(ctx *middleware.Context, params martini.Params) {
 	ctx.HTML(200, "repo/single")
 }
 
+func Http(ctx *middleware.Context, params martini.Params) {
+	/*if !ctx.Repo.IsValid {
+		return
+	}*/
+
+	// TODO: access check
+
+	username := params["username"]
+	reponame := params["reponame"]
+	if strings.HasSuffix(reponame, ".git") {
+		reponame = reponame[:len(reponame)-4]
+	}
+
+	prefix := path.Join("/", username, params["reponame"])
+	server := &webdav.Server{
+		Fs:         webdav.Dir(models.RepoPath(username, reponame)),
+		TrimPrefix: prefix,
+		Listings:   true,
+	}
+
+	server.ServeHTTP(ctx.ResponseWriter, ctx.Req)
+}
+
 func Setting(ctx *middleware.Context, params martini.Params) {
 	if !ctx.Repo.IsOwner {
 		ctx.Error(404)

+ 15 - 1
templates/admin/config.tmpl

@@ -71,7 +71,21 @@
 
             <div class="panel-body">
                 <div><b>Cache Adapter:</b> {{.CacheAdapter}}</div>
-                <div><b>Cache Config:</b> <code>{{.CacheConfig}}</code></div>
+                <div><b>Cache Config:</b></div>
+                <div style="padding-top: 5px;"><pre>{{.CacheConfig}}</pre></div>
+            </div>
+        </div>
+
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                Log Configuration
+            </div>
+
+            <div class="panel-body">
+                <div><b>Log Mode:</b> {{.LogMode}}</div>
+                <div><b>Log Config:</b></div>
+                <div style="padding-top: 5px;"><pre>{{.LogConfig}}</pre></div>
+                
             </div>
         </div>
     </div>

+ 6 - 1
web.go

@@ -18,6 +18,7 @@ import (
 
 	"github.com/gogits/gogs/models"
 	"github.com/gogits/gogs/modules/auth"
+	"github.com/gogits/gogs/modules/avatar"
 	"github.com/gogits/gogs/modules/base"
 	"github.com/gogits/gogs/modules/log"
 	"github.com/gogits/gogs/modules/mailer"
@@ -114,6 +115,9 @@ func runWeb(*cli.Context) {
 
 	m.Get("/help", routers.Help)
 
+	avatarHandler := avatar.HttpHandler("public/img/avatar", "public/img/avatar/default.jpg")
+	m.Get("/avatar/:hash", avatarHandler.ServeHTTP)
+
 	adminReq := middleware.AdminRequire()
 	m.Get("/admin", reqSignIn, adminReq, admin.Dashboard)
 	m.Get("/admin/users", reqSignIn, adminReq, admin.Users)
@@ -136,9 +140,10 @@ func runWeb(*cli.Context) {
 		ignSignIn, middleware.RepoAssignment(true), repo.Single)
 	m.Get("/:username/:reponame/commit/:commitid/**", ignSignIn, middleware.RepoAssignment(true), repo.Single)
 	m.Get("/:username/:reponame/commit/:commitid", ignSignIn, middleware.RepoAssignment(true), repo.Single)
-
 	m.Get("/:username/:reponame", ignSignIn, middleware.RepoAssignment(true), repo.Single)
 
+	m.Any("/:username/:reponame/**", ignSignIn, repo.Http)
+
 	if martini.Env == martini.Dev {
 		m.Get("/template/**", dev.TemplatePreview)
 	}