If that helps anyone, here is my code to speed up the download. It splits the file download in a number of file parts downloads, which uses the available bandwidth more efficiently. It still feels wrong to have to do that...
The final usage is like:
// task.pause is not implemented yet
let task = FileDownloadManager.shared.download(from:someUrl)
task.delegate = self
task.resume()
and here's the code:
/// Holds a weak reverence
class Weak<T: AnyObject> {
weak var value : T?
init (value: T) {
self.value = value
}
}
enum DownloadError: Error {
case missingData
}
/// Represents the download of one part of the file
fileprivate class DownloadTask {
/// The position (included) of the first byte
let startOffset: Int64
/// The position (not included) of the last byte
let endOffset: Int64
/// The byte length of the part
var size: Int64 { return endOffset - startOffset }
/// The number of bytes currently written
var bytesWritten: Int64 = 0
/// The URL task corresponding to the download
let request: URLSessionDownloadTask
/// The disk location of the saved file
var didWriteTo: URL?
init(for url: URL, from start: Int64, to end: Int64, in session: URLSession) {
startOffset = start
endOffset = end
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.allHTTPHeaderFields?["Range"] = "bytes=\(start)-\(end - 1)"
self.request = session.downloadTask(with: request)
}
}
/// Represents the download of a file (that is done in multi parts)
class MultiPartsDownloadTask {
weak var delegate: MultiPartDownloadTaskDelegate?
/// the current progress, from 0 to 1
var progress: CGFloat {
var total: Int64 = 0
var written: Int64 = 0
parts.forEach({ part in
total += part.size
written += part.bytesWritten
})
guard total > 0 else { return 0 }
return CGFloat(written) / CGFloat(total)
}
fileprivate var parts = [DownloadTask]()
fileprivate var contentLength: Int64?
fileprivate let url: URL
private var session: URLSession
private var isStoped = false
private var isResumed = false
/// When the download started
private var startedAt: Date
/// An estimate on how long left before the download is over
var remainingTimeEstimate: CGFloat {
let progress = self.progress
guard progress > 0 else { return CGFloat.greatestFiniteMagnitude }
return CGFloat(Date().timeIntervalSince(startedAt)) / progress * (1 - progress)
}
fileprivate init(from url: URL, in session: URLSession) {
self.url = url
self.session = session
startedAt = Date()
getRemoteResourceSize().then { [weak self] size -> Void in
guard let wself = self else { return }
wself.contentLength = size
wself.createDownloadParts()
if wself.isResumed {
wself.resume()
}
}.catch { [weak self] error in
guard let wself = self else { return }
wself.isStoped = true
}
}
/// Start the download
func resume() {
guard !isStoped else { return }
startedAt = Date()
isResumed = true
parts.forEach({ $0.request.resume() })
}
/// Cancels the download
func cancel() {
guard !isStoped else { return }
parts.forEach({ $0.request.cancel() })
}
/// Fetch the file size of a remote resource
private func getRemoteResourceSize(completion: @escaping (Int64?, Error?) -> Void) {
var headRequest = URLRequest(url: url)
headRequest.httpMethod = "HEAD"
session.dataTask(with: headRequest, completionHandler: { (data, response, error) in
if let error = error {
completion(nil, error)
return
}
guard let expectedContentLength = response?.expectedContentLength else {
completion(nil, FileCacheError.sizeNotAvailableForRemoteResource)
return
}
completion(expectedContentLength, nil)
}).resume()
}
/// Split the download request into multiple request to use more bandwidth
private func createDownloadParts() {
guard let size = contentLength else { return }
let numberOfRequests = 20
for i in 0..<numberOfRequests {
let start = Int64(ceil(CGFloat(Int64(i) * size) / CGFloat(numberOfRequests)))
let end = Int64(ceil(CGFloat(Int64(i + 1) * size) / CGFloat(numberOfRequests)))
parts.append(DownloadTask(for: url, from: start, to: end, in: session))
}
}
fileprivate func didFail(_ error: Error) {
cancel()
delegate?.didFail(self, error: error)
}
fileprivate func didFinishOnePart() {
if parts.filter({ $0.didWriteTo != nil }).count == parts.count {
mergeFiles()
}
}
/// Put together the download files
private func mergeFiles() {
let ext = self.url.pathExtension
let destination = Constants.tempDirectory
.appendingPathComponent("\(String.random(ofLength: 5))")
.appendingPathExtension(ext)
do {
let partLocations = parts.flatMap({ $0.didWriteTo })
try FileManager.default.merge(files: partLocations, to: destination)
delegate?.didFinish(self, didFinishDownloadingTo: destination)
for partLocation in partLocations {
do {
try FileManager.default.removeItem(at: partLocation)
} catch {
report(error)
}
}
} catch {
delegate?.didFail(self, error: error)
}
}
deinit {
FileDownloadManager.shared.tasks = FileDownloadManager.shared.tasks.filter({
$0.value !== self
})
}
}
protocol MultiPartDownloadTaskDelegate: class {
/// Called when the download progress changed
func didProgress(
_ downloadTask: MultiPartsDownloadTask
)
/// Called when the download finished succesfully
func didFinish(
_ downloadTask: MultiPartsDownloadTask,
didFinishDownloadingTo location: URL
)
/// Called when the download failed
func didFail(_ downloadTask: MultiPartsDownloadTask, error: Error)
}
/// Manage files downloads
class FileDownloadManager: NSObject {
static let shared = FileDownloadManager()
private var session: URLSession!
fileprivate var tasks = [Weak<MultiPartsDownloadTask>]()
private override init() {
super.init()
let config = URLSessionConfiguration.default
config.httpMaximumConnectionsPerHost = 50
session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
}
/// Create a task to download a file
func download(from url: URL) -> MultiPartsDownloadTask {
let task = MultiPartsDownloadTask(from: url, in: session)
tasks.append(Weak(value: task))
return task
}
/// Returns the download task that correspond to the URL task
fileprivate func match(request: URLSessionTask) -> (MultiPartsDownloadTask, DownloadTask)? {
for wtask in tasks {
if let task = wtask.value {
for part in task.parts {
if part.request == request {
return (task, part)
}
}
}
}
return nil
}
}
extension FileDownloadManager: URLSessionDownloadDelegate {
public func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64
) {
guard let x = match(request: downloadTask) else { return }
let multiPart = x.0
let part = x.1
part.bytesWritten = totalBytesWritten
multiPart.delegate?.didProgress(multiPart)
}
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL
) {
guard let x = match(request: downloadTask) else { return }
let multiPart = x.0
let part = x.1
let ext = multiPart.url.pathExtension
let destination = Constants.tempDirectory
.appendingPathComponent("\(String.random(ofLength: 5))")
.appendingPathExtension(ext)
do {
try FileManager.default.moveItem(at: location, to: destination)
} catch {
multiPart.didFail(error)
return
}
part.didWriteTo = destination
multiPart.didFinishOnePart()
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let error = error, let multipart = match(request: task)?.0 else { return }
multipart.didFail(error)
}
}
extension FileManager {
/// Merge the files into one (without deleting the files)
func merge(files: [URL], to destination: URL, chunkSize: Int = 1000000) throws {
FileManager.default.createFile(atPath: destination.path, contents: nil, attributes: nil)
let writer = try FileHandle(forWritingTo: destination)
try files.forEach({ partLocation in
let reader = try FileHandle(forReadingFrom: partLocation)
var data = reader.readData(ofLength: chunkSize)
while data.count > 0 {
writer.write(data)
data = reader.readData(ofLength: chunkSize)
}
reader.closeFile()
})
writer.closeFile()
}
}
config.httpMaximumConnectionsPerHost = 20
. Change host to 1 and then try – Wormhole