Skip to content

Commit 97556a8

Browse files
committed
Improve attachments deletions
1 parent f868da0 commit 97556a8

File tree

20 files changed

+355
-222
lines changed

20 files changed

+355
-222
lines changed

models/db/file_status.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package db
5+
6+
// FileStatus represents the status of a file in the disk.
7+
type FileStatus int
8+
9+
const (
10+
FileStatusNormal FileStatus = iota // FileStatusNormal indicates the file is normal and exists on disk.
11+
FileStatusToBeDeleted // FileStatusToBeDeleted indicates the file is marked for deletion but still exists on disk.
12+
)

models/issues/comment.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,9 @@ func UpdateCommentAttachments(ctx context.Context, c *Comment, uuids []string) e
599599
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
600600
}
601601
for i := range attachments {
602+
if attachments[i].IssueID != 0 || attachments[i].CommentID != 0 {
603+
return util.NewPermissionDeniedErrorf("update comment attachments permission denied")
604+
}
602605
attachments[i].IssueID = c.IssueID
603606
attachments[i].CommentID = c.ID
604607
if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {

models/issues/issue_update.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string)
305305
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
306306
}
307307
for i := range attachments {
308+
if attachments[i].IssueID != 0 {
309+
return util.NewPermissionDeniedErrorf("update issue attachments permission denied")
310+
}
308311
attachments[i].IssueID = issueID
309312
if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
310313
return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)

models/migrations/v1_25/v321.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_24
5+
6+
import (
7+
"code.gitea.io/gitea/models/db"
8+
"code.gitea.io/gitea/modules/timeutil"
9+
10+
"xorm.io/xorm"
11+
)
12+
13+
func AddFileStatusToAttachment(x *xorm.Engine) error {
14+
type Attachment struct {
15+
ID int64 `xorm:"pk autoincr"`
16+
UUID string `xorm:"uuid UNIQUE"`
17+
RepoID int64 `xorm:"INDEX"` // this should not be zero
18+
IssueID int64 `xorm:"INDEX"` // maybe zero when creating
19+
ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating
20+
UploaderID int64 `xorm:"INDEX DEFAULT 0"` // Notice: will be zero before this column added
21+
CommentID int64 `xorm:"INDEX"`
22+
Name string
23+
DownloadCount int64 `xorm:"DEFAULT 0"`
24+
Status db.FileStatus `xorm:"INDEX DEFAULT 0"`
25+
DeleteFailedCount int `xorm:"DEFAULT 0"` // Number of times the deletion failed, used to prevent infinite loop
26+
LastDeleteFailedTime timeutil.TimeStamp // Last time the deletion failed, used to prevent infinite loop
27+
Size int64 `xorm:"DEFAULT 0"`
28+
CreatedUnix timeutil.TimeStamp `xorm:"created"`
29+
CustomDownloadURL string `xorm:"-"`
30+
}
31+
32+
if err := x.Sync(new(Attachment)); err != nil {
33+
return err
34+
}
35+
36+
if _, err := x.Exec("UPDATE `attachment` SET status = ? WHERE status IS NULL", db.FileStatusNormal); err != nil {
37+
return err
38+
}
39+
40+
return nil
41+
}

models/repo/attachment.go

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,22 @@ import (
1818

1919
// Attachment represent a attachment of issue/comment/release.
2020
type Attachment struct {
21-
ID int64 `xorm:"pk autoincr"`
22-
UUID string `xorm:"uuid UNIQUE"`
23-
RepoID int64 `xorm:"INDEX"` // this should not be zero
24-
IssueID int64 `xorm:"INDEX"` // maybe zero when creating
25-
ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating
26-
UploaderID int64 `xorm:"INDEX DEFAULT 0"` // Notice: will be zero before this column added
27-
CommentID int64 `xorm:"INDEX"`
28-
Name string
29-
DownloadCount int64 `xorm:"DEFAULT 0"`
30-
Size int64 `xorm:"DEFAULT 0"`
31-
CreatedUnix timeutil.TimeStamp `xorm:"created"`
32-
CustomDownloadURL string `xorm:"-"`
21+
ID int64 `xorm:"pk autoincr"`
22+
UUID string `xorm:"uuid UNIQUE"`
23+
RepoID int64 `xorm:"INDEX"` // this should not be zero
24+
IssueID int64 `xorm:"INDEX"` // maybe zero when creating
25+
ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating
26+
UploaderID int64 `xorm:"INDEX DEFAULT 0"` // Notice: will be zero before this column added
27+
CommentID int64 `xorm:"INDEX"`
28+
Name string
29+
DownloadCount int64 `xorm:"DEFAULT 0"`
30+
Status db.FileStatus `xorm:"INDEX DEFAULT 0"`
31+
DeleteFailedCount int `xorm:"DEFAULT 0"` // Number of times the deletion failed, used to prevent infinite loop
32+
LastDeleteFailedReason string `xorm:"TEXT"` // Last reason the deletion failed, used to prevent infinite loop
33+
LastDeleteFailedTime timeutil.TimeStamp // Last time the deletion failed, used to prevent infinite loop
34+
Size int64 `xorm:"DEFAULT 0"`
35+
CreatedUnix timeutil.TimeStamp `xorm:"created"`
36+
CustomDownloadURL string `xorm:"-"`
3337
}
3438

3539
func init() {
@@ -88,7 +92,9 @@ func (err ErrAttachmentNotExist) Unwrap() error {
8892
// GetAttachmentByID returns attachment by given id
8993
func GetAttachmentByID(ctx context.Context, id int64) (*Attachment, error) {
9094
attach := &Attachment{}
91-
if has, err := db.GetEngine(ctx).ID(id).Get(attach); err != nil {
95+
if has, err := db.GetEngine(ctx).ID(id).
96+
And("status = ?", db.FileStatusNormal).
97+
Get(attach); err != nil {
9298
return nil, err
9399
} else if !has {
94100
return nil, ErrAttachmentNotExist{ID: id, UUID: ""}
@@ -99,7 +105,9 @@ func GetAttachmentByID(ctx context.Context, id int64) (*Attachment, error) {
99105
// GetAttachmentByUUID returns attachment by given UUID.
100106
func GetAttachmentByUUID(ctx context.Context, uuid string) (*Attachment, error) {
101107
attach := &Attachment{}
102-
has, err := db.GetEngine(ctx).Where("uuid=?", uuid).Get(attach)
108+
has, err := db.GetEngine(ctx).Where("uuid=?", uuid).
109+
And("status = ?", db.FileStatusNormal).
110+
Get(attach)
103111
if err != nil {
104112
return nil, err
105113
} else if !has {
@@ -116,18 +124,24 @@ func GetAttachmentsByUUIDs(ctx context.Context, uuids []string) ([]*Attachment,
116124

117125
// Silently drop invalid uuids.
118126
attachments := make([]*Attachment, 0, len(uuids))
119-
return attachments, db.GetEngine(ctx).In("uuid", uuids).Find(&attachments)
127+
return attachments, db.GetEngine(ctx).In("uuid", uuids).
128+
And("status = ?", db.FileStatusNormal).
129+
Find(&attachments)
120130
}
121131

122132
// ExistAttachmentsByUUID returns true if attachment exists with the given UUID
123133
func ExistAttachmentsByUUID(ctx context.Context, uuid string) (bool, error) {
124-
return db.GetEngine(ctx).Where("`uuid`=?", uuid).Exist(new(Attachment))
134+
return db.GetEngine(ctx).Where("`uuid`=?", uuid).
135+
And("status = ?", db.FileStatusNormal).
136+
Exist(new(Attachment))
125137
}
126138

127139
// GetAttachmentsByIssueID returns all attachments of an issue.
128140
func GetAttachmentsByIssueID(ctx context.Context, issueID int64) ([]*Attachment, error) {
129141
attachments := make([]*Attachment, 0, 10)
130-
return attachments, db.GetEngine(ctx).Where("issue_id = ? AND comment_id = 0", issueID).Find(&attachments)
142+
return attachments, db.GetEngine(ctx).Where("issue_id = ? AND comment_id = 0", issueID).
143+
And("status = ?", db.FileStatusNormal).
144+
Find(&attachments)
131145
}
132146

133147
// GetAttachmentsByIssueIDImagesLatest returns the latest image attachments of an issue.
@@ -142,19 +156,23 @@ func GetAttachmentsByIssueIDImagesLatest(ctx context.Context, issueID int64) ([]
142156
OR name like '%.jxl'
143157
OR name like '%.png'
144158
OR name like '%.svg'
145-
OR name like '%.webp')`, issueID).Desc("comment_id").Limit(5).Find(&attachments)
159+
OR name like '%.webp')`, issueID).
160+
And("status = ?", db.FileStatusNormal).
161+
Desc("comment_id").Limit(5).Find(&attachments)
146162
}
147163

148164
// GetAttachmentsByCommentID returns all attachments if comment by given ID.
149165
func GetAttachmentsByCommentID(ctx context.Context, commentID int64) ([]*Attachment, error) {
150166
attachments := make([]*Attachment, 0, 10)
151-
return attachments, db.GetEngine(ctx).Where("comment_id=?", commentID).Find(&attachments)
167+
return attachments, db.GetEngine(ctx).Where("comment_id=?", commentID).
168+
And("status = ?", db.FileStatusNormal).
169+
Find(&attachments)
152170
}
153171

154172
// GetAttachmentByReleaseIDFileName returns attachment by given releaseId and fileName.
155173
func GetAttachmentByReleaseIDFileName(ctx context.Context, releaseID int64, fileName string) (*Attachment, error) {
156174
attach := &Attachment{ReleaseID: releaseID, Name: fileName}
157-
has, err := db.GetEngine(ctx).Get(attach)
175+
has, err := db.GetEngine(ctx).Where("status = ?", db.FileStatusNormal).Get(attach)
158176
if err != nil {
159177
return nil, err
160178
} else if !has {
@@ -185,7 +203,8 @@ func UpdateAttachment(ctx context.Context, atta *Attachment) error {
185203
return err
186204
}
187205

188-
func DeleteAttachments(ctx context.Context, attachments []*Attachment) (int64, error) {
206+
// MarkAttachmentsDeleted marks the given attachments as deleted
207+
func MarkAttachmentsDeleted(ctx context.Context, attachments []*Attachment) (int64, error) {
189208
if len(attachments) == 0 {
190209
return 0, nil
191210
}
@@ -195,15 +214,41 @@ func DeleteAttachments(ctx context.Context, attachments []*Attachment) (int64, e
195214
ids = append(ids, a.ID)
196215
}
197216

198-
return db.GetEngine(ctx).In("id", ids).NoAutoCondition().Delete(attachments[0])
217+
return db.GetEngine(ctx).Table("attachment").In("id", ids).Update(map[string]any{
218+
"status": db.FileStatusToBeDeleted,
219+
})
199220
}
200221

201-
// DeleteAttachmentsByRelease deletes all attachments associated with the given release.
202-
func DeleteAttachmentsByRelease(ctx context.Context, releaseID int64) error {
203-
_, err := db.GetEngine(ctx).Where("release_id = ?", releaseID).Delete(&Attachment{})
222+
// MarkAttachmentsDeletedByRelease marks all attachments associated with the given release as deleted.
223+
func MarkAttachmentsDeletedByRelease(ctx context.Context, releaseID int64) error {
224+
_, err := db.GetEngine(ctx).Table("attachment").Where("release_id = ?", releaseID).Update(map[string]any{
225+
"status": db.FileStatusToBeDeleted,
226+
})
204227
return err
205228
}
206229

230+
// DeleteAttachmentByID deletes the attachment which has been marked as deleted by given id
231+
func DeleteAttachmentByID(ctx context.Context, id int64) error {
232+
cnt, err := db.GetEngine(ctx).ID(id).Where("status = ?", db.FileStatusToBeDeleted).Delete(new(Attachment))
233+
if err != nil {
234+
return fmt.Errorf("delete attachment by id: %w", err)
235+
}
236+
if cnt != 1 {
237+
return fmt.Errorf("the attachment with id %d was not found or is not marked for deletion", id)
238+
}
239+
return nil
240+
}
241+
242+
func UpdateAttachmentFailure(ctx context.Context, attachment *Attachment, err error) error {
243+
attachment.DeleteFailedCount++
244+
_, updateErr := db.GetEngine(ctx).Table("attachment").ID(attachment.ID).Update(map[string]any{
245+
"delete_failed_count": attachment.DeleteFailedCount,
246+
"last_delete_failed_reason": err.Error(),
247+
"last_delete_failed_time": timeutil.TimeStampNow(),
248+
})
249+
return updateErr
250+
}
251+
207252
// CountOrphanedAttachments returns the number of bad attachments
208253
func CountOrphanedAttachments(ctx context.Context) (int64, error) {
209254
return db.GetEngine(ctx).Where("(issue_id > 0 and issue_id not in (select id from issue)) or (release_id > 0 and release_id not in (select id from `release`))").

modules/util/post_tx_action.go

Lines changed: 0 additions & 18 deletions
This file was deleted.

routers/api/v1/repo/issue_attachment.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package repo
66
import (
77
"net/http"
88

9+
"code.gitea.io/gitea/models/db"
910
issues_model "code.gitea.io/gitea/models/issues"
1011
repo_model "code.gitea.io/gitea/models/repo"
1112
"code.gitea.io/gitea/modules/log"
@@ -360,6 +361,10 @@ func getIssueAttachmentSafeRead(ctx *context.APIContext, issue *issues_model.Iss
360361
if !attachmentBelongsToRepoOrIssue(ctx, attachment, issue) {
361362
return nil
362363
}
364+
if attachment.Status != db.FileStatusNormal {
365+
ctx.APIErrorNotFound()
366+
return nil
367+
}
363368
return attachment
364369
}
365370

routers/api/v1/repo/issue_comment_attachment.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"net/http"
99

10+
"code.gitea.io/gitea/models/db"
1011
issues_model "code.gitea.io/gitea/models/issues"
1112
repo_model "code.gitea.io/gitea/models/repo"
1213
user_model "code.gitea.io/gitea/models/user"
@@ -391,6 +392,10 @@ func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_
391392
if !attachmentBelongsToRepoOrComment(ctx, attachment, comment) {
392393
return nil
393394
}
395+
if attachment.Status != db.FileStatusNormal {
396+
ctx.APIErrorNotFound()
397+
return nil
398+
}
394399
return attachment
395400
}
396401

routers/api/v1/repo/release_attachment.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,6 @@ func DeleteReleaseAttachment(ctx *context.APIContext) {
393393
return
394394
}
395395
// FIXME Should prove the existence of the given repo, but results in unnecessary database requests
396-
397396
if err := attachment_service.DeleteAttachment(ctx, attach); err != nil {
398397
ctx.APIErrorInternal(err)
399398
return

routers/init.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
web_routers "code.gitea.io/gitea/routers/web"
3737
actions_service "code.gitea.io/gitea/services/actions"
3838
asymkey_service "code.gitea.io/gitea/services/asymkey"
39+
attachment_service "code.gitea.io/gitea/services/attachment"
3940
"code.gitea.io/gitea/services/auth"
4041
"code.gitea.io/gitea/services/auth/source/oauth2"
4142
"code.gitea.io/gitea/services/automerge"
@@ -174,6 +175,7 @@ func InitWebInstalled(ctx context.Context) {
174175
mustInitCtx(ctx, actions_service.Init)
175176

176177
mustInit(repo_service.InitLicenseClassifier)
178+
mustInit(attachment_service.Init)
177179

178180
// Finally start up the cron
179181
cron.NewContext(ctx)

0 commit comments

Comments
 (0)