gitea源码

issue_attachment.go 10KB

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