From 303fa37b608a4925f5a0137c89e20a906b3d9fdb Mon Sep 17 00:00:00 2001 From: Sergey Dryabzhinsky Date: Sun, 17 Jun 2018 15:21:52 +0300 Subject: [PATCH] repo: support avatars (#5221) * First code for repository avatars * Last code for repository avatars - add new option for repo avatars location on filesystem - add route catch in web - add new fields to repo model - add migration - update settings handlers - update repo header template * Update locale messages * Add repo avatars to home page * Add repo avatars to organization right panel * Show repo avatars in repo list * Remove AvatarEamil field, remove Gravatar support, use generic locale messages * Fix migration * Fix seed and not used tool * Revert public css changes, add them to less files * Latest lessc (2.6.0) don't put result into file but output to stdout So redirect output to file * Simplify things: - migration don't needed, and table changes too - just upload file to repo avatar storage - or generate random image * Fix repo image seed - name not unique * Get rid of not needed model fields * Class value is enough, remove height attribute * Don't generate random avatar for repository - use html and semantic ui icons if no avatar found * Update styles and templates for repo - use repo icon as default avatar - use globe icon for public repos - add micro style for repo avatars at dashboard * Remvoe redundant empty line * Fix nl2br filter - must return string * Fix css style for micro-repo-avatar in dashboard list * Remove `|len`, works fine w/o it. * Update after review 2: - use static route for repository avatar - format images settings block in settings * Update after review 2: - no random avatar for repo * Update after review 2: - no random avatar for repo 2 - update imports - update UploadAvatar* functions * Update after review 2: - update templates * Fix trace call * Remove unused immport since we use static route for repo avatars. --- Makefile | 2 +- cmd/web.go | 10 ++++ conf/app.ini | 2 + models/repo.go | 62 +++++++++++++++++++++++++ pkg/setting/setting.go | 16 +++++-- public/less/_dashboard.less | 18 +++++-- routes/repo/setting.go | 58 ++++++++++++++++++++++- templates/explore/repo_list.tmpl | 11 ++++- templates/org/team/repositories.tmpl | 1 + templates/repo/header.tmpl | 3 +- templates/repo/settings/options.tmpl | 15 ++++++ templates/user/dashboard/dashboard.tmpl | 8 +++- 12 files changed, 191 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index e2c08489e..9834ddf44 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,7 @@ pkg/bindata/bindata.go: $(DATA_FILES) less: public/css/gogs.css public/css/gogs.css: $(LESS_FILES) - lessc $< $@ + lessc $< >$@ clean: go clean -i ./... diff --git a/cmd/web.go b/cmd/web.go index dc8937f9a..cd4e49cbc 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -100,6 +100,13 @@ func newMacaron() *macaron.Macaron { SkipLogging: setting.DisableRouterLog, }, )) + m.Use(macaron.Static( + setting.RepositoryAvatarUploadPath, + macaron.StaticOptions{ + Prefix: "repo-avatars", + SkipLogging: setting.DisableRouterLog, + }, + )) funcMap := template.NewFuncMap() m.Use(macaron.Renderer(macaron.RenderOptions{ @@ -419,6 +426,9 @@ func runWeb(c *cli.Context) error { m.Group("/settings", func() { m.Combo("").Get(repo.Settings). Post(bindIgnErr(form.RepoSetting{}), repo.SettingsPost) + m.Combo("/avatar").Get(repo.SettingsAvatar). + Post(binding.MultipartForm(form.Avatar{}), repo.SettingsAvatarPost) + m.Post("/avatar/delete", repo.SettingsDeleteAvatar) m.Group("/collaboration", func() { m.Combo("").Get(repo.SettingsCollaboration).Post(repo.SettingsCollaborationPost) m.Post("/access_mode", repo.ChangeCollaborationAccessMode) diff --git a/conf/app.ini b/conf/app.ini index 798712cbd..b910d82b9 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -286,6 +286,8 @@ CSRF_COOKIE_NAME = _csrf [picture] ; Path to store user uploaded avatars AVATAR_UPLOAD_PATH = data/avatars +; Path to store repository uploaded avatars +REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars ; Chinese users can choose "duoshuo" ; or a custom avatar source, like: http://cn.gravatar.com/avatar/ GRAVATAR_SOURCE = gravatar diff --git a/models/repo.go b/models/repo.go index b15b01753..3a9f388b3 100644 --- a/models/repo.go +++ b/models/repo.go @@ -15,10 +15,14 @@ import ( "sort" "strings" "time" + "image" + _ "image/jpeg" + "image/png" "github.com/Unknwon/cae/zip" "github.com/Unknwon/com" "github.com/go-xorm/xorm" + "github.com/nfnt/resize" "github.com/mcuadros/go-version" log "gopkg.in/clog.v1" "gopkg.in/ini.v1" @@ -27,6 +31,7 @@ import ( api "github.com/gogs/go-gogs-client" "github.com/gogs/gogs/models/errors" + "github.com/gogs/gogs/pkg/avatar" "github.com/gogs/gogs/pkg/bindata" "github.com/gogs/gogs/pkg/markup" "github.com/gogs/gogs/pkg/process" @@ -284,6 +289,61 @@ func (repo *Repository) HTMLURL() string { return setting.AppURL + repo.FullName() } +// CustomAvatarPath returns repository custom avatar file path. +func (repo *Repository) CustomAvatarPath() string { + return filepath.Join(setting.RepositoryAvatarUploadPath, com.ToStr(repo.ID)) +} + +// RelAvatarLink returns relative avatar link to the site domain, +// which includes app sub-url as prefix. +// Since Gravatar support not needed here - just check for image path. +func (repo *Repository) RelAvatarLink() string { + defaultImgUrl := "" + if !com.IsExist(repo.CustomAvatarPath()) { + return defaultImgUrl + } + return setting.AppSubURL + "/repo-avatars/" + com.ToStr(repo.ID) +} + +// AvatarLink returns user avatar absolute link. +func (repo *Repository) AvatarLink() string { + link := repo.RelAvatarLink() + if link[0] == '/' && link[1] != '/' { + return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:] + } + return link +} + +// UploadAvatar saves custom avatar for repository. +// FIXME: split uploads to different subdirs +// in case we have massive number of repositories. +func (repo *Repository) UploadAvatar(data []byte) error { + img, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + return fmt.Errorf("Decode: %v", err) + } + + m := resize.Resize(avatar.AVATAR_SIZE, avatar.AVATAR_SIZE, img, resize.NearestNeighbor) + os.MkdirAll(setting.RepositoryAvatarUploadPath, os.ModePerm) + fw, err := os.Create(repo.CustomAvatarPath()) + if err != nil { + return fmt.Errorf("Create: %v", err) + } + defer fw.Close() + + if err = png.Encode(fw, m); err != nil { + return fmt.Errorf("Encode: %v", err) + } + + return nil +} + +// DeleteAvatar deletes the repository custom avatar. +func (repo *Repository) DeleteAvatar() error { + log.Trace("DeleteAvatar [%d]: %s", repo.ID, repo.CustomAvatarPath()) + return os.Remove(repo.CustomAvatarPath()) +} + // This method assumes following fields have been assigned with valid values: // Required - BaseRepo (if fork) // Arguments that are allowed to be nil: permission @@ -312,6 +372,8 @@ func (repo *Repository) APIFormat(permission *api.Permission, user ...*User) *ap Created: repo.Created, Updated: repo.Updated, Permissions: permission, +// Reserved for go-gogs-client change +// AvatarUrl: repo.AvatarLink(), } if repo.IsFork { p := &api.Permission{Pull: true} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index b06c1bf30..5e239249b 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -188,11 +188,12 @@ var ( } // Picture settings - AvatarUploadPath string - GravatarSource string - DisableGravatar bool - EnableFederatedAvatar bool - LibravatarService *libravatar.Libravatar + AvatarUploadPath string + RepositoryAvatarUploadPath string + GravatarSource string + DisableGravatar bool + EnableFederatedAvatar bool + LibravatarService *libravatar.Libravatar // Log settings LogRootPath string @@ -611,6 +612,11 @@ func NewContext() { if !filepath.IsAbs(AvatarUploadPath) { AvatarUploadPath = path.Join(workDir, AvatarUploadPath) } + RepositoryAvatarUploadPath = sec.Key("REPOSITORY_AVATAR_UPLOAD_PATH").MustString(path.Join(AppDataPath, "repo-avatars")) + forcePathSeparator(RepositoryAvatarUploadPath) + if !filepath.IsAbs(RepositoryAvatarUploadPath) { + RepositoryAvatarUploadPath = path.Join(workDir, RepositoryAvatarUploadPath) + } switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source { case "duoshuo": GravatarSource = "http://gravatar.duoshuo.com/avatar/" diff --git a/public/less/_dashboard.less b/public/less/_dashboard.less index 3c84b77d9..688a023b3 100644 --- a/public/less/_dashboard.less +++ b/public/less/_dashboard.less @@ -141,18 +141,28 @@ .repo-owner-name-list { .item-name { max-width: 70%; - margin-bottom: -4px; + margin-bottom: -4px; + } + .ui.micro.image { + width: 16px; + height: auto; + display: inline-block; } } #collaborative-repo-list { .owner-and-repo { - max-width: 80%; - margin-bottom: -5px; + max-width: 75%; + margin-bottom: -5px; } .owner-name { max-width: 120px; - margin-bottom: -5px; + margin-bottom: -5px; + } + .ui.micro.image { + width: 16px; + height: auto; + display: inline-block; } } } diff --git a/routes/repo/setting.go b/routes/repo/setting.go index 0b0f294d6..da9d8fe44 100644 --- a/routes/repo/setting.go +++ b/routes/repo/setting.go @@ -8,9 +8,10 @@ import ( "fmt" "strings" "time" + "io/ioutil" log "gopkg.in/clog.v1" - + "github.com/Unknwon/com" "github.com/gogs/git-module" "github.com/gogs/gogs/models" @@ -19,10 +20,12 @@ import ( "github.com/gogs/gogs/pkg/form" "github.com/gogs/gogs/pkg/mailer" "github.com/gogs/gogs/pkg/setting" + "github.com/gogs/gogs/pkg/tool" ) const ( SETTINGS_OPTIONS = "repo/settings/options" + SETTINGS_REPO_AVATAR = "repo/settings/avatar" SETTINGS_COLLABORATION = "repo/settings/collaboration" SETTINGS_BRANCHES = "repo/settings/branches" SETTINGS_PROTECTED_BRANCH = "repo/settings/protected_branch" @@ -632,3 +635,56 @@ func DeleteDeployKey(c *context.Context) { "redirect": c.Repo.RepoLink + "/settings/keys", }) } + +func SettingsAvatar(c *context.Context) { + c.Title("settings.avatar") + c.PageIs("SettingsAvatar") + c.Success(SETTINGS_REPO_AVATAR) +} + +func SettingsAvatarPost(c *context.Context, f form.Avatar) { + f.Source = form.AVATAR_LOCAL + if err := UpdateAvatarSetting(c, f); err != nil { + c.Flash.Error(err.Error()) + } else { + c.Flash.Success(c.Tr("settings.update_avatar_success")) + } + c.SubURLRedirect(c.Repo.RepoLink + "/settings") +} + +func SettingsDeleteAvatar(c *context.Context) { + if err := c.Repo.Repository.DeleteAvatar(); err != nil { + c.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err)) + } + c.SubURLRedirect(c.Repo.RepoLink + "/settings") +} + +// FIXME: limit size. +func UpdateAvatarSetting(c *context.Context, f form.Avatar) error { + ctxRepo := c.Repo.Repository; + if f.Avatar != nil { + r, err := f.Avatar.Open() + if err != nil { + return fmt.Errorf("Avatar.Open: %v", err) + } + defer r.Close() + + data, err := ioutil.ReadAll(r) + if err != nil { + return fmt.Errorf("ioutil.ReadAll: %v", err) + } + if !tool.IsImageFile(data) { + return errors.New(c.Tr("settings.uploaded_avatar_not_a_image")) + } + if err = ctxRepo.UploadAvatar(data); err != nil { + return fmt.Errorf("UploadAvatar: %v", err) + } + } else { + // No avatar is uploaded but setting has been changed to enable + // No random avatar here. + if !com.IsFile(ctxRepo.CustomAvatarPath()) { + log.Trace("No avatar was uploaded for repo: %d. Default icon will appear instead.", ctxRepo.ID) + } + } + return nil +} diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl index 43abad416..413cdc534 100644 --- a/templates/explore/repo_list.tmpl +++ b/templates/explore/repo_list.tmpl @@ -1,7 +1,12 @@
{{range .Repos}}
-
+
+
+ {{if .RelAvatarLink}}{{else}}{{end}} +
+
+
{{if $.PageIsExplore}}{{.Owner.Name}} / {{end}}{{.Name}} {{if .IsPrivate}} @@ -9,6 +14,8 @@ {{else if .IsMirror}} + {{else}} + {{end}}
@@ -18,6 +25,8 @@
{{if .Description}}

{{.Description | Str2html}}

{{end}}

{{$.i18n.Tr "org.repo_updated"}} {{TimeSince .Updated $.i18n.Lang}}

+
+
{{end}}
diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl index 4f1ad3139..e2cfbe959 100644 --- a/templates/org/team/repositories.tmpl +++ b/templates/org/team/repositories.tmpl @@ -17,6 +17,7 @@ {{$.i18n.Tr "org.teams.remove_repo"}} {{end}} + {{$.Org.Name}}/{{.Name}} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 77685c5ed..1bef1903c 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -5,7 +5,8 @@
diff --git a/templates/user/dashboard/dashboard.tmpl b/templates/user/dashboard/dashboard.tmpl index 4a0c90a2f..f9f974474 100644 --- a/templates/user/dashboard/dashboard.tmpl +++ b/templates/user/dashboard/dashboard.tmpl @@ -32,7 +32,8 @@ {{range .Repos}}
  • - + {{if .RelAvatarLink}}{{else}}{{end}} + {{.Name}} {{.NumStars}} @@ -57,7 +58,8 @@ {{range .CollaborativeRepos}}
  • - + {{if .RelAvatarLink}}{{else}}{{end}} + {{.Owner.Name}} / {{.Name}} @@ -88,6 +90,7 @@ {{range .ContextUser.Orgs}}
  • + {{if .RelAvatarLink}}{{else}}{{end}} {{.Name}} @@ -116,6 +119,7 @@ {{range .Mirrors}}
  • + {{if .RelAvatarLink}}{{else}}{{end}} {{.Name}}