236 lines
7.3 KiB
Go
236 lines
7.3 KiB
Go
// Package gitea implements the internal Gitea REST operations used by delivery.
|
|
package gitea
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
)
|
|
|
|
type FailureKind string
|
|
|
|
const (
|
|
FailureInvalid FailureKind = "invalid_request"
|
|
FailureNotFound FailureKind = "not_found"
|
|
FailureUnauthorized FailureKind = "unauthorized"
|
|
FailureConflict FailureKind = "conflict"
|
|
FailureMalformed FailureKind = "malformed_response"
|
|
FailureUnverifiable FailureKind = "unverifiable"
|
|
FailureTransport FailureKind = "transport"
|
|
FailureRejected FailureKind = "rejected"
|
|
FailureUnknown FailureKind = "unknown"
|
|
)
|
|
|
|
// Failure describes how a Gitea operation failed. StatusCode is zero when no
|
|
// HTTP response was received.
|
|
type Failure struct {
|
|
Kind FailureKind
|
|
Operation string
|
|
StatusCode int
|
|
Err error
|
|
}
|
|
|
|
func (f *Failure) Error() string {
|
|
if f.StatusCode != 0 {
|
|
return fmt.Sprintf("gitea %s: %s (HTTP %d)", f.Operation, f.Kind, f.StatusCode)
|
|
}
|
|
if f.Err != nil {
|
|
return fmt.Sprintf("gitea %s: %s: %v", f.Operation, f.Kind, f.Err)
|
|
}
|
|
return fmt.Sprintf("gitea %s: %s", f.Operation, f.Kind)
|
|
}
|
|
|
|
func (f *Failure) Unwrap() error { return f.Err }
|
|
|
|
type Destination struct {
|
|
BaseURL string
|
|
Owner string
|
|
Repository string
|
|
Token string
|
|
}
|
|
|
|
type release struct {
|
|
ID int64
|
|
URL string
|
|
TagName string
|
|
Title string
|
|
Body string
|
|
Draft bool
|
|
Prerelease bool
|
|
}
|
|
|
|
type Client struct {
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
type tagResponse struct {
|
|
Commit struct {
|
|
SHA string `json:"sha"`
|
|
} `json:"commit"`
|
|
}
|
|
|
|
type releaseResponse struct {
|
|
ID int64 `json:"id"`
|
|
HTMLURL string `json:"html_url"`
|
|
TagName string `json:"tag_name"`
|
|
TargetCommitish string `json:"target_commitish"`
|
|
Name string `json:"name"`
|
|
Body string `json:"body"`
|
|
Draft bool `json:"draft"`
|
|
Prerelease bool `json:"prerelease"`
|
|
}
|
|
|
|
type createReleaseRequest struct {
|
|
TagName string `json:"tag_name"`
|
|
TargetCommitish string `json:"target_commitish"`
|
|
Name string `json:"name"`
|
|
Body string `json:"body"`
|
|
Draft bool `json:"draft"`
|
|
Prerelease bool `json:"prerelease"`
|
|
}
|
|
|
|
func (c Client) getRelease(ctx context.Context, client *http.Client, token, endpoint string) (release, int, error) {
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return release{}, 0, failure(FailureInvalid, "find release", 0, err)
|
|
}
|
|
request.Header.Set("Authorization", "token "+token)
|
|
request.Header.Set("Accept", "application/json")
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
return release{}, 0, failure(FailureTransport, "find release", 0, err)
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode == http.StatusNotFound {
|
|
return release{}, response.StatusCode, failure(FailureNotFound, "find release", response.StatusCode, nil)
|
|
}
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return release{}, response.StatusCode, responseFailure("find release", response.StatusCode)
|
|
}
|
|
var decoded releaseResponse
|
|
if err := decodeJSON(response.Body, &decoded); err != nil {
|
|
return release{}, response.StatusCode, failure(FailureMalformed, "decode release", response.StatusCode, err)
|
|
}
|
|
return decoded.release(), response.StatusCode, nil
|
|
}
|
|
|
|
func (c Client) getJSON(ctx context.Context, client *http.Client, token, operation, endpoint string, destination any) error {
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return failure(FailureInvalid, operation, 0, err)
|
|
}
|
|
request.Header.Set("Authorization", "token "+token)
|
|
request.Header.Set("Accept", "application/json")
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
return failure(FailureTransport, operation, 0, err)
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return responseFailure(operation, response.StatusCode)
|
|
}
|
|
if err := decodeJSON(response.Body, destination); err != nil {
|
|
return failure(FailureMalformed, operation, response.StatusCode, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c Client) postRelease(ctx context.Context, client *http.Client, token, endpoint string, body []byte) (release, int, error) {
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return release{}, 0, failure(FailureInvalid, "create release", 0, err)
|
|
}
|
|
request.Header.Set("Authorization", "token "+token)
|
|
request.Header.Set("Accept", "application/json")
|
|
request.Header.Set("Content-Type", "application/json")
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
return release{}, 0, failure(FailureTransport, "create release", 0, err)
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return release{}, response.StatusCode, responseFailure("create release", response.StatusCode)
|
|
}
|
|
var decoded releaseResponse
|
|
if err := decodeJSON(response.Body, &decoded); err != nil {
|
|
return release{}, response.StatusCode, failure(FailureMalformed, "decode created release", response.StatusCode, err)
|
|
}
|
|
return decoded.release(), response.StatusCode, nil
|
|
}
|
|
|
|
func (c Client) httpClient() *http.Client {
|
|
client := http.DefaultClient
|
|
if c.HTTPClient != nil {
|
|
clientCopy := *c.HTTPClient
|
|
clientCopy.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
|
|
return &clientCopy
|
|
}
|
|
clientCopy := *client
|
|
clientCopy.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
|
|
return &clientCopy
|
|
}
|
|
|
|
func parseBaseURL(raw string) (*url.URL, error) {
|
|
parsed, err := url.Parse(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {
|
|
return nil, errors.New("base URL must be an absolute HTTP(S) origin or path without credentials, query, or fragment")
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func (response releaseResponse) release() release {
|
|
return release{ID: response.ID, URL: response.HTMLURL, TagName: response.TagName, Title: response.Name, Body: response.Body, Draft: response.Draft, Prerelease: response.Prerelease}
|
|
}
|
|
|
|
func validObjectID(value string) bool {
|
|
if len(value) != 40 && len(value) != 64 {
|
|
return false
|
|
}
|
|
for _, r := range value {
|
|
if !(r >= '0' && r <= '9') && !(r >= 'a' && r <= 'f') {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func responseFailure(operation string, status int) error {
|
|
kind := FailureRejected
|
|
switch status {
|
|
case http.StatusUnauthorized, http.StatusForbidden:
|
|
kind = FailureUnauthorized
|
|
case http.StatusConflict:
|
|
kind = FailureConflict
|
|
case http.StatusNotFound:
|
|
kind = FailureNotFound
|
|
}
|
|
return failure(kind, operation, status, nil)
|
|
}
|
|
|
|
func failure(kind FailureKind, operation string, status int, err error) error {
|
|
return &Failure{Kind: kind, Operation: operation, StatusCode: status, Err: err}
|
|
}
|
|
|
|
func decodeJSON(reader io.Reader, destination any) error {
|
|
decoder := json.NewDecoder(reader)
|
|
if err := decoder.Decode(destination); err != nil {
|
|
return err
|
|
}
|
|
var trailing any
|
|
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
|
|
if err == nil {
|
|
return errors.New("response contains multiple JSON values")
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|