gitea源码

file.go 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package repo
  5. import (
  6. "bytes"
  7. "encoding/base64"
  8. "errors"
  9. "fmt"
  10. "io"
  11. "net/http"
  12. "strings"
  13. "time"
  14. git_model "code.gitea.io/gitea/models/git"
  15. "code.gitea.io/gitea/modules/git"
  16. "code.gitea.io/gitea/modules/httpcache"
  17. "code.gitea.io/gitea/modules/json"
  18. "code.gitea.io/gitea/modules/lfs"
  19. "code.gitea.io/gitea/modules/log"
  20. "code.gitea.io/gitea/modules/setting"
  21. "code.gitea.io/gitea/modules/storage"
  22. api "code.gitea.io/gitea/modules/structs"
  23. "code.gitea.io/gitea/modules/util"
  24. "code.gitea.io/gitea/modules/web"
  25. "code.gitea.io/gitea/routers/api/v1/utils"
  26. "code.gitea.io/gitea/routers/common"
  27. "code.gitea.io/gitea/services/context"
  28. pull_service "code.gitea.io/gitea/services/pull"
  29. files_service "code.gitea.io/gitea/services/repository/files"
  30. )
  31. const giteaObjectTypeHeader = "X-Gitea-Object-Type"
  32. // GetRawFile get a file by path on a repository
  33. func GetRawFile(ctx *context.APIContext) {
  34. // swagger:operation GET /repos/{owner}/{repo}/raw/{filepath} repository repoGetRawFile
  35. // ---
  36. // summary: Get a file from a repository
  37. // produces:
  38. // - application/octet-stream
  39. // parameters:
  40. // - name: owner
  41. // in: path
  42. // description: owner of the repo
  43. // type: string
  44. // required: true
  45. // - name: repo
  46. // in: path
  47. // description: name of the repo
  48. // type: string
  49. // required: true
  50. // - name: filepath
  51. // in: path
  52. // description: path of the file to get, it should be "{ref}/{filepath}". If there is no ref could be inferred, it will be treated as the default branch
  53. // type: string
  54. // required: true
  55. // - name: ref
  56. // in: query
  57. // description: "The name of the commit/branch/tag. Default to the repository’s default branch"
  58. // type: string
  59. // required: false
  60. // responses:
  61. // 200:
  62. // description: Returns raw file content.
  63. // schema:
  64. // type: file
  65. // "404":
  66. // "$ref": "#/responses/notFound"
  67. if ctx.Repo.Repository.IsEmpty {
  68. ctx.APIErrorNotFound()
  69. return
  70. }
  71. blob, entry, lastModified := getBlobForEntry(ctx)
  72. if ctx.Written() {
  73. return
  74. }
  75. ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
  76. if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil {
  77. ctx.APIErrorInternal(err)
  78. }
  79. }
  80. // GetRawFileOrLFS get a file by repo's path, redirecting to LFS if necessary.
  81. func GetRawFileOrLFS(ctx *context.APIContext) {
  82. // swagger:operation GET /repos/{owner}/{repo}/media/{filepath} repository repoGetRawFileOrLFS
  83. // ---
  84. // summary: Get a file or it's LFS object from a repository
  85. // produces:
  86. // - application/octet-stream
  87. // parameters:
  88. // - name: owner
  89. // in: path
  90. // description: owner of the repo
  91. // type: string
  92. // required: true
  93. // - name: repo
  94. // in: path
  95. // description: name of the repo
  96. // type: string
  97. // required: true
  98. // - name: filepath
  99. // in: path
  100. // description: path of the file to get, it should be "{ref}/{filepath}". If there is no ref could be inferred, it will be treated as the default branch
  101. // type: string
  102. // required: true
  103. // - name: ref
  104. // in: query
  105. // description: "The name of the commit/branch/tag. Default to the repository’s default branch"
  106. // type: string
  107. // required: false
  108. // responses:
  109. // 200:
  110. // description: Returns raw file content.
  111. // schema:
  112. // type: file
  113. // "404":
  114. // "$ref": "#/responses/notFound"
  115. if ctx.Repo.Repository.IsEmpty {
  116. ctx.APIErrorNotFound()
  117. return
  118. }
  119. blob, entry, lastModified := getBlobForEntry(ctx)
  120. if ctx.Written() {
  121. return
  122. }
  123. ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
  124. // LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file
  125. if blob.Size() > lfs.MetaFileMaxSize {
  126. // First handle caching for the blob
  127. if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
  128. return
  129. }
  130. // If not cached - serve!
  131. if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil {
  132. ctx.APIErrorInternal(err)
  133. }
  134. return
  135. }
  136. // OK, now the blob is known to have at most 1024 (lfs pointer max size) bytes,
  137. // we can simply read this in one go (This saves reading it twice)
  138. dataRc, err := blob.DataAsync()
  139. if err != nil {
  140. ctx.APIErrorInternal(err)
  141. return
  142. }
  143. buf, err := io.ReadAll(dataRc)
  144. if err != nil {
  145. _ = dataRc.Close()
  146. ctx.APIErrorInternal(err)
  147. return
  148. }
  149. if err := dataRc.Close(); err != nil {
  150. log.Error("Error whilst closing blob %s reader in %-v. Error: %v", blob.ID, ctx.Repo.Repository, err)
  151. }
  152. // Check if the blob represents a pointer
  153. pointer, _ := lfs.ReadPointer(bytes.NewReader(buf))
  154. // if it's not a pointer, just serve the data directly
  155. if !pointer.IsValid() {
  156. // First handle caching for the blob
  157. if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
  158. return
  159. }
  160. // If not cached - serve!
  161. common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
  162. return
  163. }
  164. // Now check if there is a MetaObject for this pointer
  165. meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid)
  166. // If there isn't one, just serve the data directly
  167. if errors.Is(err, git_model.ErrLFSObjectNotExist) {
  168. // Handle caching for the blob SHA (not the LFS object OID)
  169. if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
  170. return
  171. }
  172. common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
  173. return
  174. } else if err != nil {
  175. ctx.APIErrorInternal(err)
  176. return
  177. }
  178. // Handle caching for the LFS object OID
  179. if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
  180. return
  181. }
  182. if setting.LFS.Storage.ServeDirect() {
  183. // If we have a signed url (S3, object storage), redirect to this directly.
  184. u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name(), ctx.Req.Method, nil)
  185. if u != nil && err == nil {
  186. ctx.Redirect(u.String())
  187. return
  188. }
  189. }
  190. lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer)
  191. if err != nil {
  192. ctx.APIErrorInternal(err)
  193. return
  194. }
  195. defer lfsDataRc.Close()
  196. common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc)
  197. }
  198. func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) {
  199. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
  200. if err != nil {
  201. if git.IsErrNotExist(err) {
  202. ctx.APIErrorNotFound()
  203. } else {
  204. ctx.APIErrorInternal(err)
  205. }
  206. return nil, nil, nil
  207. }
  208. if entry.IsDir() || entry.IsSubModule() {
  209. ctx.APIErrorNotFound("getBlobForEntry", nil)
  210. return nil, nil, nil
  211. }
  212. latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath)
  213. if err != nil {
  214. ctx.APIErrorInternal(err)
  215. return nil, nil, nil
  216. }
  217. when := &latestCommit.Committer.When
  218. return entry.Blob(), entry, when
  219. }
  220. // GetArchive get archive of a repository
  221. func GetArchive(ctx *context.APIContext) {
  222. // swagger:operation GET /repos/{owner}/{repo}/archive/{archive} repository repoGetArchive
  223. // ---
  224. // summary: Get an archive of a repository
  225. // produces:
  226. // - application/json
  227. // parameters:
  228. // - name: owner
  229. // in: path
  230. // description: owner of the repo
  231. // type: string
  232. // required: true
  233. // - name: repo
  234. // in: path
  235. // description: name of the repo
  236. // type: string
  237. // required: true
  238. // - name: archive
  239. // in: path
  240. // description: the git reference for download with attached archive format (e.g. master.zip)
  241. // type: string
  242. // required: true
  243. // responses:
  244. // 200:
  245. // description: success
  246. // "404":
  247. // "$ref": "#/responses/notFound"
  248. serveRepoArchive(ctx, ctx.PathParam("*"))
  249. }
  250. // GetEditorconfig get editor config of a repository
  251. func GetEditorconfig(ctx *context.APIContext) {
  252. // swagger:operation GET /repos/{owner}/{repo}/editorconfig/{filepath} repository repoGetEditorConfig
  253. // ---
  254. // summary: Get the EditorConfig definitions of a file in a repository
  255. // produces:
  256. // - application/json
  257. // parameters:
  258. // - name: owner
  259. // in: path
  260. // description: owner of the repo
  261. // type: string
  262. // required: true
  263. // - name: repo
  264. // in: path
  265. // description: name of the repo
  266. // type: string
  267. // required: true
  268. // - name: filepath
  269. // in: path
  270. // description: filepath of file to get
  271. // type: string
  272. // required: true
  273. // - name: ref
  274. // in: query
  275. // description: "The name of the commit/branch/tag. Default to the repository’s default branch."
  276. // type: string
  277. // required: false
  278. // responses:
  279. // 200:
  280. // description: success
  281. // "404":
  282. // "$ref": "#/responses/notFound"
  283. ec, _, err := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
  284. if err != nil {
  285. if git.IsErrNotExist(err) {
  286. ctx.APIErrorNotFound(err)
  287. } else {
  288. ctx.APIErrorInternal(err)
  289. }
  290. return
  291. }
  292. fileName := ctx.PathParam("filename")
  293. def, err := ec.GetDefinitionForFilename(fileName)
  294. if def == nil {
  295. ctx.APIErrorNotFound(err)
  296. return
  297. }
  298. ctx.JSON(http.StatusOK, def)
  299. }
  300. func base64Reader(s string) (io.ReadSeeker, error) {
  301. b, err := base64.StdEncoding.DecodeString(s)
  302. if err != nil {
  303. return nil, err
  304. }
  305. return bytes.NewReader(b), nil
  306. }
  307. func ReqChangeRepoFileOptionsAndCheck(ctx *context.APIContext) {
  308. commonOpts := web.GetForm(ctx).(api.FileOptionsInterface).GetFileOptions()
  309. commonOpts.BranchName = util.IfZero(commonOpts.BranchName, ctx.Repo.Repository.DefaultBranch)
  310. commonOpts.NewBranchName = util.IfZero(commonOpts.NewBranchName, commonOpts.BranchName)
  311. if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, commonOpts.NewBranchName) && !ctx.IsUserSiteAdmin() {
  312. ctx.APIError(http.StatusForbidden, "user should have a permission to write to the target branch")
  313. return
  314. }
  315. changeFileOpts := &files_service.ChangeRepoFilesOptions{
  316. Message: commonOpts.Message,
  317. OldBranch: commonOpts.BranchName,
  318. NewBranch: commonOpts.NewBranchName,
  319. Committer: &files_service.IdentityOptions{
  320. GitUserName: commonOpts.Committer.Name,
  321. GitUserEmail: commonOpts.Committer.Email,
  322. },
  323. Author: &files_service.IdentityOptions{
  324. GitUserName: commonOpts.Author.Name,
  325. GitUserEmail: commonOpts.Author.Email,
  326. },
  327. Dates: &files_service.CommitDateOptions{
  328. Author: commonOpts.Dates.Author,
  329. Committer: commonOpts.Dates.Committer,
  330. },
  331. Signoff: commonOpts.Signoff,
  332. }
  333. if commonOpts.Dates.Author.IsZero() {
  334. commonOpts.Dates.Author = time.Now()
  335. }
  336. if commonOpts.Dates.Committer.IsZero() {
  337. commonOpts.Dates.Committer = time.Now()
  338. }
  339. ctx.Data["__APIChangeRepoFilesOptions"] = changeFileOpts
  340. }
  341. func getAPIChangeRepoFileOptions[T api.FileOptionsInterface](ctx *context.APIContext) (apiOpts T, opts *files_service.ChangeRepoFilesOptions) {
  342. return web.GetForm(ctx).(T), ctx.Data["__APIChangeRepoFilesOptions"].(*files_service.ChangeRepoFilesOptions)
  343. }
  344. // ChangeFiles handles API call for modifying multiple files
  345. func ChangeFiles(ctx *context.APIContext) {
  346. // swagger:operation POST /repos/{owner}/{repo}/contents repository repoChangeFiles
  347. // ---
  348. // summary: Modify multiple files in a repository
  349. // consumes:
  350. // - application/json
  351. // produces:
  352. // - application/json
  353. // parameters:
  354. // - name: owner
  355. // in: path
  356. // description: owner of the repo
  357. // type: string
  358. // required: true
  359. // - name: repo
  360. // in: path
  361. // description: name of the repo
  362. // type: string
  363. // required: true
  364. // - name: body
  365. // in: body
  366. // required: true
  367. // schema:
  368. // "$ref": "#/definitions/ChangeFilesOptions"
  369. // responses:
  370. // "201":
  371. // "$ref": "#/responses/FilesResponse"
  372. // "403":
  373. // "$ref": "#/responses/error"
  374. // "404":
  375. // "$ref": "#/responses/notFound"
  376. // "422":
  377. // "$ref": "#/responses/error"
  378. // "423":
  379. // "$ref": "#/responses/repoArchivedError"
  380. apiOpts, opts := getAPIChangeRepoFileOptions[*api.ChangeFilesOptions](ctx)
  381. if ctx.Written() {
  382. return
  383. }
  384. for _, file := range apiOpts.Files {
  385. contentReader, err := base64Reader(file.ContentBase64)
  386. if err != nil {
  387. ctx.APIError(http.StatusUnprocessableEntity, err)
  388. return
  389. }
  390. // FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options
  391. // But the LastCommitID is not provided in the API options, need to fully fix them in API
  392. changeRepoFile := &files_service.ChangeRepoFile{
  393. Operation: file.Operation,
  394. TreePath: file.Path,
  395. FromTreePath: file.FromPath,
  396. ContentReader: contentReader,
  397. SHA: file.SHA,
  398. }
  399. opts.Files = append(opts.Files, changeRepoFile)
  400. }
  401. if opts.Message == "" {
  402. opts.Message = changeFilesCommitMessage(ctx, opts.Files)
  403. }
  404. if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
  405. handleChangeRepoFilesError(ctx, err)
  406. } else {
  407. ctx.JSON(http.StatusCreated, filesResponse)
  408. }
  409. }
  410. // CreateFile handles API call for creating a file
  411. func CreateFile(ctx *context.APIContext) {
  412. // swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
  413. // ---
  414. // summary: Create a file in a repository
  415. // consumes:
  416. // - application/json
  417. // produces:
  418. // - application/json
  419. // parameters:
  420. // - name: owner
  421. // in: path
  422. // description: owner of the repo
  423. // type: string
  424. // required: true
  425. // - name: repo
  426. // in: path
  427. // description: name of the repo
  428. // type: string
  429. // required: true
  430. // - name: filepath
  431. // in: path
  432. // description: path of the file to create
  433. // type: string
  434. // required: true
  435. // - name: body
  436. // in: body
  437. // required: true
  438. // schema:
  439. // "$ref": "#/definitions/CreateFileOptions"
  440. // responses:
  441. // "201":
  442. // "$ref": "#/responses/FileResponse"
  443. // "403":
  444. // "$ref": "#/responses/error"
  445. // "404":
  446. // "$ref": "#/responses/notFound"
  447. // "422":
  448. // "$ref": "#/responses/error"
  449. // "423":
  450. // "$ref": "#/responses/repoArchivedError"
  451. apiOpts, opts := getAPIChangeRepoFileOptions[*api.CreateFileOptions](ctx)
  452. if ctx.Written() {
  453. return
  454. }
  455. contentReader, err := base64Reader(apiOpts.ContentBase64)
  456. if err != nil {
  457. ctx.APIError(http.StatusUnprocessableEntity, err)
  458. return
  459. }
  460. opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
  461. Operation: "create",
  462. TreePath: ctx.PathParam("*"),
  463. ContentReader: contentReader,
  464. })
  465. if opts.Message == "" {
  466. opts.Message = changeFilesCommitMessage(ctx, opts.Files)
  467. }
  468. if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
  469. handleChangeRepoFilesError(ctx, err)
  470. } else {
  471. fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
  472. ctx.JSON(http.StatusCreated, fileResponse)
  473. }
  474. }
  475. // UpdateFile handles API call for updating a file
  476. func UpdateFile(ctx *context.APIContext) {
  477. // swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
  478. // ---
  479. // summary: Update a file in a repository
  480. // consumes:
  481. // - application/json
  482. // produces:
  483. // - application/json
  484. // parameters:
  485. // - name: owner
  486. // in: path
  487. // description: owner of the repo
  488. // type: string
  489. // required: true
  490. // - name: repo
  491. // in: path
  492. // description: name of the repo
  493. // type: string
  494. // required: true
  495. // - name: filepath
  496. // in: path
  497. // description: path of the file to update
  498. // type: string
  499. // required: true
  500. // - name: body
  501. // in: body
  502. // required: true
  503. // schema:
  504. // "$ref": "#/definitions/UpdateFileOptions"
  505. // responses:
  506. // "200":
  507. // "$ref": "#/responses/FileResponse"
  508. // "403":
  509. // "$ref": "#/responses/error"
  510. // "404":
  511. // "$ref": "#/responses/notFound"
  512. // "422":
  513. // "$ref": "#/responses/error"
  514. // "423":
  515. // "$ref": "#/responses/repoArchivedError"
  516. apiOpts, opts := getAPIChangeRepoFileOptions[*api.UpdateFileOptions](ctx)
  517. if ctx.Written() {
  518. return
  519. }
  520. contentReader, err := base64Reader(apiOpts.ContentBase64)
  521. if err != nil {
  522. ctx.APIError(http.StatusUnprocessableEntity, err)
  523. return
  524. }
  525. opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
  526. Operation: "update",
  527. ContentReader: contentReader,
  528. SHA: apiOpts.SHA,
  529. FromTreePath: apiOpts.FromPath,
  530. TreePath: ctx.PathParam("*"),
  531. })
  532. if opts.Message == "" {
  533. opts.Message = changeFilesCommitMessage(ctx, opts.Files)
  534. }
  535. if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
  536. handleChangeRepoFilesError(ctx, err)
  537. } else {
  538. fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
  539. ctx.JSON(http.StatusOK, fileResponse)
  540. }
  541. }
  542. func handleChangeRepoFilesError(ctx *context.APIContext, err error) {
  543. if files_service.IsErrUserCannotCommit(err) || pull_service.IsErrFilePathProtected(err) {
  544. ctx.APIError(http.StatusForbidden, err)
  545. return
  546. }
  547. if git_model.IsErrBranchAlreadyExists(err) || files_service.IsErrFilenameInvalid(err) || pull_service.IsErrSHADoesNotMatch(err) ||
  548. files_service.IsErrFilePathInvalid(err) || files_service.IsErrRepoFileAlreadyExists(err) ||
  549. files_service.IsErrCommitIDDoesNotMatch(err) || files_service.IsErrSHAOrCommitIDNotProvided(err) {
  550. ctx.APIError(http.StatusUnprocessableEntity, err)
  551. return
  552. }
  553. if git.IsErrBranchNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
  554. ctx.APIError(http.StatusNotFound, err)
  555. return
  556. }
  557. if errors.Is(err, util.ErrNotExist) {
  558. ctx.APIError(http.StatusNotFound, err)
  559. return
  560. }
  561. ctx.APIErrorInternal(err)
  562. }
  563. // format commit message if empty
  564. func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.ChangeRepoFile) string {
  565. var (
  566. createFiles []string
  567. updateFiles []string
  568. deleteFiles []string
  569. )
  570. for _, file := range files {
  571. switch file.Operation {
  572. case "create":
  573. createFiles = append(createFiles, file.TreePath)
  574. case "update", "upload", "rename": // upload and rename works like "update", there is no translation for them at the moment
  575. updateFiles = append(updateFiles, file.TreePath)
  576. case "delete":
  577. deleteFiles = append(deleteFiles, file.TreePath)
  578. }
  579. }
  580. message := ""
  581. if len(createFiles) != 0 {
  582. message += ctx.Locale.TrString("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
  583. }
  584. if len(updateFiles) != 0 {
  585. message += ctx.Locale.TrString("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
  586. }
  587. if len(deleteFiles) != 0 {
  588. message += ctx.Locale.TrString("repo.editor.delete", strings.Join(deleteFiles, ", "))
  589. }
  590. return strings.Trim(message, "\n")
  591. }
  592. // DeleteFile Delete a file in a repository
  593. func DeleteFile(ctx *context.APIContext) {
  594. // swagger:operation DELETE /repos/{owner}/{repo}/contents/{filepath} repository repoDeleteFile
  595. // ---
  596. // summary: Delete a file in a repository
  597. // consumes:
  598. // - application/json
  599. // produces:
  600. // - application/json
  601. // parameters:
  602. // - name: owner
  603. // in: path
  604. // description: owner of the repo
  605. // type: string
  606. // required: true
  607. // - name: repo
  608. // in: path
  609. // description: name of the repo
  610. // type: string
  611. // required: true
  612. // - name: filepath
  613. // in: path
  614. // description: path of the file to delete
  615. // type: string
  616. // required: true
  617. // - name: body
  618. // in: body
  619. // required: true
  620. // schema:
  621. // "$ref": "#/definitions/DeleteFileOptions"
  622. // responses:
  623. // "200":
  624. // "$ref": "#/responses/FileDeleteResponse"
  625. // "400":
  626. // "$ref": "#/responses/error"
  627. // "403":
  628. // "$ref": "#/responses/error"
  629. // "404":
  630. // "$ref": "#/responses/error"
  631. // "422":
  632. // "$ref": "#/responses/error"
  633. // "423":
  634. // "$ref": "#/responses/repoArchivedError"
  635. apiOpts, opts := getAPIChangeRepoFileOptions[*api.DeleteFileOptions](ctx)
  636. if ctx.Written() {
  637. return
  638. }
  639. opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
  640. Operation: "delete",
  641. SHA: apiOpts.SHA,
  642. TreePath: ctx.PathParam("*"),
  643. })
  644. if opts.Message == "" {
  645. opts.Message = changeFilesCommitMessage(ctx, opts.Files)
  646. }
  647. if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
  648. handleChangeRepoFilesError(ctx, err)
  649. } else {
  650. fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
  651. ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
  652. }
  653. }
  654. func resolveRefCommit(ctx *context.APIContext, ref string, minCommitIDLen ...int) *utils.RefCommit {
  655. ref = util.IfZero(ref, ctx.Repo.Repository.DefaultBranch)
  656. refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ref, minCommitIDLen...)
  657. if errors.Is(err, util.ErrNotExist) {
  658. ctx.APIErrorNotFound(err)
  659. } else if err != nil {
  660. ctx.APIErrorInternal(err)
  661. }
  662. return refCommit
  663. }
  664. func GetContentsExt(ctx *context.APIContext) {
  665. // swagger:operation GET /repos/{owner}/{repo}/contents-ext/{filepath} repository repoGetContentsExt
  666. // ---
  667. // summary: The extended "contents" API, to get file metadata and/or content, or list a directory.
  668. // description: It guarantees that only one of the response fields is set if the request succeeds.
  669. // Users can pass "includes=file_content" or "includes=lfs_metadata" to retrieve more fields.
  670. // "includes=file_content" only works for single file, if you need to retrieve file contents in batch,
  671. // use "file-contents" API after listing the directory.
  672. // produces:
  673. // - application/json
  674. // parameters:
  675. // - name: owner
  676. // in: path
  677. // description: owner of the repo
  678. // type: string
  679. // required: true
  680. // - name: repo
  681. // in: path
  682. // description: name of the repo
  683. // type: string
  684. // required: true
  685. // - name: filepath
  686. // in: path
  687. // description: path of the dir, file, symlink or submodule in the repo. Swagger requires path parameter to be "required",
  688. // you can leave it empty or pass a single dot (".") to get the root directory.
  689. // type: string
  690. // required: true
  691. // - name: ref
  692. // in: query
  693. // description: the name of the commit/branch/tag, default to the repository’s default branch.
  694. // type: string
  695. // required: false
  696. // - name: includes
  697. // in: query
  698. // description: By default this API's response only contains file's metadata. Use comma-separated "includes" options to retrieve more fields.
  699. // Option "file_content" will try to retrieve the file content, "lfs_metadata" will try to retrieve LFS metadata,
  700. // "commit_metadata" will try to retrieve commit metadata, and "commit_message" will try to retrieve commit message.
  701. // type: string
  702. // required: false
  703. // responses:
  704. // "200":
  705. // "$ref": "#/responses/ContentsExtResponse"
  706. // "404":
  707. // "$ref": "#/responses/notFound"
  708. if treePath := ctx.PathParam("*"); treePath == "." || treePath == "/" {
  709. ctx.SetPathParam("*", "") // workaround for swagger, it requires path parameter to be "required", but we need to list root directory
  710. }
  711. opts := files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*")}
  712. for includeOpt := range strings.SplitSeq(ctx.FormString("includes"), ",") {
  713. if includeOpt == "" {
  714. continue
  715. }
  716. switch includeOpt {
  717. case "file_content":
  718. opts.IncludeSingleFileContent = true
  719. case "lfs_metadata":
  720. opts.IncludeLfsMetadata = true
  721. case "commit_metadata":
  722. opts.IncludeCommitMetadata = true
  723. case "commit_message":
  724. opts.IncludeCommitMessage = true
  725. default:
  726. ctx.APIError(http.StatusBadRequest, fmt.Sprintf("unknown include option %q", includeOpt))
  727. return
  728. }
  729. }
  730. ctx.JSON(http.StatusOK, getRepoContents(ctx, opts))
  731. }
  732. func GetContents(ctx *context.APIContext) {
  733. // swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents
  734. // ---
  735. // summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir.
  736. // description: This API follows GitHub's design, and it is not easy to use. Recommend users to use the "contents-ext" API instead.
  737. // produces:
  738. // - application/json
  739. // parameters:
  740. // - name: owner
  741. // in: path
  742. // description: owner of the repo
  743. // type: string
  744. // required: true
  745. // - name: repo
  746. // in: path
  747. // description: name of the repo
  748. // type: string
  749. // required: true
  750. // - name: filepath
  751. // in: path
  752. // description: path of the dir, file, symlink or submodule in the repo
  753. // type: string
  754. // required: true
  755. // - name: ref
  756. // in: query
  757. // description: "The name of the commit/branch/tag. Default to the repository’s default branch."
  758. // type: string
  759. // required: false
  760. // responses:
  761. // "200":
  762. // "$ref": "#/responses/ContentsResponse"
  763. // "404":
  764. // "$ref": "#/responses/notFound"
  765. ret := getRepoContents(ctx, files_service.GetContentsOrListOptions{
  766. TreePath: ctx.PathParam("*"),
  767. IncludeSingleFileContent: true,
  768. IncludeCommitMetadata: true,
  769. })
  770. if ctx.Written() {
  771. return
  772. }
  773. ctx.JSON(http.StatusOK, util.Iif[any](ret.FileContents != nil, ret.FileContents, ret.DirContents))
  774. }
  775. func getRepoContents(ctx *context.APIContext, opts files_service.GetContentsOrListOptions) *api.ContentsExtResponse {
  776. refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref"))
  777. if ctx.Written() {
  778. return nil
  779. }
  780. ret, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts)
  781. if err != nil {
  782. if git.IsErrNotExist(err) {
  783. ctx.APIErrorNotFound("GetContentsOrList", err)
  784. return nil
  785. }
  786. ctx.APIErrorInternal(err)
  787. }
  788. return &ret
  789. }
  790. func GetContentsList(ctx *context.APIContext) {
  791. // swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList
  792. // ---
  793. // summary: Gets the metadata of all the entries of the root dir.
  794. // description: This API follows GitHub's design, and it is not easy to use. Recommend users to use our "contents-ext" API instead.
  795. // produces:
  796. // - application/json
  797. // parameters:
  798. // - name: owner
  799. // in: path
  800. // description: owner of the repo
  801. // type: string
  802. // required: true
  803. // - name: repo
  804. // in: path
  805. // description: name of the repo
  806. // type: string
  807. // required: true
  808. // - name: ref
  809. // in: query
  810. // description: "The name of the commit/branch/tag. Default to the repository’s default branch."
  811. // type: string
  812. // required: false
  813. // responses:
  814. // "200":
  815. // "$ref": "#/responses/ContentsListResponse"
  816. // "404":
  817. // "$ref": "#/responses/notFound"
  818. // same as GetContents(), this function is here because swagger fails if path is empty in GetContents() interface
  819. GetContents(ctx)
  820. }
  821. func GetFileContentsGet(ctx *context.APIContext) {
  822. // swagger:operation GET /repos/{owner}/{repo}/file-contents repository repoGetFileContents
  823. // ---
  824. // summary: Get the metadata and contents of requested files
  825. // description: See the POST method. This GET method supports using JSON encoded request body in query parameter.
  826. // produces:
  827. // - application/json
  828. // parameters:
  829. // - name: owner
  830. // in: path
  831. // description: owner of the repo
  832. // type: string
  833. // required: true
  834. // - name: repo
  835. // in: path
  836. // description: name of the repo
  837. // type: string
  838. // required: true
  839. // - name: ref
  840. // in: query
  841. // description: "The name of the commit/branch/tag. Default to the repository’s default branch."
  842. // type: string
  843. // required: false
  844. // - name: body
  845. // in: query
  846. // description: "The JSON encoded body (see the POST request): {\"files\": [\"filename1\", \"filename2\"]}"
  847. // type: string
  848. // required: true
  849. // responses:
  850. // "200":
  851. // "$ref": "#/responses/ContentsListResponse"
  852. // "404":
  853. // "$ref": "#/responses/notFound"
  854. // The POST method requires "write" permission, so we also support this "GET" method
  855. handleGetFileContents(ctx)
  856. }
  857. func GetFileContentsPost(ctx *context.APIContext) {
  858. // swagger:operation POST /repos/{owner}/{repo}/file-contents repository repoGetFileContentsPost
  859. // ---
  860. // summary: Get the metadata and contents of requested files
  861. // description: Uses automatic pagination based on default page size and
  862. // max response size and returns the maximum allowed number of files.
  863. // Files which could not be retrieved are null. Files which are too large
  864. // are being returned with `encoding == null`, `content == null` and `size > 0`,
  865. // they can be requested separately by using the `download_url`.
  866. // produces:
  867. // - application/json
  868. // parameters:
  869. // - name: owner
  870. // in: path
  871. // description: owner of the repo
  872. // type: string
  873. // required: true
  874. // - name: repo
  875. // in: path
  876. // description: name of the repo
  877. // type: string
  878. // required: true
  879. // - name: ref
  880. // in: query
  881. // description: "The name of the commit/branch/tag. Default to the repository’s default branch."
  882. // type: string
  883. // required: false
  884. // - name: body
  885. // in: body
  886. // required: true
  887. // schema:
  888. // "$ref": "#/definitions/GetFilesOptions"
  889. // responses:
  890. // "200":
  891. // "$ref": "#/responses/ContentsListResponse"
  892. // "404":
  893. // "$ref": "#/responses/notFound"
  894. // This is actually a "read" request, but we need to accept a "files" list, then POST method seems easy to use.
  895. // But the permission system requires that the caller must have "write" permission to use POST method.
  896. // At the moment, there is no other way to get around the permission check, so there is a "GET" workaround method above.
  897. handleGetFileContents(ctx)
  898. }
  899. func handleGetFileContents(ctx *context.APIContext) {
  900. opts, ok := web.GetForm(ctx).(*api.GetFilesOptions)
  901. if !ok {
  902. err := json.Unmarshal(util.UnsafeStringToBytes(ctx.FormString("body")), &opts)
  903. if err != nil {
  904. ctx.APIError(http.StatusBadRequest, "invalid body parameter")
  905. return
  906. }
  907. }
  908. refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref"))
  909. if ctx.Written() {
  910. return
  911. }
  912. filesResponse := files_service.GetContentsListFromTreePaths(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts.Files)
  913. ctx.JSON(http.StatusOK, util.SliceNilAsEmpty(filesResponse))
  914. }