@@ -5,91 +5,49 @@ import NIO
55import NIOFoundationCompat
66import NIOHTTP1
77
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
1710}
1811
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 ( )
6215
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)
6418 }
6519}
6620
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+
6727/// HTTPClient wrapper used for interfacing with various REST APIs and downloading things.
6828public struct SwiftlyHTTPClient {
69- fileprivate static let client = HTTPClientWrapper ( )
70-
7129 private struct Response {
7230 let status : HTTPResponseStatus
7331 let buffer : ByteBuffer
7432 }
7533
76- private let downloader : ToolchainDownloader
34+ private let executor : HTTPRequestExecutor
7735
7836 /// The GitHub authentication token to use for any requests made to the GitHub API.
7937 public var githubToken : String ?
8038
81- public init ( toolchainDownloader : ToolchainDownloader ? = nil ) {
82- self . downloader = toolchainDownloader ?? HTTPToolchainDownloader ( )
39+ public init ( executor : HTTPRequestExecutor ? = nil ) {
40+ self . executor = executor ?? HTTPRequestExecutorImpl ( )
8341 }
8442
8543 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)
8745
8846 for (k, v) in headers {
8947 request. headers. add ( name: k, value: v)
9048 }
9149
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 ) )
9351
9452 // if defined, the content-length headers announces the size of the body
9553 let expectedBytes = response. headers. first ( name: " content-length " ) . flatMap ( Int . init) ?? 1024 * 1024
@@ -179,30 +137,53 @@ public struct SwiftlyHTTPClient {
179137 public let url : String
180138 }
181139
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 ( )
194181 }
195182}
196183
197184private class HTTPClientWrapper {
198185 fileprivate let inner = HTTPClient ( eventLoopGroupProvider: . singleton)
199186
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-
206187 deinit {
207188 try ? self . inner. syncShutdown ( )
208189 }
0 commit comments