package snapshot import ( "context" "encoding/json" "os" "path/filepath" "sort" "strings" "time" ) const manifestFileName = "manifest.json" // Store 描述快照落盘与读取能力。 type Store interface { Read(ctx context.Context, root string) (*Snapshot, error) Write(ctx context.Context, root string, snap *Snapshot) error } // FileStore 提供基于目录树的快照读写实现。 type FileStore struct{} type manifest struct { Version int `json:"version"` CreatedAt string `json:"created_at"` } // NewFileStore 创建新的文件快照存储。 func NewFileStore() *FileStore { return &FileStore{} } // Read 从目录树读取快照。 func (s *FileStore) Read(ctx context.Context, root string) (*Snapshot, error) { _ = ctx info, err := os.Stat(root) if os.IsNotExist(err) { return New(), nil } if err != nil { return nil, err } if !info.IsDir() { return New(), nil } snap := New() if err := s.readManifest(root, snap); err != nil { return nil, err } entries, err := os.ReadDir(root) if err != nil { return nil, err } for _, entry := range entries { if !entry.IsDir() { continue } kind := entry.Name() records, err := s.readKind(filepath.Join(root, kind), kind) if err != nil { return nil, err } if len(records) == 0 { continue } sort.Slice(records, func(i int, j int) bool { return records[i].ID < records[j].ID }) snap.Resources[kind] = records } return snap, nil } // Write 将快照写入目录树。 func (s *FileStore) Write(ctx context.Context, root string, snap *Snapshot) error { _ = ctx if err := os.RemoveAll(root); err != nil { return err } if err := os.MkdirAll(root, 0755); err != nil { return err } if err := s.writeManifest(root, snap); err != nil { return err } for _, kind := range sortedKinds(snap.Resources) { kindDir := filepath.Join(root, kind) if err := os.MkdirAll(kindDir, 0755); err != nil { return err } records := append([]Record(nil), snap.Resources[kind]...) sort.Slice(records, func(i int, j int) bool { return records[i].ID < records[j].ID }) for _, record := range records { if len(record.Blobs) == 0 { if err := writeJSON(filepath.Join(kindDir, record.ID+".json"), record.Values); err != nil { return err } continue } recordDir := filepath.Join(kindDir, record.ID) if err := os.MkdirAll(recordDir, 0755); err != nil { return err } if err := writeJSON(filepath.Join(recordDir, "record.json"), record.Values); err != nil { return err } blobNames := make([]string, 0, len(record.Blobs)) for name := range record.Blobs { blobNames = append(blobNames, name) } sort.Strings(blobNames) for _, blobName := range blobNames { if err := os.WriteFile(filepath.Join(recordDir, blobName), record.Blobs[blobName], 0644); err != nil { return err } } } } return nil } // readManifest 读取快照 manifest。 func (s *FileStore) readManifest(root string, snap *Snapshot) error { data, err := os.ReadFile(filepath.Join(root, manifestFileName)) if os.IsNotExist(err) { return nil } if err != nil { return err } var current manifest if err := json.Unmarshal(data, ¤t); err != nil { return err } snap.Version = current.Version if current.CreatedAt != "" { createdAt, err := time.Parse(time.RFC3339, current.CreatedAt) if err != nil { return err } snap.CreatedAt = createdAt } return nil } // writeManifest 写入快照 manifest。 func (s *FileStore) writeManifest(root string, snap *Snapshot) error { payload := manifest{ Version: snap.Version, CreatedAt: snap.CreatedAt.Format(time.RFC3339), } return writeJSON(filepath.Join(root, manifestFileName), payload) } // readKind 读取单类资源目录。 func (s *FileStore) readKind(root string, kind string) ([]Record, error) { entries, err := os.ReadDir(root) if err != nil { return nil, err } records := make([]Record, 0, len(entries)) for _, entry := range entries { switch { case entry.IsDir(): record, err := s.readBlobRecord(filepath.Join(root, entry.Name()), kind) if err != nil { return nil, err } records = append(records, record) case strings.HasSuffix(entry.Name(), ".json"): record, err := s.readFlatRecord(filepath.Join(root, entry.Name()), kind) if err != nil { return nil, err } records = append(records, record) } } return records, nil } // readFlatRecord 读取单文件记录。 func (s *FileStore) readFlatRecord(path string, kind string) (Record, error) { values, err := readValues(path) if err != nil { return Record{}, err } id := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) return NewRecord(kind, id, values, nil) } // readBlobRecord 读取目录型记录。 func (s *FileStore) readBlobRecord(root string, kind string) (Record, error) { values, err := readValues(filepath.Join(root, "record.json")) if err != nil { return Record{}, err } entries, err := os.ReadDir(root) if err != nil { return Record{}, err } blobs := make(map[string][]byte) for _, entry := range entries { if entry.IsDir() || entry.Name() == "record.json" { continue } content, err := os.ReadFile(filepath.Join(root, entry.Name())) if err != nil { return Record{}, err } blobs[entry.Name()] = content } return NewRecord(kind, filepath.Base(root), values, blobs) } // readValues 读取 JSON 字段集合。 func readValues(path string) (map[string]interface{}, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } values := make(map[string]interface{}) if err := json.Unmarshal(data, &values); err != nil { return nil, err } return values, nil } // writeJSON 将结构体格式化写入 JSON 文件。 func writeJSON(path string, value interface{}) error { data, err := json.MarshalIndent(value, "", " ") if err != nil { return err } data = append(data, '\n') if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err } return os.WriteFile(path, data, 0644) }