code.gitea.io/gitea@v1.22.3/models/repo/attachment.go (about) 1 // Copyright 2017 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package repo 5 6 import ( 7 "context" 8 "fmt" 9 "net/url" 10 "path" 11 12 "code.gitea.io/gitea/models/db" 13 "code.gitea.io/gitea/modules/setting" 14 "code.gitea.io/gitea/modules/storage" 15 "code.gitea.io/gitea/modules/timeutil" 16 "code.gitea.io/gitea/modules/util" 17 ) 18 19 // Attachment represent a attachment of issue/comment/release. 20 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:"-"` 33 } 34 35 func init() { 36 db.RegisterModel(new(Attachment)) 37 } 38 39 // IncreaseDownloadCount is update download count + 1 40 func (a *Attachment) IncreaseDownloadCount(ctx context.Context) error { 41 // Update download count. 42 if _, err := db.GetEngine(ctx).Exec("UPDATE `attachment` SET download_count=download_count+1 WHERE id=?", a.ID); err != nil { 43 return fmt.Errorf("increase attachment count: %w", err) 44 } 45 46 return nil 47 } 48 49 // AttachmentRelativePath returns the relative path 50 func AttachmentRelativePath(uuid string) string { 51 return path.Join(uuid[0:1], uuid[1:2], uuid) 52 } 53 54 // RelativePath returns the relative path of the attachment 55 func (a *Attachment) RelativePath() string { 56 return AttachmentRelativePath(a.UUID) 57 } 58 59 // DownloadURL returns the download url of the attached file 60 func (a *Attachment) DownloadURL() string { 61 if a.CustomDownloadURL != "" { 62 return a.CustomDownloadURL 63 } 64 65 return setting.AppURL + "attachments/" + url.PathEscape(a.UUID) 66 } 67 68 // ErrAttachmentNotExist represents a "AttachmentNotExist" kind of error. 69 type ErrAttachmentNotExist struct { 70 ID int64 71 UUID string 72 } 73 74 // IsErrAttachmentNotExist checks if an error is a ErrAttachmentNotExist. 75 func IsErrAttachmentNotExist(err error) bool { 76 _, ok := err.(ErrAttachmentNotExist) 77 return ok 78 } 79 80 func (err ErrAttachmentNotExist) Error() string { 81 return fmt.Sprintf("attachment does not exist [id: %d, uuid: %s]", err.ID, err.UUID) 82 } 83 84 func (err ErrAttachmentNotExist) Unwrap() error { 85 return util.ErrNotExist 86 } 87 88 // GetAttachmentByID returns attachment by given id 89 func GetAttachmentByID(ctx context.Context, id int64) (*Attachment, error) { 90 attach := &Attachment{} 91 if has, err := db.GetEngine(ctx).ID(id).Get(attach); err != nil { 92 return nil, err 93 } else if !has { 94 return nil, ErrAttachmentNotExist{ID: id, UUID: ""} 95 } 96 return attach, nil 97 } 98 99 // GetAttachmentByUUID returns attachment by given UUID. 100 func GetAttachmentByUUID(ctx context.Context, uuid string) (*Attachment, error) { 101 attach := &Attachment{} 102 has, err := db.GetEngine(ctx).Where("uuid=?", uuid).Get(attach) 103 if err != nil { 104 return nil, err 105 } else if !has { 106 return nil, ErrAttachmentNotExist{0, uuid} 107 } 108 return attach, nil 109 } 110 111 // GetAttachmentsByUUIDs returns attachment by given UUID list. 112 func GetAttachmentsByUUIDs(ctx context.Context, uuids []string) ([]*Attachment, error) { 113 if len(uuids) == 0 { 114 return []*Attachment{}, nil 115 } 116 117 // Silently drop invalid uuids. 118 attachments := make([]*Attachment, 0, len(uuids)) 119 return attachments, db.GetEngine(ctx).In("uuid", uuids).Find(&attachments) 120 } 121 122 // ExistAttachmentsByUUID returns true if attachment exists with the given UUID 123 func ExistAttachmentsByUUID(ctx context.Context, uuid string) (bool, error) { 124 return db.GetEngine(ctx).Where("`uuid`=?", uuid).Exist(new(Attachment)) 125 } 126 127 // GetAttachmentsByIssueID returns all attachments of an issue. 128 func GetAttachmentsByIssueID(ctx context.Context, issueID int64) ([]*Attachment, error) { 129 attachments := make([]*Attachment, 0, 10) 130 return attachments, db.GetEngine(ctx).Where("issue_id = ? AND comment_id = 0", issueID).Find(&attachments) 131 } 132 133 // GetAttachmentsByIssueIDImagesLatest returns the latest image attachments of an issue. 134 func GetAttachmentsByIssueIDImagesLatest(ctx context.Context, issueID int64) ([]*Attachment, error) { 135 attachments := make([]*Attachment, 0, 5) 136 return attachments, db.GetEngine(ctx).Where(`issue_id = ? AND (name like '%.apng' 137 OR name like '%.avif' 138 OR name like '%.bmp' 139 OR name like '%.gif' 140 OR name like '%.jpg' 141 OR name like '%.jpeg' 142 OR name like '%.jxl' 143 OR name like '%.png' 144 OR name like '%.svg' 145 OR name like '%.webp')`, issueID).Desc("comment_id").Limit(5).Find(&attachments) 146 } 147 148 // GetAttachmentsByCommentID returns all attachments if comment by given ID. 149 func GetAttachmentsByCommentID(ctx context.Context, commentID int64) ([]*Attachment, error) { 150 attachments := make([]*Attachment, 0, 10) 151 return attachments, db.GetEngine(ctx).Where("comment_id=?", commentID).Find(&attachments) 152 } 153 154 // GetAttachmentByReleaseIDFileName returns attachment by given releaseId and fileName. 155 func GetAttachmentByReleaseIDFileName(ctx context.Context, releaseID int64, fileName string) (*Attachment, error) { 156 attach := &Attachment{ReleaseID: releaseID, Name: fileName} 157 has, err := db.GetEngine(ctx).Get(attach) 158 if err != nil { 159 return nil, err 160 } else if !has { 161 return nil, err 162 } 163 return attach, nil 164 } 165 166 // DeleteAttachment deletes the given attachment and optionally the associated file. 167 func DeleteAttachment(ctx context.Context, a *Attachment, remove bool) error { 168 _, err := DeleteAttachments(ctx, []*Attachment{a}, remove) 169 return err 170 } 171 172 // DeleteAttachments deletes the given attachments and optionally the associated files. 173 func DeleteAttachments(ctx context.Context, attachments []*Attachment, remove bool) (int, error) { 174 if len(attachments) == 0 { 175 return 0, nil 176 } 177 178 ids := make([]int64, 0, len(attachments)) 179 for _, a := range attachments { 180 ids = append(ids, a.ID) 181 } 182 183 cnt, err := db.GetEngine(ctx).In("id", ids).NoAutoCondition().Delete(attachments[0]) 184 if err != nil { 185 return 0, err 186 } 187 188 if remove { 189 for i, a := range attachments { 190 if err := storage.Attachments.Delete(a.RelativePath()); err != nil { 191 return i, err 192 } 193 } 194 } 195 return int(cnt), nil 196 } 197 198 // DeleteAttachmentsByIssue deletes all attachments associated with the given issue. 199 func DeleteAttachmentsByIssue(ctx context.Context, issueID int64, remove bool) (int, error) { 200 attachments, err := GetAttachmentsByIssueID(ctx, issueID) 201 if err != nil { 202 return 0, err 203 } 204 205 return DeleteAttachments(ctx, attachments, remove) 206 } 207 208 // DeleteAttachmentsByComment deletes all attachments associated with the given comment. 209 func DeleteAttachmentsByComment(ctx context.Context, commentID int64, remove bool) (int, error) { 210 attachments, err := GetAttachmentsByCommentID(ctx, commentID) 211 if err != nil { 212 return 0, err 213 } 214 215 return DeleteAttachments(ctx, attachments, remove) 216 } 217 218 // UpdateAttachmentByUUID Updates attachment via uuid 219 func UpdateAttachmentByUUID(ctx context.Context, attach *Attachment, cols ...string) error { 220 if attach.UUID == "" { 221 return fmt.Errorf("attachment uuid should be not blank") 222 } 223 _, err := db.GetEngine(ctx).Where("uuid=?", attach.UUID).Cols(cols...).Update(attach) 224 return err 225 } 226 227 // UpdateAttachment updates the given attachment in database 228 func UpdateAttachment(ctx context.Context, atta *Attachment) error { 229 sess := db.GetEngine(ctx).Cols("name", "issue_id", "release_id", "comment_id", "download_count") 230 if atta.ID != 0 && atta.UUID == "" { 231 sess = sess.ID(atta.ID) 232 } else { 233 // Use uuid only if id is not set and uuid is set 234 sess = sess.Where("uuid = ?", atta.UUID) 235 } 236 _, err := sess.Update(atta) 237 return err 238 } 239 240 // DeleteAttachmentsByRelease deletes all attachments associated with the given release. 241 func DeleteAttachmentsByRelease(ctx context.Context, releaseID int64) error { 242 _, err := db.GetEngine(ctx).Where("release_id = ?", releaseID).Delete(&Attachment{}) 243 return err 244 } 245 246 // CountOrphanedAttachments returns the number of bad attachments 247 func CountOrphanedAttachments(ctx context.Context) (int64, error) { 248 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`))"). 249 Count(new(Attachment)) 250 } 251 252 // DeleteOrphanedAttachments delete all bad attachments 253 func DeleteOrphanedAttachments(ctx context.Context) error { 254 _, err := 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`))"). 255 Delete(new(Attachment)) 256 return err 257 }