249 lines
5.8 KiB
Go
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
|
|
}
|