@@ -125,47 +125,13 @@ public class SignatureValidator {
125
125
}
126
126
}
127
127
128
- public func download( src: URL , dest: URL , urlSession: URLSession ) async throws ( DownloadError) {
129
- var req = URLRequest ( url: src)
130
- if FileManager . default. fileExists ( atPath: dest. path) {
131
- if let existingFileData = try ? Data ( contentsOf: dest, options: . mappedIfSafe) {
132
- req. setValue ( etag ( data: existingFileData) , forHTTPHeaderField: " If-None-Match " )
133
- }
134
- }
135
- // TODO: Add Content-Length headers to coderd, add download progress delegate
136
- let tempURL : URL
137
- let response : URLResponse
138
- do {
139
- ( tempURL, response) = try await urlSession. download ( for: req)
140
- } catch {
141
- throw . networkError( error, url: src. absoluteString)
142
- }
143
- defer {
144
- if FileManager . default. fileExists ( atPath: tempURL. path) {
145
- try ? FileManager . default. removeItem ( at: tempURL)
146
- }
147
- }
148
-
149
- guard let httpResponse = response as? HTTPURLResponse else {
150
- throw . invalidResponse
151
- }
152
- guard httpResponse. statusCode != 304 else {
153
- // We already have the latest dylib downloaded on disk
154
- return
155
- }
156
-
157
- guard httpResponse. statusCode == 200 else {
158
- throw . unexpectedStatusCode( httpResponse. statusCode)
159
- }
160
-
161
- do {
162
- if FileManager . default. fileExists ( atPath: dest. path) {
163
- try FileManager . default. removeItem ( at: dest)
164
- }
165
- try FileManager . default. moveItem ( at: tempURL, to: dest)
166
- } catch {
167
- throw . fileOpError( error)
168
- }
128
+ public func download(
129
+ src: URL ,
130
+ dest: URL ,
131
+ urlSession: URLSession ,
132
+ progressUpdates: ( ( DownloadProgress ) -> Void ) ? = nil
133
+ ) async throws ( DownloadError) {
134
+ try await DownloadManager ( ) . download ( src: src, dest: dest, urlSession: urlSession, progressUpdates: progressUpdates)
169
135
}
170
136
171
137
func etag( data: Data ) -> String {
@@ -195,3 +161,104 @@ public enum DownloadError: Error {
195
161
196
162
public var localizedDescription : String { description }
197
163
}
164
+
165
+ // The async `URLSession.download` api ignores the passed-in delegate, so we
166
+ // wrap the older delegate methods in an async adapter with a continuation.
167
+ private final class DownloadManager : NSObject , @unchecked Sendable {
168
+ private var continuation : CheckedContinuation < Void , Error > !
169
+ private var progressHandler : ( ( DownloadProgress ) -> Void ) ?
170
+ private var dest : URL !
171
+
172
+ func download(
173
+ src: URL ,
174
+ dest: URL ,
175
+ urlSession: URLSession ,
176
+ progressUpdates: ( ( DownloadProgress ) -> Void ) ?
177
+ ) async throws ( DownloadError) {
178
+ var req = URLRequest ( url: src)
179
+ if FileManager . default. fileExists ( atPath: dest. path) {
180
+ if let existingFileData = try ? Data ( contentsOf: dest, options: . mappedIfSafe) {
181
+ req. setValue ( etag ( data: existingFileData) , forHTTPHeaderField: " If-None-Match " )
182
+ }
183
+ }
184
+
185
+ let downloadTask = urlSession. downloadTask ( with: req)
186
+ progressHandler = progressUpdates
187
+ self . dest = dest
188
+ downloadTask. delegate = self
189
+ do {
190
+ try await withCheckedThrowingContinuation { continuation in
191
+ self . continuation = continuation
192
+ downloadTask. resume ( )
193
+ }
194
+ } catch let error as DownloadError {
195
+ throw error
196
+ } catch {
197
+ throw . networkError( error, url: src. absoluteString)
198
+ }
199
+ }
200
+ }
201
+
202
+ extension DownloadManager : URLSessionDownloadDelegate {
203
+ // Progress
204
+ func urlSession(
205
+ _: URLSession ,
206
+ downloadTask: URLSessionDownloadTask ,
207
+ didWriteData _: Int64 ,
208
+ totalBytesWritten: Int64 ,
209
+ totalBytesExpectedToWrite _: Int64
210
+ ) {
211
+ let maybeLength = ( downloadTask. response as? HTTPURLResponse ) ?
212
+ . value ( forHTTPHeaderField: " X-Original-Content-Length " )
213
+ . flatMap ( Int64 . init)
214
+ progressHandler ? ( . init( totalBytesWritten: totalBytesWritten, totalBytesToWrite: maybeLength) )
215
+ }
216
+
217
+ // Completion
218
+ func urlSession( _: URLSession , downloadTask: URLSessionDownloadTask , didFinishDownloadingTo location: URL ) {
219
+ guard let httpResponse = downloadTask. response as? HTTPURLResponse else {
220
+ continuation. resume ( throwing: DownloadError . invalidResponse)
221
+ return
222
+ }
223
+ guard httpResponse. statusCode != 304 else {
224
+ // We already have the latest dylib downloaded in dest
225
+ continuation. resume ( )
226
+ return
227
+ }
228
+
229
+ guard httpResponse. statusCode == 200 else {
230
+ continuation. resume ( throwing: DownloadError . unexpectedStatusCode ( httpResponse. statusCode) )
231
+ return
232
+ }
233
+
234
+ do {
235
+ if FileManager . default. fileExists ( atPath: dest. path) {
236
+ try FileManager . default. removeItem ( at: dest)
237
+ }
238
+ try FileManager . default. moveItem ( at: location, to: dest)
239
+ } catch {
240
+ continuation. resume ( throwing: DownloadError . fileOpError ( error) )
241
+ }
242
+
243
+ continuation. resume ( )
244
+ }
245
+
246
+ // Failure
247
+ func urlSession( _: URLSession , task _: URLSessionTask , didCompleteWithError error: Error ? ) {
248
+ if let error {
249
+ continuation. resume ( throwing: error)
250
+ }
251
+ }
252
+ }
253
+
254
+ public struct DownloadProgress : Sendable , CustomStringConvertible {
255
+ let totalBytesWritten : Int64
256
+ let totalBytesToWrite : Int64 ?
257
+
258
+ public var description : String {
259
+ let fmt = ByteCountFormatter ( )
260
+ let done = fmt. string ( fromByteCount: totalBytesWritten)
261
+ let total = totalBytesToWrite. map { fmt. string ( fromByteCount: $0) } ?? " Unknown "
262
+ return " \( done) / \( total) "
263
+ }
264
+ }
0 commit comments