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
|
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) {
|
func (c *Client) getResponse(method, path string, header http.Header, body io.Reader) ([]byte, *Response, error) {
|
||||||
resp, err := c.doRequest(method, path, header, body)
|
resp, err := c.doRequest(method, path, header, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -205,32 +247,18 @@ func (c *Client) getResponse(method, path string, header http.Header, body io.Re
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
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 {
|
if err != nil {
|
||||||
return nil, resp, err
|
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
|
return data, resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -420,3 +421,20 @@ const (
|
||||||
func (c *Client) GetArchive(owner, repo, ref string, ext ArchiveType) ([]byte, *Response, error) {
|
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)
|
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
|
package gitea
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -139,6 +141,22 @@ func TestGetArchive(t *testing.T) {
|
||||||
assert.EqualValues(t, 1620, len(archive))
|
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
|
// standard func to create a init repo for test routines
|
||||||
func createTestRepo(t *testing.T, name string, c *Client) (*Repository, error) {
|
func createTestRepo(t *testing.T, name string, c *Client) (*Repository, error) {
|
||||||
user, _, uErr := c.GetMyUserInfo()
|
user, _, uErr := c.GetMyUserInfo()
|
||||||
|
|
Loading…
Reference in a new issue