Scout/internal/registry/registry.go
2026-08-30 21:45:23 +08:00

296 lines
7.7 KiB
Go

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}$`)
)
type Package struct {
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description,omitempty"`
Platform string `json:"platform,omitempty"`
Architecture string `json:"architecture,omitempty"`
Size int64 `json:"size"`
SHA256 string `json:"sha256"`
UploadedAt time.Time `json:"uploaded_at"`
}
type Update struct {
Package Package `json:"package"`
}
type Registry struct {
root string
mu sync.RWMutex
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
}
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 (r *Registry) AddPackage(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("包文件不能为空")
}
packageDir := filepath.Join(r.root, input.Name)
if err := os.MkdirAll(packageDir, 0o755); err != nil {
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 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()
versionDir := filepath.Join(packageDir, input.Version)
if err := os.MkdirAll(versionDir, 0o755); err != nil {
return Package{}, fmt.Errorf("创建版本目录失败: %w", err)
}
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)
}
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) {
item, ok := r.Get(name, version)
if !ok {
return nil, Package{}, os.ErrNotExist
}
file, err := os.Open(filepath.Join(r.root, name, version, "package.bin"))
if err != nil {
return nil, Package{}, err
}
return file, item, nil
}
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
}