package registry import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "regexp" "sort" "strings" "sync" "time" ) var ( packageNamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`) versionPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9.+_-]{0,63}$`) fileNamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`) ) type Package struct { Project string `json:"project,omitempty"` Name string `json:"name"` Version string `json:"version"` Description string `json:"description,omitempty"` Platform string `json:"platform,omitempty"` Architecture string `json:"architecture,omitempty"` PackageType string `json:"package_type,omitempty"` Channel string `json:"channel,omitempty"` Status string `json:"status,omitempty"` FileName string `json:"file_name,omitempty"` UploaderID string `json:"uploader_id,omitempty"` UploaderEmail string `json:"uploader_email,omitempty"` Size int64 `json:"size"` SHA256 string `json:"sha256"` UploadedAt time.Time `json:"uploaded_at"` Targets []Target `json:"targets,omitempty"` } type Target struct { ID string `json:"id"` Platform string `json:"platform"` Architecture string `json:"architecture"` PackageType string `json:"package_type"` FileName string `json:"file_name"` Size int64 `json:"size"` SHA256 string `json:"sha256"` ArtifactID string `json:"artifact_id"` Entrypoint string `json:"entrypoint,omitempty"` UpgradeFrom []string `json:"upgrade_from,omitempty"` } type Update struct { Package Package `json:"package"` } type Registry struct { root string mu sync.RWMutex writeMu sync.Mutex packages map[string]map[string]Package subs map[int]chan Update nextSub int } func New(root string) (*Registry, error) { if root == "" { return nil, errors.New("存储目录不能为空") } if err := os.MkdirAll(root, 0o755); err != nil { return nil, fmt.Errorf("创建存储目录失败: %w", err) } registry := &Registry{ root: root, packages: make(map[string]map[string]Package), subs: make(map[int]chan Update), } if err := registry.load(); err != nil { return nil, err } if err := registry.loadReleases(); err != nil { return nil, err } return registry, nil } func ValidatePackage(name, version string) error { if !packageNamePattern.MatchString(name) { return errors.New("包名只能包含字母、数字、点、下划线和短横线,长度为 1 到 64") } if !versionPattern.MatchString(version) { return errors.New("版本只能包含字母、数字、点、加号、下划线和短横线,长度为 1 到 64") } return nil } func ValidateFileName(fileName string) error { fileName = filepath.Base(strings.TrimSpace(fileName)) if fileName == "." || fileName == "" || !fileNamePattern.MatchString(fileName) { return errors.New("包文件名无效") } return nil } func (r *Registry) addLegacyPackage(input Package, data io.Reader) (Package, error) { if err := ValidatePackage(input.Name, input.Version); err != nil { return Package{}, err } if data == nil { return Package{}, errors.New("包文件不能为空") } if input.FileName == "" { input.FileName = input.Name + "-" + input.Version + ".pkg" } input.FileName = filepath.Base(input.FileName) if err := ValidateFileName(input.FileName); err != nil { return Package{}, err } r.writeMu.Lock() defer r.writeMu.Unlock() packageDir := filepath.Join(r.root, input.Name) if err := os.MkdirAll(packageDir, 0o755); err != nil { return Package{}, fmt.Errorf("创建包目录失败: %w", err) } versionDir := filepath.Join(packageDir, input.Version) packagePath := filepath.Join(versionDir, "package.bin") metadataPath := filepath.Join(versionDir, "metadata.json") if _, err := os.Stat(packagePath); err == nil { return Package{}, errors.New("该包版本已经存在") } else if !os.IsNotExist(err) { return Package{}, fmt.Errorf("检查包版本失败: %w", err) } temp, err := os.CreateTemp(packageDir, ".upload-*") if err != nil { return Package{}, fmt.Errorf("创建临时文件失败: %w", err) } tempName := temp.Name() defer os.Remove(tempName) hash := sha256.New() size, err := io.Copy(io.MultiWriter(temp, hash), data) if err != nil { temp.Close() return Package{}, fmt.Errorf("写入包文件失败: %w", err) } if size == 0 { temp.Close() return Package{}, errors.New("包文件不能为空") } if err := temp.Close(); err != nil { return Package{}, fmt.Errorf("关闭包文件失败: %w", err) } input.Size = size input.SHA256 = hex.EncodeToString(hash.Sum(nil)) input.UploadedAt = time.Now().UTC() if err := os.MkdirAll(versionDir, 0o755); err != nil { return Package{}, fmt.Errorf("创建版本目录失败: %w", err) } if err := os.Rename(tempName, packagePath); err != nil { return Package{}, fmt.Errorf("保存包文件失败: %w", err) } metadata, err := json.MarshalIndent(input, "", " ") if err != nil { return Package{}, fmt.Errorf("编码包元数据失败: %w", err) } if err := os.WriteFile(metadataPath, metadata, 0o644); err != nil { os.Remove(packagePath) return Package{}, fmt.Errorf("保存包元数据失败: %w", err) } r.mu.Lock() if r.packages[input.Name] == nil { r.packages[input.Name] = make(map[string]Package) } r.packages[input.Name][input.Version] = input subs := make([]chan Update, 0, len(r.subs)) for _, subscriber := range r.subs { subs = append(subs, subscriber) } r.mu.Unlock() update := Update{Package: input} for _, subscriber := range subs { select { case subscriber <- update: default: } } return input, nil } func (r *Registry) Latest(name string) (Package, bool) { r.mu.RLock() defer r.mu.RUnlock() versions := r.packages[name] if len(versions) == 0 { return Package{}, false } keys := make([]string, 0, len(versions)) for version := range versions { keys = append(keys, version) } sort.Slice(keys, func(i, j int) bool { return CompareVersions(keys[i], keys[j]) > 0 }) return versions[keys[0]], true } func (r *Registry) Versions(name string) []Package { r.mu.RLock() defer r.mu.RUnlock() versions := r.packages[name] result := make([]Package, 0, len(versions)) for _, item := range versions { result = append(result, item) } sort.Slice(result, func(i, j int) bool { return CompareVersions(result[i].Version, result[j].Version) > 0 }) return result } func (r *Registry) Get(name, version string) (Package, bool) { r.mu.RLock() defer r.mu.RUnlock() item, ok := r.packages[name][version] return item, ok } func (r *Registry) OpenPackage(name, version string) (*os.File, Package, error) { return r.OpenPackageFor(name, version, "", "", "", "") } func (r *Registry) Subscribe() (<-chan Update, func()) { r.mu.Lock() defer r.mu.Unlock() id := r.nextSub r.nextSub++ channel := make(chan Update, 16) r.subs[id] = channel return channel, func() { r.mu.Lock() defer r.mu.Unlock() if existing, ok := r.subs[id]; ok { delete(r.subs, id) close(existing) } } } func (r *Registry) load() error { entries, err := os.ReadDir(r.root) if err != nil { return fmt.Errorf("读取存储目录失败: %w", err) } for _, packageEntry := range entries { if !packageEntry.IsDir() || !packageNamePattern.MatchString(packageEntry.Name()) { continue } versions, err := os.ReadDir(filepath.Join(r.root, packageEntry.Name())) if err != nil { return fmt.Errorf("读取包目录失败: %w", err) } for _, versionEntry := range versions { if !versionEntry.IsDir() || !versionPattern.MatchString(versionEntry.Name()) { continue } metadataPath := filepath.Join(r.root, packageEntry.Name(), versionEntry.Name(), "metadata.json") metadata, err := os.ReadFile(metadataPath) if err != nil { continue } var item Package if json.Unmarshal(metadata, &item) == nil { if r.packages[item.Name] == nil { r.packages[item.Name] = make(map[string]Package) } r.packages[item.Name][item.Version] = item } } } return nil } func CompareVersions(left, right string) int { leftParts := splitVersion(left) rightParts := splitVersion(right) for index := 0; index < len(leftParts) || index < len(rightParts); index++ { if index >= len(leftParts) { return -1 } if index >= len(rightParts) { return 1 } leftNumber, leftOK := numericPart(leftParts[index]) rightNumber, rightOK := numericPart(rightParts[index]) if leftOK && rightOK { if leftNumber != rightNumber { if leftNumber > rightNumber { return 1 } return -1 } continue } if leftParts[index] != rightParts[index] { return strings.Compare(leftParts[index], rightParts[index]) } } return 0 } func splitVersion(version string) []string { version = strings.TrimPrefix(version, "v") version = strings.SplitN(version, "+", 2)[0] return strings.FieldsFunc(version, func(r rune) bool { return r == '.' || r == '-' || r == '_' }) } func numericPart(value string) (int64, bool) { var number int64 if value == "" { return 0, false } for _, char := range value { if char < '0' || char > '9' { return 0, false } number = number*10 + int64(char-'0') } return number, true }