[DEPRECATED] Go implementation of plcbundle
at main 350 lines 8.0 kB view raw
1package bundleindex 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// GetStats returns statistics about the index 173func (idx *Index) GetStats() map[string]interface{} { 174 idx.mu.RLock() 175 defer idx.mu.RUnlock() 176 177 if len(idx.Bundles) == 0 { 178 return map[string]interface{}{ 179 "bundle_count": 0, 180 "total_size": 0, 181 "total_uncompressed_size": 0, 182 } 183 } 184 185 first := idx.Bundles[0] 186 last := idx.Bundles[len(idx.Bundles)-1] 187 188 return map[string]interface{}{ 189 "bundle_count": len(idx.Bundles), 190 "first_bundle": first.BundleNumber, 191 "last_bundle": last.BundleNumber, 192 "total_size": idx.TotalSize, 193 "total_uncompressed_size": idx.TotalUncompressedSize, 194 "start_time": first.StartTime, 195 "end_time": last.EndTime, 196 "updated_at": idx.UpdatedAt, 197 "gaps": len(idx.FindGaps()), 198 } 199} 200 201// sort sorts bundles by bundle number 202func (idx *Index) sort() { 203 sort.Slice(idx.Bundles, func(i, j int) bool { 204 return idx.Bundles[i].BundleNumber < idx.Bundles[j].BundleNumber 205 }) 206} 207 208// recalculate recalculates derived fields (called after modifications) 209func (idx *Index) recalculate() { 210 if len(idx.Bundles) == 0 { 211 idx.LastBundle = 0 212 idx.TotalSize = 0 213 idx.TotalUncompressedSize = 0 214 return 215 } 216 217 // Find last bundle 218 maxBundle := 0 219 totalSize := int64(0) 220 totalUncompressed := int64(0) 221 222 for _, meta := range idx.Bundles { 223 if meta.BundleNumber > maxBundle { 224 maxBundle = meta.BundleNumber 225 } 226 totalSize += meta.CompressedSize 227 totalUncompressed += meta.UncompressedSize 228 } 229 230 idx.LastBundle = maxBundle 231 idx.TotalSize = totalSize 232 idx.TotalUncompressedSize = totalUncompressed 233} 234 235// Rebuild rebuilds the index from bundle metadata 236func (idx *Index) Rebuild(bundles []*BundleMetadata) { 237 idx.mu.Lock() 238 defer idx.mu.Unlock() 239 240 idx.Bundles = bundles 241 idx.sort() 242 idx.recalculate() 243 idx.UpdatedAt = time.Now().UTC() 244} 245 246// Clear clears all bundles from the index 247func (idx *Index) Clear() { 248 idx.mu.Lock() 249 defer idx.mu.Unlock() 250 251 idx.Bundles = make([]*BundleMetadata, 0) 252 idx.LastBundle = 0 253 idx.TotalSize = 0 254 idx.UpdatedAt = time.Now().UTC() 255} 256 257// FindGaps finds missing bundle numbers in the sequence 258func (idx *Index) FindGaps() []int { 259 idx.mu.RLock() 260 defer idx.mu.RUnlock() 261 262 if len(idx.Bundles) == 0 { 263 return nil 264 } 265 266 var gaps []int 267 first := idx.Bundles[0].BundleNumber 268 last := idx.Bundles[len(idx.Bundles)-1].BundleNumber 269 270 bundleMap := make(map[int]bool) 271 for _, meta := range idx.Bundles { 272 bundleMap[meta.BundleNumber] = true 273 } 274 275 for i := first; i <= last; i++ { 276 if !bundleMap[i] { 277 gaps = append(gaps, i) 278 } 279 } 280 281 return gaps 282} 283 284// Logger interface for bundleindex 285type Logger interface { 286 Printf(format string, v ...interface{}) 287} 288 289// UpdateFromRemote updates the index with metadata from remote bundles 290// fileExists is a callback to check if bundle file exists locally 291func (idx *Index) UpdateFromRemote( 292 bundleNumbers []int, 293 remoteMeta map[int]*BundleMetadata, 294 fileExists func(bundleNum int) bool, 295 verbose bool, 296 logger Logger, 297) error { 298 if len(bundleNumbers) == 0 { 299 return nil 300 } 301 302 idx.mu.Lock() 303 defer idx.mu.Unlock() 304 305 addedCount := 0 306 for _, num := range bundleNumbers { 307 meta, exists := remoteMeta[num] 308 if !exists { 309 continue 310 } 311 312 // Verify the file exists locally 313 if !fileExists(num) { 314 if logger != nil && verbose { 315 logger.Printf("Warning: bundle %06d not found locally, skipping", num) 316 } 317 continue 318 } 319 320 // Add to index (uses existing AddBundle logic but we're already locked) 321 // Check if bundle already exists 322 found := false 323 for i, existing := range idx.Bundles { 324 if existing.BundleNumber == num { 325 // Update existing 326 idx.Bundles[i] = meta 327 found = true 328 break 329 } 330 } 331 332 if !found { 333 // Add new bundle 334 idx.Bundles = append(idx.Bundles, meta) 335 } 336 337 addedCount++ 338 339 if logger != nil && verbose { 340 logger.Printf("Added bundle %06d to index", num) 341 } 342 } 343 344 if addedCount > 0 { 345 idx.sort() 346 idx.recalculate() 347 } 348 349 return nil 350}