Skip to content

Commit f0ba87f

Browse files
authored
Avatar refactor, move avatar code from models to models.avatars, remove duplicated code (#17123)
Why this refactor The goal is to move most files from `models` package to `models.xxx` package. Many models depend on avatar model, so just move this first. And the existing logic is not clear, there are too many function like `AvatarLink`, `RelAvatarLink`, `SizedRelAvatarLink`, `SizedAvatarLink`, `MakeFinalAvatarURL`, `HashedAvatarLink`, etc. This refactor make everything clear: * user.AvatarLink() * user.AvatarLinkWithSize(size) * avatars.GenerateEmailAvatarFastLink(email, size) * avatars.GenerateEmailAvatarFinalLink(email, size) And many duplicated code are deleted in route handler, the handler and the model share the same avatar logic now.
1 parent 48c2578 commit f0ba87f

File tree

15 files changed

+274
-300
lines changed

15 files changed

+274
-300
lines changed

integrations/user_avatar_test.go

+2-9
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"mime/multipart"
1212
"net/http"
1313
"net/url"
14-
"strings"
1514
"testing"
1615

1716
"code.gitea.io/gitea/models"
@@ -75,14 +74,8 @@ func TestUserAvatar(t *testing.T) {
7574
user2 = db.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo3, is an org
7675

7776
req = NewRequest(t, "GET", user2.AvatarLink())
78-
resp := session.MakeRequest(t, req, http.StatusFound)
79-
location := resp.Header().Get("Location")
80-
if !strings.HasPrefix(location, "/avatars") {
81-
assert.Fail(t, "Avatar location is not local: %s", location)
82-
}
83-
req = NewRequest(t, "GET", location)
84-
session.MakeRequest(t, req, http.StatusOK)
77+
_ = session.MakeRequest(t, req, http.StatusOK)
8578

86-
// Can't test if the response matches because the image is regened on upload but checking that this at least doesn't give a 404 should be enough.
79+
// Can't test if the response matches because the image is re-generated on upload but checking that this at least doesn't give a 404 should be enough.
8780
})
8881
}

models/avatar.go

-148
This file was deleted.

models/avatars/avatar.go

+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package avatars
6+
7+
import (
8+
"context"
9+
"net/url"
10+
"path"
11+
"strconv"
12+
"strings"
13+
14+
"code.gitea.io/gitea/models/db"
15+
"code.gitea.io/gitea/modules/base"
16+
"code.gitea.io/gitea/modules/cache"
17+
"code.gitea.io/gitea/modules/log"
18+
"code.gitea.io/gitea/modules/setting"
19+
)
20+
21+
// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
22+
const DefaultAvatarPixelSize = 28
23+
24+
// AvatarRenderedSizeFactor is the factor by which the default size is increased for finer rendering
25+
const AvatarRenderedSizeFactor = 4
26+
27+
// EmailHash represents a pre-generated hash map (mainly used by LibravatarURL, it queries email server's DNS records)
28+
type EmailHash struct {
29+
Hash string `xorm:"pk varchar(32)"`
30+
Email string `xorm:"UNIQUE NOT NULL"`
31+
}
32+
33+
func init() {
34+
db.RegisterModel(new(EmailHash))
35+
}
36+
37+
// DefaultAvatarLink the default avatar link
38+
func DefaultAvatarLink() string {
39+
u, err := url.Parse(setting.AppSubURL)
40+
if err != nil {
41+
log.Error("GetUserByEmail: %v", err)
42+
return ""
43+
}
44+
45+
u.Path = path.Join(u.Path, "/assets/img/avatar_default.png")
46+
return u.String()
47+
}
48+
49+
// HashEmail hashes email address to MD5 string. https://en.gravatar.com/site/implement/hash/
50+
func HashEmail(email string) string {
51+
return base.EncodeMD5(strings.ToLower(strings.TrimSpace(email)))
52+
}
53+
54+
// GetEmailForHash converts a provided md5sum to the email
55+
func GetEmailForHash(md5Sum string) (string, error) {
56+
return cache.GetString("Avatar:"+md5Sum, func() (string, error) {
57+
emailHash := EmailHash{
58+
Hash: strings.ToLower(strings.TrimSpace(md5Sum)),
59+
}
60+
61+
_, err := db.GetEngine(db.DefaultContext).Get(&emailHash)
62+
return emailHash.Email, err
63+
})
64+
}
65+
66+
// LibravatarURL returns the URL for the given email. Slow due to the DNS lookup.
67+
// This function should only be called if a federated avatar service is enabled.
68+
func LibravatarURL(email string) (*url.URL, error) {
69+
urlStr, err := setting.LibravatarService.FromEmail(email)
70+
if err != nil {
71+
log.Error("LibravatarService.FromEmail(email=%s): error %v", email, err)
72+
return nil, err
73+
}
74+
u, err := url.Parse(urlStr)
75+
if err != nil {
76+
log.Error("Failed to parse libravatar url(%s): error %v", urlStr, err)
77+
return nil, err
78+
}
79+
return u, nil
80+
}
81+
82+
// saveEmailHash returns an avatar link for a provided email,
83+
// the email and hash are saved into database, which will be used by GetEmailForHash later
84+
func saveEmailHash(email string) string {
85+
lowerEmail := strings.ToLower(strings.TrimSpace(email))
86+
emailHash := HashEmail(lowerEmail)
87+
_, _ = cache.GetString("Avatar:"+emailHash, func() (string, error) {
88+
emailHash := &EmailHash{
89+
Email: lowerEmail,
90+
Hash: emailHash,
91+
}
92+
// OK we're going to open a session just because I think that that might hide away any problems with postgres reporting errors
93+
if err := db.WithTx(func(ctx context.Context) error {
94+
has, err := db.GetEngine(ctx).Where("email = ? AND hash = ?", emailHash.Email, emailHash.Hash).Get(new(EmailHash))
95+
if has || err != nil {
96+
// Seriously we don't care about any DB problems just return the lowerEmail - we expect the transaction to fail most of the time
97+
return nil
98+
}
99+
_, _ = db.GetEngine(ctx).Insert(emailHash)
100+
return nil
101+
}); err != nil {
102+
// Seriously we don't care about any DB problems just return the lowerEmail - we expect the transaction to fail most of the time
103+
return lowerEmail, nil
104+
}
105+
return lowerEmail, nil
106+
})
107+
return emailHash
108+
}
109+
110+
// GenerateUserAvatarFastLink returns a fast link (302) to the user's avatar: "/user/avatar/${User.Name}/${size}"
111+
func GenerateUserAvatarFastLink(userName string, size int) string {
112+
if size < 0 {
113+
size = 0
114+
}
115+
return setting.AppSubURL + "/user/avatar/" + userName + "/" + strconv.Itoa(size)
116+
}
117+
118+
// GenerateUserAvatarImageLink returns a link for `User.Avatar` image file: "/avatars/${User.Avatar}"
119+
func GenerateUserAvatarImageLink(userAvatar string, size int) string {
120+
if size > 0 {
121+
return setting.AppSubURL + "/avatars/" + userAvatar + "?size=" + strconv.Itoa(size)
122+
}
123+
return setting.AppSubURL + "/avatars/" + userAvatar
124+
}
125+
126+
// generateRecognizedAvatarURL generate a recognized avatar (Gravatar/Libravatar) URL, it modifies the URL so the parameter is passed by a copy
127+
func generateRecognizedAvatarURL(u url.URL, size int) string {
128+
urlQuery := u.Query()
129+
urlQuery.Set("d", "identicon")
130+
if size > 0 {
131+
urlQuery.Set("s", strconv.Itoa(size))
132+
}
133+
u.RawQuery = urlQuery.Encode()
134+
return u.String()
135+
}
136+
137+
// generateEmailAvatarLink returns a email avatar link.
138+
// if final is true, it may use a slow path (eg: query DNS).
139+
// if final is false, it always uses a fast path.
140+
func generateEmailAvatarLink(email string, size int, final bool) string {
141+
email = strings.TrimSpace(email)
142+
if email == "" {
143+
return DefaultAvatarLink()
144+
}
145+
146+
var err error
147+
if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
148+
emailHash := saveEmailHash(email)
149+
if final {
150+
// for final link, we can spend more time on slow external query
151+
var avatarURL *url.URL
152+
if avatarURL, err = LibravatarURL(email); err != nil {
153+
return DefaultAvatarLink()
154+
}
155+
return generateRecognizedAvatarURL(*avatarURL, size)
156+
}
157+
// for non-final link, we should return fast (use a 302 redirection link)
158+
urlStr := setting.AppSubURL + "/avatar/" + emailHash
159+
if size > 0 {
160+
urlStr += "?size=" + strconv.Itoa(size)
161+
}
162+
return urlStr
163+
} else if !setting.DisableGravatar {
164+
// copy GravatarSourceURL, because we will modify its Path.
165+
avatarURLCopy := *setting.GravatarSourceURL
166+
avatarURLCopy.Path = path.Join(avatarURLCopy.Path, HashEmail(email))
167+
return generateRecognizedAvatarURL(avatarURLCopy, size)
168+
}
169+
return DefaultAvatarLink()
170+
}
171+
172+
//GenerateEmailAvatarFastLink returns a avatar link (fast, the link may be a delegated one: "/avatar/${hash}")
173+
func GenerateEmailAvatarFastLink(email string, size int) string {
174+
return generateEmailAvatarLink(email, size, false)
175+
}
176+
177+
//GenerateEmailAvatarFinalLink returns a avatar final link (maybe slow)
178+
func GenerateEmailAvatarFinalLink(email string, size int) string {
179+
return generateEmailAvatarLink(email, size, true)
180+
}

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

+3-3
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("gitea@example.com", 100))
47+
GenerateEmailAvatarFastLink("gitea@example.com", 100))
4848

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

0 commit comments

Comments
 (0)