Skip to content

Commit ff00c13

Browse files
petergardfjallpetergardfjall
authored andcommitted
Add GetArchiveReader() (#476)
This PR close #475 It complements the `Client.GetArchive` call, which returns a byte slice and hence is unsuitable for use with large repositories, with a `Client.GetArchiveReader` method that returns a `io.ReadCloser` that streams the retrieved archvie and, therefore, induces a much smaller memory footprint on the calling client. Co-authored-by: Peter Gardfjäll <[email protected]> Reviewed-on: https://gitea.com/gitea/go-sdk/pulls/476 Reviewed-by: 6543 <[email protected]> Reviewed-by: Norwin <[email protected]> Co-authored-by: petergardfjall <[email protected]> Co-committed-by: petergardfjall <[email protected]>
1 parent 30e7dc9 commit ff00c13

File tree

3 files changed

+82
-18
lines changed

3 files changed

+82
-18
lines changed

gitea/client.go

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -198,37 +198,65 @@ func (c *Client) doRequest(method, path string, header http.Header, body io.Read
198198
return &Response{resp}, nil
199199
}
200200

201-
func (c *Client) getResponse(method, path string, header http.Header, body io.Reader) ([]byte, *Response, error) {
202-
resp, err := c.doRequest(method, path, header, body)
203-
if err != nil {
204-
return nil, nil, err
201+
// Converts a response for a HTTP status code indicating an error condition
202+
// (non-2XX) to a well-known error value and response body. For non-problematic
203+
// (2XX) status codes nil will be returned. Note that on a non-2XX response, the
204+
// response body stream will have been read and, hence, is closed on return.
205+
func statusCodeToErr(resp *Response) (body []byte, err error) {
206+
// no error
207+
if resp.StatusCode/100 == 2 {
208+
return nil, nil
205209
}
206-
defer resp.Body.Close()
207210

211+
//
212+
// error: body will be read for details
213+
//
214+
defer resp.Body.Close()
208215
data, err := ioutil.ReadAll(resp.Body)
209216
if err != nil {
210-
return nil, resp, err
217+
return nil, fmt.Errorf("body read on HTTP error %d: %v", resp.StatusCode, err)
211218
}
212219

213220
switch resp.StatusCode {
214221
case 403:
215-
return data, resp, errors.New("403 Forbidden")
222+
return data, errors.New("403 Forbidden")
216223
case 404:
217-
return data, resp, errors.New("404 Not Found")
224+
return data, errors.New("404 Not Found")
218225
case 409:
219-
return data, resp, errors.New("409 Conflict")
226+
return data, errors.New("409 Conflict")
220227
case 422:
221-
return data, resp, fmt.Errorf("422 Unprocessable Entity: %s", string(data))
228+
return data, fmt.Errorf("422 Unprocessable Entity: %s", string(data))
229+
}
230+
231+
path := resp.Request.URL.Path
232+
method := resp.Request.Method
233+
header := resp.Request.Header
234+
errMap := make(map[string]interface{})
235+
if err = json.Unmarshal(data, &errMap); err != nil {
236+
// when the JSON can't be parsed, data was probably empty or a
237+
// plain string, so we try to return a helpful error anyway
238+
return data, fmt.Errorf("Unknown API Error: %d\nRequest: '%s' with '%s' method '%s' header and '%s' body", resp.StatusCode, path, method, header, string(data))
239+
}
240+
return data, errors.New(errMap["message"].(string))
241+
}
242+
243+
func (c *Client) getResponse(method, path string, header http.Header, body io.Reader) ([]byte, *Response, error) {
244+
resp, err := c.doRequest(method, path, header, body)
245+
if err != nil {
246+
return nil, nil, err
222247
}
248+
defer resp.Body.Close()
223249

224-
if resp.StatusCode/100 != 2 {
225-
errMap := make(map[string]interface{})
226-
if err = json.Unmarshal(data, &errMap); err != nil {
227-
// when the JSON can't be parsed, data was probably empty or a plain string,
228-
// so we try to return a helpful error anyway
229-
return data, resp, fmt.Errorf("Unknown API Error: %d\nRequest: '%s' with '%s' method '%s' header and '%s' body", resp.StatusCode, path, method, header, string(data))
230-
}
231-
return data, resp, errors.New(errMap["message"].(string))
250+
// check for errors
251+
data, err := statusCodeToErr(resp)
252+
if err != nil {
253+
return data, resp, err
254+
}
255+
256+
// success (2XX), read body
257+
data, err = ioutil.ReadAll(resp.Body)
258+
if err != nil {
259+
return nil, resp, err
232260
}
233261

234262
return data, resp, nil

gitea/repo.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"bytes"
1010
"encoding/json"
1111
"fmt"
12+
"io"
1213
"net/url"
1314
"strings"
1415
"time"
@@ -420,3 +421,20 @@ const (
420421
func (c *Client) GetArchive(owner, repo, ref string, ext ArchiveType) ([]byte, *Response, error) {
421422
return c.getResponse("GET", fmt.Sprintf("/repos/%s/%s/archive/%s%s", owner, repo, url.PathEscape(ref), ext), nil, nil)
422423
}
424+
425+
// GetArchiveReader gets a `git archive` for a particular tree-ish git reference
426+
// such as a branch name (`master`), a commit hash (`70b7c74b33`), a tag
427+
// (`v1.2.1`). The archive is returned as a byte stream in a ReadCloser. It is
428+
// the responsibility of the client to close the reader.
429+
func (c *Client) GetArchiveReader(owner, repo, ref string, ext ArchiveType) (io.ReadCloser, *Response, error) {
430+
resp, err := c.doRequest("GET", fmt.Sprintf("/repos/%s/%s/archive/%s%s", owner, repo, url.PathEscape(ref), ext), nil, nil)
431+
if err != nil {
432+
return nil, resp, err
433+
}
434+
435+
if _, err := statusCodeToErr(resp); err != nil {
436+
return nil, resp, err
437+
}
438+
439+
return resp.Body, resp, nil
440+
}

gitea/repo_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
package gitea
66

77
import (
8+
"bytes"
9+
"io"
810
"log"
911
"testing"
1012
"time"
@@ -139,6 +141,22 @@ func TestGetArchive(t *testing.T) {
139141
assert.EqualValues(t, 1620, len(archive))
140142
}
141143

144+
func TestGetArchiveReader(t *testing.T) {
145+
log.Println("== TestGetArchiveReader ==")
146+
c := newTestClient()
147+
repo, _ := createTestRepo(t, "ToDownload", c)
148+
time.Sleep(time.Second / 2)
149+
r, _, err := c.GetArchiveReader(repo.Owner.UserName, repo.Name, "master", ZipArchive)
150+
assert.NoError(t, err)
151+
defer r.Close()
152+
153+
archive := bytes.NewBuffer(nil)
154+
nBytes, err := io.Copy(archive, r)
155+
assert.NoError(t, err)
156+
assert.EqualValues(t, 1620, nBytes)
157+
assert.EqualValues(t, 1620, len(archive.Bytes()))
158+
}
159+
142160
// standard func to create a init repo for test routines
143161
func createTestRepo(t *testing.T, name string, c *Client) (*Repository, error) {
144162
user, _, uErr := c.GetMyUserInfo()

0 commit comments

Comments
 (0)