A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory
at test-validate 269 lines 6.0 kB view raw
1package bundle 2 3import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "sort" 8 "sync" 9 "time" 10) 11 12const ( 13 // INDEX_FILE is the default index filename 14 INDEX_FILE = "plc_bundles.json" 15 16 // INDEX_VERSION is the current index format version 17 INDEX_VERSION = "1.0" 18) 19 20// Index represents the JSON index file 21type Index struct { 22 Version string `json:"version"` 23 LastBundle int `json:"last_bundle"` 24 UpdatedAt time.Time `json:"updated_at"` 25 TotalSize int64 `json:"total_size_bytes"` 26 Bundles []*BundleMetadata `json:"bundles"` 27 28 mu sync.RWMutex `json:"-"` 29} 30 31// NewIndex creates a new empty index 32func NewIndex() *Index { 33 return &Index{ 34 Version: INDEX_VERSION, 35 Bundles: make([]*BundleMetadata, 0), 36 UpdatedAt: time.Now().UTC(), 37 } 38} 39 40// LoadIndex loads an index from a file 41func LoadIndex(path string) (*Index, error) { 42 data, err := os.ReadFile(path) 43 if err != nil { 44 return nil, fmt.Errorf("failed to read index file: %w", err) 45 } 46 47 var idx Index 48 if err := json.Unmarshal(data, &idx); err != nil { 49 return nil, fmt.Errorf("failed to parse index file: %w", err) 50 } 51 52 // Validate version 53 if idx.Version != INDEX_VERSION { 54 return nil, fmt.Errorf("unsupported index version: %s (expected %s)", idx.Version, INDEX_VERSION) 55 } 56 57 return &idx, nil 58} 59 60// Save saves the index to a file 61func (idx *Index) Save(path string) error { 62 idx.mu.Lock() 63 defer idx.mu.Unlock() 64 65 idx.UpdatedAt = time.Now().UTC() 66 67 data, err := json.MarshalIndent(idx, "", " ") 68 if err != nil { 69 return fmt.Errorf("failed to marshal index: %w", err) 70 } 71 72 // Write atomically (write to temp file, then rename) 73 tempPath := path + ".tmp" 74 if err := os.WriteFile(tempPath, data, 0644); err != nil { 75 return fmt.Errorf("failed to write temp file: %w", err) 76 } 77 78 if err := os.Rename(tempPath, path); err != nil { 79 os.Remove(tempPath) // Clean up temp file 80 return fmt.Errorf("failed to rename temp file: %w", err) 81 } 82 83 return nil 84} 85 86// AddBundle adds a bundle to the index 87func (idx *Index) AddBundle(meta *BundleMetadata) { 88 idx.mu.Lock() 89 defer idx.mu.Unlock() 90 91 // Check if bundle already exists 92 for i, existing := range idx.Bundles { 93 if existing.BundleNumber == meta.BundleNumber { 94 // Update existing 95 idx.Bundles[i] = meta 96 idx.recalculate() 97 return 98 } 99 } 100 101 // Add new bundle 102 idx.Bundles = append(idx.Bundles, meta) 103 idx.sort() 104 idx.recalculate() 105} 106 107// GetBundle retrieves a bundle metadata by number 108func (idx *Index) GetBundle(bundleNumber int) (*BundleMetadata, error) { 109 idx.mu.RLock() 110 defer idx.mu.RUnlock() 111 112 for _, meta := range idx.Bundles { 113 if meta.BundleNumber == bundleNumber { 114 return meta, nil 115 } 116 } 117 118 return nil, fmt.Errorf("bundle %d not found in index", bundleNumber) 119} 120 121// GetLastBundle returns the metadata of the last bundle 122func (idx *Index) GetLastBundle() *BundleMetadata { 123 idx.mu.RLock() 124 defer idx.mu.RUnlock() 125 126 if len(idx.Bundles) == 0 { 127 return nil 128 } 129 130 return idx.Bundles[len(idx.Bundles)-1] 131} 132 133// GetBundles returns all bundle metadata 134func (idx *Index) GetBundles() []*BundleMetadata { 135 idx.mu.RLock() 136 defer idx.mu.RUnlock() 137 138 // Return a copy 139 result := make([]*BundleMetadata, len(idx.Bundles)) 140 copy(result, idx.Bundles) 141 return result 142} 143 144// GetBundleRange returns bundles in a specific range 145func (idx *Index) GetBundleRange(start, end int) []*BundleMetadata { 146 idx.mu.RLock() 147 defer idx.mu.RUnlock() 148 149 var result []*BundleMetadata 150 for _, meta := range idx.Bundles { 151 if meta.BundleNumber >= start && meta.BundleNumber <= end { 152 result = append(result, meta) 153 } 154 } 155 return result 156} 157 158// Count returns the number of bundles in the index 159func (idx *Index) Count() int { 160 idx.mu.RLock() 161 defer idx.mu.RUnlock() 162 return len(idx.Bundles) 163} 164 165// FindGaps finds missing bundle numbers in the sequence 166func (idx *Index) FindGaps() []int { 167 idx.mu.RLock() 168 defer idx.mu.RUnlock() 169 170 if len(idx.Bundles) == 0 { 171 return nil 172 } 173 174 var gaps []int 175 first := idx.Bundles[0].BundleNumber 176 last := idx.Bundles[len(idx.Bundles)-1].BundleNumber 177 178 bundleMap := make(map[int]bool) 179 for _, meta := range idx.Bundles { 180 bundleMap[meta.BundleNumber] = true 181 } 182 183 for i := first; i <= last; i++ { 184 if !bundleMap[i] { 185 gaps = append(gaps, i) 186 } 187 } 188 189 return gaps 190} 191 192// GetStats returns statistics about the index 193func (idx *Index) GetStats() map[string]interface{} { 194 idx.mu.RLock() 195 defer idx.mu.RUnlock() 196 197 if len(idx.Bundles) == 0 { 198 return map[string]interface{}{ 199 "bundle_count": 0, 200 "total_size": 0, 201 } 202 } 203 204 first := idx.Bundles[0] 205 last := idx.Bundles[len(idx.Bundles)-1] 206 207 return map[string]interface{}{ 208 "bundle_count": len(idx.Bundles), 209 "first_bundle": first.BundleNumber, 210 "last_bundle": last.BundleNumber, 211 "total_size": idx.TotalSize, 212 "start_time": first.StartTime, 213 "end_time": last.EndTime, 214 "updated_at": idx.UpdatedAt, 215 "gaps": len(idx.FindGaps()), 216 } 217} 218 219// sort sorts bundles by bundle number 220func (idx *Index) sort() { 221 sort.Slice(idx.Bundles, func(i, j int) bool { 222 return idx.Bundles[i].BundleNumber < idx.Bundles[j].BundleNumber 223 }) 224} 225 226// recalculate recalculates derived fields (called after modifications) 227func (idx *Index) recalculate() { 228 if len(idx.Bundles) == 0 { 229 idx.LastBundle = 0 230 idx.TotalSize = 0 231 return 232 } 233 234 // Find last bundle 235 maxBundle := 0 236 totalSize := int64(0) 237 238 for _, meta := range idx.Bundles { 239 if meta.BundleNumber > maxBundle { 240 maxBundle = meta.BundleNumber 241 } 242 totalSize += meta.CompressedSize 243 } 244 245 idx.LastBundle = maxBundle 246 idx.TotalSize = totalSize 247} 248 249// Rebuild rebuilds the index from bundle metadata 250func (idx *Index) Rebuild(bundles []*BundleMetadata) { 251 idx.mu.Lock() 252 defer idx.mu.Unlock() 253 254 idx.Bundles = bundles 255 idx.sort() 256 idx.recalculate() 257 idx.UpdatedAt = time.Now().UTC() 258} 259 260// Clear clears all bundles from the index 261func (idx *Index) Clear() { 262 idx.mu.Lock() 263 defer idx.mu.Unlock() 264 265 idx.Bundles = make([]*BundleMetadata, 0) 266 idx.LastBundle = 0 267 idx.TotalSize = 0 268 idx.UpdatedAt = time.Now().UTC() 269}