From 8eba27c79257c6bc68cefbdffbb36d3596e6d3ee Mon Sep 17 00:00:00 2001 From: Mario Lubenka Date: Sun, 2 Jun 2019 08:40:12 +0200 Subject: [PATCH] Repository avatar fallback configuration (#7087) * Only show repository avatar in list when one was selected Signed-off-by: Mario Lubenka * Adds fallback configuration option for repository avatar Signed-off-by: Mario Lubenka * Implements repository avatar fallback Signed-off-by: Mario Lubenka * Adds admin task for deleting generated repository avatars Signed-off-by: Mario Lubenka * Solve linting issues Signed-off-by: Mario Lubenka * Save avatar before updating database * Linting * Update models/repo.go Co-Authored-By: zeripath --- custom/conf/app.ini.sample | 4 + .../doc/advanced/config-cheat-sheet.en-us.md | 5 ++ models/repo.go | 77 ++++++++++++++++-- modules/setting/setting.go | 24 +++--- options/locale/locale_en-US.ini | 2 + public/img/repo_default.png | Bin 0 -> 2464 bytes routers/admin/admin.go | 4 + templates/admin/dashboard.tmpl | 4 + templates/explore/repo_list.tmpl | 4 +- 9 files changed, 105 insertions(+), 19 deletions(-) create mode 100644 public/img/repo_default.png diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index e8e3ffada..a674984a2 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -505,6 +505,10 @@ SESSION_LIFE_TIME = 86400 [picture] AVATAR_UPLOAD_PATH = data/avatars REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars +; How Gitea deals with missing repository avatars +; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used +REPOSITORY_AVATAR_FALLBACK = none +REPOSITORY_AVATAR_FALLBACK_IMAGE = /img/repo_default.png ; Max Width and Height of uploaded avatars. ; This is to limit the amount of RAM used when resizing the image. AVATAR_MAX_WIDTH = 4096 diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 052ced6e2..ecc196c86 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -292,6 +292,11 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. [http://www.libravatar.org](http://www.libravatar.org)). - `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files. - `REPOSITORY_AVATAR_UPLOAD_PATH`: **data/repo-avatars**: Path to store repository avatar image files. +- `REPOSITORY_AVATAR_FALLBACK`: **none**: How Gitea deals with missing repository avatars + - none = no avatar will be displayed + - random = random avatar will be generated + - image = default image will be used (which is set in `REPOSITORY_AVATAR_DEFAULT_IMAGE`) +- `REPOSITORY_AVATAR_FALLBACK_IMAGE`: **/img/repo_default.png**: Image used as default repository avatar (if `REPOSITORY_AVATAR_FALLBACK` is set to image and none was uploaded) - `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. - `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height in pixels. - `AVATAR_MAX_FILE_SIZE`: **1048576** (1Mb): Maximum avatar image file size in bytes. diff --git a/models/repo.go b/models/repo.go index 16684bdee..d5eca3d22 100644 --- a/models/repo.go +++ b/models/repo.go @@ -2528,17 +2528,78 @@ func (repo *Repository) CustomAvatarPath() string { return filepath.Join(setting.RepositoryAvatarUploadPath, repo.Avatar) } -// RelAvatarLink returns a relative link to the user's avatar. -// The link a sub-URL to this site -// Since Gravatar support not needed here - just check for image path. +// GenerateRandomAvatar generates a random avatar for repository. +func (repo *Repository) GenerateRandomAvatar() error { + return repo.generateRandomAvatar(x) +} + +func (repo *Repository) generateRandomAvatar(e Engine) error { + idToString := fmt.Sprintf("%d", repo.ID) + + seed := idToString + img, err := avatar.RandomImage([]byte(seed)) + if err != nil { + return fmt.Errorf("RandomImage: %v", err) + } + + repo.Avatar = idToString + if err = os.MkdirAll(filepath.Dir(repo.CustomAvatarPath()), os.ModePerm); err != nil { + return fmt.Errorf("MkdirAll: %v", err) + } + fw, err := os.Create(repo.CustomAvatarPath()) + if err != nil { + return fmt.Errorf("Create: %v", err) + } + defer fw.Close() + + if err = png.Encode(fw, img); err != nil { + return fmt.Errorf("Encode: %v", err) + } + log.Info("New random avatar created for repository: %d", repo.ID) + + if _, err := e.ID(repo.ID).Cols("avatar").NoAutoTime().Update(repo); err != nil { + return err + } + + return nil +} + +// RemoveRandomAvatars removes the randomly generated avatars that were created for repositories +func RemoveRandomAvatars() error { + var ( + err error + ) + err = x. + Where("id > 0").BufferSize(setting.IterateBufferSize). + Iterate(new(Repository), + func(idx int, bean interface{}) error { + repository := bean.(*Repository) + stringifiedID := strconv.FormatInt(repository.ID, 10) + if repository.Avatar == stringifiedID { + return repository.DeleteAvatar() + } + return nil + }) + return err +} + +// RelAvatarLink returns a relative link to the repository's avatar. func (repo *Repository) RelAvatarLink() string { + // If no avatar - path is empty avatarPath := repo.CustomAvatarPath() - if len(avatarPath) <= 0 { - return "" - } - if !com.IsFile(avatarPath) { - return "" + if len(avatarPath) <= 0 || !com.IsFile(avatarPath) { + switch mode := setting.RepositoryAvatarFallback; mode { + case "image": + return setting.RepositoryAvatarFallbackImage + case "random": + if err := repo.GenerateRandomAvatar(); err != nil { + log.Error("GenerateRandomAvatar: %v", err) + } + default: + // default behaviour: do not display avatar + return "" + } } return setting.AppSubURL + "/repo-avatars/" + repo.Avatar } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 9e9610578..ff53e9a37 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -250,16 +250,18 @@ var ( } // Picture settings - AvatarUploadPath string - AvatarMaxWidth int - AvatarMaxHeight int - GravatarSource string - GravatarSourceURL *url.URL - DisableGravatar bool - EnableFederatedAvatar bool - LibravatarService *libravatar.Libravatar - AvatarMaxFileSize int64 - RepositoryAvatarUploadPath string + AvatarUploadPath string + AvatarMaxWidth int + AvatarMaxHeight int + GravatarSource string + GravatarSourceURL *url.URL + DisableGravatar bool + EnableFederatedAvatar bool + LibravatarService *libravatar.Libravatar + AvatarMaxFileSize int64 + RepositoryAvatarUploadPath string + RepositoryAvatarFallback string + RepositoryAvatarFallbackImage string // Log settings LogLevel string @@ -842,6 +844,8 @@ func NewContext() { if !filepath.IsAbs(RepositoryAvatarUploadPath) { RepositoryAvatarUploadPath = path.Join(AppWorkPath, RepositoryAvatarUploadPath) } + RepositoryAvatarFallback = sec.Key("REPOSITORY_AVATAR_FALLBACK").MustString("none") + RepositoryAvatarFallbackImage = sec.Key("REPOSITORY_AVATAR_FALLBACK_IMAGE").MustString("/img/repo_default.png") AvatarMaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096) AvatarMaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(3072) AvatarMaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1048576) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 645c9770a..ebc6ca31c 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1522,6 +1522,8 @@ dashboard.delete_repo_archives = Delete all repository archives dashboard.delete_repo_archives_success = All repository archives have been deleted. dashboard.delete_missing_repos = Delete all repositories missing their Git files dashboard.delete_missing_repos_success = All repositories missing their Git files have been deleted. +dashboard.delete_generated_repository_avatars = Delete generated repository avatars +dashboard.delete_generated_repository_avatars_success = Generated repository avatars were deleted. dashboard.git_gc_repos = Garbage collect all repositories dashboard.git_gc_repos_success = All repositories have finished garbage collection. dashboard.resync_all_sshkeys = Update the '.ssh/authorized_keys' file with Gitea SSH keys. (Not needed for the built-in SSH server.) diff --git a/public/img/repo_default.png b/public/img/repo_default.png new file mode 100644 index 0000000000000000000000000000000000000000..dbfa8437235208c9c565581c3ea54c38f0a70d43 GIT binary patch literal 2464 zcmb_edsGuw9-hXy;XoBuTu}%KFLA3vLJ*pSsG!hM1Bv*6SW<)-C?YSzLnCNRBeK*- zh>92^Nmq+hc}TQsBM;w_kdRoQU0%v=nM{fY*b?Yw(?r>sp3~FQKX%Wq>>qQ!`F+3d z`|h1PckY?Wtt@bXtG6ov01Gy6iroeP?^L0e3l?Ek-gyHefc-coAqD`{Qa5M%e5Cf< zvuRrb02~SifTPC%;3tGS`aJ*?lL6pW8UWBQ0DxD)rF)4Cq=Dfv!B_yeekzcHRJfwe z330g5`Gf`UV_T~fp#b3W#pc+U9R~*|UUWCW8NQuUCTh}!K<~jVxW)bN&X8pu%QRUZi_LvY%f*8MO~2R&|^1XQqaP5!P!%No0M!>UmsZ!BZAuQoxmil~3e6!~!KdhLmmMp6y;(HU`@ zzYPl4Wa(N>uX4=?*D302yzg`Uh+WBr-|%_|m`Yu({SD=cdno0{@9=sx_nzUSOq1|% zp2;jE*IPi6TGAG3vs`gAWaLN5hNhh}&+XBnx_O$6vXzd98#TFf*;u_6SI8w?s!y>t zjU_YcRT8}3_p#>y`{lWf_9^JWKm~`WbzdT~l(*-uBI<1LvxL;lt$!$m=t%hI#^`W6bP zQqoNGTF2KXG$ru7sqM23q78)c&m=T@cvd!!HfCGfpD2al%CK-TxBdKx`yL{Vv43Yb z5Ah9to4rUy`1y1hV0-fAMk3+t`C~8%cGl#@<*A=G}kjR8Xn(OVg9tz zKU|FWMo3K#PixS(r8r<^OX(d^^}9~P;dQRA;%I-5KVmWE(Vuy^EXIg8_-%E;{KkpVzr-Am%Nm@oUo4U?TqR?THj>cHU_J+@mZ@sm88_ zZ?`tTOe)g;*RPs#M;;{>`gG1cirV>Le^SX?zp$StrPkp(b8{U_PG20N{vV=fzdL>F zreqGg$OZf9>%xNJZ+qO**3UKQHUWPiZEQX{uNs`4>t6cM{j{P5+1z;f-OxF%QyB7m zZo-Mke6?}oi{PBewIj`<_;=+4F-jlg%d|V5RLP^2zR0bxgr|-76D>vA*C@+66`y{K z5gEo0uG)PPy=8>);g;78(~uS)sf!S1+bRs>erz*Zc3$xm8rl;N&OXOSKAn<>Z5Nro zFCds6{(0B9hy)MHD<-x_X+7=m^PtFw?_X7ZV|*Rgrm(J3IeFkEqs&>7**Cnrlwv#8 z%@egASScO2f9+5P%Y6NzaGcGy9Czj-$DBlYA6lr|X~yK5dkcm9e^%Fxn5}L5iUJ(n z>N25Vc!qgny-wC9JQ(cgtSnnuzXHcvhog<2VN|6&tG`KPoEx$_$F+8^RF%mwJu!7{ z{O}5R`hw_B)j=e$5lhLhC6*3A zDy^#>p#P_Xk-viRKo)brF@7xZYoqS_UZrxYWL1=u!f@R}70$LVD zDZrcIl@GRz7ZG%C*Tl50Y`S5JBr%P>_xak_ftTR8T4mvZzo2EW4DC z3KXE=2r4Kc=GzzCg|j`oN5V^loo@t*45SgKv{1R zJ9Eb3e%LmP>9b$HO8&e!0QrqsUKGbE;-?p7(Dw*35CACTFj@#XA|#x=BP^UA5kV)f p2_dhclgU@Q_ka5vLvB8QZ>I3y8+xjA*@ywy9LI`n-jMdmzW{dZ%{Tx6 literal 0 HcmV?d00001 diff --git a/routers/admin/admin.go b/routers/admin/admin.go index 0e6fa2c24..5107e18b7 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -125,6 +125,7 @@ const ( reinitMissingRepository syncExternalUsers gitFsck + deleteGeneratedRepositoryAvatars ) // Dashboard show admin panel dashboard @@ -167,6 +168,9 @@ func Dashboard(ctx *context.Context) { case gitFsck: success = ctx.Tr("admin.dashboard.git_fsck_started") go models.GitFsck() + case deleteGeneratedRepositoryAvatars: + success = ctx.Tr("admin.dashboard.delete_generated_repository_avatars_success") + err = models.RemoveRandomAvatars() } if err != nil { diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index 13c06334a..262db04b9 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -53,6 +53,10 @@ {{.i18n.Tr "admin.dashboard.git_fsck"}} {{.i18n.Tr "admin.dashboard.operation_run"}} + + {{.i18n.Tr "admin.dashboard.delete_generated_repository_avatars"}} + {{.i18n.Tr "admin.dashboard.operation_run"}} + diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl index 34aab6477..8c7ba51a5 100644 --- a/templates/explore/repo_list.tmpl +++ b/templates/explore/repo_list.tmpl @@ -2,7 +2,9 @@ {{range .Repos}}