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  }