126 lines
2.7 KiB
Go
126 lines
2.7 KiB
Go
package update
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type ReleaseInfo struct {
|
|
TagName string `json:"tag_name"`
|
|
Assets []struct {
|
|
Name string `json:"name"`
|
|
URL string `json:"browser_download_url"`
|
|
} `json:"assets"`
|
|
}
|
|
|
|
type Client struct {
|
|
BaseURL string
|
|
HTTP *http.Client
|
|
}
|
|
|
|
func (c *Client) LatestRelease(ownerRepo string) (*ReleaseInfo, error) {
|
|
req, err := http.NewRequest(http.MethodGet, c.apiURL(ownerRepo, "/releases/latest"), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var info ReleaseInfo
|
|
if err := c.doJSON(req, &info); err != nil {
|
|
return nil, err
|
|
}
|
|
return &info, nil
|
|
}
|
|
|
|
func (c *Client) DownloadAsset(url, dst string) error {
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := c.httpClient().Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("download failed: %s", resp.Status)
|
|
}
|
|
f, err := os.Create(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
_, err = io.Copy(f, resp.Body)
|
|
return err
|
|
}
|
|
|
|
func ExtractZip(zipPath, dstDir string) error {
|
|
zr, err := zip.OpenReader(zipPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer zr.Close()
|
|
root := filepath.Clean(dstDir) + string(os.PathSeparator)
|
|
for _, f := range zr.File {
|
|
target := filepath.Clean(filepath.Join(dstDir, f.Name))
|
|
if target != filepath.Clean(dstDir) && !strings.HasPrefix(target, root) {
|
|
return fmt.Errorf("zip path escapes destination: %s", f.Name)
|
|
}
|
|
if f.FileInfo().IsDir() {
|
|
if err := os.MkdirAll(target, 0o755); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
|
return err
|
|
}
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out, err := os.Create(target)
|
|
if err != nil {
|
|
rc.Close()
|
|
return err
|
|
}
|
|
_, copyErr := io.Copy(out, rc)
|
|
out.Close()
|
|
rc.Close()
|
|
if copyErr != nil {
|
|
return copyErr
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) apiURL(ownerRepo, suffix string) string {
|
|
base := strings.TrimRight(c.BaseURL, "/")
|
|
return base + "/api/v1/repos/" + strings.Trim(ownerRepo, "/") + suffix
|
|
}
|
|
|
|
func (c *Client) doJSON(req *http.Request, out any) error {
|
|
resp, err := c.httpClient().Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
return fmt.Errorf("request failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
|
}
|
|
return json.NewDecoder(resp.Body).Decode(out)
|
|
}
|
|
|
|
func (c *Client) httpClient() *http.Client {
|
|
if c.HTTP != nil {
|
|
return c.HTTP
|
|
}
|
|
return &http.Client{Timeout: 30 * time.Second}
|
|
}
|