diff options
Diffstat (limited to '')
-rw-r--r-- | models/migrations/v1_11/v102.go | 22 | ||||
-rw-r--r-- | models/migrations/v1_11/v103.go | 17 | ||||
-rw-r--r-- | models/migrations/v1_11/v104.go | 34 | ||||
-rw-r--r-- | models/migrations/v1_11/v105.go | 23 | ||||
-rw-r--r-- | models/migrations/v1_11/v106.go | 25 | ||||
-rw-r--r-- | models/migrations/v1_11/v107.go | 17 | ||||
-rw-r--r-- | models/migrations/v1_11/v108.go | 17 | ||||
-rw-r--r-- | models/migrations/v1_11/v109.go | 16 | ||||
-rw-r--r-- | models/migrations/v1_11/v110.go | 26 | ||||
-rw-r--r-- | models/migrations/v1_11/v111.go | 437 | ||||
-rw-r--r-- | models/migrations/v1_11/v112.go | 47 | ||||
-rw-r--r-- | models/migrations/v1_11/v113.go | 22 | ||||
-rw-r--r-- | models/migrations/v1_11/v114.go | 50 | ||||
-rw-r--r-- | models/migrations/v1_11/v115.go | 159 | ||||
-rw-r--r-- | models/migrations/v1_11/v116.go | 32 |
15 files changed, 944 insertions, 0 deletions
diff --git a/models/migrations/v1_11/v102.go b/models/migrations/v1_11/v102.go new file mode 100644 index 00000000..9358e4ce --- /dev/null +++ b/models/migrations/v1_11/v102.go @@ -0,0 +1,22 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_11 //nolint + +import ( + "code.gitea.io/gitea/models/migrations/base" + + "xorm.io/xorm" +) + +func DropColumnHeadUserNameOnPullRequest(x *xorm.Engine) error { + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + if err := base.DropTableColumns(sess, "pull_request", "head_user_name"); err != nil { + return err + } + return sess.Commit() +} diff --git a/models/migrations/v1_11/v103.go b/models/migrations/v1_11/v103.go new file mode 100644 index 00000000..53527dac --- /dev/null +++ b/models/migrations/v1_11/v103.go @@ -0,0 +1,17 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_11 //nolint + +import ( + "xorm.io/xorm" +) + +func AddWhitelistDeployKeysToBranches(x *xorm.Engine) error { + type ProtectedBranch struct { + ID int64 + WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"` + } + + return x.Sync(new(ProtectedBranch)) +} diff --git a/models/migrations/v1_11/v104.go b/models/migrations/v1_11/v104.go new file mode 100644 index 00000000..3e8ee64b --- /dev/null +++ b/models/migrations/v1_11/v104.go @@ -0,0 +1,34 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_11 //nolint + +import ( + "code.gitea.io/gitea/models/migrations/base" + + "xorm.io/xorm" +) + +func RemoveLabelUneededCols(x *xorm.Engine) error { + // Make sure the columns exist before dropping them + type Label struct { + QueryString string + IsSelected bool + } + if err := x.Sync(new(Label)); err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + if err := base.DropTableColumns(sess, "label", "query_string"); err != nil { + return err + } + if err := base.DropTableColumns(sess, "label", "is_selected"); err != nil { + return err + } + return sess.Commit() +} diff --git a/models/migrations/v1_11/v105.go b/models/migrations/v1_11/v105.go new file mode 100644 index 00000000..b91340c3 --- /dev/null +++ b/models/migrations/v1_11/v105.go @@ -0,0 +1,23 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_11 //nolint + +import ( + "xorm.io/xorm" +) + +func AddTeamIncludesAllRepositories(x *xorm.Engine) error { + type Team struct { + ID int64 `xorm:"pk autoincr"` + IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"` + } + + if err := x.Sync(new(Team)); err != nil { + return err + } + + _, err := x.Exec("UPDATE `team` SET `includes_all_repositories` = ? WHERE `name`=?", + true, "Owners") + return err +} diff --git a/models/migrations/v1_11/v106.go b/models/migrations/v1_11/v106.go new file mode 100644 index 00000000..ecb11cdd --- /dev/null +++ b/models/migrations/v1_11/v106.go @@ -0,0 +1,25 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_11 //nolint + +import ( + "xorm.io/xorm" +) + +// RepoWatchMode specifies what kind of watch the user has on a repository +type RepoWatchMode int8 + +// Watch is connection request for receiving repository notification. +type Watch struct { + ID int64 `xorm:"pk autoincr"` + Mode RepoWatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"` +} + +func AddModeColumnToWatch(x *xorm.Engine) error { + if err := x.Sync(new(Watch)); err != nil { + return err + } + _, err := x.Exec("UPDATE `watch` SET `mode` = 1") + return err +} diff --git a/models/migrations/v1_11/v107.go b/models/migrations/v1_11/v107.go new file mode 100644 index 00000000..f0bfe586 --- /dev/null +++ b/models/migrations/v1_11/v107.go @@ -0,0 +1,17 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_11 //nolint + +import ( + "xorm.io/xorm" +) + +func AddTemplateToRepo(x *xorm.Engine) error { + type Repository struct { + IsTemplate bool `xorm:"INDEX NOT NULL DEFAULT false"` + TemplateID int64 `xorm:"INDEX"` + } + + return x.Sync(new(Repository)) +} diff --git a/models/migrations/v1_11/v108.go b/models/migrations/v1_11/v108.go new file mode 100644 index 00000000..a8509623 --- /dev/null +++ b/models/migrations/v1_11/v108.go @@ -0,0 +1,17 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_11 //nolint + +import ( + "xorm.io/xorm" +) + +func AddCommentIDOnNotification(x *xorm.Engine) error { + type Notification struct { + ID int64 `xorm:"pk autoincr"` + CommentID int64 + } + + return x.Sync(new(Notification)) +} diff --git a/models/migrations/v1_11/v109.go b/models/migrations/v1_11/v109.go new file mode 100644 index 00000000..ea565ccd --- /dev/null +++ b/models/migrations/v1_11/v109.go @@ -0,0 +1,16 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_11 //nolint + +import ( + "xorm.io/xorm" +) + +func AddCanCreateOrgRepoColumnForTeam(x *xorm.Engine) error { + type Team struct { + CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"` + } + + return x.Sync(new(Team)) +} diff --git a/models/migrations/v1_11/v110.go b/models/migrations/v1_11/v110.go new file mode 100644 index 00000000..fce9be84 --- /dev/null +++ b/models/migrations/v1_11/v110.go @@ -0,0 +1,26 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_11 //nolint + +import ( + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +func ChangeReviewContentToText(x *xorm.Engine) error { + switch x.Dialect().URI().DBType { + case schemas.MYSQL: + _, err := x.Exec("ALTER TABLE review MODIFY COLUMN content TEXT") + return err + case schemas.ORACLE: + _, err := x.Exec("ALTER TABLE review MODIFY content TEXT") + return err + case schemas.POSTGRES: + _, err := x.Exec("ALTER TABLE review ALTER COLUMN content TYPE TEXT") + return err + default: + // SQLite doesn't support ALTER COLUMN, and it seem to already make String to _TEXT_ default so no migration needed + return nil + } +} diff --git a/models/migrations/v1_11/v111.go b/models/migrations/v1_11/v111.go new file mode 100644 index 00000000..cc3dc0d5 --- /dev/null +++ b/models/migrations/v1_11/v111.go @@ -0,0 +1,437 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_11 //nolint + +import ( + "fmt" + + "xorm.io/xorm" +) + +func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error { + type ProtectedBranch struct { + CanPush bool `xorm:"NOT NULL DEFAULT false"` + EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"` + ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"` + ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"` + RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"` + } + + type User struct { + ID int64 `xorm:"pk autoincr"` + Type int + + // Permissions + IsAdmin bool + // IsRestricted bool `xorm:"NOT NULL DEFAULT false"` glitch: this column was added in v1_12/v121.go + // Visibility int `xorm:"NOT NULL DEFAULT 0"` glitch: this column was added in v1_12/v124.go + } + + type Review struct { + ID int64 `xorm:"pk autoincr"` + Official bool `xorm:"NOT NULL DEFAULT false"` + + ReviewerID int64 `xorm:"index"` + IssueID int64 `xorm:"index"` + } + + if err := x.Sync(new(ProtectedBranch)); err != nil { + return err + } + + if err := x.Sync(new(Review)); err != nil { + return err + } + + const ( + // ReviewTypeApprove approves changes + ReviewTypeApprove int = 1 + // ReviewTypeReject gives feedback blocking merge + ReviewTypeReject int = 3 + + // VisibleTypePublic Visible for everyone + // VisibleTypePublic int = 0 + // VisibleTypePrivate Visible only for organization's members + // VisibleTypePrivate int = 2 + + // unit.UnitTypeCode is unit type code + UnitTypeCode int = 1 + + // AccessModeNone no access + AccessModeNone int = 0 + // AccessModeRead read access + AccessModeRead int = 1 + // AccessModeWrite write access + AccessModeWrite int = 2 + // AccessModeOwner owner access + AccessModeOwner int = 4 + ) + + // Repository represents a git repository. + type Repository struct { + ID int64 `xorm:"pk autoincr"` + OwnerID int64 `xorm:"UNIQUE(s) index"` + + IsPrivate bool `xorm:"INDEX"` + } + + type PullRequest struct { + ID int64 `xorm:"pk autoincr"` + + BaseRepoID int64 `xorm:"INDEX"` + BaseBranch string + } + + // RepoUnit describes all units of a repository + type RepoUnit struct { + ID int64 + RepoID int64 `xorm:"INDEX(s)"` + Type int `xorm:"INDEX(s)"` + } + + type Permission struct { + AccessMode int + Units []*RepoUnit + UnitsMode map[int]int + } + + type TeamUser struct { + ID int64 `xorm:"pk autoincr"` + TeamID int64 `xorm:"UNIQUE(s)"` + UID int64 `xorm:"UNIQUE(s)"` + } + + type Collaboration struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + Mode int `xorm:"DEFAULT 2 NOT NULL"` + } + + type Access struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"UNIQUE(s)"` + RepoID int64 `xorm:"UNIQUE(s)"` + Mode int + } + + type TeamUnit struct { + ID int64 `xorm:"pk autoincr"` + OrgID int64 `xorm:"INDEX"` + TeamID int64 `xorm:"UNIQUE(s)"` + Type int `xorm:"UNIQUE(s)"` + } + + // Team represents a organization team. + type Team struct { + ID int64 `xorm:"pk autoincr"` + OrgID int64 `xorm:"INDEX"` + Authorize int + } + + // getUserRepoPermission static function based on issues_model.IsOfficialReviewer at 5d78792385 + getUserRepoPermission := func(sess *xorm.Session, repo *Repository, user *User) (Permission, error) { + var perm Permission + + repoOwner := new(User) + has, err := sess.ID(repo.OwnerID).Get(repoOwner) + if err != nil || !has { + return perm, err + } + + // Prevent strangers from checking out public repo of private organization + // Allow user if they are collaborator of a repo within a private organization but not a member of the organization itself + hasOrgVisible := true + // Not SignedUser + if user == nil { + // hasOrgVisible = repoOwner.Visibility == VisibleTypePublic // VisibleTypePublic is the default + } else if !user.IsAdmin { + _, err := sess. + Where("uid=?", user.ID). + And("org_id=?", repoOwner.ID). + Table("org_user"). + Exist() + if err != nil { + hasOrgVisible = false + } + // VisibleTypePublic is the default so the condition below is always false + // if (repoOwner.Visibility == VisibleTypePrivate) && !hasMemberWithUserID { + // hasOrgVisible = false + // } + } + + isCollaborator, err := sess.Get(&Collaboration{RepoID: repo.ID, UserID: user.ID}) + if err != nil { + return perm, err + } + + if repoOwner.Type == 1 && !hasOrgVisible && !isCollaborator { + perm.AccessMode = AccessModeNone + return perm, err + } + + var units []*RepoUnit + if err := sess.Where("repo_id = ?", repo.ID).Find(&units); err != nil { + return perm, err + } + perm.Units = units + + // anonymous visit public repo + if user == nil { + perm.AccessMode = AccessModeRead + return perm, err + } + + // Admin or the owner has super access to the repository + if user.IsAdmin || user.ID == repo.OwnerID { + perm.AccessMode = AccessModeOwner + return perm, err + } + + accessLevel := func(user *User, repo *Repository) (int, error) { + mode := AccessModeNone + var userID int64 + restricted := false + + if user != nil { + userID = user.ID + restricted = false + } + + if !restricted && !repo.IsPrivate { + mode = AccessModeRead + } + + if userID == 0 { + return mode, nil + } + + if userID == repo.OwnerID { + return AccessModeOwner, nil + } + + a := &Access{UserID: userID, RepoID: repo.ID} + if has, err := sess.Get(a); !has || err != nil { + return mode, err + } + return a.Mode, nil + } + + // plain user + perm.AccessMode, err = accessLevel(user, repo) + if err != nil { + return perm, err + } + + // If Owner is no Org + if repoOwner.Type != 1 { + return perm, err + } + + perm.UnitsMode = make(map[int]int) + + // Collaborators on organization + if isCollaborator { + for _, u := range units { + perm.UnitsMode[u.Type] = perm.AccessMode + } + } + + // get units mode from teams + var teams []*Team + err = sess. + Join("INNER", "team_user", "team_user.team_id = team.id"). + Join("INNER", "team_repo", "team_repo.team_id = team.id"). + Where("team.org_id = ?", repo.OwnerID). + And("team_user.uid=?", user.ID). + And("team_repo.repo_id=?", repo.ID). + Find(&teams) + if err != nil { + return perm, err + } + + // if user in an owner team + for _, team := range teams { + if team.Authorize >= AccessModeOwner { + perm.AccessMode = AccessModeOwner + perm.UnitsMode = nil + return perm, err + } + } + + for _, u := range units { + var found bool + for _, team := range teams { + var teamU []*TeamUnit + var unitEnabled bool + err = sess.Where("team_id = ?", team.ID).Find(&teamU) + + for _, tu := range teamU { + if tu.Type == u.Type { + unitEnabled = true + break + } + } + + if unitEnabled { + m := perm.UnitsMode[u.Type] + if m < team.Authorize { + perm.UnitsMode[u.Type] = team.Authorize + } + found = true + } + } + + // for a public repo on an organization, a non-restricted user has read permission on non-team defined units. + if !found && !repo.IsPrivate { + if _, ok := perm.UnitsMode[u.Type]; !ok { + perm.UnitsMode[u.Type] = AccessModeRead + } + } + } + + // remove no permission units + perm.Units = make([]*RepoUnit, 0, len(units)) + for t := range perm.UnitsMode { + for _, u := range units { + if u.Type == t { + perm.Units = append(perm.Units, u) + } + } + } + + return perm, err + } + + // isOfficialReviewer static function based on 5d78792385 + isOfficialReviewer := func(sess *xorm.Session, issueID int64, reviewer *User) (bool, error) { + pr := new(PullRequest) + has, err := sess.ID(issueID).Get(pr) + if err != nil { + return false, err + } else if !has { + return false, fmt.Errorf("PullRequest for issueID %d not exist", issueID) + } + + baseRepo := new(Repository) + has, err = sess.ID(pr.BaseRepoID).Get(baseRepo) + if err != nil { + return false, err + } else if !has { + return false, fmt.Errorf("baseRepo with id %d not exist", pr.BaseRepoID) + } + protectedBranch := new(ProtectedBranch) + has, err = sess.Where("repo_id=? AND branch_name=?", baseRepo.ID, pr.BaseBranch).Get(protectedBranch) + if err != nil { + return false, err + } + if !has { + return false, nil + } + + if !protectedBranch.EnableApprovalsWhitelist { + perm, err := getUserRepoPermission(sess, baseRepo, reviewer) + if err != nil { + return false, err + } + if perm.UnitsMode == nil { + for _, u := range perm.Units { + if u.Type == UnitTypeCode { + return AccessModeWrite <= perm.AccessMode, nil + } + } + return false, nil + } + return AccessModeWrite <= perm.UnitsMode[UnitTypeCode], nil + } + for _, id := range protectedBranch.ApprovalsWhitelistUserIDs { + if id == reviewer.ID { + return true, nil + } + } + + // isUserInTeams + return sess.Where("uid=?", reviewer.ID).In("team_id", protectedBranch.ApprovalsWhitelistTeamIDs).Exist(new(TeamUser)) + } + + if _, err := x.Exec("UPDATE `protected_branch` SET `enable_whitelist` = ? WHERE enable_whitelist IS NULL", false); err != nil { + return err + } + if _, err := x.Exec("UPDATE `protected_branch` SET `can_push` = `enable_whitelist`"); err != nil { + return err + } + if _, err := x.Exec("UPDATE `protected_branch` SET `enable_approvals_whitelist` = ? WHERE `required_approvals` > ?", true, 0); err != nil { + return err + } + + var pageSize int64 = 20 + qresult, err := x.QueryInterface("SELECT max(id) as max_id FROM issue") + if err != nil { + return err + } + var totalIssues int64 + totalIssues, ok := qresult[0]["max_id"].(int64) + if !ok { + // If there are no issues at all we ignore it + return nil + } + totalPages := totalIssues / pageSize + + executeBody := func(page, pageSize int64) error { + // Find latest review of each user in each pull request, and set official field if appropriate + reviews := []*Review{} + + if err := x.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id > ? AND issue_id <= ? AND type in (?, ?) GROUP BY issue_id, reviewer_id)", + page*pageSize, (page+1)*pageSize, ReviewTypeApprove, ReviewTypeReject). + Find(&reviews); err != nil { + return err + } + + if len(reviews) == 0 { + return nil + } + + sess := x.NewSession() + defer sess.Close() + + if err := sess.Begin(); err != nil { + return err + } + + var updated int + for _, review := range reviews { + reviewer := new(User) + has, err := sess.ID(review.ReviewerID).Get(reviewer) + if err != nil || !has { + // Error might occur if user doesn't exist, ignore it. + continue + } + + official, err := isOfficialReviewer(sess, review.IssueID, reviewer) + if err != nil { + // Branch might not be proteced or other error, ignore it. + continue + } + review.Official = official + updated++ + if _, err := sess.ID(review.ID).Cols("official").Update(review); err != nil { + return err + } + } + + if updated > 0 { + return sess.Commit() + } + return nil + } + + var page int64 + for page = 0; page <= totalPages; page++ { + if err := executeBody(page, pageSize); err != nil { + return err + } + } + + return nil +} diff --git a/models/migrations/v1_11/v112.go b/models/migrations/v1_11/v112.go new file mode 100644 index 00000000..08576631 --- /dev/null +++ b/models/migrations/v1_11/v112.go @@ -0,0 +1,47 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_11 //nolint + +import ( + "fmt" + "path/filepath" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" + "xorm.io/xorm" +) + +func RemoveAttachmentMissedRepo(x *xorm.Engine) error { + type Attachment struct { + UUID string `xorm:"uuid"` + } + var start int + attachments := make([]*Attachment, 0, 50) + for { + err := x.Select("uuid").Where(builder.NotIn("release_id", builder.Select("id").From("`release`"))). + And("release_id > 0"). + OrderBy("id").Limit(50, start).Find(&attachments) + if err != nil { + return err + } + + for i := 0; i < len(attachments); i++ { + uuid := attachments[i].UUID + if err = util.RemoveAll(filepath.Join(setting.Attachment.Storage.Path, uuid[0:1], uuid[1:2], uuid)); err != nil { + fmt.Printf("Error: %v", err) //nolint:forbidigo + } + } + + if len(attachments) < 50 { + break + } + start += 50 + attachments = attachments[:0] + } + + _, err := x.Exec("DELETE FROM attachment WHERE release_id > 0 AND release_id NOT IN (SELECT id FROM `release`)") + return err +} diff --git a/models/migrations/v1_11/v113.go b/models/migrations/v1_11/v113.go new file mode 100644 index 00000000..dea344a4 --- /dev/null +++ b/models/migrations/v1_11/v113.go @@ -0,0 +1,22 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_11 //nolint + +import ( + "fmt" + + "xorm.io/xorm" +) + +func FeatureChangeTargetBranch(x *xorm.Engine) error { + type Comment struct { + OldRef string + NewRef string + } + + if err := x.Sync(new(Comment)); err != nil { + return fmt.Errorf("Sync: %w", err) + } + return nil +} diff --git a/models/migrations/v1_11/v114.go b/models/migrations/v1_11/v114.go new file mode 100644 index 00000000..95adcee9 --- /dev/null +++ b/models/migrations/v1_11/v114.go @@ -0,0 +1,50 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_11 //nolint + +import ( + "net/url" + + "xorm.io/xorm" +) + +func SanitizeOriginalURL(x *xorm.Engine) error { + type Repository struct { + ID int64 + OriginalURL string `xorm:"VARCHAR(2048)"` + } + + var last int + const batchSize = 50 + for { + results := make([]Repository, 0, batchSize) + err := x.Where("original_url <> '' AND original_url IS NOT NULL"). + And("original_service_type = 0 OR original_service_type IS NULL"). + OrderBy("id"). + Limit(batchSize, last). + Find(&results) + if err != nil { + return err + } + if len(results) == 0 { + break + } + last += len(results) + + for _, res := range results { + u, err := url.Parse(res.OriginalURL) + if err != nil { + // it is ok to continue here, we only care about fixing URLs that we can read + continue + } + u.User = nil + originalURL := u.String() + _, err = x.Exec("UPDATE repository SET original_url = ? WHERE id = ?", originalURL, res.ID) + if err != nil { + return err + } + } + } + return nil +} diff --git a/models/migrations/v1_11/v115.go b/models/migrations/v1_11/v115.go new file mode 100644 index 00000000..8c631cfd --- /dev/null +++ b/models/migrations/v1_11/v115.go @@ -0,0 +1,159 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_11 //nolint + +import ( + "crypto/md5" + "fmt" + "io" + "math" + "os" + "path/filepath" + "time" + + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + + "xorm.io/xorm" +) + +func RenameExistingUserAvatarName(x *xorm.Engine) error { + sess := x.NewSession() + defer sess.Close() + + type User struct { + ID int64 `xorm:"pk autoincr"` + LowerName string `xorm:"UNIQUE NOT NULL"` + Avatar string + } + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + count, err := x.Count(new(User)) + if err != nil { + return err + } + log.Info("%d User Avatar(s) to migrate ...", count) + + deleteList := make(container.Set[string]) + start := 0 + migrated := 0 + for { + if err := sess.Begin(); err != nil { + return fmt.Errorf("session.Begin: %w", err) + } + users := make([]*User, 0, 50) + if err := sess.Table("user").Asc("id").Limit(50, start).Find(&users); err != nil { + return fmt.Errorf("select users from id [%d]: %w", start, err) + } + if len(users) == 0 { + _ = sess.Rollback() + break + } + + log.Info("select users [%d - %d]", start, start+len(users)) + start += 50 + + for _, user := range users { + oldAvatar := user.Avatar + + if stat, err := os.Stat(filepath.Join(setting.Avatar.Storage.Path, oldAvatar)); err != nil || !stat.Mode().IsRegular() { + if err == nil { + err = fmt.Errorf("Error: \"%s\" is not a regular file", oldAvatar) + } + log.Warn("[user: %s] os.Stat: %v", user.LowerName, err) + // avatar doesn't exist in the storage + // no need to move avatar and update database + // we can just skip this + continue + } + + newAvatar, err := copyOldAvatarToNewLocation(user.ID, oldAvatar) + if err != nil { + _ = sess.Rollback() + return fmt.Errorf("[user: %s] %w", user.LowerName, err) + } else if newAvatar == oldAvatar { + continue + } + + user.Avatar = newAvatar + if _, err := sess.ID(user.ID).Cols("avatar").Update(user); err != nil { + _ = sess.Rollback() + return fmt.Errorf("[user: %s] user table update: %w", user.LowerName, err) + } + + deleteList.Add(filepath.Join(setting.Avatar.Storage.Path, oldAvatar)) + migrated++ + select { + case <-ticker.C: + log.Info( + "%d/%d (%2.0f%%) User Avatar(s) migrated (%d old avatars to be deleted) in %d batches. %d Remaining ...", + migrated, + count, + float64(migrated)/float64(count)*100, + len(deleteList), + int(math.Ceil(float64(migrated)/float64(50))), + count-int64(migrated)) + default: + } + } + if err := sess.Commit(); err != nil { + _ = sess.Rollback() + return fmt.Errorf("commit session: %w", err) + } + } + + deleteCount := len(deleteList) + log.Info("Deleting %d old avatars ...", deleteCount) + i := 0 + for file := range deleteList { + if err := util.Remove(file); err != nil { + log.Warn("util.Remove: %v", err) + } + i++ + select { + case <-ticker.C: + log.Info( + "%d/%d (%2.0f%%) Old User Avatar(s) deleted. %d Remaining ...", + i, + deleteCount, + float64(i)/float64(deleteCount)*100, + deleteCount-i) + default: + } + } + + log.Info("Completed migrating %d User Avatar(s) and deleting %d Old Avatars", count, deleteCount) + + return nil +} + +// copyOldAvatarToNewLocation copies oldAvatar to newAvatarLocation +// and returns newAvatar location +func copyOldAvatarToNewLocation(userID int64, oldAvatar string) (string, error) { + fr, err := os.Open(filepath.Join(setting.Avatar.Storage.Path, oldAvatar)) + if err != nil { + return "", fmt.Errorf("os.Open: %w", err) + } + defer fr.Close() + + data, err := io.ReadAll(fr) + if err != nil { + return "", fmt.Errorf("io.ReadAll: %w", err) + } + + newAvatar := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", userID, md5.Sum(data))))) + if newAvatar == oldAvatar { + return newAvatar, nil + } + + if err := os.WriteFile(filepath.Join(setting.Avatar.Storage.Path, newAvatar), data, 0o666); err != nil { + return "", fmt.Errorf("os.WriteFile: %w", err) + } + + return newAvatar, nil +} diff --git a/models/migrations/v1_11/v116.go b/models/migrations/v1_11/v116.go new file mode 100644 index 00000000..85aa76c1 --- /dev/null +++ b/models/migrations/v1_11/v116.go @@ -0,0 +1,32 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_11 //nolint + +import ( + "xorm.io/xorm" +) + +func ExtendTrackedTimes(x *xorm.Engine) error { + type TrackedTime struct { + Time int64 `xorm:"NOT NULL"` + Deleted bool `xorm:"NOT NULL DEFAULT false"` + } + + sess := x.NewSession() + defer sess.Close() + + if err := sess.Begin(); err != nil { + return err + } + + if _, err := sess.Exec("DELETE FROM tracked_time WHERE time IS NULL"); err != nil { + return err + } + + if err := sess.Sync(new(TrackedTime)); err != nil { + return err + } + + return sess.Commit() +} |