Repository avatar fallback configuration (#7087)
* Only show repository avatar in list when one was selected Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adds fallback configuration option for repository avatar Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Implements repository avatar fallback Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adds admin task for deleting generated repository avatars Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Solve linting issues Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Save avatar before updating database * Linting * Update models/repo.go Co-Authored-By: zeripath <art27@cantab.net>
This commit is contained in:
parent
356854fc5f
commit
8eba27c792
|
@ -505,6 +505,10 @@ SESSION_LIFE_TIME = 86400
|
||||||
[picture]
|
[picture]
|
||||||
AVATAR_UPLOAD_PATH = data/avatars
|
AVATAR_UPLOAD_PATH = data/avatars
|
||||||
REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-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.
|
; Max Width and Height of uploaded avatars.
|
||||||
; This is to limit the amount of RAM used when resizing the image.
|
; This is to limit the amount of RAM used when resizing the image.
|
||||||
AVATAR_MAX_WIDTH = 4096
|
AVATAR_MAX_WIDTH = 4096
|
||||||
|
|
|
@ -292,6 +292,11 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
||||||
[http://www.libravatar.org](http://www.libravatar.org)).
|
[http://www.libravatar.org](http://www.libravatar.org)).
|
||||||
- `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files.
|
- `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_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_WIDTH`: **4096**: Maximum avatar image width in pixels.
|
||||||
- `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height 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.
|
- `AVATAR_MAX_FILE_SIZE`: **1048576** (1Mb): Maximum avatar image file size in bytes.
|
||||||
|
|
|
@ -2528,17 +2528,78 @@ func (repo *Repository) CustomAvatarPath() string {
|
||||||
return filepath.Join(setting.RepositoryAvatarUploadPath, repo.Avatar)
|
return filepath.Join(setting.RepositoryAvatarUploadPath, repo.Avatar)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RelAvatarLink returns a relative link to the user's avatar.
|
// GenerateRandomAvatar generates a random avatar for repository.
|
||||||
// The link a sub-URL to this site
|
func (repo *Repository) GenerateRandomAvatar() error {
|
||||||
// Since Gravatar support not needed here - just check for image path.
|
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 {
|
func (repo *Repository) RelAvatarLink() string {
|
||||||
|
|
||||||
// If no avatar - path is empty
|
// If no avatar - path is empty
|
||||||
avatarPath := repo.CustomAvatarPath()
|
avatarPath := repo.CustomAvatarPath()
|
||||||
if len(avatarPath) <= 0 {
|
if len(avatarPath) <= 0 || !com.IsFile(avatarPath) {
|
||||||
return ""
|
switch mode := setting.RepositoryAvatarFallback; mode {
|
||||||
}
|
case "image":
|
||||||
if !com.IsFile(avatarPath) {
|
return setting.RepositoryAvatarFallbackImage
|
||||||
return ""
|
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
|
return setting.AppSubURL + "/repo-avatars/" + repo.Avatar
|
||||||
}
|
}
|
||||||
|
|
|
@ -250,16 +250,18 @@ var (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Picture settings
|
// Picture settings
|
||||||
AvatarUploadPath string
|
AvatarUploadPath string
|
||||||
AvatarMaxWidth int
|
AvatarMaxWidth int
|
||||||
AvatarMaxHeight int
|
AvatarMaxHeight int
|
||||||
GravatarSource string
|
GravatarSource string
|
||||||
GravatarSourceURL *url.URL
|
GravatarSourceURL *url.URL
|
||||||
DisableGravatar bool
|
DisableGravatar bool
|
||||||
EnableFederatedAvatar bool
|
EnableFederatedAvatar bool
|
||||||
LibravatarService *libravatar.Libravatar
|
LibravatarService *libravatar.Libravatar
|
||||||
AvatarMaxFileSize int64
|
AvatarMaxFileSize int64
|
||||||
RepositoryAvatarUploadPath string
|
RepositoryAvatarUploadPath string
|
||||||
|
RepositoryAvatarFallback string
|
||||||
|
RepositoryAvatarFallbackImage string
|
||||||
|
|
||||||
// Log settings
|
// Log settings
|
||||||
LogLevel string
|
LogLevel string
|
||||||
|
@ -842,6 +844,8 @@ func NewContext() {
|
||||||
if !filepath.IsAbs(RepositoryAvatarUploadPath) {
|
if !filepath.IsAbs(RepositoryAvatarUploadPath) {
|
||||||
RepositoryAvatarUploadPath = path.Join(AppWorkPath, 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)
|
AvatarMaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096)
|
||||||
AvatarMaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(3072)
|
AvatarMaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(3072)
|
||||||
AvatarMaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1048576)
|
AvatarMaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1048576)
|
||||||
|
|
|
@ -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_repo_archives_success = All repository archives have been deleted.
|
||||||
dashboard.delete_missing_repos = Delete all repositories missing their Git files
|
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_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 = Garbage collect all repositories
|
||||||
dashboard.git_gc_repos_success = All repositories have finished garbage collection.
|
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.)
|
dashboard.resync_all_sshkeys = Update the '.ssh/authorized_keys' file with Gitea SSH keys. (Not needed for the built-in SSH server.)
|
||||||
|
|
BIN
public/img/repo_default.png
Normal file
BIN
public/img/repo_default.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
|
@ -125,6 +125,7 @@ const (
|
||||||
reinitMissingRepository
|
reinitMissingRepository
|
||||||
syncExternalUsers
|
syncExternalUsers
|
||||||
gitFsck
|
gitFsck
|
||||||
|
deleteGeneratedRepositoryAvatars
|
||||||
)
|
)
|
||||||
|
|
||||||
// Dashboard show admin panel dashboard
|
// Dashboard show admin panel dashboard
|
||||||
|
@ -167,6 +168,9 @@ func Dashboard(ctx *context.Context) {
|
||||||
case gitFsck:
|
case gitFsck:
|
||||||
success = ctx.Tr("admin.dashboard.git_fsck_started")
|
success = ctx.Tr("admin.dashboard.git_fsck_started")
|
||||||
go models.GitFsck()
|
go models.GitFsck()
|
||||||
|
case deleteGeneratedRepositoryAvatars:
|
||||||
|
success = ctx.Tr("admin.dashboard.delete_generated_repository_avatars_success")
|
||||||
|
err = models.RemoveRandomAvatars()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -53,6 +53,10 @@
|
||||||
<td>{{.i18n.Tr "admin.dashboard.git_fsck"}}</td>
|
<td>{{.i18n.Tr "admin.dashboard.git_fsck"}}</td>
|
||||||
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=9">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
|
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=9">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{.i18n.Tr "admin.dashboard.delete_generated_repository_avatars"}}</td>
|
||||||
|
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=10">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
{{range .Repos}}
|
{{range .Repos}}
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<div class="ui header">
|
<div class="ui header">
|
||||||
<img class="ui avatar image" src="{{.RelAvatarLink}}">
|
{{if .RelAvatarLink}}
|
||||||
|
<img class="ui avatar image" src="{{.RelAvatarLink}}">
|
||||||
|
{{end}}
|
||||||
<a class="name" href="{{.Link}}">
|
<a class="name" href="{{.Link}}">
|
||||||
{{if or $.PageIsExplore $.PageIsProfileStarList }}{{if .Owner}}{{.Owner.Name}} / {{end}}{{end}}{{.Name}}
|
{{if or $.PageIsExplore $.PageIsProfileStarList }}{{if .Owner}}{{.Owner.Name}} / {{end}}{{end}}{{.Name}}
|
||||||
{{if .IsArchived}}<i class="archive icon archived-icon"></i>{{end}}
|
{{if .IsArchived}}<i class="archive icon archived-icon"></i>{{end}}
|
||||||
|
|
Loading…
Reference in a new issue