@@ -5,91 +5,49 @@ import NIO
5
5
import NIOFoundationCompat
6
6
import NIOHTTP1
7
7
8
- /// Protocol describing the behavior for downloading a tooclhain.
9
- /// This is used to abstract over the underlying HTTP client to allow for mocking downloads in tests.
10
- public protocol ToolchainDownloader {
11
- func downloadToolchain(
12
- _ toolchain: ToolchainVersion ,
13
- url: String ,
14
- to destination: String ,
15
- reportProgress: @escaping ( SwiftlyHTTPClient . DownloadProgress ) -> Void
16
- ) async throws
8
+ public protocol HTTPRequestExecutor {
9
+ func execute( _ request: HTTPClientRequest , timeout: TimeAmount ) async throws -> HTTPClientResponse
17
10
}
18
11
19
- /// The default implementation of a toolchain downloader.
20
- /// Downloads toolchains from swift.org.
21
- private struct HTTPToolchainDownloader : ToolchainDownloader {
22
- func downloadToolchain(
23
- _: ToolchainVersion ,
24
- url: String ,
25
- to destination: String ,
26
- reportProgress: @escaping ( SwiftlyHTTPClient . DownloadProgress ) -> Void
27
- ) async throws {
28
- let fileHandle = try FileHandle ( forWritingTo: URL ( fileURLWithPath: destination) )
29
- defer {
30
- try ? fileHandle. close ( )
31
- }
32
-
33
- let request = SwiftlyHTTPClient . client. makeRequest ( url: url)
34
- let response = try await SwiftlyHTTPClient . client. inner. execute ( request, timeout: . seconds( 30 ) )
35
-
36
- guard case response . status = HTTPResponseStatus . ok else {
37
- throw Error ( message: " Received \( response. status) when trying to download \( url) " )
38
- }
39
-
40
- // Unknown download.swift.org paths redirect to a 404 page which then returns a 200 status.
41
- // As a heuristic for if we've hit the 404 page, we check to see if the content is HTML.
42
- guard !response. headers [ " Content-Type " ] . contains ( where: { $0. contains ( " text/html " ) } ) else {
43
- throw SwiftlyHTTPClient . DownloadNotFoundError ( url: url)
44
- }
45
-
46
- // if defined, the content-length headers announces the size of the body
47
- let expectedBytes = response. headers. first ( name: " content-length " ) . flatMap ( Int . init)
48
-
49
- var receivedBytes = 0
50
- for try await buffer in response. body {
51
- receivedBytes += buffer. readableBytes
52
-
53
- try buffer. withUnsafeReadableBytes { bufferPtr in
54
- try fileHandle. write ( contentsOf: bufferPtr)
55
- }
56
- reportProgress ( SwiftlyHTTPClient . DownloadProgress (
57
- receivedBytes: receivedBytes,
58
- totalBytes: expectedBytes
59
- )
60
- )
61
- }
12
+ /// An `HTTPRequestExecutor` backed by an `HTTPClient`.
13
+ internal struct HTTPRequestExecutorImpl : HTTPRequestExecutor {
14
+ fileprivate static let client = HTTPClientWrapper ( )
62
15
63
- try fileHandle. synchronize ( )
16
+ public func execute( _ request: HTTPClientRequest , timeout: TimeAmount ) async throws -> HTTPClientResponse {
17
+ try await Self . client. inner. execute ( request, timeout: timeout)
64
18
}
65
19
}
66
20
21
+ private func makeRequest( url: String ) -> HTTPClientRequest {
22
+ var request = HTTPClientRequest ( url: url)
23
+ request. headers. add ( name: " User-Agent " , value: " swiftly/ \( SwiftlyCore . version) " )
24
+ return request
25
+ }
26
+
67
27
/// HTTPClient wrapper used for interfacing with various REST APIs and downloading things.
68
28
public struct SwiftlyHTTPClient {
69
- fileprivate static let client = HTTPClientWrapper ( )
70
-
71
29
private struct Response {
72
30
let status : HTTPResponseStatus
73
31
let buffer : ByteBuffer
74
32
}
75
33
76
- private let downloader : ToolchainDownloader
34
+ private let executor : HTTPRequestExecutor
77
35
78
36
/// The GitHub authentication token to use for any requests made to the GitHub API.
79
37
public var githubToken : String ?
80
38
81
- public init ( toolchainDownloader : ToolchainDownloader ? = nil ) {
82
- self . downloader = toolchainDownloader ?? HTTPToolchainDownloader ( )
39
+ public init ( executor : HTTPRequestExecutor ? = nil ) {
40
+ self . executor = executor ?? HTTPRequestExecutorImpl ( )
83
41
}
84
42
85
43
private func get( url: String , headers: [ String : String ] ) async throws -> Response {
86
- var request = Self . client . makeRequest ( url: url)
44
+ var request = makeRequest ( url: url)
87
45
88
46
for (k, v) in headers {
89
47
request. headers. add ( name: k, value: v)
90
48
}
91
49
92
- let response = try await Self . client . inner . execute ( request, timeout: . seconds( 30 ) )
50
+ let response = try await self . executor . execute ( request, timeout: . seconds( 30 ) )
93
51
94
52
// if defined, the content-length headers announces the size of the body
95
53
let expectedBytes = response. headers. first ( name: " content-length " ) . flatMap ( Int . init) ?? 1024 * 1024
@@ -179,30 +137,53 @@ public struct SwiftlyHTTPClient {
179
137
public let url : String
180
138
}
181
139
182
- public func downloadToolchain(
183
- _ toolchain: ToolchainVersion ,
184
- url: String ,
185
- to destination: String ,
186
- reportProgress: @escaping ( DownloadProgress ) -> Void
187
- ) async throws {
188
- try await self . downloader. downloadToolchain (
189
- toolchain,
190
- url: url,
191
- to: destination,
192
- reportProgress: reportProgress
193
- )
140
+ public func downloadFile( url: URL , to destination: URL , reportProgress: @escaping ( DownloadProgress ) -> Void ) async throws {
141
+ let fileHandle = try FileHandle ( forWritingTo: destination)
142
+ defer {
143
+ try ? fileHandle. close ( )
144
+ }
145
+
146
+ let request = makeRequest ( url: url. absoluteString)
147
+ let response = try await self . executor. execute ( request, timeout: . seconds( 30 ) )
148
+
149
+ switch response. status {
150
+ case . ok:
151
+ break
152
+ case . notFound:
153
+ throw SwiftlyHTTPClient . DownloadNotFoundError ( url: url. path)
154
+ default :
155
+ throw Error ( message: " Received \( response. status) when trying to download \( url) " )
156
+ }
157
+
158
+ // if defined, the content-length headers announces the size of the body
159
+ let expectedBytes = response. headers. first ( name: " content-length " ) . flatMap ( Int . init)
160
+
161
+ var lastUpdate = Date ( )
162
+ var receivedBytes = 0
163
+ for try await buffer in response. body {
164
+ receivedBytes += buffer. readableBytes
165
+
166
+ try buffer. withUnsafeReadableBytes { bufferPtr in
167
+ try fileHandle. write ( contentsOf: bufferPtr)
168
+ }
169
+
170
+ let now = Date ( )
171
+ if lastUpdate. distance ( to: now) > 0.25 || receivedBytes == expectedBytes {
172
+ lastUpdate = now
173
+ reportProgress ( SwiftlyHTTPClient . DownloadProgress (
174
+ receivedBytes: receivedBytes,
175
+ totalBytes: expectedBytes
176
+ ) )
177
+ }
178
+ }
179
+
180
+ try fileHandle. synchronize ( )
194
181
}
195
182
}
196
183
197
184
private class HTTPClientWrapper {
198
185
fileprivate let inner = HTTPClient ( eventLoopGroupProvider: . singleton)
199
186
200
- fileprivate func makeRequest( url: String ) -> HTTPClientRequest {
201
- var request = HTTPClientRequest ( url: url)
202
- request. headers. add ( name: " User-Agent " , value: " swiftly " )
203
- return request
204
- }
205
-
206
187
deinit {
207
188
try ? self . inner. syncShutdown ( )
208
189
}
0 commit comments