Skip to content

Commit bb35696

Browse files
committed
download progress
1 parent 9a3c402 commit bb35696

File tree

2 files changed

+115
-42
lines changed

2 files changed

+115
-42
lines changed

Coder-Desktop/VPN/Manager.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ actor Manager {
3535
// Timeout after 5 minutes, or if there's no data for 60 seconds
3636
sessionConfig.timeoutIntervalForRequest = 60
3737
sessionConfig.timeoutIntervalForResource = 300
38-
try await download(src: dylibPath, dest: dest, urlSession: URLSession(configuration: sessionConfig))
38+
try await download(
39+
src: dylibPath,
40+
dest: dest,
41+
urlSession: URLSession(configuration: sessionConfig)
42+
) { progress in
43+
pushProgress(msg: "Downloading library...\n\(progress.description)")
44+
}
3945
} catch {
4046
throw .download(error)
4147
}

Coder-Desktop/VPNLib/Download.swift

Lines changed: 108 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -125,47 +125,13 @@ public class SignatureValidator {
125125
}
126126
}
127127

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)
169135
}
170136

171137
func etag(data: Data) -> String {
@@ -195,3 +161,104 @@ public enum DownloadError: Error {
195161

196162
public var localizedDescription: String { description }
197163
}
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

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy