Skip to content

Commit cc8a453

Browse files
committed
avatar-refactor
1 parent f2e7d54 commit cc8a453

File tree

16 files changed

+133
-175
lines changed

16 files changed

+133
-175
lines changed

integrations/api_user_orgs_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func TestUserOrgs(t *testing.T) {
3232
ID: 3,
3333
UserName: user3.Name,
3434
FullName: user3.FullName,
35-
AvatarURL: user3.AvatarLink(),
35+
AvatarURL: user3.AvatarLinkDefaultSize(),
3636
Description: "",
3737
Website: "",
3838
Location: "",
@@ -88,7 +88,7 @@ func TestMyOrgs(t *testing.T) {
8888
ID: 3,
8989
UserName: user3.Name,
9090
FullName: user3.FullName,
91-
AvatarURL: user3.AvatarLink(),
91+
AvatarURL: user3.AvatarLinkDefaultSize(),
9292
Description: "",
9393
Website: "",
9494
Location: "",

integrations/user_avatar_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func TestUserAvatar(t *testing.T) {
7474

7575
user2 = db.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo3, is an org
7676

77-
req = NewRequest(t, "GET", user2.AvatarLink())
77+
req = NewRequest(t, "GET", user2.AvatarLinkDefaultSize())
7878
resp := session.MakeRequest(t, req, http.StatusFound)
7979
location := resp.Header().Get("Location")
8080
if !strings.HasPrefix(location, "/avatars") {

models/avatar.go renamed to models/avatars/avatar.go

Lines changed: 71 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
// Use of this source code is governed by a MIT-style
33
// license that can be found in the LICENSE file.
44

5-
package models
5+
package avatars
66

77
import (
8-
"crypto/md5"
9-
"fmt"
108
"net/url"
119
"path"
1210
"strconv"
@@ -19,7 +17,16 @@ import (
1917
"code.gitea.io/gitea/modules/setting"
2018
)
2119

22-
// EmailHash represents a pre-generated hash map
20+
// DefaultAvatarSize is a sentinel value for the default avatar size, as determined by the avatar-hosting service.
21+
const DefaultAvatarSize = -1
22+
23+
// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
24+
const DefaultAvatarPixelSize = 28
25+
26+
// AvatarRenderedSizeFactor is the factor by which the default size is increased for finer rendering
27+
const AvatarRenderedSizeFactor = 4
28+
29+
// EmailHash represents a pre-generated hash map (mainly used by LibravatarURL, it queries email server's DNS records)
2330
type EmailHash struct {
2431
Hash string `xorm:"pk varchar(32)"`
2532
Email string `xorm:"UNIQUE NOT NULL"`
@@ -41,18 +48,7 @@ func DefaultAvatarLink() string {
4148
return u.String()
4249
}
4350

44-
// DefaultAvatarSize is a sentinel value for the default avatar size, as
45-
// determined by the avatar-hosting service.
46-
const DefaultAvatarSize = -1
47-
48-
// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
49-
const DefaultAvatarPixelSize = 28
50-
51-
// AvatarRenderedSizeFactor is the factor by which the default size is increased for finer rendering
52-
const AvatarRenderedSizeFactor = 4
53-
54-
// HashEmail hashes email address to MD5 string.
55-
// https://en.gravatar.com/site/implement/hash/
51+
// HashEmail hashes email address to MD5 string. https://en.gravatar.com/site/implement/hash/
5652
func HashEmail(email string) string {
5753
return base.EncodeMD5(strings.ToLower(strings.TrimSpace(email)))
5854
}
@@ -69,8 +65,8 @@ func GetEmailForHash(md5Sum string) (string, error) {
6965
})
7066
}
7167

72-
// LibravatarURL returns the URL for the given email. This function should only
73-
// be called if a federated avatar service is enabled.
68+
// LibravatarURL returns the URL for the given email. Slow due to the DNS lookup.
69+
// This function should only be called if a federated avatar service is enabled.
7470
func LibravatarURL(email string) (*url.URL, error) {
7571
urlStr, err := setting.LibravatarService.FromEmail(email)
7672
if err != nil {
@@ -85,14 +81,15 @@ func LibravatarURL(email string) (*url.URL, error) {
8581
return u, nil
8682
}
8783

88-
// HashedAvatarLink returns an avatar link for a provided email
89-
func HashedAvatarLink(email string, size int) string {
84+
// saveEmailHash returns an avatar link for a provided email,
85+
// the email and hash are saved into database, which will be used by GetEmailForHash later
86+
func saveEmailHash(email string) string {
9087
lowerEmail := strings.ToLower(strings.TrimSpace(email))
91-
sum := fmt.Sprintf("%x", md5.Sum([]byte(lowerEmail)))
92-
_, _ = cache.GetString("Avatar:"+sum, func() (string, error) {
88+
emailHash := HashEmail(lowerEmail)
89+
_, _ = cache.GetString("Avatar:"+emailHash, func() (string, error) {
9390
emailHash := &EmailHash{
9491
Email: lowerEmail,
95-
Hash: sum,
92+
Hash: emailHash,
9693
}
9794
// OK we're going to open a session just because I think that that might hide away any problems with postgres reporting errors
9895
if err := db.WithTx(func(ctx *db.Context) error {
@@ -109,39 +106,68 @@ func HashedAvatarLink(email string, size int) string {
109106
}
110107
return lowerEmail, nil
111108
})
112-
if size > 0 {
113-
return setting.AppSubURL + "/avatar/" + url.PathEscape(sum) + "?size=" + strconv.Itoa(size)
114-
}
115-
return setting.AppSubURL + "/avatar/" + url.PathEscape(sum)
109+
return emailHash
116110
}
117111

118-
// MakeFinalAvatarURL constructs the final avatar URL string
119-
func MakeFinalAvatarURL(u *url.URL, size int) string {
120-
vals := u.Query()
121-
vals.Set("d", "identicon")
122-
if size != DefaultAvatarSize {
123-
vals.Set("s", strconv.Itoa(size))
112+
// GenerateUserAvatarFastLink returns a fast link to the user's avatar via the local explore page.
113+
func GenerateUserAvatarFastLink(userName string, size int) string {
114+
if size < 0 {
115+
size = 0
124116
}
125-
u.RawQuery = vals.Encode()
126-
return u.String()
117+
link := setting.AppSubURL + "/user/avatar/" + userName + "/" + strconv.Itoa(size)
118+
return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:]
127119
}
128120

129-
// SizedAvatarLink returns a sized link to the avatar for the given email address.
130-
func SizedAvatarLink(email string, size int) string {
121+
// generateEmailAvatarLink returns a email avatar link.
122+
// if final is true, it may use a slow path (eg: query DNS).
123+
// if final is false, it always uses a fast path.
124+
func generateEmailAvatarLink(email string, size int, final bool) string {
125+
if size <= 0 {
126+
size = DefaultAvatarSize
127+
}
128+
129+
email = strings.TrimSpace(email)
130+
if email == "" {
131+
return DefaultAvatarLink()
132+
}
133+
131134
var avatarURL *url.URL
135+
var err error
136+
132137
if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
133-
// This is the slow path that would need to call LibravatarURL() which
134-
// does DNS lookups. Avoid it by issuing a redirect so we don't block
135-
// the template render with network requests.
136-
return HashedAvatarLink(email, size)
138+
emailHash := saveEmailHash(email)
139+
if final {
140+
if avatarURL, err = LibravatarURL(email); err != nil {
141+
return DefaultAvatarLink()
142+
}
143+
} else {
144+
if size > 0 {
145+
return setting.AppSubURL + "/avatar/" + emailHash + "?size=" + strconv.Itoa(size)
146+
}
147+
return setting.AppSubURL + "/avatar/" + emailHash
148+
}
137149
} else if !setting.DisableGravatar {
138-
// copy GravatarSourceURL, because we will modify its Path.
139-
copyOfGravatarSourceURL := *setting.GravatarSourceURL
140-
avatarURL = &copyOfGravatarSourceURL
150+
avatarURLDummy := *setting.GravatarSourceURL // copy GravatarSourceURL, because we will modify its Path.
151+
avatarURL = &avatarURLDummy
141152
avatarURL.Path = path.Join(avatarURL.Path, HashEmail(email))
142153
} else {
143154
return DefaultAvatarLink()
144155
}
145156

146-
return MakeFinalAvatarURL(avatarURL, size)
157+
avatarURL.Query().Set("d", "identicon")
158+
if size > 0 {
159+
avatarURL.Query().Set("s", strconv.Itoa(size))
160+
}
161+
avatarURL.RawQuery = avatarURL.Query().Encode()
162+
return avatarURL.String()
163+
}
164+
165+
//GenerateEmailAvatarFastLink returns a avatar link (fast, the link may be a delegated one)
166+
func GenerateEmailAvatarFastLink(email string, size int) string {
167+
return generateEmailAvatarLink(email, size, false)
168+
}
169+
170+
//GenerateEmailAvatarFinalLink returns a avatar final link (maybe slow)
171+
func GenerateEmailAvatarFinalLink(email string, size int) string {
172+
return generateEmailAvatarLink(email, size, true)
147173
}

models/avatar_test.go renamed to models/avatars/avatar_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a MIT-style
33
// license that can be found in the LICENSE file.
44

5-
package models
5+
package avatars
66

77
import (
88
"net/url"
@@ -44,11 +44,11 @@ func TestSizedAvatarLink(t *testing.T) {
4444

4545
disableGravatar()
4646
assert.Equal(t, "/testsuburl/assets/img/avatar_default.png",
47-
SizedAvatarLink("[email protected]", 100))
47+
GenerateEmailAvatarFastLink("[email protected]", 100))
4848

4949
enableGravatar(t)
5050
assert.Equal(t,
5151
"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100",
52-
SizedAvatarLink("[email protected]", 100),
52+
GenerateEmailAvatarFastLink("[email protected]", 100),
5353
)
5454
}

models/repo_activity.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func GetActivityStatsTopAuthors(repo *Repository, timeFrom time.Time, count int)
9494
}
9595
users := make(map[int64]*ActivityAuthorData)
9696
var unknownUserID int64
97-
unknownUserAvatarLink := NewGhostUser().AvatarLink()
97+
unknownUserAvatarLink := NewGhostUser().AvatarLinkDefaultSize()
9898
for _, v := range code.Authors {
9999
if len(v.Email) == 0 {
100100
continue
@@ -116,7 +116,7 @@ func GetActivityStatsTopAuthors(repo *Repository, timeFrom time.Time, count int)
116116
users[u.ID] = &ActivityAuthorData{
117117
Name: u.DisplayName(),
118118
Login: u.LowerName,
119-
AvatarLink: u.AvatarLink(),
119+
AvatarLink: u.AvatarLinkDefaultSize(),
120120
HomeLink: u.HomeLink(),
121121
Commits: v.Commits,
122122
}

models/user_avatar.go

Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010
"image/png"
1111
"io"
1212
"strconv"
13-
"strings"
1413

14+
"code.gitea.io/gitea/models/avatars"
1515
"code.gitea.io/gitea/models/db"
1616
"code.gitea.io/gitea/modules/avatar"
1717
"code.gitea.io/gitea/modules/log"
@@ -40,7 +40,7 @@ func (u *User) generateRandomAvatar(e db.Engine) error {
4040
return fmt.Errorf("RandomImage: %v", err)
4141
}
4242

43-
u.Avatar = HashEmail(seed)
43+
u.Avatar = avatars.HashEmail(seed)
4444

4545
// Don't share the images so that we can delete them easily
4646
if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
@@ -60,61 +60,45 @@ func (u *User) generateRandomAvatar(e db.Engine) error {
6060
return nil
6161
}
6262

63-
// SizedRelAvatarLink returns a link to the user's avatar via
64-
// the local explore page. Function returns immediately.
65-
// When applicable, the link is for an avatar of the indicated size (in pixels).
66-
func (u *User) SizedRelAvatarLink(size int) string {
67-
return setting.AppSubURL + "/user/avatar/" + u.Name + "/" + strconv.Itoa(size)
68-
}
69-
70-
// RealSizedAvatarLink returns a link to the user's avatar. When
71-
// applicable, the link is for an avatar of the indicated size (in pixels).
72-
//
73-
// This function make take time to return when federated avatars
74-
// are in use, due to a DNS lookup need
75-
//
76-
func (u *User) RealSizedAvatarLink(size int) string {
63+
// AvatarLinkWithSize returns a link to the user's avatar with size
64+
func (u *User) AvatarLinkWithSize(size int) string {
7765
if u.ID == -1 {
78-
return DefaultAvatarLink()
66+
// ghost user
67+
return avatars.DefaultAvatarLink()
7968
}
8069

70+
var useLocalAvatar bool
71+
8172
switch {
8273
case u.UseCustomAvatar:
83-
if u.Avatar == "" {
84-
return DefaultAvatarLink()
85-
}
86-
if size > 0 {
87-
return setting.AppSubURL + "/avatars/" + u.Avatar + "?size=" + strconv.Itoa(size)
88-
}
89-
return setting.AppSubURL + "/avatars/" + u.Avatar
74+
useLocalAvatar = true
9075
case setting.DisableGravatar, setting.OfflineMode:
76+
useLocalAvatar = true
9177
if u.Avatar == "" {
9278
if err := u.GenerateRandomAvatar(); err != nil {
9379
log.Error("GenerateRandomAvatar: %v", err)
9480
}
9581
}
82+
default:
83+
useLocalAvatar = false
84+
}
85+
86+
if useLocalAvatar {
87+
if u.Avatar == "" {
88+
return avatars.DefaultAvatarLink()
89+
}
9690
if size > 0 {
9791
return setting.AppSubURL + "/avatars/" + u.Avatar + "?size=" + strconv.Itoa(size)
9892
}
9993
return setting.AppSubURL + "/avatars/" + u.Avatar
10094
}
101-
return SizedAvatarLink(u.AvatarEmail, size)
102-
}
10395

104-
// RelAvatarLink returns a relative link to the user's avatar. The link
105-
// may either be a sub-URL to this site, or a full URL to an external avatar
106-
// service.
107-
func (u *User) RelAvatarLink() string {
108-
return u.SizedRelAvatarLink(DefaultAvatarSize)
96+
return avatars.GenerateEmailAvatarFastLink(u.AvatarEmail, size)
10997
}
11098

111-
// AvatarLink returns user avatar absolute link.
112-
func (u *User) AvatarLink() string {
113-
link := u.RelAvatarLink()
114-
if link[0] == '/' && link[1] != '/' {
115-
return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:]
116-
}
117-
return link
99+
// AvatarLinkDefaultSize returns a avatar link with default size
100+
func (u *User) AvatarLinkDefaultSize() string {
101+
return u.AvatarLinkWithSize(avatars.DefaultAvatarSize)
118102
}
119103

120104
// UploadAvatar saves custom avatar for user.

modules/convert/convert.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ func ToDeployKey(apiLink string, key *models.DeployKey) *api.DeployKey {
277277
func ToOrganization(org *models.User) *api.Organization {
278278
return &api.Organization{
279279
ID: org.ID,
280-
AvatarURL: org.AvatarLink(),
280+
AvatarURL: org.AvatarLinkDefaultSize(),
281281
UserName: org.Name,
282282
FullName: org.FullName,
283283
Description: org.Description,

modules/convert/user.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func toUser(user *models.User, signed, authed bool) *api.User {
5151
UserName: user.Name,
5252
FullName: markup.Sanitize(user.FullName),
5353
Email: user.GetEmail(),
54-
AvatarURL: user.AvatarLink(),
54+
AvatarURL: user.AvatarLinkDefaultSize(),
5555
Created: user.CreatedUnix.AsTime(),
5656
Restricted: user.IsRestricted,
5757
Location: user.Location,

modules/repository/commits.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"code.gitea.io/gitea/models"
12+
"code.gitea.io/gitea/models/avatars"
1213
"code.gitea.io/gitea/modules/git"
1314
"code.gitea.io/gitea/modules/log"
1415
api "code.gitea.io/gitea/modules/structs"
@@ -139,14 +140,14 @@ func (pc *PushCommits) AvatarLink(email string) string {
139140
return avatar
140141
}
141142

142-
size := models.DefaultAvatarPixelSize * models.AvatarRenderedSizeFactor
143+
size := avatars.DefaultAvatarPixelSize * avatars.AvatarRenderedSizeFactor
143144

144145
u, ok := pc.emailUsers[email]
145146
if !ok {
146147
var err error
147148
u, err = models.GetUserByEmail(email)
148149
if err != nil {
149-
pc.avatars[email] = models.SizedAvatarLink(email, size)
150+
pc.avatars[email] = avatars.GenerateEmailAvatarFastLink(email, size)
150151
if !models.IsErrUserNotExist(err) {
151152
log.Error("GetUserByEmail: %v", err)
152153
return ""
@@ -156,7 +157,7 @@ func (pc *PushCommits) AvatarLink(email string) string {
156157
}
157158
}
158159
if u != nil {
159-
pc.avatars[email] = u.RealSizedAvatarLink(size)
160+
pc.avatars[email] = u.AvatarLinkWithSize(size)
160161
}
161162

162163
return pc.avatars[email]

0 commit comments

Comments
 (0)