306 lines
7.4 KiB
Go
306 lines
7.4 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"context"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"mime"
|
||
|
"mime/multipart"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
type ApiClient interface {
|
||
|
Do(*http.Request) (*http.Response, error)
|
||
|
}
|
||
|
|
||
|
type GitHub struct {
|
||
|
client ApiClient
|
||
|
}
|
||
|
|
||
|
type ApiError struct {
|
||
|
Message string `json:"message"`
|
||
|
URL string `json:"url"`
|
||
|
}
|
||
|
|
||
|
func (e ApiError) Error() string {
|
||
|
return e.Message
|
||
|
}
|
||
|
|
||
|
type ApiNotFoundError struct {
|
||
|
ApiError
|
||
|
Errors []string `json:"errors"`
|
||
|
}
|
||
|
|
||
|
type ApiValidationError struct {
|
||
|
ApiError
|
||
|
}
|
||
|
|
||
|
func NewGitHub(client ApiClient) *GitHub {
|
||
|
return &GitHub{client: client}
|
||
|
}
|
||
|
|
||
|
// Determine whether the request `content-type` includes a
|
||
|
// server-acceptable mime-type
|
||
|
//
|
||
|
// Failure should yield an HTTP 415 (`http.StatusUnsupportedMediaType`)
|
||
|
func HasContentType(r *http.Response, mimetype string) bool {
|
||
|
contentType := r.Header.Get("Content-type")
|
||
|
if contentType == "" {
|
||
|
return mimetype == "application/octet-stream"
|
||
|
}
|
||
|
|
||
|
for _, v := range strings.Split(contentType, ",") {
|
||
|
t, _, err := mime.ParseMediaType(v)
|
||
|
if err != nil {
|
||
|
break
|
||
|
}
|
||
|
if t == mimetype {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
type GitTag struct {
|
||
|
Commit struct {
|
||
|
Created string `json:"created,omitempty"`
|
||
|
SHA string `json:"sha,omitempty"`
|
||
|
URL string `json:"url,omitempty"`
|
||
|
} `json:"commit"`
|
||
|
Id string `json:"id,omitempty"`
|
||
|
Message string `json:"message,omitempty"`
|
||
|
Name string `json:"name,omitempty"`
|
||
|
TarballURL string `json:"tarball_url,omitempty"`
|
||
|
ZipballURL string `json:"zipball_url,omitempty"`
|
||
|
}
|
||
|
|
||
|
func (gh *GitHub) GetGitTagInfo(owner, repo, tag string, ctx context.Context) (*GitTag, error) {
|
||
|
url := fmt.Sprintf("/repos/%s/%s/tags/%s", owner, repo, tag)
|
||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
req.Header.Set("accept", "application/json")
|
||
|
|
||
|
res, err := gh.client.Do(req)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer res.Body.Close()
|
||
|
if !HasContentType(res, "application/json") {
|
||
|
return nil, fmt.Errorf("content-type: %q should be %q",
|
||
|
res.Header.Get("content-type"), "application/json")
|
||
|
}
|
||
|
dec := json.NewDecoder(io.LimitReader(res.Body, 32*1024))
|
||
|
|
||
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||
|
var cres GitTag
|
||
|
err := dec.Decode(&cres)
|
||
|
return &cres, err
|
||
|
} else {
|
||
|
switch res.StatusCode {
|
||
|
case 404:
|
||
|
var error ApiNotFoundError
|
||
|
err := dec.Decode(&error)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return nil, error
|
||
|
default:
|
||
|
return nil, fmt.Errorf("an unexpected error occurred")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type CreateReleaseRequest struct {
|
||
|
Owner string `json:"-"`
|
||
|
Repo string `json:"-"`
|
||
|
|
||
|
Body string `json:"body,omitempty"`
|
||
|
Draft bool `json:"draft,omitempty"`
|
||
|
HideArchiveLinks bool `json:"hide_archive_links,omitempty"`
|
||
|
Name string `json:"name,omitempty"`
|
||
|
PreRelease bool `json:"prerelease,omitempty"`
|
||
|
TagName string `json:"tag_name"`
|
||
|
TargetCommitish string `json:"target_commitish,omitempty"`
|
||
|
}
|
||
|
type CreateReleaseResponse struct {
|
||
|
Body string `json:"body"`
|
||
|
CreatedAt string `json:"created_at"`
|
||
|
Draft bool `json:"draft"`
|
||
|
HideArchiveLinks bool `json:"hide_archive_links"`
|
||
|
HtmlURL string `json:"html_url"`
|
||
|
Id int64 `json:"id"`
|
||
|
Name string `json:"name"`
|
||
|
PreRelease bool `json:"prerelease"`
|
||
|
PublishedAt string `json:"published_at"`
|
||
|
TagName string `json:"tag_name"`
|
||
|
TarballURL string `json:"tarball_url"`
|
||
|
TargetCommitish string `json:"target_commitish"`
|
||
|
UploadURL string `json:"upload_url"`
|
||
|
URL string `json:"url"`
|
||
|
ZipballURL string `json:"zipball_url"`
|
||
|
}
|
||
|
|
||
|
func (gh *GitHub) CreateRelease(creq *CreateReleaseRequest, ctx context.Context) (*CreateReleaseResponse, error) {
|
||
|
var b bytes.Buffer
|
||
|
|
||
|
if err := json.NewEncoder(&b).Encode(creq); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
url := fmt.Sprintf("/repos/%s/%s/releases", creq.Owner, creq.Repo)
|
||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &b)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
req.Header.Set("content-type", "application/json")
|
||
|
req.Header.Set("accept", "application/json")
|
||
|
|
||
|
res, err := gh.client.Do(req)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer res.Body.Close()
|
||
|
if !HasContentType(res, "application/json") {
|
||
|
return nil, fmt.Errorf("content-type: %q should be %q",
|
||
|
res.Header.Get("content-type"), "application/json")
|
||
|
}
|
||
|
dec := json.NewDecoder(io.LimitReader(res.Body, 32*1024))
|
||
|
|
||
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||
|
var cres CreateReleaseResponse
|
||
|
err := dec.Decode(&cres)
|
||
|
return &cres, err
|
||
|
} else {
|
||
|
switch res.StatusCode {
|
||
|
case 404:
|
||
|
var error ApiNotFoundError
|
||
|
err := dec.Decode(&error)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return nil, error
|
||
|
case 409:
|
||
|
var error ApiError
|
||
|
err := dec.Decode(&error)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return nil, error
|
||
|
case 422:
|
||
|
var error ApiValidationError
|
||
|
err := dec.Decode(&error)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return nil, error
|
||
|
default:
|
||
|
return nil, fmt.Errorf("an unexpected error occurred")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type CreateReleaseArtifactRequest struct {
|
||
|
Owner string `json:"-"`
|
||
|
Repo string `json:"-"`
|
||
|
|
||
|
Id int64 `json:"-"`
|
||
|
|
||
|
Name string `json:"-"`
|
||
|
ExternalUrl string `json:"-"`
|
||
|
|
||
|
Attachment io.Reader `json:"-"`
|
||
|
}
|
||
|
type CreateReleaseArtifactResponse struct {
|
||
|
BrowserDownloadUrl string `json:"browser_download_url"`
|
||
|
CreatedAt string `json:"created_at"`
|
||
|
DownloadCount int64 `json:"download_count"`
|
||
|
Id int64 `json:"id"`
|
||
|
Name string `json:"name"`
|
||
|
Size int64 `json:"size"`
|
||
|
Type string `json:"type"`
|
||
|
Uuid string `json:"uuid"`
|
||
|
}
|
||
|
|
||
|
func (gh *GitHub) CreateReleaseArtifact(creq *CreateReleaseArtifactRequest, ctx context.Context) (*CreateReleaseArtifactResponse, error) {
|
||
|
var b bytes.Buffer
|
||
|
|
||
|
w := multipart.NewWriter(&b)
|
||
|
|
||
|
if c, ok := creq.Attachment.(io.Closer); ok {
|
||
|
defer c.Close()
|
||
|
}
|
||
|
var fw io.Writer
|
||
|
var rd io.Reader
|
||
|
var err error
|
||
|
if f, ok := creq.Attachment.(*os.File); ok {
|
||
|
fw, err = w.CreateFormFile("attachment", f.Name())
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
rd = f
|
||
|
} else {
|
||
|
fw, err = w.CreateFormField("external_url")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
rd = bytes.NewBufferString(creq.ExternalUrl)
|
||
|
}
|
||
|
if _, err := io.Copy(fw, rd); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
w.Close()
|
||
|
|
||
|
url := fmt.Sprintf("/repos/%s/%s/releases/%d/assets", creq.Owner, creq.Repo, creq.Id)
|
||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &b)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
req.Header.Set("content-type", w.FormDataContentType())
|
||
|
req.Header.Set("accept", "application/json")
|
||
|
|
||
|
res, err := gh.client.Do(req)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer res.Body.Close()
|
||
|
if !HasContentType(res, "application/json") {
|
||
|
return nil, fmt.Errorf("content-type: %q should be %q",
|
||
|
res.Header.Get("content-type"), "application/json")
|
||
|
}
|
||
|
dec := json.NewDecoder(io.LimitReader(res.Body, 32*1024))
|
||
|
|
||
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||
|
var cres CreateReleaseArtifactResponse
|
||
|
err := dec.Decode(&cres)
|
||
|
return &cres, err
|
||
|
} else {
|
||
|
switch res.StatusCode {
|
||
|
case 400:
|
||
|
var error ApiError
|
||
|
err := dec.Decode(&error)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return nil, error
|
||
|
case 404:
|
||
|
var error ApiNotFoundError
|
||
|
err := dec.Decode(&error)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return nil, error
|
||
|
case 413:
|
||
|
fallthrough // quota?
|
||
|
default:
|
||
|
return nil, fmt.Errorf("an unexpected error occurred")
|
||
|
}
|
||
|
}
|
||
|
}
|