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>
This commit is contained in:
		
					parent
					
						
							
								8fb1e53ca2
							
						
					
				
			
			
				commit
				
					
						3c59d31bc6
					
				
			
		
					 21 changed files with 1754 additions and 84 deletions
				
			
		|  | @ -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 | ||||
| 		} | ||||
| 	} | ||||
|  |  | |||
							
								
								
									
										30
									
								
								modules/convert/attachment.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								modules/convert/attachment.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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() { | ||||
|  |  | |||
							
								
								
									
										372
									
								
								routers/api/v1/repo/issue_attachment.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										372
									
								
								routers/api/v1/repo/issue_attachment.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										383
									
								
								routers/api/v1/repo/issue_comment_attachment.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										383
									
								
								routers/api/v1/repo/issue_comment_attachment.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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": [ | ||||
|  | @ -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" | ||||
|         }, | ||||
|  |  | |||
							
								
								
									
										154
									
								
								tests/integration/api_comment_attachment_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								tests/integration/api_comment_attachment_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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}) | ||||
| } | ||||
							
								
								
									
										143
									
								
								tests/integration/api_issue_attachment_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								tests/integration/api_issue_attachment_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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}) | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 KN4CK3R
				KN4CK3R