gitea/services/repository/files/file.go
Denys Konovalov 9a071a596f
Add API endpoint to request contents of multiple files simultaniously (#34139)
Adds an API POST endpoint under `/repos/{owner}/{repo}/file-contents`
which receives a list of paths and returns a list of the contents of
these files.

This API endpoint will be helpful for applications like headless CMS
(reference: https://github.com/sveltia/sveltia-cms/issues/198) which
need to retrieve a large number of files by reducing the amount of
needed API calls.

Close #33495

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-04-22 01:20:11 +08:00

149 lines
4.8 KiB
Go

// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package files
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"time"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
)
func GetContentsListFromTreePaths(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treePaths []string) (files []*api.ContentsResponse) {
var size int64
for _, treePath := range treePaths {
fileContents, _ := GetContents(ctx, repo, refCommit, treePath, false) // ok if fails, then will be nil
if fileContents != nil && fileContents.Content != nil && *fileContents.Content != "" {
// if content isn't empty (e.g. due to the single blob being too large), add file size to response size
size += int64(len(*fileContents.Content))
}
if size > setting.API.DefaultMaxResponseSize {
break // stop if max response size would be exceeded
}
files = append(files, fileContents)
if len(files) == setting.API.DefaultPagingNum {
break // stop if paging num reached
}
}
return files
}
func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treeNames []string) (*api.FilesResponse, error) {
files := GetContentsListFromTreePaths(ctx, repo, refCommit, treeNames)
fileCommitResponse, _ := GetFileCommitResponse(repo, refCommit.Commit) // ok if fails, then will be nil
verification := GetPayloadCommitVerification(ctx, refCommit.Commit)
filesResponse := &api.FilesResponse{
Files: files,
Commit: fileCommitResponse,
Verification: verification,
}
return filesResponse, nil
}
// constructs a FileResponse with the file at the index from FilesResponse
func GetFileResponseFromFilesResponse(filesResponse *api.FilesResponse, index int) *api.FileResponse {
content := &api.ContentsResponse{}
if len(filesResponse.Files) > index {
content = filesResponse.Files[index]
}
fileResponse := &api.FileResponse{
Content: content,
Commit: filesResponse.Commit,
Verification: filesResponse.Verification,
}
return fileResponse
}
// GetFileCommitResponse Constructs a FileCommitResponse from a Commit object
func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*api.FileCommitResponse, error) {
if repo == nil {
return nil, errors.New("repo cannot be nil")
}
if commit == nil {
return nil, errors.New("commit cannot be nil")
}
commitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String()))
commitTreeURL, _ := url.Parse(repo.APIURL() + "/git/trees/" + url.PathEscape(commit.Tree.ID.String()))
parents := make([]*api.CommitMeta, commit.ParentCount())
for i := 0; i <= commit.ParentCount(); i++ {
if parent, err := commit.Parent(i); err == nil && parent != nil {
parentCommitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + url.PathEscape(parent.ID.String()))
parents[i] = &api.CommitMeta{
SHA: parent.ID.String(),
URL: parentCommitURL.String(),
}
}
}
commitHTMLURL, _ := url.Parse(repo.HTMLURL() + "/commit/" + url.PathEscape(commit.ID.String()))
fileCommit := &api.FileCommitResponse{
CommitMeta: api.CommitMeta{
SHA: commit.ID.String(),
URL: commitURL.String(),
},
HTMLURL: commitHTMLURL.String(),
Author: &api.CommitUser{
Identity: api.Identity{
Name: commit.Author.Name,
Email: commit.Author.Email,
},
Date: commit.Author.When.UTC().Format(time.RFC3339),
},
Committer: &api.CommitUser{
Identity: api.Identity{
Name: commit.Committer.Name,
Email: commit.Committer.Email,
},
Date: commit.Committer.When.UTC().Format(time.RFC3339),
},
Message: commit.Message(),
Tree: &api.CommitMeta{
URL: commitTreeURL.String(),
SHA: commit.Tree.ID.String(),
},
Parents: parents,
}
return fileCommit, nil
}
// ErrFilenameInvalid represents a "FilenameInvalid" kind of error.
type ErrFilenameInvalid struct {
Path string
}
// IsErrFilenameInvalid checks if an error is an ErrFilenameInvalid.
func IsErrFilenameInvalid(err error) bool {
_, ok := err.(ErrFilenameInvalid)
return ok
}
func (err ErrFilenameInvalid) Error() string {
return fmt.Sprintf("path contains a malformed path component [path: %s]", err.Path)
}
func (err ErrFilenameInvalid) Unwrap() error {
return util.ErrInvalidArgument
}
// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory
func CleanUploadFileName(name string) string {
// Rebase the filename
name = util.PathJoinRel(name)
// Git disallows any filenames to have a .git directory in them.
for _, part := range strings.Split(name, "/") {
if strings.ToLower(part) == ".git" {
return ""
}
}
return name
}