160 lines · 4.6 KB · Go
RawDownload
package pluginstore import (	"context"	"encoding/json"	"fmt"	"net/http"	"net/url"	"os"	"strings" 	"github.com/router-for-me/CLIProxyAPI/v7/internal/httpfetch") const userAgent = "CLIProxyAPI" // HTTPDoer abstracts the HTTP client used to execute requests.type HTTPDoer = httpfetch.Doer type Client struct {	HTTPClient  HTTPDoer	RegistryURL string	UserAgent   string} type Release struct {	TagName string         `json:"tag_name"`	Assets  []ReleaseAsset `json:"assets"`} type ReleaseAsset struct {	Name               string `json:"name"`	BrowserDownloadURL string `json:"browser_download_url"`} func (c Client) FetchRegistry(ctx context.Context) (Registry, error) {	registryURL := strings.TrimSpace(c.RegistryURL)	if registryURL == "" {		registryURL = DefaultRegistryURL	}	data, errDownload := c.get(ctx, registryURL, "application/json")	if errDownload != nil {		return Registry{}, errDownload	}	registry, errParse := ParseRegistry(data)	if errParse != nil {		return Registry{}, errParse	}	return registry, nil} // FetchLatestRelease returns the latest published release of the plugin's// GitHub repository, mirroring the WebUI panel update check.func (c Client) FetchLatestRelease(ctx context.Context, plugin Plugin) (Release, error) {	owner, repo, errRepository := GitHubRepositoryParts(plugin.Repository)	if errRepository != nil {		return Release{}, errRepository	}	releaseURL := fmt.Sprintf(		"https://api.github.com/repos/%s/%s/releases/latest",		url.PathEscape(owner),		url.PathEscape(repo),	)	data, errDownload := c.get(ctx, releaseURL, "application/vnd.github+json")	if errDownload != nil {		return Release{}, errDownload	}	var release Release	if errDecode := json.Unmarshal(data, &release); errDecode != nil {		return Release{}, fmt.Errorf("decode release: %w", errDecode)	}	return release, nil} // ReleaseVersion derives the plugin version from the release tag, stripping a// leading "v"/"V" and validating the result.func ReleaseVersion(release Release) (string, error) {	version := normalizeVersion(release.TagName)	if !validPluginVersion(version) {		return "", fmt.Errorf("invalid release tag %q", release.TagName)	}	return version, nil} func (c Client) DownloadAsset(ctx context.Context, asset ReleaseAsset) ([]byte, error) {	if strings.TrimSpace(asset.BrowserDownloadURL) == "" {		return nil, fmt.Errorf("asset %q missing browser_download_url", asset.Name)	}	return c.get(ctx, asset.BrowserDownloadURL, "application/octet-stream")} func (c Client) get(ctx context.Context, requestURL string, accept string) ([]byte, error) {	headers := map[string]string{		"Accept":     accept,		"User-Agent": c.userAgent(),	}	if token := gitHubAPIToken(requestURL); token != "" {		headers["Authorization"] = "Bearer " + token	}	return httpfetch.GetBytes(ctx, c.httpClient(), requestURL, headers, 0)} // gitHubAPIToken returns the optional GitHub token for GitHub API requests to// raise the unauthenticated rate limit, mirroring the management asset updater.func gitHubAPIToken(requestURL string) string {	parsed, errParse := url.Parse(requestURL)	if errParse != nil || !strings.EqualFold(parsed.Host, "api.github.com") {		return ""	}	gitURL := strings.ToLower(strings.TrimSpace(os.Getenv("GITSTORE_GIT_URL")))	if !strings.Contains(gitURL, "github.com") {		return ""	}	return strings.TrimSpace(os.Getenv("GITSTORE_GIT_TOKEN"))} func (c Client) httpClient() HTTPDoer {	if c.HTTPClient != nil {		return c.HTTPClient	}	return http.DefaultClient} func (c Client) userAgent() string {	if strings.TrimSpace(c.UserAgent) != "" {		return strings.TrimSpace(c.UserAgent)	}	return userAgent} func SelectReleaseAssets(release Release, id, version, goos, goarch string) (ReleaseAsset, ReleaseAsset, error) {	archiveName := ArchiveName(id, version, goos, goarch)	var archiveAsset ReleaseAsset	var checksumAsset ReleaseAsset	for _, asset := range release.Assets {		switch strings.TrimSpace(asset.Name) {		case archiveName:			archiveAsset = asset		case "checksums.txt":			checksumAsset = asset		}	}	if strings.TrimSpace(archiveAsset.Name) == "" {		return ReleaseAsset{}, ReleaseAsset{}, fmt.Errorf("release asset %s not found", archiveName)	}	if strings.TrimSpace(checksumAsset.Name) == "" {		return ReleaseAsset{}, ReleaseAsset{}, fmt.Errorf("release asset checksums.txt not found")	}	return archiveAsset, checksumAsset, nil} func ArchiveName(id, version, goos, goarch string) string {	return fmt.Sprintf(		"%s_%s_%s_%s.zip",		strings.TrimSpace(id),		strings.TrimSpace(version),		strings.TrimSpace(goos),		strings.TrimSpace(goarch),	)}