Skip to content

Commit dbd9c69

Browse files
authored
Refactor repo contents API and add "contents-ext" API (#34822)
See the updated swagger document for details.
1 parent 7be1a5e commit dbd9c69

File tree

18 files changed

+475
-256
lines changed

18 files changed

+475
-256
lines changed

modules/git/blob.go

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"encoding/base64"
1010
"errors"
1111
"io"
12+
"strings"
1213

1314
"code.gitea.io/gitea/modules/typesniffer"
1415
"code.gitea.io/gitea/modules/util"
@@ -63,33 +64,37 @@ func (b *Blob) GetBlobLineCount(w io.Writer) (int, error) {
6364
}
6465
}
6566

66-
// GetBlobContentBase64 Reads the content of the blob with a base64 encode and returns the encoded string
67-
func (b *Blob) GetBlobContentBase64() (string, error) {
67+
// GetBlobContentBase64 Reads the content of the blob with a base64 encoding and returns the encoded string
68+
func (b *Blob) GetBlobContentBase64(originContent *strings.Builder) (string, error) {
6869
dataRc, err := b.DataAsync()
6970
if err != nil {
7071
return "", err
7172
}
7273
defer dataRc.Close()
7374

74-
pr, pw := io.Pipe()
75-
encoder := base64.NewEncoder(base64.StdEncoding, pw)
76-
77-
go func() {
78-
_, err := io.Copy(encoder, dataRc)
79-
_ = encoder.Close()
80-
81-
if err != nil {
82-
_ = pw.CloseWithError(err)
83-
} else {
84-
_ = pw.Close()
75+
base64buf := &strings.Builder{}
76+
encoder := base64.NewEncoder(base64.StdEncoding, base64buf)
77+
buf := make([]byte, 32*1024)
78+
loop:
79+
for {
80+
n, err := dataRc.Read(buf)
81+
if n > 0 {
82+
if originContent != nil {
83+
_, _ = originContent.Write(buf[:n])
84+
}
85+
if _, err := encoder.Write(buf[:n]); err != nil {
86+
return "", err
87+
}
88+
}
89+
switch {
90+
case errors.Is(err, io.EOF):
91+
break loop
92+
case err != nil:
93+
return "", err
8594
}
86-
}()
87-
88-
out, err := io.ReadAll(pr)
89-
if err != nil {
90-
return "", err
9195
}
92-
return string(out), nil
96+
_ = encoder.Close()
97+
return base64buf.String(), nil
9398
}
9499

95100
// GuessContentType guesses the content type of the blob.

modules/git/tree_entry_nogogit.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type TreeEntry struct {
1818
sized bool
1919
}
2020

21-
// Name returns the name of the entry
21+
// Name returns the name of the entry (base name)
2222
func (te *TreeEntry) Name() string {
2323
return te.name
2424
}

modules/lfs/pointer.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@ import (
1515
"strings"
1616
)
1717

18+
// spec: https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
1819
const (
19-
blobSizeCutoff = 1024
20+
MetaFileMaxSize = 1024 // spec says the maximum size of a pointer file must be smaller than 1024
2021

21-
// MetaFileIdentifier is the string appearing at the first line of LFS pointer files.
22-
// https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
23-
MetaFileIdentifier = "version https://git-lfs.github.com/spec/v1"
22+
MetaFileIdentifier = "version https://git-lfs.github.com/spec/v1" // the first line of a pointer file
2423

25-
// MetaFileOidPrefix appears in LFS pointer files on a line before the sha256 hash.
26-
MetaFileOidPrefix = "oid sha256:"
24+
MetaFileOidPrefix = "oid sha256:" // spec says the only supported hash is sha256 at the moment
2725
)
2826

2927
var (
@@ -39,7 +37,7 @@ var (
3937

4038
// ReadPointer tries to read LFS pointer data from the reader
4139
func ReadPointer(reader io.Reader) (Pointer, error) {
42-
buf := make([]byte, blobSizeCutoff)
40+
buf := make([]byte, MetaFileMaxSize)
4341
n, err := io.ReadFull(reader, buf)
4442
if err != nil && err != io.ErrUnexpectedEOF {
4543
return Pointer{}, err
@@ -65,6 +63,7 @@ func ReadPointerFromBuffer(buf []byte) (Pointer, error) {
6563
return p, ErrInvalidStructure
6664
}
6765

66+
// spec says "key/value pairs MUST be sorted alphabetically in ascending order (version is exception and must be the first)"
6867
oid := strings.TrimPrefix(splitLines[1], MetaFileOidPrefix)
6968
if len(oid) != 64 || !oidPattern.MatchString(oid) {
7069
return p, ErrInvalidOIDFormat

modules/lfs/pointer_scanner_gogit.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan c
3131
default:
3232
}
3333

34-
if blob.Size > blobSizeCutoff {
34+
if blob.Size > MetaFileMaxSize {
3535
return nil
3636
}
3737

modules/structs/git_blob.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ type GitBlobResponse struct {
1010
URL string `json:"url"`
1111
SHA string `json:"sha"`
1212
Size int64 `json:"size"`
13+
14+
LfsOid *string `json:"lfs_oid,omitempty"`
15+
LfsSize *int64 `json:"lfs_size,omitempty"`
1316
}

modules/structs/repo_file.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ type FileLinksResponse struct {
119119
HTMLURL *string `json:"html"`
120120
}
121121

122+
type ContentsExtResponse struct {
123+
FileContents *ContentsResponse `json:"file_contents,omitempty"`
124+
DirContents []*ContentsResponse `json:"dir_contents,omitempty"`
125+
}
126+
122127
// ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content
123128
type ContentsResponse struct {
124129
Name string `json:"name"`
@@ -145,6 +150,9 @@ type ContentsResponse struct {
145150
// `submodule_git_url` is populated when `type` is `submodule`, otherwise null
146151
SubmoduleGitURL *string `json:"submodule_git_url"`
147152
Links *FileLinksResponse `json:"_links"`
153+
154+
LfsOid *string `json:"lfs_oid"`
155+
LfsSize *int64 `json:"lfs_size"`
148156
}
149157

150158
// FileCommitResponse contains information generated from a Git commit for a repo's file.

routers/api/v1/api.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,6 +1435,10 @@ func Routes() *web.Router {
14351435
m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.DeleteFile)
14361436
}, reqToken())
14371437
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
1438+
m.Group("/contents-ext", func() {
1439+
m.Get("", repo.GetContentsExt)
1440+
m.Get("/*", repo.GetContentsExt)
1441+
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
14381442
m.Combo("/file-contents", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()).
14391443
Get(repo.GetFileContentsGet).
14401444
Post(bind(api.GetFilesOptions{}), repo.GetFileContentsPost) // POST method requires "write" permission, so we also support "GET" method above

routers/api/v1/repo/blob.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func GetBlob(ctx *context.APIContext) {
4747
return
4848
}
4949

50-
if blob, err := files_service.GetBlobBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil {
50+
if blob, err := files_service.GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil {
5151
ctx.APIError(http.StatusBadRequest, err)
5252
} else {
5353
ctx.JSON(http.StatusOK, blob)

routers/api/v1/repo/file.go

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -905,11 +905,71 @@ func resolveRefCommit(ctx *context.APIContext, ref string, minCommitIDLen ...int
905905
return refCommit
906906
}
907907

908+
func GetContentsExt(ctx *context.APIContext) {
909+
// swagger:operation GET /repos/{owner}/{repo}/contents-ext/{filepath} repository repoGetContentsExt
910+
// ---
911+
// summary: The extended "contents" API, to get file metadata and/or content, or list a directory.
912+
// description: It guarantees that only one of the response fields is set if the request succeeds.
913+
// Users can pass "includes=file_content" or "includes=lfs_metadata" to retrieve more fields.
914+
// produces:
915+
// - application/json
916+
// parameters:
917+
// - name: owner
918+
// in: path
919+
// description: owner of the repo
920+
// type: string
921+
// required: true
922+
// - name: repo
923+
// in: path
924+
// description: name of the repo
925+
// type: string
926+
// required: true
927+
// - name: filepath
928+
// in: path
929+
// description: path of the dir, file, symlink or submodule in the repo
930+
// type: string
931+
// required: true
932+
// - name: ref
933+
// in: query
934+
// description: the name of the commit/branch/tag, default to the repository’s default branch.
935+
// type: string
936+
// required: false
937+
// - name: includes
938+
// in: query
939+
// description: By default this API's response only contains file's metadata. Use comma-separated "includes" options to retrieve more fields.
940+
// Option "file_content" will try to retrieve the file content, option "lfs_metadata" will try to retrieve LFS metadata.
941+
// type: string
942+
// required: false
943+
// responses:
944+
// "200":
945+
// "$ref": "#/responses/ContentsExtResponse"
946+
// "404":
947+
// "$ref": "#/responses/notFound"
948+
949+
opts := files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*")}
950+
for includeOpt := range strings.SplitSeq(ctx.FormString("includes"), ",") {
951+
if includeOpt == "" {
952+
continue
953+
}
954+
switch includeOpt {
955+
case "file_content":
956+
opts.IncludeSingleFileContent = true
957+
case "lfs_metadata":
958+
opts.IncludeLfsMetadata = true
959+
default:
960+
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("unknown include option %q", includeOpt))
961+
return
962+
}
963+
}
964+
ctx.JSON(http.StatusOK, getRepoContents(ctx, opts))
965+
}
966+
908967
// GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
909968
func GetContents(ctx *context.APIContext) {
910969
// swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents
911970
// ---
912-
// summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
971+
// summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir.
972+
// description: This API follows GitHub's design, and it is not easy to use. Recommend to use our "contents-ext" API instead.
913973
// produces:
914974
// - application/json
915975
// parameters:
@@ -938,29 +998,35 @@ func GetContents(ctx *context.APIContext) {
938998
// "$ref": "#/responses/ContentsResponse"
939999
// "404":
9401000
// "$ref": "#/responses/notFound"
941-
942-
treePath := ctx.PathParam("*")
943-
refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref"))
1001+
ret := getRepoContents(ctx, files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*"), IncludeSingleFileContent: true})
9441002
if ctx.Written() {
9451003
return
9461004
}
1005+
ctx.JSON(http.StatusOK, util.Iif[any](ret.FileContents != nil, ret.FileContents, ret.DirContents))
1006+
}
9471007

948-
if fileList, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, refCommit, treePath); err != nil {
1008+
func getRepoContents(ctx *context.APIContext, opts files_service.GetContentsOrListOptions) *api.ContentsExtResponse {
1009+
refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref"))
1010+
if ctx.Written() {
1011+
return nil
1012+
}
1013+
ret, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts)
1014+
if err != nil {
9491015
if git.IsErrNotExist(err) {
9501016
ctx.APIErrorNotFound("GetContentsOrList", err)
951-
return
1017+
return nil
9521018
}
9531019
ctx.APIErrorInternal(err)
954-
} else {
955-
ctx.JSON(http.StatusOK, fileList)
9561020
}
1021+
return &ret
9571022
}
9581023

9591024
// GetContentsList Get the metadata of all the entries of the root dir
9601025
func GetContentsList(ctx *context.APIContext) {
9611026
// swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList
9621027
// ---
963-
// summary: Gets the metadata of all the entries of the root dir
1028+
// summary: Gets the metadata of all the entries of the root dir.
1029+
// description: This API follows GitHub's design, and it is not easy to use. Recommend to use our "contents-ext" API instead.
9641030
// produces:
9651031
// - application/json
9661032
// parameters:
@@ -1084,6 +1150,6 @@ func handleGetFileContents(ctx *context.APIContext) {
10841150
if ctx.Written() {
10851151
return
10861152
}
1087-
filesResponse := files_service.GetContentsListFromTreePaths(ctx, ctx.Repo.Repository, refCommit, opts.Files)
1153+
filesResponse := files_service.GetContentsListFromTreePaths(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts.Files)
10881154
ctx.JSON(http.StatusOK, util.SliceNilAsEmpty(filesResponse))
10891155
}

routers/api/v1/repo/wiki.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string {
499499
if blob.Size() > setting.API.DefaultMaxBlobSize {
500500
return ""
501501
}
502-
content, err := blob.GetBlobContentBase64()
502+
content, err := blob.GetBlobContentBase64(nil)
503503
if err != nil {
504504
ctx.APIErrorInternal(err)
505505
return ""

routers/api/v1/swagger/repo.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,12 @@ type swaggerContentsListResponse struct {
331331
Body []api.ContentsResponse `json:"body"`
332332
}
333333

334+
// swagger:response ContentsExtResponse
335+
type swaggerContentsExtResponse struct {
336+
// in:body
337+
Body api.ContentsExtResponse `json:"body"`
338+
}
339+
334340
// FileDeleteResponse
335341
// swagger:response FileDeleteResponse
336342
type swaggerFileDeleteResponse struct {

0 commit comments

Comments
 (0)