From 7e23079e5462c019635fbc802397bd2cc504f9cd Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Wed, 12 Apr 2023 20:26:09 +0200 Subject: [PATCH 1/2] Pass request `Task` to `FileDownloadDelegate` `reportHead` and `reportProgress` closures ### Motivation If the HTTP response status is not 2xx or the file being downloaded becomes too big, it is desirable to cancel the file download eagerly. This is currently quite hard because the `reportHead` and `reportProgress` closures do not have direct access to the `HTTPClient.Task`. ### Modifications - Pass `HTTPClient.Task` additionally to `reportHead` and `reportProgress` closures ### Result A file download can now easily be cancelled at any time. --- .../FileDownloadDelegate.swift | 88 +++++++++++++------ 1 file changed, 62 insertions(+), 26 deletions(-) diff --git a/Sources/AsyncHTTPClient/FileDownloadDelegate.swift b/Sources/AsyncHTTPClient/FileDownloadDelegate.swift index c328c7211..ccab2a1b3 100644 --- a/Sources/AsyncHTTPClient/FileDownloadDelegate.swift +++ b/Sources/AsyncHTTPClient/FileDownloadDelegate.swift @@ -31,12 +31,42 @@ public final class FileDownloadDelegate: HTTPClientResponseDelegate { private let filePath: String private(set) var fileIOThreadPool: NIOThreadPool? - private let reportHead: ((HTTPResponseHead) -> Void)? - private let reportProgress: ((Progress) -> Void)? + private let reportHead: ((HTTPClient.Task, HTTPResponseHead) -> Void)? + private let reportProgress: ((HTTPClient.Task, Progress) -> Void)? private var fileHandleFuture: EventLoopFuture? private var writeFuture: EventLoopFuture? + /// Initializes a new file download delegate. + /// + /// - parameters: + /// - path: Path to a file you'd like to write the download to. + /// - pool: A thread pool to use for asynchronous file I/O. If nil, a shared thread pool will be used. Defaults to nil. + /// - reportHead: A closure called when the response head is available. + /// - reportProgress: A closure called when a body chunk has been downloaded, with + /// the total byte count and download byte count passed to it as arguments. The callbacks + /// will be invoked in the same threading context that the delegate itself is invoked, + /// as controlled by `EventLoopPreference`. + public init( + path: String, + pool: NIOThreadPool? = nil, + reportHead: ((HTTPClient.Task, HTTPResponseHead) -> Void)? = nil, + reportProgress: ((HTTPClient.Task, Progress) -> Void)? = nil + ) throws { + if let pool = pool { + self.fileIOThreadPool = pool + } else { + // we should use the shared thread pool from the HTTPClient which + // we will get from the `HTTPClient.Task` + self.fileIOThreadPool = nil + } + + self.filePath = path + + self.reportHead = reportHead + self.reportProgress = reportProgress + } + /// Initializes a new file download delegate. /// /// - parameters: @@ -53,7 +83,20 @@ public final class FileDownloadDelegate: HTTPClientResponseDelegate { reportHead: ((HTTPResponseHead) -> Void)? = nil, reportProgress: ((Progress) -> Void)? = nil ) throws { - try self.init(path: path, pool: .some(pool), reportHead: reportHead, reportProgress: reportProgress) + try self.init( + path: path, + pool: .some(pool), + reportHead: reportHead.map { reportHead in + return { _, head in + reportHead(head) + } + }, + reportProgress: reportProgress.map { reportProgress in + return { _, head in + reportProgress(head) + } + } + ) } /// Initializes a new file download delegate and uses the shared thread pool of the ``HTTPClient`` for file I/O. @@ -70,34 +113,27 @@ public final class FileDownloadDelegate: HTTPClientResponseDelegate { reportHead: ((HTTPResponseHead) -> Void)? = nil, reportProgress: ((Progress) -> Void)? = nil ) throws { - try self.init(path: path, pool: nil, reportHead: reportHead, reportProgress: reportProgress) - } - - private init( - path: String, - pool: NIOThreadPool?, - reportHead: ((HTTPResponseHead) -> Void)? = nil, - reportProgress: ((Progress) -> Void)? = nil - ) throws { - if let pool = pool { - self.fileIOThreadPool = pool - } else { - // we should use the shared thread pool from the HTTPClient which - // we will get from the `HTTPClient.Task` - self.fileIOThreadPool = nil - } - - self.filePath = path - - self.reportHead = reportHead - self.reportProgress = reportProgress + try self.init( + path: path, + pool: nil, + reportHead: reportHead.map { reportHead in + return { _, head in + reportHead(head) + } + }, + reportProgress: reportProgress.map { reportProgress in + return { _, head in + reportProgress(head) + } + } + ) } public func didReceiveHead( task: HTTPClient.Task, _ head: HTTPResponseHead ) -> EventLoopFuture { - self.reportHead?(head) + self.reportHead?(task, head) if let totalBytesString = head.headers.first(name: "Content-Length"), let totalBytes = Int(totalBytesString) { @@ -121,7 +157,7 @@ public final class FileDownloadDelegate: HTTPClientResponseDelegate { }() let io = NonBlockingFileIO(threadPool: threadPool) self.progress.receivedBytes += buffer.readableBytes - self.reportProgress?(self.progress) + self.reportProgress?(task, self.progress) let writeFuture: EventLoopFuture if let fileHandleFuture = self.fileHandleFuture { From 70e30a4ec81561602c402405bef360cf616ea9c4 Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Wed, 12 Apr 2023 20:29:42 +0200 Subject: [PATCH 2/2] SwiftFormat --- Sources/AsyncHTTPClient/FileDownloadDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AsyncHTTPClient/FileDownloadDelegate.swift b/Sources/AsyncHTTPClient/FileDownloadDelegate.swift index ccab2a1b3..9a351f3c1 100644 --- a/Sources/AsyncHTTPClient/FileDownloadDelegate.swift +++ b/Sources/AsyncHTTPClient/FileDownloadDelegate.swift @@ -66,7 +66,7 @@ public final class FileDownloadDelegate: HTTPClientResponseDelegate { self.reportHead = reportHead self.reportProgress = reportProgress } - + /// Initializes a new file download delegate. /// /// - parameters: