* 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 readabilitytags/v1.21.12.1
| @@ -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 | |||||
| ; used when resizing the image. | |||||
| REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars | |||||
| ; 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)) | |||||
| } | |||||
| @@ -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) | |||||
| } | |||||
| @@ -250,14 +250,16 @@ var ( | |||||
| } | } | ||||
| // Picture settings | // Picture settings | ||||
| AvatarUploadPath string | |||||
| AvatarMaxWidth int | |||||
| AvatarMaxHeight int | |||||
| GravatarSource string | |||||
| GravatarSourceURL *url.URL | |||||
| DisableGravatar bool | |||||
| EnableFederatedAvatar bool | |||||
| LibravatarService *libravatar.Libravatar | |||||
| AvatarUploadPath string | |||||
| AvatarMaxWidth int | |||||
| AvatarMaxHeight int | |||||
| GravatarSource string | |||||
| GravatarSourceURL *url.URL | |||||
| DisableGravatar bool | |||||
| EnableFederatedAvatar bool | |||||
| 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}} | ||||
| @@ -14,7 +15,7 @@ | |||||
| <span><i class="octicon octicon-repo-clone"></i></span> | <span><i class="octicon octicon-repo-clone"></i></span> | ||||
| {{else if .Owner}} | {{else if .Owner}} | ||||
| {{if .Owner.Visibility.IsPrivate}} | {{if .Owner.Visibility.IsPrivate}} | ||||
| <span class="text gold"><i class="octicon octicon-lock"></i></span> | |||||
| <span class="text gold"><i class="octicon octicon-lock"></i></span> | |||||
| {{end}} | {{end}} | ||||
| {{end}} | {{end}} | ||||
| <div class="ui right metas"> | <div class="ui right metas"> | ||||
| @@ -22,15 +23,17 @@ | |||||
| <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> | ||||
| {{if .DescriptionHTML}}<p class="has-emoji">{{.DescriptionHTML}}</p>{{end}} | |||||
| {{if .Topics }} | |||||
| <div class="ui tags"> | |||||
| {{range .Topics}} | |||||
| {{if ne . "" }}<a href="{{AppSubUrl}}/explore/repos?q={{.}}&topic=1"><div class="ui small label topic">{{.}}</div></a>{{end}} | |||||
| <div class="description"> | |||||
| {{if .DescriptionHTML}}<p class="has-emoji">{{.DescriptionHTML}}</p>{{end}} | |||||
| {{if .Topics }} | |||||
| <div class="ui tags"> | |||||
| {{range .Topics}} | |||||
| {{if ne . "" }}<a href="{{AppSubUrl}}/explore/repos?q={{.}}&topic=1"><div class="ui small label topic">{{.}}</div></a>{{end}} | |||||
| {{end}} | |||||
| </div> | |||||
| {{end}} | {{end}} | ||||
| </div> | |||||
| {{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> | ||||
| @@ -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" | ||||