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 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package repo
  6. import (
  7. "encoding/base64"
  8. "fmt"
  9. "net/http"
  10. "time"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/modules/context"
  13. "code.gitea.io/gitea/modules/git"
  14. "code.gitea.io/gitea/modules/repofiles"
  15. api "code.gitea.io/gitea/modules/structs"
  16. "code.gitea.io/gitea/routers/repo"
  17. )
  18. // GetRawFile get a file by path on a repository
  19. func GetRawFile(ctx *context.APIContext) {
  20. // swagger:operation GET /repos/{owner}/{repo}/raw/{filepath} repository repoGetRawFile
  21. // ---
  22. // summary: Get a file from a repository
  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: filepath
  37. // in: path
  38. // description: filepath of the file to get
  39. // type: string
  40. // required: true
  41. // responses:
  42. // 200:
  43. // description: success
  44. // "404":
  45. // "$ref": "#/responses/notFound"
  46. if ctx.Repo.Repository.IsEmpty {
  47. ctx.NotFound()
  48. return
  49. }
  50. blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath)
  51. if err != nil {
  52. if git.IsErrNotExist(err) {
  53. ctx.NotFound()
  54. } else {
  55. ctx.Error(http.StatusInternalServerError, "GetBlobByPath", err)
  56. }
  57. return
  58. }
  59. if err = repo.ServeBlob(ctx.Context, blob); err != nil {
  60. ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
  61. }
  62. }
  63. // GetArchive get archive of a repository
  64. func GetArchive(ctx *context.APIContext) {
  65. // swagger:operation GET /repos/{owner}/{repo}/archive/{archive} repository repoGetArchive
  66. // ---
  67. // summary: Get an archive of a repository
  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: archive
  82. // in: path
  83. // description: archive to download, consisting of a git reference and archive
  84. // type: string
  85. // required: true
  86. // responses:
  87. // 200:
  88. // description: success
  89. // "404":
  90. // "$ref": "#/responses/notFound"
  91. repoPath := models.RepoPath(ctx.Params(":username"), ctx.Params(":reponame"))
  92. gitRepo, err := git.OpenRepository(repoPath)
  93. if err != nil {
  94. ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
  95. return
  96. }
  97. ctx.Repo.GitRepo = gitRepo
  98. defer gitRepo.Close()
  99. repo.Download(ctx.Context)
  100. }
  101. // GetEditorconfig get editor config of a repository
  102. func GetEditorconfig(ctx *context.APIContext) {
  103. // swagger:operation GET /repos/{owner}/{repo}/editorconfig/{filepath} repository repoGetEditorConfig
  104. // ---
  105. // summary: Get the EditorConfig definitions of a file in a repository
  106. // produces:
  107. // - application/json
  108. // parameters:
  109. // - name: owner
  110. // in: path
  111. // description: owner of the repo
  112. // type: string
  113. // required: true
  114. // - name: repo
  115. // in: path
  116. // description: name of the repo
  117. // type: string
  118. // required: true
  119. // - name: filepath
  120. // in: path
  121. // description: filepath of file to get
  122. // type: string
  123. // required: true
  124. // responses:
  125. // 200:
  126. // description: success
  127. // "404":
  128. // "$ref": "#/responses/notFound"
  129. ec, err := ctx.Repo.GetEditorconfig()
  130. if err != nil {
  131. if git.IsErrNotExist(err) {
  132. ctx.NotFound(err)
  133. } else {
  134. ctx.Error(http.StatusInternalServerError, "GetEditorconfig", err)
  135. }
  136. return
  137. }
  138. fileName := ctx.Params("filename")
  139. def, err := ec.GetDefinitionForFilename(fileName)
  140. if def == nil {
  141. ctx.NotFound(err)
  142. return
  143. }
  144. ctx.JSON(http.StatusOK, def)
  145. }
  146. // canWriteFiles returns true if repository is editable and user has proper access level.
  147. func canWriteFiles(r *context.Repository) bool {
  148. return r.Permission.CanWrite(models.UnitTypeCode) && !r.Repository.IsMirror && !r.Repository.IsArchived
  149. }
  150. // canReadFiles returns true if repository is readable and user has proper access level.
  151. func canReadFiles(r *context.Repository) bool {
  152. return r.Permission.CanRead(models.UnitTypeCode)
  153. }
  154. // CreateFile handles API call for creating a file
  155. func CreateFile(ctx *context.APIContext, apiOpts api.CreateFileOptions) {
  156. // swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
  157. // ---
  158. // summary: Create a file in a repository
  159. // consumes:
  160. // - application/json
  161. // produces:
  162. // - application/json
  163. // parameters:
  164. // - name: owner
  165. // in: path
  166. // description: owner of the repo
  167. // type: string
  168. // required: true
  169. // - name: repo
  170. // in: path
  171. // description: name of the repo
  172. // type: string
  173. // required: true
  174. // - name: filepath
  175. // in: path
  176. // description: path of the file to create
  177. // type: string
  178. // required: true
  179. // - name: body
  180. // in: body
  181. // required: true
  182. // schema:
  183. // "$ref": "#/definitions/CreateFileOptions"
  184. // responses:
  185. // "201":
  186. // "$ref": "#/responses/FileResponse"
  187. // "403":
  188. // "$ref": "#/responses/error"
  189. // "404":
  190. // "$ref": "#/responses/notFound"
  191. // "422":
  192. // "$ref": "#/responses/error"
  193. if ctx.Repo.Repository.IsEmpty {
  194. ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
  195. }
  196. if apiOpts.BranchName == "" {
  197. apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
  198. }
  199. opts := &repofiles.UpdateRepoFileOptions{
  200. Content: apiOpts.Content,
  201. IsNewFile: true,
  202. Message: apiOpts.Message,
  203. TreePath: ctx.Params("*"),
  204. OldBranch: apiOpts.BranchName,
  205. NewBranch: apiOpts.NewBranchName,
  206. Committer: &repofiles.IdentityOptions{
  207. Name: apiOpts.Committer.Name,
  208. Email: apiOpts.Committer.Email,
  209. },
  210. Author: &repofiles.IdentityOptions{
  211. Name: apiOpts.Author.Name,
  212. Email: apiOpts.Author.Email,
  213. },
  214. Dates: &repofiles.CommitDateOptions{
  215. Author: apiOpts.Dates.Author,
  216. Committer: apiOpts.Dates.Committer,
  217. },
  218. }
  219. if opts.Dates.Author.IsZero() {
  220. opts.Dates.Author = time.Now()
  221. }
  222. if opts.Dates.Committer.IsZero() {
  223. opts.Dates.Committer = time.Now()
  224. }
  225. if opts.Message == "" {
  226. opts.Message = ctx.Tr("repo.editor.add", opts.TreePath)
  227. }
  228. if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
  229. handleCreateOrUpdateFileError(ctx, err)
  230. } else {
  231. ctx.JSON(http.StatusCreated, fileResponse)
  232. }
  233. }
  234. // UpdateFile handles API call for updating a file
  235. func UpdateFile(ctx *context.APIContext, apiOpts api.UpdateFileOptions) {
  236. // swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
  237. // ---
  238. // summary: Update a file in a repository
  239. // consumes:
  240. // - application/json
  241. // produces:
  242. // - application/json
  243. // parameters:
  244. // - name: owner
  245. // in: path
  246. // description: owner of the repo
  247. // type: string
  248. // required: true
  249. // - name: repo
  250. // in: path
  251. // description: name of the repo
  252. // type: string
  253. // required: true
  254. // - name: filepath
  255. // in: path
  256. // description: path of the file to update
  257. // type: string
  258. // required: true
  259. // - name: body
  260. // in: body
  261. // required: true
  262. // schema:
  263. // "$ref": "#/definitions/UpdateFileOptions"
  264. // responses:
  265. // "200":
  266. // "$ref": "#/responses/FileResponse"
  267. // "403":
  268. // "$ref": "#/responses/error"
  269. // "404":
  270. // "$ref": "#/responses/notFound"
  271. // "422":
  272. // "$ref": "#/responses/error"
  273. if ctx.Repo.Repository.IsEmpty {
  274. ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
  275. }
  276. if apiOpts.BranchName == "" {
  277. apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
  278. }
  279. opts := &repofiles.UpdateRepoFileOptions{
  280. Content: apiOpts.Content,
  281. SHA: apiOpts.SHA,
  282. IsNewFile: false,
  283. Message: apiOpts.Message,
  284. FromTreePath: apiOpts.FromPath,
  285. TreePath: ctx.Params("*"),
  286. OldBranch: apiOpts.BranchName,
  287. NewBranch: apiOpts.NewBranchName,
  288. Committer: &repofiles.IdentityOptions{
  289. Name: apiOpts.Committer.Name,
  290. Email: apiOpts.Committer.Email,
  291. },
  292. Author: &repofiles.IdentityOptions{
  293. Name: apiOpts.Author.Name,
  294. Email: apiOpts.Author.Email,
  295. },
  296. Dates: &repofiles.CommitDateOptions{
  297. Author: apiOpts.Dates.Author,
  298. Committer: apiOpts.Dates.Committer,
  299. },
  300. }
  301. if opts.Dates.Author.IsZero() {
  302. opts.Dates.Author = time.Now()
  303. }
  304. if opts.Dates.Committer.IsZero() {
  305. opts.Dates.Committer = time.Now()
  306. }
  307. if opts.Message == "" {
  308. opts.Message = ctx.Tr("repo.editor.update", opts.TreePath)
  309. }
  310. if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
  311. handleCreateOrUpdateFileError(ctx, err)
  312. } else {
  313. ctx.JSON(http.StatusOK, fileResponse)
  314. }
  315. }
  316. func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) {
  317. if models.IsErrUserCannotCommit(err) || models.IsErrFilePathProtected(err) {
  318. ctx.Error(http.StatusForbidden, "Access", err)
  319. return
  320. }
  321. if models.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) ||
  322. models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) {
  323. ctx.Error(http.StatusUnprocessableEntity, "Invalid", err)
  324. return
  325. }
  326. ctx.Error(http.StatusInternalServerError, "UpdateFile", err)
  327. }
  328. // Called from both CreateFile or UpdateFile to handle both
  329. func createOrUpdateFile(ctx *context.APIContext, opts *repofiles.UpdateRepoFileOptions) (*api.FileResponse, error) {
  330. if !canWriteFiles(ctx.Repo) {
  331. return nil, models.ErrUserDoesNotHaveAccessToRepo{
  332. UserID: ctx.User.ID,
  333. RepoName: ctx.Repo.Repository.LowerName,
  334. }
  335. }
  336. content, err := base64.StdEncoding.DecodeString(opts.Content)
  337. if err != nil {
  338. return nil, err
  339. }
  340. opts.Content = string(content)
  341. return repofiles.CreateOrUpdateRepoFile(ctx.Repo.Repository, ctx.User, opts)
  342. }
  343. // DeleteFile Delete a fle in a repository
  344. func DeleteFile(ctx *context.APIContext, apiOpts api.DeleteFileOptions) {
  345. // swagger:operation DELETE /repos/{owner}/{repo}/contents/{filepath} repository repoDeleteFile
  346. // ---
  347. // summary: Delete a file in a repository
  348. // consumes:
  349. // - application/json
  350. // produces:
  351. // - application/json
  352. // parameters:
  353. // - name: owner
  354. // in: path
  355. // description: owner of the repo
  356. // type: string
  357. // required: true
  358. // - name: repo
  359. // in: path
  360. // description: name of the repo
  361. // type: string
  362. // required: true
  363. // - name: filepath
  364. // in: path
  365. // description: path of the file to delete
  366. // type: string
  367. // required: true
  368. // - name: body
  369. // in: body
  370. // required: true
  371. // schema:
  372. // "$ref": "#/definitions/DeleteFileOptions"
  373. // responses:
  374. // "200":
  375. // "$ref": "#/responses/FileDeleteResponse"
  376. // "400":
  377. // "$ref": "#/responses/error"
  378. // "403":
  379. // "$ref": "#/responses/error"
  380. // "404":
  381. // "$ref": "#/responses/error"
  382. if !canWriteFiles(ctx.Repo) {
  383. ctx.Error(http.StatusForbidden, "DeleteFile", models.ErrUserDoesNotHaveAccessToRepo{
  384. UserID: ctx.User.ID,
  385. RepoName: ctx.Repo.Repository.LowerName,
  386. })
  387. return
  388. }
  389. if apiOpts.BranchName == "" {
  390. apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
  391. }
  392. opts := &repofiles.DeleteRepoFileOptions{
  393. Message: apiOpts.Message,
  394. OldBranch: apiOpts.BranchName,
  395. NewBranch: apiOpts.NewBranchName,
  396. SHA: apiOpts.SHA,
  397. TreePath: ctx.Params("*"),
  398. Committer: &repofiles.IdentityOptions{
  399. Name: apiOpts.Committer.Name,
  400. Email: apiOpts.Committer.Email,
  401. },
  402. Author: &repofiles.IdentityOptions{
  403. Name: apiOpts.Author.Name,
  404. Email: apiOpts.Author.Email,
  405. },
  406. Dates: &repofiles.CommitDateOptions{
  407. Author: apiOpts.Dates.Author,
  408. Committer: apiOpts.Dates.Committer,
  409. },
  410. }
  411. if opts.Dates.Author.IsZero() {
  412. opts.Dates.Author = time.Now()
  413. }
  414. if opts.Dates.Committer.IsZero() {
  415. opts.Dates.Committer = time.Now()
  416. }
  417. if opts.Message == "" {
  418. opts.Message = ctx.Tr("repo.editor.delete", opts.TreePath)
  419. }
  420. if fileResponse, err := repofiles.DeleteRepoFile(ctx.Repo.Repository, ctx.User, opts); err != nil {
  421. if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
  422. ctx.Error(http.StatusNotFound, "DeleteFile", err)
  423. return
  424. } else if models.IsErrBranchAlreadyExists(err) ||
  425. models.IsErrFilenameInvalid(err) ||
  426. models.IsErrSHADoesNotMatch(err) ||
  427. models.IsErrCommitIDDoesNotMatch(err) ||
  428. models.IsErrSHAOrCommitIDNotProvided(err) {
  429. ctx.Error(http.StatusBadRequest, "DeleteFile", err)
  430. return
  431. } else if models.IsErrUserCannotCommit(err) {
  432. ctx.Error(http.StatusForbidden, "DeleteFile", err)
  433. return
  434. }
  435. ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
  436. } else {
  437. ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
  438. }
  439. }
  440. // GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
  441. func GetContents(ctx *context.APIContext) {
  442. // swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents
  443. // ---
  444. // summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
  445. // produces:
  446. // - application/json
  447. // parameters:
  448. // - name: owner
  449. // in: path
  450. // description: owner of the repo
  451. // type: string
  452. // required: true
  453. // - name: repo
  454. // in: path
  455. // description: name of the repo
  456. // type: string
  457. // required: true
  458. // - name: filepath
  459. // in: path
  460. // description: path of the dir, file, symlink or submodule in the repo
  461. // type: string
  462. // required: true
  463. // - name: ref
  464. // in: query
  465. // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
  466. // type: string
  467. // required: false
  468. // responses:
  469. // "200":
  470. // "$ref": "#/responses/ContentsResponse"
  471. // "404":
  472. // "$ref": "#/responses/notFound"
  473. if !canReadFiles(ctx.Repo) {
  474. ctx.Error(http.StatusInternalServerError, "GetContentsOrList", models.ErrUserDoesNotHaveAccessToRepo{
  475. UserID: ctx.User.ID,
  476. RepoName: ctx.Repo.Repository.LowerName,
  477. })
  478. return
  479. }
  480. treePath := ctx.Params("*")
  481. ref := ctx.QueryTrim("ref")
  482. if fileList, err := repofiles.GetContentsOrList(ctx.Repo.Repository, treePath, ref); err != nil {
  483. if git.IsErrNotExist(err) {
  484. ctx.NotFound("GetContentsOrList", err)
  485. return
  486. }
  487. ctx.Error(http.StatusInternalServerError, "GetContentsOrList", err)
  488. } else {
  489. ctx.JSON(http.StatusOK, fileList)
  490. }
  491. }
  492. // GetContentsList Get the metadata of all the entries of the root dir
  493. func GetContentsList(ctx *context.APIContext) {
  494. // swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList
  495. // ---
  496. // summary: Gets the metadata of all the entries of the root dir
  497. // produces:
  498. // - application/json
  499. // parameters:
  500. // - name: owner
  501. // in: path
  502. // description: owner of the repo
  503. // type: string
  504. // required: true
  505. // - name: repo
  506. // in: path
  507. // description: name of the repo
  508. // type: string
  509. // required: true
  510. // - name: ref
  511. // in: query
  512. // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
  513. // type: string
  514. // required: false
  515. // responses:
  516. // "200":
  517. // "$ref": "#/responses/ContentsListResponse"
  518. // "404":
  519. // "$ref": "#/responses/notFound"
  520. // same as GetContents(), this function is here because swagger fails if path is empty in GetContents() interface
  521. GetContents(ctx)
  522. }