From a835fa493fa066b6533c41bf11947b1fbbfd03f4 Mon Sep 17 00:00:00 2001 From: Ely Deckers Date: Sat, 27 Mar 2021 14:44:11 +0100 Subject: [PATCH] chore: wip add iOS code for raw request --- ios/BlobCourier.m | 3 + ios/BlobCourier.swift | 29 ++++++ ios/BlobCourier.xcodeproj/project.pbxproj | 24 +++++ ios/Send/BlobSender.swift | 112 ++++++++++++++++++++++ ios/Send/SendParameters.swift | 32 +++++++ ios/Send/SenderDelegate.swift | 109 +++++++++++++++++++++ ios/Send/SenderParameterFactory.swift | 64 +++++++++++++ react-native-blob-courier.podspec | 2 +- 8 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 ios/Send/BlobSender.swift create mode 100644 ios/Send/SendParameters.swift create mode 100644 ios/Send/SenderDelegate.swift create mode 100644 ios/Send/SenderParameterFactory.swift diff --git a/ios/BlobCourier.m b/ios/BlobCourier.m index 654e1206..888b5a09 100644 --- a/ios/BlobCourier.m +++ b/ios/BlobCourier.m @@ -14,6 +14,9 @@ @interface RCT_EXTERN_MODULE(BlobCourier, NSObject) RCT_EXTERN_METHOD(fetchBlob:(NSDictionary *)input withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(sendBlob:(NSDictionary *)input + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(uploadBlob:(NSDictionary *)input withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) diff --git a/ios/BlobCourier.swift b/ios/BlobCourier.swift index 09120c2a..0c459ad6 100644 --- a/ios/BlobCourier.swift +++ b/ios/BlobCourier.swift @@ -64,6 +64,35 @@ open class BlobCourier: NSObject { } } + @objc(sendBlob:withResolver:withRejecter:) + func sendBlob( + input: NSDictionary, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + DispatchQueue.global(qos: .background).async { + do { + let errorOrParameters = SenderParameterFactory.fromInput(input: input) + + if case .failure(let error) = errorOrParameters { reject(error.code, error.message, error.error) } + guard case .success(let parameters) = errorOrParameters else { return } + + let result = BlobSender.sendBlobFromValidatedParameters(parameters: parameters) + + switch result { + case .success(let success): + resolve(success) + case .failure(let error): + reject(error.code, error.message, error.error) + } + } catch { + let unexpectedError = Errors.createUnexpectedError(error: error) + + reject(unexpectedError.code, unexpectedError.message, unexpectedError.error) + } + } + } + @objc(uploadBlob:withResolver:withRejecter:) func uploadBlob( input: NSDictionary, diff --git a/ios/BlobCourier.xcodeproj/project.pbxproj b/ios/BlobCourier.xcodeproj/project.pbxproj index 31b39b55..06eadb37 100644 --- a/ios/BlobCourier.xcodeproj/project.pbxproj +++ b/ios/BlobCourier.xcodeproj/project.pbxproj @@ -33,6 +33,10 @@ B96A993E25BE166900F42B65 /* UploaderParameterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B96A993B25BE166900F42B65 /* UploaderParameterFactory.swift */; }; B96A993F25BE166900F42B65 /* UploadParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = B96A993C25BE166900F42B65 /* UploadParameters.swift */; }; B96A994025BE166900F42B65 /* UploadParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = B96A993C25BE166900F42B65 /* UploadParameters.swift */; }; + B9854CDA261269B10054135B /* BlobSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9854CD5261269350054135B /* BlobSender.swift */; }; + B9854CDD261269B60054135B /* SenderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9854CD6261269350054135B /* SenderDelegate.swift */; }; + B9854CE0261269BB0054135B /* SendParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9854CD8261269350054135B /* SendParameters.swift */; }; + B9854CE3261269C10054135B /* SenderParameterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9854CD9261269350054135B /* SenderParameterFactory.swift */; }; B9AF759125DF263E00B68816 /* CancelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9AF756E25DF203800B68816 /* CancelController.swift */; }; B9AF759925DF264E00B68816 /* RequestCanceller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9AF756F25DF203800B68816 /* RequestCanceller.swift */; }; B9AF75BF25DF29DF00B68816 /* CancelParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9AF75BD25DF29DF00B68816 /* CancelParameters.swift */; }; @@ -87,6 +91,10 @@ B96A96FF25BDEA7D00F42B65 /* DownloaderParameterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloaderParameterFactory.swift; sourceTree = ""; }; B96A993B25BE166900F42B65 /* UploaderParameterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderParameterFactory.swift; sourceTree = ""; }; B96A993C25BE166900F42B65 /* UploadParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadParameters.swift; sourceTree = ""; }; + B9854CD5261269350054135B /* BlobSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlobSender.swift; sourceTree = ""; }; + B9854CD6261269350054135B /* SenderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SenderDelegate.swift; sourceTree = ""; }; + B9854CD8261269350054135B /* SendParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendParameters.swift; sourceTree = ""; }; + B9854CD9261269350054135B /* SenderParameterFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SenderParameterFactory.swift; sourceTree = ""; }; B9AF756E25DF203800B68816 /* CancelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelController.swift; sourceTree = ""; }; B9AF756F25DF203800B68816 /* RequestCanceller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestCanceller.swift; sourceTree = ""; }; B9AF75BD25DF29DF00B68816 /* CancelParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CancelParameters.swift; sourceTree = ""; }; @@ -162,6 +170,7 @@ B96A888C25BC566300F42B65 /* Fetch */, B96A88AE25BC5EB800F42B65 /* Progress */, B96A945D25BD86AB00F42B65 /* React */, + B9854CD4261269350054135B /* Send */, B96A889425BC567000F42B65 /* Upload */, B96829E6254EC872002B4B04 /* BlobCourierTests */, 134814211AA4EA7D00B7C361 /* Products */, @@ -228,6 +237,17 @@ path = React; sourceTree = ""; }; + B9854CD4261269350054135B /* Send */ = { + isa = PBXGroup; + children = ( + B9854CD5261269350054135B /* BlobSender.swift */, + B9854CD6261269350054135B /* SenderDelegate.swift */, + B9854CD8261269350054135B /* SendParameters.swift */, + B9854CD9261269350054135B /* SenderParameterFactory.swift */, + ); + path = Send; + sourceTree = ""; + }; B9AF756D25DF203800B68816 /* Cancel */ = { isa = PBXGroup; children = ( @@ -402,6 +422,7 @@ B96A954A25BD8D3B00F42B65 /* BlobCourierEventEmitter.swift in Sources */, F4FF95D7245B92E800C19C63 /* BlobCourier.swift in Sources */, B96A993D25BE166900F42B65 /* UploaderParameterFactory.swift in Sources */, + B9854CDA261269B10054135B /* BlobSender.swift in Sources */, B96829DF254EC736002B4B04 /* DownloaderDelegate.swift in Sources */, B96A8FEC25BCB87E00F42B65 /* BlobCourierEventEmitter.m in Sources */, B96A8F0925BCB1DA00F42B65 /* BlobCourier.m in Sources */, @@ -410,12 +431,15 @@ B96A8E3925BCA76300F42B65 /* BlobCourierDelayedEventEmitter.swift in Sources */, B9AF75BF25DF29DF00B68816 /* CancelParameters.swift in Sources */, B96A889625BC567000F42B65 /* BlobUploader.swift in Sources */, + B9854CE3261269C10054135B /* SenderParameterFactory.swift in Sources */, B96A96D825BDE55900F42B65 /* DownloadParameters.swift in Sources */, B9AF759125DF263E00B68816 /* CancelController.swift in Sources */, B9AF75C025DF29DF00B68816 /* CancelParameterFactory.swift in Sources */, + B9854CE0261269BB0054135B /* SendParameters.swift in Sources */, B96A888625BC565300F42B65 /* Errors.swift in Sources */, B96A888425BC565300F42B65 /* Constants.swift in Sources */, B96829D9254EC727002B4B04 /* UploaderDelegate.swift in Sources */, + B9854CDD261269B60054135B /* SenderDelegate.swift in Sources */, B9AF759925DF264E00B68816 /* RequestCanceller.swift in Sources */, B96A993F25BE166900F42B65 /* UploadParameters.swift in Sources */, ); diff --git a/ios/Send/BlobSender.swift b/ios/Send/BlobSender.swift new file mode 100644 index 00000000..9a241bc9 --- /dev/null +++ b/ios/Send/BlobSender.swift @@ -0,0 +1,112 @@ +// Copyright (c) Ely Deckers. +// +// This source code is licensed under the MPL-2.0 license found in the +// LICENSE file in the root directory of this source tree. +import Foundation + +open class BlobSender: NSObject { + static func filterHeaders(unfilteredHeaders: NSDictionary) -> NSDictionary { + Dictionary(uniqueKeysWithValues: unfilteredHeaders + .map { key, value in (key as? String, value as? String) } + .filter({ $0.1 != nil })) + .mapValues { $0! } as NSDictionary + } + + static func isValidTargetValue(_ value: String) -> Bool { + return Constants.targetValues.contains(value) + } + + static func buildRequestDataForFileSend( + method: String, + url: URL, + absoluteFilePath: String, + headers: NSDictionary) throws -> (URLRequest, Data) { + var request = URLRequest(url: url) + request.httpMethod = method + + for (key, value) in headers { + if let headerKey = key as? String, let headerValue = value as? String { + request.setValue( + headerValue, + forHTTPHeaderField: headerKey) + } + } + + let fileUrl = URL(string: absoluteFilePath)! + + let fileData = try Data(contentsOf: fileUrl) + + return (request, fileData) + } + + // swiftlint:disable function_body_length + static func sendBlobFromValidatedParameters(parameters: SendParameters) -> + Result { + let sessionConfig = URLSessionConfiguration.default + + let group = DispatchGroup() + let groupId = UUID().uuidString + + let queue = DispatchQueue.global() + + var result: Result = .success([:]) + + print("Entering group (id=\(groupId))") + group.enter() + + var cancelObserver: NSObjectProtocol? + + queue.async(group: group) { + let successfulResult = { (theResult: NSDictionary) -> Void in + result = .success(theResult) + + print("Leaving group (id=\(groupId),status=resolve)") + group.leave() + } + + let failedResult = { (error: BlobCourierError) -> Void in + result = .failure(error) + + print("Leaving group (id=\(groupId),status=reject)") + group.leave() + } + + let senderDelegate = + SenderDelegate( + taskId: parameters.taskId, + returnResponse: parameters.returnResponse, + progressIntervalMilliseconds: parameters.progressIntervalMilliseconds, + resolve: successfulResult, + reject: failedResult) + + let session = URLSession(configuration: sessionConfig, delegate: senderDelegate, delegateQueue: nil) + + let headers = parameters.headers + + do { + let (request, fileData) = + try buildRequestDataForFileSend( + method: parameters.method, + url: parameters.url, + absoluteFilePath: parameters.absoluteFilePath, + headers: headers) + + session.uploadTask(with: request, from: fileData).resume() + + cancelObserver = CancelController.registerCancelObserver( + session: session, taskId: parameters.taskId) + } catch { + failedResult(Errors.createUnexpectedError(error: error)) + } + } + + print("Waiting for group (id=\(groupId))") + group.wait() + print("Left group (id=\(groupId))") + + NotificationCenter.default.removeObserver(cancelObserver) + + return result + } + // swiftlint:enable function_body_length +} diff --git a/ios/Send/SendParameters.swift b/ios/Send/SendParameters.swift new file mode 100644 index 00000000..d7592b82 --- /dev/null +++ b/ios/Send/SendParameters.swift @@ -0,0 +1,32 @@ +// Copyright (c) Ely Deckers. +// +// This source code is licensed under the MPL-2.0 license found in the +// LICENSE file in the root directory of this source tree. +import Foundation + +struct SendParameters { + let absoluteFilePath: String + let headers: NSDictionary + let method: String + let progressIntervalMilliseconds: Int + let returnResponse: Bool + let taskId: String + let url: URL + + init( + absoluteFilePath: String, + headers: NSDictionary, + method: String, + progressIntervalMilliseconds: Int, + returnResponse: Bool, + taskId: String, + url: URL) { + self.absoluteFilePath = absoluteFilePath + self.headers = headers + self.method = method + self.progressIntervalMilliseconds = progressIntervalMilliseconds + self.returnResponse = returnResponse + self.taskId = taskId + self.url = url + } +} diff --git a/ios/Send/SenderDelegate.swift b/ios/Send/SenderDelegate.swift new file mode 100644 index 00000000..5abc70e8 --- /dev/null +++ b/ios/Send/SenderDelegate.swift @@ -0,0 +1,109 @@ +// Copyright (c) Ely Deckers. +// +// This source code is licensed under the MPL-2.0 license found in the +// LICENSE file in the root directory of this source tree. +import Foundation + +open class SenderDelegate: NSObject, URLSessionDataDelegate, URLSessionTaskDelegate { + typealias SuccessHandler = (NSDictionary) -> Void + typealias FailureHandler = (BlobCourierError) -> Void + + private let resolve: SuccessHandler + private let reject: FailureHandler + + private let taskId: String + private let returnResponse: Bool + + private let eventEmitter: BlobCourierDelayedEventEmitter + + private var receivedData: Data = Data() + + init( + taskId: String, + returnResponse: Bool, + progressIntervalMilliseconds: Int, + resolve: @escaping SuccessHandler, + reject: @escaping FailureHandler) { + self.taskId = taskId + self.returnResponse = returnResponse + + self.resolve = resolve + self.reject = reject + + self.eventEmitter = + BlobCourierDelayedEventEmitter( + taskId: taskId, + progressIntervalMilliseconds: progressIntervalMilliseconds) + } + + public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + guard let theError = error else { + processCompletedSend(data: self.receivedData, response: task.response, error: error) + return + } + + processFailedSend(error: theError) + } + + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + self.receivedData.append(data) + } + + public func urlSession( + _ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) { + self.eventEmitter.notifyBridgeOfProgress( + totalBytesWritten: totalBytesSent, + totalBytesExpectedToWrite: totalBytesExpectedToSend) + } + + func processFailedSend(error: Error) { + if (error as NSError).code == NSURLErrorCancelled { + self.reject(BlobCourierError(code: Errors.errorCanceledException, message: "Request was cancelled", error: error)) + + return + } + + self.reject(Errors.createUnexpectedError(error: error)) + } + + func processCompletedSend(data: Data, response: URLResponse?, error: Error?) { + if let error = error { + print( + "Error while sending a file. Error description: \(error.localizedDescription)" + ) + reject(Errors.createUnexpectedError(error: error)) + return + } + + if let statusCode = (response as? HTTPURLResponse)?.statusCode { + let maybeRawResponse = returnResponse ? String(data: data, encoding: String.Encoding.utf8) : nil + let rawResponse = maybeRawResponse ?? "" + + let result: NSDictionary = [ + "response": [ + "code": statusCode, + "data": rawResponse, + "headers": [] + ] + ] + + resolve(result) + return + } + + let noStatusCodeError = + NSError( + domain: Constants.libraryDomain, + code: -1, + userInfo: [NSLocalizedDescriptionKey: "Received no status code"]) + + reject(Errors.createUnexpectedError(error: noStatusCodeError)) + } +} diff --git a/ios/Send/SenderParameterFactory.swift b/ios/Send/SenderParameterFactory.swift new file mode 100644 index 00000000..c012278b --- /dev/null +++ b/ios/Send/SenderParameterFactory.swift @@ -0,0 +1,64 @@ +// Copyright (c) Ely Deckers. +// +// This source code is licensed under the MPL-2.0 license found in the +// LICENSE file in the root directory of this source tree. +import Foundation + +open class SenderParameterFactory: NSObject { + static func filterHeaders(unfilteredHeaders: NSDictionary) -> NSDictionary { + Dictionary(uniqueKeysWithValues: unfilteredHeaders + .map { key, value in (key as? String, value as? String) } + .filter({ $0.1 != nil })) + .mapValues { $0! } as NSDictionary + } + + static func validateParameters(input: NSDictionary) -> Result { + guard let taskId = input[Constants.parameterTaskId] as? String else { + return .failure(Errors.createMissingParameter(parameterName: Constants.parameterTaskId, type: "String")) + } + + guard let url = input[Constants.parameterUrl] as? String else { + return .failure(Errors.createMissingParameter(parameterName: Constants.parameterUrl, type: "String")) + } + + guard let absoluteFilePath = input[Constants.parameterAbsoluteFilePath] as? String else { + return .failure(Errors.createMissingParameter(parameterName: Constants.parameterAbsoluteFilePath, type: "String")) + } + + guard let method = input[Constants.parameterMethod] as? String else { + return .failure(Errors.createMissingParameter(parameterName: Constants.parameterMethod, type: "String")) + } + + let progressIntervalMilliseconds = + (input[Constants.parameterProgressInterval] as? Int) ?? + Constants.defaultProgressIntervalMilliseconds + + let returnResponse = (input[Constants.parameterReturnResponse] as? Bool) ?? false + + let headers = + filterHeaders(unfilteredHeaders: + (input[Constants.parameterHeaders] as? NSDictionary) ?? + NSDictionary()) + + guard let fileUrl = URL(string: url) else { + return .failure(Errors.createInvalidValue(parameterName: Constants.parameterUrl, receivedValue: url)) + } + + return .success(SendParameters( + absoluteFilePath: absoluteFilePath, + headers: headers, + method: method, + progressIntervalMilliseconds: progressIntervalMilliseconds, + returnResponse: returnResponse, + taskId: taskId, + url: fileUrl)) + } + + static func fromInput(input: NSDictionary) -> Result { + do { + return validateParameters(input: input) + } catch { + return .failure(Errors.createUnexpectedError(error: error)) + } + } +} diff --git a/react-native-blob-courier.podspec b/react-native-blob-courier.podspec index de35ae08..9abf370c 100644 --- a/react-native-blob-courier.podspec +++ b/react-native-blob-courier.podspec @@ -14,7 +14,7 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/edeckers/react-native-blob-courier.git", :tag => "#{s.version}" } - s.source_files = "ios/*.{h,m,mm,swift}", "ios/Cancel/*.{h,m,swift}", "ios/Common/*.{h,m,swift}", "ios/Fetch/*.{h,m,swift}", "ios/Progress/*.{h,m,swift}", "ios/React/*.{h,m,swift}", "ios/Upload/*.{h,m,swift}" + s.source_files = "ios/*.{h,m,mm,swift}", "ios/Cancel/*.{h,m,swift}", "ios/Common/*.{h,m,swift}", "ios/Fetch/*.{h,m,swift}", "ios/Progress/*.{h,m,swift}", "ios/React/*.{h,m,swift}", "ios/Send/*.{h,m,swift}", "ios/Upload/*.{h,m,swift}" s.dependency "React" end