Skip to content

Commit 35f0d67

Browse files
authored
Merge pull request #28 from hartbit/downloader-improvements
Report Downloader progress as a number of bytes instead of percentage
2 parents f0cb169 + ad15268 commit 35f0d67

File tree

2 files changed

+45
-22
lines changed

2 files changed

+45
-22
lines changed

Sources/TSCUtility/Downloader.swift

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,42 @@ public enum DownloaderError: Error {
3030
/// The `Downloader` protocol abstract away the download of a file with a progress report.
3131
public protocol Downloader {
3232

33+
/// The progress closure type. The first arguments contains the number of bytes downloaded, and the second argument
34+
/// contains the total number of bytes to download, if known.
35+
typealias Progress = (Int64, Int64?) -> Void
36+
37+
/// The completion closure type. The only argument contains the result type containing the
38+
/// `DownloaderError` encountered on failure.
39+
typealias Completion = (Result<Void, DownloaderError>) -> Void
40+
3341
/// Downloads a file and keeps the caller updated on the progress and completion.
3442
///
3543
/// - Parameters:
3644
/// - url: The `URL` to the file to download.
3745
/// - destination: The `AbsolutePath` to download the file to.
38-
/// - progress: A closure to receive the download's progress as a fractional value between `0.0` and `1.0`.
39-
/// - completion: A closure to be notifed of the completion of the download as a `Result` type containing the
40-
/// `DownloaderError` encountered on failure.
46+
/// - progress: A closure to receive the download's progress as number of bytes.
47+
/// - completion: A closure to be notifed of the completion of the download.
4148
func downloadFile(
4249
at url: Foundation.URL,
4350
to destination: AbsolutePath,
44-
progress: @escaping (Double) -> Void,
45-
completion: @escaping (Result<Void, DownloaderError>) -> Void
51+
progress: @escaping Progress,
52+
completion: @escaping Completion
4653
)
4754
}
4855

56+
extension DownloaderError: LocalizedError {
57+
public var errorDescription: String? {
58+
switch self {
59+
case .clientError(let error):
60+
return (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
61+
case .serverError(let statusCode):
62+
return "invalid status code \(statusCode)"
63+
case .fileSystemError(let error):
64+
return (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
65+
}
66+
}
67+
}
68+
4969
/// A `Downloader` conformance that uses Foundation's `URLSession`.
5070
public final class FoundationDownloader: NSObject, Downloader {
5171

@@ -56,8 +76,8 @@ public final class FoundationDownloader: NSObject, Downloader {
5676
fileprivate struct Download {
5777
let task: URLSessionDownloadTask
5878
let destination: AbsolutePath
59-
let progress: (Double) -> Void
60-
let completion: (Result<Void, DownloaderError>) -> Void
79+
let progress: Downloader.Progress
80+
let completion: Downloader.Completion
6181
}
6282

6383
/// The `URLSession` used for all downloads.
@@ -89,8 +109,8 @@ public final class FoundationDownloader: NSObject, Downloader {
89109
public func downloadFile(
90110
at url: Foundation.URL,
91111
to destination: AbsolutePath,
92-
progress: @escaping (Double) -> Void,
93-
completion: @escaping (Result<Void, DownloaderError>) -> Void
112+
progress: @escaping Downloader.Progress,
113+
completion: @escaping Downloader.Completion
94114
) {
95115
queue.addOperation {
96116
let task = self.session.downloadTask(with: url)
@@ -114,8 +134,9 @@ extension FoundationDownloader: URLSessionDownloadDelegate {
114134
totalBytesExpectedToWrite: Int64
115135
) {
116136
let download = self.download(for: downloadTask)
117-
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
118-
download.notifyProgress(progress)
137+
let totalBytesToDownload = totalBytesExpectedToWrite != NSURLSessionTransferSizeUnknown ?
138+
totalBytesExpectedToWrite : nil
139+
download.notifyProgress(bytesDownloaded: totalBytesWritten, totalBytesToDownload: totalBytesToDownload)
119140
}
120141

121142
public func urlSession(
@@ -162,9 +183,9 @@ extension FoundationDownloader {
162183
}
163184

164185
extension FoundationDownloader.Download {
165-
func notifyProgress(_ progress: Double) {
186+
func notifyProgress(bytesDownloaded: Int64, totalBytesToDownload: Int64?) {
166187
DispatchQueue.global().async {
167-
self.progress(progress)
188+
self.progress(bytesDownloaded, totalBytesToDownload)
168189
}
169190
}
170191

Tests/TSCUtilityTests/DownloaderTests.swift

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ class DownloaderTests: XCTestCase {
3535
let successExpectation = XCTestExpectation(description: "success")
3636
MockURLProtocol.notifyDidStartLoading(for: url, completion: { didStartLoadingExpectation.fulfill() })
3737

38-
downloader.downloadFile(at: url, to: destination, progress: { progress in
39-
if progress.spm_isAlmostEqual(to: 0.5) {
38+
downloader.downloadFile(at: url, to: destination, progress: { bytesDownloaded, totalBytesToDownload in
39+
switch (bytesDownloaded, totalBytesToDownload) {
40+
case (512, 1024):
4041
progress50Expectation.fulfill()
41-
} else if progress.spm_isAlmostEqual(to: 1) {
42+
case (1024, 1024):
4243
progress100Expectation.fulfill()
43-
} else {
44+
default:
4445
XCTFail("unexpected progress")
4546
}
4647
}, completion: { result in
@@ -85,10 +86,11 @@ class DownloaderTests: XCTestCase {
8586
let errorExpectation = XCTestExpectation(description: "error")
8687
MockURLProtocol.notifyDidStartLoading(for: url, completion: { didStartLoadingExpectation.fulfill() })
8788

88-
downloader.downloadFile(at: url, to: AbsolutePath("/"), progress: { progress in
89-
if progress.spm_isAlmostEqual(to: 0.5) {
89+
downloader.downloadFile(at: url, to: AbsolutePath("/"), progress: { bytesDownloaded, totalBytesToDownload in
90+
switch (bytesDownloaded, totalBytesToDownload) {
91+
case (512, 1024):
9092
progress50Expectation.fulfill()
91-
} else {
93+
default:
9294
XCTFail("unexpected progress")
9395
}
9496
}, completion: { result in
@@ -135,7 +137,7 @@ class DownloaderTests: XCTestCase {
135137
let errorExpectation = XCTestExpectation(description: "error")
136138
MockURLProtocol.notifyDidStartLoading(for: url, completion: { didStartLoadingExpectation.fulfill() })
137139

138-
downloader.downloadFile(at: url, to: AbsolutePath("/"), progress: { progress in
140+
downloader.downloadFile(at: url, to: AbsolutePath("/"), progress: { _, _ in
139141
XCTFail("unexpected progress")
140142
}, completion: { result in
141143
switch result {
@@ -174,7 +176,7 @@ class DownloaderTests: XCTestCase {
174176
let errorExpectation = XCTestExpectation(description: "error")
175177
MockURLProtocol.notifyDidStartLoading(for: url, completion: { didStartLoadingExpectation.fulfill() })
176178

177-
downloader.downloadFile(at: url, to: AbsolutePath("/"), progress: { _ in }, completion: { result in
179+
downloader.downloadFile(at: url, to: AbsolutePath("/"), progress: { _, _ in }, completion: { result in
178180
switch result {
179181
case .success:
180182
XCTFail("unexpected success")

0 commit comments

Comments
 (0)