Skip to content

Commit e57fa98

Browse files
Wrap error and display for debugging
1 parent d3dee70 commit e57fa98

File tree

1 file changed

+117
-60
lines changed

1 file changed

+117
-60
lines changed

pkg/purge_repos.go

Lines changed: 117 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -323,22 +323,23 @@ func (opt *purgeOptions) executePurgeWorkflow(setupOpt restic.SetupOptions, cuto
323323
return fmt.Errorf("failed to create restic wrapper: %v", err)
324324
}
325325

326-
repoList, err := opt.findRepositoriesToPurge(rw, cutoffTime)
326+
// Get repository base URL for display purposes
327+
repoBase, err := opt.getResticRepoFromEnv(rw)
327328
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)
329336
}
330337

331338
if len(repoList) == 0 {
332339
opt.displayNoRepositoriesMessage()
333340
return nil
334341
}
335342

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-
342343
opt.displayRepositoriesTable(repoList, repoBase)
343344
if opt.dryRun {
344345
displayDryRunMessage(len(repoList))
@@ -352,29 +353,21 @@ func (opt *purgeOptions) executePurgeWorkflow(setupOpt restic.SetupOptions, cuto
352353
return opt.deleteRepositories(rw, repoList)
353354
}
354355

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) {
356357
var repos []repositoryInfo
357358
subDirs, err := opt.listSubdirectories("")
358359
if err != nil {
359360
return nil, fmt.Errorf("cannot list sub-dirs: %w", err)
360361
}
361362

362-
repoBase, err := opt.getResticRepoFromEnv(rw)
363-
if err != nil {
364-
return nil, err
365-
}
366-
367363
script := opt.generateRepoListScript(repoBase, rw, subDirs)
368364
out, err := runResticScriptViaDocker(script)
369365
if err != nil {
370366
return nil, fmt.Errorf("Error running repo check script: %v\nOutput:\n%s", err, out)
371367
}
372368

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
378371
}
379372

380373
func (opt *purgeOptions) listSubdirectories(path string) ([]string, error) {
@@ -434,61 +427,125 @@ func runResticScriptViaDocker(script string) (string, error) {
434427
return string(out), err
435428
}
436429

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+
444436
lines := strings.Split(out, "\n")
445437
for _, line := range lines {
446438
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 == "" {
454440
continue
455441
}
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 {
464445
continue
465446
}
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++
482454
}
483-
dirIndex++
484455
}
485456
}
486457

487458
return kerr.NewAggregate(errs)
488459
}
489460

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\tERROR\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+
490547
func (opt *purgeOptions) displayNoRepositoriesMessage() {
491-
fmt.Println("✅ No repositories found matching the criteria.")
548+
fmt.Println("\n✅ No repositories found matching the criteria.")
492549
fmt.Printf(" - Age filter: older than %s\n", opt.olderThan)
493550
}
494551

@@ -574,7 +631,7 @@ func (opt *purgeOptions) deleteRepositories(rw *restic.ResticWrapper, repos []re
574631
}
575632

576633
// 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.")
578635
script := opt.generateRepoPurgeScript(rw, repoBase, repos)
579636
out, err := runResticScriptViaDocker(script)
580637
if err != nil {

0 commit comments

Comments
 (0)