[DEPRECATED] Go implementation of plcbundle
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}