code.gitea.io/gitea@v1.21.7/routers/api/v1/repo/issue_attachment.go (about) 1 // Copyright 2021 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package repo 5 6 import ( 7 "net/http" 8 9 issues_model "code.gitea.io/gitea/models/issues" 10 repo_model "code.gitea.io/gitea/models/repo" 11 "code.gitea.io/gitea/modules/context" 12 "code.gitea.io/gitea/modules/log" 13 "code.gitea.io/gitea/modules/setting" 14 api "code.gitea.io/gitea/modules/structs" 15 "code.gitea.io/gitea/modules/web" 16 "code.gitea.io/gitea/services/attachment" 17 "code.gitea.io/gitea/services/convert" 18 issue_service "code.gitea.io/gitea/services/issue" 19 ) 20 21 // GetIssueAttachment gets a single attachment of the issue 22 func GetIssueAttachment(ctx *context.APIContext) { 23 // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueGetIssueAttachment 24 // --- 25 // summary: Get an issue attachment 26 // produces: 27 // - application/json 28 // parameters: 29 // - name: owner 30 // in: path 31 // description: owner of the repo 32 // type: string 33 // required: true 34 // - name: repo 35 // in: path 36 // description: name of the repo 37 // type: string 38 // required: true 39 // - name: index 40 // in: path 41 // description: index of the issue 42 // type: integer 43 // format: int64 44 // required: true 45 // - name: attachment_id 46 // in: path 47 // description: id of the attachment to get 48 // type: integer 49 // format: int64 50 // required: true 51 // responses: 52 // "200": 53 // "$ref": "#/responses/Attachment" 54 // "404": 55 // "$ref": "#/responses/error" 56 57 issue := getIssueFromContext(ctx) 58 if issue == nil { 59 return 60 } 61 62 attach := getIssueAttachmentSafeRead(ctx, issue) 63 if attach == nil { 64 return 65 } 66 67 ctx.JSON(http.StatusOK, convert.ToAPIAttachment(ctx.Repo.Repository, attach)) 68 } 69 70 // ListIssueAttachments lists all attachments of the issue 71 func ListIssueAttachments(ctx *context.APIContext) { 72 // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assets issue issueListIssueAttachments 73 // --- 74 // summary: List issue's attachments 75 // produces: 76 // - application/json 77 // parameters: 78 // - name: owner 79 // in: path 80 // description: owner of the repo 81 // type: string 82 // required: true 83 // - name: repo 84 // in: path 85 // description: name of the repo 86 // type: string 87 // required: true 88 // - name: index 89 // in: path 90 // description: index of the issue 91 // type: integer 92 // format: int64 93 // required: true 94 // responses: 95 // "200": 96 // "$ref": "#/responses/AttachmentList" 97 // "404": 98 // "$ref": "#/responses/error" 99 100 issue := getIssueFromContext(ctx) 101 if issue == nil { 102 return 103 } 104 105 if err := issue.LoadAttributes(ctx); err != nil { 106 ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) 107 return 108 } 109 110 ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue).Attachments) 111 } 112 113 // CreateIssueAttachment creates an attachment and saves the given file 114 func CreateIssueAttachment(ctx *context.APIContext) { 115 // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/assets issue issueCreateIssueAttachment 116 // --- 117 // summary: Create an issue attachment 118 // produces: 119 // - application/json 120 // consumes: 121 // - multipart/form-data 122 // parameters: 123 // - name: owner 124 // in: path 125 // description: owner of the repo 126 // type: string 127 // required: true 128 // - name: repo 129 // in: path 130 // description: name of the repo 131 // type: string 132 // required: true 133 // - name: index 134 // in: path 135 // description: index of the issue 136 // type: integer 137 // format: int64 138 // required: true 139 // - name: name 140 // in: query 141 // description: name of the attachment 142 // type: string 143 // required: false 144 // - name: attachment 145 // in: formData 146 // description: attachment to upload 147 // type: file 148 // required: true 149 // responses: 150 // "201": 151 // "$ref": "#/responses/Attachment" 152 // "400": 153 // "$ref": "#/responses/error" 154 // "404": 155 // "$ref": "#/responses/error" 156 157 issue := getIssueFromContext(ctx) 158 if issue == nil { 159 return 160 } 161 162 if !canUserWriteIssueAttachment(ctx, issue) { 163 return 164 } 165 166 // Get uploaded file from request 167 file, header, err := ctx.Req.FormFile("attachment") 168 if err != nil { 169 ctx.Error(http.StatusInternalServerError, "FormFile", err) 170 return 171 } 172 defer file.Close() 173 174 filename := header.Filename 175 if query := ctx.FormString("name"); query != "" { 176 filename = query 177 } 178 179 attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{ 180 Name: filename, 181 UploaderID: ctx.Doer.ID, 182 RepoID: ctx.Repo.Repository.ID, 183 IssueID: issue.ID, 184 }) 185 if err != nil { 186 ctx.Error(http.StatusInternalServerError, "UploadAttachment", err) 187 return 188 } 189 190 issue.Attachments = append(issue.Attachments, attachment) 191 192 if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, issue.Content); err != nil { 193 ctx.Error(http.StatusInternalServerError, "ChangeContent", err) 194 return 195 } 196 197 ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attachment)) 198 } 199 200 // EditIssueAttachment updates the given attachment 201 func EditIssueAttachment(ctx *context.APIContext) { 202 // swagger:operation PATCH /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueEditIssueAttachment 203 // --- 204 // summary: Edit an issue attachment 205 // produces: 206 // - application/json 207 // consumes: 208 // - application/json 209 // parameters: 210 // - name: owner 211 // in: path 212 // description: owner of the repo 213 // type: string 214 // required: true 215 // - name: repo 216 // in: path 217 // description: name of the repo 218 // type: string 219 // required: true 220 // - name: index 221 // in: path 222 // description: index of the issue 223 // type: integer 224 // format: int64 225 // required: true 226 // - name: attachment_id 227 // in: path 228 // description: id of the attachment to edit 229 // type: integer 230 // format: int64 231 // required: true 232 // - name: body 233 // in: body 234 // schema: 235 // "$ref": "#/definitions/EditAttachmentOptions" 236 // responses: 237 // "201": 238 // "$ref": "#/responses/Attachment" 239 // "404": 240 // "$ref": "#/responses/error" 241 242 attachment := getIssueAttachmentSafeWrite(ctx) 243 if attachment == nil { 244 return 245 } 246 247 // do changes to attachment. only meaningful change is name. 248 form := web.GetForm(ctx).(*api.EditAttachmentOptions) 249 if form.Name != "" { 250 attachment.Name = form.Name 251 } 252 253 if err := repo_model.UpdateAttachment(ctx, attachment); err != nil { 254 ctx.Error(http.StatusInternalServerError, "UpdateAttachment", err) 255 } 256 257 ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attachment)) 258 } 259 260 // DeleteIssueAttachment delete a given attachment 261 func DeleteIssueAttachment(ctx *context.APIContext) { 262 // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueDeleteIssueAttachment 263 // --- 264 // summary: Delete an issue attachment 265 // produces: 266 // - application/json 267 // parameters: 268 // - name: owner 269 // in: path 270 // description: owner of the repo 271 // type: string 272 // required: true 273 // - name: repo 274 // in: path 275 // description: name of the repo 276 // type: string 277 // required: true 278 // - name: index 279 // in: path 280 // description: index of the issue 281 // type: integer 282 // format: int64 283 // required: true 284 // - name: attachment_id 285 // in: path 286 // description: id of the attachment to delete 287 // type: integer 288 // format: int64 289 // required: true 290 // responses: 291 // "204": 292 // "$ref": "#/responses/empty" 293 // "404": 294 // "$ref": "#/responses/error" 295 296 attachment := getIssueAttachmentSafeWrite(ctx) 297 if attachment == nil { 298 return 299 } 300 301 if err := repo_model.DeleteAttachment(ctx, attachment, true); err != nil { 302 ctx.Error(http.StatusInternalServerError, "DeleteAttachment", err) 303 return 304 } 305 306 ctx.Status(http.StatusNoContent) 307 } 308 309 func getIssueFromContext(ctx *context.APIContext) *issues_model.Issue { 310 issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64("index")) 311 if err != nil { 312 ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err) 313 return nil 314 } 315 316 issue.Repo = ctx.Repo.Repository 317 318 return issue 319 } 320 321 func getIssueAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment { 322 issue := getIssueFromContext(ctx) 323 if issue == nil { 324 return nil 325 } 326 327 if !canUserWriteIssueAttachment(ctx, issue) { 328 return nil 329 } 330 331 return getIssueAttachmentSafeRead(ctx, issue) 332 } 333 334 func getIssueAttachmentSafeRead(ctx *context.APIContext, issue *issues_model.Issue) *repo_model.Attachment { 335 attachment, err := repo_model.GetAttachmentByID(ctx, ctx.ParamsInt64("attachment_id")) 336 if err != nil { 337 ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err) 338 return nil 339 } 340 if !attachmentBelongsToRepoOrIssue(ctx, attachment, issue) { 341 return nil 342 } 343 return attachment 344 } 345 346 func canUserWriteIssueAttachment(ctx *context.APIContext, issue *issues_model.Issue) bool { 347 canEditIssue := ctx.IsSigned && (ctx.Doer.ID == issue.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) 348 if !canEditIssue { 349 ctx.Error(http.StatusForbidden, "", "user should have permission to write issue") 350 return false 351 } 352 353 return true 354 } 355 356 func attachmentBelongsToRepoOrIssue(ctx *context.APIContext, attachment *repo_model.Attachment, issue *issues_model.Issue) bool { 357 if attachment.RepoID != ctx.Repo.Repository.ID { 358 log.Debug("Requested attachment[%d] does not belong to repo[%-v].", attachment.ID, ctx.Repo.Repository) 359 ctx.NotFound("no such attachment in repo") 360 return false 361 } 362 if attachment.IssueID == 0 { 363 log.Debug("Requested attachment[%d] is not in an issue.", attachment.ID) 364 ctx.NotFound("no such attachment in issue") 365 return false 366 } else if issue != nil && attachment.IssueID != issue.ID { 367 log.Debug("Requested attachment[%d] does not belong to issue[%d, #%d].", attachment.ID, issue.ID, issue.Index) 368 ctx.NotFound("no such attachment in issue") 369 return false 370 } 371 return true 372 }