diff --git a/README.md b/README.md index 176815d0b9..f14c4c1baf 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,6 @@ There are 5 ways to install Gogs: - Router and middleware mechanism of [Macaron](https://github.com/Unknwon/macaron). - Mail Service, modules design is inspired by [WeTalk](https://github.com/beego/wetalk). - System Monitor Status is inspired by [GoBlog](https://github.com/fuxiaohei/goblog). -- Usage and modification from [beego](http://beego.me) modules. - Thanks [lavachen](http://www.lavachen.cn/) and [Rocker](http://weibo.com/rocker1989) for designing Logo. - Thanks [gobuild.io](http://gobuild.io) for providing binary compile and download service. - Thanks [Crowdin](https://crowdin.com/project/gogs) for providing open source translation plan. diff --git a/README_ZH.md b/README_ZH.md index ae60c36b34..7adc5dc566 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -59,8 +59,7 @@ Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自 ## 特别鸣谢 -- [Macaron](https://github.com/Unknwon/macaron) 的路由与中间件机制。 -- [beego](http://beego.me) 模块的使用与修改。 +- 基于 [Macaron](https://github.com/Unknwon/macaron) 的路由与中间件机制。 - 基于 [WeTalk](https://github.com/beego/wetalk) 修改的邮件服务和模块设计。 - 基于 [GoBlog](https://github.com/fuxiaohei/goblog) 修改的系统监视状态。 - 感谢 [gobuild.io](http://gobuild.io) 提供二进制编译与下载服务。 diff --git a/cmd/web.go b/cmd/web.go index de222d6fae..c2017a2850 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -94,6 +94,13 @@ func newMacaron() *macaron.Macaron { SkipLogging: !setting.DisableRouterLog, }, )) + m.Use(macaron.Static( + setting.AvatarUploadPath, + macaron.StaticOptions{ + Prefix: "avatars", + SkipLogging: !setting.DisableRouterLog, + }, + )) m.Use(macaron.Renderer(macaron.RenderOptions{ Directory: path.Join(setting.StaticRootPath, "templates"), Funcs: []template.FuncMap{base.TemplateFuncs}, @@ -214,6 +221,7 @@ func runWeb(*cli.Context) { m.Group("/user/settings", func() { m.Get("", user.Settings) m.Post("", bindIgnErr(auth.UpdateProfileForm{}), user.SettingsPost) + m.Post("/avatar", binding.MultipartForm(auth.UploadAvatarForm{}), user.SettingsAvatar) m.Get("/password", user.SettingsPassword) m.Post("/password", bindIgnErr(auth.ChangePasswordForm{}), user.SettingsPasswordPost) m.Get("/ssh", user.SettingsSSHKeys) diff --git a/conf/app.ini b/conf/app.ini index dbae8a4fd0..6374c2423f 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -167,6 +167,7 @@ SESSION_LIFE_TIME = 86400 [picture] ; The place to picture data, either "server" or "qiniu", default is "server" SERVICE = server +AVATAR_UPLOAD_PATH = data/avatars ; Chinese users can choose "duoshuo" GRAVATAR_SOURCE = gravatar DISABLE_GRAVATAR = false diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini index 3cfc967158..6334966602 100644 --- a/conf/locale/locale_en-US.ini +++ b/conf/locale/locale_en-US.ini @@ -173,6 +173,7 @@ target_branch_not_exist = Target branch does not exist [user] change_avatar = Change your avatar at gravatar.com +change_custom_avatar = Change your avatar in settings join_on = Joined on repositories = Repositories activity = Public Activity @@ -201,6 +202,10 @@ change_username = Username Changed change_username_desc = Username has been changed, do you want to continue? This will affect all links relate to your account. continue = Continue cancel = Cancel +choose_new_avatar = Choose new avatar +upload_avatar = Upload Avatar +uploaded_avatar_not_a_image = Uploaded file is not a image +upload_avatar_success = Your new avatar has been uploaded successfully. change_password = Change Password old_password = Current Password diff --git a/gogs.go b/gogs.go index 656b1a2cc4..4027659863 100644 --- a/gogs.go +++ b/gogs.go @@ -17,7 +17,7 @@ import ( "github.com/gogits/gogs/modules/setting" ) -const APP_VER = "0.5.8.1119 Beta" +const APP_VER = "0.5.8.1121 Beta" func init() { runtime.GOMAXPROCS(runtime.NumCPU()) diff --git a/models/action.go b/models/action.go index 334d143d2c..269fd753e8 100644 --- a/models/action.go +++ b/models/action.go @@ -58,6 +58,7 @@ type Action struct { ActUserId int64 // Action user id. ActUserName string // Action user name. ActEmail string + ActAvatar string `xorm:"-"` RepoId int64 RepoUserName string RepoName string diff --git a/models/user.go b/models/user.go index 31f4a289ff..1337ca2350 100644 --- a/models/user.go +++ b/models/user.go @@ -5,17 +5,21 @@ package models import ( + "bytes" "container/list" "crypto/sha256" "encoding/hex" "errors" "fmt" + "image" + "image/jpeg" "os" "path/filepath" "strings" "time" "github.com/Unknwon/com" + "github.com/nfnt/resize" "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/git" @@ -45,33 +49,40 @@ var ( // User represents the object of individual and member of organization. type User struct { - Id int64 - LowerName string `xorm:"UNIQUE NOT NULL"` - Name string `xorm:"UNIQUE NOT NULL"` - FullName string - Email string `xorm:"UNIQUE NOT NULL"` - Passwd string `xorm:"NOT NULL"` - LoginType LoginType - LoginSource int64 `xorm:"NOT NULL DEFAULT 0"` - LoginName string - Type UserType - Orgs []*User `xorm:"-"` - Repos []*Repository `xorm:"-"` + Id int64 + LowerName string `xorm:"UNIQUE NOT NULL"` + Name string `xorm:"UNIQUE NOT NULL"` + FullName string + Email string `xorm:"UNIQUE NOT NULL"` + Passwd string `xorm:"NOT NULL"` + LoginType LoginType + LoginSource int64 `xorm:"NOT NULL DEFAULT 0"` + LoginName string + Type UserType + Orgs []*User `xorm:"-"` + Repos []*Repository `xorm:"-"` + Location string + Website string + Rands string `xorm:"VARCHAR(10)"` + Salt string `xorm:"VARCHAR(10)"` + Created time.Time `xorm:"CREATED"` + Updated time.Time `xorm:"UPDATED"` + + // Permissions. + IsActive bool + IsAdmin bool + AllowGitHook bool + + // Avatar. + Avatar string `xorm:"VARCHAR(2048) NOT NULL"` + AvatarEmail string `xorm:"NOT NULL"` + UseCustomAvatar bool + + // Counters. NumFollowers int NumFollowings int NumStars int NumRepos int - Avatar string `xorm:"VARCHAR(2048) NOT NULL"` - AvatarEmail string `xorm:"NOT NULL"` - Location string - Website string - IsActive bool - IsAdmin bool - AllowGitHook bool - Rands string `xorm:"VARCHAR(10)"` - Salt string `xorm:"VARCHAR(10)"` - Created time.Time `xorm:"CREATED"` - Updated time.Time `xorm:"UPDATED"` // For organization. Description string @@ -96,9 +107,12 @@ func (u *User) HomeLink() string { // AvatarLink returns user gravatar link. func (u *User) AvatarLink() string { - if setting.DisableGravatar { + switch { + case u.UseCustomAvatar: + return setting.AppSubUrl + "/avatars/" + com.ToStr(u.Id) + case setting.DisableGravatar: return setting.AppSubUrl + "/img/avatar_default.jpg" - } else if setting.Service.EnableCacheAvatar { + case setting.Service.EnableCacheAvatar: return setting.AppSubUrl + "/avatar/" + u.Avatar } return setting.GravatarSource + u.Avatar @@ -126,6 +140,43 @@ func (u *User) ValidtePassword(passwd string) bool { return u.Passwd == newUser.Passwd } +// UploadAvatar saves custom avatar for user. +// FIXME: splite uploads to different subdirs in case we have massive users. +func (u *User) UploadAvatar(data []byte) error { + savePath := filepath.Join(setting.AvatarUploadPath, com.ToStr(u.Id)) + u.UseCustomAvatar = true + + img, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + return err + } + m := resize.Resize(200, 200, img, resize.NearestNeighbor) + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + if _, err = sess.Id(u.Id).AllCols().Update(u); err != nil { + sess.Rollback() + return err + } + + fw, err := os.Create(savePath) + if err != nil { + sess.Rollback() + return err + } + defer fw.Close() + if err = jpeg.Encode(fw, m, nil); err != nil { + sess.Rollback() + return err + } + + return sess.Commit() +} + // IsOrganization returns true if user is actually a organization. func (u *User) IsOrganization() bool { return u.Type == ORGANIZATION @@ -517,41 +568,38 @@ func GetUserIdsByNames(names []string) []int64 { // UserCommit represtns a commit with validation of user. type UserCommit struct { - UserName string + User *User *git.Commit } // ValidateCommitWithEmail chceck if author's e-mail of commit is corresponsind to a user. -func ValidateCommitWithEmail(c *git.Commit) (uname string) { +func ValidateCommitWithEmail(c *git.Commit) *User { u, err := GetUserByEmail(c.Author.Email) - if err == nil { - uname = u.Name + if err != nil { + return nil } - return uname + return u } // ValidateCommitsWithEmails checks if authors' e-mails of commits are corresponding to users. func ValidateCommitsWithEmails(oldCommits *list.List) *list.List { - emails := map[string]string{} + emails := map[string]*User{} newCommits := list.New() e := oldCommits.Front() for e != nil { c := e.Value.(*git.Commit) - uname := "" + var u *User if v, ok := emails[c.Author.Email]; !ok { - u, err := GetUserByEmail(c.Author.Email) - if err == nil { - uname = u.Name - } - emails[c.Author.Email] = uname + u, _ = GetUserByEmail(c.Author.Email) + emails[c.Author.Email] = u } else { - uname = v + u = v } newCommits.PushBack(UserCommit{ - UserName: uname, - Commit: c, + User: u, + Commit: c, }) e = e.Next() } diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go index 6046a8d1ee..afdd8be0c9 100644 --- a/modules/auth/user_form.go +++ b/modules/auth/user_form.go @@ -5,6 +5,8 @@ package auth import ( + "mime/multipart" + "github.com/Unknwon/macaron" "github.com/macaron-contrib/binding" ) @@ -86,6 +88,14 @@ func (f *UpdateProfileForm) Validate(ctx *macaron.Context, errs binding.Errors) return validate(errs, ctx.Data, f, ctx.Locale) } +type UploadAvatarForm struct { + Avatar *multipart.FileHeader `form:"avatar" binding:"Required"` +} + +func (f *UploadAvatarForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { + return validate(errs, ctx.Data, f, ctx.Locale) +} + type ChangePasswordForm struct { OldPassword string `form:"old_password" binding:"Required;MinSize(6);MaxSize(255)"` Password string `form:"password" binding:"Required;MinSize(6);MaxSize(255)"` diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index fb198da149..144fda387e 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -121,7 +121,7 @@ func (this *Avatar) Encode(wr io.Writer, size int) (err error) { if img, err = decodeImageFile(imgPath); err != nil { return } - m := resize.Resize(uint(size), 0, img, resize.Lanczos3) + m := resize.Resize(uint(size), 0, img, resize.NearestNeighbor) return jpeg.Encode(wr, m, nil) } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 0f293b6905..49bd11c4c2 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -66,9 +66,10 @@ var ( ScriptType string // Picture settings. - PictureService string - GravatarSource string - DisableGravatar bool + PictureService string + AvatarUploadPath string + GravatarSource string + DisableGravatar bool // Log settings. LogRootPath string @@ -259,6 +260,9 @@ func NewConfigContext() { ScriptType = Cfg.MustValue("repository", "SCRIPT_TYPE", "bash") PictureService = Cfg.MustValueRange("picture", "SERVICE", "server", []string{"server"}) + AvatarUploadPath = Cfg.MustValue("picture", "AVATAR_UPLOAD_PATH", "data/avatars") + os.MkdirAll(AvatarUploadPath, os.ModePerm) + switch Cfg.MustValue("picture", "GRAVATAR_SOURCE", "gravatar") { case "duoshuo": GravatarSource = "http://gravatar.duoshuo.com/avatar/" diff --git a/routers/user/home.go b/routers/user/home.go index 031872fca9..1bb9701104 100644 --- a/routers/user/home.go +++ b/routers/user/home.go @@ -100,6 +100,13 @@ func Dashboard(ctx *middleware.Context) { continue } } + // FIXME: cache results? + u, err := models.GetUserByName(act.ActUserName) + if err != nil { + ctx.Handle(500, "GetUserByName", err) + return + } + act.ActAvatar = u.AvatarLink() feeds = append(feeds, act) } ctx.Data["Feeds"] = feeds diff --git a/routers/user/setting.go b/routers/user/setting.go index bb0fa9103e..559e10fd89 100644 --- a/routers/user/setting.go +++ b/routers/user/setting.go @@ -5,6 +5,7 @@ package user import ( + "io/ioutil" "strings" "github.com/Unknwon/com" @@ -83,6 +84,34 @@ func SettingsPost(ctx *middleware.Context, form auth.UpdateProfileForm) { ctx.Redirect(setting.AppSubUrl + "/user/settings") } +// FIXME: limit size. +func SettingsAvatar(ctx *middleware.Context, form auth.UploadAvatarForm) { + defer ctx.Redirect(setting.AppSubUrl + "/user/settings") + + if form.Avatar != nil { + fr, err := form.Avatar.Open() + if err != nil { + ctx.Flash.Error(err.Error()) + return + } + + data, err := ioutil.ReadAll(fr) + if err != nil { + ctx.Flash.Error(err.Error()) + return + } + if _, ok := base.IsImageFile(data); !ok { + ctx.Flash.Error(ctx.Tr("settings.uploaded_avatar_not_a_image")) + return + } + if err = ctx.User.UploadAvatar(data); err != nil { + ctx.Flash.Error(err.Error()) + return + } + ctx.Flash.Success(ctx.Tr("settings.upload_avatar_success")) + } +} + func SettingsPassword(ctx *middleware.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsUserSettings"] = true diff --git a/templates/.VERSION b/templates/.VERSION index 92a14ffc74..5de719a000 100644 --- a/templates/.VERSION +++ b/templates/.VERSION @@ -1 +1 @@ -0.5.8.1119 Beta \ No newline at end of file +0.5.8.1121 Beta \ No newline at end of file diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl index d48d61a5a7..eb819e387f 100644 --- a/templates/repo/commits_table.tmpl +++ b/templates/repo/commits_table.tmpl @@ -24,7 +24,13 @@ {{$r := List .Commits}} {{range $r}}
diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 46bc99bda4..44c2212383 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -4,7 +4,11 @@