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  }