This commit is contained in:
zyj
2026-03-03 18:20:18 +08:00
commit a9f9330744
25 changed files with 5004 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
package collector
import (
"context"
"time"
)
// Category 采集器分类
type Category string
const (
CategoryRuntime Category = "runtime" // 编程语言运行时
CategoryPackage Category = "package" // 包管理器
CategoryEditor Category = "editor" // 编辑器/IDE
CategoryShell Category = "shell" // Shell 配置
CategoryGit Category = "git" // Git 配置
CategoryEnv Category = "env" // 环境变量
CategoryFont Category = "font" // 字体
CategorySSH Category = "ssh" // SSH/GPG 密钥
CategoryCustom Category = "custom" // 自定义
)
// Collector 定义了所有采集器必须实现的接口
type Collector interface {
// Name 返回采集器名称(唯一标识)
Name() string
// DisplayName 返回采集器的显示名称
DisplayName() string
// Description 返回采集器的描述信息
Description() string
// Category 返回采集器所属分类
Category() Category
// IsAvailable 检测当前系统是否支持此采集器
IsAvailable(ctx context.Context) bool
// Scan 扫描当前环境,返回扫描结果
Scan(ctx context.Context, opts ScanOptions) (*ScanResult, error)
// Capture 捕获环境数据,写入到指定目录
Capture(ctx context.Context, targetDir string, opts CaptureOptions) error
// Restore 从指定目录还原环境
Restore(ctx context.Context, sourceDir string, opts RestoreOptions) error
// Verify 验证还原后的环境是否正确
Verify(ctx context.Context) (*VerifyResult, error)
}
// ScanOptions 扫描选项
type ScanOptions struct {
Detailed bool `json:"detailed"`
Filters map[string]string `json:"filters,omitempty"`
Timeout time.Duration `json:"timeout,omitempty"`
}
// CaptureOptions 捕获选项
type CaptureOptions struct {
IncludePatterns []string `json:"include_patterns,omitempty"`
ExcludePatterns []string `json:"exclude_patterns,omitempty"`
Encrypt bool `json:"encrypt"`
}
// RestoreOptions 还原选项
type RestoreOptions struct {
ConflictStrategy ConflictStrategy `json:"conflict_strategy"`
DryRun bool `json:"dry_run"`
Force bool `json:"force"`
}
// ConflictStrategy 冲突处理策略
type ConflictStrategy string
const (
ConflictSkip ConflictStrategy = "skip"
ConflictOverwrite ConflictStrategy = "overwrite"
ConflictMerge ConflictStrategy = "merge"
ConflictPrompt ConflictStrategy = "prompt"
ConflictNewest ConflictStrategy = "newest"
)
// PlatformInfo 平台信息
type PlatformInfo struct {
OS string `json:"os"`
Version string `json:"os_version"`
Arch string `json:"arch"`
}
// ScanResult 扫描结果
type ScanResult struct {
Collector string `json:"collector"`
Category Category `json:"category"`
Items []ScanItem `json:"items"`
Timestamp time.Time `json:"timestamp"`
Platform PlatformInfo `json:"platform"`
Errors []string `json:"errors,omitempty"`
}
// ScanItem 单个扫描项
type ScanItem struct {
Name string `json:"name"`
Version string `json:"version,omitempty"`
Path string `json:"path,omitempty"`
Type string `json:"type,omitempty"`
Properties map[string]string `json:"properties,omitempty"`
Size int64 `json:"size,omitempty"`
Sensitive bool `json:"sensitive,omitempty"`
Children []ScanItem `json:"children,omitempty"`
}
// VerifyResult 验证结果
type VerifyResult struct {
Success bool `json:"success"`
Items []VerifyItem `json:"items"`
}
// VerifyItem 验证项
type VerifyItem struct {
Name string `json:"name"`
Status string `json:"status"` // ok, missing, version_mismatch, error
Message string `json:"message,omitempty"`
}

View File

@@ -0,0 +1,121 @@
package collector
import (
"context"
"fmt"
"sync"
)
// Registry 采集器注册中心
type Registry struct {
collectors map[string]Collector
mu sync.RWMutex
}
// NewRegistry 创建采集器注册中心
func NewRegistry() *Registry {
return &Registry{
collectors: make(map[string]Collector),
}
}
// Register 注册一个采集器
func (r *Registry) Register(c Collector) error {
r.mu.Lock()
defer r.mu.Unlock()
name := c.Name()
if _, exists := r.collectors[name]; exists {
return fmt.Errorf("collector already registered: %s", name)
}
r.collectors[name] = c
return nil
}
// Get 获取指定名称的采集器
func (r *Registry) Get(name string) (Collector, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
c, ok := r.collectors[name]
return c, ok
}
// List 列出所有已注册的采集器
func (r *Registry) List() []Collector {
r.mu.RLock()
defer r.mu.RUnlock()
result := make([]Collector, 0, len(r.collectors))
for _, c := range r.collectors {
result = append(result, c)
}
return result
}
// ListByCategory 按分类列出采集器
func (r *Registry) ListByCategory(cat Category) []Collector {
r.mu.RLock()
defer r.mu.RUnlock()
var result []Collector
for _, c := range r.collectors {
if c.Category() == cat {
result = append(result, c)
}
}
return result
}
// Available 返回当前系统上可用的采集器
func (r *Registry) Available(ctx context.Context) []Collector {
r.mu.RLock()
defer r.mu.RUnlock()
var result []Collector
for _, c := range r.collectors {
if c.IsAvailable(ctx) {
result = append(result, c)
}
}
return result
}
// ScanAll 使用所有可用的采集器扫描
func (r *Registry) ScanAll(ctx context.Context, opts ScanOptions) ([]*ScanResult, error) {
available := r.Available(ctx)
var (
results []*ScanResult
mu sync.Mutex
wg sync.WaitGroup
errs []error
)
for _, c := range available {
wg.Add(1)
go func(collector Collector) {
defer wg.Done()
result, err := collector.Scan(ctx, opts)
mu.Lock()
defer mu.Unlock()
if err != nil {
errs = append(errs, fmt.Errorf("%s: %w", collector.Name(), err))
return
}
results = append(results, result)
}(c)
}
wg.Wait()
if len(errs) > 0 && len(results) == 0 {
return nil, fmt.Errorf("all collectors failed: %v", errs)
}
return results, nil
}

View File

@@ -0,0 +1,87 @@
package collector
import (
"context"
"testing"
)
func TestRegistry_Register(t *testing.T) {
r := NewRegistry()
mock := &mockCollector{name: "test"}
err := r.Register(mock)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Duplicate registration should fail
err = r.Register(mock)
if err == nil {
t.Fatal("expected error for duplicate registration")
}
}
func TestRegistry_Get(t *testing.T) {
r := NewRegistry()
mock := &mockCollector{name: "test"}
_ = r.Register(mock)
c, ok := r.Get("test")
if !ok {
t.Fatal("expected to find collector")
}
if c.Name() != "test" {
t.Fatalf("expected name 'test', got '%s'", c.Name())
}
_, ok = r.Get("nonexistent")
if ok {
t.Fatal("expected not to find collector")
}
}
func TestRegistry_List(t *testing.T) {
r := NewRegistry()
_ = r.Register(&mockCollector{name: "a", category: CategoryRuntime})
_ = r.Register(&mockCollector{name: "b", category: CategoryEditor})
_ = r.Register(&mockCollector{name: "c", category: CategoryRuntime})
all := r.List()
if len(all) != 3 {
t.Fatalf("expected 3 collectors, got %d", len(all))
}
runtime := r.ListByCategory(CategoryRuntime)
if len(runtime) != 2 {
t.Fatalf("expected 2 runtime collectors, got %d", len(runtime))
}
}
// mockCollector implements Collector for testing
type mockCollector struct {
name string
category Category
available bool
}
func (m *mockCollector) Name() string { return m.name }
func (m *mockCollector) DisplayName() string { return m.name }
func (m *mockCollector) Description() string { return "mock collector" }
func (m *mockCollector) Category() Category { return m.category }
func (m *mockCollector) IsAvailable(ctx context.Context) bool {
return m.available
}
func (m *mockCollector) Scan(ctx context.Context, opts ScanOptions) (*ScanResult, error) {
return &ScanResult{Collector: m.name, Category: m.category}, nil
}
func (m *mockCollector) Capture(ctx context.Context, targetDir string, opts CaptureOptions) error {
return nil
}
func (m *mockCollector) Restore(ctx context.Context, sourceDir string, opts RestoreOptions) error {
return nil
}
func (m *mockCollector) Verify(ctx context.Context) (*VerifyResult, error) {
return &VerifyResult{Success: true}, nil
}