Files
luma/Sources/LumaCore/VirtualMachines/ProgressiveDownload.swift
Ole André Vadla Ravnås 9a545579f7 Say why a download failed
A rejected response became a file-does-not-exist error, and both
libraries then threw the cause away and guessed at it: a refused
agent was reported as one nobody had published.

Name the status the server answered, and carry it through.
2026-09-15 13:45:03 +02:00

86 lines
2.8 KiB
Swift

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
final class ProgressiveDownload: NSObject, URLSessionDownloadDelegate, @unchecked Sendable {
static func fetch(
_ url: URL,
reporting report: @escaping @Sendable (Double?) -> Void
) async throws -> URL {
let delegate = ProgressiveDownload(report: report)
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
defer { session.finishTasksAndInvalidate() }
return try await withCheckedThrowingContinuation { continuation in
delegate.continuation = continuation
session.downloadTask(with: url).resume()
}
}
private let report: @Sendable (Double?) -> Void
private var continuation: CheckedContinuation<URL, any Error>?
private var lastReported: Double = 0
private init(report: @escaping @Sendable (Double?) -> Void) {
self.report = report
}
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64
) {
guard totalBytesExpectedToWrite > 0 else {
report(nil)
return
}
let fraction = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
guard fraction - lastReported >= 0.01 || fraction == 1 else { return }
lastReported = fraction
report(fraction)
}
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL
) {
let result: Result<URL, any Error>
if let http = downloadTask.response as? HTTPURLResponse, http.statusCode != 200 {
result = .failure(DownloadError.rejected(status: http.statusCode))
} else {
let kept = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: false)
result = Result { try FileManager.default.moveItem(at: location, to: kept) }
.map { kept }
}
continuation?.resume(with: result)
continuation = nil
}
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: (any Error)?
) {
guard let error else { return }
continuation?.resume(throwing: error)
continuation = nil
}
}
enum DownloadError: Swift.Error, LocalizedError {
case rejected(status: Int)
var errorDescription: String? {
switch self {
case .rejected(let status):
return "the server answered \(status) \(HTTPURLResponse.localizedString(forStatusCode: status))"
}
}
}