package publish import ( "context" "errors" "fmt" "io" "os" "path/filepath" "sort" "strings" ) type Attempt struct { RecordPath string Evidence Evidence } type AttemptsResult struct { RecordDir string Attempts []Attempt Diagnostics []*RecoveryError } func ListAttempts(ctx context.Context, repositoryDir, override string) (AttemptsResult, error) { return listAttempts(ctx, repositoryDir, override, recordPaths) } func listAttempts(ctx context.Context, repositoryDir, override string, enumerate func(context.Context, string) ([]string, error)) (AttemptsResult, error) { if repositoryDir == "" { repositoryDir = "." } result := AttemptsResult{Attempts: []Attempt{}, Diagnostics: []*RecoveryError{}} if err := ctx.Err(); err != nil { return result, recoveryFailure(ctx, "storage_enumeration", err) } var directory string { resolved, err := recordDirectoryFor(ctx, repositoryDir, override) if err != nil { return result, recoveryFailure(ctx, "storage_enumeration", err) } directory = resolved } result.RecordDir = directory if err := ctx.Err(); err != nil { return result, &RecoveryError{Kind: "canceled", cause: err} } paths, enumerationErr := enumerate(ctx, directory) sort.Strings(paths) for _, path := range paths { if err := ctx.Err(); err != nil { return result, &RecoveryError{Kind: "canceled", cause: err} } absolute, err := filepath.Abs(path) if err != nil { result.Diagnostics = append(result.Diagnostics, &RecoveryError{Kind: "record_read", RecordPath: path, cause: err}) continue } evidence, err := LoadEvidence(absolute) if err != nil { var recordError *RecoveryError if errors.As(err, &recordError) { result.Diagnostics = append(result.Diagnostics, recordError) } continue } result.Attempts = append(result.Attempts, Attempt{RecordPath: absolute, Evidence: evidence}) } if enumerationErr != nil { return result, recoveryFailure(ctx, "storage_enumeration", fmt.Errorf("enumerate records in %q: %w", directory, enumerationErr)) } return result, nil } // recordPaths filters unsorted batches, retaining only selected record names. func recordPaths(ctx context.Context, directory string) ([]string, error) { file, err := os.Open(directory) if err != nil { return nil, err } defer file.Close() return readRecordPaths(ctx, directory, file.ReadDir) } func readRecordPaths(ctx context.Context, directory string, read func(int) ([]os.DirEntry, error)) ([]string, error) { var paths []string for { if err := ctx.Err(); err != nil { return paths, err } entries, err := read(128) for _, entry := range entries { if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".json") { paths = append(paths, filepath.Join(directory, entry.Name())) } } if errors.Is(err, io.EOF) { return paths, nil } if err != nil { return paths, err } } } func recordDirectoryFor(ctx context.Context, repositoryDir, override string) (string, error) { if filepath.IsAbs(override) { return filepath.Clean(override), nil } directory, _, err := resolveRecordDir(ctx, repositoryDir, override) return directory, err }