@ -13,6 +13,7 @@ import (
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"sync"
@ -23,22 +24,23 @@ var jsonHeader = http.Header{"content-type": []string{"application/json"}}
// Version return the library version
func Version ( ) string {
return "0.1 3 .0"
return "0.1 4 .0"
}
// Client represents a Gitea API client.
// Client represents a thread-safe Gitea API client.
type Client struct {
url string
accessToken string
username string
password string
otp string
sudo string
debug bool
client * http . Client
ctx context . Context
serverVersion * version . Version
versionLock sync . RWMutex
url string
accessToken string
username string
password string
otp string
sudo string
debug bool
client * http . Client
ctx context . Context
mutex sync . RWMutex
serverVersion * version . Version
getVersionOnce sync . Once
}
// Response represents the gitea response
@ -47,6 +49,7 @@ type Response struct {
}
// NewClient initializes and returns a API client.
// Usage of all gitea.Client methods is concurrency-safe.
func NewClient ( url string , options ... func ( * Client ) ) ( * Client , error ) {
client := & Client {
url : strings . TrimSuffix ( url , "/" ) ,
@ -56,7 +59,7 @@ func NewClient(url string, options ...func(*Client)) (*Client, error) {
for _ , opt := range options {
opt ( client )
}
if err := client . checkServerVersionGreaterThanOrEqual ( version1_1 0 _0) ; err != nil {
if err := client . checkServerVersionGreaterThanOrEqual ( version1_1 1 _0) ; err != nil {
return nil , err
}
return client , nil
@ -72,14 +75,23 @@ func NewClientWithHTTP(url string, httpClient *http.Client) *Client {
// SetHTTPClient is an option for NewClient to set custom http client
func SetHTTPClient ( httpClient * http . Client ) func ( client * Client ) {
return func ( client * Client ) {
client . client = httpClient
client . SetHTTPClient ( httpClient )
}
}
// SetHTTPClient replaces default http.Client with user given one.
func ( c * Client ) SetHTTPClient ( client * http . Client ) {
c . mutex . Lock ( )
c . client = client
c . mutex . Unlock ( )
}
// SetToken is an option for NewClient to set token
func SetToken ( token string ) func ( client * Client ) {
return func ( client * Client ) {
client . mutex . Lock ( )
client . accessToken = token
client . mutex . Unlock ( )
}
}
@ -92,7 +104,9 @@ func SetBasicAuth(username, password string) func(client *Client) {
// SetBasicAuth sets username and password
func ( c * Client ) SetBasicAuth ( username , password string ) {
c . mutex . Lock ( )
c . username , c . password = username , password
c . mutex . Unlock ( )
}
// SetOTP is an option for NewClient to set OTP for 2FA
@ -104,7 +118,9 @@ func SetOTP(otp string) func(client *Client) {
// SetOTP sets OTP for 2FA
func ( c * Client ) SetOTP ( otp string ) {
c . mutex . Lock ( )
c . otp = otp
c . mutex . Unlock ( )
}
// SetContext is an option for NewClient to set context
@ -116,12 +132,9 @@ func SetContext(ctx context.Context) func(client *Client) {
// SetContext set context witch is used for http requests
func ( c * Client ) SetContext ( ctx context . Context ) {
c . mutex . Lock ( )
c . ctx = ctx
}
// SetHTTPClient replaces default http.Client with user given one.
func ( c * Client ) SetHTTPClient ( client * http . Client ) {
c . client = client
c . mutex . Unlock ( )
}
// SetSudo is an option for NewClient to set sudo header
@ -133,43 +146,57 @@ func SetSudo(sudo string) func(client *Client) {
// SetSudo sets username to impersonate.
func ( c * Client ) SetSudo ( sudo string ) {
c . mutex . Lock ( )
c . sudo = sudo
c . mutex . Unlock ( )
}
// SetDebugMode is an option for NewClient to enable debug mode
func SetDebugMode ( ) func ( client * Client ) {
return func ( client * Client ) {
client . mutex . Lock ( )
client . debug = true
client . mutex . Unlock ( )
}
}
func ( c * Client ) getWebResponse ( method , path string , body io . Reader ) ( [ ] byte , * Response , error ) {
if c . debug {
c . mutex . RLock ( )
debug := c . debug
if debug {
fmt . Printf ( "%s: %s\nBody: %v\n" , method , c . url + path , body )
}
req , err := http . NewRequestWithContext ( c . ctx , method , c . url + path , body )
client := c . client // client ref can change from this point on so safe it
c . mutex . RUnlock ( )
if err != nil {
return nil , nil , err
}
resp , err := c . client . Do ( req )
resp , err := client . Do ( req )
if err != nil {
return nil , nil , err
}
defer resp . Body . Close ( )
data , err := ioutil . ReadAll ( resp . Body )
if c. debug {
if debug {
fmt . Printf ( "Response: %v\n\n" , resp )
}
return data , & Response { resp } , nil
}
func ( c * Client ) doRequest ( method , path string , header http . Header , body io . Reader ) ( * Response , error ) {
if c . debug {
c . mutex . RLock ( )
debug := c . debug
if debug {
fmt . Printf ( "%s: %s\nHeader: %v\nBody: %s\n" , method , c . url + "/api/v1" + path , header , body )
}
req , err := http . NewRequestWithContext ( c . ctx , method , c . url + "/api/v1" + path , body )
if err != nil {
c . mutex . RUnlock ( )
return nil , err
}
if len ( c . accessToken ) != 0 {
@ -184,51 +211,83 @@ func (c *Client) doRequest(method, path string, header http.Header, body io.Read
if len ( c . sudo ) != 0 {
req . Header . Set ( "Sudo" , c . sudo )
}
client := c . client // client ref can change from this point on so safe it
c . mutex . RUnlock ( )
for k , v := range header {
req . Header [ k ] = v
}
resp , err := c . c lient. Do ( req )
resp , err := c lient. Do ( req )
if err != nil {
return nil , err
}
if c. debug {
if debug {
fmt . Printf ( "Response: %v\n\n" , resp )
}
return & Response { resp } , nil
}
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 {
return nil , nil , err
// 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
}
defer resp . Body . Close ( )
//
// error: body will be read for details
//
defer resp . Body . Close ( )
data , err := ioutil . ReadAll ( resp . Body )
if err != nil {
return nil , resp , err
return nil , fmt. Errorf ( "body read on HTTP error %d: %v" , resp.StatusCode , err )
}
switch resp . StatusCode {
case 403 :
return data , resp, errors. New ( "403 Forbidden" )
return data , errors. New ( "403 Forbidden" )
case 404 :
return data , resp, errors. New ( "404 Not Found" )
return data , errors. New ( "404 Not Found" )
case 409 :
return data , resp, errors. New ( "409 Conflict" )
return data , errors. New ( "409 Conflict" )
case 422 :
return data , resp, fmt. Errorf ( "422 Unprocessable Entity: %s" , string ( data ) )
return data , 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 ) )
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 {
return nil , nil , err
}
defer resp . Body . Close ( )
// 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
}
return data , resp , nil
@ -251,3 +310,24 @@ func (c *Client) getStatusCode(method, path string, header http.Header, body io.
return resp . StatusCode , resp , nil
}
// pathEscapeSegments escapes segments of a path while not escaping forward slash
func pathEscapeSegments ( path string ) string {
slice := strings . Split ( path , "/" )
for index := range slice {
slice [ index ] = url . PathEscape ( slice [ index ] )
}
escapedPath := strings . Join ( slice , "/" )
return escapedPath
}
// escapeValidatePathSegments is a help function to validate and encode url path segments
func escapeValidatePathSegments ( seg ... * string ) error {
for i := range seg {
if seg [ i ] == nil || len ( * seg [ i ] ) == 0 {
return fmt . Errorf ( "path segment [%d] is empty" , i )
}
* seg [ i ] = url . PathEscape ( * seg [ i ] )
}
return nil
}