Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### 2025-08-01

- Fix progress notification for `uploadFile`.
- Fix error (only visible in logs) related to `chunkedMode` and `httpBodyStream`.

## 1.0.0

### 2025-04-14
Expand Down
18 changes: 17 additions & 1 deletion IONFileTransferLib/Delegates/IONFLTRUploadDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import Foundation
/// - Note: This class conforms to `URLSessionTaskDelegate` and `URLSessionDataDelegate` to handle task-specific events.
class IONFLTRUploadDelegate: IONFLTRBaseDelegate {

/// the URL pointing to the file to upload
let fileURL: URL

/// The total number of bytes sent during the upload.
private var totalBytesSent: Int = 0

Expand All @@ -20,7 +23,9 @@ class IONFLTRUploadDelegate: IONFLTRBaseDelegate {
/// - Parameters:
/// - publisher: The publisher used to send progress and success updates.
/// - disableRedirects: A flag indicating whether HTTP redirects should be disabled.
override init(publisher: IONFLTRPublisher, disableRedirects: Bool) {
/// - fileURL: the URL pointing to the file to upload
init(publisher: IONFLTRPublisher, disableRedirects: Bool, fileURL: URL) {
self.fileURL = fileURL
super.init(publisher: publisher, disableRedirects: disableRedirects)
}
}
Expand Down Expand Up @@ -100,4 +105,15 @@ extension IONFLTRUploadDelegate: URLSessionDataDelegate {
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
receivedData.append(data)
}

/// Handles sending a body stream of the file to upload. Relevant for chunkedMode=true
///
/// - Parameters:
/// - session: The `URLSession` containing the data task.
/// - task: The `URLSessionTask` that wiill get the input stream
func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) {
print("needNewBodyStream")
let stream = InputStream(fileAtPath: fileURL.path)
completionHandler(stream)
}
}
26 changes: 24 additions & 2 deletions IONFileTransferLib/Helpers/IONFLTRURLRequestHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,21 @@ class IONFLTRURLRequestHelper {
request.setValue(mimeType, forHTTPHeaderField: "Content-Type")
}
}

let fileLength = getFileSize(from: fileURL)

if uploadOptions.chunkedMode {
let inputStream = InputStream(fileAtPath: fileURL.path)
request.httpBodyStream = inputStream
request.setContentLengthHeader(httpOptions: httpOptions, contentLength: fileLength)
return (request, fileURL)
} else if isMultipartUpload {
let httpBody = try createMultipartBody(uploadOptions: uploadOptions, fileURL: fileURL, fileHelper: fileHelper, boundary: boundary)
let tempFileURL = FileManager.default.temporaryDirectory.appendingPathComponent("multipart_\(UUID().uuidString).tmp")
request.setContentLengthHeader(httpOptions: httpOptions, contentLength: Int64(httpBody.count))
try httpBody.write(to: tempFileURL)
return (request, tempFileURL)
}

request.setContentLengthHeader(httpOptions: httpOptions, contentLength: fileLength)
return (request, fileURL)
}

Expand Down Expand Up @@ -161,6 +164,25 @@ class IONFLTRURLRequestHelper {
body.append("--\(boundary)--\(lineEnd)".data(using: .utf8)!)
return body
}

private func getFileSize(from url: URL) -> Int64 {
do {
return try FileManager.default.attributesOfItem(atPath: url.path)[.size] as! Int64
} catch {
return 0
}
}
}

private extension URLRequest {
mutating func setContentLengthHeader(
httpOptions: IONFLTRHttpOptions,
contentLength: Int64
) {
if (!httpOptions.headers.keys.contains("Content-Length") && contentLength > 0) {
setValue(String(contentLength), forHTTPHeaderField: "Content-Length")
}
}
}

private extension Data {
Expand Down
3 changes: 2 additions & 1 deletion IONFileTransferLib/IONFLTRManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ public class IONFLTRManager: NSObject {
let publisher = IONFLTRPublisher()
let delegate = IONFLTRUploadDelegate(
publisher: publisher,
disableRedirects: httpOptions.disableRedirects
disableRedirects: httpOptions.disableRedirects,
fileURL: fileURL
)
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
if uploadOptions.chunkedMode {
Expand Down
35 changes: 33 additions & 2 deletions IONFileTransferLibTests/Delegates/IONFLTRUploadDelegateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ final class IONFLTRUploadDelegateTests: XCTestCase {
mockPublisher = MockPublisher()
delegate = IONFLTRUploadDelegate(
publisher: mockPublisher,
disableRedirects: false
disableRedirects: false,
fileURL: URL(string: "somefile_path/not_relevat_for_test.txt")!
)
}

Expand Down Expand Up @@ -76,7 +77,8 @@ final class IONFLTRUploadDelegateTests: XCTestCase {
func testRedirection_shouldBeCancelledWhenDisabled() {
delegate = IONFLTRUploadDelegate(
publisher: mockPublisher,
disableRedirects: true
disableRedirects: true,
fileURL: URL(string: "somefile_path/not_relevat_for_test.txt")!
)

let task = URLSession.shared.dataTask(with: URL(string: "https://example.com")!)
Expand Down Expand Up @@ -181,4 +183,33 @@ final class IONFLTRUploadDelegateTests: XCTestCase {
XCTAssertEqual(mockPublisher.successCalled?.2, responseBody)
XCTAssertEqual(mockPublisher.successCalled?.3["X-Custom"], "value")
}

func testBodyStream_retrievedFromTestFile() {
let testFileURL = FileManager.default.temporaryDirectory.appendingPathComponent("testFile.txt")
try? "Test content".write(to: testFileURL, atomically: true, encoding: .utf8)
delegate = IONFLTRUploadDelegate(
publisher: mockPublisher,
disableRedirects: true,
fileURL: testFileURL
)
let expectation = self.expectation(description: "stream is correct")

let task = URLSession.shared.dataTask(with: URL(string: "https://example.com")!)
delegate.urlSession(
URLSession.shared,
task: task,
needNewBodyStream: { stream in
XCTAssertNotNil(stream)
stream?.open()
var buffer = [UInt8](repeating: 0, count: 100)
let bytesRead = stream!.read(&buffer, maxLength: buffer.count)
XCTAssertGreaterThan(bytesRead, 0)
let outputString = String(bytes: buffer[0..<bytesRead], encoding: .utf8)
XCTAssertEqual(outputString, "Test content")
stream?.close()
expectation.fulfill()
}
)
waitForExpectations(timeout: 1)
}
}
16 changes: 4 additions & 12 deletions IONFileTransferLibTests/Helpers/IONFLTRURLRequestHelperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,20 +134,10 @@ class IONFLTRURLRequestHelperTests: XCTestCase {
fileHelper: fileHelper
)

let stream = configuredRequest.httpBodyStream
stream?.open()
let bufferSize = 1024
var buffer = [UInt8](repeating: 0, count: bufferSize)
var data = Data()
while let bytesRead = stream?.read(&buffer, maxLength: bufferSize), bytesRead > 0 {
data.append(buffer, count: bytesRead)
}
stream?.close()

let fileData = try Data(contentsOf: testFileURL)

XCTAssertEqual(data, fileData)
XCTAssertEqual(fileURL, testFileURL)
XCTAssertNil(configuredRequest.httpBodyStream)
XCTAssertEqual(configuredRequest.value(forHTTPHeaderField: "Content-Length"), String(fileData.count))
}

func testConfigureRequestForUpload_withMultipartUpload() throws {
Expand All @@ -169,7 +159,9 @@ class IONFLTRURLRequestHelperTests: XCTestCase {
fileHelper: fileHelper
)

let fileData = try Data(contentsOf: testFileURL)
XCTAssertEqual(configuredRequest.value(forHTTPHeaderField: "Content-Type")?.contains("multipart/form-data"), true)
XCTAssertTrue(Int(configuredRequest.value(forHTTPHeaderField: "Content-Length")!)! > fileData.count)
XCTAssertTrue(FileManager.default.fileExists(atPath: tempFileURL.path))
try? FileManager.default.removeItem(at: tempFileURL)
}
Expand Down
3 changes: 1 addition & 2 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ default_platform(:ios)
platform :ios do
desc "Lane to run the unit tests"
lane :unit_tests do
run_tests(device: "iPhone 8", scheme: "IONFileTransferLib",
slack_url: ENV['SLACK_WEBHOOK'])
run_tests(scheme: "IONFileTransferLib")
end

desc "Code coverage"
Expand Down
Loading