Skip to content

Commit 18811c7

Browse files
committed
Switch pagination method and add test
1 parent 5c5577a commit 18811c7

File tree

5 files changed

+108
-61
lines changed

5 files changed

+108
-61
lines changed

routers/api/v1/repo/file.go

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ import (
2525
"code.gitea.io/gitea/modules/setting"
2626
"code.gitea.io/gitea/modules/storage"
2727
api "code.gitea.io/gitea/modules/structs"
28-
"code.gitea.io/gitea/modules/util"
2928
"code.gitea.io/gitea/modules/web"
30-
"code.gitea.io/gitea/routers/api/v1/utils"
3129
"code.gitea.io/gitea/routers/common"
3230
"code.gitea.io/gitea/services/context"
3331
pull_service "code.gitea.io/gitea/services/pull"
@@ -990,6 +988,11 @@ func GetFiles(ctx *context.APIContext) {
990988
// swagger:operation POST /repos/{owner}/{repo}/files repository repoGetFiles
991989
// ---
992990
// summary: Get the metadata and contents of requested files
991+
// description: Uses automatic pagination based on default page size and
992+
// max response size and returns the maximum allowed number of files.
993+
// Files which could not be retrieved are null. Blobs which are too large
994+
// are being returned with `content = ""` and `size > 0`, they can be
995+
// requested using the `download_url`.
993996
// produces:
994997
// - application/json
995998
// parameters:
@@ -1008,14 +1011,6 @@ func GetFiles(ctx *context.APIContext) {
10081011
// description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
10091012
// type: string
10101013
// required: false
1011-
// - name: page
1012-
// in: query
1013-
// description: page number of results to return (1-based)
1014-
// type: integer
1015-
// - name: limit
1016-
// in: query
1017-
// description: page size of results
1018-
// type: integer
10191014
// - name: body
10201015
// in: body
10211016
// required: true
@@ -1026,8 +1021,6 @@ func GetFiles(ctx *context.APIContext) {
10261021
// "$ref": "#/responses/ContentsListResponse"
10271022
// "404":
10281023
// "$ref": "#/responses/notFound"
1029-
// "413":
1030-
// "$ref": "#/responses/contentTooLarge"
10311024

10321025
apiOpts := web.GetForm(ctx).(*api.GetFilesOptions)
10331026

@@ -1042,16 +1035,7 @@ func GetFiles(ctx *context.APIContext) {
10421035
}
10431036

10441037
files := apiOpts.Files
1038+
filesResponse := files_service.GetContentsListFromTrees(ctx, ctx.Repo.Repository, ref, files)
10451039

1046-
count := len(files)
1047-
listOpts := utils.GetListOptions(ctx)
1048-
files = util.PaginateSlice(files, listOpts.Page, listOpts.PageSize).([]string)
1049-
1050-
filesResponse, err := files_service.GetContentsListFromTrees(ctx, ctx.Repo.Repository, ref, files)
1051-
if err != nil {
1052-
ctx.APIError(http.StatusRequestEntityTooLarge, err.Error())
1053-
}
1054-
1055-
ctx.SetTotalCountHeader(int64(count))
10561040
ctx.JSON(http.StatusOK, filesResponse)
10571041
}

services/context/api.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,6 @@ type APIForbiddenError struct {
9191
// swagger:response notFound
9292
type APINotFound struct{}
9393

94-
// APIContentTooLarge is a content too large error response
95-
// swagger:response contentTooLarge
96-
type APIContentTooLarge struct {
97-
APIError
98-
}
99-
10094
// APIConflict is a conflict empty response
10195
// swagger:response conflict
10296
type APIConflict struct{}

services/repository/files/file.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
"code.gitea.io/gitea/modules/util"
1919
)
2020

21-
func GetContentsListFromTrees(ctx context.Context, repo *repo_model.Repository, branch string, treeNames []string) ([]*api.ContentsResponse, error) {
21+
func GetContentsListFromTrees(ctx context.Context, repo *repo_model.Repository, branch string, treeNames []string) []*api.ContentsResponse {
2222
files := []*api.ContentsResponse{}
2323
var size int64
2424
for _, file := range treeNames {
@@ -27,18 +27,18 @@ func GetContentsListFromTrees(ctx context.Context, repo *repo_model.Repository,
2727
size += fileContents.Size // if content isn't empty (e. g. due to the single blob being too large), add file size to response size
2828
}
2929
if size > setting.API.DefaultMaxResponseSize {
30-
return nil, errors.New("the combined size of the requested blobs exceeds the per-request limit set by the server administrator")
30+
return files // return if max page size would be exceeded
3131
}
3232
files = append(files, fileContents)
33+
if len(files) == setting.API.DefaultPagingNum {
34+
return files // return if paging num or max size reached
35+
}
3336
}
34-
return files, nil
37+
return files
3538
}
3639

3740
func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branch string, treeNames []string) (*api.FilesResponse, error) {
38-
files, err := GetContentsListFromTrees(ctx, repo, branch, treeNames)
39-
if err != nil {
40-
return nil, err
41-
}
41+
files := GetContentsListFromTrees(ctx, repo, branch, treeNames)
4242
fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil
4343
verification := GetPayloadCommitVerification(ctx, commit)
4444
filesResponse := &api.FilesResponse{

templates/swagger/v1_json.tmpl

Lines changed: 1 addition & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/api_repo_files_get_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@
44
package integration
55

66
import (
7+
"encoding/base64"
78
"fmt"
89
"net/http"
910
"net/url"
1011
"testing"
12+
"time"
1113

1214
auth_model "code.gitea.io/gitea/models/auth"
15+
"code.gitea.io/gitea/models/db"
1316
repo_model "code.gitea.io/gitea/models/repo"
1417
"code.gitea.io/gitea/models/unittest"
1518
user_model "code.gitea.io/gitea/models/user"
1619
"code.gitea.io/gitea/modules/git"
1720
"code.gitea.io/gitea/modules/gitrepo"
21+
"code.gitea.io/gitea/modules/setting"
1822
api "code.gitea.io/gitea/modules/structs"
1923
repo_service "code.gitea.io/gitea/services/repository"
2024

@@ -142,4 +146,94 @@ func testAPIGetRequestedFiles(t *testing.T, u *url.URL) {
142146
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/files", org3.Name, repo3.Name), &filesOptions).
143147
AddTokenAuth(token2)
144148
MakeRequest(t, req, http.StatusOK)
149+
150+
// Test pagination
151+
for i := 0; i < 40; i++ {
152+
filesOptions.Files = append(filesOptions.Files, filesOptions.Files[0])
153+
}
154+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/files", user2.Name, repo1.Name), &filesOptions)
155+
resp = MakeRequest(t, req, http.StatusOK)
156+
DecodeJSON(t, resp, &contentsListResponse)
157+
assert.NotNil(t, contentsListResponse)
158+
assert.Equal(t, setting.API.DefaultPagingNum, len(contentsListResponse))
159+
160+
// create new repo for large file tests
161+
baseRepo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
162+
Name: "repo-test-files-api",
163+
Description: "test files api",
164+
AutoInit: true,
165+
Gitignores: "Go",
166+
License: "MIT",
167+
Readme: "Default",
168+
DefaultBranch: "main",
169+
IsPrivate: false,
170+
})
171+
assert.NoError(t, err)
172+
assert.NotEmpty(t, baseRepo)
173+
174+
// Test file size limit
175+
largeFile := make([]byte, 15728640) // 15 MiB -> over max blob size
176+
for i := range largeFile {
177+
largeFile[i] = byte(i % 256)
178+
}
179+
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
180+
doAPICreateFile(user2APICtx, "large-file.txt", &api.CreateFileOptions{
181+
FileOptions: api.FileOptions{
182+
Message: "create large-file.txt",
183+
Author: api.Identity{
184+
Name: user2.Name,
185+
Email: user2.Email,
186+
},
187+
Committer: api.Identity{
188+
Name: user2.Name,
189+
Email: user2.Email,
190+
},
191+
Dates: api.CommitDateOptions{
192+
Author: time.Now(),
193+
Committer: time.Now(),
194+
},
195+
},
196+
ContentBase64: base64.StdEncoding.EncodeToString(largeFile),
197+
})(t)
198+
199+
filesOptions.Files = []string{"large-file.txt"}
200+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/files", user2.Name, baseRepo.Name), &filesOptions)
201+
resp = MakeRequest(t, req, http.StatusOK)
202+
DecodeJSON(t, resp, &contentsListResponse)
203+
assert.NotNil(t, contentsListResponse)
204+
assert.Equal(t, int64(15728640), contentsListResponse[0].Size)
205+
assert.Equal(t, "", *contentsListResponse[0].Content)
206+
207+
// Test response size limit
208+
smallFile := make([]byte, 5242880) // 5 MiB -> under max blob size
209+
for i := range smallFile {
210+
smallFile[i] = byte(i % 256)
211+
}
212+
doAPICreateFile(user2APICtx, "small-file.txt", &api.CreateFileOptions{
213+
FileOptions: api.FileOptions{
214+
Message: "create small-file.txt",
215+
Author: api.Identity{
216+
Name: user2.Name,
217+
Email: user2.Email,
218+
},
219+
Committer: api.Identity{
220+
Name: user2.Name,
221+
Email: user2.Email,
222+
},
223+
Dates: api.CommitDateOptions{
224+
Author: time.Now(),
225+
Committer: time.Now(),
226+
},
227+
},
228+
ContentBase64: base64.StdEncoding.EncodeToString(smallFile),
229+
})(t)
230+
filesOptions.Files = []string{"small-file.txt"}
231+
for i := 0; i < 40; i++ {
232+
filesOptions.Files = append(filesOptions.Files, filesOptions.Files[0])
233+
}
234+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/files", user2.Name, baseRepo.Name), &filesOptions)
235+
resp = MakeRequest(t, req, http.StatusOK)
236+
DecodeJSON(t, resp, &contentsListResponse)
237+
assert.NotNil(t, contentsListResponse)
238+
assert.Equal(t, 20, len(contentsListResponse))
145239
}

0 commit comments

Comments
 (0)