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 <peter.gardfjall.work@gmail.com> Reviewed-on: https://gitea.com/gitea/go-sdk/pulls/476 Reviewed-by: 6543 <6543@obermui.de> Reviewed-by: Norwin <noerw@noreply.gitea.io> Co-authored-by: petergardfjall <petergardfjall@noreply.gitea.io> Co-committed-by: petergardfjall <petergardfjall@noreply.gitea.io>
This commit is contained in:
parent
30e7dc9ccb
commit
ff00c13597
3 changed files with 86 additions and 22 deletions
|
@ -198,6 +198,48 @@ func (c *Client) doRequest(method, path string, header http.Header, body io.Read
|
|||
return &Response{resp}, nil
|
||||
}
|
||||
|
||||
// Converts a response for a HTTP status code indicating an error condition
|
||||
// (non-2XX) to a well-known error value and response body. For non-problematic
|
||||
// (2XX) status codes nil will be returned. Note that on a non-2XX response, the
|
||||
// response body stream will have been read and, hence, is closed on return.
|
||||
func statusCodeToErr(resp *Response) (body []byte, err error) {
|
||||
// no error
|
||||
if resp.StatusCode/100 == 2 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
//
|
||||
// error: body will be read for details
|
||||
//
|
||||
defer resp.Body.Close()
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("body read on HTTP error %d: %v", resp.StatusCode, err)
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case 403:
|
||||
return data, errors.New("403 Forbidden")
|
||||
case 404:
|
||||
return data, errors.New("404 Not Found")
|
||||
case 409:
|
||||
return data, errors.New("409 Conflict")
|
||||
case 422:
|
||||
return data, fmt.Errorf("422 Unprocessable Entity: %s", string(data))
|
||||
}
|
||||
|
||||
path := resp.Request.URL.Path
|
||||
method := resp.Request.Method
|
||||
header := resp.Request.Header
|
||||
errMap := make(map[string]interface{})
|
||||
if err = json.Unmarshal(data, &errMap); err != nil {
|
||||
// when the JSON can't be parsed, data was probably empty or a
|
||||
// plain string, so we try to return a helpful error anyway
|
||||
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))
|
||||
}
|
||||
return data, errors.New(errMap["message"].(string))
|
||||
}
|
||||
|
||||
func (c *Client) getResponse(method, path string, header http.Header, body io.Reader) ([]byte, *Response, error) {
|
||||
resp, err := c.doRequest(method, path, header, body)
|
||||
if err != nil {
|
||||
|
@ -205,32 +247,18 @@ func (c *Client) getResponse(method, path string, header http.Header, body io.Re
|
|||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
// check for errors
|
||||
data, err := statusCodeToErr(resp)
|
||||
if err != nil {
|
||||
return data, resp, err
|
||||
}
|
||||
|
||||
// success (2XX), read body
|
||||
data, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case 403:
|
||||
return data, resp, errors.New("403 Forbidden")
|
||||
case 404:
|
||||
return data, resp, errors.New("404 Not Found")
|
||||
case 409:
|
||||
return data, resp, errors.New("409 Conflict")
|
||||
case 422:
|
||||
return data, resp, fmt.Errorf("422 Unprocessable Entity: %s", string(data))
|
||||
}
|
||||
|
||||
if resp.StatusCode/100 != 2 {
|
||||
errMap := make(map[string]interface{})
|
||||
if err = json.Unmarshal(data, &errMap); err != nil {
|
||||
// when the JSON can't be parsed, data was probably empty or a plain string,
|
||||
// so we try to return a helpful error anyway
|
||||
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))
|
||||
}
|
||||
return data, resp, errors.New(errMap["message"].(string))
|
||||
}
|
||||
|
||||
return data, resp, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -420,3 +421,20 @@ const (
|
|||
func (c *Client) GetArchive(owner, repo, ref string, ext ArchiveType) ([]byte, *Response, error) {
|
||||
return c.getResponse("GET", fmt.Sprintf("/repos/%s/%s/archive/%s%s", owner, repo, url.PathEscape(ref), ext), nil, nil)
|
||||
}
|
||||
|
||||
// GetArchiveReader gets a `git archive` for a particular tree-ish git reference
|
||||
// such as a branch name (`master`), a commit hash (`70b7c74b33`), a tag
|
||||
// (`v1.2.1`). The archive is returned as a byte stream in a ReadCloser. It is
|
||||
// the responsibility of the client to close the reader.
|
||||
func (c *Client) GetArchiveReader(owner, repo, ref string, ext ArchiveType) (io.ReadCloser, *Response, error) {
|
||||
resp, err := c.doRequest("GET", fmt.Sprintf("/repos/%s/%s/archive/%s%s", owner, repo, url.PathEscape(ref), ext), nil, nil)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
if _, err := statusCodeToErr(resp); err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return resp.Body, resp, nil
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
package gitea
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"log"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -139,6 +141,22 @@ func TestGetArchive(t *testing.T) {
|
|||
assert.EqualValues(t, 1620, len(archive))
|
||||
}
|
||||
|
||||
func TestGetArchiveReader(t *testing.T) {
|
||||
log.Println("== TestGetArchiveReader ==")
|
||||
c := newTestClient()
|
||||
repo, _ := createTestRepo(t, "ToDownload", c)
|
||||
time.Sleep(time.Second / 2)
|
||||
r, _, err := c.GetArchiveReader(repo.Owner.UserName, repo.Name, "master", ZipArchive)
|
||||
assert.NoError(t, err)
|
||||
defer r.Close()
|
||||
|
||||
archive := bytes.NewBuffer(nil)
|
||||
nBytes, err := io.Copy(archive, r)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1620, nBytes)
|
||||
assert.EqualValues(t, 1620, len(archive.Bytes()))
|
||||
}
|
||||
|
||||
// standard func to create a init repo for test routines
|
||||
func createTestRepo(t *testing.T, name string, c *Client) (*Repository, error) {
|
||||
user, _, uErr := c.GetMyUserInfo()
|
||||
|
|
Loading…
Reference in a new issue