Browse Source

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>
tags/v1.9.0-rc1
Mario Lubenka 5 years ago
parent
commit
8eba27c792

+ 4
- 0
custom/conf/app.ini.sample View File

[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

+ 5
- 0
docs/content/doc/advanced/config-cheat-sheet.en-us.md View File

[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.

+ 69
- 8
models/repo.go View File

return filepath.Join(setting.RepositoryAvatarUploadPath, repo.Avatar) 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 { 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 {
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 return setting.AppSubURL + "/repo-avatars/" + repo.Avatar
} }

+ 14
- 10
modules/setting/setting.go View File

} }


// Picture settings // 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 // Log settings
LogLevel string LogLevel string
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)

+ 2
- 0
options/locale/locale_en-US.ini View File

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 View File


+ 4
- 0
routers/admin/admin.go View File

reinitMissingRepository reinitMissingRepository
syncExternalUsers syncExternalUsers
gitFsck gitFsck
deleteGeneratedRepositoryAvatars
) )


// Dashboard show admin panel dashboard // Dashboard show admin panel dashboard
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 {

+ 4
- 0
templates/admin/dashboard.tmpl View File

<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>

+ 3
- 1
templates/explore/repo_list.tmpl View File

{{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…
Cancel
Save