@@ -323,9 +323,10 @@ func (opt *purgeOptions) executePurgeWorkflow(setupOpt restic.SetupOptions, cuto
323323 return fmt .Errorf ("failed to create restic wrapper: %v" , err )
324324 }
325325
326+ fmt .Println ("\n 🔎 Searching for repositories. This may take a while depending on the number of repositories..." )
326327 repoList , err := opt .findRepositoriesToPurge (rw , cutoffTime )
327328 if err != nil {
328- return err
329+ displayRepositoryErrors ( err )
329330 }
330331
331332 if len (repoList ) == 0 {
@@ -371,10 +372,7 @@ func (opt *purgeOptions) findRepositoriesToPurge(rw *restic.ResticWrapper, cutof
371372 }
372373
373374 err = extractRepoListFromOutput (out , subDirs , cutoffTime , & repos )
374- if err != nil {
375- return nil , err
376- }
377- return repos , nil
375+ return repos , err
378376}
379377
380378func (opt * purgeOptions ) listSubdirectories (path string ) ([]string , error ) {
@@ -435,58 +433,101 @@ func runResticScriptViaDocker(script string) (string, error) {
435433}
436434
437435func 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
436+ var (
437+ dirIndex int
438+ errs []error
439+ )
440+
444441 lines := strings .Split (out , "\n " )
445442 for _ , line := range lines {
446443 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- }
444+ if line == "" {
454445 continue
455446 }
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- }
447+ switch {
448+ case strings .HasPrefix (line , "[" ):
449+ if err := processSnapshotLine (line , subDirs , & dirIndex , cutoffTime , repos , & errs ); err != nil {
464450 continue
465451 }
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- }
452+ case strings .HasPrefix (line , "{" ):
453+ processErrorJSONLine (line , subDirs , dirIndex , & errs )
454+ case strings .HasPrefix (line , "Failed to access repository" ) ||
455+ strings .Contains (line , "Fatal: repository does not exist" ):
456+ // Handle plain text error lines
457+ if dirIndex < len (subDirs ) {
458+ dirIndex ++
482459 }
483- dirIndex ++
484460 }
485461 }
486462
487463 return kerr .NewAggregate (errs )
488464}
489465
466+ func processSnapshotLine (line string , subDirs []string , dirIndex * int , cutoffTime time.Time , repos * []repositoryInfo , errs * []error ) error {
467+ type snapshot struct {
468+ Time string `json:"time"`
469+ }
470+
471+ increaseDirIndexAndAppendErr := func (dirIndex * int , err error ) {
472+ if * dirIndex < len (subDirs ) {
473+ * errs = append (* errs , err )
474+ * dirIndex ++
475+ }
476+ }
477+
478+ var snapshots []snapshot
479+ if err := json .Unmarshal ([]byte (line ), & snapshots ); err != nil {
480+ increaseDirIndexAndAppendErr (dirIndex , fmt .Errorf ("failed to parse JSON for %s: %v" , subDirs [* dirIndex ], err ))
481+ return err
482+ }
483+
484+ if len (snapshots ) > 0 {
485+ snapshotTime , err := time .Parse (time .RFC3339Nano , snapshots [0 ].Time )
486+ if err != nil {
487+ increaseDirIndexAndAppendErr (dirIndex , fmt .Errorf ("failed to parse time for %s: %v" , subDirs [* dirIndex ], err ))
488+ return err
489+ }
490+ if * dirIndex < len (subDirs ) && snapshotTime .Before (cutoffTime ) {
491+ * repos = append (* repos , repositoryInfo {
492+ Path : subDirs [* dirIndex ],
493+ LastModified : snapshotTime ,
494+ })
495+ }
496+ }
497+ * dirIndex ++
498+ return nil
499+ }
500+
501+ func processErrorJSONLine (line string , subDirs []string , dirIndex int , errs * []error ) {
502+ errMsg := struct {
503+ MessageType string `json:"message_type"`
504+ Code int `json:"code"`
505+ Message string `json:"message"`
506+ }{}
507+ if err := json .Unmarshal ([]byte (line ), & errMsg ); err == nil && errMsg .Message != "" {
508+ // Skip "repository does not exist" (no repo to purge)
509+ if dirIndex < len (subDirs ) && ! strings .Contains (strings .ToLower (errMsg .Message ), "repository does not exist" ) {
510+ * errs = append (* errs , fmt .Errorf ("repository %s: %s" , subDirs [dirIndex ], errMsg .Message ))
511+ }
512+ }
513+ }
514+
515+ func displayRepositoryErrors (err error ) {
516+ if err == nil {
517+ return
518+ }
519+ fmt .Println ("\n ⚠️ Some repositories could not be processed:" )
520+ // kerr.NewAggregate returns something that implements Errors()
521+ if agg , ok := err .(interface { Errors () []error }); ok {
522+ for _ , e := range agg .Errors () {
523+ fmt .Printf (" - %s\n " , e .Error ())
524+ }
525+ } else {
526+ // fallback in case it's not an aggregate
527+ fmt .Printf (" - %s\n " , err .Error ())
528+ }
529+ }
530+
490531func (opt * purgeOptions ) displayNoRepositoriesMessage () {
491532 fmt .Println ("✅ No repositories found matching the criteria." )
492533 fmt .Printf (" - Age filter: older than %s\n " , opt .olderThan )
@@ -574,7 +615,7 @@ func (opt *purgeOptions) deleteRepositories(rw *restic.ResticWrapper, repos []re
574615 }
575616
576617 // Execute restic purge operations
577- fmt .Println ("Starting repository deletion process.. ." )
618+ fmt .Println ("\n 🔥 Starting repository deletion. This process can be lengthy, please do not interrupt ." )
578619 script := opt .generateRepoPurgeScript (rw , repoBase , repos )
579620 out , err := runResticScriptViaDocker (script )
580621 if err != nil {
0 commit comments