mirror of https://github.com/go-gitea/gitea
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.pull/17154/head^2
parent
48c2578bd8
commit
f0ba87fda8
@ -1,148 +0,0 @@ |
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/md5" |
||||
"fmt" |
||||
"net/url" |
||||
"path" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"code.gitea.io/gitea/models/db" |
||||
"code.gitea.io/gitea/modules/base" |
||||
"code.gitea.io/gitea/modules/cache" |
||||
"code.gitea.io/gitea/modules/log" |
||||
"code.gitea.io/gitea/modules/setting" |
||||
) |
||||
|
||||
// EmailHash represents a pre-generated hash map
|
||||
type EmailHash struct { |
||||
Hash string `xorm:"pk varchar(32)"` |
||||
Email string `xorm:"UNIQUE NOT NULL"` |
||||
} |
||||
|
||||
func init() { |
||||
db.RegisterModel(new(EmailHash)) |
||||
} |
||||
|
||||
// DefaultAvatarLink the default avatar link
|
||||
func DefaultAvatarLink() string { |
||||
u, err := url.Parse(setting.AppSubURL) |
||||
if err != nil { |
||||
log.Error("GetUserByEmail: %v", err) |
||||
return "" |
||||
} |
||||
|
||||
u.Path = path.Join(u.Path, "/assets/img/avatar_default.png") |
||||
return u.String() |
||||
} |
||||
|
||||
// DefaultAvatarSize is a sentinel value for the default avatar size, as
|
||||
// determined by the avatar-hosting service.
|
||||
const DefaultAvatarSize = -1 |
||||
|
||||
// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
|
||||
const DefaultAvatarPixelSize = 28 |
||||
|
||||
// AvatarRenderedSizeFactor is the factor by which the default size is increased for finer rendering
|
||||
const AvatarRenderedSizeFactor = 4 |
||||
|
||||
// HashEmail hashes email address to MD5 string.
|
||||
// https://en.gravatar.com/site/implement/hash/
|
||||
func HashEmail(email string) string { |
||||
return base.EncodeMD5(strings.ToLower(strings.TrimSpace(email))) |
||||
} |
||||
|
||||
// GetEmailForHash converts a provided md5sum to the email
|
||||
func GetEmailForHash(md5Sum string) (string, error) { |
||||
return cache.GetString("Avatar:"+md5Sum, func() (string, error) { |
||||
emailHash := EmailHash{ |
||||
Hash: strings.ToLower(strings.TrimSpace(md5Sum)), |
||||
} |
||||
|
||||
_, err := db.GetEngine(db.DefaultContext).Get(&emailHash) |
||||
return emailHash.Email, err |
||||
}) |
||||
} |
||||
|
||||
// LibravatarURL returns the URL for the given email. This function should only
|
||||
// be called if a federated avatar service is enabled.
|
||||
func LibravatarURL(email string) (*url.URL, error) { |
||||
urlStr, err := setting.LibravatarService.FromEmail(email) |
||||
if err != nil { |
||||
log.Error("LibravatarService.FromEmail(email=%s): error %v", email, err) |
||||
return nil, err |
||||
} |
||||
u, err := url.Parse(urlStr) |
||||
if err != nil { |
||||
log.Error("Failed to parse libravatar url(%s): error %v", urlStr, err) |
||||
return nil, err |
||||
} |
||||
return u, nil |
||||
} |
||||
|
||||
// HashedAvatarLink returns an avatar link for a provided email
|
||||
func HashedAvatarLink(email string, size int) string { |
||||
lowerEmail := strings.ToLower(strings.TrimSpace(email)) |
||||
sum := fmt.Sprintf("%x", md5.Sum([]byte(lowerEmail))) |
||||
_, _ = cache.GetString("Avatar:"+sum, func() (string, error) { |
||||
emailHash := &EmailHash{ |
||||
Email: lowerEmail, |
||||
Hash: sum, |
||||
} |
||||
// OK we're going to open a session just because I think that that might hide away any problems with postgres reporting errors
|
||||
if err := db.WithTx(func(ctx context.Context) error { |
||||
has, err := db.GetEngine(ctx).Where("email = ? AND hash = ?", emailHash.Email, emailHash.Hash).Get(new(EmailHash)) |
||||
if has || err != nil { |
||||
// Seriously we don't care about any DB problems just return the lowerEmail - we expect the transaction to fail most of the time
|
||||
return nil |
||||
} |
||||
_, _ = db.GetEngine(ctx).Insert(emailHash) |
||||
return nil |
||||
}); err != nil { |
||||
// Seriously we don't care about any DB problems just return the lowerEmail - we expect the transaction to fail most of the time
|
||||
return lowerEmail, nil |
||||
} |
||||
return lowerEmail, nil |
||||
}) |
||||
if size > 0 { |
||||
return setting.AppSubURL + "/avatar/" + url.PathEscape(sum) + "?size=" + strconv.Itoa(size) |
||||
} |
||||
return setting.AppSubURL + "/avatar/" + url.PathEscape(sum) |
||||
} |
||||
|
||||
// MakeFinalAvatarURL constructs the final avatar URL string
|
||||
func MakeFinalAvatarURL(u *url.URL, size int) string { |
||||
vals := u.Query() |
||||
vals.Set("d", "identicon") |
||||
if size != DefaultAvatarSize { |
||||
vals.Set("s", strconv.Itoa(size)) |
||||
} |
||||
u.RawQuery = vals.Encode() |
||||
return u.String() |
||||
} |
||||
|
||||
// SizedAvatarLink returns a sized link to the avatar for the given email address.
|
||||
func SizedAvatarLink(email string, size int) string { |
||||
var avatarURL *url.URL |
||||
if setting.EnableFederatedAvatar && setting.LibravatarService != nil { |
||||
// This is the slow path that would need to call LibravatarURL() which
|
||||
// does DNS lookups. Avoid it by issuing a redirect so we don't block
|
||||
// the template render with network requests.
|
||||
return HashedAvatarLink(email, size) |
||||
} else if !setting.DisableGravatar { |
||||
// copy GravatarSourceURL, because we will modify its Path.
|
||||
copyOfGravatarSourceURL := *setting.GravatarSourceURL |
||||
avatarURL = ©OfGravatarSourceURL |
||||
avatarURL.Path = path.Join(avatarURL.Path, HashEmail(email)) |
||||
} else { |
||||
return DefaultAvatarLink() |
||||
} |
||||
|
||||
return MakeFinalAvatarURL(avatarURL, size) |
||||
} |
@ -0,0 +1,180 @@ |
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package avatars |
||||
|
||||
import ( |
||||
"context" |
||||
"net/url" |
||||
"path" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"code.gitea.io/gitea/models/db" |
||||
"code.gitea.io/gitea/modules/base" |
||||
"code.gitea.io/gitea/modules/cache" |
||||
"code.gitea.io/gitea/modules/log" |
||||
"code.gitea.io/gitea/modules/setting" |
||||
) |
||||
|
||||
// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
|
||||
const DefaultAvatarPixelSize = 28 |
||||
|
||||
// AvatarRenderedSizeFactor is the factor by which the default size is increased for finer rendering
|
||||
const AvatarRenderedSizeFactor = 4 |
||||
|
||||
// EmailHash represents a pre-generated hash map (mainly used by LibravatarURL, it queries email server's DNS records)
|
||||
type EmailHash struct { |
||||
Hash string `xorm:"pk varchar(32)"` |
||||
Email string `xorm:"UNIQUE NOT NULL"` |
||||
} |
||||
|
||||
func init() { |
||||
db.RegisterModel(new(EmailHash)) |
||||
} |
||||
|
||||
// DefaultAvatarLink the default avatar link
|
||||
func DefaultAvatarLink() string { |
||||
u, err := url.Parse(setting.AppSubURL) |
||||
if err != nil { |
||||
log.Error("GetUserByEmail: %v", err) |
||||
return "" |
||||
} |
||||
|
||||
u.Path = path.Join(u.Path, "/assets/img/avatar_default.png") |
||||
return u.String() |
||||
} |
||||
|
||||
// HashEmail hashes email address to MD5 string. https://en.gravatar.com/site/implement/hash/
|
||||
func HashEmail(email string) string { |
||||
return base.EncodeMD5(strings.ToLower(strings.TrimSpace(email))) |
||||
} |
||||
|
||||
// GetEmailForHash converts a provided md5sum to the email
|
||||
func GetEmailForHash(md5Sum string) (string, error) { |
||||
return cache.GetString("Avatar:"+md5Sum, func() (string, error) { |
||||
emailHash := EmailHash{ |
||||
Hash: strings.ToLower(strings.TrimSpace(md5Sum)), |
||||
} |
||||
|
||||
_, err := db.GetEngine(db.DefaultContext).Get(&emailHash) |
||||
return emailHash.Email, err |
||||
}) |
||||
} |
||||
|
||||
// LibravatarURL returns the URL for the given email. Slow due to the DNS lookup.
|
||||
// This function should only be called if a federated avatar service is enabled.
|
||||
func LibravatarURL(email string) (*url.URL, error) { |
||||
urlStr, err := setting.LibravatarService.FromEmail(email) |
||||
if err != nil { |
||||
log.Error("LibravatarService.FromEmail(email=%s): error %v", email, err) |
||||
return nil, err |
||||
} |
||||
u, err := url.Parse(urlStr) |
||||
if err != nil { |
||||
log.Error("Failed to parse libravatar url(%s): error %v", urlStr, err) |
||||
return nil, err |
||||
} |
||||
return u, nil |
||||
} |
||||
|
||||
// saveEmailHash returns an avatar link for a provided email,
|
||||
// the email and hash are saved into database, which will be used by GetEmailForHash later
|
||||
func saveEmailHash(email string) string { |
||||
lowerEmail := strings.ToLower(strings.TrimSpace(email)) |
||||
emailHash := HashEmail(lowerEmail) |
||||
_, _ = cache.GetString("Avatar:"+emailHash, func() (string, error) { |
||||
emailHash := &EmailHash{ |
||||
Email: lowerEmail, |
||||
Hash: emailHash, |
||||
} |
||||
// OK we're going to open a session just because I think that that might hide away any problems with postgres reporting errors
|
||||
if err := db.WithTx(func(ctx context.Context) error { |
||||
has, err := db.GetEngine(ctx).Where("email = ? AND hash = ?", emailHash.Email, emailHash.Hash).Get(new(EmailHash)) |
||||
if has || err != nil { |
||||
// Seriously we don't care about any DB problems just return the lowerEmail - we expect the transaction to fail most of the time
|
||||
return nil |
||||
} |
||||
_, _ = db.GetEngine(ctx).Insert(emailHash) |
||||
return nil |
||||
}); err != nil { |
||||
// Seriously we don't care about any DB problems just return the lowerEmail - we expect the transaction to fail most of the time
|
||||
return lowerEmail, nil |
||||
} |
||||
return lowerEmail, nil |
||||
}) |
||||
return emailHash |
||||
} |
||||
|
||||
// GenerateUserAvatarFastLink returns a fast link (302) to the user's avatar: "/user/avatar/${User.Name}/${size}"
|
||||
func GenerateUserAvatarFastLink(userName string, size int) string { |
||||
if size < 0 { |
||||
size = 0 |
||||
} |
||||
return setting.AppSubURL + "/user/avatar/" + userName + "/" + strconv.Itoa(size) |
||||
} |
||||
|
||||
// GenerateUserAvatarImageLink returns a link for `User.Avatar` image file: "/avatars/${User.Avatar}"
|
||||
func GenerateUserAvatarImageLink(userAvatar string, size int) string { |
||||
if size > 0 { |
||||
return setting.AppSubURL + "/avatars/" + userAvatar + "?size=" + strconv.Itoa(size) |
||||
} |
||||
return setting.AppSubURL + "/avatars/" + userAvatar |
||||
} |
||||
|
||||
// generateRecognizedAvatarURL generate a recognized avatar (Gravatar/Libravatar) URL, it modifies the URL so the parameter is passed by a copy
|
||||
func generateRecognizedAvatarURL(u url.URL, size int) string { |
||||
urlQuery := u.Query() |
||||
urlQuery.Set("d", "identicon") |
||||
if size > 0 { |
||||
urlQuery.Set("s", strconv.Itoa(size)) |
||||
} |
||||
u.RawQuery = urlQuery.Encode() |
||||
return u.String() |
||||
} |
||||
|
||||
// generateEmailAvatarLink returns a email avatar link.
|
||||
// if final is true, it may use a slow path (eg: query DNS).
|
||||
// if final is false, it always uses a fast path.
|
||||
func generateEmailAvatarLink(email string, size int, final bool) string { |
||||
email = strings.TrimSpace(email) |
||||
if email == "" { |
||||
return DefaultAvatarLink() |
||||
} |
||||
|
||||
var err error |
||||
if setting.EnableFederatedAvatar && setting.LibravatarService != nil { |
||||
emailHash := saveEmailHash(email) |
||||
if final { |
||||
// for final link, we can spend more time on slow external query
|
||||
var avatarURL *url.URL |
||||
if avatarURL, err = LibravatarURL(email); err != nil { |
||||
return DefaultAvatarLink() |
||||
} |
||||
return generateRecognizedAvatarURL(*avatarURL, size) |
||||
} |
||||
// for non-final link, we should return fast (use a 302 redirection link)
|
||||
urlStr := setting.AppSubURL + "/avatar/" + emailHash |
||||
if size > 0 { |
||||
urlStr += "?size=" + strconv.Itoa(size) |
||||
} |
||||
return urlStr |
||||
} else if !setting.DisableGravatar { |
||||
// copy GravatarSourceURL, because we will modify its Path.
|
||||
avatarURLCopy := *setting.GravatarSourceURL |
||||
avatarURLCopy.Path = path.Join(avatarURLCopy.Path, HashEmail(email)) |
||||
return generateRecognizedAvatarURL(avatarURLCopy, size) |
||||
} |
||||
return DefaultAvatarLink() |
||||
} |
||||
|
||||
//GenerateEmailAvatarFastLink returns a avatar link (fast, the link may be a delegated one: "/avatar/${hash}")
|
||||
func GenerateEmailAvatarFastLink(email string, size int) string { |
||||
return generateEmailAvatarLink(email, size, false) |
||||
} |
||||
|
||||
//GenerateEmailAvatarFinalLink returns a avatar final link (maybe slow)
|
||||
func GenerateEmailAvatarFinalLink(email string, size int) string { |
||||
return generateEmailAvatarLink(email, size, true) |
||||
} |
Loading…
Reference in new issue