Skip to content

Commit 3ace45c

Browse files
GiteaBotlunny
andauthored
Fix doctor deleting orphaned issues attachments (#34142) (#34571)
Backport #34142 by @lunny Fix the bug when deleting orphaned issues attachments. The attachments maybe stored on other storages service rather than disk. Co-authored-by: Lunny Xiao <[email protected]>
1 parent 5d6c5ce commit 3ace45c

File tree

5 files changed

+93
-154
lines changed

5 files changed

+93
-154
lines changed

models/issues/issue_update.go

Lines changed: 8 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ import (
1212
"code.gitea.io/gitea/models/db"
1313
"code.gitea.io/gitea/models/organization"
1414
access_model "code.gitea.io/gitea/models/perm/access"
15-
project_model "code.gitea.io/gitea/models/project"
1615
repo_model "code.gitea.io/gitea/models/repo"
17-
system_model "code.gitea.io/gitea/models/system"
1816
"code.gitea.io/gitea/models/unit"
1917
user_model "code.gitea.io/gitea/models/user"
2018
"code.gitea.io/gitea/modules/git"
@@ -715,138 +713,13 @@ func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.Git
715713
return err
716714
}
717715

718-
// DeleteIssuesByRepoID deletes issues by repositories id
719-
func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
720-
// MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289
721-
// so here it uses "DELETE ... WHERE IN" with pre-queried IDs.
722-
sess := db.GetEngine(ctx)
723-
724-
for {
725-
issueIDs := make([]int64, 0, db.DefaultMaxInSize)
726-
727-
err := sess.Table(&Issue{}).Where("repo_id = ?", repoID).OrderBy("id").Limit(db.DefaultMaxInSize).Cols("id").Find(&issueIDs)
728-
if err != nil {
729-
return nil, err
730-
}
731-
732-
if len(issueIDs) == 0 {
733-
break
734-
}
735-
736-
// Delete content histories
737-
_, err = sess.In("issue_id", issueIDs).Delete(&ContentHistory{})
738-
if err != nil {
739-
return nil, err
740-
}
741-
742-
// Delete comments and attachments
743-
_, err = sess.In("issue_id", issueIDs).Delete(&Comment{})
744-
if err != nil {
745-
return nil, err
746-
}
747-
748-
// Dependencies for issues in this repository
749-
_, err = sess.In("issue_id", issueIDs).Delete(&IssueDependency{})
750-
if err != nil {
751-
return nil, err
752-
}
753-
754-
// Delete dependencies for issues in other repositories
755-
_, err = sess.In("dependency_id", issueIDs).Delete(&IssueDependency{})
756-
if err != nil {
757-
return nil, err
758-
}
759-
760-
_, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{})
761-
if err != nil {
762-
return nil, err
763-
}
764-
765-
_, err = sess.In("issue_id", issueIDs).Delete(&Reaction{})
766-
if err != nil {
767-
return nil, err
768-
}
769-
770-
_, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{})
771-
if err != nil {
772-
return nil, err
773-
}
774-
775-
_, err = sess.In("issue_id", issueIDs).Delete(&Stopwatch{})
776-
if err != nil {
777-
return nil, err
778-
}
779-
780-
_, err = sess.In("issue_id", issueIDs).Delete(&TrackedTime{})
781-
if err != nil {
782-
return nil, err
783-
}
784-
785-
_, err = sess.In("issue_id", issueIDs).Delete(&project_model.ProjectIssue{})
786-
if err != nil {
787-
return nil, err
788-
}
789-
790-
_, err = sess.In("dependent_issue_id", issueIDs).Delete(&Comment{})
791-
if err != nil {
792-
return nil, err
793-
}
794-
795-
var attachments []*repo_model.Attachment
796-
err = sess.In("issue_id", issueIDs).Find(&attachments)
797-
if err != nil {
798-
return nil, err
799-
}
800-
801-
for j := range attachments {
802-
attachmentPaths = append(attachmentPaths, attachments[j].RelativePath())
803-
}
804-
805-
_, err = sess.In("issue_id", issueIDs).Delete(&repo_model.Attachment{})
806-
if err != nil {
807-
return nil, err
808-
}
809-
810-
_, err = sess.In("id", issueIDs).Delete(&Issue{})
811-
if err != nil {
812-
return nil, err
813-
}
814-
}
815-
816-
return attachmentPaths, err
817-
}
818-
819-
// DeleteOrphanedIssues delete issues without a repo
820-
func DeleteOrphanedIssues(ctx context.Context) error {
821-
var attachmentPaths []string
822-
err := db.WithTx(ctx, func(ctx context.Context) error {
823-
var ids []int64
824-
825-
if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
826-
Join("LEFT", "repository", "issue.repo_id=repository.id").
827-
Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id").
828-
Find(&ids); err != nil {
829-
return err
830-
}
831-
832-
for i := range ids {
833-
paths, err := DeleteIssuesByRepoID(ctx, ids[i])
834-
if err != nil {
835-
return err
836-
}
837-
attachmentPaths = append(attachmentPaths, paths...)
838-
}
839-
840-
return nil
841-
})
842-
if err != nil {
843-
return err
844-
}
845-
846-
// Remove issue attachment files.
847-
for i := range attachmentPaths {
848-
// FIXME: it's not right, because the attachment might not be on local filesystem
849-
system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i])
716+
func GetOrphanedIssueRepoIDs(ctx context.Context) ([]int64, error) {
717+
var repoIDs []int64
718+
if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
719+
Join("LEFT", "repository", "issue.repo_id=repository.id").
720+
Where(builder.IsNull{"repository.id"}).
721+
Find(&repoIDs); err != nil {
722+
return nil, err
850723
}
851-
return nil
724+
return repoIDs, nil
852725
}

services/doctor/dbconsistency.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
secret_model "code.gitea.io/gitea/models/secret"
1616
"code.gitea.io/gitea/modules/log"
1717
"code.gitea.io/gitea/modules/setting"
18+
issue_service "code.gitea.io/gitea/services/issue"
1819
)
1920

2021
type consistencyCheck struct {
@@ -93,7 +94,7 @@ func prepareDBConsistencyChecks() []consistencyCheck {
9394
// find issues without existing repository
9495
Name: "Orphaned Issues without existing repository",
9596
Counter: issues_model.CountOrphanedIssues,
96-
Fixer: asFixer(issues_model.DeleteOrphanedIssues),
97+
Fixer: asFixer(issue_service.DeleteOrphanedIssues),
9798
},
9899
// find releases without existing repository
99100
genericOrphanCheck("Orphaned Releases without existing repository",

services/issue/issue.go

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,13 @@ func DeleteIssue(ctx context.Context, doer *user_model.User, gitRepo *git.Reposi
190190
}
191191

192192
// delete entries in database
193-
if err := deleteIssue(ctx, issue); err != nil {
193+
attachmentPaths, err := deleteIssue(ctx, issue)
194+
if err != nil {
194195
return err
195196
}
197+
for _, attachmentPath := range attachmentPaths {
198+
system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", attachmentPath)
199+
}
196200

197201
// delete pull request related git data
198202
if issue.IsPull && gitRepo != nil {
@@ -256,45 +260,45 @@ func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[i
256260
}
257261

258262
// deleteIssue deletes the issue
259-
func deleteIssue(ctx context.Context, issue *issues_model.Issue) error {
263+
func deleteIssue(ctx context.Context, issue *issues_model.Issue) ([]string, error) {
260264
ctx, committer, err := db.TxContext(ctx)
261265
if err != nil {
262-
return err
266+
return nil, err
263267
}
264268
defer committer.Close()
265269

266-
e := db.GetEngine(ctx)
267-
if _, err := e.ID(issue.ID).NoAutoCondition().Delete(issue); err != nil {
268-
return err
270+
if _, err := db.GetEngine(ctx).ID(issue.ID).NoAutoCondition().Delete(issue); err != nil {
271+
return nil, err
269272
}
270273

271274
// update the total issue numbers
272275
if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false); err != nil {
273-
return err
276+
return nil, err
274277
}
275278
// if the issue is closed, update the closed issue numbers
276279
if issue.IsClosed {
277280
if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil {
278-
return err
281+
return nil, err
279282
}
280283
}
281284

282285
if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
283-
return fmt.Errorf("error updating counters for milestone id %d: %w",
286+
return nil, fmt.Errorf("error updating counters for milestone id %d: %w",
284287
issue.MilestoneID, err)
285288
}
286289

287290
if err := activities_model.DeleteIssueActions(ctx, issue.RepoID, issue.ID, issue.Index); err != nil {
288-
return err
291+
return nil, err
289292
}
290293

291294
// find attachments related to this issue and remove them
292-
if err := issue.LoadAttributes(ctx); err != nil {
293-
return err
295+
if err := issue.LoadAttachments(ctx); err != nil {
296+
return nil, err
294297
}
295298

299+
var attachmentPaths []string
296300
for i := range issue.Attachments {
297-
system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", issue.Attachments[i].RelativePath())
301+
attachmentPaths = append(attachmentPaths, issue.Attachments[i].RelativePath())
298302
}
299303

300304
// delete all database data still assigned to this issue
@@ -318,8 +322,68 @@ func deleteIssue(ctx context.Context, issue *issues_model.Issue) error {
318322
&issues_model.Comment{DependentIssueID: issue.ID},
319323
&issues_model.IssuePin{IssueID: issue.ID},
320324
); err != nil {
325+
return nil, err
326+
}
327+
328+
if err := committer.Commit(); err != nil {
329+
return nil, err
330+
}
331+
return attachmentPaths, nil
332+
}
333+
334+
// DeleteOrphanedIssues delete issues without a repo
335+
func DeleteOrphanedIssues(ctx context.Context) error {
336+
var attachmentPaths []string
337+
err := db.WithTx(ctx, func(ctx context.Context) error {
338+
repoIDs, err := issues_model.GetOrphanedIssueRepoIDs(ctx)
339+
if err != nil {
340+
return err
341+
}
342+
for i := range repoIDs {
343+
paths, err := DeleteIssuesByRepoID(ctx, repoIDs[i])
344+
if err != nil {
345+
return err
346+
}
347+
attachmentPaths = append(attachmentPaths, paths...)
348+
}
349+
return nil
350+
})
351+
if err != nil {
321352
return err
322353
}
323354

324-
return committer.Commit()
355+
// Remove issue attachment files.
356+
for i := range attachmentPaths {
357+
system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", attachmentPaths[i])
358+
}
359+
return nil
360+
}
361+
362+
// DeleteIssuesByRepoID deletes issues by repositories id
363+
func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
364+
for {
365+
issues := make([]*issues_model.Issue, 0, db.DefaultMaxInSize)
366+
if err := db.GetEngine(ctx).
367+
Where("repo_id = ?", repoID).
368+
OrderBy("id").
369+
Limit(db.DefaultMaxInSize).
370+
Find(&issues); err != nil {
371+
return nil, err
372+
}
373+
374+
if len(issues) == 0 {
375+
break
376+
}
377+
378+
for _, issue := range issues {
379+
issueAttachPaths, err := deleteIssue(ctx, issue)
380+
if err != nil {
381+
return nil, fmt.Errorf("deleteIssue [issue_id: %d]: %w", issue.ID, err)
382+
}
383+
384+
attachmentPaths = append(attachmentPaths, issueAttachPaths...)
385+
}
386+
}
387+
388+
return attachmentPaths, err
325389
}

services/issue/issue_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func TestIssue_DeleteIssue(t *testing.T) {
4444
ID: issueIDs[2],
4545
}
4646

47-
err = deleteIssue(db.DefaultContext, issue)
47+
_, err = deleteIssue(db.DefaultContext, issue)
4848
assert.NoError(t, err)
4949
issueIDs, err = issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1)
5050
assert.NoError(t, err)
@@ -55,7 +55,7 @@ func TestIssue_DeleteIssue(t *testing.T) {
5555
assert.NoError(t, err)
5656
issue, err = issues_model.GetIssueByID(db.DefaultContext, 4)
5757
assert.NoError(t, err)
58-
err = deleteIssue(db.DefaultContext, issue)
58+
_, err = deleteIssue(db.DefaultContext, issue)
5959
assert.NoError(t, err)
6060
assert.Len(t, attachments, 2)
6161
for i := range attachments {
@@ -78,7 +78,7 @@ func TestIssue_DeleteIssue(t *testing.T) {
7878
assert.NoError(t, err)
7979
assert.False(t, left)
8080

81-
err = deleteIssue(db.DefaultContext, issue2)
81+
_, err = deleteIssue(db.DefaultContext, issue2)
8282
assert.NoError(t, err)
8383
left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1)
8484
assert.NoError(t, err)

services/repository/delete.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"code.gitea.io/gitea/modules/storage"
3030
actions_service "code.gitea.io/gitea/services/actions"
3131
asymkey_service "code.gitea.io/gitea/services/asymkey"
32+
issue_service "code.gitea.io/gitea/services/issue"
3233

3334
"xorm.io/builder"
3435
)
@@ -193,7 +194,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
193194

194195
// Delete Issues and related objects
195196
var attachmentPaths []string
196-
if attachmentPaths, err = issues_model.DeleteIssuesByRepoID(ctx, repoID); err != nil {
197+
if attachmentPaths, err = issue_service.DeleteIssuesByRepoID(ctx, repoID); err != nil {
197198
return err
198199
}
199200

0 commit comments

Comments
 (0)