Scout/pkg/client/platform.go
2026-08-31 19:44:58 +08:00

228 lines
6.8 KiB
Go

package client
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strings"
)
type QueryOptions struct {
Platform string
Architecture string
Channel string
CurrentVersion string
}
type WatchRequest struct {
Packages []string
Versions map[string]string
Platform string
Architecture string
Channel string
}
func (c *Client) ListPackagesFor(ctx context.Context, options QueryOptions) ([]Package, error) {
var response struct {
Packages []Package `json:"packages"`
}
if err := c.getJSON(ctx, withQuery(c.resourcePath("/api/v1/packages"), options.values()), &response); err != nil {
return nil, err
}
return response.Packages, nil
}
func (c *Client) GetLatestFor(ctx context.Context, name string, options QueryOptions) (Package, error) {
var item Package
path := c.resourcePath("/api/v1/packages/") + url.PathEscape(name)
if err := c.getJSON(ctx, withQuery(path, options.values()), &item); err != nil {
return Package{}, err
}
return item, nil
}
func (c *Client) ListVersionsFor(ctx context.Context, name string, options QueryOptions) ([]Package, error) {
var response struct {
Name string `json:"name"`
Versions []Package `json:"versions"`
}
path := c.resourcePath("/api/v1/packages/") + url.PathEscape(name) + "/versions"
if err := c.getJSON(ctx, withQuery(path, options.values()), &response); err != nil {
return nil, err
}
return response.Versions, nil
}
func (c *Client) CheckUpdateFor(ctx context.Context, name, currentVersion string, options QueryOptions) (Package, bool, error) {
options.CurrentVersion = currentVersion
latest, err := c.GetLatestFor(ctx, name, options)
if err != nil {
return Package{}, false, err
}
return latest, CompareVersions(latest.Version, currentVersion) > 0, nil
}
func (c *Client) DownloadFor(ctx context.Context, name, version, destination string, options QueryOptions) (Package, error) {
if destination == "" {
return Package{}, errors.New("\u4fdd\u5b58\u8def\u5f84\u4e0d\u80fd\u4e3a\u7a7a")
}
if version == "" {
latest, err := c.GetLatestFor(ctx, name, options)
if err != nil {
return Package{}, err
}
version = latest.Version
}
path := c.resourcePath("/api/v1/packages/") + url.PathEscape(name) + "/" + url.PathEscape(version) + "/download"
request, err := c.newRequest(ctx, http.MethodGet, withQuery(path, options.values()), nil)
if err != nil {
return Package{}, err
}
response, err := c.httpClient.Do(request)
if err != nil {
return Package{}, fmt.Errorf("\u8bf7\u6c42\u4e0b\u8f7d\u5931\u8d25: %w", err)
}
defer response.Body.Close()
if err := checkResponse(response); err != nil {
return Package{}, err
}
if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil {
return Package{}, fmt.Errorf("\u521b\u5efa\u4fdd\u5b58\u76ee\u5f55\u5931\u8d25: %w", err)
}
temporary, err := os.CreateTemp(filepath.Dir(destination), ".scout-download-*")
if err != nil {
return Package{}, fmt.Errorf("\u521b\u5efa\u4e0b\u8f7d\u4e34\u65f6\u6587\u4ef6\u5931\u8d25: %w", err)
}
temporaryName := temporary.Name()
defer os.Remove(temporaryName)
hash := sha256.New()
size, copyErr := io.Copy(io.MultiWriter(temporary, hash), response.Body)
closeErr := temporary.Close()
if copyErr != nil {
return Package{}, fmt.Errorf("\u4fdd\u5b58\u5305\u6587\u4ef6\u5931\u8d25: %w", copyErr)
}
if closeErr != nil {
return Package{}, fmt.Errorf("\u5173\u95ed\u4e0b\u8f7d\u4e34\u65f6\u6587\u4ef6\u5931\u8d25: %w", closeErr)
}
if response.ContentLength >= 0 && size != response.ContentLength {
return Package{}, fmt.Errorf("\u5305\u6587\u4ef6\u5927\u5c0f\u6821\u9a8c\u5931\u8d25: \u671f\u671b %d\uff0c\u5b9e\u9645 %d", response.ContentLength, size)
}
digest := hex.EncodeToString(hash.Sum(nil))
expected := response.Header.Get("X-Scout-SHA256")
if expected != "" && !strings.EqualFold(expected, digest) {
return Package{}, fmt.Errorf("\u5305\u6587\u4ef6\u6821\u9a8c\u5931\u8d25: \u671f\u671b %s\uff0c\u5b9e\u9645 %s", expected, digest)
}
if err := copyDownloadedFile(temporaryName, destination); err != nil {
return Package{}, err
}
target := Target{
Platform: response.Header.Get("X-Scout-Platform"),
Architecture: response.Header.Get("X-Scout-Architecture"),
PackageType: response.Header.Get("X-Scout-Package-Type"),
Size: size,
SHA256: digest,
}
return Package{
Project: c.project,
Name: name,
Version: version,
Platform: target.Platform,
Architecture: target.Architecture,
PackageType: target.PackageType,
Size: target.Size,
SHA256: digest,
Targets: []Target{target},
}, nil
}
func (c *Client) WatchUpdatesFor(ctx context.Context, input WatchRequest) (<-chan Update, error) {
query := url.Values{}
packages := compactValues(input.Packages)
if len(packages) > 0 {
query.Set("packages", strings.Join(packages, ","))
}
if len(input.Versions) > 0 {
names := make([]string, 0, len(input.Versions))
for name := range input.Versions {
names = append(names, name)
}
sort.Strings(names)
versions := make([]string, 0, len(names))
for _, name := range names {
if strings.TrimSpace(name) != "" {
versions = append(versions, strings.TrimSpace(name)+":"+strings.TrimSpace(input.Versions[name]))
}
}
if len(versions) > 0 {
query.Set("versions", strings.Join(versions, ","))
}
}
if input.Platform != "" {
query.Set("platform", input.Platform)
}
if input.Architecture != "" {
query.Set("architecture", input.Architecture)
}
if input.Channel != "" {
query.Set("channel", input.Channel)
}
path := withQuery(c.resourcePath("/api/v1/updates/stream"), query)
request, err := c.newRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
}
response, err := c.httpClient.Do(request)
if err != nil {
return nil, fmt.Errorf("\u8fde\u63a5\u66f4\u65b0\u901a\u77e5\u5931\u8d25: %w", err)
}
if err := checkResponse(response); err != nil {
response.Body.Close()
return nil, err
}
updates := make(chan Update)
go readUpdates(ctx, response.Body, updates)
return updates, nil
}
func (options QueryOptions) values() url.Values {
query := url.Values{}
if options.Platform != "" {
query.Set("platform", options.Platform)
}
if options.Architecture != "" {
query.Set("architecture", options.Architecture)
}
if options.Channel != "" {
query.Set("channel", options.Channel)
}
if options.CurrentVersion != "" {
query.Set("current_version", options.CurrentVersion)
}
return query
}
func withQuery(path string, query url.Values) string {
if encoded := query.Encode(); encoded != "" {
return path + "?" + encoded
}
return path
}
func compactValues(values []string) []string {
result := make([]string, 0, len(values))
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {
result = append(result, value)
}
}
return result
}