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, channel, currentVersion string) (*os.File, Package, error) { item, ok := r.GetFor(name, version, platform, architecture, channel, 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 }