* 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>tags/v1.9.0-rc1
@@ -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 |
@@ -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. |
@@ -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 | |||
} |
@@ -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) |
@@ -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.) |
@@ -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 { |
@@ -53,6 +53,10 @@ | |||
<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> | |||
</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> | |||
</table> | |||
</div> |
@@ -2,7 +2,9 @@ | |||
{{range .Repos}} | |||
<div class="item"> | |||
<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}}"> | |||
{{if or $.PageIsExplore $.PageIsProfileStarList }}{{if .Owner}}{{.Owner.Name}} / {{end}}{{end}}{{.Name}} | |||
{{if .IsArchived}}<i class="archive icon archived-icon"></i>{{end}} |