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