创建Go包管理服务端骨架
This commit is contained in:
parent
f0ed1688ff
commit
5ae932f5db
222
cmd/scout-client/main.go
Normal file
222
cmd/scout-client/main.go
Normal file
@ -0,0 +1,222 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"scout/internal/registry"
|
||||
)
|
||||
|
||||
type packageInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type updateEvent struct {
|
||||
Package packageInfo `json:"package"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
printUsage()
|
||||
os.Exit(2)
|
||||
}
|
||||
switch os.Args[1] {
|
||||
case "check":
|
||||
checkCommand(os.Args[2:])
|
||||
case "download":
|
||||
downloadCommand(os.Args[2:])
|
||||
case "watch":
|
||||
watchCommand(os.Args[2:])
|
||||
default:
|
||||
printUsage()
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
func checkCommand(args []string) {
|
||||
flags := flag.NewFlagSet("check", flag.ExitOnError)
|
||||
server := flags.String("server", "http://localhost:8080", "服务端地址")
|
||||
name := flags.String("package", "", "包名")
|
||||
current := flags.String("current", "", "当前版本")
|
||||
flags.Parse(args)
|
||||
requirePackage(*name)
|
||||
var latest packageInfo
|
||||
requestJSON(http.MethodGet, strings.TrimRight(*server, "/")+"/api/v1/packages/"+*name, nil, "", &latest)
|
||||
if registry.CompareVersions(latest.Version, *current) > 0 {
|
||||
fmt.Printf("发现更新:%s %s -> %s\n", latest.Name, *current, latest.Version)
|
||||
return
|
||||
}
|
||||
fmt.Printf("当前已是最新版本:%s\n", latest.Version)
|
||||
}
|
||||
|
||||
func downloadCommand(args []string) {
|
||||
flags := flag.NewFlagSet("download", flag.ExitOnError)
|
||||
server := flags.String("server", "http://localhost:8080", "服务端地址")
|
||||
name := flags.String("package", "", "包名")
|
||||
version := flags.String("version", "", "版本;为空时下载最新版本")
|
||||
output := flags.String("output", "", "保存路径")
|
||||
flags.Parse(args)
|
||||
requirePackage(*name)
|
||||
if *version == "" {
|
||||
var latest packageInfo
|
||||
requestJSON(http.MethodGet, strings.TrimRight(*server, "/")+"/api/v1/packages/"+*name, nil, "", &latest)
|
||||
*version = latest.Version
|
||||
}
|
||||
if *output == "" {
|
||||
*output = filepath.Join(".", fmt.Sprintf("%s-%s.pkg", *name, *version))
|
||||
}
|
||||
url := fmt.Sprintf("%s/api/v1/packages/%s/%s/download", strings.TrimRight(*server, "/"), *name, *version)
|
||||
response, err := http.Get(url)
|
||||
if err != nil {
|
||||
fatal("请求下载失败: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
fatalResponse(response)
|
||||
}
|
||||
file, err := os.Create(*output)
|
||||
if err != nil {
|
||||
fatal("创建输出文件失败: %v", err)
|
||||
}
|
||||
_, copyErr := io.Copy(file, response.Body)
|
||||
closeErr := file.Close()
|
||||
if copyErr != nil || closeErr != nil {
|
||||
fatal("保存包文件失败")
|
||||
}
|
||||
digest, err := fileSHA256(*output)
|
||||
if err != nil {
|
||||
fatal("校验包文件失败: %v", err)
|
||||
}
|
||||
if expected := response.Header.Get("X-Scout-SHA256"); expected != "" && digest != expected {
|
||||
_ = os.Remove(*output)
|
||||
fatal("包文件校验失败,文件已删除")
|
||||
}
|
||||
fmt.Printf("下载完成:%s\nSHA-256:%s\n", *output, digest)
|
||||
}
|
||||
|
||||
func watchCommand(args []string) {
|
||||
flags := flag.NewFlagSet("watch", flag.ExitOnError)
|
||||
server := flags.String("server", "http://localhost:8080", "服务端地址")
|
||||
packages := flags.String("packages", "", "要监听的包名,多个包用逗号分隔")
|
||||
flags.Parse(args)
|
||||
url := strings.TrimRight(*server, "/") + "/api/v1/updates/stream"
|
||||
if *packages != "" {
|
||||
url += "?packages=" + *packages
|
||||
}
|
||||
response, err := http.Get(url)
|
||||
if err != nil {
|
||||
fatal("连接更新通知失败: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
fatalResponse(response)
|
||||
}
|
||||
fmt.Println("已连接更新通知,等待新包……")
|
||||
readSSE(response.Body)
|
||||
}
|
||||
|
||||
func requestJSON(method, url string, body io.Reader, token string, target any) {
|
||||
request, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
fatal("创建请求失败: %v", err)
|
||||
}
|
||||
if token != "" {
|
||||
request.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
fatal("请求失败: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
fatalResponse(response)
|
||||
}
|
||||
if err := json.NewDecoder(response.Body).Decode(target); err != nil {
|
||||
fatal("解析服务端响应失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func readSSE(reader io.Reader) {
|
||||
dataLines := make([]string, 0, 1)
|
||||
buffer := make([]byte, 4096)
|
||||
pending := ""
|
||||
for {
|
||||
count, err := reader.Read(buffer)
|
||||
if count > 0 {
|
||||
pending += string(buffer[:count])
|
||||
for {
|
||||
lineEnd := strings.IndexByte(pending, '\n')
|
||||
if lineEnd < 0 {
|
||||
break
|
||||
}
|
||||
line := strings.TrimSuffix(pending[:lineEnd], "\r")
|
||||
pending = pending[lineEnd+1:]
|
||||
if line == "" {
|
||||
if len(dataLines) > 0 {
|
||||
var event updateEvent
|
||||
if json.Unmarshal([]byte(strings.Join(dataLines, "\n")), &event) == nil {
|
||||
fmt.Printf("收到更新:%s %s\n", event.Package.Name, event.Package.Version)
|
||||
}
|
||||
}
|
||||
dataLines = dataLines[:0]
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "data: ") {
|
||||
dataLines = append(dataLines, strings.TrimPrefix(line, "data: "))
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
fatal("读取更新通知失败: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fileSHA256(path string) (string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func fatalResponse(response *http.Response) {
|
||||
body, _ := io.ReadAll(io.LimitReader(response.Body, 1<<20))
|
||||
fatal("服务端返回 %s:%s", response.Status, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
func requirePackage(name string) {
|
||||
if name == "" {
|
||||
fatal("必须指定 --package")
|
||||
}
|
||||
}
|
||||
|
||||
func fatal(format string, args ...any) {
|
||||
fmt.Printf("错误:"+format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Println("Scout 轻量化包管理客户端")
|
||||
fmt.Println("用法:scout-client <check|download|watch> [参数]")
|
||||
}
|
||||
35
cmd/scout-server/main.go
Normal file
35
cmd/scout-server/main.go
Normal file
@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"scout/internal/httpapi"
|
||||
"scout/internal/registry"
|
||||
)
|
||||
|
||||
func main() {
|
||||
address := envOr("SCOUT_ADDR", ":8080")
|
||||
dataDir := envOr("SCOUT_DATA_DIR", "data")
|
||||
token := os.Getenv("SCOUT_DEVELOPER_TOKEN")
|
||||
if token == "" {
|
||||
log.Fatal("请设置 SCOUT_DEVELOPER_TOKEN 后再启动服务端")
|
||||
}
|
||||
store, err := registry.New(dataDir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
server := &httpapi.Server{Registry: store, DeveloperToken: token}
|
||||
log.Printf("Scout 包服务端正在监听 %s,数据目录为 %s", address, dataDir)
|
||||
if err := http.ListenAndServe(address, server.Handler()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func envOr(name, fallback string) string {
|
||||
if value := os.Getenv(name); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
187
internal/httpapi/server.go
Normal file
187
internal/httpapi/server.go
Normal file
@ -0,0 +1,187 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"scout/internal/registry"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Registry *registry.Registry
|
||||
DeveloperToken string
|
||||
MaxUploadBytes int64
|
||||
}
|
||||
|
||||
func (s *Server) Handler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz", s.health)
|
||||
mux.HandleFunc("POST /api/v1/packages/upload", s.upload)
|
||||
mux.HandleFunc("GET /api/v1/updates/stream", s.stream)
|
||||
mux.HandleFunc("GET /api/v1/packages/", s.packageRoute)
|
||||
return logging(mux)
|
||||
}
|
||||
|
||||
func (s *Server) health(writer http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(writer, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) upload(writer http.ResponseWriter, request *http.Request) {
|
||||
if !s.authorized(request) {
|
||||
writeError(writer, http.StatusUnauthorized, "开发者令牌无效")
|
||||
return
|
||||
}
|
||||
maxBytes := s.MaxUploadBytes
|
||||
if maxBytes <= 0 {
|
||||
maxBytes = 512 << 20
|
||||
}
|
||||
request.Body = http.MaxBytesReader(writer, request.Body, maxBytes)
|
||||
if err := request.ParseMultipartForm(32 << 20); err != nil {
|
||||
writeError(writer, http.StatusBadRequest, "上传表单无效: "+err.Error())
|
||||
return
|
||||
}
|
||||
file, _, err := request.FormFile("file")
|
||||
if err != nil {
|
||||
writeError(writer, http.StatusBadRequest, "缺少包文件字段 file")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
item, err := s.Registry.AddPackage(registry.Package{
|
||||
Name: request.FormValue("name"),
|
||||
Version: request.FormValue("version"),
|
||||
Description: request.FormValue("description"),
|
||||
Platform: request.FormValue("platform"),
|
||||
Architecture: request.FormValue("architecture"),
|
||||
}, file)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if strings.Contains(err.Error(), "已经存在") {
|
||||
status = http.StatusConflict
|
||||
}
|
||||
writeError(writer, status, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(writer, http.StatusCreated, item)
|
||||
}
|
||||
|
||||
func (s *Server) packageRoute(writer http.ResponseWriter, request *http.Request) {
|
||||
path := strings.TrimPrefix(request.URL.Path, "/api/v1/packages/")
|
||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
if len(parts) == 1 && parts[0] != "" {
|
||||
item, ok := s.Registry.Latest(parts[0])
|
||||
if !ok {
|
||||
writeError(writer, http.StatusNotFound, "包不存在")
|
||||
return
|
||||
}
|
||||
writeJSON(writer, http.StatusOK, item)
|
||||
return
|
||||
}
|
||||
if len(parts) == 2 && parts[0] != "" {
|
||||
if parts[1] == "versions" {
|
||||
writeJSON(writer, http.StatusOK, map[string]any{"name": parts[0], "versions": s.Registry.Versions(parts[0])})
|
||||
return
|
||||
}
|
||||
writeError(writer, http.StatusNotFound, "接口不存在")
|
||||
return
|
||||
}
|
||||
if len(parts) == 3 && parts[2] == "download" {
|
||||
s.download(writer, request, parts[0], parts[1])
|
||||
return
|
||||
}
|
||||
writeError(writer, http.StatusNotFound, "接口不存在")
|
||||
}
|
||||
|
||||
func (s *Server) download(writer http.ResponseWriter, request *http.Request, name, version string) {
|
||||
file, item, err := s.Registry.OpenPackage(name, version)
|
||||
if err != nil {
|
||||
writeError(writer, http.StatusNotFound, "包版本不存在")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
writer.Header().Set("Content-Type", "application/octet-stream")
|
||||
writer.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s-%s.pkg"`, name, version))
|
||||
writer.Header().Set("X-Scout-SHA256", item.SHA256)
|
||||
writer.Header().Set("Content-Length", strconv.FormatInt(item.Size, 10))
|
||||
if request.Method == http.MethodHead {
|
||||
return
|
||||
}
|
||||
if _, err := io.Copy(writer, file); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) stream(writer http.ResponseWriter, request *http.Request) {
|
||||
flusher, ok := writer.(http.Flusher)
|
||||
if !ok {
|
||||
writeError(writer, http.StatusInternalServerError, "当前服务器不支持实时通知")
|
||||
return
|
||||
}
|
||||
wanted := make(map[string]bool)
|
||||
for _, name := range strings.Split(request.URL.Query().Get("packages"), ",") {
|
||||
if name = strings.TrimSpace(name); name != "" {
|
||||
wanted[name] = true
|
||||
}
|
||||
}
|
||||
updates, unsubscribe := s.Registry.Subscribe()
|
||||
defer unsubscribe()
|
||||
writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
|
||||
writer.Header().Set("Cache-Control", "no-cache")
|
||||
writer.Header().Set("Connection", "keep-alive")
|
||||
fmt.Fprint(writer, ": 已连接\n\n")
|
||||
flusher.Flush()
|
||||
for {
|
||||
select {
|
||||
case <-request.Context().Done():
|
||||
return
|
||||
case update, ok := <-updates:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if len(wanted) > 0 && !wanted[update.Package.Name] {
|
||||
continue
|
||||
}
|
||||
payload, _ := json.Marshal(update)
|
||||
fmt.Fprintf(writer, "event: package_updated\ndata: %s\n\n", payload)
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) authorized(request *http.Request) bool {
|
||||
if s.DeveloperToken == "" {
|
||||
return false
|
||||
}
|
||||
value := request.Header.Get("Authorization")
|
||||
if strings.HasPrefix(value, "Bearer ") {
|
||||
return strings.TrimPrefix(value, "Bearer ") == s.DeveloperToken
|
||||
}
|
||||
return request.Header.Get("X-Developer-Token") == s.DeveloperToken
|
||||
}
|
||||
|
||||
func writeJSON(writer http.ResponseWriter, status int, value any) {
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(status)
|
||||
_ = json.NewEncoder(writer).Encode(value)
|
||||
}
|
||||
|
||||
func writeError(writer http.ResponseWriter, status int, message string) {
|
||||
writeJSON(writer, status, map[string]string{"error": message})
|
||||
}
|
||||
|
||||
func logging(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.Method == http.MethodOptions {
|
||||
writer.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(writer, request)
|
||||
})
|
||||
}
|
||||
|
||||
var _ = errors.New
|
||||
295
internal/registry/registry.go
Normal file
295
internal/registry/registry.go
Normal file
@ -0,0 +1,295 @@
|
||||
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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user