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:
petergardfjall 2021-01-18 01:14:07 +08:00 committed by 6543
parent 30e7dc9ccb
commit ff00c13597
3 changed files with 86 additions and 22 deletions

View file

@ -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
} }

View file

@ -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
}

View file

@ -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()