mirror of
https://github.com/GrandpaNutz/fafda.git
synced 2026-01-12 02:32:59 +00:00
142 lines
3.7 KiB
Go
142 lines
3.7 KiB
Go
package github
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"fafda/config"
|
|
"fafda/internal"
|
|
)
|
|
|
|
const headerRateLimitRetryAfter = "retry-after"
|
|
const headerRateLimitReset = "x-ratelimit-remaining"
|
|
const headerRateLimitRemaining = "x-ratelimit-reset"
|
|
|
|
type Client struct {
|
|
partSize int
|
|
client *http.Client
|
|
resources *ReleaseManager
|
|
}
|
|
|
|
func NewClient(cfg config.GitHub) (*Client, error) {
|
|
resources, err := NewReleaseManager(cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to initialize resource manager: %w", err)
|
|
}
|
|
|
|
return &Client{
|
|
client: &http.Client{},
|
|
resources: resources,
|
|
}, nil
|
|
}
|
|
|
|
func handleRateLimit(resp *http.Response) time.Duration {
|
|
if resp.StatusCode != http.StatusForbidden &&
|
|
resp.StatusCode != http.StatusTooManyRequests {
|
|
return 0
|
|
}
|
|
|
|
if retryAfter := resp.Header.Get(headerRateLimitRetryAfter); retryAfter != "" {
|
|
seconds, _ := strconv.ParseInt(retryAfter, 10, 64)
|
|
return time.Duration(seconds) * time.Second
|
|
}
|
|
|
|
if resp.Header.Get(headerRateLimitRemaining) == "0" {
|
|
resetTime, _ := strconv.ParseInt(resp.Header.Get(headerRateLimitReset), 10, 64)
|
|
waitTime := resetTime - time.Now().UTC().Unix()
|
|
if waitTime > 0 {
|
|
return time.Duration(waitTime) * time.Second
|
|
}
|
|
}
|
|
|
|
// Should never come to this - FUCK YOU GITHUB
|
|
return time.Minute
|
|
}
|
|
|
|
func (c *Client) doRequest(req *http.Request) (*http.Response, error) {
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if waitTime := handleRateLimit(resp); waitTime > 0 {
|
|
time.Sleep(waitTime)
|
|
return c.doRequest(req)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (c *Client) UploadAsset(filename string, size int64, b []byte) (*Asset, error) {
|
|
release := c.resources.GetNextRelease()
|
|
url := fmt.Sprintf(
|
|
"%s/repos/%s/%s/releases/%d/assets",
|
|
uploadURL, release.Username, release.Repository, release.ReleaseId,
|
|
)
|
|
|
|
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s?name=%s", url, filename), bytes.NewReader(b))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set(internal.HeaderAccept, internal.MediaTypeGithubJSON)
|
|
req.Header.Set(internal.HeaderContentType, internal.MediaTypeOctetStream)
|
|
req.Header.Set(internal.HeaderAuthorization, "Bearer "+release.AuthToken)
|
|
req.ContentLength = size
|
|
|
|
resp, err := c.doRequest(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("upload asset: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusCreated {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("upload asset failed: %s", string(body))
|
|
}
|
|
|
|
var asset Asset
|
|
if err := json.NewDecoder(resp.Body).Decode(&asset); err != nil {
|
|
return nil, fmt.Errorf("decode response: %w", err)
|
|
}
|
|
|
|
asset.Name = filename
|
|
asset.Username = release.Username
|
|
asset.Repository = release.Repository
|
|
return &asset, nil
|
|
}
|
|
|
|
func (c *Client) DownloadAsset(asset *Asset, start, end int) (io.ReadCloser, error) {
|
|
token := c.resources.GetUserToken(asset.Username)
|
|
|
|
if token == "" {
|
|
return nil, fmt.Errorf("token not found for given asset username:%s", asset.Username)
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodGet, asset.url(), nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set(internal.HeaderAuthorization, "Bearer "+token)
|
|
req.Header.Set(internal.HeaderAccept, internal.MediaTypeOctetStream)
|
|
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end))
|
|
|
|
resp, err := c.doRequest(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("download asset: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
_ = resp.Body.Close()
|
|
return nil, fmt.Errorf("download asset failed: %s", string(body))
|
|
}
|
|
|
|
return resp.Body, nil
|
|
}
|