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