mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-27 00:23:41 +09:00 
			
		
		
		
	Refactor Cron and merge dashboard tasks (#10745)
* Refactor Cron and merge dashboard tasks * Merge Cron and Dashboard tasks * Make every cron task report a system notice on completion * Refactor the creation of these tasks * Ensure that execution counts of tasks is correct * Allow cron tasks to be started from the cron page * golangci-lint fixes * Enforce that only one task with the same name can be registered Signed-off-by: Andrew Thornton <art27@cantab.net> * fix name check Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @guillep2k * as per @lafriks Signed-off-by: Andrew Thornton <art27@cantab.net> * Add git.CommandContext variants Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: Lauris BH <lauris@nix.lv> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
		| @@ -151,7 +151,7 @@ func TestLDAPUserSync(t *testing.T) { | ||||
| 	} | ||||
| 	defer prepareTestEnv(t)() | ||||
| 	addAuthSourceLDAP(t, "") | ||||
| 	models.SyncExternalUsers(context.Background()) | ||||
| 	models.SyncExternalUsers(context.Background(), true) | ||||
|  | ||||
| 	session := loginUser(t, "user1") | ||||
| 	// Check if users exists | ||||
| @@ -216,7 +216,7 @@ func TestLDAPUserSSHKeySync(t *testing.T) { | ||||
| 	defer prepareTestEnv(t)() | ||||
| 	addAuthSourceLDAP(t, "sshPublicKey") | ||||
|  | ||||
| 	models.SyncExternalUsers(context.Background()) | ||||
| 	models.SyncExternalUsers(context.Background(), true) | ||||
|  | ||||
| 	// Check if users has SSH keys synced | ||||
| 	for _, u := range gitLDAPUsers { | ||||
|   | ||||
| @@ -20,6 +20,8 @@ type NoticeType int | ||||
| const ( | ||||
| 	//NoticeRepository type | ||||
| 	NoticeRepository NoticeType = iota + 1 | ||||
| 	// NoticeTask type | ||||
| 	NoticeTask | ||||
| ) | ||||
|  | ||||
| // Notice represents a system notice for admin. | ||||
| @@ -36,11 +38,14 @@ func (n *Notice) TrStr() string { | ||||
| } | ||||
|  | ||||
| // CreateNotice creates new system notice. | ||||
| func CreateNotice(tp NoticeType, desc string) error { | ||||
| 	return createNotice(x, tp, desc) | ||||
| func CreateNotice(tp NoticeType, desc string, args ...interface{}) error { | ||||
| 	return createNotice(x, tp, desc, args...) | ||||
| } | ||||
|  | ||||
| func createNotice(e Engine, tp NoticeType, desc string) error { | ||||
| func createNotice(e Engine, tp NoticeType, desc string, args ...interface{}) error { | ||||
| 	if len(args) > 0 { | ||||
| 		desc = fmt.Sprintf(desc, args...) | ||||
| 	} | ||||
| 	n := &Notice{ | ||||
| 		Type:        tp, | ||||
| 		Description: desc, | ||||
| @@ -50,8 +55,8 @@ func createNotice(e Engine, tp NoticeType, desc string) error { | ||||
| } | ||||
|  | ||||
| // CreateRepositoryNotice creates new system notice with type NoticeRepository. | ||||
| func CreateRepositoryNotice(desc string) error { | ||||
| 	return createNotice(x, NoticeRepository, desc) | ||||
| func CreateRepositoryNotice(desc string, args ...interface{}) error { | ||||
| 	return createNotice(x, NoticeRepository, desc, args...) | ||||
| } | ||||
|  | ||||
| // RemoveAllWithNotice removes all directories in given path and | ||||
|   | ||||
| @@ -12,7 +12,6 @@ import ( | ||||
|  | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
|  | ||||
| @@ -561,11 +560,11 @@ func RemoveDeletedBranch(repoID int64, branch string) error { | ||||
| } | ||||
|  | ||||
| // RemoveOldDeletedBranches removes old deleted branches | ||||
| func RemoveOldDeletedBranches(ctx context.Context) { | ||||
| func RemoveOldDeletedBranches(ctx context.Context, olderThan time.Duration) { | ||||
| 	// Nothing to do for shutdown or terminate | ||||
| 	log.Trace("Doing: DeletedBranchesCleanup") | ||||
|  | ||||
| 	deleteBefore := time.Now().Add(-setting.Cron.DeletedBranchesCleanup.OlderThan) | ||||
| 	deleteBefore := time.Now().Add(-olderThan) | ||||
| 	_, err := x.Where("deleted_unix < ?", deleteBefore.Unix()).Delete(new(DeletedBranch)) | ||||
| 	if err != nil { | ||||
| 		log.Error("DeletedBranchesCleanup: %v", err) | ||||
|   | ||||
| @@ -85,6 +85,28 @@ func (err ErrSSHDisabled) Error() string { | ||||
| 	return "SSH is disabled" | ||||
| } | ||||
|  | ||||
| // ErrCancelled represents an error due to context cancellation | ||||
| type ErrCancelled struct { | ||||
| 	Message string | ||||
| } | ||||
|  | ||||
| // IsErrCancelled checks if an error is a ErrCancelled. | ||||
| func IsErrCancelled(err error) bool { | ||||
| 	_, ok := err.(ErrCancelled) | ||||
| 	return ok | ||||
| } | ||||
|  | ||||
| func (err ErrCancelled) Error() string { | ||||
| 	return "Cancelled: " + err.Message | ||||
| } | ||||
|  | ||||
| // ErrCancelledf returns an ErrCancelled for the provided format and args | ||||
| func ErrCancelledf(format string, args ...interface{}) error { | ||||
| 	return ErrCancelled{ | ||||
| 		fmt.Sprintf(format, args...), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| //  ____ ___ | ||||
| // |    |   \______ ___________ | ||||
| // |    |   /  ___// __ \_  __ \ | ||||
|   | ||||
| @@ -1853,35 +1853,44 @@ func GetPrivateRepositoryCount(u *User) (int64, error) { | ||||
| } | ||||
|  | ||||
| // DeleteRepositoryArchives deletes all repositories' archives. | ||||
| func DeleteRepositoryArchives() error { | ||||
| func DeleteRepositoryArchives(ctx context.Context) error { | ||||
| 	return x. | ||||
| 		Where("id > 0"). | ||||
| 		Iterate(new(Repository), | ||||
| 			func(idx int, bean interface{}) error { | ||||
| 				repo := bean.(*Repository) | ||||
| 				select { | ||||
| 				case <-ctx.Done(): | ||||
| 					return ErrCancelledf("before deleting repository archives for %s", repo.FullName()) | ||||
| 				default: | ||||
| 				} | ||||
| 				return os.RemoveAll(filepath.Join(repo.RepoPath(), "archives")) | ||||
| 			}) | ||||
| } | ||||
|  | ||||
| // DeleteOldRepositoryArchives deletes old repository archives. | ||||
| func DeleteOldRepositoryArchives(ctx context.Context) { | ||||
| func DeleteOldRepositoryArchives(ctx context.Context, olderThan time.Duration) error { | ||||
| 	log.Trace("Doing: ArchiveCleanup") | ||||
|  | ||||
| 	if err := x.Where("id > 0").Iterate(new(Repository), func(idx int, bean interface{}) error { | ||||
| 		return deleteOldRepositoryArchives(ctx, idx, bean) | ||||
| 		return deleteOldRepositoryArchives(ctx, olderThan, idx, bean) | ||||
| 	}); err != nil { | ||||
| 		log.Error("ArchiveClean: %v", err) | ||||
| 		log.Trace("Error: ArchiveClean: %v", err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	log.Trace("Finished: ArchiveCleanup") | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func deleteOldRepositoryArchives(ctx context.Context, idx int, bean interface{}) error { | ||||
| func deleteOldRepositoryArchives(ctx context.Context, olderThan time.Duration, idx int, bean interface{}) error { | ||||
| 	repo := bean.(*Repository) | ||||
| 	basePath := filepath.Join(repo.RepoPath(), "archives") | ||||
|  | ||||
| 	for _, ty := range []string{"zip", "targz"} { | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			return fmt.Errorf("Aborted due to shutdown:\nin delete of old repository archives %v\nat delete file %s", repo, ty) | ||||
| 			return ErrCancelledf("before deleting old repository archives with filetype %s for %s", ty, repo.FullName()) | ||||
| 		default: | ||||
| 		} | ||||
|  | ||||
| @@ -1904,12 +1913,12 @@ func deleteOldRepositoryArchives(ctx context.Context, idx int, bean interface{}) | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		minimumOldestTime := time.Now().Add(-setting.Cron.ArchiveCleanup.OlderThan) | ||||
| 		minimumOldestTime := time.Now().Add(-olderThan) | ||||
| 		for _, info := range files { | ||||
| 			if info.ModTime().Before(minimumOldestTime) && !info.IsDir() { | ||||
| 				select { | ||||
| 				case <-ctx.Done(): | ||||
| 					return fmt.Errorf("Aborted due to shutdown:\nin delete of old repository archives %v\nat delete file %s - %s", repo, ty, info.Name()) | ||||
| 					return ErrCancelledf("before deleting old repository archive file %s with filetype %s for %s", info.Name(), ty, repo.FullName()) | ||||
| 				default: | ||||
| 				} | ||||
| 				toDelete := filepath.Join(path, info.Name()) | ||||
| @@ -1936,13 +1945,13 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) { | ||||
| 		return | ||||
| 	} | ||||
| 	for _, result := range results { | ||||
| 		id := com.StrTo(result["id"]).MustInt64() | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			log.Warn("CheckRepoStats: Aborting due to shutdown") | ||||
| 			log.Warn("CheckRepoStats: Cancelled before checking %s for Repo[%d]", checker.desc, id) | ||||
| 			return | ||||
| 		default: | ||||
| 		} | ||||
| 		id := com.StrTo(result["id"]).MustInt64() | ||||
| 		log.Trace("Updating %s: %d", checker.desc, id) | ||||
| 		_, err = x.Exec(checker.correctSQL, id, id) | ||||
| 		if err != nil { | ||||
| @@ -1952,7 +1961,7 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) { | ||||
| } | ||||
|  | ||||
| // CheckRepoStats checks the repository stats | ||||
| func CheckRepoStats(ctx context.Context) { | ||||
| func CheckRepoStats(ctx context.Context) error { | ||||
| 	log.Trace("Doing: CheckRepoStats") | ||||
|  | ||||
| 	checkers := []*repoChecker{ | ||||
| @@ -1987,13 +1996,13 @@ func CheckRepoStats(ctx context.Context) { | ||||
| 			"issue count 'num_comments'", | ||||
| 		}, | ||||
| 	} | ||||
| 	for i := range checkers { | ||||
| 	for _, checker := range checkers { | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			log.Warn("CheckRepoStats: Aborting due to shutdown") | ||||
| 			return | ||||
| 			log.Warn("CheckRepoStats: Cancelled before %s", checker.desc) | ||||
| 			return ErrCancelledf("before checking %s", checker.desc) | ||||
| 		default: | ||||
| 			repoStatsCheck(ctx, checkers[i]) | ||||
| 			repoStatsCheck(ctx, checker) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -2004,13 +2013,13 @@ func CheckRepoStats(ctx context.Context) { | ||||
| 		log.Error("Select %s: %v", desc, err) | ||||
| 	} else { | ||||
| 		for _, result := range results { | ||||
| 			id := com.StrTo(result["id"]).MustInt64() | ||||
| 			select { | ||||
| 			case <-ctx.Done(): | ||||
| 				log.Warn("CheckRepoStats: Aborting due to shutdown") | ||||
| 				return | ||||
| 				log.Warn("CheckRepoStats: Cancelled during %s for repo ID %d", desc, id) | ||||
| 				return ErrCancelledf("during %s for repo ID %d", desc, id) | ||||
| 			default: | ||||
| 			} | ||||
| 			id := com.StrTo(result["id"]).MustInt64() | ||||
| 			log.Trace("Updating %s: %d", desc, id) | ||||
| 			_, err = x.Exec("UPDATE `repository` SET num_closed_issues=(SELECT COUNT(*) FROM `issue` WHERE repo_id=? AND is_closed=? AND is_pull=?) WHERE id=?", id, true, false, id) | ||||
| 			if err != nil { | ||||
| @@ -2027,13 +2036,13 @@ func CheckRepoStats(ctx context.Context) { | ||||
| 		log.Error("Select %s: %v", desc, err) | ||||
| 	} else { | ||||
| 		for _, result := range results { | ||||
| 			id := com.StrTo(result["id"]).MustInt64() | ||||
| 			select { | ||||
| 			case <-ctx.Done(): | ||||
| 				log.Warn("CheckRepoStats: Aborting due to shutdown") | ||||
| 				return | ||||
| 				log.Warn("CheckRepoStats: Cancelled") | ||||
| 				return ErrCancelledf("during %s for repo ID %d", desc, id) | ||||
| 			default: | ||||
| 			} | ||||
| 			id := com.StrTo(result["id"]).MustInt64() | ||||
| 			log.Trace("Updating %s: %d", desc, id) | ||||
| 			_, err = x.Exec("UPDATE `repository` SET num_closed_pulls=(SELECT COUNT(*) FROM `issue` WHERE repo_id=? AND is_closed=? AND is_pull=?) WHERE id=?", id, true, true, id) | ||||
| 			if err != nil { | ||||
| @@ -2050,13 +2059,13 @@ func CheckRepoStats(ctx context.Context) { | ||||
| 		log.Error("Select repository count 'num_forks': %v", err) | ||||
| 	} else { | ||||
| 		for _, result := range results { | ||||
| 			id := com.StrTo(result["id"]).MustInt64() | ||||
| 			select { | ||||
| 			case <-ctx.Done(): | ||||
| 				log.Warn("CheckRepoStats: Aborting due to shutdown") | ||||
| 				return | ||||
| 				log.Warn("CheckRepoStats: Cancelled") | ||||
| 				return ErrCancelledf("during %s for repo ID %d", desc, id) | ||||
| 			default: | ||||
| 			} | ||||
| 			id := com.StrTo(result["id"]).MustInt64() | ||||
| 			log.Trace("Updating repository count 'num_forks': %d", id) | ||||
|  | ||||
| 			repo, err := GetRepositoryByID(id) | ||||
| @@ -2079,6 +2088,7 @@ func CheckRepoStats(ctx context.Context) { | ||||
| 		} | ||||
| 	} | ||||
| 	// ***** END: Repository.NumForks ***** | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // SetArchiveRepoState sets if a repo is archived | ||||
| @@ -2189,12 +2199,17 @@ func (repo *Repository) generateRandomAvatar(e Engine) error { | ||||
| } | ||||
|  | ||||
| // RemoveRandomAvatars removes the randomly generated avatars that were created for repositories | ||||
| func RemoveRandomAvatars() error { | ||||
| func RemoveRandomAvatars(ctx context.Context) error { | ||||
| 	return x. | ||||
| 		Where("id > 0").BufferSize(setting.Database.IterateBufferSize). | ||||
| 		Iterate(new(Repository), | ||||
| 			func(idx int, bean interface{}) error { | ||||
| 				repository := bean.(*Repository) | ||||
| 				select { | ||||
| 				case <-ctx.Done(): | ||||
| 					return ErrCancelledf("before random avatars removed for %s", repository.FullName()) | ||||
| 				default: | ||||
| 				} | ||||
| 				stringifiedID := strconv.FormatInt(repository.ID, 10) | ||||
| 				if repository.Avatar == stringifiedID { | ||||
| 					return repository.DeleteAvatar() | ||||
|   | ||||
| @@ -1321,16 +1321,30 @@ func DeleteUser(u *User) (err error) { | ||||
| 	return sess.Commit() | ||||
| } | ||||
|  | ||||
| // DeleteInactivateUsers deletes all inactivate users and email addresses. | ||||
| func DeleteInactivateUsers() (err error) { | ||||
| // DeleteInactiveUsers deletes all inactive users and email addresses. | ||||
| func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) (err error) { | ||||
| 	users := make([]*User, 0, 10) | ||||
| 	if err = x. | ||||
| 		Where("is_active = ?", false). | ||||
| 		Find(&users); err != nil { | ||||
| 		return fmt.Errorf("get all inactive users: %v", err) | ||||
| 	if olderThan > 0 { | ||||
| 		if err = x. | ||||
| 			Where("is_active = ? and created_unix < ?", false, time.Now().Add(-olderThan).Unix()). | ||||
| 			Find(&users); err != nil { | ||||
| 			return fmt.Errorf("get all inactive users: %v", err) | ||||
| 		} | ||||
| 	} else { | ||||
| 		if err = x. | ||||
| 			Where("is_active = ?", false). | ||||
| 			Find(&users); err != nil { | ||||
| 			return fmt.Errorf("get all inactive users: %v", err) | ||||
| 		} | ||||
|  | ||||
| 	} | ||||
| 	// FIXME: should only update authorized_keys file once after all deletions. | ||||
| 	for _, u := range users { | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			return ErrCancelledf("Before delete inactive user %s", u.Name) | ||||
| 		default: | ||||
| 		} | ||||
| 		if err = DeleteUser(u); err != nil { | ||||
| 			// Ignore users that were set inactive by admin. | ||||
| 			if IsErrUserOwnRepos(err) || IsErrUserHasOrgs(err) { | ||||
| @@ -1814,25 +1828,23 @@ func synchronizeLdapSSHPublicKeys(usr *User, s *LoginSource, sshPublicKeys []str | ||||
| } | ||||
|  | ||||
| // SyncExternalUsers is used to synchronize users with external authorization source | ||||
| func SyncExternalUsers(ctx context.Context) { | ||||
| func SyncExternalUsers(ctx context.Context, updateExisting bool) error { | ||||
| 	log.Trace("Doing: SyncExternalUsers") | ||||
|  | ||||
| 	ls, err := LoginSources() | ||||
| 	if err != nil { | ||||
| 		log.Error("SyncExternalUsers: %v", err) | ||||
| 		return | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	updateExisting := setting.Cron.SyncExternalUsers.UpdateExisting | ||||
|  | ||||
| 	for _, s := range ls { | ||||
| 		if !s.IsActived || !s.IsSyncEnabled { | ||||
| 			continue | ||||
| 		} | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			log.Warn("SyncExternalUsers: Aborted due to shutdown before update of %s", s.Name) | ||||
| 			return | ||||
| 			log.Warn("SyncExternalUsers: Cancelled before update of %s", s.Name) | ||||
| 			return ErrCancelledf("Before update of %s", s.Name) | ||||
| 		default: | ||||
| 		} | ||||
|  | ||||
| @@ -1850,12 +1862,12 @@ func SyncExternalUsers(ctx context.Context) { | ||||
| 				Find(&users) | ||||
| 			if err != nil { | ||||
| 				log.Error("SyncExternalUsers: %v", err) | ||||
| 				return | ||||
| 				return err | ||||
| 			} | ||||
| 			select { | ||||
| 			case <-ctx.Done(): | ||||
| 				log.Warn("SyncExternalUsers: Aborted due to shutdown before update of %s", s.Name) | ||||
| 				return | ||||
| 				log.Warn("SyncExternalUsers: Cancelled before update of %s", s.Name) | ||||
| 				return ErrCancelledf("Before update of %s", s.Name) | ||||
| 			default: | ||||
| 			} | ||||
|  | ||||
| @@ -1877,7 +1889,7 @@ func SyncExternalUsers(ctx context.Context) { | ||||
| 			for _, su := range sr { | ||||
| 				select { | ||||
| 				case <-ctx.Done(): | ||||
| 					log.Warn("SyncExternalUsers: Aborted due to shutdown at update of %s before completed update of users", s.Name) | ||||
| 					log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", s.Name) | ||||
| 					// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed | ||||
| 					if sshKeysNeedUpdate { | ||||
| 						err = RewriteAllPublicKeys() | ||||
| @@ -1885,7 +1897,7 @@ func SyncExternalUsers(ctx context.Context) { | ||||
| 							log.Error("RewriteAllPublicKeys: %v", err) | ||||
| 						} | ||||
| 					} | ||||
| 					return | ||||
| 					return ErrCancelledf("During update of %s before completed update of users", s.Name) | ||||
| 				default: | ||||
| 				} | ||||
| 				if len(su.Username) == 0 { | ||||
| @@ -1980,8 +1992,8 @@ func SyncExternalUsers(ctx context.Context) { | ||||
|  | ||||
| 			select { | ||||
| 			case <-ctx.Done(): | ||||
| 				log.Warn("SyncExternalUsers: Aborted due to shutdown at update of %s before delete users", s.Name) | ||||
| 				return | ||||
| 				log.Warn("SyncExternalUsers: Cancelled during update of %s before delete users", s.Name) | ||||
| 				return ErrCancelledf("During update of %s before delete users", s.Name) | ||||
| 			default: | ||||
| 			} | ||||
|  | ||||
| @@ -2008,4 +2020,5 @@ func SyncExternalUsers(ctx context.Context) { | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -51,7 +51,7 @@ func (f *AdminEditUserForm) Validate(ctx *macaron.Context, errs binding.Errors) | ||||
|  | ||||
| // AdminDashboardForm form for admin dashboard operations | ||||
| type AdminDashboardForm struct { | ||||
| 	Op int `binding:"required"` | ||||
| 	Op string `binding:"required"` | ||||
| } | ||||
|  | ||||
| // Validate validates form fields | ||||
|   | ||||
| @@ -9,143 +9,86 @@ import ( | ||||
| 	"context" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/graceful" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/migrations" | ||||
| 	repo_module "code.gitea.io/gitea/modules/repository" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/sync" | ||||
| 	mirror_service "code.gitea.io/gitea/services/mirror" | ||||
|  | ||||
| 	"github.com/gogs/cron" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	mirrorUpdate            = "mirror_update" | ||||
| 	gitFsck                 = "git_fsck" | ||||
| 	checkRepos              = "check_repos" | ||||
| 	archiveCleanup          = "archive_cleanup" | ||||
| 	syncExternalUsers       = "sync_external_users" | ||||
| 	deletedBranchesCleanup  = "deleted_branches_cleanup" | ||||
| 	updateMigrationPosterID = "update_migration_post_id" | ||||
| ) | ||||
|  | ||||
| var c = cron.New() | ||||
|  | ||||
| // Prevent duplicate running tasks. | ||||
| var taskStatusTable = sync.NewStatusTable() | ||||
|  | ||||
| // Func defines a cron function body | ||||
| type Func func() | ||||
|  | ||||
| // WithUnique wrap a cron func with an unique running check | ||||
| func WithUnique(name string, body func(context.Context)) Func { | ||||
| 	return func() { | ||||
| 		if !taskStatusTable.StartIfNotRunning(name) { | ||||
| 			return | ||||
| 		} | ||||
| 		defer taskStatusTable.Stop(name) | ||||
| 		graceful.GetManager().RunWithShutdownContext(body) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // NewContext begins cron tasks | ||||
| // Each cron task is run within the shutdown context as a running server | ||||
| // AtShutdown the cron server is stopped | ||||
| func NewContext() { | ||||
| 	var ( | ||||
| 		entry *cron.Entry | ||||
| 		err   error | ||||
| 	) | ||||
| 	if setting.Cron.UpdateMirror.Enabled { | ||||
| 		entry, err = c.AddFunc("Update mirrors", setting.Cron.UpdateMirror.Schedule, WithUnique(mirrorUpdate, mirror_service.Update)) | ||||
| 		if err != nil { | ||||
| 			log.Fatal("Cron[Update mirrors]: %v", err) | ||||
| 		} | ||||
| 		if setting.Cron.UpdateMirror.RunAtStart { | ||||
| 			entry.Prev = time.Now() | ||||
| 			entry.ExecTimes++ | ||||
| 			go WithUnique(mirrorUpdate, mirror_service.Update)() | ||||
| 		} | ||||
| 	} | ||||
| 	if setting.Cron.RepoHealthCheck.Enabled { | ||||
| 		entry, err = c.AddFunc("Repository health check", setting.Cron.RepoHealthCheck.Schedule, WithUnique(gitFsck, func(ctx context.Context) { | ||||
| 			if err := repo_module.GitFsck(ctx); err != nil { | ||||
| 				log.Error("GitFsck: %s", err) | ||||
| 			} | ||||
| 		})) | ||||
| 		if err != nil { | ||||
| 			log.Fatal("Cron[Repository health check]: %v", err) | ||||
| 		} | ||||
| 		if setting.Cron.RepoHealthCheck.RunAtStart { | ||||
| 			entry.Prev = time.Now() | ||||
| 			entry.ExecTimes++ | ||||
| 			go WithUnique(gitFsck, func(ctx context.Context) { | ||||
| 				if err := repo_module.GitFsck(ctx); err != nil { | ||||
| 					log.Error("GitFsck: %s", err) | ||||
| 				} | ||||
| 			})() | ||||
| 		} | ||||
| 	} | ||||
| 	if setting.Cron.CheckRepoStats.Enabled { | ||||
| 		entry, err = c.AddFunc("Check repository statistics", setting.Cron.CheckRepoStats.Schedule, WithUnique(checkRepos, models.CheckRepoStats)) | ||||
| 		if err != nil { | ||||
| 			log.Fatal("Cron[Check repository statistics]: %v", err) | ||||
| 		} | ||||
| 		if setting.Cron.CheckRepoStats.RunAtStart { | ||||
| 			entry.Prev = time.Now() | ||||
| 			entry.ExecTimes++ | ||||
| 			go WithUnique(checkRepos, models.CheckRepoStats)() | ||||
| 		} | ||||
| 	} | ||||
| 	if setting.Cron.ArchiveCleanup.Enabled { | ||||
| 		entry, err = c.AddFunc("Clean up old repository archives", setting.Cron.ArchiveCleanup.Schedule, WithUnique(archiveCleanup, models.DeleteOldRepositoryArchives)) | ||||
| 		if err != nil { | ||||
| 			log.Fatal("Cron[Clean up old repository archives]: %v", err) | ||||
| 		} | ||||
| 		if setting.Cron.ArchiveCleanup.RunAtStart { | ||||
| 			entry.Prev = time.Now() | ||||
| 			entry.ExecTimes++ | ||||
| 			go WithUnique(archiveCleanup, models.DeleteOldRepositoryArchives)() | ||||
| 		} | ||||
| 	} | ||||
| 	if setting.Cron.SyncExternalUsers.Enabled { | ||||
| 		entry, err = c.AddFunc("Synchronize external users", setting.Cron.SyncExternalUsers.Schedule, WithUnique(syncExternalUsers, models.SyncExternalUsers)) | ||||
| 		if err != nil { | ||||
| 			log.Fatal("Cron[Synchronize external users]: %v", err) | ||||
| 		} | ||||
| 		if setting.Cron.SyncExternalUsers.RunAtStart { | ||||
| 			entry.Prev = time.Now() | ||||
| 			entry.ExecTimes++ | ||||
| 			go WithUnique(syncExternalUsers, models.SyncExternalUsers)() | ||||
| 		} | ||||
| 	} | ||||
| 	if setting.Cron.DeletedBranchesCleanup.Enabled { | ||||
| 		entry, err = c.AddFunc("Remove old deleted branches", setting.Cron.DeletedBranchesCleanup.Schedule, WithUnique(deletedBranchesCleanup, models.RemoveOldDeletedBranches)) | ||||
| 		if err != nil { | ||||
| 			log.Fatal("Cron[Remove old deleted branches]: %v", err) | ||||
| 		} | ||||
| 		if setting.Cron.DeletedBranchesCleanup.RunAtStart { | ||||
| 			entry.Prev = time.Now() | ||||
| 			entry.ExecTimes++ | ||||
| 			go WithUnique(deletedBranchesCleanup, models.RemoveOldDeletedBranches)() | ||||
| 		} | ||||
| 	} | ||||
| 	initBasicTasks() | ||||
| 	initExtendedTasks() | ||||
|  | ||||
| 	entry, err = c.AddFunc("Update migrated repositories' issues and comments' posterid", setting.Cron.UpdateMigrationPosterID.Schedule, WithUnique(updateMigrationPosterID, migrations.UpdateMigrationPosterID)) | ||||
| 	if err != nil { | ||||
| 		log.Fatal("Cron[Update migrated repositories]: %v", err) | ||||
| 	lock.Lock() | ||||
| 	for _, task := range tasks { | ||||
| 		if task.IsEnabled() && task.DoRunAtStart() { | ||||
| 			go task.Run() | ||||
| 		} | ||||
| 	} | ||||
| 	entry.Prev = time.Now() | ||||
| 	entry.ExecTimes++ | ||||
| 	go WithUnique(updateMigrationPosterID, migrations.UpdateMigrationPosterID)() | ||||
|  | ||||
| 	c.Start() | ||||
| 	graceful.GetManager().RunAtShutdown(context.Background(), c.Stop) | ||||
| 	started = true | ||||
| 	lock.Unlock() | ||||
| 	graceful.GetManager().RunAtShutdown(context.Background(), func() { | ||||
| 		c.Stop() | ||||
| 		lock.Lock() | ||||
| 		started = false | ||||
| 		lock.Unlock() | ||||
| 	}) | ||||
|  | ||||
| } | ||||
|  | ||||
| // ListTasks returns all running cron tasks. | ||||
| func ListTasks() []*cron.Entry { | ||||
| 	return c.Entries() | ||||
| // TaskTableRow represents a task row in the tasks table | ||||
| type TaskTableRow struct { | ||||
| 	Name      string | ||||
| 	Spec      string | ||||
| 	Next      time.Time | ||||
| 	Prev      time.Time | ||||
| 	ExecTimes int64 | ||||
| } | ||||
|  | ||||
| // TaskTable represents a table of tasks | ||||
| type TaskTable []*TaskTableRow | ||||
|  | ||||
| // ListTasks returns all running cron tasks. | ||||
| func ListTasks() TaskTable { | ||||
| 	entries := c.Entries() | ||||
| 	eMap := map[string]*cron.Entry{} | ||||
| 	for _, e := range entries { | ||||
| 		eMap[e.Description] = e | ||||
| 	} | ||||
| 	lock.Lock() | ||||
| 	defer lock.Unlock() | ||||
| 	tTable := make([]*TaskTableRow, 0, len(tasks)) | ||||
| 	for _, task := range tasks { | ||||
| 		spec := "-" | ||||
| 		var ( | ||||
| 			next time.Time | ||||
| 			prev time.Time | ||||
| 		) | ||||
| 		if e, ok := eMap[task.Name]; ok { | ||||
| 			spec = e.Spec | ||||
| 			next = e.Next | ||||
| 			prev = e.Prev | ||||
| 		} | ||||
| 		task.lock.Lock() | ||||
| 		tTable = append(tTable, &TaskTableRow{ | ||||
| 			Name:      task.Name, | ||||
| 			Spec:      spec, | ||||
| 			Next:      next, | ||||
| 			Prev:      prev, | ||||
| 			ExecTimes: task.ExecTimes, | ||||
| 		}) | ||||
| 		task.lock.Unlock() | ||||
| 	} | ||||
|  | ||||
| 	return tTable | ||||
| } | ||||
|   | ||||
							
								
								
									
										72
									
								
								modules/cron/setting.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								modules/cron/setting.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| // 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 cron | ||||
|  | ||||
| import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"github.com/unknwon/i18n" | ||||
| ) | ||||
|  | ||||
| // Config represents a basic configuration interface that cron task | ||||
| type Config interface { | ||||
| 	IsEnabled() bool | ||||
| 	DoRunAtStart() bool | ||||
| 	GetSchedule() string | ||||
| 	FormatMessage(name, status string, doer *models.User, args ...interface{}) string | ||||
| } | ||||
|  | ||||
| // BaseConfig represents the basic config for a Cron task | ||||
| type BaseConfig struct { | ||||
| 	Enabled    bool | ||||
| 	RunAtStart bool | ||||
| 	Schedule   string | ||||
| } | ||||
|  | ||||
| // OlderThanConfig represents a cron task with OlderThan setting | ||||
| type OlderThanConfig struct { | ||||
| 	BaseConfig | ||||
| 	OlderThan time.Duration | ||||
| } | ||||
|  | ||||
| // UpdateExistingConfig represents a cron task with UpdateExisting setting | ||||
| type UpdateExistingConfig struct { | ||||
| 	BaseConfig | ||||
| 	UpdateExisting bool | ||||
| } | ||||
|  | ||||
| // GetSchedule returns the schedule for the base config | ||||
| func (b *BaseConfig) GetSchedule() string { | ||||
| 	return b.Schedule | ||||
| } | ||||
|  | ||||
| // IsEnabled returns the enabled status for the config | ||||
| func (b *BaseConfig) IsEnabled() bool { | ||||
| 	return b.Enabled | ||||
| } | ||||
|  | ||||
| // DoRunAtStart returns whether the task should be run at the start | ||||
| func (b *BaseConfig) DoRunAtStart() bool { | ||||
| 	return b.RunAtStart | ||||
| } | ||||
|  | ||||
| // FormatMessage returns a message for the task | ||||
| func (b *BaseConfig) FormatMessage(name, status string, doer *models.User, args ...interface{}) string { | ||||
| 	realArgs := make([]interface{}, 0, len(args)+2) | ||||
| 	realArgs = append(realArgs, i18n.Tr("en-US", "admin.dashboard."+name)) | ||||
| 	if doer == nil { | ||||
| 		realArgs = append(realArgs, "(Cron)") | ||||
| 	} else { | ||||
| 		realArgs = append(realArgs, doer.Name) | ||||
| 	} | ||||
| 	if len(args) > 0 { | ||||
| 		realArgs = append(realArgs, args...) | ||||
| 	} | ||||
| 	if doer == nil || (doer.ID == -1 && doer.Name == "(Cron)") { | ||||
| 		return i18n.Tr("en-US", "admin.dashboard.cron."+status, realArgs...) | ||||
| 	} | ||||
| 	return i18n.Tr("en-US", "admin.dashboard.task."+status, realArgs...) | ||||
| } | ||||
							
								
								
									
										166
									
								
								modules/cron/tasks.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								modules/cron/tasks.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| // 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 cron | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"sync" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/graceful" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/process" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| ) | ||||
|  | ||||
| var lock = sync.Mutex{} | ||||
| var started = false | ||||
| var tasks = []*Task{} | ||||
| var tasksMap = map[string]*Task{} | ||||
|  | ||||
| // Task represents a Cron task | ||||
| type Task struct { | ||||
| 	lock      sync.Mutex | ||||
| 	Name      string | ||||
| 	config    Config | ||||
| 	fun       func(context.Context, *models.User, Config) error | ||||
| 	ExecTimes int64 | ||||
| } | ||||
|  | ||||
| // DoRunAtStart returns if this task should run at the start | ||||
| func (t *Task) DoRunAtStart() bool { | ||||
| 	return t.config.DoRunAtStart() | ||||
| } | ||||
|  | ||||
| // IsEnabled returns if this task is enabled as cron task | ||||
| func (t *Task) IsEnabled() bool { | ||||
| 	return t.config.IsEnabled() | ||||
| } | ||||
|  | ||||
| // GetConfig will return a copy of the task's config | ||||
| func (t *Task) GetConfig() Config { | ||||
| 	if reflect.TypeOf(t.config).Kind() == reflect.Ptr { | ||||
| 		// Pointer: | ||||
| 		return reflect.New(reflect.ValueOf(t.config).Elem().Type()).Interface().(Config) | ||||
| 	} | ||||
| 	// Not pointer: | ||||
| 	return reflect.New(reflect.TypeOf(t.config)).Elem().Interface().(Config) | ||||
| } | ||||
|  | ||||
| // Run will run the task incrementing the cron counter with no user defined | ||||
| func (t *Task) Run() { | ||||
| 	t.RunWithUser(&models.User{ | ||||
| 		ID:        -1, | ||||
| 		Name:      "(Cron)", | ||||
| 		LowerName: "(cron)", | ||||
| 	}, t.config) | ||||
| } | ||||
|  | ||||
| // RunWithUser will run the task incrementing the cron counter at the time with User | ||||
| func (t *Task) RunWithUser(doer *models.User, config Config) { | ||||
| 	if !taskStatusTable.StartIfNotRunning(t.Name) { | ||||
| 		return | ||||
| 	} | ||||
| 	t.lock.Lock() | ||||
| 	if config == nil { | ||||
| 		config = t.config | ||||
| 	} | ||||
| 	t.ExecTimes++ | ||||
| 	t.lock.Unlock() | ||||
| 	defer func() { | ||||
| 		taskStatusTable.Stop(t.Name) | ||||
| 		if err := recover(); err != nil { | ||||
| 			// Recover a panic within the | ||||
| 			combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) | ||||
| 			log.Error("PANIC whilst running task: %s Value: %v", t.Name, combinedErr) | ||||
| 		} | ||||
| 	}() | ||||
| 	graceful.GetManager().RunWithShutdownContext(func(baseCtx context.Context) { | ||||
| 		ctx, cancel := context.WithCancel(baseCtx) | ||||
| 		defer cancel() | ||||
| 		pm := process.GetManager() | ||||
| 		pid := pm.Add(config.FormatMessage(t.Name, "process", doer), cancel) | ||||
| 		defer pm.Remove(pid) | ||||
| 		if err := t.fun(ctx, doer, config); err != nil { | ||||
| 			if models.IsErrCancelled(err) { | ||||
| 				message := err.(models.ErrCancelled).Message | ||||
| 				if err := models.CreateNotice(models.NoticeTask, config.FormatMessage(t.Name, "aborted", doer, message)); err != nil { | ||||
| 					log.Error("CreateNotice: %v", err) | ||||
| 				} | ||||
| 				return | ||||
| 			} | ||||
| 			if err := models.CreateNotice(models.NoticeTask, config.FormatMessage(t.Name, "error", doer, err)); err != nil { | ||||
| 				log.Error("CreateNotice: %v", err) | ||||
| 			} | ||||
| 			return | ||||
| 		} | ||||
| 		if err := models.CreateNotice(models.NoticeTask, config.FormatMessage(t.Name, "finished", doer)); err != nil { | ||||
| 			log.Error("CreateNotice: %v", err) | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // GetTask gets the named task | ||||
| func GetTask(name string) *Task { | ||||
| 	lock.Lock() | ||||
| 	defer lock.Unlock() | ||||
| 	log.Info("Getting %s in %v", name, tasksMap[name]) | ||||
|  | ||||
| 	return tasksMap[name] | ||||
| } | ||||
|  | ||||
| // RegisterTask allows a task to be registered with the cron service | ||||
| func RegisterTask(name string, config Config, fun func(context.Context, *models.User, Config) error) error { | ||||
| 	log.Debug("Registering task: %s", name) | ||||
| 	_, err := setting.GetCronSettings(name, config) | ||||
| 	if err != nil { | ||||
| 		log.Error("Unable to register cron task with name: %s Error: %v", name, err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	task := &Task{ | ||||
| 		Name:   name, | ||||
| 		config: config, | ||||
| 		fun:    fun, | ||||
| 	} | ||||
| 	lock.Lock() | ||||
| 	locked := true | ||||
| 	defer func() { | ||||
| 		if locked { | ||||
| 			lock.Unlock() | ||||
| 		} | ||||
| 	}() | ||||
| 	if _, has := tasksMap[task.Name]; has { | ||||
| 		log.Error("A task with this name: %s has already been registered", name) | ||||
| 		return fmt.Errorf("duplicate task with name: %s", task.Name) | ||||
| 	} | ||||
|  | ||||
| 	if config.IsEnabled() { | ||||
| 		// We cannot use the entry return as there is no way to lock it | ||||
| 		if _, err = c.AddJob(name, config.GetSchedule(), task); err != nil { | ||||
| 			log.Error("Unable to register cron task with name: %s Error: %v", name, err) | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	tasks = append(tasks, task) | ||||
| 	tasksMap[task.Name] = task | ||||
| 	if started && config.IsEnabled() && config.DoRunAtStart() { | ||||
| 		lock.Unlock() | ||||
| 		locked = false | ||||
| 		task.Run() | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // RegisterTaskFatal will register a task but if there is an error log.Fatal | ||||
| func RegisterTaskFatal(name string, config Config, fun func(context.Context, *models.User, Config) error) { | ||||
| 	if err := RegisterTask(name, config, fun); err != nil { | ||||
| 		log.Fatal("Unable to register cron task %s Error: %v", name, err) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										118
									
								
								modules/cron/tasks_basic.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								modules/cron/tasks_basic.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| // 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 cron | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/migrations" | ||||
| 	repository_service "code.gitea.io/gitea/modules/repository" | ||||
| 	mirror_service "code.gitea.io/gitea/services/mirror" | ||||
| ) | ||||
|  | ||||
| func registerUpdateMirrorTask() { | ||||
| 	RegisterTaskFatal("update_mirrors", &BaseConfig{ | ||||
| 		Enabled:    true, | ||||
| 		RunAtStart: false, | ||||
| 		Schedule:   "@every 10m", | ||||
| 	}, func(ctx context.Context, _ *models.User, _ Config) error { | ||||
| 		return mirror_service.Update(ctx) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func registerRepoHealthCheck() { | ||||
| 	type RepoHealthCheckConfig struct { | ||||
| 		BaseConfig | ||||
| 		Timeout time.Duration | ||||
| 		Args    []string `delim:" "` | ||||
| 	} | ||||
| 	RegisterTaskFatal("repo_health_check", &RepoHealthCheckConfig{ | ||||
| 		BaseConfig: BaseConfig{ | ||||
| 			Enabled:    true, | ||||
| 			RunAtStart: false, | ||||
| 			Schedule:   "@every 24h", | ||||
| 		}, | ||||
| 		Timeout: 60 * time.Second, | ||||
| 		Args:    []string{}, | ||||
| 	}, func(ctx context.Context, _ *models.User, config Config) error { | ||||
| 		rhcConfig := config.(*RepoHealthCheckConfig) | ||||
| 		return repository_service.GitFsck(ctx, rhcConfig.Timeout, rhcConfig.Args) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func registerCheckRepoStats() { | ||||
| 	RegisterTaskFatal("check_repo_stats", &BaseConfig{ | ||||
| 		Enabled:    true, | ||||
| 		RunAtStart: true, | ||||
| 		Schedule:   "@every 24h", | ||||
| 	}, func(ctx context.Context, _ *models.User, _ Config) error { | ||||
| 		return models.CheckRepoStats(ctx) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func registerArchiveCleanup() { | ||||
| 	RegisterTaskFatal("archive_cleanup", &OlderThanConfig{ | ||||
| 		BaseConfig: BaseConfig{ | ||||
| 			Enabled:    true, | ||||
| 			RunAtStart: true, | ||||
| 			Schedule:   "@every 24h", | ||||
| 		}, | ||||
| 		OlderThan: 24 * time.Hour, | ||||
| 	}, func(ctx context.Context, _ *models.User, config Config) error { | ||||
| 		acConfig := config.(*OlderThanConfig) | ||||
| 		return models.DeleteOldRepositoryArchives(ctx, acConfig.OlderThan) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func registerSyncExternalUsers() { | ||||
| 	RegisterTaskFatal("sync_external_users", &UpdateExistingConfig{ | ||||
| 		BaseConfig: BaseConfig{ | ||||
| 			Enabled:    true, | ||||
| 			RunAtStart: false, | ||||
| 			Schedule:   "@every 24h", | ||||
| 		}, | ||||
| 		UpdateExisting: true, | ||||
| 	}, func(ctx context.Context, _ *models.User, config Config) error { | ||||
| 		realConfig := config.(*UpdateExistingConfig) | ||||
| 		return models.SyncExternalUsers(ctx, realConfig.UpdateExisting) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func registerDeletedBranchesCleanup() { | ||||
| 	RegisterTaskFatal("deleted_branches_cleanup", &OlderThanConfig{ | ||||
| 		BaseConfig: BaseConfig{ | ||||
| 			Enabled:    true, | ||||
| 			RunAtStart: true, | ||||
| 			Schedule:   "@every 24h", | ||||
| 		}, | ||||
| 		OlderThan: 24 * time.Hour, | ||||
| 	}, func(ctx context.Context, _ *models.User, config Config) error { | ||||
| 		realConfig := config.(*OlderThanConfig) | ||||
| 		models.RemoveOldDeletedBranches(ctx, realConfig.OlderThan) | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func registerUpdateMigrationPosterID() { | ||||
| 	RegisterTaskFatal("update_migration_poster_id", &BaseConfig{ | ||||
| 		Enabled:    true, | ||||
| 		RunAtStart: true, | ||||
| 		Schedule:   "@every 24h", | ||||
| 	}, func(ctx context.Context, _ *models.User, _ Config) error { | ||||
| 		return migrations.UpdateMigrationPosterID(ctx) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func initBasicTasks() { | ||||
| 	registerUpdateMirrorTask() | ||||
| 	registerRepoHealthCheck() | ||||
| 	registerCheckRepoStats() | ||||
| 	registerArchiveCleanup() | ||||
| 	registerSyncExternalUsers() | ||||
| 	registerDeletedBranchesCleanup() | ||||
| 	registerUpdateMigrationPosterID() | ||||
| } | ||||
							
								
								
									
										119
									
								
								modules/cron/tasks_extended.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								modules/cron/tasks_extended.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| // 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 cron | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	repo_module "code.gitea.io/gitea/modules/repository" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| ) | ||||
|  | ||||
| func registerDeleteInactiveUsers() { | ||||
| 	RegisterTaskFatal("delete_inactive_accounts", &OlderThanConfig{ | ||||
| 		BaseConfig: BaseConfig{ | ||||
| 			Enabled:    false, | ||||
| 			RunAtStart: false, | ||||
| 			Schedule:   "@annually", | ||||
| 		}, | ||||
| 		OlderThan: 0 * time.Second, | ||||
| 	}, func(ctx context.Context, _ *models.User, config Config) error { | ||||
| 		olderThanConfig := config.(*OlderThanConfig) | ||||
| 		return models.DeleteInactiveUsers(ctx, olderThanConfig.OlderThan) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func registerDeleteRepositoryArchives() { | ||||
| 	RegisterTaskFatal("delete_repo_archives", &BaseConfig{ | ||||
| 		Enabled:    false, | ||||
| 		RunAtStart: false, | ||||
| 		Schedule:   "@annually", | ||||
| 	}, func(ctx context.Context, _ *models.User, _ Config) error { | ||||
| 		return models.DeleteRepositoryArchives(ctx) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func registerGarbageCollectRepositories() { | ||||
| 	type RepoHealthCheckConfig struct { | ||||
| 		BaseConfig | ||||
| 		Timeout time.Duration | ||||
| 		Args    []string `delim:" "` | ||||
| 	} | ||||
| 	RegisterTaskFatal("git_gc_repos", &RepoHealthCheckConfig{ | ||||
| 		BaseConfig: BaseConfig{ | ||||
| 			Enabled:    false, | ||||
| 			RunAtStart: false, | ||||
| 			Schedule:   "@every 72h", | ||||
| 		}, | ||||
| 		Timeout: time.Duration(setting.Git.Timeout.GC) * time.Second, | ||||
| 		Args:    setting.Git.GCArgs, | ||||
| 	}, func(ctx context.Context, _ *models.User, config Config) error { | ||||
| 		rhcConfig := config.(*RepoHealthCheckConfig) | ||||
| 		return repo_module.GitGcRepos(ctx, rhcConfig.Timeout, rhcConfig.Args...) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func registerRewriteAllPublicKeys() { | ||||
| 	RegisterTaskFatal("resync_all_sshkeys", &BaseConfig{ | ||||
| 		Enabled:    false, | ||||
| 		RunAtStart: false, | ||||
| 		Schedule:   "@every 72h", | ||||
| 	}, func(_ context.Context, _ *models.User, _ Config) error { | ||||
| 		return models.RewriteAllPublicKeys() | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func registerRepositoryUpdateHook() { | ||||
| 	RegisterTaskFatal("resync_all_hooks", &BaseConfig{ | ||||
| 		Enabled:    false, | ||||
| 		RunAtStart: false, | ||||
| 		Schedule:   "@every 72h", | ||||
| 	}, func(ctx context.Context, _ *models.User, _ Config) error { | ||||
| 		return repo_module.SyncRepositoryHooks(ctx) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func registerReinitMissingRepositories() { | ||||
| 	RegisterTaskFatal("reinit_missing_repos", &BaseConfig{ | ||||
| 		Enabled:    false, | ||||
| 		RunAtStart: false, | ||||
| 		Schedule:   "@every 72h", | ||||
| 	}, func(ctx context.Context, _ *models.User, _ Config) error { | ||||
| 		return repo_module.ReinitMissingRepositories(ctx) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func registerDeleteMissingRepositories() { | ||||
| 	RegisterTaskFatal("delete_missing_repos", &BaseConfig{ | ||||
| 		Enabled:    false, | ||||
| 		RunAtStart: false, | ||||
| 		Schedule:   "@every 72h", | ||||
| 	}, func(ctx context.Context, user *models.User, _ Config) error { | ||||
| 		return repo_module.DeleteMissingRepositories(ctx, user) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func registerRemoveRandomAvatars() { | ||||
| 	RegisterTaskFatal("delete_generated_repository_avatars", &BaseConfig{ | ||||
| 		Enabled:    false, | ||||
| 		RunAtStart: false, | ||||
| 		Schedule:   "@every 72h", | ||||
| 	}, func(ctx context.Context, _ *models.User, _ Config) error { | ||||
| 		return models.RemoveRandomAvatars(ctx) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func initExtendedTasks() { | ||||
| 	registerDeleteInactiveUsers() | ||||
| 	registerDeleteRepositoryArchives() | ||||
| 	registerGarbageCollectRepositories() | ||||
| 	registerRewriteAllPublicKeys() | ||||
| 	registerRepositoryUpdateHook() | ||||
| 	registerReinitMissingRepositories() | ||||
| 	registerDeleteMissingRepositories() | ||||
| 	registerRemoveRandomAvatars() | ||||
| } | ||||
| @@ -45,22 +45,32 @@ func (c *Command) String() string { | ||||
|  | ||||
| // NewCommand creates and returns a new Git Command based on given command and arguments. | ||||
| func NewCommand(args ...string) *Command { | ||||
| 	return NewCommandContext(DefaultContext, args...) | ||||
| } | ||||
|  | ||||
| // NewCommandContext creates and returns a new Git Command based on given command and arguments. | ||||
| func NewCommandContext(ctx context.Context, args ...string) *Command { | ||||
| 	// Make an explicit copy of GlobalCommandArgs, otherwise append might overwrite it | ||||
| 	cargs := make([]string, len(GlobalCommandArgs)) | ||||
| 	copy(cargs, GlobalCommandArgs) | ||||
| 	return &Command{ | ||||
| 		name:          GitExecutable, | ||||
| 		args:          append(cargs, args...), | ||||
| 		parentContext: DefaultContext, | ||||
| 		parentContext: ctx, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // NewCommandNoGlobals creates and returns a new Git Command based on given command and arguments only with the specify args and don't care global command args | ||||
| func NewCommandNoGlobals(args ...string) *Command { | ||||
| 	return NewCommandContextNoGlobals(DefaultContext, args...) | ||||
| } | ||||
|  | ||||
| // NewCommandContextNoGlobals creates and returns a new Git Command based on given command and arguments only with the specify args and don't care global command args | ||||
| func NewCommandContextNoGlobals(ctx context.Context, args ...string) *Command { | ||||
| 	return &Command{ | ||||
| 		name:          GitExecutable, | ||||
| 		args:          args, | ||||
| 		parentContext: DefaultContext, | ||||
| 		parentContext: ctx, | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -150,11 +150,11 @@ func Init(ctx context.Context) error { | ||||
| } | ||||
|  | ||||
| // Fsck verifies the connectivity and validity of the objects in the database | ||||
| func Fsck(repoPath string, timeout time.Duration, args ...string) error { | ||||
| func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args ...string) error { | ||||
| 	// Make sure timeout makes sense. | ||||
| 	if timeout <= 0 { | ||||
| 		timeout = -1 | ||||
| 	} | ||||
| 	_, err := NewCommand("fsck").AddArguments(args...).RunInDirTimeout(timeout, repoPath) | ||||
| 	_, err := NewCommandContext(ctx, "fsck").AddArguments(args...).RunInDirTimeout(timeout, repoPath) | ||||
| 	return err | ||||
| } | ||||
|   | ||||
| @@ -13,17 +13,19 @@ import ( | ||||
| ) | ||||
|  | ||||
| // UpdateMigrationPosterID updates all migrated repositories' issues and comments posterID | ||||
| func UpdateMigrationPosterID(ctx context.Context) { | ||||
| func UpdateMigrationPosterID(ctx context.Context) error { | ||||
| 	for _, gitService := range structs.SupportedFullGitService { | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			log.Warn("UpdateMigrationPosterID aborted due to shutdown before %s", gitService.Name()) | ||||
| 			log.Warn("UpdateMigrationPosterID aborted before %s", gitService.Name()) | ||||
| 			return models.ErrCancelledf("during UpdateMigrationPosterID before %s", gitService.Name()) | ||||
| 		default: | ||||
| 		} | ||||
| 		if err := updateMigrationPosterIDByGitService(ctx, gitService); err != nil { | ||||
| 			log.Error("updateMigrationPosterIDByGitService failed: %v", err) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func updateMigrationPosterIDByGitService(ctx context.Context, tp structs.GitServiceType) error { | ||||
| @@ -37,7 +39,7 @@ func updateMigrationPosterIDByGitService(ctx context.Context, tp structs.GitServ | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			log.Warn("UpdateMigrationPosterIDByGitService(%s) aborted due to shutdown", tp.Name()) | ||||
| 			log.Warn("UpdateMigrationPosterIDByGitService(%s) cancelled", tp.Name()) | ||||
| 			return nil | ||||
| 		default: | ||||
| 		} | ||||
| @@ -54,7 +56,7 @@ func updateMigrationPosterIDByGitService(ctx context.Context, tp structs.GitServ | ||||
| 		for _, user := range users { | ||||
| 			select { | ||||
| 			case <-ctx.Done(): | ||||
| 				log.Warn("UpdateMigrationPosterIDByGitService(%s) aborted due to shutdown", tp.Name()) | ||||
| 				log.Warn("UpdateMigrationPosterIDByGitService(%s) cancelled", tp.Name()) | ||||
| 				return nil | ||||
| 			default: | ||||
| 			} | ||||
|   | ||||
| @@ -7,19 +7,19 @@ package repository | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
|  | ||||
| 	"github.com/unknwon/com" | ||||
| 	"xorm.io/builder" | ||||
| ) | ||||
|  | ||||
| // GitFsck calls 'git fsck' to check repository health. | ||||
| func GitFsck(ctx context.Context) error { | ||||
| func GitFsck(ctx context.Context, timeout time.Duration, args []string) error { | ||||
| 	log.Trace("Doing: GitFsck") | ||||
|  | ||||
| 	if err := models.Iterate( | ||||
| @@ -27,24 +27,24 @@ func GitFsck(ctx context.Context) error { | ||||
| 		new(models.Repository), | ||||
| 		builder.Expr("id>0 AND is_fsck_enabled=?", true), | ||||
| 		func(idx int, bean interface{}) error { | ||||
| 			repo := bean.(*models.Repository) | ||||
| 			select { | ||||
| 			case <-ctx.Done(): | ||||
| 				return fmt.Errorf("Aborted due to shutdown") | ||||
| 				return models.ErrCancelledf("before fsck of %s", repo.FullName()) | ||||
| 			default: | ||||
| 			} | ||||
| 			repo := bean.(*models.Repository) | ||||
| 			log.Trace("Running health check on repository %v", repo) | ||||
| 			repoPath := repo.RepoPath() | ||||
| 			log.Trace("Running health check on repository %s", repoPath) | ||||
| 			if err := git.Fsck(repoPath, setting.Cron.RepoHealthCheck.Timeout, setting.Cron.RepoHealthCheck.Args...); err != nil { | ||||
| 				desc := fmt.Sprintf("Failed to health check repository (%s): %v", repoPath, err) | ||||
| 				log.Warn(desc) | ||||
| 				if err = models.CreateRepositoryNotice(desc); err != nil { | ||||
| 			if err := git.Fsck(ctx, repoPath, timeout, args...); err != nil { | ||||
| 				log.Warn("Failed to health check repository (%v): %v", repo, err) | ||||
| 				if err = models.CreateRepositoryNotice("Failed to health check repository (%s): %v", repo.FullName(), err); err != nil { | ||||
| 					log.Error("CreateRepositoryNotice: %v", err) | ||||
| 				} | ||||
| 			} | ||||
| 			return nil | ||||
| 		}, | ||||
| 	); err != nil { | ||||
| 		log.Trace("Error: GitFsck: %v", err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| @@ -53,32 +53,43 @@ func GitFsck(ctx context.Context) error { | ||||
| } | ||||
|  | ||||
| // GitGcRepos calls 'git gc' to remove unnecessary files and optimize the local repository | ||||
| func GitGcRepos(ctx context.Context) error { | ||||
| func GitGcRepos(ctx context.Context, timeout time.Duration, args ...string) error { | ||||
| 	log.Trace("Doing: GitGcRepos") | ||||
| 	args := append([]string{"gc"}, setting.Git.GCArgs...) | ||||
| 	args = append([]string{"gc"}, args...) | ||||
|  | ||||
| 	if err := models.Iterate( | ||||
| 		models.DefaultDBContext(), | ||||
| 		new(models.Repository), | ||||
| 		builder.Gt{"id": 0}, | ||||
| 		func(idx int, bean interface{}) error { | ||||
| 			repo := bean.(*models.Repository) | ||||
| 			select { | ||||
| 			case <-ctx.Done(): | ||||
| 				return fmt.Errorf("Aborted due to shutdown") | ||||
| 				return models.ErrCancelledf("before GC of %s", repo.FullName()) | ||||
| 			default: | ||||
| 			} | ||||
|  | ||||
| 			repo := bean.(*models.Repository) | ||||
| 			if err := repo.GetOwner(); err != nil { | ||||
| 				return err | ||||
| 			log.Trace("Running git gc on %v", repo) | ||||
| 			command := git.NewCommandContext(ctx, args...). | ||||
| 				SetDescription(fmt.Sprintf("Repository Garbage Collection: %s", repo.FullName())) | ||||
| 			var stdout string | ||||
| 			var err error | ||||
| 			if timeout > 0 { | ||||
| 				var stdoutBytes []byte | ||||
| 				stdoutBytes, err = command.RunInDirTimeout( | ||||
| 					timeout, | ||||
| 					repo.RepoPath()) | ||||
| 				stdout = string(stdoutBytes) | ||||
| 			} else { | ||||
| 				stdout, err = command.RunInDir(repo.RepoPath()) | ||||
| 			} | ||||
| 			if stdout, err := git.NewCommand(args...). | ||||
| 				SetDescription(fmt.Sprintf("Repository Garbage Collection: %s", repo.FullName())). | ||||
| 				RunInDirTimeout( | ||||
| 					time.Duration(setting.Git.Timeout.GC)*time.Second, | ||||
| 					repo.RepoPath()); err != nil { | ||||
|  | ||||
| 			if err != nil { | ||||
| 				log.Error("Repository garbage collection failed for %v. Stdout: %s\nError: %v", repo, stdout, err) | ||||
| 				return fmt.Errorf("Repository garbage collection failed: Error: %v", err) | ||||
| 				desc := fmt.Sprintf("Repository garbage collection failed for %s. Stdout: %s\nError: %v", repo.RepoPath(), stdout, err) | ||||
| 				if err = models.CreateRepositoryNotice(desc); err != nil { | ||||
| 					log.Error("CreateRepositoryNotice: %v", err) | ||||
| 				} | ||||
| 				return fmt.Errorf("Repository garbage collection failed in repo: %s: Error: %v", repo.FullName(), err) | ||||
| 			} | ||||
| 			return nil | ||||
| 		}, | ||||
| @@ -90,7 +101,7 @@ func GitGcRepos(ctx context.Context) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func gatherMissingRepoRecords() ([]*models.Repository, error) { | ||||
| func gatherMissingRepoRecords(ctx context.Context) ([]*models.Repository, error) { | ||||
| 	repos := make([]*models.Repository, 0, 10) | ||||
| 	if err := models.Iterate( | ||||
| 		models.DefaultDBContext(), | ||||
| @@ -98,24 +109,33 @@ func gatherMissingRepoRecords() ([]*models.Repository, error) { | ||||
| 		builder.Gt{"id": 0}, | ||||
| 		func(idx int, bean interface{}) error { | ||||
| 			repo := bean.(*models.Repository) | ||||
| 			select { | ||||
| 			case <-ctx.Done(): | ||||
| 				return models.ErrCancelledf("during gathering missing repo records before checking %s", repo.FullName()) | ||||
| 			default: | ||||
| 			} | ||||
| 			if !com.IsDir(repo.RepoPath()) { | ||||
| 				repos = append(repos, repo) | ||||
| 			} | ||||
| 			return nil | ||||
| 		}, | ||||
| 	); err != nil { | ||||
| 		if err2 := models.CreateRepositoryNotice(fmt.Sprintf("gatherMissingRepoRecords: %v", err)); err2 != nil { | ||||
| 			return nil, fmt.Errorf("CreateRepositoryNotice: %v", err) | ||||
| 		if strings.HasPrefix("Aborted gathering missing repo", err.Error()) { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if err2 := models.CreateRepositoryNotice("gatherMissingRepoRecords: %v", err); err2 != nil { | ||||
| 			log.Error("CreateRepositoryNotice: %v", err2) | ||||
| 		} | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return repos, nil | ||||
| } | ||||
|  | ||||
| // DeleteMissingRepositories deletes all repository records that lost Git files. | ||||
| func DeleteMissingRepositories(doer *models.User) error { | ||||
| 	repos, err := gatherMissingRepoRecords() | ||||
| func DeleteMissingRepositories(ctx context.Context, doer *models.User) error { | ||||
| 	repos, err := gatherMissingRepoRecords(ctx) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("gatherMissingRepoRecords: %v", err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if len(repos) == 0 { | ||||
| @@ -123,10 +143,16 @@ func DeleteMissingRepositories(doer *models.User) error { | ||||
| 	} | ||||
|  | ||||
| 	for _, repo := range repos { | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			return models.ErrCancelledf("during DeleteMissingRepositories before %s", repo.FullName()) | ||||
| 		default: | ||||
| 		} | ||||
| 		log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID) | ||||
| 		if err := models.DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil { | ||||
| 			if err2 := models.CreateRepositoryNotice(fmt.Sprintf("DeleteRepository [%d]: %v", repo.ID, err)); err2 != nil { | ||||
| 				return fmt.Errorf("CreateRepositoryNotice: %v", err) | ||||
| 			log.Error("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err) | ||||
| 			if err2 := models.CreateRepositoryNotice("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err); err2 != nil { | ||||
| 				log.Error("CreateRepositoryNotice: %v", err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| @@ -134,10 +160,10 @@ func DeleteMissingRepositories(doer *models.User) error { | ||||
| } | ||||
|  | ||||
| // ReinitMissingRepositories reinitializes all repository records that lost Git files. | ||||
| func ReinitMissingRepositories() error { | ||||
| 	repos, err := gatherMissingRepoRecords() | ||||
| func ReinitMissingRepositories(ctx context.Context) error { | ||||
| 	repos, err := gatherMissingRepoRecords(ctx) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("gatherMissingRepoRecords: %v", err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if len(repos) == 0 { | ||||
| @@ -145,10 +171,16 @@ func ReinitMissingRepositories() error { | ||||
| 	} | ||||
|  | ||||
| 	for _, repo := range repos { | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			return models.ErrCancelledf("during ReinitMissingRepositories before %s", repo.FullName()) | ||||
| 		default: | ||||
| 		} | ||||
| 		log.Trace("Initializing %d/%d...", repo.OwnerID, repo.ID) | ||||
| 		if err := git.InitRepository(repo.RepoPath(), true); err != nil { | ||||
| 			if err2 := models.CreateRepositoryNotice(fmt.Sprintf("InitRepository [%d]: %v", repo.ID, err)); err2 != nil { | ||||
| 				return fmt.Errorf("CreateRepositoryNotice: %v", err) | ||||
| 			log.Error("Unable (re)initialize repository %d at %s. Error: %v", repo.ID, repo.RepoPath(), err) | ||||
| 			if err2 := models.CreateRepositoryNotice("InitRepository [%d]: %v", repo.ID, err); err2 != nil { | ||||
| 				log.Error("CreateRepositoryNotice: %v", err2) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -160,17 +160,18 @@ func SyncRepositoryHooks(ctx context.Context) error { | ||||
| 		new(models.Repository), | ||||
| 		builder.Gt{"id": 0}, | ||||
| 		func(idx int, bean interface{}) error { | ||||
| 			repo := bean.(*models.Repository) | ||||
| 			select { | ||||
| 			case <-ctx.Done(): | ||||
| 				return fmt.Errorf("Aborted due to shutdown") | ||||
| 				return models.ErrCancelledf("before sync repository hooks for %s", repo.FullName()) | ||||
| 			default: | ||||
| 			} | ||||
|  | ||||
| 			if err := createDelegateHooks(bean.(*models.Repository).RepoPath()); err != nil { | ||||
| 			if err := createDelegateHooks(repo.RepoPath()); err != nil { | ||||
| 				return fmt.Errorf("SyncRepositoryHook: %v", err) | ||||
| 			} | ||||
| 			if bean.(*models.Repository).HasWiki() { | ||||
| 				if err := createDelegateHooks(bean.(*models.Repository).WikiPath()); err != nil { | ||||
| 			if repo.HasWiki() { | ||||
| 				if err := createDelegateHooks(repo.WikiPath()); err != nil { | ||||
| 					return fmt.Errorf("SyncRepositoryHook: %v", err) | ||||
| 				} | ||||
| 			} | ||||
|   | ||||
| @@ -4,129 +4,8 @@ | ||||
|  | ||||
| package setting | ||||
|  | ||||
| import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
|  | ||||
| 	// Cron tasks | ||||
| 	Cron = struct { | ||||
| 		UpdateMirror struct { | ||||
| 			Enabled    bool | ||||
| 			RunAtStart bool | ||||
| 			Schedule   string | ||||
| 		} `ini:"cron.update_mirrors"` | ||||
| 		RepoHealthCheck struct { | ||||
| 			Enabled    bool | ||||
| 			RunAtStart bool | ||||
| 			Schedule   string | ||||
| 			Timeout    time.Duration | ||||
| 			Args       []string `delim:" "` | ||||
| 		} `ini:"cron.repo_health_check"` | ||||
| 		CheckRepoStats struct { | ||||
| 			Enabled    bool | ||||
| 			RunAtStart bool | ||||
| 			Schedule   string | ||||
| 		} `ini:"cron.check_repo_stats"` | ||||
| 		ArchiveCleanup struct { | ||||
| 			Enabled    bool | ||||
| 			RunAtStart bool | ||||
| 			Schedule   string | ||||
| 			OlderThan  time.Duration | ||||
| 		} `ini:"cron.archive_cleanup"` | ||||
| 		SyncExternalUsers struct { | ||||
| 			Enabled        bool | ||||
| 			RunAtStart     bool | ||||
| 			Schedule       string | ||||
| 			UpdateExisting bool | ||||
| 		} `ini:"cron.sync_external_users"` | ||||
| 		DeletedBranchesCleanup struct { | ||||
| 			Enabled    bool | ||||
| 			RunAtStart bool | ||||
| 			Schedule   string | ||||
| 			OlderThan  time.Duration | ||||
| 		} `ini:"cron.deleted_branches_cleanup"` | ||||
| 		UpdateMigrationPosterID struct { | ||||
| 			Schedule string | ||||
| 		} `ini:"cron.update_migration_poster_id"` | ||||
| 	}{ | ||||
| 		UpdateMirror: struct { | ||||
| 			Enabled    bool | ||||
| 			RunAtStart bool | ||||
| 			Schedule   string | ||||
| 		}{ | ||||
| 			Enabled:    true, | ||||
| 			RunAtStart: false, | ||||
| 			Schedule:   "@every 10m", | ||||
| 		}, | ||||
| 		RepoHealthCheck: struct { | ||||
| 			Enabled    bool | ||||
| 			RunAtStart bool | ||||
| 			Schedule   string | ||||
| 			Timeout    time.Duration | ||||
| 			Args       []string `delim:" "` | ||||
| 		}{ | ||||
| 			Enabled:    true, | ||||
| 			RunAtStart: false, | ||||
| 			Schedule:   "@every 24h", | ||||
| 			Timeout:    60 * time.Second, | ||||
| 			Args:       []string{}, | ||||
| 		}, | ||||
| 		CheckRepoStats: struct { | ||||
| 			Enabled    bool | ||||
| 			RunAtStart bool | ||||
| 			Schedule   string | ||||
| 		}{ | ||||
| 			Enabled:    true, | ||||
| 			RunAtStart: true, | ||||
| 			Schedule:   "@every 24h", | ||||
| 		}, | ||||
| 		ArchiveCleanup: struct { | ||||
| 			Enabled    bool | ||||
| 			RunAtStart bool | ||||
| 			Schedule   string | ||||
| 			OlderThan  time.Duration | ||||
| 		}{ | ||||
| 			Enabled:    true, | ||||
| 			RunAtStart: true, | ||||
| 			Schedule:   "@every 24h", | ||||
| 			OlderThan:  24 * time.Hour, | ||||
| 		}, | ||||
| 		SyncExternalUsers: struct { | ||||
| 			Enabled        bool | ||||
| 			RunAtStart     bool | ||||
| 			Schedule       string | ||||
| 			UpdateExisting bool | ||||
| 		}{ | ||||
| 			Enabled:        true, | ||||
| 			RunAtStart:     false, | ||||
| 			Schedule:       "@every 24h", | ||||
| 			UpdateExisting: true, | ||||
| 		}, | ||||
| 		DeletedBranchesCleanup: struct { | ||||
| 			Enabled    bool | ||||
| 			RunAtStart bool | ||||
| 			Schedule   string | ||||
| 			OlderThan  time.Duration | ||||
| 		}{ | ||||
| 			Enabled:    true, | ||||
| 			RunAtStart: true, | ||||
| 			Schedule:   "@every 24h", | ||||
| 			OlderThan:  24 * time.Hour, | ||||
| 		}, | ||||
| 		UpdateMigrationPosterID: struct { | ||||
| 			Schedule string | ||||
| 		}{ | ||||
| 			Schedule: "@every 24h", | ||||
| 		}, | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| func newCron() { | ||||
| 	if err := Cfg.Section("cron").MapTo(&Cron); err != nil { | ||||
| 		log.Fatal("Failed to map Cron settings: %v", err) | ||||
| 	} | ||||
| // GetCronSettings maps the cron subsection to the provided config | ||||
| func GetCronSettings(name string, config interface{}) (interface{}, error) { | ||||
| 	err := Cfg.Section("cron." + name).MapTo(config) | ||||
| 	return config, err | ||||
| } | ||||
|   | ||||
| @@ -981,7 +981,6 @@ func NewContext() { | ||||
| 	u.Path = path.Join(u.Path, "api", "swagger") | ||||
| 	API.SwaggerURL = u.String() | ||||
|  | ||||
| 	newCron() | ||||
| 	newGit() | ||||
|  | ||||
| 	sec = Cfg.Section("mirror") | ||||
|   | ||||
| @@ -1842,26 +1842,37 @@ dashboard.operation_switch = Switch | ||||
| dashboard.operation_run = Run | ||||
| dashboard.clean_unbind_oauth = Clean unbound OAuth connections | ||||
| dashboard.clean_unbind_oauth_success = All unbound OAuth connections have been deleted. | ||||
| dashboard.delete_inactivate_accounts = Delete all unactivated accounts | ||||
| dashboard.delete_inactivate_accounts_success = All unactivated accounts have been deleted. | ||||
| dashboard.task.started=Started Task: %s | ||||
| dashboard.task.process=Task: %s | ||||
| dashboard.task.cancelled=Task: %s cancelled: %[3]s  | ||||
| dashboard.task.error=Error in Task: %s: %[3]s | ||||
| dashboard.task.finished=Task: %s started by %s has finished | ||||
| dashboard.task.unknown=Unknown task: %s | ||||
| dashboard.cron.started=Started Cron: %[1]s | ||||
| dashboard.cron.process=Cron: %[1]s | ||||
| dashboard.cron.cancelled=Cron: %s cancelled: %[3]s  | ||||
| dashboard.cron.error=Error in Cron: %s: %[3]s | ||||
| dashboard.cron.finished=Cron: %[1]s has finished | ||||
| dashboard.delete_inactive_accounts = Delete all unactivated accounts | ||||
| dashboard.delete_inactive_accounts.started = Delete all unactivated accounts task started. | ||||
| dashboard.delete_repo_archives = Delete all repository archives | ||||
| dashboard.delete_repo_archives_success = All repository archives have been deleted. | ||||
| dashboard.delete_repo_archives.started = Delete all repository archives task started. | ||||
| 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.started = Delete all repositories missing their Git files task started. | ||||
| dashboard.delete_generated_repository_avatars = Delete generated repository avatars | ||||
| dashboard.delete_generated_repository_avatars_success = Generated repository avatars were deleted. | ||||
| dashboard.update_mirrors = Update Mirrors | ||||
| dashboard.repo_health_check = Health check all repositories | ||||
| dashboard.check_repo_stats = Check all repository statistics | ||||
| dashboard.archive_cleanup = Delete old repository archives | ||||
| dashboard.deleted_branches_cleanup = Clean-up deleted branches | ||||
| dashboard.update_migration_poster_id = Update migration poster IDs | ||||
| dashboard.git_gc_repos = Garbage collect all repositories | ||||
| 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_success = The public SSH keys controlled by Gitea have been updated. | ||||
| dashboard.resync_all_sshkeys = Update the '.ssh/authorized_keys' file with Gitea SSH keys. | ||||
| dashboard.resync_all_sshkeys.desc = (Not needed for the built-in SSH server.) | ||||
| dashboard.resync_all_hooks = Resynchronize pre-receive, update and post-receive hooks of all repositories. | ||||
| dashboard.resync_all_hooks_success = All pre-receive, update and post-receive repository hooks have been resynchronized. | ||||
| dashboard.reinit_missing_repos = Reinitialize all missing Git repositories for which records exist | ||||
| dashboard.reinit_missing_repos_success = All missing Git repositories for which records existed have been reinitialized. | ||||
| dashboard.sync_external_users = Synchronize external user data | ||||
| dashboard.sync_external_users_started = External user data synchronization has started. | ||||
| dashboard.git_fsck = Execute health checks on all repositories | ||||
| dashboard.git_fsck_started = Repository health checks have started. | ||||
| dashboard.server_uptime = Server Uptime | ||||
| dashboard.current_goroutine = Current Goroutines | ||||
| dashboard.current_memory_usage = Current Memory Usage | ||||
| @@ -2266,6 +2277,7 @@ notices.delete_selected = Delete Selected | ||||
| notices.delete_all = Delete All Notices | ||||
| notices.type = Type | ||||
| notices.type_1 = Repository | ||||
| notices.type_2 = Task | ||||
| notices.desc = Description | ||||
| notices.op = Op. | ||||
| notices.delete_success = The system notices have been deleted. | ||||
|   | ||||
| @@ -21,11 +21,9 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/cron" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/graceful" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/process" | ||||
| 	"code.gitea.io/gitea/modules/queue" | ||||
| 	repo_module "code.gitea.io/gitea/modules/repository" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/services/mailer" | ||||
| @@ -124,22 +122,6 @@ func updateSystemStatus() { | ||||
| 	sysStatus.NumGC = m.NumGC | ||||
| } | ||||
|  | ||||
| // Operation Operation types. | ||||
| type Operation int | ||||
|  | ||||
| const ( | ||||
| 	cleanInactivateUser Operation = iota + 1 | ||||
| 	cleanRepoArchives | ||||
| 	cleanMissingRepos | ||||
| 	gitGCRepos | ||||
| 	syncSSHAuthorizedKey | ||||
| 	syncRepositoryUpdateHook | ||||
| 	reinitMissingRepository | ||||
| 	syncExternalUsers | ||||
| 	gitFsck | ||||
| 	deleteGeneratedRepositoryAvatars | ||||
| ) | ||||
|  | ||||
| // Dashboard show admin panel dashboard | ||||
| func Dashboard(ctx *context.Context) { | ||||
| 	ctx.Data["Title"] = ctx.Tr("admin.dashboard") | ||||
| @@ -162,48 +144,13 @@ func DashboardPost(ctx *context.Context, form auth.AdminDashboardForm) { | ||||
| 	ctx.Data["SysStatus"] = sysStatus | ||||
|  | ||||
| 	// Run operation. | ||||
| 	if form.Op > 0 { | ||||
| 		var err error | ||||
| 		var success string | ||||
| 		shutdownCtx := graceful.GetManager().ShutdownContext() | ||||
|  | ||||
| 		switch Operation(form.Op) { | ||||
| 		case cleanInactivateUser: | ||||
| 			success = ctx.Tr("admin.dashboard.delete_inactivate_accounts_success") | ||||
| 			err = models.DeleteInactivateUsers() | ||||
| 		case cleanRepoArchives: | ||||
| 			success = ctx.Tr("admin.dashboard.delete_repo_archives_success") | ||||
| 			err = models.DeleteRepositoryArchives() | ||||
| 		case cleanMissingRepos: | ||||
| 			success = ctx.Tr("admin.dashboard.delete_missing_repos_success") | ||||
| 			err = repo_module.DeleteMissingRepositories(ctx.User) | ||||
| 		case gitGCRepos: | ||||
| 			success = ctx.Tr("admin.dashboard.git_gc_repos_success") | ||||
| 			err = repo_module.GitGcRepos(shutdownCtx) | ||||
| 		case syncSSHAuthorizedKey: | ||||
| 			success = ctx.Tr("admin.dashboard.resync_all_sshkeys_success") | ||||
| 			err = models.RewriteAllPublicKeys() | ||||
| 		case syncRepositoryUpdateHook: | ||||
| 			success = ctx.Tr("admin.dashboard.resync_all_hooks_success") | ||||
| 			err = repo_module.SyncRepositoryHooks(shutdownCtx) | ||||
| 		case reinitMissingRepository: | ||||
| 			success = ctx.Tr("admin.dashboard.reinit_missing_repos_success") | ||||
| 			err = repo_module.ReinitMissingRepositories() | ||||
| 		case syncExternalUsers: | ||||
| 			success = ctx.Tr("admin.dashboard.sync_external_users_started") | ||||
| 			go graceful.GetManager().RunWithShutdownContext(models.SyncExternalUsers) | ||||
| 		case gitFsck: | ||||
| 			success = ctx.Tr("admin.dashboard.git_fsck_started") | ||||
| 			err = repo_module.GitFsck(shutdownCtx) | ||||
| 		case deleteGeneratedRepositoryAvatars: | ||||
| 			success = ctx.Tr("admin.dashboard.delete_generated_repository_avatars_success") | ||||
| 			err = models.RemoveRandomAvatars() | ||||
| 		} | ||||
|  | ||||
| 		if err != nil { | ||||
| 			ctx.Flash.Error(err.Error()) | ||||
| 	if form.Op != "" { | ||||
| 		task := cron.GetTask(form.Op) | ||||
| 		if task != nil { | ||||
| 			go task.RunWithUser(ctx.User, nil) | ||||
| 			ctx.Flash.Success(ctx.Tr("admin.dashboard.task.started", ctx.Tr("admin.dashboard."+form.Op))) | ||||
| 		} else { | ||||
| 			ctx.Flash.Success(success) | ||||
| 			ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -25,6 +25,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/markup/external" | ||||
| 	"code.gitea.io/gitea/modules/notification" | ||||
| 	"code.gitea.io/gitea/modules/options" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/ssh" | ||||
| 	"code.gitea.io/gitea/modules/task" | ||||
| @@ -33,7 +34,9 @@ import ( | ||||
| 	mirror_service "code.gitea.io/gitea/services/mirror" | ||||
| 	pull_service "code.gitea.io/gitea/services/pull" | ||||
|  | ||||
| 	"gitea.com/macaron/i18n" | ||||
| 	"gitea.com/macaron/macaron" | ||||
| 	unknwoni18n "github.com/unknwon/i18n" | ||||
| ) | ||||
|  | ||||
| func checkRunMode() { | ||||
| @@ -79,6 +82,33 @@ func initDBEngine(ctx context.Context) (err error) { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // InitLocales loads the locales | ||||
| func InitLocales() { | ||||
| 	localeNames, err := options.Dir("locale") | ||||
|  | ||||
| 	if err != nil { | ||||
| 		log.Fatal("Failed to list locale files: %v", err) | ||||
| 	} | ||||
| 	localFiles := make(map[string][]byte) | ||||
|  | ||||
| 	for _, name := range localeNames { | ||||
| 		localFiles[name], err = options.Locale(name) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			log.Fatal("Failed to load %s locale file. %v", name, err) | ||||
| 		} | ||||
| 	} | ||||
| 	i18n.I18n(i18n.Options{ | ||||
| 		SubURL:       setting.AppSubURL, | ||||
| 		Files:        localFiles, | ||||
| 		Langs:        setting.Langs, | ||||
| 		Names:        setting.Names, | ||||
| 		DefaultLang:  "en-US", | ||||
| 		Redirect:     false, | ||||
| 		CookieDomain: setting.SessionConfig.Domain, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // GlobalInit is for global configuration reload-able. | ||||
| func GlobalInit(ctx context.Context) { | ||||
| 	setting.NewContext() | ||||
| @@ -91,6 +121,11 @@ func GlobalInit(ctx context.Context) { | ||||
| 	log.Trace("Custom path: %s", setting.CustomPath) | ||||
| 	log.Trace("Log path: %s", setting.LogRootPath) | ||||
|  | ||||
| 	// Setup i18n | ||||
| 	InitLocales() | ||||
|  | ||||
| 	log.Info("%s", unknwoni18n.Tr("en-US", "admin.dashboard.delete_repo_archives")) | ||||
|  | ||||
| 	NewServices() | ||||
|  | ||||
| 	if setting.InstallLock { | ||||
|   | ||||
| @@ -296,7 +296,7 @@ func Password(m *models.Mirror) string { | ||||
| } | ||||
|  | ||||
| // Update checks and updates mirror repositories. | ||||
| func Update(ctx context.Context) { | ||||
| func Update(ctx context.Context) error { | ||||
| 	log.Trace("Doing: Update") | ||||
| 	if err := models.MirrorsIterate(func(idx int, bean interface{}) error { | ||||
| 		m := bean.(*models.Mirror) | ||||
| @@ -306,14 +306,17 @@ func Update(ctx context.Context) { | ||||
| 		} | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			return fmt.Errorf("Aborted due to shutdown") | ||||
| 			return fmt.Errorf("Aborted") | ||||
| 		default: | ||||
| 			mirrorQueue.Add(m.RepoID) | ||||
| 			return nil | ||||
| 		} | ||||
| 	}); err != nil { | ||||
| 		log.Error("Update: %v", err) | ||||
| 		log.Trace("Update: %v", err) | ||||
| 		return err | ||||
| 	} | ||||
| 	log.Trace("Finished: Update") | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // SyncMirrors checks and syncs mirrors. | ||||
|   | ||||
| @@ -15,49 +15,50 @@ | ||||
| 			{{.i18n.Tr "admin.dashboard.operations"}} | ||||
| 		</h4> | ||||
| 		<div class="ui attached table segment"> | ||||
| 			<form method="post" action="{{AppSubUrl}}/admin"> | ||||
| 			<form method="post" action="{{.AppSubUrl}}/admin"> | ||||
| 				{{.CsrfTokenHtml}} | ||||
| 				<table class="ui very basic table"> | ||||
| 					<tbody> | ||||
| 						<tr> | ||||
| 							<td>{{.i18n.Tr "admin.dashboard.delete_inactivate_accounts"}}</td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="1">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 							<td>{{.i18n.Tr "admin.dashboard.delete_inactive_accounts"}}</td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="delete_inactive_accounts">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 						</tr> | ||||
| 						<tr> | ||||
| 							<td>{{.i18n.Tr "admin.dashboard.delete_repo_archives"}}</td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="2">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="delete_repo_archives">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 						</tr> | ||||
| 						<tr> | ||||
| 							<td>{{.i18n.Tr "admin.dashboard.delete_missing_repos"}}</td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="3">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="delete_missing_repos">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 						</tr> | ||||
| 						<tr> | ||||
| 							<td>{{.i18n.Tr "admin.dashboard.git_gc_repos"}}</td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="4">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="git_gc_repos">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 						</tr> | ||||
| 						<tr> | ||||
| 							<td>{{.i18n.Tr "admin.dashboard.resync_all_sshkeys"}}</td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="5">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 							<td>{{.i18n.Tr "admin.dashboard.resync_all_sshkeys"}}<br/> | ||||
| 							{{.i18n.Tr "admin.dashboard.resync_all_sshkeys.desc"}}</td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="resync_all_sshkeys">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 						</tr> | ||||
| 						<tr> | ||||
| 							<td>{{.i18n.Tr "admin.dashboard.resync_all_hooks"}}</td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="6">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="resync_all_hooks">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 						</tr> | ||||
| 						<tr> | ||||
| 							<td>{{.i18n.Tr "admin.dashboard.reinit_missing_repos"}}</td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="7">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="reinit_missing_repos">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 						</tr> | ||||
| 						<tr> | ||||
| 							<td>{{.i18n.Tr "admin.dashboard.sync_external_users"}}</td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="8">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="sync_external_users">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 						</tr> | ||||
| 						<tr> | ||||
| 							<td>{{.i18n.Tr "admin.dashboard.git_fsck"}}</td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="9">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="git_fsck">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 						</tr> | ||||
| 						<tr> | ||||
| 							<td>{{.i18n.Tr "admin.dashboard.delete_generated_repository_avatars"}}</td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="10">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 							<td><button type="submit" class="ui green button" name="op" value="delete_generated_repository_avatars">{{svg "octicon-triangle-right" 16}} {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 						</tr> | ||||
| 					</tbody> | ||||
| 				</table> | ||||
|   | ||||
| @@ -7,28 +7,33 @@ | ||||
| 			{{.i18n.Tr "admin.monitor.cron"}} | ||||
| 		</h4> | ||||
| 		<div class="ui attached table segment"> | ||||
| 			<table class="ui very basic striped table"> | ||||
| 				<thead> | ||||
| 					<tr> | ||||
| 						<th>{{.i18n.Tr "admin.monitor.name"}}</th> | ||||
| 						<th>{{.i18n.Tr "admin.monitor.schedule"}}</th> | ||||
| 						<th>{{.i18n.Tr "admin.monitor.next"}}</th> | ||||
| 						<th>{{.i18n.Tr "admin.monitor.previous"}}</th> | ||||
| 						<th>{{.i18n.Tr "admin.monitor.execute_times"}}</th> | ||||
| 					</tr> | ||||
| 				</thead> | ||||
| 				<tbody> | ||||
| 					{{range .Entries}} | ||||
| 			<form method="post" action="{{.AppSubUrl}}/admin"> | ||||
| 				{{.CsrfTokenHtml}} | ||||
| 				<table class="ui very basic striped table"> | ||||
| 					<thead> | ||||
| 						<tr> | ||||
| 							<td>{{.Description}}</td> | ||||
| 							<td>{{.Spec}}</td> | ||||
| 							<td>{{DateFmtLong .Next}}</td> | ||||
| 							<td>{{if gt .Prev.Year 1 }}{{DateFmtLong .Prev}}{{else}}N/A{{end}}</td> | ||||
| 							<td>{{.ExecTimes}}</td> | ||||
| 							<th></th> | ||||
| 							<th>{{.i18n.Tr "admin.monitor.name"}}</th> | ||||
| 							<th>{{.i18n.Tr "admin.monitor.schedule"}}</th> | ||||
| 							<th>{{.i18n.Tr "admin.monitor.next"}}</th> | ||||
| 							<th>{{.i18n.Tr "admin.monitor.previous"}}</th> | ||||
| 							<th>{{.i18n.Tr "admin.monitor.execute_times"}}</th> | ||||
| 						</tr> | ||||
| 					{{end}} | ||||
| 				</tbody> | ||||
| 			</table> | ||||
| 					</thead> | ||||
| 					<tbody> | ||||
| 						{{range .Entries}} | ||||
| 							<tr> | ||||
| 								<td><button type="submit" class="ui green button" name="op" value="{{.Name}}" title="{{$.i18n.Tr "admin.dashboard.operation_run"}}">{{svg "octicon-triangle-right" 16}}</button></td> | ||||
| 								<td>{{$.i18n.Tr (printf "admin.dashboard.%s" .Name)}}</td> | ||||
| 								<td>{{.Spec}}</td> | ||||
| 								<td>{{DateFmtLong .Next}}</td> | ||||
| 								<td>{{if gt .Prev.Year 1 }}{{DateFmtLong .Prev}}{{else}}N/A{{end}}</td> | ||||
| 								<td>{{.ExecTimes}}</td> | ||||
| 							</tr> | ||||
| 						{{end}} | ||||
| 					</tbody> | ||||
| 				</table> | ||||
| 			</form> | ||||
| 		</div> | ||||
|  | ||||
| 		<h4 class="ui top attached header"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user