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>tags/v1.19.0-rc0
@@ -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 |
@@ -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 | |||
} | |||
} |
@@ -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 | |||
} |
@@ -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{ |
@@ -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(), | |||
} | |||
} | |||
@@ -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), | |||
} | |||
} |
@@ -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 { |
@@ -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"` |
@@ -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 |
@@ -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() { |
@@ -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 | |||
} |
@@ -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 |
@@ -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 | |||
} |
@@ -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 |
@@ -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 { |
@@ -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) |
@@ -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)) | |||
} |
@@ -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, | |||
} | |||
} | |||
} | |||
@@ -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": [ | |||
@@ -5106,8 +5373,159 @@ | |||
"tags": [ | |||
"issue" | |||
], | |||
"summary": "Get a list of reactions from a comment of an issue", | |||
"operationId": "issueGetCommentReactions", | |||
"summary": "Get a list of reactions from a comment of an issue", | |||
"operationId": "issueGetCommentReactions", | |||
"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 to edit", | |||
"name": "id", | |||
"in": "path", | |||
"required": true | |||
} | |||
], | |||
"responses": { | |||
"200": { | |||
"$ref": "#/responses/ReactionList" | |||
}, | |||
"403": { | |||
"$ref": "#/responses/forbidden" | |||
} | |||
} | |||
}, | |||
"post": { | |||
"consumes": [ | |||
"application/json" | |||
], | |||
"produces": [ | |||
"application/json" | |||
], | |||
"tags": [ | |||
"issue" | |||
], | |||
"summary": "Add a reaction to a comment of an issue", | |||
"operationId": "issuePostCommentReaction", | |||
"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 to edit", | |||
"name": "id", | |||
"in": "path", | |||
"required": true | |||
}, | |||
{ | |||
"name": "content", | |||
"in": "body", | |||
"schema": { | |||
"$ref": "#/definitions/EditReactionOption" | |||
} | |||
} | |||
], | |||
"responses": { | |||
"200": { | |||
"$ref": "#/responses/Reaction" | |||
}, | |||
"201": { | |||
"$ref": "#/responses/Reaction" | |||
}, | |||
"403": { | |||
"$ref": "#/responses/forbidden" | |||
} | |||
} | |||
}, | |||
"delete": { | |||
"consumes": [ | |||
"application/json" | |||
], | |||
"produces": [ | |||
"application/json" | |||
], | |||
"tags": [ | |||
"issue" | |||
], | |||
"summary": "Remove a reaction from a comment of an issue", | |||
"operationId": "issueDeleteCommentReaction", | |||
"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 to edit", | |||
"name": "id", | |||
"in": "path", | |||
"required": true | |||
}, | |||
{ | |||
"name": "content", | |||
"in": "body", | |||
"schema": { | |||
"$ref": "#/definitions/EditReactionOption" | |||
} | |||
} | |||
], | |||
"responses": { | |||
"200": { | |||
"$ref": "#/responses/empty" | |||
}, | |||
"403": { | |||
"$ref": "#/responses/forbidden" | |||
} | |||
} | |||
} | |||
}, | |||
"/repos/{owner}/{repo}/issues/{index}": { | |||
"get": { | |||
"produces": [ | |||
"application/json" | |||
], | |||
"tags": [ | |||
"issue" | |||
], | |||
"summary": "Get an issue", | |||
"operationId": "issueGetIssue", | |||
"parameters": [ | |||
{ | |||
"type": "string", | |||
@@ -5126,22 +5544,64 @@ | |||
{ | |||
"type": "integer", | |||
"format": "int64", | |||
"description": "id of the comment to edit", | |||
"name": "id", | |||
"description": "index of the issue to get", | |||
"name": "index", | |||
"in": "path", | |||
"required": true | |||
} | |||
], | |||
"responses": { | |||
"200": { | |||
"$ref": "#/responses/ReactionList" | |||
"$ref": "#/responses/Issue" | |||
}, | |||
"404": { | |||
"$ref": "#/responses/notFound" | |||
} | |||
} | |||
}, | |||
"delete": { | |||
"tags": [ | |||
"issue" | |||
], | |||
"summary": "Delete an issue", | |||
"operationId": "issueDelete", | |||
"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 issue to delete", | |||
"name": "index", | |||
"in": "path", | |||
"required": true | |||
} | |||
], | |||
"responses": { | |||
"204": { | |||
"$ref": "#/responses/empty" | |||
}, | |||
"403": { | |||
"$ref": "#/responses/forbidden" | |||
}, | |||
"404": { | |||
"$ref": "#/responses/notFound" | |||
} | |||
} | |||
}, | |||
"post": { | |||
"patch": { | |||
"consumes": [ | |||
"application/json" | |||
], | |||
@@ -5151,8 +5611,8 @@ | |||
"tags": [ | |||
"issue" | |||
], | |||
"summary": "Add a reaction to a comment of an issue", | |||
"operationId": "issuePostCommentReaction", | |||
"summary": "Edit an issue. If using deadline only the date will be taken into account, and time of day ignored.", | |||
"operationId": "issueEditIssue", | |||
"parameters": [ | |||
{ | |||
"type": "string", | |||
@@ -5171,34 +5631,81 @@ | |||
{ | |||
"type": "integer", | |||
"format": "int64", | |||
"description": "id of the comment to edit", | |||
"name": "id", | |||
"description": "index of the issue to edit", | |||
"name": "index", | |||
"in": "path", | |||
"required": true | |||
}, | |||
{ | |||
"name": "content", | |||
"name": "body", | |||
"in": "body", | |||
"schema": { | |||
"$ref": "#/definitions/EditReactionOption" | |||
"$ref": "#/definitions/EditIssueOption" | |||
} | |||
} | |||
], | |||
"responses": { | |||
"200": { | |||
"$ref": "#/responses/Reaction" | |||
}, | |||
"201": { | |||
"$ref": "#/responses/Reaction" | |||
"$ref": "#/responses/Issue" | |||
}, | |||
"403": { | |||
"$ref": "#/responses/forbidden" | |||
}, | |||
"404": { | |||
"$ref": "#/responses/notFound" | |||
}, | |||
"412": { | |||
"$ref": "#/responses/error" | |||
} | |||
} | |||
} | |||
}, | |||
"/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" | |||
} | |||
} | |||
}, | |||
"delete": { | |||
"post": { | |||
"consumes": [ | |||
"application/json" | |||
"multipart/form-data" | |||
], | |||
"produces": [ | |||
"application/json" | |||
@@ -5206,8 +5713,8 @@ | |||
"tags": [ | |||
"issue" | |||
], | |||
"summary": "Remove a reaction from a comment of an issue", | |||
"operationId": "issueDeleteCommentReaction", | |||
"summary": "Create an issue attachment", | |||
"operationId": "issueCreateIssueAttachment", | |||
"parameters": [ | |||
{ | |||
"type": "string", | |||
@@ -5226,30 +5733,39 @@ | |||
{ | |||
"type": "integer", | |||
"format": "int64", | |||
"description": "id of the comment to edit", | |||
"name": "id", | |||
"description": "index of the issue", | |||
"name": "index", | |||
"in": "path", | |||
"required": true | |||
}, | |||
{ | |||
"name": "content", | |||
"in": "body", | |||
"schema": { | |||
"$ref": "#/definitions/EditReactionOption" | |||
} | |||
"type": "string", | |||
"description": "name of the attachment", | |||
"name": "name", | |||
"in": "query" | |||
}, | |||
{ | |||
"type": "file", | |||
"description": "attachment to upload", | |||
"name": "attachment", | |||
"in": "formData", | |||
"required": true | |||
} | |||
], | |||
"responses": { | |||
"200": { | |||
"$ref": "#/responses/empty" | |||
"201": { | |||
"$ref": "#/responses/Attachment" | |||
}, | |||
"403": { | |||
"$ref": "#/responses/forbidden" | |||
"400": { | |||
"$ref": "#/responses/error" | |||
}, | |||
"404": { | |||
"$ref": "#/responses/error" | |||
} | |||
} | |||
} | |||
}, | |||
"/repos/{owner}/{repo}/issues/{index}": { | |||
"/repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}": { | |||
"get": { | |||
"produces": [ | |||
"application/json" | |||
@@ -5257,8 +5773,8 @@ | |||
"tags": [ | |||
"issue" | |||
], | |||
"summary": "Get an issue", | |||
"operationId": "issueGetIssue", | |||
"summary": "Get an issue attachment", | |||
"operationId": "issueGetIssueAttachment", | |||
"parameters": [ | |||
{ | |||
"type": "string", | |||
@@ -5277,27 +5793,38 @@ | |||
{ | |||
"type": "integer", | |||
"format": "int64", | |||
"description": "index of the issue to get", | |||
"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/Issue" | |||
"$ref": "#/responses/Attachment" | |||
}, | |||
"404": { | |||
"$ref": "#/responses/notFound" | |||
"$ref": "#/responses/error" | |||
} | |||
} | |||
}, | |||
"delete": { | |||
"produces": [ | |||
"application/json" | |||
], | |||
"tags": [ | |||
"issue" | |||
], | |||
"summary": "Delete an issue", | |||
"operationId": "issueDelete", | |||
"summary": "Delete an issue attachment", | |||
"operationId": "issueDeleteIssueAttachment", | |||
"parameters": [ | |||
{ | |||
"type": "string", | |||
@@ -5316,21 +5843,26 @@ | |||
{ | |||
"type": "integer", | |||
"format": "int64", | |||
"description": "index of issue to delete", | |||
"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" | |||
}, | |||
"403": { | |||
"$ref": "#/responses/forbidden" | |||
}, | |||
"404": { | |||
"$ref": "#/responses/notFound" | |||
"$ref": "#/responses/error" | |||
} | |||
} | |||
}, | |||
@@ -5344,8 +5876,8 @@ | |||
"tags": [ | |||
"issue" | |||
], | |||
"summary": "Edit an issue. If using deadline only the date will be taken into account, and time of day ignored.", | |||
"operationId": "issueEditIssue", | |||
"summary": "Edit an issue attachment", | |||
"operationId": "issueEditIssueAttachment", | |||
"parameters": [ | |||
{ | |||
"type": "string", | |||
@@ -5364,30 +5896,32 @@ | |||
{ | |||
"type": "integer", | |||
"format": "int64", | |||
"description": "index of the issue to edit", | |||
"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/EditIssueOption" | |||
"$ref": "#/definitions/EditAttachmentOptions" | |||
} | |||
} | |||
], | |||
"responses": { | |||
"201": { | |||
"$ref": "#/responses/Issue" | |||
}, | |||
"403": { | |||
"$ref": "#/responses/forbidden" | |||
"$ref": "#/responses/Attachment" | |||
}, | |||
"404": { | |||
"$ref": "#/responses/notFound" | |||
}, | |||
"412": { | |||
"$ref": "#/responses/error" | |||
} | |||
} | |||
@@ -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" | |||
}, |
@@ -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}) | |||
} |
@@ -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}) | |||
} |