Skip to content
27 changes: 25 additions & 2 deletions Sources/AsyncHTTPClient/FileDownloadDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,34 @@ import NIOPosix
/// Handles a streaming download to a given file path, allowing headers and progress to be reported.
public final class FileDownloadDelegate: HTTPClientResponseDelegate {
/// The response type for this delegate: the total count of bytes as reported by the response
/// "Content-Length" header (if available) and the count of bytes downloaded.
/// "Content-Length" header (if available), the count of bytes downloaded, and the
/// response head.
public struct Progress: Sendable {
public var totalBytes: Int?
public var receivedBytes: Int

public var head: HTTPResponseHead {
get {
assert(self._head != nil)
return self._head!
}
set {
self._head = newValue
}
}

fileprivate var _head: HTTPResponseHead? = nil

internal init(totalBytes: Int? = nil, receivedBytes: Int) {
self.totalBytes = totalBytes
self.receivedBytes = receivedBytes
}
}

private var progress = Progress(totalBytes: nil, receivedBytes: 0)
private var progress = Progress(
totalBytes: nil,
receivedBytes: 0
)

public typealias Response = Progress

Expand Down Expand Up @@ -133,6 +154,8 @@ public final class FileDownloadDelegate: HTTPClientResponseDelegate {
task: HTTPClient.Task<Response>,
_ head: HTTPResponseHead
) -> EventLoopFuture<Void> {
self.progress._head = head

self.reportHead?(task, head)

if let totalBytesString = head.headers.first(name: "Content-Length"),
Expand Down
46 changes: 28 additions & 18 deletions Tests/AsyncHTTPClientTests/HTTPClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -729,51 +729,57 @@ final class HTTPClientTests: XCTestCaseHTTPClientTestsBaseClass {
var request = try Request(url: self.defaultHTTPBinURLPrefix + "events/10/content-length")
request.headers.add(name: "Accept", value: "text/event-stream")

let progress =
try TemporaryFileHelpers.withTemporaryFilePath { path -> FileDownloadDelegate.Progress in
let response =
try TemporaryFileHelpers.withTemporaryFilePath { path -> FileDownloadDelegate.Response in
let delegate = try FileDownloadDelegate(path: path)

let progress = try self.defaultClient.execute(
let response = try self.defaultClient.execute(
request: request,
delegate: delegate
)
.wait()

try XCTAssertEqual(50, TemporaryFileHelpers.fileSize(path: path))

return progress
return response
}

XCTAssertEqual(50, progress.totalBytes)
XCTAssertEqual(50, progress.receivedBytes)
XCTAssertEqual(.ok, response.head.status)
XCTAssertEqual("50", response.head.headers.first(name: "content-length"))

XCTAssertEqual(50, response.totalBytes)
XCTAssertEqual(50, response.receivedBytes)
}

func testFileDownloadError() throws {
var request = try Request(url: self.defaultHTTPBinURLPrefix + "not-found")
request.headers.add(name: "Accept", value: "text/event-stream")

let progress =
try TemporaryFileHelpers.withTemporaryFilePath { path -> FileDownloadDelegate.Progress in
let response =
try TemporaryFileHelpers.withTemporaryFilePath { path -> FileDownloadDelegate.Response in
let delegate = try FileDownloadDelegate(
path: path,
reportHead: {
XCTAssertEqual($0.status, .notFound)
}
)

let progress = try self.defaultClient.execute(
let response = try self.defaultClient.execute(
request: request,
delegate: delegate
)
.wait()

XCTAssertFalse(TemporaryFileHelpers.fileExists(path: path))

return progress
return response
}

XCTAssertEqual(nil, progress.totalBytes)
XCTAssertEqual(0, progress.receivedBytes)
XCTAssertEqual(.notFound, response.head.status)
XCTAssertFalse(response.head.headers.contains(name: "content-length"))

XCTAssertEqual(nil, response.totalBytes)
XCTAssertEqual(0, response.receivedBytes)
}

func testFileDownloadCustomError() throws {
Expand Down Expand Up @@ -3910,23 +3916,27 @@ final class HTTPClientTests: XCTestCaseHTTPClientTestsBaseClass {
var request = try Request(url: self.defaultHTTPBinURLPrefix + "chunked")
request.headers.add(name: "Accept", value: "text/event-stream")

let progress =
try TemporaryFileHelpers.withTemporaryFilePath { path -> FileDownloadDelegate.Progress in
let response =
try TemporaryFileHelpers.withTemporaryFilePath { path -> FileDownloadDelegate.Response in
let delegate = try FileDownloadDelegate(path: path)

let progress = try self.defaultClient.execute(
let response = try self.defaultClient.execute(
request: request,
delegate: delegate
)
.wait()

try XCTAssertEqual(50, TemporaryFileHelpers.fileSize(path: path))

return progress
return response
}

XCTAssertEqual(nil, progress.totalBytes)
XCTAssertEqual(50, progress.receivedBytes)
XCTAssertEqual(.ok, response.head.status)
XCTAssertEqual("chunked", response.head.headers.first(name: "transfer-encoding"))
XCTAssertFalse(response.head.headers.contains(name: "content-length"))

XCTAssertEqual(nil, response.totalBytes)
XCTAssertEqual(50, response.receivedBytes)
}

func testCloseWhileBackpressureIsExertedIsFine() throws {
Expand Down