[DEPRECATED] Go implementation of plcbundle

cmd clean

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