tangled
alpha
login
or
join now
atscan.net
/
plcbundle-go
1
fork
atom
[DEPRECATED] Go implementation of plcbundle
1
fork
atom
overview
issues
pulls
pipelines
cmd clean
tree.fail
4 months ago
4d881e0f
0c9db21b
+480
-5
3 changed files
expand all
collapse all
unified
split
cmd
plcbundle
commands
clean.go
op.go
main.go
+474
cmd/plcbundle/commands/clean.go
···
1
1
+
package commands
2
2
+
3
3
+
import (
4
4
+
"fmt"
5
5
+
"os"
6
6
+
"path/filepath"
7
7
+
"sort"
8
8
+
"strings"
9
9
+
"time"
10
10
+
11
11
+
"github.com/spf13/cobra"
12
12
+
)
13
13
+
14
14
+
func NewCleanCommand() *cobra.Command {
15
15
+
var (
16
16
+
dryRun bool
17
17
+
force bool
18
18
+
aggressive bool
19
19
+
keepTemp bool
20
20
+
keepOldMempool bool
21
21
+
olderThanDays int
22
22
+
)
23
23
+
24
24
+
cmd := &cobra.Command{
25
25
+
Use: "clean [flags]",
26
26
+
Aliases: []string{"cleanup", "gc"},
27
27
+
Short: "Clean up temporary and orphaned files",
28
28
+
Long: `Clean up temporary and orphaned files from repository
29
29
+
30
30
+
Removes:
31
31
+
• Temporary files (.tmp, .tmp.*)
32
32
+
• Old mempool files (keeps current)
33
33
+
• Orphaned bundle files (not in index)
34
34
+
• DID index cache (if --aggressive)
35
35
+
36
36
+
By default, performs a safe clean. Use --aggressive for deeper cleaning.
37
37
+
Always use --dry-run first to preview what will be deleted.`,
38
38
+
39
39
+
Example: ` # Preview what would be cleaned (RECOMMENDED FIRST)
40
40
+
plcbundle clean --dry-run
41
41
+
42
42
+
# Safe clean (remove temp files and old mempools)
43
43
+
plcbundle clean
44
44
+
45
45
+
# Aggressive clean (also remove orphaned bundles and caches)
46
46
+
plcbundle clean --aggressive
47
47
+
48
48
+
# Clean but keep temporary download files
49
49
+
plcbundle clean --keep-temp
50
50
+
51
51
+
# Force clean without confirmation
52
52
+
plcbundle clean --force
53
53
+
54
54
+
# Clean files older than 7 days only
55
55
+
plcbundle clean --older-than 7
56
56
+
57
57
+
# Full cleanup with preview
58
58
+
plcbundle clean --aggressive --dry-run`,
59
59
+
60
60
+
RunE: func(cmd *cobra.Command, args []string) error {
61
61
+
verbose, _ := cmd.Root().PersistentFlags().GetBool("verbose")
62
62
+
63
63
+
mgr, dir, err := getManager(&ManagerOptions{Cmd: cmd})
64
64
+
if err != nil {
65
65
+
return err
66
66
+
}
67
67
+
defer mgr.Close()
68
68
+
69
69
+
return runClean(mgr, dir, cleanOptions{
70
70
+
dryRun: dryRun,
71
71
+
force: force,
72
72
+
aggressive: aggressive,
73
73
+
keepTemp: keepTemp,
74
74
+
keepOldMempool: keepOldMempool,
75
75
+
olderThanDays: olderThanDays,
76
76
+
verbose: verbose,
77
77
+
})
78
78
+
},
79
79
+
}
80
80
+
81
81
+
// Flags
82
82
+
cmd.Flags().BoolVarP(&dryRun, "dry-run", "n", false, "Show what would be deleted without deleting")
83
83
+
cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation prompt")
84
84
+
cmd.Flags().BoolVar(&aggressive, "aggressive", false, "Also remove orphaned bundles and caches")
85
85
+
cmd.Flags().BoolVar(&keepTemp, "keep-temp", false, "Don't delete temporary files")
86
86
+
cmd.Flags().BoolVar(&keepOldMempool, "keep-old-mempool", false, "Don't delete old mempool files")
87
87
+
cmd.Flags().IntVar(&olderThanDays, "older-than", 0, "Only clean files older than N days (0 = all)")
88
88
+
89
89
+
return cmd
90
90
+
}
91
91
+
92
92
+
type cleanOptions struct {
93
93
+
dryRun bool
94
94
+
force bool
95
95
+
aggressive bool
96
96
+
keepTemp bool
97
97
+
keepOldMempool bool
98
98
+
olderThanDays int
99
99
+
verbose bool
100
100
+
}
101
101
+
102
102
+
type cleanPlan struct {
103
103
+
tempFiles []fileInfo
104
104
+
oldMempoolFiles []fileInfo
105
105
+
orphanedBundles []fileInfo
106
106
+
didIndexCache bool
107
107
+
totalSize int64
108
108
+
totalFiles int
109
109
+
}
110
110
+
111
111
+
type fileInfo struct {
112
112
+
path string
113
113
+
size int64
114
114
+
modTime time.Time
115
115
+
reason string
116
116
+
}
117
117
+
118
118
+
func runClean(mgr BundleManager, dir string, opts cleanOptions) error {
119
119
+
fmt.Printf("Analyzing repository: %s\n\n", dir)
120
120
+
121
121
+
// Calculate age cutoff
122
122
+
var cutoffTime time.Time
123
123
+
if opts.olderThanDays > 0 {
124
124
+
cutoffTime = time.Now().AddDate(0, 0, -opts.olderThanDays)
125
125
+
fmt.Printf("Only cleaning files older than %d days (%s)\n\n",
126
126
+
opts.olderThanDays, cutoffTime.Format("2006-01-02"))
127
127
+
}
128
128
+
129
129
+
// Build clean plan
130
130
+
plan, err := buildCleanPlan(mgr, dir, opts, cutoffTime)
131
131
+
if err != nil {
132
132
+
return fmt.Errorf("failed to build clean plan: %w", err)
133
133
+
}
134
134
+
135
135
+
// Display plan
136
136
+
displayCleanPlan(plan, opts)
137
137
+
138
138
+
// If dry-run, stop here
139
139
+
if opts.dryRun {
140
140
+
fmt.Printf("\n💡 This was a dry-run. No files were deleted.\n")
141
141
+
fmt.Printf(" Run without --dry-run to perform cleanup.\n")
142
142
+
return nil
143
143
+
}
144
144
+
145
145
+
// If nothing to clean
146
146
+
if plan.totalFiles == 0 {
147
147
+
fmt.Printf("✓ Repository is clean (nothing to remove)\n")
148
148
+
return nil
149
149
+
}
150
150
+
151
151
+
// Confirm unless forced
152
152
+
if !opts.force {
153
153
+
if !confirmClean(plan) {
154
154
+
fmt.Println("Cancelled")
155
155
+
return nil
156
156
+
}
157
157
+
fmt.Println()
158
158
+
}
159
159
+
160
160
+
// Execute cleanup
161
161
+
return executeClean(plan, opts)
162
162
+
}
163
163
+
164
164
+
func buildCleanPlan(mgr BundleManager, dir string, opts cleanOptions, cutoffTime time.Time) (*cleanPlan, error) {
165
165
+
plan := &cleanPlan{
166
166
+
tempFiles: make([]fileInfo, 0),
167
167
+
oldMempoolFiles: make([]fileInfo, 0),
168
168
+
orphanedBundles: make([]fileInfo, 0),
169
169
+
}
170
170
+
171
171
+
// Get current mempool info
172
172
+
mempoolStats := mgr.GetMempoolStats()
173
173
+
currentMempoolBundle := mempoolStats["target_bundle"].(int)
174
174
+
currentMempoolFile := fmt.Sprintf("plc_mempool_%06d.jsonl", currentMempoolBundle)
175
175
+
176
176
+
// Scan directory
177
177
+
entries, err := os.ReadDir(dir)
178
178
+
if err != nil {
179
179
+
return nil, err
180
180
+
}
181
181
+
182
182
+
// Get index for orphan detection
183
183
+
var indexedBundles map[int]bool
184
184
+
if opts.aggressive {
185
185
+
indexedBundles = make(map[int]bool)
186
186
+
index := mgr.GetIndex()
187
187
+
for _, meta := range index.GetBundles() {
188
188
+
indexedBundles[meta.BundleNumber] = true
189
189
+
}
190
190
+
}
191
191
+
192
192
+
for _, entry := range entries {
193
193
+
if entry.IsDir() {
194
194
+
continue
195
195
+
}
196
196
+
197
197
+
name := entry.Name()
198
198
+
fullPath := filepath.Join(dir, name)
199
199
+
info, err := entry.Info()
200
200
+
if err != nil {
201
201
+
continue
202
202
+
}
203
203
+
204
204
+
// Skip if newer than cutoff
205
205
+
if !cutoffTime.IsZero() && info.ModTime().After(cutoffTime) {
206
206
+
continue
207
207
+
}
208
208
+
209
209
+
// 1. Temporary files
210
210
+
if !opts.keepTemp && isTempFile(name) {
211
211
+
plan.tempFiles = append(plan.tempFiles, fileInfo{
212
212
+
path: fullPath,
213
213
+
size: info.Size(),
214
214
+
modTime: info.ModTime(),
215
215
+
reason: "temporary file",
216
216
+
})
217
217
+
plan.totalSize += info.Size()
218
218
+
plan.totalFiles++
219
219
+
continue
220
220
+
}
221
221
+
222
222
+
// 2. Old mempool files
223
223
+
if !opts.keepOldMempool && isMempoolFile(name) && name != currentMempoolFile {
224
224
+
plan.oldMempoolFiles = append(plan.oldMempoolFiles, fileInfo{
225
225
+
path: fullPath,
226
226
+
size: info.Size(),
227
227
+
modTime: info.ModTime(),
228
228
+
reason: "old mempool file",
229
229
+
})
230
230
+
plan.totalSize += info.Size()
231
231
+
plan.totalFiles++
232
232
+
continue
233
233
+
}
234
234
+
235
235
+
// 3. Orphaned bundles (aggressive mode only)
236
236
+
if opts.aggressive && isBundleFile(name) {
237
237
+
bundleNum := extractBundleNumber(name)
238
238
+
if bundleNum > 0 && !indexedBundles[bundleNum] {
239
239
+
plan.orphanedBundles = append(plan.orphanedBundles, fileInfo{
240
240
+
path: fullPath,
241
241
+
size: info.Size(),
242
242
+
modTime: info.ModTime(),
243
243
+
reason: "not in index",
244
244
+
})
245
245
+
plan.totalSize += info.Size()
246
246
+
plan.totalFiles++
247
247
+
}
248
248
+
}
249
249
+
}
250
250
+
251
251
+
// 4. DID index cache (aggressive mode only)
252
252
+
if opts.aggressive {
253
253
+
didIndexDir := filepath.Join(dir, ".plcbundle")
254
254
+
if stat, err := os.Stat(didIndexDir); err == nil && stat.IsDir() {
255
255
+
plan.didIndexCache = true
256
256
+
}
257
257
+
}
258
258
+
259
259
+
return plan, nil
260
260
+
}
261
261
+
262
262
+
func displayCleanPlan(plan *cleanPlan, opts cleanOptions) {
263
263
+
if opts.dryRun {
264
264
+
fmt.Printf("╔════════════════════════════════════════════════════════════════╗\n")
265
265
+
fmt.Printf("║ DRY-RUN MODE ║\n")
266
266
+
fmt.Printf("║ (showing what would be deleted) ║\n")
267
267
+
fmt.Printf("╚════════════════════════════════════════════════════════════════╝\n\n")
268
268
+
} else {
269
269
+
fmt.Printf("Clean Plan\n")
270
270
+
fmt.Printf("══════════\n\n")
271
271
+
}
272
272
+
273
273
+
// Display by category
274
274
+
if len(plan.tempFiles) > 0 {
275
275
+
displayFileCategory("Temporary Files", plan.tempFiles, opts.verbose)
276
276
+
}
277
277
+
278
278
+
if len(plan.oldMempoolFiles) > 0 {
279
279
+
displayFileCategory("Old Mempool Files", plan.oldMempoolFiles, opts.verbose)
280
280
+
}
281
281
+
282
282
+
if len(plan.orphanedBundles) > 0 {
283
283
+
displayFileCategory("Orphaned Bundles", plan.orphanedBundles, opts.verbose)
284
284
+
}
285
285
+
286
286
+
if plan.didIndexCache {
287
287
+
fmt.Printf("📁 DID Index Cache\n")
288
288
+
fmt.Printf(" .plcbundle/ directory (will be preserved but shards unloaded)\n\n")
289
289
+
}
290
290
+
291
291
+
// Summary
292
292
+
if plan.totalFiles > 0 {
293
293
+
fmt.Printf("Summary\n")
294
294
+
fmt.Printf("───────\n")
295
295
+
fmt.Printf(" Files to delete: %s\n", formatNumber(plan.totalFiles))
296
296
+
fmt.Printf(" Space to free: %s\n", formatBytes(plan.totalSize))
297
297
+
fmt.Printf("\n")
298
298
+
} else {
299
299
+
fmt.Printf("✓ No files to clean\n\n")
300
300
+
}
301
301
+
}
302
302
+
303
303
+
func displayFileCategory(title string, files []fileInfo, verbose bool) {
304
304
+
if len(files) == 0 {
305
305
+
return
306
306
+
}
307
307
+
308
308
+
totalSize := int64(0)
309
309
+
for _, f := range files {
310
310
+
totalSize += f.size
311
311
+
}
312
312
+
313
313
+
fmt.Printf("🗑️ %s (%d files, %s)\n", title, len(files), formatBytes(totalSize))
314
314
+
315
315
+
if verbose {
316
316
+
// Sort by size (largest first)
317
317
+
sorted := make([]fileInfo, len(files))
318
318
+
copy(sorted, files)
319
319
+
sort.Slice(sorted, func(i, j int) bool {
320
320
+
return sorted[i].size > sorted[j].size
321
321
+
})
322
322
+
323
323
+
displayCount := len(sorted)
324
324
+
if displayCount > 20 {
325
325
+
displayCount = 20
326
326
+
}
327
327
+
328
328
+
for i := 0; i < displayCount; i++ {
329
329
+
f := sorted[i]
330
330
+
age := time.Since(f.modTime)
331
331
+
fmt.Printf(" • %-40s %10s %s ago\n",
332
332
+
filepath.Base(f.path),
333
333
+
formatBytes(f.size),
334
334
+
formatDurationShort(age))
335
335
+
}
336
336
+
337
337
+
if len(sorted) > displayCount {
338
338
+
fmt.Printf(" ... and %d more\n", len(sorted)-displayCount)
339
339
+
}
340
340
+
} else {
341
341
+
// Compact summary
342
342
+
fmt.Printf(" %d file(s) • %s\n", len(files), formatBytes(totalSize))
343
343
+
}
344
344
+
fmt.Printf("\n")
345
345
+
}
346
346
+
347
347
+
func confirmClean(plan *cleanPlan) bool {
348
348
+
fmt.Printf("⚠️ This will delete %d file(s) (%s)\n",
349
349
+
plan.totalFiles, formatBytes(plan.totalSize))
350
350
+
fmt.Printf("Type 'clean' to confirm: ")
351
351
+
352
352
+
var response string
353
353
+
fmt.Scanln(&response)
354
354
+
355
355
+
return strings.TrimSpace(response) == "clean"
356
356
+
}
357
357
+
358
358
+
func executeClean(plan *cleanPlan, opts cleanOptions) error {
359
359
+
deleted := 0
360
360
+
var totalFreed int64
361
361
+
failed := 0
362
362
+
var firstError error
363
363
+
364
364
+
// Delete temp files
365
365
+
for _, f := range plan.tempFiles {
366
366
+
if opts.verbose {
367
367
+
fmt.Printf(" Deleting: %s\n", filepath.Base(f.path))
368
368
+
}
369
369
+
370
370
+
if err := os.Remove(f.path); err != nil {
371
371
+
failed++
372
372
+
if firstError == nil {
373
373
+
firstError = err
374
374
+
}
375
375
+
if opts.verbose {
376
376
+
fmt.Printf(" ✗ Failed: %v\n", err)
377
377
+
}
378
378
+
} else {
379
379
+
deleted++
380
380
+
totalFreed += f.size
381
381
+
}
382
382
+
}
383
383
+
384
384
+
// Delete old mempool files
385
385
+
for _, f := range plan.oldMempoolFiles {
386
386
+
if opts.verbose {
387
387
+
fmt.Printf(" Deleting: %s\n", filepath.Base(f.path))
388
388
+
}
389
389
+
390
390
+
if err := os.Remove(f.path); err != nil {
391
391
+
failed++
392
392
+
if firstError == nil {
393
393
+
firstError = err
394
394
+
}
395
395
+
if opts.verbose {
396
396
+
fmt.Printf(" ✗ Failed: %v\n", err)
397
397
+
}
398
398
+
} else {
399
399
+
deleted++
400
400
+
totalFreed += f.size
401
401
+
}
402
402
+
}
403
403
+
404
404
+
// Delete orphaned bundles (aggressive)
405
405
+
if len(plan.orphanedBundles) > 0 {
406
406
+
fmt.Printf("\nRemoving orphaned bundles...\n")
407
407
+
for _, f := range plan.orphanedBundles {
408
408
+
if opts.verbose {
409
409
+
fmt.Printf(" Deleting: %s\n", filepath.Base(f.path))
410
410
+
}
411
411
+
412
412
+
if err := os.Remove(f.path); err != nil {
413
413
+
failed++
414
414
+
if firstError == nil {
415
415
+
firstError = err
416
416
+
}
417
417
+
if opts.verbose {
418
418
+
fmt.Printf(" ✗ Failed: %v\n", err)
419
419
+
}
420
420
+
} else {
421
421
+
deleted++
422
422
+
totalFreed += f.size
423
423
+
}
424
424
+
}
425
425
+
}
426
426
+
427
427
+
// Summary
428
428
+
fmt.Printf("\n")
429
429
+
if failed > 0 {
430
430
+
fmt.Printf("⚠️ Cleanup completed with errors\n")
431
431
+
fmt.Printf(" Deleted: %d files\n", deleted)
432
432
+
fmt.Printf(" Failed: %d files\n", failed)
433
433
+
fmt.Printf(" Space freed: %s\n", formatBytes(totalFreed))
434
434
+
if firstError != nil {
435
435
+
fmt.Printf(" First error: %v\n", firstError)
436
436
+
}
437
437
+
return fmt.Errorf("cleanup completed with %d errors", failed)
438
438
+
}
439
439
+
440
440
+
fmt.Printf("✓ Cleanup complete\n")
441
441
+
fmt.Printf(" Deleted: %d files\n", deleted)
442
442
+
fmt.Printf(" Space freed: %s\n", formatBytes(totalFreed))
443
443
+
444
444
+
return nil
445
445
+
}
446
446
+
447
447
+
// Helper functions
448
448
+
449
449
+
func isTempFile(name string) bool {
450
450
+
return strings.HasSuffix(name, ".tmp") ||
451
451
+
strings.Contains(name, ".tmp.") ||
452
452
+
strings.HasPrefix(name, "._") ||
453
453
+
strings.HasPrefix(name, ".~")
454
454
+
}
455
455
+
456
456
+
func isMempoolFile(name string) bool {
457
457
+
return strings.HasPrefix(name, "plc_mempool_") &&
458
458
+
strings.HasSuffix(name, ".jsonl")
459
459
+
}
460
460
+
461
461
+
func isBundleFile(name string) bool {
462
462
+
return strings.HasSuffix(name, ".jsonl.zst") &&
463
463
+
len(name) == len("000000.jsonl.zst")
464
464
+
}
465
465
+
466
466
+
func extractBundleNumber(name string) int {
467
467
+
if !strings.HasSuffix(name, ".jsonl.zst") {
468
468
+
return 0
469
469
+
}
470
470
+
numStr := strings.TrimSuffix(name, ".jsonl.zst")
471
471
+
var num int
472
472
+
fmt.Sscanf(numStr, "%d", &num)
473
473
+
return num
474
474
+
}
+1
-1
cmd/plcbundle/commands/op.go
···
17
17
func NewOpCommand() *cobra.Command {
18
18
cmd := &cobra.Command{
19
19
Use: "op",
20
20
-
Aliases: []string{"operation"},
20
20
+
Aliases: []string{"operation", "record"},
21
21
Short: "Operation queries and inspection",
22
22
Long: `Operation queries and inspection
23
23
+5
-4
cmd/plcbundle/main.go
···
67
67
68
68
// Monitoring & maintenance
69
69
/*cmd.AddCommand(commands.NewWatchCommand())
70
70
-
cmd.AddCommand(commands.NewHealCommand())
71
71
-
cmd.AddCommand(commands.NewCleanCommand())*/
70
70
+
cmd.AddCommand(commands.NewHealCommand())*/
71
71
+
cmd.AddCommand(commands.NewCleanCommand())
72
72
73
73
// Server
74
74
cmd.AddCommand(commands.NewServerCommand())
···
148
148
server Start HTTP server
149
149
150
150
Command Groups:
151
151
-
Bundle: clone, sync, pull, export, get, rollback
151
151
+
Bundle: clone, sync, export, get, rollback
152
152
Status: status, log, gaps, verify, diff, stats, inspect
153
153
-
DID: did <lookup|resolve|history|batch|search|stats>
153
153
+
Ops: op <get|show|find>
154
154
+
DID: did <resolve|lookup|history|batch|stats>
154
155
Index: index <build|repair|stats|verify>
155
156
Tools: watch, heal, clean, mempool, detector
156
157