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 }