232 lines
6.3 KiB
Go
232 lines
6.3 KiB
Go
package registry
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const DefaultProject = "default"
|
|
|
|
var projectIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`)
|
|
|
|
type Project struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
Status string `json:"status"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type ProjectRegistry struct {
|
|
root string
|
|
projects map[string]Project
|
|
registries map[string]*Registry
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func ValidateProjectID(id string) error {
|
|
if !projectIDPattern.MatchString(strings.TrimSpace(id)) {
|
|
return errors.New("项目标识只能包含字母、数字、点、下划线和短横线,长度为 1 到 64")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func NewProjectRegistry(root string) (*ProjectRegistry, error) {
|
|
if strings.TrimSpace(root) == "" {
|
|
return nil, errors.New("项目存储目录不能为空")
|
|
}
|
|
projectsRoot := filepath.Join(root, "projects")
|
|
if err := os.MkdirAll(projectsRoot, 0o755); err != nil {
|
|
return nil, fmt.Errorf("创建项目存储目录失败: %w", err)
|
|
}
|
|
result := &ProjectRegistry{
|
|
root: projectsRoot,
|
|
projects: make(map[string]Project),
|
|
registries: make(map[string]*Registry),
|
|
}
|
|
entries, err := os.ReadDir(projectsRoot)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("读取项目目录失败: %w", err)
|
|
}
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() || ValidateProjectID(entry.Name()) != nil {
|
|
continue
|
|
}
|
|
if err := result.loadProject(entry.Name()); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (r *ProjectRegistry) Create(input Project) (Project, error) {
|
|
input.ID = strings.TrimSpace(input.ID)
|
|
if err := ValidateProjectID(input.ID); err != nil {
|
|
return Project{}, err
|
|
}
|
|
if input.ID == DefaultProject {
|
|
return Project{}, errors.New("default 是兼容项目标识,不能创建")
|
|
}
|
|
if input.Name == "" {
|
|
input.Name = input.ID
|
|
}
|
|
input.Status = strings.TrimSpace(input.Status)
|
|
if input.Status == "" {
|
|
input.Status = "active"
|
|
}
|
|
if input.Status != "active" && input.Status != "disabled" {
|
|
return Project{}, errors.New("项目状态只能是 active 或 disabled")
|
|
}
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if _, exists := r.projects[input.ID]; exists {
|
|
return Project{}, errors.New("项目已经存在")
|
|
}
|
|
now := time.Now().UTC()
|
|
input.CreatedAt = now
|
|
input.UpdatedAt = now
|
|
projectDir := filepath.Join(r.root, input.ID)
|
|
if err := os.MkdirAll(projectDir, 0o755); err != nil {
|
|
return Project{}, fmt.Errorf("创建项目目录失败: %w", err)
|
|
}
|
|
if err := writeProjectMetadata(projectDir, input); err != nil {
|
|
return Project{}, err
|
|
}
|
|
store, err := New(projectDir)
|
|
if err != nil {
|
|
return Project{}, err
|
|
}
|
|
r.projects[input.ID] = input
|
|
r.registries[input.ID] = store
|
|
return input, nil
|
|
}
|
|
|
|
func (r *ProjectRegistry) Update(id string, input Project) (Project, error) {
|
|
id = strings.TrimSpace(id)
|
|
if err := ValidateProjectID(id); err != nil {
|
|
return Project{}, err
|
|
}
|
|
if id == DefaultProject {
|
|
return Project{}, errors.New("default 是兼容项目标识,不能修改")
|
|
}
|
|
if strings.TrimSpace(input.Name) == "" {
|
|
return Project{}, errors.New("项目名称不能为空")
|
|
}
|
|
if input.Status != "active" && input.Status != "disabled" {
|
|
return Project{}, errors.New("项目状态只能是 active 或 disabled")
|
|
}
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
current, exists := r.projects[id]
|
|
if !exists {
|
|
return Project{}, errors.New("项目不存在")
|
|
}
|
|
current.Name = strings.TrimSpace(input.Name)
|
|
current.Description = strings.TrimSpace(input.Description)
|
|
current.Status = input.Status
|
|
current.UpdatedAt = time.Now().UTC()
|
|
if err := writeProjectMetadata(filepath.Join(r.root, id), current); err != nil {
|
|
return Project{}, err
|
|
}
|
|
r.projects[id] = current
|
|
return current, nil
|
|
}
|
|
func (r *ProjectRegistry) Ensure(id, name, description string) (Project, *Registry, error) {
|
|
id = strings.TrimSpace(id)
|
|
if err := ValidateProjectID(id); err != nil {
|
|
return Project{}, nil, err
|
|
}
|
|
if id == DefaultProject {
|
|
return Project{}, nil, errors.New("default 是兼容项目标识,请继续使用旧接口")
|
|
}
|
|
r.mu.RLock()
|
|
project, exists := r.projects[id]
|
|
store := r.registries[id]
|
|
r.mu.RUnlock()
|
|
if exists {
|
|
return project, store, nil
|
|
}
|
|
project, err := r.Create(Project{ID: id, Name: name, Description: description})
|
|
if err != nil {
|
|
return Project{}, nil, err
|
|
}
|
|
r.mu.RLock()
|
|
store = r.registries[id]
|
|
r.mu.RUnlock()
|
|
return project, store, nil
|
|
}
|
|
|
|
func (r *ProjectRegistry) Get(id string) (Project, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
project, ok := r.projects[strings.TrimSpace(id)]
|
|
return project, ok
|
|
}
|
|
|
|
func (r *ProjectRegistry) List() []Project {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
result := make([]Project, 0, len(r.projects))
|
|
for _, project := range r.projects {
|
|
result = append(result, project)
|
|
}
|
|
sort.Slice(result, func(i, j int) bool { return result[i].ID < result[j].ID })
|
|
return result
|
|
}
|
|
|
|
func (r *ProjectRegistry) Store(id string) (*Registry, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
store, ok := r.registries[strings.TrimSpace(id)]
|
|
return store, ok
|
|
}
|
|
|
|
func (r *ProjectRegistry) loadProject(id string) error {
|
|
projectDir := filepath.Join(r.root, id)
|
|
project := Project{ID: id, Name: id, Status: "active"}
|
|
metadata, err := os.ReadFile(filepath.Join(projectDir, "project.json"))
|
|
if err == nil {
|
|
if err := json.Unmarshal(metadata, &project); err != nil {
|
|
return fmt.Errorf("读取项目元数据失败: %w", err)
|
|
}
|
|
project.ID = id
|
|
if project.Name == "" {
|
|
project.Name = id
|
|
}
|
|
if project.Status == "" {
|
|
project.Status = "active"
|
|
}
|
|
} else if !os.IsNotExist(err) {
|
|
return fmt.Errorf("读取项目元数据失败: %w", err)
|
|
}
|
|
store, err := New(projectDir)
|
|
if err != nil {
|
|
return fmt.Errorf("加载项目 %s 失败: %w", id, err)
|
|
}
|
|
r.projects[id] = project
|
|
r.registries[id] = store
|
|
return nil
|
|
}
|
|
|
|
func writeProjectMetadata(projectDir string, project Project) error {
|
|
data, err := json.MarshalIndent(project, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("编码项目元数据失败: %w", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(projectDir, "project.json"), data, 0o644); err != nil {
|
|
return fmt.Errorf("保存项目元数据失败: %w", err)
|
|
}
|
|
return nil
|
|
}
|