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 := "/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) } file, err := os.Create(destination) if err != nil { return Package{}, fmt.Errorf("\u521b\u5efa\u4fdd\u5b58\u6587\u4ef6\u5931\u8d25: %w", err) } hash := sha256.New() _, copyErr := io.Copy(io.MultiWriter(file, hash), response.Body) closeErr := file.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\u4fdd\u5b58\u6587\u4ef6\u5931\u8d25: %w", closeErr) } 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) } 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: response.ContentLength, SHA256: digest, } return Package{ 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 }