Repository avatars (#6986)
* Repository avatars - first variant of code from old work for gogs - add migration 87 - add new option in app.ini - add en-US locale string - add new class in repository.less * Add changed index.css, remove unused template name * Update en-us doc about configuration options * Add comments to new functions, add new option to docker app.ini * Add comment for lint * Remove variable, not needed * Fix formatting * Update swagger api template * Check if avatar exists * Fix avatar link/path checks * Typo * TEXT column can't have a default value * Fixes: - remove old avatar file on upload - use ID in name of avatar file - users may upload same files - add simple tests * Fix fmt check * Generate PNG instead of "static" GIF * More informative comment * Fix error message * Update avatar upload checks: - add file size check - add new option - update config docs - add new string to en-us locale * Fixes: - use FileHEader field for check file size - add new test - upload big image * Fix formatting * Update comments * Update log message * Removed wrong style - not needed * Use Sync2 to migrate * Update repos list view - bigger avatar - fix html blocks alignment * A little adjust avatar size * Use small icons for explore/repo list * Use new cool avatar preparation func by @lafriks * Missing changes for new function * Remove unused import, move imports * Missed new option definition in app.ini Add file size check in user/profile avatar upload * Use smaller field length for Avatar * Use session to update repo DB data, update DeleteAvatar - use session too * Fix err variable definition * As suggested @lafriks - return as soon as possible, code readabilityrelease/v1.15
parent
d7494046ac
commit
3fd18838aa
|
@ -504,10 +504,14 @@ SESSION_LIFE_TIME = 86400
|
||||||
|
|
||||||
[picture]
|
[picture]
|
||||||
AVATAR_UPLOAD_PATH = data/avatars
|
AVATAR_UPLOAD_PATH = data/avatars
|
||||||
; Max Width and Height of uploaded avatars. This is to limit the amount of RAM
|
REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars
|
||||||
; used when resizing the image.
|
; Max Width and Height of uploaded avatars.
|
||||||
|
; This is to limit the amount of RAM used when resizing the image.
|
||||||
AVATAR_MAX_WIDTH = 4096
|
AVATAR_MAX_WIDTH = 4096
|
||||||
AVATAR_MAX_HEIGHT = 3072
|
AVATAR_MAX_HEIGHT = 3072
|
||||||
|
; Maximum alloved file size for uploaded avatars.
|
||||||
|
; This is to limit the amount of RAM used when resizing the image.
|
||||||
|
AVATAR_MAX_FILE_SIZE = 1048576
|
||||||
; Chinese users can choose "duoshuo"
|
; Chinese users can choose "duoshuo"
|
||||||
; or a custom avatar source, like: http://cn.gravatar.com/avatar/
|
; or a custom avatar source, like: http://cn.gravatar.com/avatar/
|
||||||
GRAVATAR_SOURCE = gravatar
|
GRAVATAR_SOURCE = gravatar
|
||||||
|
|
|
@ -35,6 +35,7 @@ PROVIDER_CONFIG = /data/gitea/sessions
|
||||||
|
|
||||||
[picture]
|
[picture]
|
||||||
AVATAR_UPLOAD_PATH = /data/gitea/avatars
|
AVATAR_UPLOAD_PATH = /data/gitea/avatars
|
||||||
|
REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars
|
||||||
|
|
||||||
[attachment]
|
[attachment]
|
||||||
PATH = /data/gitea/attachments
|
PATH = /data/gitea/attachments
|
||||||
|
|
|
@ -290,7 +290,11 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
||||||
- `DISABLE_GRAVATAR`: **false**: Enable this to use local avatars only.
|
- `DISABLE_GRAVATAR`: **false**: Enable this to use local avatars only.
|
||||||
- `ENABLE_FEDERATED_AVATAR`: **false**: Enable support for federated avatars (see
|
- `ENABLE_FEDERATED_AVATAR`: **false**: Enable support for federated avatars (see
|
||||||
[http://www.libravatar.org](http://www.libravatar.org)).
|
[http://www.libravatar.org](http://www.libravatar.org)).
|
||||||
- `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store local and cached 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.
|
||||||
|
- `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.
|
||||||
|
|
||||||
## Attachment (`attachment`)
|
## Attachment (`attachment`)
|
||||||
|
|
||||||
|
|
|
@ -227,6 +227,8 @@ var migrations = []Migration{
|
||||||
NewMigration("hash application token", hashAppToken),
|
NewMigration("hash application token", hashAppToken),
|
||||||
// v86 -> v87
|
// v86 -> v87
|
||||||
NewMigration("add http method to webhook", addHTTPMethodToWebhook),
|
NewMigration("add http method to webhook", addHTTPMethodToWebhook),
|
||||||
|
// v87 -> v88
|
||||||
|
NewMigration("add avatar field to repository", addAvatarFieldToRepository),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate database to current version
|
// Migrate database to current version
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright 2019 Gitea. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-xorm/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addAvatarFieldToRepository(x *xorm.Engine) error {
|
||||||
|
type Repository struct {
|
||||||
|
// ID(10-20)-md5(32) - must fit into 64 symbols
|
||||||
|
Avatar string `xorm:"VARCHAR(64)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Sync2(new(Repository))
|
||||||
|
}
|
134
models/repo.go
134
models/repo.go
|
@ -7,9 +7,14 @@ package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/md5"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
|
||||||
|
// Needed for jpeg support
|
||||||
|
_ "image/jpeg"
|
||||||
|
"image/png"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
@ -21,6 +26,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/avatar"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
@ -166,6 +172,9 @@ type Repository struct {
|
||||||
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
|
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
Topics []string `xorm:"TEXT JSON"`
|
Topics []string `xorm:"TEXT JSON"`
|
||||||
|
|
||||||
|
// Avatar: ID(10-20)-md5(32) - must fit into 64 symbols
|
||||||
|
Avatar string `xorm:"VARCHAR(64)"`
|
||||||
|
|
||||||
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
|
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
|
||||||
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
|
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
|
||||||
}
|
}
|
||||||
|
@ -290,6 +299,7 @@ func (repo *Repository) innerAPIFormat(e Engine, mode AccessMode, isParent bool)
|
||||||
Created: repo.CreatedUnix.AsTime(),
|
Created: repo.CreatedUnix.AsTime(),
|
||||||
Updated: repo.UpdatedUnix.AsTime(),
|
Updated: repo.UpdatedUnix.AsTime(),
|
||||||
Permissions: permission,
|
Permissions: permission,
|
||||||
|
AvatarURL: repo.AvatarLink(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1869,6 +1879,15 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
|
||||||
go HookQueue.Add(repo.ID)
|
go HookQueue.Add(repo.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(repo.Avatar) > 0 {
|
||||||
|
avatarPath := repo.CustomAvatarPath()
|
||||||
|
if com.IsExist(avatarPath) {
|
||||||
|
if err := os.Remove(avatarPath); err != nil {
|
||||||
|
return fmt.Errorf("Failed to remove %s: %v", avatarPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DeleteRepoFromIndexer(repo)
|
DeleteRepoFromIndexer(repo)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -2452,3 +2471,118 @@ func (repo *Repository) GetUserFork(userID int64) (*Repository, error) {
|
||||||
}
|
}
|
||||||
return &forkedRepo, nil
|
return &forkedRepo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CustomAvatarPath returns repository custom avatar file path.
|
||||||
|
func (repo *Repository) CustomAvatarPath() string {
|
||||||
|
// Avatar empty by default
|
||||||
|
if len(repo.Avatar) <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
func (repo *Repository) RelAvatarLink() string {
|
||||||
|
// If no avatar - path is empty
|
||||||
|
avatarPath := repo.CustomAvatarPath()
|
||||||
|
if len(avatarPath) <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if !com.IsFile(avatarPath) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return setting.AppSubURL + "/repo-avatars/" + repo.Avatar
|
||||||
|
}
|
||||||
|
|
||||||
|
// AvatarLink returns user avatar absolute link.
|
||||||
|
func (repo *Repository) AvatarLink() string {
|
||||||
|
link := repo.RelAvatarLink()
|
||||||
|
// link may be empty!
|
||||||
|
if len(link) > 0 {
|
||||||
|
if link[0] == '/' && link[1] != '/' {
|
||||||
|
return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return link
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadAvatar saves custom avatar for repository.
|
||||||
|
// FIXME: split uploads to different subdirs in case we have massive number of repos.
|
||||||
|
func (repo *Repository) UploadAvatar(data []byte) error {
|
||||||
|
m, err := avatar.Prepare(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := x.NewSession()
|
||||||
|
defer sess.Close()
|
||||||
|
if err = sess.Begin(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldAvatarPath := repo.CustomAvatarPath()
|
||||||
|
|
||||||
|
// Users can upload the same image to other repo - prefix it with ID
|
||||||
|
// Then repo will be removed - only it avatar file will be removed
|
||||||
|
repo.Avatar = fmt.Sprintf("%d-%x", repo.ID, md5.Sum(data))
|
||||||
|
if _, err := sess.ID(repo.ID).Cols("avatar").Update(repo); err != nil {
|
||||||
|
return fmt.Errorf("UploadAvatar: Update repository avatar: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(setting.RepositoryAvatarUploadPath, os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("UploadAvatar: Failed to create dir %s: %v", setting.RepositoryAvatarUploadPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fw, err := os.Create(repo.CustomAvatarPath())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("UploadAvatar: Create file: %v", err)
|
||||||
|
}
|
||||||
|
defer fw.Close()
|
||||||
|
|
||||||
|
if err = png.Encode(fw, *m); err != nil {
|
||||||
|
return fmt.Errorf("UploadAvatar: Encode png: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(oldAvatarPath) > 0 && oldAvatarPath != repo.CustomAvatarPath() {
|
||||||
|
if err := os.Remove(oldAvatarPath); err != nil {
|
||||||
|
return fmt.Errorf("UploadAvatar: Failed to remove old repo avatar %s: %v", oldAvatarPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sess.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAvatar deletes the repos's custom avatar.
|
||||||
|
func (repo *Repository) DeleteAvatar() error {
|
||||||
|
|
||||||
|
// Avatar not exists
|
||||||
|
if len(repo.Avatar) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarPath := repo.CustomAvatarPath()
|
||||||
|
log.Trace("DeleteAvatar[%d]: %s", repo.ID, avatarPath)
|
||||||
|
|
||||||
|
sess := x.NewSession()
|
||||||
|
defer sess.Close()
|
||||||
|
if err := sess.Begin(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.Avatar = ""
|
||||||
|
if _, err := sess.ID(repo.ID).Cols("avatar").Update(repo); err != nil {
|
||||||
|
return fmt.Errorf("DeleteAvatar: Update repository avatar: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(avatarPath); err == nil {
|
||||||
|
if err := os.Remove(avatarPath); err != nil {
|
||||||
|
return fmt.Errorf("DeleteAvatar: Failed to remove %s: %v", avatarPath, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// // Schrodinger: file may or may not exist. See err for details.
|
||||||
|
log.Trace("DeleteAvatar[%d]: %v", err)
|
||||||
|
}
|
||||||
|
return sess.Commit()
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,11 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
@ -158,3 +163,51 @@ func TestTransferOwnership(t *testing.T) {
|
||||||
|
|
||||||
CheckConsistencyFor(t, &Repository{}, &User{}, &Team{})
|
CheckConsistencyFor(t, &Repository{}, &User{}, &Team{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUploadAvatar(t *testing.T) {
|
||||||
|
|
||||||
|
// Generate image
|
||||||
|
myImage := image.NewRGBA(image.Rect(0, 0, 1, 1))
|
||||||
|
var buff bytes.Buffer
|
||||||
|
png.Encode(&buff, myImage)
|
||||||
|
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
repo := AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository)
|
||||||
|
|
||||||
|
err := repo.UploadAvatar(buff.Bytes())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, fmt.Sprintf("%d-%x", 10, md5.Sum(buff.Bytes())), repo.Avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUploadBigAvatar(t *testing.T) {
|
||||||
|
|
||||||
|
// Generate BIG image
|
||||||
|
myImage := image.NewRGBA(image.Rect(0, 0, 5000, 1))
|
||||||
|
var buff bytes.Buffer
|
||||||
|
png.Encode(&buff, myImage)
|
||||||
|
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
repo := AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository)
|
||||||
|
|
||||||
|
err := repo.UploadAvatar(buff.Bytes())
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteAvatar(t *testing.T) {
|
||||||
|
|
||||||
|
// Generate image
|
||||||
|
myImage := image.NewRGBA(image.Rect(0, 0, 1, 1))
|
||||||
|
var buff bytes.Buffer
|
||||||
|
png.Encode(&buff, myImage)
|
||||||
|
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
repo := AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository)
|
||||||
|
|
||||||
|
err := repo.UploadAvatar(buff.Bytes())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = repo.DeleteAvatar()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "", repo.Avatar)
|
||||||
|
}
|
||||||
|
|
|
@ -258,6 +258,8 @@ var (
|
||||||
DisableGravatar bool
|
DisableGravatar bool
|
||||||
EnableFederatedAvatar bool
|
EnableFederatedAvatar bool
|
||||||
LibravatarService *libravatar.Libravatar
|
LibravatarService *libravatar.Libravatar
|
||||||
|
AvatarMaxFileSize int64
|
||||||
|
RepositoryAvatarUploadPath string
|
||||||
|
|
||||||
// Log settings
|
// Log settings
|
||||||
LogLevel string
|
LogLevel string
|
||||||
|
@ -835,8 +837,14 @@ func NewContext() {
|
||||||
if !filepath.IsAbs(AvatarUploadPath) {
|
if !filepath.IsAbs(AvatarUploadPath) {
|
||||||
AvatarUploadPath = path.Join(AppWorkPath, AvatarUploadPath)
|
AvatarUploadPath = path.Join(AppWorkPath, AvatarUploadPath)
|
||||||
}
|
}
|
||||||
|
RepositoryAvatarUploadPath = sec.Key("REPOSITORY_AVATAR_UPLOAD_PATH").MustString(path.Join(AppDataPath, "repo-avatars"))
|
||||||
|
forcePathSeparator(RepositoryAvatarUploadPath)
|
||||||
|
if !filepath.IsAbs(RepositoryAvatarUploadPath) {
|
||||||
|
RepositoryAvatarUploadPath = path.Join(AppWorkPath, RepositoryAvatarUploadPath)
|
||||||
|
}
|
||||||
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)
|
||||||
switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source {
|
switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source {
|
||||||
case "duoshuo":
|
case "duoshuo":
|
||||||
GravatarSource = "http://gravatar.duoshuo.com/avatar/"
|
GravatarSource = "http://gravatar.duoshuo.com/avatar/"
|
||||||
|
|
|
@ -43,6 +43,7 @@ type Repository struct {
|
||||||
// swagger:strfmt date-time
|
// swagger:strfmt date-time
|
||||||
Updated time.Time `json:"updated_at"`
|
Updated time.Time `json:"updated_at"`
|
||||||
Permissions *Permission `json:"permissions,omitempty"`
|
Permissions *Permission `json:"permissions,omitempty"`
|
||||||
|
AvatarURL string `json:"avatar_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRepoOption options when creating repository
|
// CreateRepoOption options when creating repository
|
||||||
|
|
|
@ -389,6 +389,7 @@ choose_new_avatar = Choose new avatar
|
||||||
update_avatar = Update Avatar
|
update_avatar = Update Avatar
|
||||||
delete_current_avatar = Delete Current Avatar
|
delete_current_avatar = Delete Current Avatar
|
||||||
uploaded_avatar_not_a_image = The uploaded file is not an image.
|
uploaded_avatar_not_a_image = The uploaded file is not an image.
|
||||||
|
uploaded_avatar_is_too_big = The uploaded file has exceeded the maximum size.
|
||||||
update_avatar_success = Your avatar has been updated.
|
update_avatar_success = Your avatar has been updated.
|
||||||
|
|
||||||
change_password = Update Password
|
change_password = Update Password
|
||||||
|
@ -1314,6 +1315,7 @@ settings.unarchive.header = Un-Archive This Repo
|
||||||
settings.unarchive.text = Un-Archiving the repo will restore its ability to recieve commits and pushes, as well as new issues and pull-requests.
|
settings.unarchive.text = Un-Archiving the repo will restore its ability to recieve commits and pushes, as well as new issues and pull-requests.
|
||||||
settings.unarchive.success = The repo was successfully un-archived.
|
settings.unarchive.success = The repo was successfully un-archived.
|
||||||
settings.unarchive.error = An error occured while trying to un-archive the repo. See the log for more details.
|
settings.unarchive.error = An error occured while trying to un-archive the repo. See the log for more details.
|
||||||
|
settings.update_avatar_success = The repository avatar has been updated.
|
||||||
|
|
||||||
diff.browse_source = Browse Source
|
diff.browse_source = Browse Source
|
||||||
diff.parent = parent
|
diff.parent = parent
|
||||||
|
|
|
@ -956,6 +956,7 @@ tbody.commit-list{vertical-align:baseline}
|
||||||
.ui.repository.list .item .ui.header .metas span:not(:last-child){margin-right:5px}
|
.ui.repository.list .item .ui.header .metas span:not(:last-child){margin-right:5px}
|
||||||
.ui.repository.list .item .time{font-size:12px;color:grey}
|
.ui.repository.list .item .time{font-size:12px;color:grey}
|
||||||
.ui.repository.list .item .ui.tags{margin-bottom:1em}
|
.ui.repository.list .item .ui.tags{margin-bottom:1em}
|
||||||
|
.ui.repository.list .item .ui.avatar.image{width:24px;height:24px}
|
||||||
.ui.repository.branches .time{font-size:12px;color:grey}
|
.ui.repository.branches .time{font-size:12px;color:grey}
|
||||||
.ui.user.list .item{padding-bottom:25px}
|
.ui.user.list .item{padding-bottom:25px}
|
||||||
.ui.user.list .item:not(:first-child){border-top:1px solid #eee;padding-top:25px}
|
.ui.user.list .item:not(:first-child){border-top:1px solid #eee;padding-top:25px}
|
||||||
|
|
|
@ -53,6 +53,11 @@
|
||||||
.ui.tags {
|
.ui.tags {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui.avatar.image {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,14 @@ package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Unknwon/com"
|
||||||
"mvdan.cc/xurls/v2"
|
"mvdan.cc/xurls/v2"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
|
@ -727,3 +730,59 @@ func init() {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateAvatarSetting update repo's avatar
|
||||||
|
func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm) error {
|
||||||
|
ctxRepo := ctx.Repo.Repository
|
||||||
|
|
||||||
|
if form.Avatar == nil {
|
||||||
|
// No avatar is uploaded and we not removing it here.
|
||||||
|
// No random avatar generated here.
|
||||||
|
// Just exit, no action.
|
||||||
|
if !com.IsFile(ctxRepo.CustomAvatarPath()) {
|
||||||
|
log.Trace("No avatar was uploaded for repo: %d. Default icon will appear instead.", ctxRepo.ID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := form.Avatar.Open()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Avatar.Open: %v", err)
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
if form.Avatar.Size > setting.AvatarMaxFileSize {
|
||||||
|
return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big"))
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ioutil.ReadAll: %v", err)
|
||||||
|
}
|
||||||
|
if !base.IsImageFile(data) {
|
||||||
|
return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
|
||||||
|
}
|
||||||
|
if err = ctxRepo.UploadAvatar(data); err != nil {
|
||||||
|
return fmt.Errorf("UploadAvatar: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SettingsAvatar save new POSTed repository avatar
|
||||||
|
func SettingsAvatar(ctx *context.Context, form auth.AvatarForm) {
|
||||||
|
form.Source = auth.AvatarLocal
|
||||||
|
if err := UpdateAvatarSetting(ctx, form); err != nil {
|
||||||
|
ctx.Flash.Error(err.Error())
|
||||||
|
} else {
|
||||||
|
ctx.Flash.Success(ctx.Tr("repo.settings.update_avatar_success"))
|
||||||
|
}
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SettingsDeleteAvatar delete repository avatar
|
||||||
|
func SettingsDeleteAvatar(ctx *context.Context) {
|
||||||
|
if err := ctx.Repo.Repository.DeleteAvatar(); err != nil {
|
||||||
|
ctx.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err))
|
||||||
|
}
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
|
||||||
|
}
|
||||||
|
|
|
@ -159,6 +159,14 @@ func NewMacaron() *macaron.Macaron {
|
||||||
ExpiresAfter: time.Hour * 6,
|
ExpiresAfter: time.Hour * 6,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
m.Use(public.StaticHandler(
|
||||||
|
setting.RepositoryAvatarUploadPath,
|
||||||
|
&public.Options{
|
||||||
|
Prefix: "repo-avatars",
|
||||||
|
SkipLogging: setting.DisableRouterLog,
|
||||||
|
ExpiresAfter: time.Hour * 6,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
|
||||||
m.Use(templates.HTMLRenderer())
|
m.Use(templates.HTMLRenderer())
|
||||||
models.InitMailRender(templates.Mailer())
|
models.InitMailRender(templates.Mailer())
|
||||||
|
@ -613,6 +621,9 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
m.Group("/settings", func() {
|
m.Group("/settings", func() {
|
||||||
m.Combo("").Get(repo.Settings).
|
m.Combo("").Get(repo.Settings).
|
||||||
Post(bindIgnErr(auth.RepoSettingForm{}), repo.SettingsPost)
|
Post(bindIgnErr(auth.RepoSettingForm{}), repo.SettingsPost)
|
||||||
|
m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), repo.SettingsAvatar)
|
||||||
|
m.Post("/avatar/delete", repo.SettingsDeleteAvatar)
|
||||||
|
|
||||||
m.Group("/collaboration", func() {
|
m.Group("/collaboration", func() {
|
||||||
m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost)
|
m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost)
|
||||||
m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
|
m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
|
||||||
|
|
|
@ -127,6 +127,10 @@ func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm, ctxUser *mo
|
||||||
}
|
}
|
||||||
defer fr.Close()
|
defer fr.Close()
|
||||||
|
|
||||||
|
if form.Avatar.Size > setting.AvatarMaxFileSize {
|
||||||
|
return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big"))
|
||||||
|
}
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(fr)
|
data, err := ioutil.ReadAll(fr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ioutil.ReadAll: %v", err)
|
return fmt.Errorf("ioutil.ReadAll: %v", err)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
{{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}}">
|
||||||
<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}}
|
||||||
|
@ -22,6 +23,7 @@
|
||||||
<span class="text grey"><i class="octicon octicon-git-branch"></i> {{.NumForks}}</span>
|
<span class="text grey"><i class="octicon octicon-git-branch"></i> {{.NumForks}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="description">
|
||||||
{{if .DescriptionHTML}}<p class="has-emoji">{{.DescriptionHTML}}</p>{{end}}
|
{{if .DescriptionHTML}}<p class="has-emoji">{{.DescriptionHTML}}</p>{{end}}
|
||||||
{{if .Topics }}
|
{{if .Topics }}
|
||||||
<div class="ui tags">
|
<div class="ui tags">
|
||||||
|
@ -32,6 +34,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
<p class="time">{{$.i18n.Tr "org.repo_updated"}} {{TimeSinceUnix .UpdatedUnix $.i18n.Lang}}</p>
|
<p class="time">{{$.i18n.Tr "org.repo_updated"}} {{TimeSinceUnix .UpdatedUnix $.i18n.Lang}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div>
|
<div>
|
||||||
{{$.i18n.Tr "explore.repo_no_results"}}
|
{{$.i18n.Tr "explore.repo_no_results"}}
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div class="repo-header">
|
<div class="repo-header">
|
||||||
<div class="ui huge breadcrumb repo-title">
|
<div class="ui huge breadcrumb repo-title">
|
||||||
|
{{if .RelAvatarLink}}
|
||||||
|
<img class="ui avatar image" src="{{.RelAvatarLink}}">
|
||||||
|
{{else}}
|
||||||
<i class="mega-octicon octicon-{{if .IsPrivate}}lock{{else if .IsMirror}}repo-clone{{else if .IsFork}}repo-forked{{else}}repo{{end}}"></i>
|
<i class="mega-octicon octicon-{{if .IsPrivate}}lock{{else if .IsMirror}}repo-clone{{else if .IsFork}}repo-forked{{else}}repo{{end}}"></i>
|
||||||
|
{{end}}
|
||||||
<a href="{{AppSubUrl}}/{{.Owner.Name}}">{{.Owner.Name}}</a>
|
<a href="{{AppSubUrl}}/{{.Owner.Name}}">{{.Owner.Name}}</a>
|
||||||
<div class="divider"> / </div>
|
<div class="divider"> / </div>
|
||||||
<a href="{{$.RepoLink}}">{{.Name}}</a>
|
<a href="{{$.RepoLink}}">{{.Name}}</a>
|
||||||
|
|
|
@ -41,6 +41,22 @@
|
||||||
<button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button>
|
<button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
|
||||||
|
<form class="ui form" action="{{.Link}}/avatar" method="post" enctype="multipart/form-data">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<div class="inline field">
|
||||||
|
<label for="avatar">{{.i18n.Tr "settings.choose_new_avatar"}}</label>
|
||||||
|
<input name="avatar" type="file" >
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<button class="ui green button">{{$.i18n.Tr "settings.update_avatar"}}</button>
|
||||||
|
<a class="ui red button delete-post" data-request-url="{{.Link}}/avatar/delete" data-done-url="{{.Link}}">{{$.i18n.Tr "settings.delete_current_avatar"}}</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .Repository.IsMirror}}
|
{{if .Repository.IsMirror}}
|
||||||
|
|
|
@ -9066,6 +9066,10 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"x-go-name": "Archived"
|
"x-go-name": "Archived"
|
||||||
},
|
},
|
||||||
|
"avatar_url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "AvatarURL"
|
||||||
|
},
|
||||||
"clone_url": {
|
"clone_url": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "CloneURL"
|
"x-go-name": "CloneURL"
|
||||||
|
|
Loading…
Reference in New Issue