aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKN4CK3R <admin@oldschoolhack.me>2022-12-09 07:35:56 +0100
committerGitHub <noreply@github.com>2022-12-09 14:35:56 +0800
commit3c59d31bc605bbefc6636e9b0a93e90ad2696ed9 (patch)
treed26f47a8d0e0ea4cad3f01ea7bbc35668220f81b
parent8fb1e53ca2bea37d9d6b89a47cb13e253355829b (diff)
downloadgitea-3c59d31bc605bbefc6636e9b0a93e90ad2696ed9.tar.gz
gitea-3c59d31bc605bbefc6636e9b0a93e90ad2696ed9.zip
Add API management for issue/pull and comment attachments (#21783)
Close #14601 Fix #3690 Revive of #14601. Updated to current code, cleanup and added more read/write checks. Signed-off-by: Andrew Thornton <art27@cantab.net> Signed-off-by: Andre Bruch <ab@andrebruch.com> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Norwin <git@nroo.de> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
-rw-r--r--models/issues/comment.go2
-rw-r--r--models/migrations/v1_10/v96.go12
-rw-r--r--modules/convert/attachment.go30
-rw-r--r--modules/convert/issue.go29
-rw-r--r--modules/convert/issue_comment.go17
-rw-r--r--modules/convert/release.go19
-rw-r--r--modules/notification/webhook/webhook.go5
-rw-r--r--modules/structs/issue.go25
-rw-r--r--modules/structs/issue_comment.go17
-rw-r--r--routers/api/v1/api.go25
-rw-r--r--routers/api/v1/repo/issue_attachment.go372
-rw-r--r--routers/api/v1/repo/issue_comment.go9
-rw-r--r--routers/api/v1/repo/issue_comment_attachment.go383
-rw-r--r--routers/api/v1/repo/release_attachment.go13
-rw-r--r--routers/web/repo/attachment.go8
-rw-r--r--routers/web/repo/issue.go5
-rw-r--r--services/attachment/attachment.go11
-rw-r--r--services/release/release.go11
-rw-r--r--templates/swagger/v1_json.tmpl548
-rw-r--r--tests/integration/api_comment_attachment_test.go154
-rw-r--r--tests/integration/api_issue_attachment_test.go143
21 files changed, 1754 insertions, 84 deletions
diff --git a/models/issues/comment.go b/models/issues/comment.go
index f49c6e2c1f..2abe692a6f 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -875,6 +875,8 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err)
}
}
+
+ comment.Attachments = attachments
case CommentTypeReopen, CommentTypeClose:
if err = repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.Issue.IsPull, true); err != nil {
return err
diff --git a/models/migrations/v1_10/v96.go b/models/migrations/v1_10/v96.go
index 2abd260be4..422defe838 100644
--- a/models/migrations/v1_10/v96.go
+++ b/models/migrations/v1_10/v96.go
@@ -30,19 +30,19 @@ func DeleteOrphanedAttachments(x *xorm.Engine) error {
}
for {
- attachements := make([]Attachment, 0, limit)
+ attachments := make([]Attachment, 0, limit)
if err := sess.Where("`issue_id` = 0 and (`release_id` = 0 or `release_id` not in (select `id` from `release`))").
Cols("id, uuid").Limit(limit).
Asc("id").
- Find(&attachements); err != nil {
+ Find(&attachments); err != nil {
return err
}
- if len(attachements) == 0 {
+ if len(attachments) == 0 {
return nil
}
ids := make([]int64, 0, limit)
- for _, attachment := range attachements {
+ for _, attachment := range attachments {
ids = append(ids, attachment.ID)
}
if len(ids) > 0 {
@@ -51,13 +51,13 @@ func DeleteOrphanedAttachments(x *xorm.Engine) error {
}
}
- for _, attachment := range attachements {
+ for _, attachment := range attachments {
uuid := attachment.UUID
if err := util.RemoveAll(filepath.Join(setting.Attachment.Path, uuid[0:1], uuid[1:2], uuid)); err != nil {
return err
}
}
- if len(attachements) < limit {
+ if len(attachments) < limit {
return nil
}
}
diff --git a/modules/convert/attachment.go b/modules/convert/attachment.go
new file mode 100644
index 0000000000..ddba181a12
--- /dev/null
+++ b/modules/convert/attachment.go
@@ -0,0 +1,30 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ repo_model "code.gitea.io/gitea/models/repo"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToAttachment converts models.Attachment to api.Attachment
+func ToAttachment(a *repo_model.Attachment) *api.Attachment {
+ return &api.Attachment{
+ ID: a.ID,
+ Name: a.Name,
+ Created: a.CreatedUnix.AsTime(),
+ DownloadCount: a.DownloadCount,
+ Size: a.Size,
+ UUID: a.UUID,
+ DownloadURL: a.DownloadURL(),
+ }
+}
+
+func ToAttachments(attachments []*repo_model.Attachment) []*api.Attachment {
+ converted := make([]*api.Attachment, 0, len(attachments))
+ for _, attachment := range attachments {
+ converted = append(converted, ToAttachment(attachment))
+ }
+ return converted
+}
diff --git a/modules/convert/issue.go b/modules/convert/issue.go
index 3bc1006507..f3af03ed94 100644
--- a/modules/convert/issue.go
+++ b/modules/convert/issue.go
@@ -37,20 +37,21 @@ func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
}
apiIssue := &api.Issue{
- ID: issue.ID,
- URL: issue.APIURL(),
- HTMLURL: issue.HTMLURL(),
- Index: issue.Index,
- Poster: ToUser(issue.Poster, nil),
- Title: issue.Title,
- Body: issue.Content,
- Ref: issue.Ref,
- Labels: ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner),
- State: issue.State(),
- IsLocked: issue.IsLocked,
- Comments: issue.NumComments,
- Created: issue.CreatedUnix.AsTime(),
- Updated: issue.UpdatedUnix.AsTime(),
+ ID: issue.ID,
+ URL: issue.APIURL(),
+ HTMLURL: issue.HTMLURL(),
+ Index: issue.Index,
+ Poster: ToUser(issue.Poster, nil),
+ Title: issue.Title,
+ Body: issue.Content,
+ Attachments: ToAttachments(issue.Attachments),
+ Ref: issue.Ref,
+ Labels: ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner),
+ State: issue.State(),
+ IsLocked: issue.IsLocked,
+ Comments: issue.NumComments,
+ Created: issue.CreatedUnix.AsTime(),
+ Updated: issue.UpdatedUnix.AsTime(),
}
apiIssue.Repo = &api.RepositoryMeta{
diff --git a/modules/convert/issue_comment.go b/modules/convert/issue_comment.go
index c4fed6b8a1..983354438a 100644
--- a/modules/convert/issue_comment.go
+++ b/modules/convert/issue_comment.go
@@ -16,14 +16,15 @@ import (
// ToComment converts a issues_model.Comment to the api.Comment format
func ToComment(c *issues_model.Comment) *api.Comment {
return &api.Comment{
- ID: c.ID,
- Poster: ToUser(c.Poster, nil),
- HTMLURL: c.HTMLURL(),
- IssueURL: c.IssueURL(),
- PRURL: c.PRURL(),
- Body: c.Content,
- Created: c.CreatedUnix.AsTime(),
- Updated: c.UpdatedUnix.AsTime(),
+ ID: c.ID,
+ Poster: ToUser(c.Poster, nil),
+ HTMLURL: c.HTMLURL(),
+ IssueURL: c.IssueURL(),
+ PRURL: c.PRURL(),
+ Body: c.Content,
+ Attachments: ToAttachments(c.Attachments),
+ Created: c.CreatedUnix.AsTime(),
+ Updated: c.UpdatedUnix.AsTime(),
}
}
diff --git a/modules/convert/release.go b/modules/convert/release.go
index 95c6d03ab1..3afa53c03f 100644
--- a/modules/convert/release.go
+++ b/modules/convert/release.go
@@ -10,10 +10,6 @@ import (
// ToRelease convert a repo_model.Release to api.Release
func ToRelease(r *repo_model.Release) *api.Release {
- assets := make([]*api.Attachment, 0)
- for _, att := range r.Attachments {
- assets = append(assets, ToReleaseAttachment(att))
- }
return &api.Release{
ID: r.ID,
TagName: r.TagName,
@@ -29,19 +25,6 @@ func ToRelease(r *repo_model.Release) *api.Release {
CreatedAt: r.CreatedUnix.AsTime(),
PublishedAt: r.CreatedUnix.AsTime(),
Publisher: ToUser(r.Publisher, nil),
- Attachments: assets,
- }
-}
-
-// ToReleaseAttachment converts models.Attachment to api.Attachment
-func ToReleaseAttachment(a *repo_model.Attachment) *api.Attachment {
- return &api.Attachment{
- ID: a.ID,
- Name: a.Name,
- Created: a.CreatedUnix.AsTime(),
- DownloadCount: a.DownloadCount,
- Size: a.Size,
- UUID: a.UUID,
- DownloadURL: a.DownloadURL(),
+ Attachments: ToAttachments(r.Attachments),
}
}
diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go
index 6334583058..cf056f54c1 100644
--- a/modules/notification/webhook/webhook.go
+++ b/modules/notification/webhook/webhook.go
@@ -314,6 +314,11 @@ func (m *webhookNotifier) NotifyNewPullRequest(ctx context.Context, pull *issues
}
func (m *webhookNotifier) NotifyIssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string) {
+ if err := issue.LoadRepo(ctx); err != nil {
+ log.Error("LoadRepo: %v", err)
+ return
+ }
+
mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo)
var err error
if issue.IsPull {
diff --git a/modules/structs/issue.go b/modules/structs/issue.go
index 00166b7a07..48e4e0e7e3 100644
--- a/modules/structs/issue.go
+++ b/modules/structs/issue.go
@@ -41,18 +41,19 @@ type RepositoryMeta struct {
// Issue represents an issue in a repository
// swagger:model
type Issue struct {
- ID int64 `json:"id"`
- URL string `json:"url"`
- HTMLURL string `json:"html_url"`
- Index int64 `json:"number"`
- Poster *User `json:"user"`
- OriginalAuthor string `json:"original_author"`
- OriginalAuthorID int64 `json:"original_author_id"`
- Title string `json:"title"`
- Body string `json:"body"`
- Ref string `json:"ref"`
- Labels []*Label `json:"labels"`
- Milestone *Milestone `json:"milestone"`
+ ID int64 `json:"id"`
+ URL string `json:"url"`
+ HTMLURL string `json:"html_url"`
+ Index int64 `json:"number"`
+ Poster *User `json:"user"`
+ OriginalAuthor string `json:"original_author"`
+ OriginalAuthorID int64 `json:"original_author_id"`
+ Title string `json:"title"`
+ Body string `json:"body"`
+ Ref string `json:"ref"`
+ Attachments []*Attachment `json:"assets"`
+ Labels []*Label `json:"labels"`
+ Milestone *Milestone `json:"milestone"`
// deprecated
Assignee *User `json:"assignee"`
Assignees []*User `json:"assignees"`
diff --git a/modules/structs/issue_comment.go b/modules/structs/issue_comment.go
index 4a1085ba50..9e8f5c4bf3 100644
--- a/modules/structs/issue_comment.go
+++ b/modules/structs/issue_comment.go
@@ -9,14 +9,15 @@ import (
// Comment represents a comment on a commit or issue
type Comment struct {
- ID int64 `json:"id"`
- HTMLURL string `json:"html_url"`
- PRURL string `json:"pull_request_url"`
- IssueURL string `json:"issue_url"`
- Poster *User `json:"user"`
- OriginalAuthor string `json:"original_author"`
- OriginalAuthorID int64 `json:"original_author_id"`
- Body string `json:"body"`
+ ID int64 `json:"id"`
+ HTMLURL string `json:"html_url"`
+ PRURL string `json:"pull_request_url"`
+ IssueURL string `json:"issue_url"`
+ Poster *User `json:"user"`
+ OriginalAuthor string `json:"original_author"`
+ OriginalAuthorID int64 `json:"original_author_id"`
+ Body string `json:"body"`
+ Attachments []*Attachment `json:"assets"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 3f5cf431f8..14b168c242 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -567,6 +567,13 @@ func mustNotBeArchived(ctx *context.APIContext) {
}
}
+func mustEnableAttachments(ctx *context.APIContext) {
+ if !setting.Attachment.Enabled {
+ ctx.NotFound()
+ return
+ }
+}
+
// bind binding an obj to a func(ctx *context.APIContext)
func bind(obj interface{}) http.HandlerFunc {
tp := reflect.TypeOf(obj)
@@ -892,6 +899,15 @@ func Routes(ctx gocontext.Context) *web.Route {
Get(repo.GetIssueCommentReactions).
Post(reqToken(), bind(api.EditReactionOption{}), repo.PostIssueCommentReaction).
Delete(reqToken(), bind(api.EditReactionOption{}), repo.DeleteIssueCommentReaction)
+ m.Group("/assets", func() {
+ m.Combo("").
+ Get(repo.ListIssueCommentAttachments).
+ Post(reqToken(), mustNotBeArchived, repo.CreateIssueCommentAttachment)
+ m.Combo("/{asset}").
+ Get(repo.GetIssueCommentAttachment).
+ Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
+ Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueCommentAttachment)
+ }, mustEnableAttachments)
})
})
m.Group("/{index}", func() {
@@ -935,6 +951,15 @@ func Routes(ctx gocontext.Context) *web.Route {
Get(repo.GetIssueReactions).
Post(reqToken(), bind(api.EditReactionOption{}), repo.PostIssueReaction).
Delete(reqToken(), bind(api.EditReactionOption{}), repo.DeleteIssueReaction)
+ m.Group("/assets", func() {
+ m.Combo("").
+ Get(repo.ListIssueAttachments).
+ Post(reqToken(), mustNotBeArchived, repo.CreateIssueAttachment)
+ m.Combo("/{asset}").
+ Get(repo.GetIssueAttachment).
+ Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment).
+ Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueAttachment)
+ }, mustEnableAttachments)
})
}, mustEnableIssuesOrPulls)
m.Group("/labels", func() {
diff --git a/routers/api/v1/repo/issue_attachment.go b/routers/api/v1/repo/issue_attachment.go
new file mode 100644
index 0000000000..4cf108b413
--- /dev/null
+++ b/routers/api/v1/repo/issue_attachment.go
@@ -0,0 +1,372 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/convert"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/attachment"
+ issue_service "code.gitea.io/gitea/services/issue"
+)
+
+// GetIssueAttachment gets a single attachment of the issue
+func GetIssueAttachment(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueGetIssueAttachment
+ // ---
+ // summary: Get an issue attachment
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: index
+ // in: path
+ // description: index of the issue
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: attachment_id
+ // in: path
+ // description: id of the attachment to get
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/Attachment"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ issue := getIssueFromContext(ctx)
+ if issue == nil {
+ return
+ }
+
+ attach := getIssueAttachmentSafeRead(ctx, issue)
+ if attach == nil {
+ return
+ }
+
+ ctx.JSON(http.StatusOK, convert.ToAttachment(attach))
+}
+
+// ListIssueAttachments lists all attachments of the issue
+func ListIssueAttachments(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assets issue issueListIssueAttachments
+ // ---
+ // summary: List issue's attachments
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: index
+ // in: path
+ // description: index of the issue
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/AttachmentList"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ issue := getIssueFromContext(ctx)
+ if issue == nil {
+ return
+ }
+
+ if err := issue.LoadAttributes(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue).Attachments)
+}
+
+// CreateIssueAttachment creates an attachment and saves the given file
+func CreateIssueAttachment(ctx *context.APIContext) {
+ // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/assets issue issueCreateIssueAttachment
+ // ---
+ // summary: Create an issue attachment
+ // produces:
+ // - application/json
+ // consumes:
+ // - multipart/form-data
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: index
+ // in: path
+ // description: index of the issue
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: name
+ // in: query
+ // description: name of the attachment
+ // type: string
+ // required: false
+ // - name: attachment
+ // in: formData
+ // description: attachment to upload
+ // type: file
+ // required: true
+ // responses:
+ // "201":
+ // "$ref": "#/responses/Attachment"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ issue := getIssueFromContext(ctx)
+ if issue == nil {
+ return
+ }
+
+ if !canUserWriteIssueAttachment(ctx, issue) {
+ return
+ }
+
+ // Get uploaded file from request
+ file, header, err := ctx.Req.FormFile("attachment")
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FormFile", err)
+ return
+ }
+ defer file.Close()
+
+ filename := header.Filename
+ if query := ctx.FormString("name"); query != "" {
+ filename = query
+ }
+
+ attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, &repo_model.Attachment{
+ Name: filename,
+ UploaderID: ctx.Doer.ID,
+ RepoID: ctx.Repo.Repository.ID,
+ IssueID: issue.ID,
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)
+ return
+ }
+
+ issue.Attachments = append(issue.Attachments, attachment)
+
+ if err := issue_service.ChangeContent(issue, ctx.Doer, issue.Content); err != nil {
+ ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
+ return
+ }
+
+ ctx.JSON(http.StatusCreated, convert.ToAttachment(attachment))
+}
+
+// EditIssueAttachment updates the given attachment
+func EditIssueAttachment(ctx *context.APIContext) {
+ // swagger:operation PATCH /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueEditIssueAttachment
+ // ---
+ // summary: Edit an issue attachment
+ // produces:
+ // - application/json
+ // consumes:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: index
+ // in: path
+ // description: index of the issue
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: attachment_id
+ // in: path
+ // description: id of the attachment to edit
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/EditAttachmentOptions"
+ // responses:
+ // "201":
+ // "$ref": "#/responses/Attachment"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ attachment := getIssueAttachmentSafeWrite(ctx)
+ if attachment == nil {
+ return
+ }
+
+ // do changes to attachment. only meaningful change is name.
+ form := web.GetForm(ctx).(*api.EditAttachmentOptions)
+ if form.Name != "" {
+ attachment.Name = form.Name
+ }
+
+ if err := repo_model.UpdateAttachment(ctx, attachment); err != nil {
+ ctx.Error(http.StatusInternalServerError, "UpdateAttachment", err)
+ }
+
+ ctx.JSON(http.StatusCreated, convert.ToAttachment(attachment))
+}
+
+// DeleteIssueAttachment delete a given attachment
+func DeleteIssueAttachment(ctx *context.APIContext) {
+ // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueDeleteIssueAttachment
+ // ---
+ // summary: Delete an issue attachment
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: index
+ // in: path
+ // description: index of the issue
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: attachment_id
+ // in: path
+ // description: id of the attachment to delete
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ attachment := getIssueAttachmentSafeWrite(ctx)
+ if attachment == nil {
+ return
+ }
+
+ if err := repo_model.DeleteAttachment(attachment, true); err != nil {
+ ctx.Error(http.StatusInternalServerError, "DeleteAttachment", err)
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+func getIssueFromContext(ctx *context.APIContext) *issues_model.Issue {
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64("index"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err)
+ return nil
+ }
+
+ issue.Repo = ctx.Repo.Repository
+
+ return issue
+}
+
+func getIssueAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment {
+ issue := getIssueFromContext(ctx)
+ if issue == nil {
+ return nil
+ }
+
+ if !canUserWriteIssueAttachment(ctx, issue) {
+ return nil
+ }
+
+ return getIssueAttachmentSafeRead(ctx, issue)
+}
+
+func getIssueAttachmentSafeRead(ctx *context.APIContext, issue *issues_model.Issue) *repo_model.Attachment {
+ attachment, err := repo_model.GetAttachmentByID(ctx, ctx.ParamsInt64("asset"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err)
+ return nil
+ }
+ if !attachmentBelongsToRepoOrIssue(ctx, attachment, issue) {
+ return nil
+ }
+ return attachment
+}
+
+func canUserWriteIssueAttachment(ctx *context.APIContext, issue *issues_model.Issue) bool {
+ canEditIssue := ctx.IsSigned && (ctx.Doer.ID == issue.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
+ if !canEditIssue {
+ ctx.Error(http.StatusForbidden, "", "user should have permission to write issue")
+ return false
+ }
+
+ return true
+}
+
+func attachmentBelongsToRepoOrIssue(ctx *context.APIContext, attachment *repo_model.Attachment, issue *issues_model.Issue) bool {
+ if attachment.RepoID != ctx.Repo.Repository.ID {
+ log.Debug("Requested attachment[%d] does not belong to repo[%-v].", attachment.ID, ctx.Repo.Repository)
+ ctx.NotFound("no such attachment in repo")
+ return false
+ }
+ if attachment.IssueID == 0 {
+ log.Debug("Requested attachment[%d] is not in an issue.", attachment.ID)
+ ctx.NotFound("no such attachment in issue")
+ return false
+ } else if issue != nil && attachment.IssueID != issue.ID {
+ log.Debug("Requested attachment[%d] does not belong to issue[%d, #%d].", attachment.ID, issue.ID, issue.Index)
+ ctx.NotFound("no such attachment in issue")
+ return false
+ }
+ return true
+}
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index 120c1d88a0..a584a7a174 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -95,6 +95,11 @@ func ListIssueComments(ctx *context.APIContext) {
return
}
+ if err := issues_model.CommentList(comments).LoadAttachments(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
+ return
+ }
+
apiComments := make([]*api.Comment, len(comments))
for i, comment := range comments {
comment.Issue = issue
@@ -294,6 +299,10 @@ func ListRepoIssueComments(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
return
}
+ if err := issues_model.CommentList(comments).LoadAttachments(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
+ return
+ }
if _, err := issues_model.CommentList(comments).Issues().LoadRepositories(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadRepositories", err)
return
diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go
new file mode 100644
index 0000000000..60ea8d1b83
--- /dev/null
+++ b/routers/api/v1/repo/issue_comment_attachment.go
@@ -0,0 +1,383 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/convert"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/attachment"
+ comment_service "code.gitea.io/gitea/services/comments"
+)
+
+// GetIssueCommentAttachment gets a single attachment of the comment
+func GetIssueCommentAttachment(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueGetIssueCommentAttachment
+ // ---
+ // summary: Get a comment attachment
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: id
+ // in: path
+ // description: id of the comment
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: attachment_id
+ // in: path
+ // description: id of the attachment to get
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/Attachment"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ comment := getIssueCommentSafe(ctx)
+ if comment == nil {
+ return
+ }
+ attachment := getIssueCommentAttachmentSafeRead(ctx, comment)
+ if attachment == nil {
+ return
+ }
+ if attachment.CommentID != comment.ID {
+ log.Debug("User requested attachment[%d] is not in comment[%d].", attachment.ID, comment.ID)
+ ctx.NotFound("attachment not in comment")
+ return
+ }
+
+ ctx.JSON(http.StatusOK, convert.ToAttachment(attachment))
+}
+
+// ListIssueCommentAttachments lists all attachments of the comment
+func ListIssueCommentAttachments(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/assets issue issueListIssueCommentAttachments
+ // ---
+ // summary: List comment's attachments
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: id
+ // in: path
+ // description: id of the comment
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/AttachmentList"
+ // "404":
+ // "$ref": "#/responses/error"
+ comment := getIssueCommentSafe(ctx)
+ if comment == nil {
+ return
+ }
+
+ if err := comment.LoadAttachments(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, convert.ToAttachments(comment.Attachments))
+}
+
+// CreateIssueCommentAttachment creates an attachment and saves the given file
+func CreateIssueCommentAttachment(ctx *context.APIContext) {
+ // swagger:operation POST /repos/{owner}/{repo}/issues/comments/{id}/assets issue issueCreateIssueCommentAttachment
+ // ---
+ // summary: Create a comment attachment
+ // produces:
+ // - application/json
+ // consumes:
+ // - multipart/form-data
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: id
+ // in: path
+ // description: id of the comment
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: name
+ // in: query
+ // description: name of the attachment
+ // type: string
+ // required: false
+ // - name: attachment
+ // in: formData
+ // description: attachment to upload
+ // type: file
+ // required: true
+ // responses:
+ // "201":
+ // "$ref": "#/responses/Attachment"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ // Check if comment exists and load comment
+ comment := getIssueCommentSafe(ctx)
+ if comment == nil {
+ return
+ }
+
+ if !canUserWriteIssueCommentAttachment(ctx, comment) {
+ return
+ }
+
+ // Get uploaded file from request
+ file, header, err := ctx.Req.FormFile("attachment")
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FormFile", err)
+ return
+ }
+ defer file.Close()
+
+ filename := header.Filename
+ if query := ctx.FormString("name"); query != "" {
+ filename = query
+ }
+
+ attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, &repo_model.Attachment{
+ Name: filename,
+ UploaderID: ctx.Doer.ID,
+ RepoID: ctx.Repo.Repository.ID,
+ IssueID: comment.IssueID,
+ CommentID: comment.ID,
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)
+ return
+ }
+ if err := comment.LoadAttachments(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
+ return
+ }
+
+ if err = comment_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil {
+ ctx.ServerError("UpdateComment", err)
+ return
+ }
+
+ ctx.JSON(http.StatusCreated, convert.ToAttachment(attachment))
+}
+
+// EditIssueCommentAttachment updates the given attachment
+func EditIssueCommentAttachment(ctx *context.APIContext) {
+ // swagger:operation PATCH /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueEditIssueCommentAttachment
+ // ---
+ // summary: Edit a comment attachment
+ // produces:
+ // - application/json
+ // consumes:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: id
+ // in: path
+ // description: id of the comment
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: attachment_id
+ // in: path
+ // description: id of the attachment to edit
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/EditAttachmentOptions"
+ // responses:
+ // "201":
+ // "$ref": "#/responses/Attachment"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ attach := getIssueCommentAttachmentSafeWrite(ctx)
+ if attach == nil {
+ return
+ }
+
+ form := web.GetForm(ctx).(*api.EditAttachmentOptions)
+ if form.Name != "" {
+ attach.Name = form.Name
+ }
+
+ if err := repo_model.UpdateAttachment(ctx, attach); err != nil {
+ ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach)
+ }
+ ctx.JSON(http.StatusCreated, convert.ToAttachment(attach))
+}
+
+// DeleteIssueCommentAttachment delete a given attachment
+func DeleteIssueCommentAttachment(ctx *context.APIContext) {
+ // swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueDeleteIssueCommentAttachment
+ // ---
+ // summary: Delete a comment attachment
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: id
+ // in: path
+ // description: id of the comment
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: attachment_id
+ // in: path
+ // description: id of the attachment to delete
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ attach := getIssueCommentAttachmentSafeWrite(ctx)
+ if attach == nil {
+ return
+ }
+
+ if err := repo_model.DeleteAttachment(attach, true); err != nil {
+ ctx.Error(http.StatusInternalServerError, "DeleteAttachment", err)
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
+
+func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
+ comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64("id"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
+ return nil
+ }
+ if err := comment.LoadIssue(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err)
+ return nil
+ }
+ if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID {
+ ctx.Error(http.StatusNotFound, "", "no matching issue comment found")
+ return nil
+ }
+
+ comment.Issue.Repo = ctx.Repo.Repository
+
+ return comment
+}
+
+func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment {
+ comment := getIssueCommentSafe(ctx)
+ if comment == nil {
+ return nil
+ }
+ if !canUserWriteIssueCommentAttachment(ctx, comment) {
+ return nil
+ }
+ return getIssueCommentAttachmentSafeRead(ctx, comment)
+}
+
+func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool {
+ canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)
+ if !canEditComment {
+ ctx.Error(http.StatusForbidden, "", "user should have permission to edit comment")
+ return false
+ }
+
+ return true
+}
+
+func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_model.Comment) *repo_model.Attachment {
+ attachment, err := repo_model.GetAttachmentByID(ctx, ctx.ParamsInt64("asset"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err)
+ return nil
+ }
+ if !attachmentBelongsToRepoOrComment(ctx, attachment, comment) {
+ return nil
+ }
+ return attachment
+}
+
+func attachmentBelongsToRepoOrComment(ctx *context.APIContext, attachment *repo_model.Attachment, comment *issues_model.Comment) bool {
+ if attachment.RepoID != ctx.Repo.Repository.ID {
+ log.Debug("Requested attachment[%d] does not belong to repo[%-v].", attachment.ID, ctx.Repo.Repository)
+ ctx.NotFound("no such attachment in repo")
+ return false
+ }
+ if attachment.IssueID == 0 || attachment.CommentID == 0 {
+ log.Debug("Requested attachment[%d] is not in a comment.", attachment.ID)
+ ctx.NotFound("no such attachment in comment")
+ return false
+ }
+ if comment != nil && attachment.CommentID != comment.ID {
+ log.Debug("Requested attachment[%d] does not belong to comment[%d].", attachment.ID, comment.ID)
+ ctx.NotFound("no such attachment in comment")
+ return false
+ }
+ return true
+}
diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go
index 88632b4637..e7dbb42c74 100644
--- a/routers/api/v1/repo/release_attachment.go
+++ b/routers/api/v1/repo/release_attachment.go
@@ -68,7 +68,7 @@ func GetReleaseAttachment(ctx *context.APIContext) {
return
}
// FIXME Should prove the existence of the given repo, but results in unnecessary database requests
- ctx.JSON(http.StatusOK, convert.ToReleaseAttachment(attach))
+ ctx.JSON(http.StatusOK, convert.ToAttachment(attach))
}
// ListReleaseAttachments lists all attachments of the release
@@ -194,7 +194,12 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
}
// Create a new attachment and save the file
- attach, err := attachment.UploadAttachment(file, ctx.Doer.ID, release.RepoID, releaseID, filename, setting.Repository.Release.AllowedTypes)
+ attach, err := attachment.UploadAttachment(file, setting.Repository.Release.AllowedTypes, &repo_model.Attachment{
+ Name: filename,
+ UploaderID: ctx.Doer.ID,
+ RepoID: release.RepoID,
+ ReleaseID: releaseID,
+ })
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.Error(http.StatusBadRequest, "DetectContentType", err)
@@ -204,7 +209,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
return
}
- ctx.JSON(http.StatusCreated, convert.ToReleaseAttachment(attach))
+ ctx.JSON(http.StatusCreated, convert.ToAttachment(attach))
}
// EditReleaseAttachment updates the given attachment
@@ -274,7 +279,7 @@ func EditReleaseAttachment(ctx *context.APIContext) {
if err := repo_model.UpdateAttachment(ctx, attach); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach)
}
- ctx.JSON(http.StatusCreated, convert.ToReleaseAttachment(attach))
+ ctx.JSON(http.StatusCreated, convert.ToAttachment(attach))
}
// DeleteReleaseAttachment delete a given attachment
diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go
index 656e40f878..589632ad6e 100644
--- a/routers/web/repo/attachment.go
+++ b/routers/web/repo/attachment.go
@@ -44,7 +44,11 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
}
defer file.Close()
- attach, err := attachment.UploadAttachment(file, ctx.Doer.ID, repoID, 0, header.Filename, allowedTypes)
+ attach, err := attachment.UploadAttachment(file, allowedTypes, &repo_model.Attachment{
+ Name: header.Filename,
+ UploaderID: ctx.Doer.ID,
+ RepoID: repoID,
+ })
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.Error(http.StatusBadRequest, err.Error())
@@ -82,7 +86,7 @@ func DeleteAttachment(ctx *context.Context) {
})
}
-// GetAttachment serve attachements
+// GetAttachment serve attachments
func GetAttachment(ctx *context.Context) {
attach, err := repo_model.GetAttachmentByUUID(ctx, ctx.Params(":uuid"))
if err != nil {
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index a531e83206..b11cc58e41 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -2749,6 +2749,7 @@ func UpdateCommentContent(ctx *context.Context) {
})
return
}
+
if err = comment_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
ctx.ServerError("UpdateComment", err)
return
@@ -3050,7 +3051,7 @@ func GetIssueAttachments(ctx *context.Context) {
issue := GetActionIssue(ctx)
attachments := make([]*api.Attachment, len(issue.Attachments))
for i := 0; i < len(issue.Attachments); i++ {
- attachments[i] = convert.ToReleaseAttachment(issue.Attachments[i])
+ attachments[i] = convert.ToAttachment(issue.Attachments[i])
}
ctx.JSON(http.StatusOK, attachments)
}
@@ -3069,7 +3070,7 @@ func GetCommentAttachments(ctx *context.Context) {
return
}
for i := 0; i < len(comment.Attachments); i++ {
- attachments = append(attachments, convert.ToReleaseAttachment(comment.Attachments[i]))
+ attachments = append(attachments, convert.ToAttachment(comment.Attachments[i]))
}
}
ctx.JSON(http.StatusOK, attachments)
diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go
index 522acd00a3..7fdacc6aae 100644
--- a/services/attachment/attachment.go
+++ b/services/attachment/attachment.go
@@ -39,19 +39,14 @@ func NewAttachment(attach *repo_model.Attachment, file io.Reader) (*repo_model.A
}
// UploadAttachment upload new attachment into storage and update database
-func UploadAttachment(file io.Reader, actorID, repoID, releaseID int64, fileName, allowedTypes string) (*repo_model.Attachment, error) {
+func UploadAttachment(file io.Reader, allowedTypes string, opts *repo_model.Attachment) (*repo_model.Attachment, error) {
buf := make([]byte, 1024)
n, _ := util.ReadAtMost(file, buf)
buf = buf[:n]
- if err := upload.Verify(buf, fileName, allowedTypes); err != nil {
+ if err := upload.Verify(buf, opts.Name, allowedTypes); err != nil {
return nil, err
}
- return NewAttachment(&repo_model.Attachment{
- RepoID: repoID,
- UploaderID: actorID,
- ReleaseID: releaseID,
- Name: fileName,
- }, io.MultiReader(bytes.NewReader(buf), file))
+ return NewAttachment(opts, io.MultiReader(bytes.NewReader(buf), file))
}
diff --git a/services/release/release.go b/services/release/release.go
index 1d599fcda1..13042cd3ac 100644
--- a/services/release/release.go
+++ b/services/release/release.go
@@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
)
func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Release, msg string) (bool, error) {
@@ -218,7 +219,10 @@ func UpdateRelease(doer *user_model.User, gitRepo *git.Repository, rel *repo_mod
}
for _, attach := range attachments {
if attach.ReleaseID != rel.ID {
- return errors.New("delete attachement of release permission denied")
+ return util.SilentWrap{
+ Message: "delete attachment of release permission denied",
+ Err: util.ErrPermissionDenied,
+ }
}
deletedUUIDs.Add(attach.UUID)
}
@@ -240,7 +244,10 @@ func UpdateRelease(doer *user_model.User, gitRepo *git.Repository, rel *repo_mod
}
for _, attach := range attachments {
if attach.ReleaseID != rel.ID {
- return errors.New("update attachement of release permission denied")
+ return util.SilentWrap{
+ Message: "update attachment of release permission denied",
+ Err: util.ErrPermissionDenied,
+ }
}
}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index ddafc146a1..c86c6744de 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -5095,6 +5095,273 @@
}
}
},
+ "/repos/{owner}/{repo}/issues/comments/{id}/assets": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "List comment's attachments",
+ "operationId": "issueListIssueCommentAttachments",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the comment",
+ "name": "id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/AttachmentList"
+ },
+ "404": {
+ "$ref": "#/responses/error"
+ }
+ }
+ },
+ "post": {
+ "consumes": [
+ "multipart/form-data"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "Create a comment attachment",
+ "operationId": "issueCreateIssueCommentAttachment",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the comment",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the attachment",
+ "name": "name",
+ "in": "query"
+ },
+ {
+ "type": "file",
+ "description": "attachment to upload",
+ "name": "attachment",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "201": {
+ "$ref": "#/responses/Attachment"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/error"
+ }
+ }
+ }
+ },
+ "/repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id}": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "Get a comment attachment",
+ "operationId": "issueGetIssueCommentAttachment",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the comment",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the attachment to get",
+ "name": "attachment_id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/Attachment"
+ },
+ "404": {
+ "$ref": "#/responses/error"
+ }
+ }
+ },
+ "delete": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "Delete a comment attachment",
+ "operationId": "issueDeleteIssueCommentAttachment",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the comment",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the attachment to delete",
+ "name": "attachment_id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "$ref": "#/responses/empty"
+ },
+ "404": {
+ "$ref": "#/responses/error"
+ }
+ }
+ },
+ "patch": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "Edit a comment attachment",
+ "operationId": "issueEditIssueCommentAttachment",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the comment",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the attachment to edit",
+ "name": "attachment_id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/EditAttachmentOptions"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "$ref": "#/responses/Attachment"
+ },
+ "404": {
+ "$ref": "#/responses/error"
+ }
+ }
+ }
+ },
"/repos/{owner}/{repo}/issues/comments/{id}/reactions": {
"get": {
"consumes": [
@@ -5393,6 +5660,273 @@
}
}
},
+ "/repos/{owner}/{repo}/issues/{index}/assets": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "List issue's attachments",
+ "operationId": "issueListIssueAttachments",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "index of the issue",
+ "name": "index",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/AttachmentList"
+ },
+ "404": {
+ "$ref": "#/responses/error"
+ }
+ }
+ },
+ "post": {
+ "consumes": [
+ "multipart/form-data"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "Create an issue attachment",
+ "operationId": "issueCreateIssueAttachment",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "index of the issue",
+ "name": "index",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the attachment",
+ "name": "name",
+ "in": "query"
+ },
+ {
+ "type": "file",
+ "description": "attachment to upload",
+ "name": "attachment",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "201": {
+ "$ref": "#/responses/Attachment"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/error"
+ }
+ }
+ }
+ },
+ "/repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "Get an issue attachment",
+ "operationId": "issueGetIssueAttachment",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "index of the issue",
+ "name": "index",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the attachment to get",
+ "name": "attachment_id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/Attachment"
+ },
+ "404": {
+ "$ref": "#/responses/error"
+ }
+ }
+ },
+ "delete": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "Delete an issue attachment",
+ "operationId": "issueDeleteIssueAttachment",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "index of the issue",
+ "name": "index",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the attachment to delete",
+ "name": "attachment_id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "$ref": "#/responses/empty"
+ },
+ "404": {
+ "$ref": "#/responses/error"
+ }
+ }
+ },
+ "patch": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "Edit an issue attachment",
+ "operationId": "issueEditIssueAttachment",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "index of the issue",
+ "name": "index",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the attachment to edit",
+ "name": "attachment_id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/EditAttachmentOptions"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "$ref": "#/responses/Attachment"
+ },
+ "404": {
+ "$ref": "#/responses/error"
+ }
+ }
+ }
+ },
"/repos/{owner}/{repo}/issues/{index}/comments": {
"get": {
"produces": [
@@ -13882,6 +14416,13 @@
"description": "Comment represents a comment on a commit or issue",
"type": "object",
"properties": {
+ "assets": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Attachment"
+ },
+ "x-go-name": "Attachments"
+ },
"body": {
"type": "string",
"x-go-name": "Body"
@@ -16634,6 +17175,13 @@
"description": "Issue represents an issue in a repository",
"type": "object",
"properties": {
+ "assets": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Attachment"
+ },
+ "x-go-name": "Attachments"
+ },
"assignee": {
"$ref": "#/definitions/User"
},
diff --git a/tests/integration/api_comment_attachment_test.go b/tests/integration/api_comment_attachment_test.go
new file mode 100644
index 0000000000..22bf502ef6
--- /dev/null
+++ b/tests/integration/api_comment_attachment_test.go
@@ -0,0 +1,154 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/convert"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIGetCommentAttachment(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
+ assert.NoError(t, comment.LoadIssue(db.DefaultContext))
+ assert.NoError(t, comment.LoadAttachments(db.DefaultContext))
+ attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: comment.Attachments[0].ID})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.Name, comment.ID, attachment.ID)
+ session.MakeRequest(t, req, http.StatusOK)
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var apiAttachment api.Attachment
+ DecodeJSON(t, resp, &apiAttachment)
+
+ expect := convert.ToAttachment(attachment)
+ assert.Equal(t, expect.ID, apiAttachment.ID)
+ assert.Equal(t, expect.Name, apiAttachment.Name)
+ assert.Equal(t, expect.UUID, apiAttachment.UUID)
+ assert.Equal(t, expect.Created.Unix(), apiAttachment.Created.Unix())
+}
+
+func TestAPIListCommentAttachments(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets",
+ repoOwner.Name, repo.Name, comment.ID)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var apiAttachments []*api.Attachment
+ DecodeJSON(t, resp, &apiAttachments)
+ expectedCount := unittest.GetCount(t, &repo_model.Attachment{CommentID: comment.ID})
+ assert.EqualValues(t, expectedCount, len(apiAttachments))
+
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachments[0].ID, CommentID: comment.ID})
+}
+
+func TestAPICreateCommentAttachment(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets?token=%s",
+ repoOwner.Name, repo.Name, comment.ID, token)
+
+ filename := "image.png"
+ buff := generateImg()
+ body := &bytes.Buffer{}
+
+ // Setup multi-part
+ writer := multipart.NewWriter(body)
+ part, err := writer.CreateFormFile("attachment", filename)
+ assert.NoError(t, err)
+ _, err = io.Copy(part, &buff)
+ assert.NoError(t, err)
+ err = writer.Close()
+ assert.NoError(t, err)
+
+ req := NewRequestWithBody(t, "POST", urlStr, body)
+ req.Header.Add("Content-Type", writer.FormDataContentType())
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+
+ apiAttachment := new(api.Attachment)
+ DecodeJSON(t, resp, &apiAttachment)
+
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, CommentID: comment.ID})
+}
+
+func TestAPIEditCommentAttachment(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ const newAttachmentName = "newAttachmentName"
+
+ attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 6})
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: attachment.CommentID})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s",
+ repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
+ req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
+ "name": newAttachmentName,
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ apiAttachment := new(api.Attachment)
+ DecodeJSON(t, resp, &apiAttachment)
+
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, CommentID: comment.ID, Name: apiAttachment.Name})
+}
+
+func TestAPIDeleteCommentAttachment(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 6})
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: attachment.CommentID})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s",
+ repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
+
+ req := NewRequestf(t, "DELETE", urlStr)
+ session.MakeRequest(t, req, http.StatusNoContent)
+
+ unittest.AssertNotExistsBean(t, &repo_model.Attachment{ID: attachment.ID, CommentID: comment.ID})
+}
diff --git a/tests/integration/api_issue_attachment_test.go b/tests/integration/api_issue_attachment_test.go
new file mode 100644
index 0000000000..0558dda56a
--- /dev/null
+++ b/tests/integration/api_issue_attachment_test.go
@@ -0,0 +1,143 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "testing"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIGetIssueAttachment(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 1})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: attachment.IssueID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d?token=%s",
+ repoOwner.Name, repo.Name, issue.Index, attachment.ID, token)
+
+ req := NewRequest(t, "GET", urlStr)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ apiAttachment := new(api.Attachment)
+ DecodeJSON(t, resp, &apiAttachment)
+
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
+}
+
+func TestAPIListIssueAttachments(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 1})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: attachment.IssueID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets?token=%s",
+ repoOwner.Name, repo.Name, issue.Index, token)
+
+ req := NewRequest(t, "GET", urlStr)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ apiAttachment := new([]api.Attachment)
+ DecodeJSON(t, resp, &apiAttachment)
+
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: (*apiAttachment)[0].ID, IssueID: issue.ID})
+}
+
+func TestAPICreateIssueAttachment(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets?token=%s",
+ repoOwner.Name, repo.Name, issue.Index, token)
+
+ filename := "image.png"
+ buff := generateImg()
+ body := &bytes.Buffer{}
+
+ // Setup multi-part
+ writer := multipart.NewWriter(body)
+ part, err := writer.CreateFormFile("attachment", filename)
+ assert.NoError(t, err)
+ _, err = io.Copy(part, &buff)
+ assert.NoError(t, err)
+ err = writer.Close()
+ assert.NoError(t, err)
+
+ req := NewRequestWithBody(t, "POST", urlStr, body)
+ req.Header.Add("Content-Type", writer.FormDataContentType())
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+
+ apiAttachment := new(api.Attachment)
+ DecodeJSON(t, resp, &apiAttachment)
+
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
+}
+
+func TestAPIEditIssueAttachment(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ const newAttachmentName = "newAttachmentName"
+
+ attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 1})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: attachment.IssueID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d?token=%s",
+ repoOwner.Name, repo.Name, issue.Index, attachment.ID, token)
+ req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
+ "name": newAttachmentName,
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ apiAttachment := new(api.Attachment)
+ DecodeJSON(t, resp, &apiAttachment)
+
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID, Name: apiAttachment.Name})
+}
+
+func TestAPIDeleteIssueAttachment(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 1})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: attachment.IssueID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d?token=%s",
+ repoOwner.Name, repo.Name, issue.Index, attachment.ID, token)
+
+ req := NewRequest(t, "DELETE", urlStr)
+ session.MakeRequest(t, req, http.StatusNoContent)
+
+ unittest.AssertNotExistsBean(t, &repo_model.Attachment{ID: attachment.ID, IssueID: issue.ID})
+}