mirror of
https://github.com/gabrie30/ghorg.git
synced 2025-08-10 08:17:10 +02:00
511 lines
12 KiB
Go
511 lines
12 KiB
Go
package bitbucket
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/net/context"
|
|
"golang.org/x/oauth2"
|
|
"golang.org/x/oauth2/bitbucket"
|
|
"golang.org/x/oauth2/clientcredentials"
|
|
)
|
|
|
|
const DEFAULT_PAGE_LENGTH = 10
|
|
const DEFAULT_LIMIT_PAGES = 0
|
|
const DEFAULT_MAX_DEPTH = 1
|
|
const DEFAULT_BITBUCKET_API_BASE_URL = "https://api.bitbucket.org/2.0"
|
|
|
|
func apiBaseUrl() (*url.URL, error) {
|
|
ev := os.Getenv("BITBUCKET_API_BASE_URL")
|
|
if ev == "" {
|
|
ev = DEFAULT_BITBUCKET_API_BASE_URL
|
|
}
|
|
|
|
return url.Parse(ev)
|
|
}
|
|
|
|
type Client struct {
|
|
Auth *auth
|
|
Users *Users
|
|
User user
|
|
Teams teams
|
|
Repositories *Repositories
|
|
Workspaces *Workspace
|
|
Pagelen int
|
|
MaxDepth int
|
|
// LimitPages limits the number of pages for a request
|
|
// default value as 0 -- disable limits
|
|
LimitPages int
|
|
// DisableAutoPaging allows you to disable the default behavior of automatically requesting
|
|
// all the pages for a paginated response.
|
|
DisableAutoPaging bool
|
|
apiBaseURL *url.URL
|
|
|
|
HttpClient *http.Client
|
|
}
|
|
|
|
type auth struct {
|
|
appID, secret string
|
|
user, password string
|
|
token oauth2.Token
|
|
bearerToken string
|
|
}
|
|
|
|
type Response struct {
|
|
*http.Response `json:"-"`
|
|
Size int `json:"size"`
|
|
Page int `json:"page"`
|
|
Pagelen int `json:"pagelen"`
|
|
Next string `json:"next"`
|
|
Previous string `json:"previous"`
|
|
Values []interface{} `json:"values"`
|
|
}
|
|
|
|
// Uses the Client Credentials Grant oauth2 flow to authenticate to Bitbucket
|
|
func NewOAuthClientCredentials(i, s string) *Client {
|
|
a := &auth{appID: i, secret: s}
|
|
ctx := context.Background()
|
|
conf := &clientcredentials.Config{
|
|
ClientID: i,
|
|
ClientSecret: s,
|
|
TokenURL: bitbucket.Endpoint.TokenURL,
|
|
}
|
|
|
|
tok, err := conf.Token(ctx)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
a.token = *tok
|
|
return injectClient(a)
|
|
|
|
}
|
|
|
|
func NewOAuth(i, s string) *Client {
|
|
a := &auth{appID: i, secret: s}
|
|
ctx := context.Background()
|
|
conf := &oauth2.Config{
|
|
ClientID: i,
|
|
ClientSecret: s,
|
|
Endpoint: bitbucket.Endpoint,
|
|
}
|
|
|
|
// Redirect user to consent page to ask for permission
|
|
// for the scopes specified above.
|
|
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
|
|
fmt.Printf("Visit the URL for the auth dialog:\n%v", url)
|
|
|
|
// Use the authorization code that is pushed to the redirect
|
|
// URL. Exchange will do the handshake to retrieve the
|
|
// initial access token. The HTTP Client returned by
|
|
// conf.Client will refresh the token as necessary.
|
|
var code string
|
|
fmt.Printf("Enter the code in the return URL: ")
|
|
if _, err := fmt.Scan(&code); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
tok, err := conf.Exchange(ctx, code)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
a.token = *tok
|
|
return injectClient(a)
|
|
}
|
|
|
|
// NewOAuthWithCode finishes the OAuth handshake with a given code
|
|
// and returns a *Client
|
|
func NewOAuthWithCode(i, s, c string) (*Client, string) {
|
|
a := &auth{appID: i, secret: s}
|
|
ctx := context.Background()
|
|
conf := &oauth2.Config{
|
|
ClientID: i,
|
|
ClientSecret: s,
|
|
Endpoint: bitbucket.Endpoint,
|
|
}
|
|
|
|
tok, err := conf.Exchange(ctx, c)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
a.token = *tok
|
|
return injectClient(a), tok.AccessToken
|
|
}
|
|
|
|
// NewOAuthWithRefreshToken obtains a new access token with a given refresh token
|
|
// and returns a *Client
|
|
func NewOAuthWithRefreshToken(i, s, rt string) (*Client, string) {
|
|
a := &auth{appID: i, secret: s}
|
|
ctx := context.Background()
|
|
conf := &oauth2.Config{
|
|
ClientID: i,
|
|
ClientSecret: s,
|
|
Endpoint: bitbucket.Endpoint,
|
|
}
|
|
|
|
tokenSource := conf.TokenSource(ctx, &oauth2.Token{
|
|
RefreshToken: rt,
|
|
})
|
|
tok, err := tokenSource.Token()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
a.token = *tok
|
|
return injectClient(a), tok.AccessToken
|
|
}
|
|
|
|
func NewOAuthbearerToken(t string) *Client {
|
|
a := &auth{bearerToken: t}
|
|
return injectClient(a)
|
|
}
|
|
|
|
func NewBasicAuth(u, p string) *Client {
|
|
a := &auth{user: u, password: p}
|
|
return injectClient(a)
|
|
}
|
|
|
|
func injectClient(a *auth) *Client {
|
|
bitbucketUrl, err := apiBaseUrl()
|
|
if err != nil {
|
|
log.Fatalf("invalid bitbucket url")
|
|
}
|
|
c := &Client{Auth: a, Pagelen: DEFAULT_PAGE_LENGTH, MaxDepth: DEFAULT_MAX_DEPTH,
|
|
apiBaseURL: bitbucketUrl, LimitPages: DEFAULT_LIMIT_PAGES}
|
|
c.Repositories = &Repositories{
|
|
c: c,
|
|
PullRequests: &PullRequests{c: c},
|
|
Pipelines: &Pipelines{c: c},
|
|
Repository: &Repository{c: c},
|
|
Issues: &Issues{c: c},
|
|
Commits: &Commits{c: c},
|
|
Diff: &Diff{c: c},
|
|
BranchRestrictions: &BranchRestrictions{c: c},
|
|
Webhooks: &Webhooks{c: c},
|
|
Downloads: &Downloads{c: c},
|
|
DeployKeys: &DeployKeys{c: c},
|
|
}
|
|
c.Users = &Users{
|
|
c: c,
|
|
SSHKeys: &SSHKeys{c: c},
|
|
}
|
|
c.User = &User{c: c}
|
|
c.Teams = &Teams{c: c}
|
|
c.Workspaces = &Workspace{c: c, Repositories: c.Repositories, Permissions: &Permission{c: c}}
|
|
c.HttpClient = new(http.Client)
|
|
return c
|
|
}
|
|
|
|
func (c *Client) GetOAuthToken() oauth2.Token {
|
|
return c.Auth.token
|
|
}
|
|
|
|
func (c *Client) GetApiBaseURL() string {
|
|
return fmt.Sprintf("%s%s", c.GetApiHostnameURL(), c.apiBaseURL.Path)
|
|
}
|
|
|
|
func (c *Client) GetApiHostnameURL() string {
|
|
return fmt.Sprintf("%s://%s", c.apiBaseURL.Scheme, c.apiBaseURL.Host)
|
|
}
|
|
|
|
func (c *Client) SetApiBaseURL(urlStr url.URL) {
|
|
c.apiBaseURL = &urlStr
|
|
}
|
|
|
|
func (c *Client) executeRaw(method string, urlStr string, text string) (io.ReadCloser, error) {
|
|
body := strings.NewReader(text)
|
|
|
|
req, err := http.NewRequest(method, urlStr, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if text != "" {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
c.authenticateRequest(req)
|
|
return c.doRawRequest(req, false)
|
|
}
|
|
|
|
func (c *Client) execute(method string, urlStr string, text string) (interface{}, error) {
|
|
return c.executeWithContext(method, urlStr, text, context.Background())
|
|
}
|
|
|
|
func (c *Client) executeWithContext(method string, urlStr string, text string, ctx context.Context) (interface{}, error) {
|
|
body := strings.NewReader(text)
|
|
req, err := http.NewRequest(method, urlStr, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if text != "" {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
if ctx != nil {
|
|
req.WithContext(ctx)
|
|
}
|
|
c.authenticateRequest(req)
|
|
result, err := c.doRequest(req, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) executePaginated(method string, urlStr string, text string, page *int) (interface{}, error) {
|
|
if c.Pagelen != DEFAULT_PAGE_LENGTH {
|
|
urlObj, err := url.Parse(urlStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
q := urlObj.Query()
|
|
q.Set("pagelen", strconv.Itoa(c.Pagelen))
|
|
urlObj.RawQuery = q.Encode()
|
|
urlStr = urlObj.String()
|
|
}
|
|
|
|
body := strings.NewReader(text)
|
|
req, err := http.NewRequest(method, urlStr, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if text != "" {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
c.authenticateRequest(req)
|
|
result, err := c.doPaginatedRequest(req, page, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) executeFileUpload(method string, urlStr string, files []File, filesToDelete []string, params map[string]string, ctx context.Context) (interface{}, error) {
|
|
// Prepare a form that you will submit to that URL.
|
|
var b bytes.Buffer
|
|
w := multipart.NewWriter(&b)
|
|
|
|
var fw io.Writer
|
|
for _, file := range files {
|
|
fileReader, err := os.Open(file.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer fileReader.Close()
|
|
|
|
if fw, err = w.CreateFormFile(file.Name, file.Name); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err = io.Copy(fw, fileReader); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
for key, value := range params {
|
|
err := w.WriteField(key, value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
for _, filename := range filesToDelete {
|
|
err := w.WriteField("files", filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Don't forget to close the multipart writer.
|
|
// If you don't close it, your request will be missing the terminating boundary.
|
|
w.Close()
|
|
|
|
// Now that you have a form, you can submit it to your handler.
|
|
req, err := http.NewRequest(method, urlStr, &b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Don't forget to set the content type, this will contain the boundary.
|
|
req.Header.Set("Content-Type", w.FormDataContentType())
|
|
if ctx != nil {
|
|
req.WithContext(ctx)
|
|
}
|
|
c.authenticateRequest(req)
|
|
return c.doRequest(req, true)
|
|
|
|
}
|
|
|
|
func (c *Client) authenticateRequest(req *http.Request) {
|
|
if c.Auth.bearerToken != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.Auth.bearerToken)
|
|
}
|
|
|
|
if c.Auth.user != "" && c.Auth.password != "" {
|
|
req.SetBasicAuth(c.Auth.user, c.Auth.password)
|
|
} else if c.Auth.token.Valid() {
|
|
c.Auth.token.SetAuthHeader(req)
|
|
}
|
|
}
|
|
|
|
func (c *Client) doRequest(req *http.Request, emptyResponse bool) (interface{}, error) {
|
|
resBody, err := c.doRawRequest(req, emptyResponse)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if emptyResponse || resBody == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
defer resBody.Close()
|
|
|
|
responseBytes, err := ioutil.ReadAll(resBody)
|
|
if err != nil {
|
|
return resBody, err
|
|
}
|
|
|
|
var result interface{}
|
|
if err := json.Unmarshal(responseBytes, &result); err != nil {
|
|
return responseBytes, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) doPaginatedRequest(req *http.Request, page *int, emptyResponse bool) (interface{}, error) {
|
|
disableAutoPaging := c.DisableAutoPaging
|
|
curPage := 1
|
|
if page != nil {
|
|
disableAutoPaging = true
|
|
curPage = *page
|
|
q := req.URL.Query()
|
|
q.Set("page", strconv.Itoa(curPage))
|
|
req.URL.RawQuery = q.Encode()
|
|
}
|
|
// q.Encode() does not encode "~".
|
|
req.URL.RawQuery = strings.ReplaceAll(req.URL.RawQuery, "~", "%7E")
|
|
|
|
resBody, err := c.doRawRequest(req, emptyResponse)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if emptyResponse || resBody == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
defer resBody.Close()
|
|
|
|
responseBytes, err := ioutil.ReadAll(resBody)
|
|
if err != nil {
|
|
return resBody, err
|
|
}
|
|
|
|
responsePaginated := &Response{}
|
|
err = json.Unmarshal(responseBytes, responsePaginated)
|
|
if err == nil && len(responsePaginated.Values) > 0 {
|
|
values := responsePaginated.Values
|
|
for {
|
|
if disableAutoPaging || responsePaginated.Next == "" ||
|
|
(curPage >= c.LimitPages && c.LimitPages != 0) {
|
|
break
|
|
}
|
|
curPage++
|
|
newReq, err := http.NewRequest(req.Method, responsePaginated.Next, nil)
|
|
if err != nil {
|
|
return resBody, err
|
|
}
|
|
c.authenticateRequest(newReq)
|
|
resp, err := c.doRawRequest(newReq, false)
|
|
if err != nil {
|
|
return resBody, err
|
|
}
|
|
|
|
responsePaginated = &Response{}
|
|
json.NewDecoder(resp).Decode(responsePaginated)
|
|
values = append(values, responsePaginated.Values...)
|
|
}
|
|
responsePaginated.Values = values
|
|
responseBytes, err = json.Marshal(responsePaginated)
|
|
if err != nil {
|
|
return resBody, err
|
|
}
|
|
}
|
|
|
|
var result interface{}
|
|
if err := json.Unmarshal(responseBytes, &result); err != nil {
|
|
return resBody, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) doRawRequest(req *http.Request, emptyResponse bool) (io.ReadCloser, error) {
|
|
resp, err := c.HttpClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if unexpectedHttpStatusCode(resp.StatusCode) {
|
|
defer resp.Body.Close()
|
|
|
|
out := &UnexpectedResponseStatusError{Status: resp.Status}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
out.Body = []byte(fmt.Sprintf("could not read the response body: %v", err))
|
|
} else {
|
|
out.Body = body
|
|
}
|
|
|
|
return nil, out
|
|
}
|
|
|
|
if emptyResponse || resp.StatusCode == http.StatusNoContent {
|
|
resp.Body.Close()
|
|
return nil, nil
|
|
}
|
|
|
|
if resp.Body == nil {
|
|
return nil, fmt.Errorf("response body is nil")
|
|
}
|
|
|
|
return resp.Body, nil
|
|
}
|
|
|
|
func unexpectedHttpStatusCode(statusCode int) bool {
|
|
switch statusCode {
|
|
case http.StatusOK,
|
|
http.StatusCreated,
|
|
http.StatusNoContent,
|
|
http.StatusAccepted:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func (c *Client) requestUrl(template string, args ...interface{}) string {
|
|
|
|
if len(args) == 1 && args[0] == "" {
|
|
return c.GetApiBaseURL() + template
|
|
}
|
|
return c.GetApiBaseURL() + fmt.Sprintf(template, args...)
|
|
}
|
|
|
|
func (c *Client) addMaxDepthParam(params *url.Values, customMaxDepth *int) {
|
|
maxDepth := c.MaxDepth
|
|
if customMaxDepth != nil && *customMaxDepth > 0 {
|
|
maxDepth = *customMaxDepth
|
|
}
|
|
|
|
if maxDepth != DEFAULT_MAX_DEPTH {
|
|
params.Set("max_depth", strconv.Itoa(maxDepth))
|
|
}
|
|
}
|