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