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} }