Files
voidraft/internal/syncer/snapshot/snapshot.go
2026-03-30 00:03:23 +08:00

249 lines
5.8 KiB
Go

package snapshot
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"maps"
"sort"
"strings"
"time"
)
const (
// CurrentVersion 是当前快照格式版本。
CurrentVersion = 1
)
// Snapshot 描述一次完整的数据快照。
type Snapshot struct {
Version int
CreatedAt time.Time
Resources map[string][]Record
}
// Record 描述单条资源记录。
type Record struct {
Kind string
ID string
UpdatedAt time.Time
DeletedAt *time.Time
Values map[string]interface{}
Blobs map[string][]byte
}
// Snapshotter 描述快照导出与应用接口。
type Snapshotter interface {
Export(ctx context.Context) (*Snapshot, error)
Apply(ctx context.Context, snap *Snapshot) error
}
// New 创建新的空快照。
func New() *Snapshot {
return &Snapshot{
Version: CurrentVersion,
CreatedAt: time.Now(),
Resources: make(map[string][]Record),
}
}
// NewRecord 根据业务字段构造规范化记录。
func NewRecord(kind string, id string, values map[string]interface{}, blobs map[string][]byte) (Record, error) {
if strings.TrimSpace(kind) == "" {
return Record{}, errors.New("record kind is required")
}
normalizedValues := cloneValues(values)
if id == "" {
uuid, _ := normalizedValues["uuid"].(string)
id = uuid
}
if id == "" {
return Record{}, errors.New("record id is required")
}
normalizedValues["uuid"] = id
updatedAt, err := parseRequiredTime(normalizedValues["updated_at"])
if err != nil {
return Record{}, fmt.Errorf("record %s updated_at: %w", id, err)
}
deletedAt, err := parseOptionalTime(normalizedValues["deleted_at"])
if err != nil {
return Record{}, fmt.Errorf("record %s deleted_at: %w", id, err)
}
return Record{
Kind: kind,
ID: id,
UpdatedAt: updatedAt,
DeletedAt: deletedAt,
Values: normalizedValues,
Blobs: cloneBlobs(blobs),
}, nil
}
// Clone 返回快照的深拷贝。
func Clone(snap *Snapshot) *Snapshot {
if snap == nil {
return New()
}
cloned := &Snapshot{
Version: snap.Version,
CreatedAt: snap.CreatedAt,
Resources: make(map[string][]Record, len(snap.Resources)),
}
for kind, records := range snap.Resources {
copied := make([]Record, 0, len(records))
for _, record := range records {
copied = append(copied, CloneRecord(record))
}
cloned.Resources[kind] = copied
}
return cloned
}
// CloneRecord 返回记录的深拷贝。
func CloneRecord(record Record) Record {
return Record{
Kind: record.Kind,
ID: record.ID,
UpdatedAt: record.UpdatedAt,
DeletedAt: cloneTime(record.DeletedAt),
Values: cloneValues(record.Values),
Blobs: cloneBlobs(record.Blobs),
}
}
// Digest 计算快照的稳定摘要。
func Digest(snap *Snapshot) (string, error) {
normalized := Clone(snap)
type digestRecord struct {
ID string `json:"id"`
UpdatedAt string `json:"updated_at"`
DeletedAt *string `json:"deleted_at,omitempty"`
Values map[string]interface{} `json:"values"`
Blobs map[string][]byte `json:"blobs,omitempty"`
}
payload := struct {
Version int `json:"version"`
Resources map[string][]digestRecord `json:"resources"`
}{
Version: normalized.Version,
Resources: make(map[string][]digestRecord, len(normalized.Resources)),
}
for _, kind := range sortedKinds(normalized.Resources) {
records := normalized.Resources[kind]
sort.Slice(records, func(i int, j int) bool {
return records[i].ID < records[j].ID
})
items := make([]digestRecord, 0, len(records))
for _, record := range records {
var deletedAt *string
if record.DeletedAt != nil {
value := record.DeletedAt.Format(time.RFC3339)
deletedAt = &value
}
items = append(items, digestRecord{
ID: record.ID,
UpdatedAt: record.UpdatedAt.Format(time.RFC3339),
DeletedAt: deletedAt,
Values: record.Values,
Blobs: record.Blobs,
})
}
payload.Resources[kind] = items
}
data, err := json.Marshal(payload)
if err != nil {
return "", err
}
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:]), nil
}
// RecordDigest 计算单条记录的稳定摘要。
func RecordDigest(record Record) string {
sum, err := Digest(&Snapshot{
Version: CurrentVersion,
Resources: map[string][]Record{
record.Kind: {CloneRecord(record)},
},
})
if err != nil {
return ""
}
return sum
}
// cloneValues 复制字段 map。
func cloneValues(values map[string]interface{}) map[string]interface{} {
if values == nil {
return map[string]interface{}{}
}
return maps.Clone(values)
}
// cloneBlobs 复制二进制 blob 集合。
func cloneBlobs(blobs map[string][]byte) map[string][]byte {
if len(blobs) == 0 {
return nil
}
copied := make(map[string][]byte, len(blobs))
for name, blob := range blobs {
copied[name] = append([]byte(nil), blob...)
}
return copied
}
// cloneTime 复制时间指针。
func cloneTime(value *time.Time) *time.Time {
if value == nil {
return nil
}
cloned := *value
return &cloned
}
// parseRequiredTime 解析必填时间字段。
func parseRequiredTime(value interface{}) (time.Time, error) {
text, _ := value.(string)
if text == "" {
return time.Time{}, errors.New("time value is required")
}
return time.Parse(time.RFC3339, text)
}
// parseOptionalTime 解析可选时间字段。
func parseOptionalTime(value interface{}) (*time.Time, error) {
text, _ := value.(string)
if text == "" {
return nil, nil
}
parsed, err := time.Parse(time.RFC3339, text)
if err != nil {
return nil, err
}
return &parsed, nil
}
// sortedKinds 返回稳定排序后的资源类型列表。
func sortedKinds(resources map[string][]Record) []string {
kinds := make([]string, 0, len(resources))
for kind := range resources {
kinds = append(kinds, kind)
}
sort.Strings(kinds)
return kinds
}