fafda/internal/github/client.go
2024-12-19 01:41:46 +05:30

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
}