You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

file.go 27KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977
  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. "path"
  13. "strings"
  14. "time"
  15. "code.gitea.io/gitea/models"
  16. git_model "code.gitea.io/gitea/models/git"
  17. repo_model "code.gitea.io/gitea/models/repo"
  18. "code.gitea.io/gitea/models/unit"
  19. "code.gitea.io/gitea/modules/git"
  20. "code.gitea.io/gitea/modules/gitrepo"
  21. "code.gitea.io/gitea/modules/httpcache"
  22. "code.gitea.io/gitea/modules/lfs"
  23. "code.gitea.io/gitea/modules/log"
  24. "code.gitea.io/gitea/modules/setting"
  25. "code.gitea.io/gitea/modules/storage"
  26. api "code.gitea.io/gitea/modules/structs"
  27. "code.gitea.io/gitea/modules/web"
  28. "code.gitea.io/gitea/routers/common"
  29. "code.gitea.io/gitea/services/context"
  30. archiver_service "code.gitea.io/gitea/services/repository/archiver"
  31. files_service "code.gitea.io/gitea/services/repository/files"
  32. )
  33. const giteaObjectTypeHeader = "X-Gitea-Object-Type"
  34. // GetRawFile get a file by path on a repository
  35. func GetRawFile(ctx *context.APIContext) {
  36. // swagger:operation GET /repos/{owner}/{repo}/raw/{filepath} repository repoGetRawFile
  37. // ---
  38. // summary: Get a file from a repository
  39. // produces:
  40. // - application/json
  41. // parameters:
  42. // - name: owner
  43. // in: path
  44. // description: owner of the repo
  45. // type: string
  46. // required: true
  47. // - name: repo
  48. // in: path
  49. // description: name of the repo
  50. // type: string
  51. // required: true
  52. // - name: filepath
  53. // in: path
  54. // description: filepath of the file to get
  55. // type: string
  56. // required: true
  57. // - name: ref
  58. // in: query
  59. // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
  60. // type: string
  61. // required: false
  62. // responses:
  63. // 200:
  64. // description: Returns raw file content.
  65. // "404":
  66. // "$ref": "#/responses/notFound"
  67. if ctx.Repo.Repository.IsEmpty {
  68. ctx.NotFound()
  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.TreePath, blob, lastModified); err != nil {
  77. ctx.Error(http.StatusInternalServerError, "ServeBlob", 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. // parameters:
  86. // - name: owner
  87. // in: path
  88. // description: owner of the repo
  89. // type: string
  90. // required: true
  91. // - name: repo
  92. // in: path
  93. // description: name of the repo
  94. // type: string
  95. // required: true
  96. // - name: filepath
  97. // in: path
  98. // description: filepath of the file to get
  99. // type: string
  100. // required: true
  101. // - name: ref
  102. // in: query
  103. // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
  104. // type: string
  105. // required: false
  106. // responses:
  107. // 200:
  108. // description: Returns raw file content.
  109. // "404":
  110. // "$ref": "#/responses/notFound"
  111. if ctx.Repo.Repository.IsEmpty {
  112. ctx.NotFound()
  113. return
  114. }
  115. blob, entry, lastModified := getBlobForEntry(ctx)
  116. if ctx.Written() {
  117. return
  118. }
  119. ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
  120. // LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file
  121. if blob.Size() > 1024 {
  122. // First handle caching for the blob
  123. if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
  124. return
  125. }
  126. // OK not cached - serve!
  127. if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
  128. ctx.ServerError("ServeBlob", err)
  129. }
  130. return
  131. }
  132. // OK, now the blob is known to have at most 1024 bytes we can simply read this in one go (This saves reading it twice)
  133. dataRc, err := blob.DataAsync()
  134. if err != nil {
  135. ctx.ServerError("DataAsync", err)
  136. return
  137. }
  138. // FIXME: code from #19689, what if the file is large ... OOM ...
  139. buf, err := io.ReadAll(dataRc)
  140. if err != nil {
  141. _ = dataRc.Close()
  142. ctx.ServerError("DataAsync", err)
  143. return
  144. }
  145. if err := dataRc.Close(); err != nil {
  146. log.Error("Error whilst closing blob %s reader in %-v. Error: %v", blob.ID, ctx.Repo.Repository, err)
  147. }
  148. // Check if the blob represents a pointer
  149. pointer, _ := lfs.ReadPointer(bytes.NewReader(buf))
  150. // if it's not a pointer, just serve the data directly
  151. if !pointer.IsValid() {
  152. // First handle caching for the blob
  153. if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
  154. return
  155. }
  156. // OK not cached - serve!
  157. common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
  158. return
  159. }
  160. // Now check if there is a MetaObject for this pointer
  161. meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid)
  162. // If there isn't one, just serve the data directly
  163. if err == git_model.ErrLFSObjectNotExist {
  164. // Handle caching for the blob SHA (not the LFS object OID)
  165. if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
  166. return
  167. }
  168. common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
  169. return
  170. } else if err != nil {
  171. ctx.ServerError("GetLFSMetaObjectByOid", err)
  172. return
  173. }
  174. // Handle caching for the LFS object OID
  175. if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
  176. return
  177. }
  178. if setting.LFS.Storage.MinioConfig.ServeDirect {
  179. // If we have a signed url (S3, object storage), redirect to this directly.
  180. u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name())
  181. if u != nil && err == nil {
  182. ctx.Redirect(u.String())
  183. return
  184. }
  185. }
  186. lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer)
  187. if err != nil {
  188. ctx.ServerError("ReadMetaObject", err)
  189. return
  190. }
  191. defer lfsDataRc.Close()
  192. common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc)
  193. }
  194. func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) {
  195. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
  196. if err != nil {
  197. if git.IsErrNotExist(err) {
  198. ctx.NotFound()
  199. } else {
  200. ctx.Error(http.StatusInternalServerError, "GetTreeEntryByPath", err)
  201. }
  202. return nil, nil, nil
  203. }
  204. if entry.IsDir() || entry.IsSubModule() {
  205. ctx.NotFound("getBlobForEntry", nil)
  206. return nil, nil, nil
  207. }
  208. info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:])
  209. if err != nil {
  210. ctx.Error(http.StatusInternalServerError, "GetCommitsInfo", err)
  211. return nil, nil, nil
  212. }
  213. if len(info) == 1 {
  214. // Not Modified
  215. lastModified = &info[0].Commit.Committer.When
  216. }
  217. blob = entry.Blob()
  218. return blob, entry, lastModified
  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. if ctx.Repo.GitRepo == nil {
  249. gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
  250. if err != nil {
  251. ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
  252. return
  253. }
  254. ctx.Repo.GitRepo = gitRepo
  255. defer gitRepo.Close()
  256. }
  257. archiveDownload(ctx)
  258. }
  259. func archiveDownload(ctx *context.APIContext) {
  260. uri := ctx.Params("*")
  261. aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
  262. if err != nil {
  263. if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
  264. ctx.Error(http.StatusBadRequest, "unknown archive format", err)
  265. } else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
  266. ctx.Error(http.StatusNotFound, "unrecognized reference", err)
  267. } else {
  268. ctx.ServerError("archiver_service.NewRequest", err)
  269. }
  270. return
  271. }
  272. archiver, err := aReq.Await(ctx)
  273. if err != nil {
  274. ctx.ServerError("archiver.Await", err)
  275. return
  276. }
  277. download(ctx, aReq.GetArchiveName(), archiver)
  278. }
  279. func download(ctx *context.APIContext, archiveName string, archiver *repo_model.RepoArchiver) {
  280. downloadName := ctx.Repo.Repository.Name + "-" + archiveName
  281. rPath := archiver.RelativePath()
  282. if setting.RepoArchive.Storage.MinioConfig.ServeDirect {
  283. // If we have a signed url (S3, object storage), redirect to this directly.
  284. u, err := storage.RepoArchives.URL(rPath, downloadName)
  285. if u != nil && err == nil {
  286. ctx.Redirect(u.String())
  287. return
  288. }
  289. }
  290. // If we have matched and access to release or issue
  291. fr, err := storage.RepoArchives.Open(rPath)
  292. if err != nil {
  293. ctx.ServerError("Open", err)
  294. return
  295. }
  296. defer fr.Close()
  297. ctx.ServeContent(fr, &context.ServeHeaderOptions{
  298. Filename: downloadName,
  299. LastModified: archiver.CreatedUnix.AsLocalTime(),
  300. })
  301. }
  302. // GetEditorconfig get editor config of a repository
  303. func GetEditorconfig(ctx *context.APIContext) {
  304. // swagger:operation GET /repos/{owner}/{repo}/editorconfig/{filepath} repository repoGetEditorConfig
  305. // ---
  306. // summary: Get the EditorConfig definitions of a file in a repository
  307. // produces:
  308. // - application/json
  309. // parameters:
  310. // - name: owner
  311. // in: path
  312. // description: owner of the repo
  313. // type: string
  314. // required: true
  315. // - name: repo
  316. // in: path
  317. // description: name of the repo
  318. // type: string
  319. // required: true
  320. // - name: filepath
  321. // in: path
  322. // description: filepath of file to get
  323. // type: string
  324. // required: true
  325. // - name: ref
  326. // in: query
  327. // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
  328. // type: string
  329. // required: false
  330. // responses:
  331. // 200:
  332. // description: success
  333. // "404":
  334. // "$ref": "#/responses/notFound"
  335. ec, _, err := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
  336. if err != nil {
  337. if git.IsErrNotExist(err) {
  338. ctx.NotFound(err)
  339. } else {
  340. ctx.Error(http.StatusInternalServerError, "GetEditorconfig", err)
  341. }
  342. return
  343. }
  344. fileName := ctx.Params("filename")
  345. def, err := ec.GetDefinitionForFilename(fileName)
  346. if def == nil {
  347. ctx.NotFound(err)
  348. return
  349. }
  350. ctx.JSON(http.StatusOK, def)
  351. }
  352. // canWriteFiles returns true if repository is editable and user has proper access level.
  353. func canWriteFiles(ctx *context.APIContext, branch string) bool {
  354. return ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, branch) &&
  355. !ctx.Repo.Repository.IsMirror &&
  356. !ctx.Repo.Repository.IsArchived
  357. }
  358. // canReadFiles returns true if repository is readable and user has proper access level.
  359. func canReadFiles(r *context.Repository) bool {
  360. return r.Permission.CanRead(unit.TypeCode)
  361. }
  362. func base64Reader(s string) (io.ReadSeeker, error) {
  363. b, err := base64.StdEncoding.DecodeString(s)
  364. if err != nil {
  365. return nil, err
  366. }
  367. return bytes.NewReader(b), nil
  368. }
  369. // ChangeFiles handles API call for modifying multiple files
  370. func ChangeFiles(ctx *context.APIContext) {
  371. // swagger:operation POST /repos/{owner}/{repo}/contents repository repoChangeFiles
  372. // ---
  373. // summary: Modify multiple files in a repository
  374. // consumes:
  375. // - application/json
  376. // produces:
  377. // - application/json
  378. // parameters:
  379. // - name: owner
  380. // in: path
  381. // description: owner of the repo
  382. // type: string
  383. // required: true
  384. // - name: repo
  385. // in: path
  386. // description: name of the repo
  387. // type: string
  388. // required: true
  389. // - name: body
  390. // in: body
  391. // required: true
  392. // schema:
  393. // "$ref": "#/definitions/ChangeFilesOptions"
  394. // responses:
  395. // "201":
  396. // "$ref": "#/responses/FilesResponse"
  397. // "403":
  398. // "$ref": "#/responses/error"
  399. // "404":
  400. // "$ref": "#/responses/notFound"
  401. // "422":
  402. // "$ref": "#/responses/error"
  403. // "423":
  404. // "$ref": "#/responses/repoArchivedError"
  405. apiOpts := web.GetForm(ctx).(*api.ChangeFilesOptions)
  406. if apiOpts.BranchName == "" {
  407. apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
  408. }
  409. var files []*files_service.ChangeRepoFile
  410. for _, file := range apiOpts.Files {
  411. contentReader, err := base64Reader(file.ContentBase64)
  412. if err != nil {
  413. ctx.Error(http.StatusUnprocessableEntity, "Invalid base64 content", err)
  414. return
  415. }
  416. changeRepoFile := &files_service.ChangeRepoFile{
  417. Operation: file.Operation,
  418. TreePath: file.Path,
  419. FromTreePath: file.FromPath,
  420. ContentReader: contentReader,
  421. SHA: file.SHA,
  422. }
  423. files = append(files, changeRepoFile)
  424. }
  425. opts := &files_service.ChangeRepoFilesOptions{
  426. Files: files,
  427. Message: apiOpts.Message,
  428. OldBranch: apiOpts.BranchName,
  429. NewBranch: apiOpts.NewBranchName,
  430. Committer: &files_service.IdentityOptions{
  431. Name: apiOpts.Committer.Name,
  432. Email: apiOpts.Committer.Email,
  433. },
  434. Author: &files_service.IdentityOptions{
  435. Name: apiOpts.Author.Name,
  436. Email: apiOpts.Author.Email,
  437. },
  438. Dates: &files_service.CommitDateOptions{
  439. Author: apiOpts.Dates.Author,
  440. Committer: apiOpts.Dates.Committer,
  441. },
  442. Signoff: apiOpts.Signoff,
  443. }
  444. if opts.Dates.Author.IsZero() {
  445. opts.Dates.Author = time.Now()
  446. }
  447. if opts.Dates.Committer.IsZero() {
  448. opts.Dates.Committer = time.Now()
  449. }
  450. if opts.Message == "" {
  451. opts.Message = changeFilesCommitMessage(ctx, files)
  452. }
  453. if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
  454. handleCreateOrUpdateFileError(ctx, err)
  455. } else {
  456. ctx.JSON(http.StatusCreated, filesResponse)
  457. }
  458. }
  459. // CreateFile handles API call for creating a file
  460. func CreateFile(ctx *context.APIContext) {
  461. // swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
  462. // ---
  463. // summary: Create a file in a repository
  464. // consumes:
  465. // - application/json
  466. // produces:
  467. // - application/json
  468. // parameters:
  469. // - name: owner
  470. // in: path
  471. // description: owner of the repo
  472. // type: string
  473. // required: true
  474. // - name: repo
  475. // in: path
  476. // description: name of the repo
  477. // type: string
  478. // required: true
  479. // - name: filepath
  480. // in: path
  481. // description: path of the file to create
  482. // type: string
  483. // required: true
  484. // - name: body
  485. // in: body
  486. // required: true
  487. // schema:
  488. // "$ref": "#/definitions/CreateFileOptions"
  489. // responses:
  490. // "201":
  491. // "$ref": "#/responses/FileResponse"
  492. // "403":
  493. // "$ref": "#/responses/error"
  494. // "404":
  495. // "$ref": "#/responses/notFound"
  496. // "422":
  497. // "$ref": "#/responses/error"
  498. // "423":
  499. // "$ref": "#/responses/repoArchivedError"
  500. apiOpts := web.GetForm(ctx).(*api.CreateFileOptions)
  501. if apiOpts.BranchName == "" {
  502. apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
  503. }
  504. contentReader, err := base64Reader(apiOpts.ContentBase64)
  505. if err != nil {
  506. ctx.Error(http.StatusUnprocessableEntity, "Invalid base64 content", err)
  507. return
  508. }
  509. opts := &files_service.ChangeRepoFilesOptions{
  510. Files: []*files_service.ChangeRepoFile{
  511. {
  512. Operation: "create",
  513. TreePath: ctx.Params("*"),
  514. ContentReader: contentReader,
  515. },
  516. },
  517. Message: apiOpts.Message,
  518. OldBranch: apiOpts.BranchName,
  519. NewBranch: apiOpts.NewBranchName,
  520. Committer: &files_service.IdentityOptions{
  521. Name: apiOpts.Committer.Name,
  522. Email: apiOpts.Committer.Email,
  523. },
  524. Author: &files_service.IdentityOptions{
  525. Name: apiOpts.Author.Name,
  526. Email: apiOpts.Author.Email,
  527. },
  528. Dates: &files_service.CommitDateOptions{
  529. Author: apiOpts.Dates.Author,
  530. Committer: apiOpts.Dates.Committer,
  531. },
  532. Signoff: apiOpts.Signoff,
  533. }
  534. if opts.Dates.Author.IsZero() {
  535. opts.Dates.Author = time.Now()
  536. }
  537. if opts.Dates.Committer.IsZero() {
  538. opts.Dates.Committer = time.Now()
  539. }
  540. if opts.Message == "" {
  541. opts.Message = changeFilesCommitMessage(ctx, opts.Files)
  542. }
  543. if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
  544. handleCreateOrUpdateFileError(ctx, err)
  545. } else {
  546. fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
  547. ctx.JSON(http.StatusCreated, fileResponse)
  548. }
  549. }
  550. // UpdateFile handles API call for updating a file
  551. func UpdateFile(ctx *context.APIContext) {
  552. // swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
  553. // ---
  554. // summary: Update a file in a repository
  555. // consumes:
  556. // - application/json
  557. // produces:
  558. // - application/json
  559. // parameters:
  560. // - name: owner
  561. // in: path
  562. // description: owner of the repo
  563. // type: string
  564. // required: true
  565. // - name: repo
  566. // in: path
  567. // description: name of the repo
  568. // type: string
  569. // required: true
  570. // - name: filepath
  571. // in: path
  572. // description: path of the file to update
  573. // type: string
  574. // required: true
  575. // - name: body
  576. // in: body
  577. // required: true
  578. // schema:
  579. // "$ref": "#/definitions/UpdateFileOptions"
  580. // responses:
  581. // "200":
  582. // "$ref": "#/responses/FileResponse"
  583. // "403":
  584. // "$ref": "#/responses/error"
  585. // "404":
  586. // "$ref": "#/responses/notFound"
  587. // "422":
  588. // "$ref": "#/responses/error"
  589. // "423":
  590. // "$ref": "#/responses/repoArchivedError"
  591. apiOpts := web.GetForm(ctx).(*api.UpdateFileOptions)
  592. if ctx.Repo.Repository.IsEmpty {
  593. ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
  594. return
  595. }
  596. if apiOpts.BranchName == "" {
  597. apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
  598. }
  599. contentReader, err := base64Reader(apiOpts.ContentBase64)
  600. if err != nil {
  601. ctx.Error(http.StatusUnprocessableEntity, "Invalid base64 content", err)
  602. return
  603. }
  604. opts := &files_service.ChangeRepoFilesOptions{
  605. Files: []*files_service.ChangeRepoFile{
  606. {
  607. Operation: "update",
  608. ContentReader: contentReader,
  609. SHA: apiOpts.SHA,
  610. FromTreePath: apiOpts.FromPath,
  611. TreePath: ctx.Params("*"),
  612. },
  613. },
  614. Message: apiOpts.Message,
  615. OldBranch: apiOpts.BranchName,
  616. NewBranch: apiOpts.NewBranchName,
  617. Committer: &files_service.IdentityOptions{
  618. Name: apiOpts.Committer.Name,
  619. Email: apiOpts.Committer.Email,
  620. },
  621. Author: &files_service.IdentityOptions{
  622. Name: apiOpts.Author.Name,
  623. Email: apiOpts.Author.Email,
  624. },
  625. Dates: &files_service.CommitDateOptions{
  626. Author: apiOpts.Dates.Author,
  627. Committer: apiOpts.Dates.Committer,
  628. },
  629. Signoff: apiOpts.Signoff,
  630. }
  631. if opts.Dates.Author.IsZero() {
  632. opts.Dates.Author = time.Now()
  633. }
  634. if opts.Dates.Committer.IsZero() {
  635. opts.Dates.Committer = time.Now()
  636. }
  637. if opts.Message == "" {
  638. opts.Message = changeFilesCommitMessage(ctx, opts.Files)
  639. }
  640. if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
  641. handleCreateOrUpdateFileError(ctx, err)
  642. } else {
  643. fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
  644. ctx.JSON(http.StatusOK, fileResponse)
  645. }
  646. }
  647. func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) {
  648. if models.IsErrUserCannotCommit(err) || models.IsErrFilePathProtected(err) {
  649. ctx.Error(http.StatusForbidden, "Access", err)
  650. return
  651. }
  652. if git_model.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) ||
  653. models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) {
  654. ctx.Error(http.StatusUnprocessableEntity, "Invalid", err)
  655. return
  656. }
  657. if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) {
  658. ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err)
  659. return
  660. }
  661. ctx.Error(http.StatusInternalServerError, "UpdateFile", err)
  662. }
  663. // Called from both CreateFile or UpdateFile to handle both
  664. func createOrUpdateFiles(ctx *context.APIContext, opts *files_service.ChangeRepoFilesOptions) (*api.FilesResponse, error) {
  665. if !canWriteFiles(ctx, opts.OldBranch) {
  666. return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{
  667. UserID: ctx.Doer.ID,
  668. RepoName: ctx.Repo.Repository.LowerName,
  669. }
  670. }
  671. return files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts)
  672. }
  673. // format commit message if empty
  674. func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.ChangeRepoFile) string {
  675. var (
  676. createFiles []string
  677. updateFiles []string
  678. deleteFiles []string
  679. )
  680. for _, file := range files {
  681. switch file.Operation {
  682. case "create":
  683. createFiles = append(createFiles, file.TreePath)
  684. case "update":
  685. updateFiles = append(updateFiles, file.TreePath)
  686. case "delete":
  687. deleteFiles = append(deleteFiles, file.TreePath)
  688. }
  689. }
  690. message := ""
  691. if len(createFiles) != 0 {
  692. message += ctx.Locale.TrString("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
  693. }
  694. if len(updateFiles) != 0 {
  695. message += ctx.Locale.TrString("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
  696. }
  697. if len(deleteFiles) != 0 {
  698. message += ctx.Locale.TrString("repo.editor.delete", strings.Join(deleteFiles, ", "))
  699. }
  700. return strings.Trim(message, "\n")
  701. }
  702. // DeleteFile Delete a file in a repository
  703. func DeleteFile(ctx *context.APIContext) {
  704. // swagger:operation DELETE /repos/{owner}/{repo}/contents/{filepath} repository repoDeleteFile
  705. // ---
  706. // summary: Delete a file in a repository
  707. // consumes:
  708. // - application/json
  709. // produces:
  710. // - application/json
  711. // parameters:
  712. // - name: owner
  713. // in: path
  714. // description: owner of the repo
  715. // type: string
  716. // required: true
  717. // - name: repo
  718. // in: path
  719. // description: name of the repo
  720. // type: string
  721. // required: true
  722. // - name: filepath
  723. // in: path
  724. // description: path of the file to delete
  725. // type: string
  726. // required: true
  727. // - name: body
  728. // in: body
  729. // required: true
  730. // schema:
  731. // "$ref": "#/definitions/DeleteFileOptions"
  732. // responses:
  733. // "200":
  734. // "$ref": "#/responses/FileDeleteResponse"
  735. // "400":
  736. // "$ref": "#/responses/error"
  737. // "403":
  738. // "$ref": "#/responses/error"
  739. // "404":
  740. // "$ref": "#/responses/error"
  741. // "423":
  742. // "$ref": "#/responses/repoArchivedError"
  743. apiOpts := web.GetForm(ctx).(*api.DeleteFileOptions)
  744. if !canWriteFiles(ctx, apiOpts.BranchName) {
  745. ctx.Error(http.StatusForbidden, "DeleteFile", repo_model.ErrUserDoesNotHaveAccessToRepo{
  746. UserID: ctx.Doer.ID,
  747. RepoName: ctx.Repo.Repository.LowerName,
  748. })
  749. return
  750. }
  751. if apiOpts.BranchName == "" {
  752. apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
  753. }
  754. opts := &files_service.ChangeRepoFilesOptions{
  755. Files: []*files_service.ChangeRepoFile{
  756. {
  757. Operation: "delete",
  758. SHA: apiOpts.SHA,
  759. TreePath: ctx.Params("*"),
  760. },
  761. },
  762. Message: apiOpts.Message,
  763. OldBranch: apiOpts.BranchName,
  764. NewBranch: apiOpts.NewBranchName,
  765. Committer: &files_service.IdentityOptions{
  766. Name: apiOpts.Committer.Name,
  767. Email: apiOpts.Committer.Email,
  768. },
  769. Author: &files_service.IdentityOptions{
  770. Name: apiOpts.Author.Name,
  771. Email: apiOpts.Author.Email,
  772. },
  773. Dates: &files_service.CommitDateOptions{
  774. Author: apiOpts.Dates.Author,
  775. Committer: apiOpts.Dates.Committer,
  776. },
  777. Signoff: apiOpts.Signoff,
  778. }
  779. if opts.Dates.Author.IsZero() {
  780. opts.Dates.Author = time.Now()
  781. }
  782. if opts.Dates.Committer.IsZero() {
  783. opts.Dates.Committer = time.Now()
  784. }
  785. if opts.Message == "" {
  786. opts.Message = changeFilesCommitMessage(ctx, opts.Files)
  787. }
  788. if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
  789. if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
  790. ctx.Error(http.StatusNotFound, "DeleteFile", err)
  791. return
  792. } else if git_model.IsErrBranchAlreadyExists(err) ||
  793. models.IsErrFilenameInvalid(err) ||
  794. models.IsErrSHADoesNotMatch(err) ||
  795. models.IsErrCommitIDDoesNotMatch(err) ||
  796. models.IsErrSHAOrCommitIDNotProvided(err) {
  797. ctx.Error(http.StatusBadRequest, "DeleteFile", err)
  798. return
  799. } else if models.IsErrUserCannotCommit(err) {
  800. ctx.Error(http.StatusForbidden, "DeleteFile", err)
  801. return
  802. }
  803. ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
  804. } else {
  805. fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
  806. ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
  807. }
  808. }
  809. // GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
  810. func GetContents(ctx *context.APIContext) {
  811. // swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents
  812. // ---
  813. // summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
  814. // produces:
  815. // - application/json
  816. // parameters:
  817. // - name: owner
  818. // in: path
  819. // description: owner of the repo
  820. // type: string
  821. // required: true
  822. // - name: repo
  823. // in: path
  824. // description: name of the repo
  825. // type: string
  826. // required: true
  827. // - name: filepath
  828. // in: path
  829. // description: path of the dir, file, symlink or submodule in the repo
  830. // type: string
  831. // required: true
  832. // - name: ref
  833. // in: query
  834. // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
  835. // type: string
  836. // required: false
  837. // responses:
  838. // "200":
  839. // "$ref": "#/responses/ContentsResponse"
  840. // "404":
  841. // "$ref": "#/responses/notFound"
  842. if !canReadFiles(ctx.Repo) {
  843. ctx.Error(http.StatusInternalServerError, "GetContentsOrList", repo_model.ErrUserDoesNotHaveAccessToRepo{
  844. UserID: ctx.Doer.ID,
  845. RepoName: ctx.Repo.Repository.LowerName,
  846. })
  847. return
  848. }
  849. treePath := ctx.Params("*")
  850. ref := ctx.FormTrim("ref")
  851. if fileList, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, treePath, ref); err != nil {
  852. if git.IsErrNotExist(err) {
  853. ctx.NotFound("GetContentsOrList", err)
  854. return
  855. }
  856. ctx.Error(http.StatusInternalServerError, "GetContentsOrList", err)
  857. } else {
  858. ctx.JSON(http.StatusOK, fileList)
  859. }
  860. }
  861. // GetContentsList Get the metadata of all the entries of the root dir
  862. func GetContentsList(ctx *context.APIContext) {
  863. // swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList
  864. // ---
  865. // summary: Gets the metadata of all the entries of the root dir
  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 the repository’s default branch (usually master)"
  882. // type: string
  883. // required: false
  884. // responses:
  885. // "200":
  886. // "$ref": "#/responses/ContentsListResponse"
  887. // "404":
  888. // "$ref": "#/responses/notFound"
  889. // same as GetContents(), this function is here because swagger fails if path is empty in GetContents() interface
  890. GetContents(ctx)
  891. }