Skip to content

Commit 2262811

Browse files
richmahntechknowlogick
authored andcommitted
Fixes 4762 - Content API for Creating, Updating, Deleting Files (#6314)
1 parent 059195b commit 2262811

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+4154
-563
lines changed

custom/conf/app.ini.sample

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,8 @@ MAX_RESPONSE_ITEMS = 50
672672
DEFAULT_PAGING_NUM = 30
673673
; Default and maximum number of items per page for git trees api
674674
DEFAULT_GIT_TREES_PER_PAGE = 1000
675+
; Default size of a blob returned by the blobs API (default is 10MiB)
676+
DEFAULT_MAX_BLOB_SIZE = 10485760
675677

676678
[oauth2]
677679
; Enables OAuth2 provider

docs/content/doc/advanced/config-cheat-sheet.en-us.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,8 +402,9 @@ NB: You must `REDIRECT_MACARON_LOG` and have `DISABLE_ROUTER_LOG` set to `false`
402402

403403
- `ENABLE_SWAGGER`: **true**: Enables /api/swagger, /api/v1/swagger etc. endpoints. True or false; default is true.
404404
- `MAX_RESPONSE_ITEMS`: **50**: Max number of items in a page.
405-
- `DEFAULT_PAGING_NUM`: **30**: Default paging number of api.
406-
- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: Default and maximum number of items per page for git trees api.
405+
- `DEFAULT_PAGING_NUM`: **30**: Default paging number of API.
406+
- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: Default and maximum number of items per page for git trees API.
407+
- `DEFAULT_MAX_BLOB_SIZE`: **10485760**: Default max size of a blob that can be return by the blobs API.
407408

408409
## OAuth2 (`oauth2`)
409410

docs/content/doc/advanced/config-cheat-sheet.zh-cn.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,8 @@ menu:
215215
- `ENABLE_SWAGGER`: **true**: 是否启用swagger路由 /api/swagger, /api/v1/swagger etc. endpoints. True 或 false; 默认是 true.
216216
- `MAX_RESPONSE_ITEMS`: **50**: 一个页面最大的项目数。
217217
- `DEFAULT_PAGING_NUM`: **30**: API中默认分页条数。
218-
- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: GIT TREES API每页的默认和最大项数.
218+
- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: GIT TREES API每页的默认最大项数.
219+
- `DEFAULT_MAX_BLOB_SIZE`: **10485760**: BLOBS API默认最大大小.
219220

220221
## Markup (`markup`)
221222

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package integrations
6+
7+
import (
8+
"net/http"
9+
"path/filepath"
10+
"testing"
11+
12+
"code.gitea.io/gitea/models"
13+
"code.gitea.io/gitea/modules/base"
14+
"code.gitea.io/gitea/modules/context"
15+
"code.gitea.io/gitea/modules/setting"
16+
api "code.gitea.io/sdk/gitea"
17+
18+
"github.com/stretchr/testify/assert"
19+
)
20+
21+
func getExpectedFileContentResponseForFileContents(branch string) *api.FileContentResponse {
22+
treePath := "README.md"
23+
sha := "4b4851ad51df6a7d9f25c979345979eaeb5b349f"
24+
return &api.FileContentResponse{
25+
Name: filepath.Base(treePath),
26+
Path: treePath,
27+
SHA: sha,
28+
Size: 30,
29+
URL: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
30+
HTMLURL: setting.AppURL + "user2/repo1/blob/" + branch + "/" + treePath,
31+
GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
32+
DownloadURL: setting.AppURL + "user2/repo1/raw/branch/" + branch + "/" + treePath,
33+
Type: "blob",
34+
Links: &api.FileLinksResponse{
35+
Self: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
36+
GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
37+
HTMLURL: setting.AppURL + "user2/repo1/blob/" + branch + "/" + treePath,
38+
},
39+
}
40+
}
41+
42+
func TestAPIGetFileContents(t *testing.T) {
43+
prepareTestEnv(t)
44+
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16
45+
user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3, is an org
46+
user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos
47+
repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo
48+
repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo
49+
repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
50+
treePath := "README.md"
51+
52+
// Get user2's token
53+
session := loginUser(t, user2.Name)
54+
token2 := getTokenForLoggedInUser(t, session)
55+
session = emptyTestSession(t)
56+
// Get user4's token
57+
session = loginUser(t, user4.Name)
58+
token4 := getTokenForLoggedInUser(t, session)
59+
session = emptyTestSession(t)
60+
61+
// Make a second master branch in repo1
62+
repo1.CreateNewBranch(user2, repo1.DefaultBranch, "master2")
63+
64+
// ref is default branch
65+
branch := repo1.DefaultBranch
66+
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, branch)
67+
resp := session.MakeRequest(t, req, http.StatusOK)
68+
var fileContentResponse api.FileContentResponse
69+
DecodeJSON(t, resp, &fileContentResponse)
70+
assert.NotNil(t, fileContentResponse)
71+
expectedFileContentResponse := getExpectedFileContentResponseForFileContents(branch)
72+
assert.EqualValues(t, *expectedFileContentResponse, fileContentResponse)
73+
74+
// No ref
75+
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath)
76+
resp = session.MakeRequest(t, req, http.StatusOK)
77+
DecodeJSON(t, resp, &fileContentResponse)
78+
assert.NotNil(t, fileContentResponse)
79+
expectedFileContentResponse = getExpectedFileContentResponseForFileContents(repo1.DefaultBranch)
80+
assert.EqualValues(t, *expectedFileContentResponse, fileContentResponse)
81+
82+
// ref is master2
83+
branch = "master2"
84+
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, branch)
85+
resp = session.MakeRequest(t, req, http.StatusOK)
86+
DecodeJSON(t, resp, &fileContentResponse)
87+
assert.NotNil(t, fileContentResponse)
88+
expectedFileContentResponse = getExpectedFileContentResponseForFileContents("master2")
89+
assert.EqualValues(t, *expectedFileContentResponse, fileContentResponse)
90+
91+
// Test file contents a file with the wrong branch
92+
branch = "badbranch"
93+
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, branch)
94+
resp = session.MakeRequest(t, req, http.StatusInternalServerError)
95+
expectedAPIError := context.APIError{
96+
Message: "object does not exist [id: " + branch + ", rel_path: ]",
97+
URL: base.DocURL,
98+
}
99+
var apiError context.APIError
100+
DecodeJSON(t, resp, &apiError)
101+
assert.Equal(t, expectedAPIError, apiError)
102+
103+
// Test accessing private branch with user token that does not have access - should fail
104+
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4)
105+
session.MakeRequest(t, req, http.StatusNotFound)
106+
107+
// Test access private branch of owner of token
108+
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/readme.md?token=%s", user2.Name, repo16.Name, token2)
109+
session.MakeRequest(t, req, http.StatusOK)
110+
111+
// Test access of org user3 private repo file by owner user2
112+
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2)
113+
session.MakeRequest(t, req, http.StatusOK)
114+
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package integrations
6+
7+
import (
8+
"encoding/base64"
9+
"fmt"
10+
"net/http"
11+
"path/filepath"
12+
"testing"
13+
14+
"code.gitea.io/gitea/models"
15+
"code.gitea.io/gitea/modules/base"
16+
"code.gitea.io/gitea/modules/context"
17+
"code.gitea.io/gitea/modules/git"
18+
"code.gitea.io/gitea/modules/setting"
19+
api "code.gitea.io/sdk/gitea"
20+
21+
"github.com/stretchr/testify/assert"
22+
)
23+
24+
func getCreateFileOptions() api.CreateFileOptions {
25+
content := "This is new text"
26+
contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
27+
return api.CreateFileOptions{
28+
FileOptions: api.FileOptions{
29+
BranchName: "master",
30+
NewBranchName: "master",
31+
Message: "Creates new/file.txt",
32+
Author: api.Identity{
33+
Name: "John Doe",
34+
35+
},
36+
Committer: api.Identity{
37+
Name: "Jane Doe",
38+
39+
},
40+
},
41+
Content: contentEncoded,
42+
}
43+
}
44+
45+
func getExpectedFileResponseForCreate(commitID, treePath string) *api.FileResponse {
46+
sha := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
47+
return &api.FileResponse{
48+
Content: &api.FileContentResponse{
49+
Name: filepath.Base(treePath),
50+
Path: treePath,
51+
SHA: sha,
52+
Size: 16,
53+
URL: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
54+
HTMLURL: setting.AppURL + "user2/repo1/blob/master/" + treePath,
55+
GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
56+
DownloadURL: setting.AppURL + "user2/repo1/raw/branch/master/" + treePath,
57+
Type: "blob",
58+
Links: &api.FileLinksResponse{
59+
Self: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
60+
GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
61+
HTMLURL: setting.AppURL + "user2/repo1/blob/master/" + treePath,
62+
},
63+
},
64+
Commit: &api.FileCommitResponse{
65+
CommitMeta: api.CommitMeta{
66+
URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID,
67+
SHA: commitID,
68+
},
69+
HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID,
70+
Author: &api.CommitUser{
71+
Identity: api.Identity{
72+
Name: "Jane Doe",
73+
74+
},
75+
},
76+
Committer: &api.CommitUser{
77+
Identity: api.Identity{
78+
Name: "John Doe",
79+
80+
},
81+
},
82+
Message: "Updates README.md\n",
83+
},
84+
Verification: &api.PayloadCommitVerification{
85+
Verified: false,
86+
Reason: "unsigned",
87+
Signature: "",
88+
Payload: "",
89+
},
90+
}
91+
}
92+
93+
func TestAPICreateFile(t *testing.T) {
94+
prepareTestEnv(t)
95+
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16
96+
user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3, is an org
97+
user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos
98+
repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo
99+
repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo
100+
repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
101+
fileID := 0
102+
103+
// Get user2's token
104+
session := loginUser(t, user2.Name)
105+
token2 := getTokenForLoggedInUser(t, session)
106+
session = emptyTestSession(t)
107+
// Get user4's token
108+
session = loginUser(t, user4.Name)
109+
token4 := getTokenForLoggedInUser(t, session)
110+
session = emptyTestSession(t)
111+
112+
// Test creating a file in repo1 which user2 owns, try both with branch and empty branch
113+
for _, branch := range [...]string{
114+
"master", // Branch
115+
"", // Empty branch
116+
} {
117+
createFileOptions := getCreateFileOptions()
118+
createFileOptions.BranchName = branch
119+
fileID++
120+
treePath := fmt.Sprintf("new/file%d.txt", fileID)
121+
url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
122+
req := NewRequestWithJSON(t, "POST", url, &createFileOptions)
123+
resp := session.MakeRequest(t, req, http.StatusCreated)
124+
gitRepo, _ := git.OpenRepository(repo1.RepoPath())
125+
commitID, _ := gitRepo.GetBranchCommitID(createFileOptions.NewBranchName)
126+
expectedFileResponse := getExpectedFileResponseForCreate(commitID, treePath)
127+
var fileResponse api.FileResponse
128+
DecodeJSON(t, resp, &fileResponse)
129+
assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
130+
assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
131+
assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
132+
assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
133+
assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
134+
}
135+
136+
// Test creating a file in a new branch
137+
createFileOptions := getCreateFileOptions()
138+
createFileOptions.BranchName = repo1.DefaultBranch
139+
createFileOptions.NewBranchName = "new_branch"
140+
fileID++
141+
treePath := fmt.Sprintf("new/file%d.txt", fileID)
142+
url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
143+
req := NewRequestWithJSON(t, "POST", url, &createFileOptions)
144+
resp := session.MakeRequest(t, req, http.StatusCreated)
145+
var fileResponse api.FileResponse
146+
DecodeJSON(t, resp, &fileResponse)
147+
expectedSHA := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
148+
expectedHTMLURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/blob/new_branch/new/file%d.txt", fileID)
149+
expectedDownloadURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/raw/branch/new_branch/new/file%d.txt", fileID)
150+
assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
151+
assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL)
152+
assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL)
153+
154+
// Test trying to create a file that already exists, should fail
155+
createFileOptions = getCreateFileOptions()
156+
treePath = "README.md"
157+
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
158+
req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
159+
resp = session.MakeRequest(t, req, http.StatusInternalServerError)
160+
expectedAPIError := context.APIError{
161+
Message: "repository file already exists [path: " + treePath + "]",
162+
URL: base.DocURL,
163+
}
164+
var apiError context.APIError
165+
DecodeJSON(t, resp, &apiError)
166+
assert.Equal(t, expectedAPIError, apiError)
167+
168+
// Test creating a file in repo1 by user4 who does not have write access
169+
createFileOptions = getCreateFileOptions()
170+
fileID++
171+
treePath = fmt.Sprintf("new/file%d.txt", fileID)
172+
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4)
173+
req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
174+
session.MakeRequest(t, req, http.StatusNotFound)
175+
176+
// Tests a repo with no token given so will fail
177+
createFileOptions = getCreateFileOptions()
178+
fileID++
179+
treePath = fmt.Sprintf("new/file%d.txt", fileID)
180+
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath)
181+
req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
182+
session.MakeRequest(t, req, http.StatusNotFound)
183+
184+
// Test using access token for a private repo that the user of the token owns
185+
createFileOptions = getCreateFileOptions()
186+
fileID++
187+
treePath = fmt.Sprintf("new/file%d.txt", fileID)
188+
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token2)
189+
req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
190+
session.MakeRequest(t, req, http.StatusCreated)
191+
192+
// Test using org repo "user3/repo3" where user2 is a collaborator
193+
createFileOptions = getCreateFileOptions()
194+
fileID++
195+
treePath = fmt.Sprintf("new/file%d.txt", fileID)
196+
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2)
197+
req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
198+
session.MakeRequest(t, req, http.StatusCreated)
199+
200+
// Test using org repo "user3/repo3" with no user token
201+
createFileOptions = getCreateFileOptions()
202+
fileID++
203+
treePath = fmt.Sprintf("new/file%d.txt", fileID)
204+
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user3.Name, repo3.Name, treePath)
205+
req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
206+
session.MakeRequest(t, req, http.StatusNotFound)
207+
208+
// Test using repo "user2/repo1" where user4 is a NOT collaborator
209+
createFileOptions = getCreateFileOptions()
210+
fileID++
211+
treePath = fmt.Sprintf("new/file%d.txt", fileID)
212+
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token4)
213+
req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
214+
session.MakeRequest(t, req, http.StatusForbidden)
215+
}

0 commit comments

Comments
 (0)