1515import Foundation
1616import HTTPTypes
1717
18- extension RegistryClient {
19- // Internal helper method to initiate a blob upload in 'two shot' mode
20- func startBlobUploadSession( repository: ImageReference . Repository ) async throws -> URL {
21- // Upload in "two shot" mode.
22- // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#post-then-put
23- // - POST to obtain a session ID.
24- // - Do not include the digest.
25- // Response will include a 'Location' header telling us where to PUT the blob data.
26- let httpResponse = try await executeRequestThrowing (
27- . post( repository, path: " blobs/uploads/ " ) ,
28- expectingStatus: . accepted, // expected response code for a "two-shot" upload
29- decodingErrors: [ . notFound]
30- )
31-
32- guard let location = httpResponse. response. headerFields [ . location] else {
33- throw HTTPClientError . missingResponseHeader ( " Location " )
34- }
35-
36- guard let locationURL = URL ( string: location) else {
37- throw RegistryClientError . invalidUploadLocation ( " \( location) " )
38- }
39-
40- // The location may be either an absolute URL or a relative URL
41- // If it is relative we need to make it absolute
42- guard locationURL. host != nil else {
43- guard let absoluteURL = URL ( string: location, relativeTo: registryURL) else {
44- throw RegistryClientError . invalidUploadLocation ( " \( location) " )
45- }
46- return absoluteURL
47- }
48-
49- return locationURL
50- }
51- }
52-
5318public extension RegistryClient {
54- func blobExists( repository: ImageReference . Repository , digest: ImageReference . Digest ) async throws -> Bool {
55- do {
56- let _ = try await executeRequestThrowing (
57- . head( repository, path: " blobs/ \( digest) " ) ,
58- decodingErrors: [ . notFound]
59- )
60- return true
61- } catch HTTPClientError . unexpectedStatusCode( status: . notFound, _, _) { return false }
62- }
63-
6419 /// Fetches an unstructured blob of data from the registry.
6520 ///
6621 /// - Parameters:
@@ -75,72 +30,4 @@ public extension RegistryClient {
7530 )
7631 . data
7732 }
78-
79- /// Uploads a blob to the registry.
80- ///
81- /// This function uploads a blob of unstructured data to the registry.
82- /// - Parameters:
83- /// - repository: Name of the destination repository.
84- /// - mediaType: mediaType field for returned ContentDescriptor.
85- /// On the wire, all blob uploads are `application/octet-stream'.
86- /// - data: Object to be uploaded.
87- /// - Returns: An ContentDescriptor object representing the
88- /// uploaded blob.
89- /// - Throws: If the blob cannot be encoded or the upload fails.
90- func putBlob( repository: ImageReference . Repository , mediaType: String = " application/octet-stream " , data: Data )
91- async throws
92- -> ContentDescriptor
93- {
94- // Ask the server to open a session and tell us where to upload our data
95- let location = try await startBlobUploadSession ( repository: repository)
96-
97- // Append the digest to the upload location, as the specification requires.
98- // The server's URL is arbitrary and might already contain query items which we must not overwrite.
99- // The URL could even point to a different host.
100- let digest = ImageReference . Digest ( of: data)
101- let uploadURL = location. appending ( queryItems: [ . init( name: " digest " , value: " \( digest) " ) ] )
102-
103- let httpResponse = try await executeRequestThrowing (
104- // All blob uploads have Content-Type: application/octet-stream on the wire, even if mediatype is different
105- . put( repository, url: uploadURL, contentType: " application/octet-stream " ) ,
106- uploading: data,
107- expectingStatus: . created,
108- decodingErrors: [ . badRequest, . notFound]
109- )
110-
111- // The registry could compute a different digest and we should use its value
112- // as the canonical digest for linking blobs. If the registry sends a digest we
113- // should check that it matches our locally-calculated digest.
114- if let serverDigest = httpResponse. response. headerFields [ . dockerContentDigest] {
115- assert ( " \( digest) " == serverDigest)
116- }
117- return . init( mediaType: mediaType, digest: " \( digest) " , size: Int64 ( data. count) )
118- }
119-
120- /// Uploads a blob to the registry.
121- ///
122- /// This function converts an encodable blob to an `application/octet-stream',
123- /// calculates its digest and uploads it to the registry.
124- /// - Parameters:
125- /// - repository: Name of the destination repository.
126- /// - mediaType: mediaType field for returned ContentDescriptor.
127- /// On the wire, all blob uploads are `application/octet-stream'.
128- /// - data: Object to be uploaded.
129- /// - Returns: An ContentDescriptor object representing the
130- /// uploaded blob.
131- /// - Throws: If the blob cannot be encoded or the upload fails.
132- ///
133- /// Some JSON objects, such as ImageConfiguration, are stored
134- /// in the registry as plain blobs with MIME type "application/octet-stream".
135- /// This function encodes the data parameter and uploads it as a generic blob.
136- func putBlob< Body: Encodable > (
137- repository: ImageReference . Repository ,
138- mediaType: String = " application/octet-stream " ,
139- data: Body
140- )
141- async throws -> ContentDescriptor
142- {
143- let encoded = try encoder. encode ( data)
144- return try await putBlob ( repository: repository, mediaType: mediaType, data: encoded)
145- }
14633}
0 commit comments