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  }