A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory
at did-resolver 215 lines 6.2 kB view raw
1package bundle 2 3import ( 4 "fmt" 5 "path/filepath" 6 "time" 7 8 "tangled.org/atscan.net/plcbundle/plc" 9) 10 11const ( 12 // BUNDLE_SIZE is the standard number of operations per bundle 13 BUNDLE_SIZE = 10000 14) 15 16// Bundle represents a PLC bundle 17type Bundle struct { 18 BundleNumber int `json:"bundle_number"` 19 StartTime time.Time `json:"start_time"` 20 EndTime time.Time `json:"end_time"` 21 Operations []plc.PLCOperation `json:"-"` 22 DIDCount int `json:"did_count"` 23 24 Hash string `json:"hash"` // Chain hash (primary) 25 ContentHash string `json:"content_hash"` // Content hash 26 Parent string `json:"parent"` 27 28 CompressedHash string `json:"compressed_hash"` 29 CompressedSize int64 `json:"compressed_size"` 30 UncompressedSize int64 `json:"uncompressed_size"` 31 Cursor string `json:"cursor"` 32 BoundaryCIDs []string `json:"boundary_cids,omitempty"` 33 Compressed bool `json:"compressed"` 34 CreatedAt time.Time `json:"created_at"` 35} 36 37// GetFilePath returns the file path for this bundle 38func (b *Bundle) GetFilePath(bundleDir string) string { 39 return filepath.Join(bundleDir, fmt.Sprintf("%06d.jsonl.zst", b.BundleNumber)) 40} 41 42// GetUncompressedFilePath returns the uncompressed file path 43func (b *Bundle) GetUncompressedFilePath(bundleDir string) string { 44 return filepath.Join(bundleDir, fmt.Sprintf("%06d.jsonl", b.BundleNumber)) 45} 46 47// OperationCount returns the number of operations (always BUNDLE_SIZE for complete bundles) 48func (b *Bundle) OperationCount() int { 49 if len(b.Operations) > 0 { 50 return len(b.Operations) 51 } 52 return BUNDLE_SIZE 53} 54 55// CompressionRatio returns the compression ratio 56func (b *Bundle) CompressionRatio() float64 { 57 if b.CompressedSize == 0 { 58 return 0 59 } 60 return float64(b.UncompressedSize) / float64(b.CompressedSize) 61} 62 63// ValidateForSave performs validation before saving (no hash required) 64func (b *Bundle) ValidateForSave() error { 65 if b.BundleNumber < 1 { 66 return fmt.Errorf("invalid bundle number: %d", b.BundleNumber) 67 } 68 if len(b.Operations) != BUNDLE_SIZE { 69 return fmt.Errorf("invalid operation count: expected %d, got %d", BUNDLE_SIZE, len(b.Operations)) 70 } 71 if b.StartTime.After(b.EndTime) { 72 return fmt.Errorf("start_time is after end_time") 73 } 74 return nil 75} 76 77// Validate performs full validation (including hashes) 78func (b *Bundle) Validate() error { 79 if err := b.ValidateForSave(); err != nil { 80 return err 81 } 82 if b.Hash == "" { 83 return fmt.Errorf("missing hash") 84 } 85 if b.CompressedHash == "" { 86 return fmt.Errorf("missing compressed hash") 87 } 88 return nil 89} 90 91// BundleMetadata represents metadata about a bundle 92type BundleMetadata struct { 93 BundleNumber int `json:"bundle_number"` 94 StartTime time.Time `json:"start_time"` 95 EndTime time.Time `json:"end_time"` 96 OperationCount int `json:"operation_count"` 97 DIDCount int `json:"did_count"` 98 99 // Primary hash - cumulative chain hash (includes all history) 100 Hash string `json:"hash"` 101 102 // Content hash - SHA256 of bundle operations only 103 ContentHash string `json:"content_hash"` 104 105 // Parent chain hash - links to previous bundle 106 Parent string `json:"parent,omitempty"` 107 108 // File hashes and sizes 109 CompressedHash string `json:"compressed_hash"` 110 CompressedSize int64 `json:"compressed_size"` 111 UncompressedSize int64 `json:"uncompressed_size"` 112 Cursor string `json:"cursor"` 113 CreatedAt time.Time `json:"created_at"` 114} 115 116func (b *Bundle) ToMetadata() *BundleMetadata { 117 return &BundleMetadata{ 118 BundleNumber: b.BundleNumber, 119 StartTime: b.StartTime, 120 EndTime: b.EndTime, 121 OperationCount: b.OperationCount(), 122 DIDCount: b.DIDCount, 123 Hash: b.Hash, // Chain hash 124 ContentHash: b.ContentHash, // Content hash 125 Parent: b.Parent, 126 CompressedHash: b.CompressedHash, 127 CompressedSize: b.CompressedSize, 128 UncompressedSize: b.UncompressedSize, 129 Cursor: b.Cursor, 130 CreatedAt: b.CreatedAt, 131 } 132} 133 134// VerificationResult contains the result of bundle verification 135type VerificationResult struct { 136 BundleNumber int 137 Valid bool 138 HashMatch bool 139 FileExists bool 140 Error error 141 LocalHash string 142 ExpectedHash string 143} 144 145// ChainVerificationResult contains the result of chain verification 146type ChainVerificationResult struct { 147 Valid bool 148 ChainLength int 149 BrokenAt int 150 Error string 151 VerifiedBundles []int 152} 153 154// DirectoryScanResult contains results from scanning a directory 155type DirectoryScanResult struct { 156 BundleDir string 157 BundleCount int 158 FirstBundle int 159 LastBundle int 160 MissingGaps []int 161 TotalSize int64 // Compressed size 162 TotalUncompressed int64 // Uncompressed size (NEW) 163 IndexUpdated bool 164} 165 166// Logger interface for bundle operations 167type Logger interface { 168 Printf(format string, v ...interface{}) 169 Println(v ...interface{}) 170} 171 172// Config holds configuration for bundle operations 173type Config struct { 174 BundleDir string 175 VerifyOnLoad bool 176 AutoRebuild bool 177 RebuildWorkers int // Number of workers for parallel rebuild (0 = auto-detect) 178 RebuildProgress func(current, total int) // Progress callback for rebuild 179 Logger Logger 180} 181 182// DefaultConfig returns default configuration 183func DefaultConfig(bundleDir string) *Config { 184 return &Config{ 185 BundleDir: bundleDir, 186 VerifyOnLoad: true, 187 AutoRebuild: true, 188 RebuildWorkers: 0, // 0 means auto-detect CPU count 189 RebuildProgress: nil, // No progress callback by default 190 Logger: nil, 191 } 192} 193 194// CloneOptions configures cloning behavior 195type CloneOptions struct { 196 RemoteURL string 197 Workers int 198 SkipExisting bool 199 ProgressFunc func(downloaded, total int, bytesDownloaded, bytesTotal int64) 200 SaveInterval time.Duration 201 Verbose bool 202 Logger Logger 203} 204 205// CloneResult contains cloning results 206type CloneResult struct { 207 RemoteBundles int 208 Downloaded int 209 Failed int 210 Skipped int 211 TotalBytes int64 212 Duration time.Duration 213 Interrupted bool 214 FailedBundles []int 215}