646 lines
19 KiB
Go
646 lines
19 KiB
Go
package registry
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"scout/pkg/scoutpkg"
|
|
)
|
|
|
|
var allowedChannels = map[string]bool{
|
|
"stable": true,
|
|
"beta": true,
|
|
"dev": true,
|
|
}
|
|
|
|
var allowedStatuses = map[string]bool{
|
|
"draft": true,
|
|
"published": true,
|
|
"deprecated": true,
|
|
"revoked": true,
|
|
}
|
|
|
|
func (r *Registry) AddPackage(input Package, data io.Reader) (Package, error) {
|
|
target := Target{
|
|
Platform: input.Platform,
|
|
Architecture: input.Architecture,
|
|
PackageType: input.PackageType,
|
|
FileName: input.FileName,
|
|
}
|
|
return r.AddArtifact(input, target, data)
|
|
}
|
|
|
|
func (r *Registry) AddArtifact(input Package, target Target, data io.Reader) (Package, error) {
|
|
if data == nil {
|
|
return Package{}, errors.New("\u5305\u6587\u4ef6\u4e0d\u80fd\u4e3a\u7a7a")
|
|
}
|
|
if err := normalizeRelease(&input); err != nil {
|
|
return Package{}, err
|
|
}
|
|
if target.FileName == "" {
|
|
target.FileName = input.FileName
|
|
}
|
|
if target.FileName == "" {
|
|
target.FileName = input.Name + "-" + input.Version + ".pkg"
|
|
}
|
|
if err := normalizeTarget(&target, input.PackageType); err != nil {
|
|
return Package{}, err
|
|
}
|
|
|
|
r.writeMu.Lock()
|
|
defer r.writeMu.Unlock()
|
|
|
|
stored := r.release(input.Name, input.Version)
|
|
if stored.Name == "" {
|
|
stored = input
|
|
stored.Targets = nil
|
|
} else if err := mergeReleaseMetadata(&stored, input); err != nil {
|
|
return Package{}, err
|
|
}
|
|
for _, existing := range stored.Targets {
|
|
if existing.ID == target.ID {
|
|
return Package{}, errors.New("\u8be5\u7248\u672c\u7684\u5e73\u53f0\u3001\u67b6\u6784\u548c\u5305\u7c7b\u578b\u5df2\u7ecf\u5b58\u5728")
|
|
}
|
|
}
|
|
|
|
versionDir := filepath.Join(r.root, input.Name, input.Version)
|
|
if err := os.MkdirAll(versionDir, 0o755); err != nil {
|
|
return Package{}, fmt.Errorf("\u521b\u5efa\u7248\u672c\u76ee\u5f55\u5931\u8d25: %w", err)
|
|
}
|
|
tempPath, size, digest, err := writeTemporaryArtifact(versionDir, data)
|
|
if err != nil {
|
|
return Package{}, err
|
|
}
|
|
defer os.Remove(tempPath)
|
|
|
|
target.Size = size
|
|
target.SHA256 = digest
|
|
target.ArtifactID = target.ID
|
|
artifactDir := filepath.Join(versionDir, "artifacts", target.ArtifactID)
|
|
if _, err := os.Stat(artifactDir); err == nil {
|
|
return Package{}, errors.New("\u8be5\u4ea7\u7269\u5df2\u7ecf\u5b58\u5728")
|
|
} else if !os.IsNotExist(err) {
|
|
return Package{}, fmt.Errorf("\u68c0\u67e5\u4ea7\u7269\u76ee\u5f55\u5931\u8d25: %w", err)
|
|
}
|
|
if err := saveArtifact(artifactDir, tempPath, target); err != nil {
|
|
return Package{}, err
|
|
}
|
|
|
|
stored.Targets = append(stored.Targets, target)
|
|
stored.UploadedAt = time.Now().UTC()
|
|
applyTarget(&stored, target)
|
|
if err := r.persistRelease(stored); err != nil {
|
|
return Package{}, err
|
|
}
|
|
r.storeRelease(stored)
|
|
|
|
result := stored
|
|
result.Targets = []Target{target}
|
|
applyTarget(&result, target)
|
|
r.broadcast(result)
|
|
return result, nil
|
|
}
|
|
|
|
func (r *Registry) AddScoutPackage(input Package, targets []Target, data io.Reader) (Package, error) {
|
|
if data == nil {
|
|
return Package{}, errors.New("Scout \u5305\u6587\u4ef6\u4e0d\u80fd\u4e3a\u7a7a")
|
|
}
|
|
if err := normalizeRelease(&input); err != nil {
|
|
return Package{}, err
|
|
}
|
|
if len(targets) == 0 {
|
|
return Package{}, errors.New("Scout \u5305\u81f3\u5c11\u9700\u8981\u4e00\u4e2a\u76ee\u6807")
|
|
}
|
|
seen := make(map[string]bool)
|
|
for index := range targets {
|
|
if targets[index].FileName == "" {
|
|
targets[index].FileName = input.FileName
|
|
}
|
|
if err := normalizeTarget(&targets[index], input.PackageType); err != nil {
|
|
return Package{}, err
|
|
}
|
|
if seen[targets[index].ID] {
|
|
return Package{}, errors.New("Scout \u5305\u6e05\u5355\u4e2d\u5b58\u5728\u91cd\u590d\u76ee\u6807")
|
|
}
|
|
seen[targets[index].ID] = true
|
|
}
|
|
|
|
r.writeMu.Lock()
|
|
defer r.writeMu.Unlock()
|
|
|
|
stored := r.release(input.Name, input.Version)
|
|
if stored.Name == "" {
|
|
stored = input
|
|
stored.Targets = nil
|
|
} else if err := mergeReleaseMetadata(&stored, input); err != nil {
|
|
return Package{}, err
|
|
}
|
|
for _, existing := range stored.Targets {
|
|
if seen[existing.ID] {
|
|
return Package{}, errors.New("Scout \u5305\u4e2d\u7684\u67d0\u4e2a\u5e73\u53f0\u76ee\u6807\u5df2\u7ecf\u5b58\u5728")
|
|
}
|
|
}
|
|
|
|
versionDir := filepath.Join(r.root, input.Name, input.Version)
|
|
if err := os.MkdirAll(versionDir, 0o755); err != nil {
|
|
return Package{}, fmt.Errorf("\u521b\u5efa\u7248\u672c\u76ee\u5f55\u5931\u8d25: %w", err)
|
|
}
|
|
tempPath, size, digest, err := writeTemporaryArtifact(versionDir, data)
|
|
if err != nil {
|
|
return Package{}, err
|
|
}
|
|
defer os.Remove(tempPath)
|
|
artifactID := "scout-" + digest[:16]
|
|
artifactDir := filepath.Join(versionDir, "artifacts", artifactID)
|
|
if _, err := os.Stat(artifactDir); err == nil {
|
|
return Package{}, errors.New("\u8be5 Scout \u4ea7\u7269\u5df2\u7ecf\u5b58\u5728")
|
|
} else if !os.IsNotExist(err) {
|
|
return Package{}, fmt.Errorf("\u68c0\u67e5 Scout \u4ea7\u7269\u5931\u8d25: %w", err)
|
|
}
|
|
artifactMetadata := targets[0]
|
|
artifactMetadata.ID = artifactID
|
|
artifactMetadata.ArtifactID = artifactID
|
|
artifactMetadata.Size = size
|
|
artifactMetadata.SHA256 = digest
|
|
if err := saveArtifact(artifactDir, tempPath, artifactMetadata); err != nil {
|
|
return Package{}, err
|
|
}
|
|
for index := range targets {
|
|
targets[index].ArtifactID = artifactID
|
|
targets[index].Size = size
|
|
targets[index].SHA256 = digest
|
|
}
|
|
|
|
stored.Targets = append(stored.Targets, targets...)
|
|
stored.UploadedAt = time.Now().UTC()
|
|
applyTarget(&stored, targets[0])
|
|
if err := r.persistRelease(stored); err != nil {
|
|
return Package{}, err
|
|
}
|
|
r.storeRelease(stored)
|
|
|
|
result := stored
|
|
result.Targets = append([]Target(nil), targets...)
|
|
applyTarget(&result, targets[0])
|
|
r.broadcast(result)
|
|
return result, nil
|
|
}
|
|
|
|
func (r *Registry) LatestFor(name, platform, architecture, channel, currentVersion string) (Package, bool) {
|
|
r.mu.RLock()
|
|
versions := make([]Package, 0, len(r.packages[name]))
|
|
for _, item := range r.packages[name] {
|
|
versions = append(versions, clonePackage(item))
|
|
}
|
|
r.mu.RUnlock()
|
|
sort.Slice(versions, func(left, right int) bool {
|
|
return CompareVersions(versions[left].Version, versions[right].Version) > 0
|
|
})
|
|
for _, item := range versions {
|
|
if !releaseVisible(item, channel) {
|
|
continue
|
|
}
|
|
selected, ok := SelectTarget(item, platform, architecture, currentVersion)
|
|
if ok {
|
|
return selected, true
|
|
}
|
|
}
|
|
return Package{}, false
|
|
}
|
|
|
|
func (r *Registry) LatestForVersion(name, version, platform, architecture, channel, currentVersion string) (Package, bool) {
|
|
return r.GetFor(name, version, platform, architecture, channel, currentVersion)
|
|
}
|
|
|
|
func (r *Registry) VersionsFor(name, platform, architecture, channel, currentVersion string) []Package {
|
|
r.mu.RLock()
|
|
versions := make([]Package, 0, len(r.packages[name]))
|
|
for _, item := range r.packages[name] {
|
|
versions = append(versions, clonePackage(item))
|
|
}
|
|
r.mu.RUnlock()
|
|
result := make([]Package, 0, len(versions))
|
|
for _, item := range versions {
|
|
if !releaseVisible(item, channel) {
|
|
continue
|
|
}
|
|
if selected, ok := SelectTarget(item, platform, architecture, currentVersion); ok {
|
|
result = append(result, selected)
|
|
}
|
|
}
|
|
sort.Slice(result, func(left, right int) bool {
|
|
return CompareVersions(result[left].Version, result[right].Version) > 0
|
|
})
|
|
return result
|
|
}
|
|
|
|
func (r *Registry) GetFor(name, version, platform, architecture, channel, currentVersion string) (Package, bool) {
|
|
r.mu.RLock()
|
|
item, ok := r.packages[name][version]
|
|
if ok {
|
|
item = clonePackage(item)
|
|
}
|
|
r.mu.RUnlock()
|
|
if !ok || !releaseVisible(item, channel) {
|
|
return Package{}, false
|
|
}
|
|
return SelectTarget(item, platform, architecture, currentVersion)
|
|
}
|
|
|
|
func (r *Registry) OpenPackageFor(name, version, platform, architecture, currentVersion string) (*os.File, Package, error) {
|
|
item, ok := r.GetFor(name, version, platform, architecture, "", currentVersion)
|
|
if !ok || len(item.Targets) == 0 {
|
|
return nil, Package{}, os.ErrNotExist
|
|
}
|
|
target := item.Targets[0]
|
|
packagePath := filepath.Join(r.root, name, version, "artifacts", target.ArtifactID, "package.bin")
|
|
if target.ArtifactID == "legacy" {
|
|
packagePath = filepath.Join(r.root, name, version, "package.bin")
|
|
}
|
|
file, err := os.Open(packagePath)
|
|
if err != nil {
|
|
return nil, Package{}, err
|
|
}
|
|
return file, item, nil
|
|
}
|
|
|
|
func (r *Registry) ListFor(platform, architecture, channel, currentVersion string) []Package {
|
|
r.mu.RLock()
|
|
names := make([]string, 0, len(r.packages))
|
|
for name := range r.packages {
|
|
names = append(names, name)
|
|
}
|
|
r.mu.RUnlock()
|
|
sort.Strings(names)
|
|
result := make([]Package, 0, len(names))
|
|
for _, name := range names {
|
|
if item, ok := r.LatestFor(name, platform, architecture, channel, currentVersion); ok {
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func SelectTarget(item Package, platform, architecture, currentVersion string) (Package, bool) {
|
|
if len(item.Targets) == 0 {
|
|
return Package{}, false
|
|
}
|
|
platform = strings.TrimSpace(platform)
|
|
architecture = strings.TrimSpace(architecture)
|
|
if platform == "" && architecture == "" {
|
|
selected := item.Targets[0]
|
|
result := clonePackage(item)
|
|
result.Targets = []Target{selected}
|
|
applyTarget(&result, selected)
|
|
return result, true
|
|
}
|
|
normalizedPlatform, err := scoutpkg.NormalizePlatform(platform)
|
|
if err != nil {
|
|
return Package{}, false
|
|
}
|
|
normalizedArchitecture, err := scoutpkg.NormalizeArchitecture(architecture)
|
|
if err != nil {
|
|
return Package{}, false
|
|
}
|
|
bestIndex := -1
|
|
bestScore := 1000
|
|
for index, target := range item.Targets {
|
|
score, compatible := targetScore(target, normalizedPlatform, normalizedArchitecture, currentVersion)
|
|
if compatible && score < bestScore {
|
|
bestIndex = index
|
|
bestScore = score
|
|
}
|
|
}
|
|
if bestIndex < 0 {
|
|
return Package{}, false
|
|
}
|
|
selected := item.Targets[bestIndex]
|
|
result := clonePackage(item)
|
|
result.Targets = []Target{selected}
|
|
applyTarget(&result, selected)
|
|
return result, true
|
|
}
|
|
|
|
func targetScore(target Target, platform, architecture, currentVersion string) (int, bool) {
|
|
platformScore := -1
|
|
switch {
|
|
case target.Platform == platform && target.Architecture == architecture:
|
|
platformScore = 0
|
|
case target.Platform == platform && target.Architecture == "universal":
|
|
platformScore = 10
|
|
case target.Platform == "any" && target.Architecture == architecture:
|
|
platformScore = 20
|
|
case target.Platform == "any" && target.Architecture == "universal":
|
|
platformScore = 30
|
|
}
|
|
if platformScore < 0 {
|
|
return 0, false
|
|
}
|
|
if target.PackageType == "delta" {
|
|
if currentVersion == "" || !containsVersion(target.UpgradeFrom, currentVersion) {
|
|
return 0, false
|
|
}
|
|
return platformScore, true
|
|
}
|
|
return platformScore + 1, true
|
|
}
|
|
|
|
func normalizeRelease(item *Package) error {
|
|
if err := ValidatePackage(item.Name, item.Version); err != nil {
|
|
return err
|
|
}
|
|
item.Channel = strings.ToLower(strings.TrimSpace(item.Channel))
|
|
if item.Channel == "" {
|
|
item.Channel = "stable"
|
|
}
|
|
if !allowedChannels[item.Channel] {
|
|
return errors.New("\u53d1\u5e03\u901a\u9053\u53ea\u80fd\u662f stable\u3001beta \u6216 dev")
|
|
}
|
|
item.Status = strings.ToLower(strings.TrimSpace(item.Status))
|
|
if item.Status == "" {
|
|
item.Status = "published"
|
|
}
|
|
if !allowedStatuses[item.Status] {
|
|
return errors.New("\u53d1\u5e03\u72b6\u6001\u65e0\u6548")
|
|
}
|
|
item.PackageType = normalizePackageType(item.PackageType)
|
|
if item.FileName != "" {
|
|
item.FileName = filepath.Base(item.FileName)
|
|
if err := ValidateFileName(item.FileName); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeTarget(target *Target, fallbackType string) error {
|
|
platform := target.Platform
|
|
if strings.TrimSpace(platform) == "" {
|
|
platform = "any"
|
|
}
|
|
normalizedPlatform, err := scoutpkg.NormalizePlatform(platform)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
architecture := target.Architecture
|
|
if strings.TrimSpace(architecture) == "" {
|
|
architecture = "universal"
|
|
}
|
|
normalizedArchitecture, err := scoutpkg.NormalizeArchitecture(architecture)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
target.Platform = normalizedPlatform
|
|
target.Architecture = normalizedArchitecture
|
|
if target.PackageType == "" {
|
|
target.PackageType = fallbackType
|
|
}
|
|
target.PackageType = normalizePackageType(target.PackageType)
|
|
if target.PackageType == "delta" && len(target.UpgradeFrom) == 0 {
|
|
return errors.New("\u5dee\u5206\u5305\u5fc5\u987b\u6307\u5b9a upgrade_from")
|
|
}
|
|
for _, version := range target.UpgradeFrom {
|
|
if !versionPattern.MatchString(version) {
|
|
return errors.New("upgrade_from \u5305\u542b\u65e0\u6548\u7248\u672c")
|
|
}
|
|
}
|
|
target.FileName = filepath.Base(strings.TrimSpace(target.FileName))
|
|
if err := ValidateFileName(target.FileName); err != nil {
|
|
return err
|
|
}
|
|
target.ID = targetIdentity(*target)
|
|
return nil
|
|
}
|
|
|
|
func normalizePackage(item *Package) {
|
|
if item.Channel == "" {
|
|
item.Channel = "stable"
|
|
}
|
|
if item.Status == "" {
|
|
item.Status = "published"
|
|
}
|
|
if item.PackageType == "" {
|
|
item.PackageType = "full"
|
|
}
|
|
for index := range item.Targets {
|
|
if item.Targets[index].PackageType == "" {
|
|
item.Targets[index].PackageType = item.PackageType
|
|
}
|
|
}
|
|
}
|
|
|
|
func normalizePackageType(value string) string {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case "delta", "patch", "differential":
|
|
return "delta"
|
|
default:
|
|
return "full"
|
|
}
|
|
}
|
|
|
|
func targetIdentity(target Target) string {
|
|
identity := target.Platform + "-" + target.Architecture + "-" + target.PackageType
|
|
if target.PackageType == "delta" {
|
|
versions := append([]string(nil), target.UpgradeFrom...)
|
|
sort.Strings(versions)
|
|
digest := sha256.Sum256([]byte(strings.Join(versions, ",")))
|
|
identity += "-" + hex.EncodeToString(digest[:6])
|
|
}
|
|
return identity
|
|
}
|
|
|
|
func mergeReleaseMetadata(stored *Package, input Package) error {
|
|
if stored.Channel != input.Channel {
|
|
return errors.New("\u540c\u4e00\u7248\u672c\u4e0d\u80fd\u4f7f\u7528\u4e0d\u540c\u53d1\u5e03\u901a\u9053")
|
|
}
|
|
if stored.Status != input.Status {
|
|
return errors.New("\u540c\u4e00\u7248\u672c\u4e0d\u80fd\u4f7f\u7528\u4e0d\u540c\u53d1\u5e03\u72b6\u6001")
|
|
}
|
|
if stored.Description == "" {
|
|
stored.Description = input.Description
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeTemporaryArtifact(versionDir string, data io.Reader) (string, int64, string, error) {
|
|
temp, err := os.CreateTemp(versionDir, ".upload-*")
|
|
if err != nil {
|
|
return "", 0, "", fmt.Errorf("\u521b\u5efa\u4e34\u65f6\u6587\u4ef6\u5931\u8d25: %w", err)
|
|
}
|
|
tempPath := temp.Name()
|
|
hash := sha256.New()
|
|
size, copyErr := io.Copy(io.MultiWriter(temp, hash), data)
|
|
closeErr := temp.Close()
|
|
if copyErr != nil {
|
|
return tempPath, 0, "", fmt.Errorf("\u5199\u5165\u5305\u6587\u4ef6\u5931\u8d25: %w", copyErr)
|
|
}
|
|
if closeErr != nil {
|
|
return tempPath, 0, "", fmt.Errorf("\u5173\u95ed\u5305\u6587\u4ef6\u5931\u8d25: %w", closeErr)
|
|
}
|
|
if size == 0 {
|
|
return tempPath, 0, "", errors.New("\u5305\u6587\u4ef6\u4e0d\u80fd\u4e3a\u7a7a")
|
|
}
|
|
return tempPath, size, hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
func saveArtifact(artifactDir, tempPath string, metadata Target) error {
|
|
if err := os.MkdirAll(artifactDir, 0o755); err != nil {
|
|
return fmt.Errorf("\u521b\u5efa\u4ea7\u7269\u76ee\u5f55\u5931\u8d25: %w", err)
|
|
}
|
|
packagePath := filepath.Join(artifactDir, "package.bin")
|
|
if err := os.Rename(tempPath, packagePath); err != nil {
|
|
return fmt.Errorf("\u4fdd\u5b58\u4ea7\u7269\u6587\u4ef6\u5931\u8d25: %w", err)
|
|
}
|
|
data, err := json.MarshalIndent(metadata, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("\u7f16\u7801\u4ea7\u7269\u5143\u6570\u636e\u5931\u8d25: %w", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(artifactDir, "metadata.json"), data, 0o644); err != nil {
|
|
return fmt.Errorf("\u4fdd\u5b58\u4ea7\u7269\u5143\u6570\u636e\u5931\u8d25: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Registry) persistRelease(item Package) error {
|
|
data, err := json.MarshalIndent(item, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("\u7f16\u7801\u53d1\u5e03\u5143\u6570\u636e\u5931\u8d25: %w", err)
|
|
}
|
|
path := filepath.Join(r.root, item.Name, item.Version, "release.json")
|
|
if err := os.WriteFile(path, data, 0o644); err != nil {
|
|
return fmt.Errorf("\u4fdd\u5b58\u53d1\u5e03\u5143\u6570\u636e\u5931\u8d25: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Registry) release(name, version string) Package {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
return clonePackage(r.packages[name][version])
|
|
}
|
|
|
|
func (r *Registry) storeRelease(item Package) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.packages[item.Name] == nil {
|
|
r.packages[item.Name] = make(map[string]Package)
|
|
}
|
|
r.packages[item.Name][item.Version] = clonePackage(item)
|
|
}
|
|
|
|
func (r *Registry) broadcast(item Package) {
|
|
if item.Status != "published" {
|
|
return
|
|
}
|
|
r.mu.RLock()
|
|
subscribers := make([]chan Update, 0, len(r.subs))
|
|
for _, subscriber := range r.subs {
|
|
subscribers = append(subscribers, subscriber)
|
|
}
|
|
r.mu.RUnlock()
|
|
update := Update{Package: item}
|
|
for _, subscriber := range subscribers {
|
|
select {
|
|
case subscriber <- update:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func releaseVisible(item Package, channel string) bool {
|
|
if item.Status != "published" {
|
|
return false
|
|
}
|
|
channel = strings.ToLower(strings.TrimSpace(channel))
|
|
return channel == "" || item.Channel == channel
|
|
}
|
|
|
|
func applyTarget(item *Package, target Target) {
|
|
item.Platform = target.Platform
|
|
item.Architecture = target.Architecture
|
|
item.PackageType = target.PackageType
|
|
item.FileName = target.FileName
|
|
item.Size = target.Size
|
|
item.SHA256 = target.SHA256
|
|
}
|
|
|
|
func clonePackage(item Package) Package {
|
|
item.Targets = append([]Target(nil), item.Targets...)
|
|
for index := range item.Targets {
|
|
item.Targets[index].UpgradeFrom = append([]string(nil), item.Targets[index].UpgradeFrom...)
|
|
}
|
|
return item
|
|
}
|
|
|
|
func containsVersion(versions []string, wanted string) bool {
|
|
for _, version := range versions {
|
|
if version == wanted {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (r *Registry) loadReleases() error {
|
|
r.mu.Lock()
|
|
for name, versions := range r.packages {
|
|
for version, item := range versions {
|
|
normalizePackage(&item)
|
|
if len(item.Targets) == 0 {
|
|
item.Targets = []Target{{
|
|
ID: "legacy",
|
|
Platform: "any",
|
|
Architecture: "universal",
|
|
PackageType: "full",
|
|
FileName: item.FileName,
|
|
Size: item.Size,
|
|
SHA256: item.SHA256,
|
|
ArtifactID: "legacy",
|
|
}}
|
|
}
|
|
versions[version] = item
|
|
}
|
|
r.packages[name] = versions
|
|
}
|
|
r.mu.Unlock()
|
|
|
|
packages, err := os.ReadDir(r.root)
|
|
if err != nil {
|
|
return fmt.Errorf("\u8bfb\u53d6\u5b58\u50a8\u76ee\u5f55\u5931\u8d25: %w", err)
|
|
}
|
|
for _, packageEntry := range packages {
|
|
if !packageEntry.IsDir() || !packageNamePattern.MatchString(packageEntry.Name()) {
|
|
continue
|
|
}
|
|
versions, err := os.ReadDir(filepath.Join(r.root, packageEntry.Name()))
|
|
if err != nil {
|
|
return fmt.Errorf("\u8bfb\u53d6\u5305\u76ee\u5f55\u5931\u8d25: %w", err)
|
|
}
|
|
for _, versionEntry := range versions {
|
|
if !versionEntry.IsDir() || !versionPattern.MatchString(versionEntry.Name()) {
|
|
continue
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(r.root, packageEntry.Name(), versionEntry.Name(), "release.json"))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
var item Package
|
|
if err := json.Unmarshal(data, &item); err != nil {
|
|
return fmt.Errorf("\u8bfb\u53d6\u53d1\u5e03\u5143\u6570\u636e\u5931\u8d25: %w", err)
|
|
}
|
|
normalizePackage(&item)
|
|
r.storeRelease(item)
|
|
}
|
|
}
|
|
return nil
|
|
}
|