-
Notifications
You must be signed in to change notification settings - Fork 261
Expand file tree
/
Copy pathRegistryClient+Catalog.swift
More file actions
85 lines (75 loc) · 3.65 KB
/
RegistryClient+Catalog.swift
File metadata and controls
85 lines (75 loc) · 3.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import AsyncHTTPClient
import ContainerizationError
import Foundation
import NIOFoundationCompat
private struct CatalogResponse: Sendable, Decodable {
let repositories: [String]
}
extension RegistryClient {
/// List repositories in the registry.
///
/// Implements GET /v2/_catalog from the OCI Distribution Spec with pagination.
/// When prefix is provided, pagination skips ahead to the relevant portion of
/// the lexically-sorted catalog and stops once results move past the prefix.
///
/// - Parameter prefix: Optional prefix to filter repository names. Must be at least
/// two characters long to enable the skip-ahead optimization; shorter values are
/// treated as no prefix.
/// - Returns: An array of repository names matching the prefix (or all repositories
/// if no prefix is given).
public func catalog(prefix: String? = nil) async throws -> [String] {
let effectivePrefix = prefix.flatMap { $0.count >= 2 ? $0 : nil }
var allRepos: [String] = []
// When a prefix is provided, skip ahead in the lexically-sorted catalog
// by setting last to one position before the prefix. The OCI spec
// returns entries that sort after last, so dropping the last character
// of the prefix positions the cursor just before matching entries.
var last: String? = effectivePrefix.map { String($0.dropLast()) }
let pageSize = 100
while true {
var components = base
components.path = "/v2/_catalog"
var queryItems = [URLQueryItem(name: "n", value: String(pageSize))]
if let last {
queryItems.append(URLQueryItem(name: "last", value: last))
}
components.queryItems = queryItems
let repos: [String] = try await request(components: components) { response in
guard response.status == .ok else {
let url = components.url?.absoluteString ?? "unknown"
let reason = await ErrorResponse.fromResponseBody(response.body)?.jsonString
throw Error.invalidStatus(url: url, response.status, reason: reason)
}
let buffer = try await response.body.collect(upTo: self.bufferSize)
return try JSONDecoder().decode(CatalogResponse.self, from: buffer).repositories
}
if let effectivePrefix {
let matching = repos.filter { $0.hasPrefix(effectivePrefix) }
allRepos.append(contentsOf: matching)
if let lastRepo = repos.last, !lastRepo.hasPrefix(effectivePrefix) && lastRepo > effectivePrefix {
break
}
} else {
allRepos.append(contentsOf: repos)
}
if repos.count < pageSize { break }
last = repos.last
}
return allRepos
}
}