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