mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	Repository avatar fallback configuration (#7087)
* Only show repository avatar in list when one was selected Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adds fallback configuration option for repository avatar Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Implements repository avatar fallback Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adds admin task for deleting generated repository avatars Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Solve linting issues Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Save avatar before updating database * Linting * Update models/repo.go Co-Authored-By: zeripath <art27@cantab.net>
This commit is contained in:
		 Mario Lubenka
					Mario Lubenka
				
			
				
					committed by
					
						 Lunny Xiao
						Lunny Xiao
					
				
			
			
				
	
			
			
			 Lunny Xiao
						Lunny Xiao
					
				
			
						parent
						
							356854fc5f
						
					
				
				
					commit
					8eba27c792
				
			| @@ -505,6 +505,10 @@ SESSION_LIFE_TIME = 86400 | |||||||
| [picture] | [picture] | ||||||
| AVATAR_UPLOAD_PATH = data/avatars | AVATAR_UPLOAD_PATH = data/avatars | ||||||
| REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars | REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars | ||||||
|  | ; How Gitea deals with missing repository avatars | ||||||
|  | ; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used | ||||||
|  | REPOSITORY_AVATAR_FALLBACK = none | ||||||
|  | REPOSITORY_AVATAR_FALLBACK_IMAGE = /img/repo_default.png | ||||||
| ; Max Width and Height of uploaded avatars. | ; Max Width and Height of uploaded avatars. | ||||||
| ; This is to limit the amount of RAM used when resizing the image. | ; This is to limit the amount of RAM used when resizing the image. | ||||||
| AVATAR_MAX_WIDTH = 4096 | AVATAR_MAX_WIDTH = 4096 | ||||||
|   | |||||||
| @@ -292,6 +292,11 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. | |||||||
|    [http://www.libravatar.org](http://www.libravatar.org)). |    [http://www.libravatar.org](http://www.libravatar.org)). | ||||||
| - `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image 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. | - `REPOSITORY_AVATAR_UPLOAD_PATH`: **data/repo-avatars**: Path to store repository avatar image files. | ||||||
|  | - `REPOSITORY_AVATAR_FALLBACK`: **none**: How Gitea deals with missing repository avatars | ||||||
|  |   - none = no avatar will be displayed | ||||||
|  |   - random = random avatar will be generated | ||||||
|  |   - image = default image will be used (which is set in `REPOSITORY_AVATAR_DEFAULT_IMAGE`) | ||||||
|  | - `REPOSITORY_AVATAR_FALLBACK_IMAGE`: **/img/repo_default.png**: Image used as default repository avatar (if `REPOSITORY_AVATAR_FALLBACK` is set to image and none was uploaded) | ||||||
| - `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. | - `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. | ||||||
| - `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height 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. | - `AVATAR_MAX_FILE_SIZE`: **1048576** (1Mb): Maximum avatar image file size in bytes. | ||||||
|   | |||||||
| @@ -2528,17 +2528,78 @@ func (repo *Repository) CustomAvatarPath() string { | |||||||
| 	return filepath.Join(setting.RepositoryAvatarUploadPath, repo.Avatar) | 	return filepath.Join(setting.RepositoryAvatarUploadPath, repo.Avatar) | ||||||
| } | } | ||||||
|  |  | ||||||
| // RelAvatarLink returns a relative link to the user's avatar. | // GenerateRandomAvatar generates a random avatar for repository. | ||||||
| // The link a sub-URL to this site | func (repo *Repository) GenerateRandomAvatar() error { | ||||||
| // Since Gravatar support not needed here - just check for image path. | 	return repo.generateRandomAvatar(x) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (repo *Repository) generateRandomAvatar(e Engine) error { | ||||||
|  | 	idToString := fmt.Sprintf("%d", repo.ID) | ||||||
|  |  | ||||||
|  | 	seed := idToString | ||||||
|  | 	img, err := avatar.RandomImage([]byte(seed)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("RandomImage: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	repo.Avatar = idToString | ||||||
|  | 	if err = os.MkdirAll(filepath.Dir(repo.CustomAvatarPath()), os.ModePerm); err != nil { | ||||||
|  | 		return fmt.Errorf("MkdirAll: %v", err) | ||||||
|  | 	} | ||||||
|  | 	fw, err := os.Create(repo.CustomAvatarPath()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("Create: %v", err) | ||||||
|  | 	} | ||||||
|  | 	defer fw.Close() | ||||||
|  |  | ||||||
|  | 	if err = png.Encode(fw, img); err != nil { | ||||||
|  | 		return fmt.Errorf("Encode: %v", err) | ||||||
|  | 	} | ||||||
|  | 	log.Info("New random avatar created for repository: %d", repo.ID) | ||||||
|  |  | ||||||
|  | 	if _, err := e.ID(repo.ID).Cols("avatar").NoAutoTime().Update(repo); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RemoveRandomAvatars removes the randomly generated avatars that were created for repositories | ||||||
|  | func RemoveRandomAvatars() error { | ||||||
|  | 	var ( | ||||||
|  | 		err error | ||||||
|  | 	) | ||||||
|  | 	err = x. | ||||||
|  | 		Where("id > 0").BufferSize(setting.IterateBufferSize). | ||||||
|  | 		Iterate(new(Repository), | ||||||
|  | 			func(idx int, bean interface{}) error { | ||||||
|  | 				repository := bean.(*Repository) | ||||||
|  | 				stringifiedID := strconv.FormatInt(repository.ID, 10) | ||||||
|  | 				if repository.Avatar == stringifiedID { | ||||||
|  | 					return repository.DeleteAvatar() | ||||||
|  | 				} | ||||||
|  | 				return nil | ||||||
|  | 			}) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RelAvatarLink returns a relative link to the repository's avatar. | ||||||
| func (repo *Repository) RelAvatarLink() string { | func (repo *Repository) RelAvatarLink() string { | ||||||
|  |  | ||||||
| 	// If no avatar - path is empty | 	// If no avatar - path is empty | ||||||
| 	avatarPath := repo.CustomAvatarPath() | 	avatarPath := repo.CustomAvatarPath() | ||||||
| 	if len(avatarPath) <= 0 { | 	if len(avatarPath) <= 0 || !com.IsFile(avatarPath) { | ||||||
| 		return "" | 		switch mode := setting.RepositoryAvatarFallback; mode { | ||||||
| 	} | 		case "image": | ||||||
| 	if !com.IsFile(avatarPath) { | 			return setting.RepositoryAvatarFallbackImage | ||||||
| 		return "" | 		case "random": | ||||||
|  | 			if err := repo.GenerateRandomAvatar(); err != nil { | ||||||
|  | 				log.Error("GenerateRandomAvatar: %v", err) | ||||||
|  | 			} | ||||||
|  | 		default: | ||||||
|  | 			// default behaviour: do not display avatar | ||||||
|  | 			return "" | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	return setting.AppSubURL + "/repo-avatars/" + repo.Avatar | 	return setting.AppSubURL + "/repo-avatars/" + repo.Avatar | ||||||
| } | } | ||||||
|   | |||||||
| @@ -250,16 +250,18 @@ var ( | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Picture settings | 	// Picture settings | ||||||
| 	AvatarUploadPath           string | 	AvatarUploadPath              string | ||||||
| 	AvatarMaxWidth             int | 	AvatarMaxWidth                int | ||||||
| 	AvatarMaxHeight            int | 	AvatarMaxHeight               int | ||||||
| 	GravatarSource             string | 	GravatarSource                string | ||||||
| 	GravatarSourceURL          *url.URL | 	GravatarSourceURL             *url.URL | ||||||
| 	DisableGravatar            bool | 	DisableGravatar               bool | ||||||
| 	EnableFederatedAvatar      bool | 	EnableFederatedAvatar         bool | ||||||
| 	LibravatarService          *libravatar.Libravatar | 	LibravatarService             *libravatar.Libravatar | ||||||
| 	AvatarMaxFileSize          int64 | 	AvatarMaxFileSize             int64 | ||||||
| 	RepositoryAvatarUploadPath string | 	RepositoryAvatarUploadPath    string | ||||||
|  | 	RepositoryAvatarFallback      string | ||||||
|  | 	RepositoryAvatarFallbackImage string | ||||||
|  |  | ||||||
| 	// Log settings | 	// Log settings | ||||||
| 	LogLevel           string | 	LogLevel           string | ||||||
| @@ -842,6 +844,8 @@ func NewContext() { | |||||||
| 	if !filepath.IsAbs(RepositoryAvatarUploadPath) { | 	if !filepath.IsAbs(RepositoryAvatarUploadPath) { | ||||||
| 		RepositoryAvatarUploadPath = path.Join(AppWorkPath, RepositoryAvatarUploadPath) | 		RepositoryAvatarUploadPath = path.Join(AppWorkPath, RepositoryAvatarUploadPath) | ||||||
| 	} | 	} | ||||||
|  | 	RepositoryAvatarFallback = sec.Key("REPOSITORY_AVATAR_FALLBACK").MustString("none") | ||||||
|  | 	RepositoryAvatarFallbackImage = sec.Key("REPOSITORY_AVATAR_FALLBACK_IMAGE").MustString("/img/repo_default.png") | ||||||
| 	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) | 	AvatarMaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1048576) | ||||||
|   | |||||||
| @@ -1522,6 +1522,8 @@ dashboard.delete_repo_archives = Delete all repository archives | |||||||
| dashboard.delete_repo_archives_success = All repository archives have been deleted. | dashboard.delete_repo_archives_success = All repository archives have been deleted. | ||||||
| dashboard.delete_missing_repos = Delete all repositories missing their Git files | dashboard.delete_missing_repos = Delete all repositories missing their Git files | ||||||
| dashboard.delete_missing_repos_success = All repositories missing their Git files have been deleted. | dashboard.delete_missing_repos_success = All repositories missing their Git files have been deleted. | ||||||
|  | dashboard.delete_generated_repository_avatars = Delete generated repository avatars | ||||||
|  | dashboard.delete_generated_repository_avatars_success = Generated repository avatars were deleted. | ||||||
| dashboard.git_gc_repos = Garbage collect all repositories | dashboard.git_gc_repos = Garbage collect all repositories | ||||||
| dashboard.git_gc_repos_success = All repositories have finished garbage collection. | dashboard.git_gc_repos_success = All repositories have finished garbage collection. | ||||||
| dashboard.resync_all_sshkeys = Update the '.ssh/authorized_keys' file with Gitea SSH keys. (Not needed for the built-in SSH server.) | dashboard.resync_all_sshkeys = Update the '.ssh/authorized_keys' file with Gitea SSH keys. (Not needed for the built-in SSH server.) | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								public/img/repo_default.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/img/repo_default.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.4 KiB | 
| @@ -125,6 +125,7 @@ const ( | |||||||
| 	reinitMissingRepository | 	reinitMissingRepository | ||||||
| 	syncExternalUsers | 	syncExternalUsers | ||||||
| 	gitFsck | 	gitFsck | ||||||
|  | 	deleteGeneratedRepositoryAvatars | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Dashboard show admin panel dashboard | // Dashboard show admin panel dashboard | ||||||
| @@ -167,6 +168,9 @@ func Dashboard(ctx *context.Context) { | |||||||
| 		case gitFsck: | 		case gitFsck: | ||||||
| 			success = ctx.Tr("admin.dashboard.git_fsck_started") | 			success = ctx.Tr("admin.dashboard.git_fsck_started") | ||||||
| 			go models.GitFsck() | 			go models.GitFsck() | ||||||
|  | 		case deleteGeneratedRepositoryAvatars: | ||||||
|  | 			success = ctx.Tr("admin.dashboard.delete_generated_repository_avatars_success") | ||||||
|  | 			err = models.RemoveRandomAvatars() | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|   | |||||||
| @@ -53,6 +53,10 @@ | |||||||
| 						<td>{{.i18n.Tr "admin.dashboard.git_fsck"}}</td> | 						<td>{{.i18n.Tr "admin.dashboard.git_fsck"}}</td> | ||||||
| 						<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=9">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td> | 						<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=9">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td> | ||||||
| 					</tr> | 					</tr> | ||||||
|  | 					<tr> | ||||||
|  | 						<td>{{.i18n.Tr "admin.dashboard.delete_generated_repository_avatars"}}</td> | ||||||
|  | 						<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=10">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td> | ||||||
|  | 					</tr> | ||||||
| 				</tbody> | 				</tbody> | ||||||
| 			</table> | 			</table> | ||||||
| 		</div> | 		</div> | ||||||
|   | |||||||
| @@ -2,7 +2,9 @@ | |||||||
| 	{{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}}"> | 				{{if .RelAvatarLink}} | ||||||
|  | 					<img class="ui avatar image" src="{{.RelAvatarLink}}"> | ||||||
|  | 				{{end}} | ||||||
| 				<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}} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user