@@ -323,22 +323,23 @@ func (opt *purgeOptions) executePurgeWorkflow(setupOpt restic.SetupOptions, cuto
323
323
return fmt .Errorf ("failed to create restic wrapper: %v" , err )
324
324
}
325
325
326
- repoList , err := opt .findRepositoriesToPurge (rw , cutoffTime )
326
+ // Get repository base URL for display purposes
327
+ repoBase , err := opt .getResticRepoFromEnv (rw )
327
328
if err != nil {
328
- return err
329
+ return fmt .Errorf ("failed to get restic repository base: %w" , err )
330
+ }
331
+
332
+ fmt .Println ("\n 🔎 Searching for repositories. This may take a while depending on the number of repositories..." )
333
+ repoList , err := opt .findRepositoriesToPurge (rw , repoBase , cutoffTime )
334
+ if err != nil {
335
+ displayRepositoryErrors (err )
329
336
}
330
337
331
338
if len (repoList ) == 0 {
332
339
opt .displayNoRepositoriesMessage ()
333
340
return nil
334
341
}
335
342
336
- // Get repository base URL for display purposes
337
- repoBase , err := opt .getResticRepoFromEnv (rw )
338
- if err != nil {
339
- return fmt .Errorf ("failed to get restic repository base: %w" , err )
340
- }
341
-
342
343
opt .displayRepositoriesTable (repoList , repoBase )
343
344
if opt .dryRun {
344
345
displayDryRunMessage (len (repoList ))
@@ -352,29 +353,21 @@ func (opt *purgeOptions) executePurgeWorkflow(setupOpt restic.SetupOptions, cuto
352
353
return opt .deleteRepositories (rw , repoList )
353
354
}
354
355
355
- func (opt * purgeOptions ) findRepositoriesToPurge (rw * restic.ResticWrapper , cutoffTime time.Time ) ([]repositoryInfo , error ) {
356
+ func (opt * purgeOptions ) findRepositoriesToPurge (rw * restic.ResticWrapper , repoBase string , cutoffTime time.Time ) ([]repositoryInfo , error ) {
356
357
var repos []repositoryInfo
357
358
subDirs , err := opt .listSubdirectories ("" )
358
359
if err != nil {
359
360
return nil , fmt .Errorf ("cannot list sub-dirs: %w" , err )
360
361
}
361
362
362
- repoBase , err := opt .getResticRepoFromEnv (rw )
363
- if err != nil {
364
- return nil , err
365
- }
366
-
367
363
script := opt .generateRepoListScript (repoBase , rw , subDirs )
368
364
out , err := runResticScriptViaDocker (script )
369
365
if err != nil {
370
366
return nil , fmt .Errorf ("Error running repo check script: %v\n Output:\n %s" , err , out )
371
367
}
372
368
373
- err = extractRepoListFromOutput (out , subDirs , cutoffTime , & repos )
374
- if err != nil {
375
- return nil , err
376
- }
377
- return repos , nil
369
+ err = extractRepoListFromOutput (out , repoBase , subDirs , cutoffTime , & repos )
370
+ return repos , err
378
371
}
379
372
380
373
func (opt * purgeOptions ) listSubdirectories (path string ) ([]string , error ) {
@@ -434,61 +427,125 @@ func runResticScriptViaDocker(script string) (string, error) {
434
427
return string (out ), err
435
428
}
436
429
437
- func extractRepoListFromOutput (out string , subDirs []string , cutoffTime time.Time , repos * []repositoryInfo ) error {
438
- type snapshot struct {
439
- Time string `json:"time"`
440
- }
441
- dirIndex := 0
442
- var errs []error
443
- var snapshots []snapshot
430
+ func extractRepoListFromOutput (out string , repoBase string , subDirs []string , cutoffTime time.Time , repos * []repositoryInfo ) error {
431
+ var (
432
+ dirIndex int
433
+ errs []error
434
+ )
435
+
444
436
lines := strings .Split (out , "\n " )
445
437
for _ , line := range lines {
446
438
line = strings .TrimSpace (line )
447
- // Skip error messages and separators
448
- if strings .HasPrefix (line , "Failed to access repository" ) ||
449
- strings .Contains (line , "Fatal: repository does not exist" ) {
450
- // If we hit an error, we should still increment dirIndex to stay in sync
451
- if dirIndex < len (subDirs ) {
452
- dirIndex ++
453
- }
439
+ if line == "" {
454
440
continue
455
441
}
456
-
457
- // Parse JSON array
458
- if strings .HasPrefix (line , "[" ) {
459
- if err := json .Unmarshal ([]byte (line ), & snapshots ); err != nil {
460
- errs = append (errs , fmt .Errorf ("failed to parse JSON for %s: %v" , line , err ))
461
- if dirIndex < len (subDirs ) {
462
- dirIndex ++
463
- }
442
+ switch {
443
+ case strings .HasPrefix (line , "[" ):
444
+ if err := processSnapshotLine (line , subDirs , & dirIndex , cutoffTime , repos , & errs ); err != nil {
464
445
continue
465
446
}
466
-
467
- if len (snapshots ) > 0 {
468
- snapshotTime , err := time .Parse (time .RFC3339Nano , snapshots [0 ].Time )
469
- if err != nil {
470
- errs = append (errs , fmt .Errorf ("failed to parse time for %s: %v" , line , err ))
471
- if dirIndex < len (subDirs ) {
472
- dirIndex ++
473
- }
474
- continue
475
- }
476
- if dirIndex < len (subDirs ) && snapshotTime .Before (cutoffTime ) {
477
- * repos = append (* repos , repositoryInfo {
478
- Path : subDirs [dirIndex ],
479
- LastModified : snapshotTime ,
480
- })
481
- }
447
+ case strings .HasPrefix (line , "{" ):
448
+ processErrorJSONLine (line , repoBase , subDirs , dirIndex , & errs )
449
+ case strings .HasPrefix (line , "Failed to access repository" ) ||
450
+ strings .Contains (line , "Fatal: repository does not exist" ):
451
+ // Handle plain text error lines
452
+ if dirIndex < len (subDirs ) {
453
+ dirIndex ++
482
454
}
483
- dirIndex ++
484
455
}
485
456
}
486
457
487
458
return kerr .NewAggregate (errs )
488
459
}
489
460
461
+ func processSnapshotLine (line string , subDirs []string , dirIndex * int , cutoffTime time.Time , repos * []repositoryInfo , errs * []error ) error {
462
+ type snapshot struct {
463
+ Time string `json:"time"`
464
+ }
465
+
466
+ increaseDirIndexAndAppendErr := func (dirIndex * int , err error ) {
467
+ if * dirIndex < len (subDirs ) {
468
+ * errs = append (* errs , err )
469
+ * dirIndex ++
470
+ }
471
+ }
472
+
473
+ var snapshots []snapshot
474
+ if err := json .Unmarshal ([]byte (line ), & snapshots ); err != nil {
475
+ increaseDirIndexAndAppendErr (dirIndex , fmt .Errorf ("failed to parse JSON for %s: %v" , subDirs [* dirIndex ], err ))
476
+ return err
477
+ }
478
+
479
+ if len (snapshots ) > 0 {
480
+ snapshotTime , err := time .Parse (time .RFC3339Nano , snapshots [0 ].Time )
481
+ if err != nil {
482
+ increaseDirIndexAndAppendErr (dirIndex , fmt .Errorf ("failed to parse time for %s: %v" , subDirs [* dirIndex ], err ))
483
+ return err
484
+ }
485
+ if * dirIndex < len (subDirs ) && snapshotTime .Before (cutoffTime ) {
486
+ * repos = append (* repos , repositoryInfo {
487
+ Path : subDirs [* dirIndex ],
488
+ LastModified : snapshotTime ,
489
+ })
490
+ }
491
+ }
492
+ * dirIndex ++
493
+ return nil
494
+ }
495
+
496
+ func processErrorJSONLine (line string , repoBase string , subDirs []string , dirIndex int , errs * []error ) {
497
+ errMsg := struct {
498
+ MessageType string `json:"message_type"`
499
+ Code int `json:"code"`
500
+ Message string `json:"message"`
501
+ }{}
502
+ if err := json .Unmarshal ([]byte (line ), & errMsg ); err == nil && errMsg .Message != "" {
503
+ // Skip "repository does not exist" (no repo to purge)
504
+ if dirIndex < len (subDirs ) && ! strings .Contains (strings .ToLower (errMsg .Message ), "repository does not exist" ) {
505
+ repoURL := strings .TrimRight (repoBase + "/" + subDirs [dirIndex ], "/" )
506
+ * errs = append (* errs , fmt .Errorf ("%s: %s" , repoURL , errMsg .Message ))
507
+ }
508
+ }
509
+ }
510
+
511
+ func displayRepositoryErrors (err error ) {
512
+ if err == nil {
513
+ return
514
+ }
515
+ fmt .Println ("\n ⚠️ Some repositories could not be processed:" )
516
+
517
+ w := tabwriter .NewWriter (os .Stdout , TableMinWidth , TableTabWidth , TablePadding , TablePadChar , 0 )
518
+ defer func () {
519
+ _ = w .Flush () // Handle error silently for display purposes
520
+ }()
521
+
522
+ // Header
523
+ _ , _ = fmt .Fprintf (w , "REPOSITORY\t ERROR\n " )
524
+ _ , _ = fmt .Fprintf (w , "----------\t -----\n " )
525
+
526
+ printErr := func (e error ) {
527
+ parts := strings .SplitN (e .Error (), ": " , 2 )
528
+ if len (parts ) == 2 {
529
+ _ , _ = fmt .Fprintf (w , "%s\t %s\n " , parts [0 ], parts [1 ])
530
+ } else {
531
+ _ , _ = fmt .Fprintf (w , "N/A\t %s\n " , e .Error ())
532
+ }
533
+ }
534
+
535
+ // kerr.NewAggregate returns something that implements Errors()
536
+ if agg , ok := err .(interface { Errors () []error }); ok {
537
+ for _ , e := range agg .Errors () {
538
+ printErr (e )
539
+ }
540
+ } else {
541
+ // fallback in case it's not an aggregate
542
+ printErr (err )
543
+ }
544
+ fmt .Println ()
545
+ }
546
+
490
547
func (opt * purgeOptions ) displayNoRepositoriesMessage () {
491
- fmt .Println ("✅ No repositories found matching the criteria." )
548
+ fmt .Println ("\n ✅ No repositories found matching the criteria." )
492
549
fmt .Printf (" - Age filter: older than %s\n " , opt .olderThan )
493
550
}
494
551
@@ -574,7 +631,7 @@ func (opt *purgeOptions) deleteRepositories(rw *restic.ResticWrapper, repos []re
574
631
}
575
632
576
633
// Execute restic purge operations
577
- fmt .Println ("Starting repository deletion process.. ." )
634
+ fmt .Println ("\n 🔥 Starting repository deletion. This process can be lengthy, please do not interrupt ." )
578
635
script := opt .generateRepoPurgeScript (rw , repoBase , repos )
579
636
out , err := runResticScriptViaDocker (script )
580
637
if err != nil {
0 commit comments