219 lines
6.3 KiB
Go
219 lines
6.3 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 := "/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
|
|
}
|