From 56eabdf95a7efe324fe11f19567364667694923e Mon Sep 17 00:00:00 2001 From: Loay Ashraf Date: Sat, 25 May 2024 20:03:25 +0300 Subject: [PATCH] feat: enhance plain upload Resolves: none. --- RxNetworkKit.xcodeproj/project.pbxproj | 34 +++++- .../Extensions/FileManager+SizeOfFile.swift | 24 +++++ .../HTTP/Extensions/Int64+FormattedSize.swift | 18 ++++ .../Reactive+URLSessionDownloadResponse.swift | 4 +- .../Reactive+URLSessionUploadResponse.swift | 101 +++++++++++++----- .../String+SplitNameAndExtension.swift | 18 ++++ .../Extensions/URLRequest+CURLCommand.swift | 33 +++++- .../Extensions/URLRequest+HTTPHeaders.swift | 15 +++ .../Extensions/URLSession+DownloadTask.swift | 8 +- .../URLSession+OutgoingRequest.swift | 2 +- .../Extensions/URLSession+UploadTask.swift | 27 +---- Source/HTTP/Types/Client/HTTPClient.swift | 12 --- ...TPRequestLogger.swift => HTTPLogger.swift} | 50 ++++----- .../Request/Logger/HTTPUploadLogger.swift | 84 +++++++++++++++ .../Parameters/HTTPUploadRequestFile.swift | 52 +++++---- .../HTTPUploadRequestFormData.swift | 6 +- .../HTTPUploadRequestFormFile.swift | 68 ++++++++++++ .../Extensions/Reactive+RESTResponse.swift | 24 ++--- Source/REST/Extensions/Single+Decode.swift | 26 +++-- ...Configuration+setUserAgentHTTPHeader.swift | 5 +- 20 files changed, 464 insertions(+), 147 deletions(-) create mode 100644 Source/HTTP/Extensions/FileManager+SizeOfFile.swift create mode 100644 Source/HTTP/Extensions/Int64+FormattedSize.swift create mode 100644 Source/HTTP/Extensions/String+SplitNameAndExtension.swift create mode 100644 Source/HTTP/Extensions/URLRequest+HTTPHeaders.swift rename Source/HTTP/Types/Request/Logger/{HTTPRequestLogger.swift => HTTPLogger.swift} (71%) create mode 100644 Source/HTTP/Types/Request/Logger/HTTPUploadLogger.swift create mode 100644 Source/HTTP/Types/Request/Parameters/HTTPUploadRequestFormFile.swift diff --git a/RxNetworkKit.xcodeproj/project.pbxproj b/RxNetworkKit.xcodeproj/project.pbxproj index 345e70c..d4fa54a 100644 --- a/RxNetworkKit.xcodeproj/project.pbxproj +++ b/RxNetworkKit.xcodeproj/project.pbxproj @@ -41,7 +41,7 @@ 0B77E0BD29D968DE0077FBC0 /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0B77E0BC29D968DE0077FBC0 /* RxSwift */; }; 0B77E0C029D969370077FBC0 /* RxSwiftExt in Frameworks */ = {isa = PBXBuildFile; productRef = 0B77E0BF29D969370077FBC0 /* RxSwiftExt */; }; C6049B162A95307800E5727E /* RxNetworkKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C6049B152A95307800E5727E /* RxNetworkKit.h */; }; - C61A7E242B6276F800407C38 /* HTTPRequestLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61A7E232B6276F800407C38 /* HTTPRequestLogger.swift */; }; + C61A7E242B6276F800407C38 /* HTTPLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61A7E232B6276F800407C38 /* HTTPLogger.swift */; }; C61A7E262B62782D00407C38 /* Reactive+RESTResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61A7E252B62782D00407C38 /* Reactive+RESTResponse.swift */; }; C61A7E2A2B62794900407C38 /* URLSession+OutgoingRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61A7E292B62794900407C38 /* URLSession+OutgoingRequest.swift */; }; C61A7E2C2B62798800407C38 /* URLRequest+CURLCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61A7E2B2B62798800407C38 /* URLRequest+CURLCommand.swift */; }; @@ -52,7 +52,13 @@ C6513B812BF76C7000A19EBC /* WebSocketClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6513B802BF76C7000A19EBC /* WebSocketClient.swift */; }; C6554A2B2AD5BBB60090DD3A /* RESTClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6554A2A2AD5BBB60090DD3A /* RESTClient.swift */; }; C6554A2D2AD5C1560090DD3A /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6554A2C2AD5C1560090DD3A /* HTTPClient.swift */; }; + C68FB9C02C0229B900A52FC5 /* Int64+FormattedSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68FB9BF2C0229B900A52FC5 /* Int64+FormattedSize.swift */; }; + C68FB9C22C0229E600A52FC5 /* URLRequest+HTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68FB9C12C0229E600A52FC5 /* URLRequest+HTTPHeaders.swift */; }; + C68FB9C42C022A0800A52FC5 /* FileManager+SizeOfFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68FB9C32C022A0800A52FC5 /* FileManager+SizeOfFile.swift */; }; + C68FB9C62C022A7A00A52FC5 /* String+SplitNameAndExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68FB9C52C022A7A00A52FC5 /* String+SplitNameAndExtension.swift */; }; + C68FB9C82C024FEC00A52FC5 /* HTTPUploadLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68FB9C72C024FEC00A52FC5 /* HTTPUploadLogger.swift */; }; C69A78562ACF001400ECF092 /* Docs.docc in Sources */ = {isa = PBXBuildFile; fileRef = C69A78552ACEFF3200ECF092 /* Docs.docc */; }; + C69BDD082BFD0817007B4CEB /* HTTPUploadRequestFormFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69BDD072BFD0817007B4CEB /* HTTPUploadRequestFormFile.swift */; }; C6A9BEFA2A93F16200459E32 /* URLSessionConfiguration+setAdditionalHTTPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A9BEF92A93F16200459E32 /* URLSessionConfiguration+setAdditionalHTTPHeader.swift */; }; C6A9BEFD2A93FAF100459E32 /* URLSessionConfiguration+setUserAgentHTTPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A9BEFC2A93FAF100459E32 /* URLSessionConfiguration+setUserAgentHTTPHeader.swift */; }; C6A9BEFF2A93FB1D00459E32 /* ProcessInfo+operatingSystemName.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A9BEFE2A93FB1D00459E32 /* ProcessInfo+operatingSystemName.swift */; }; @@ -125,7 +131,7 @@ 0B77E08629D965D30077FBC0 /* HTTPDownloadRequestRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPDownloadRequestRouter.swift; sourceTree = ""; }; 0B77E08829D965D30077FBC0 /* HTTPUploadRequestRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPUploadRequestRouter.swift; sourceTree = ""; }; C6049B152A95307800E5727E /* RxNetworkKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RxNetworkKit.h; sourceTree = ""; }; - C61A7E232B6276F800407C38 /* HTTPRequestLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPRequestLogger.swift; sourceTree = ""; }; + C61A7E232B6276F800407C38 /* HTTPLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPLogger.swift; sourceTree = ""; }; C61A7E252B62782D00407C38 /* Reactive+RESTResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Reactive+RESTResponse.swift"; sourceTree = ""; }; C61A7E292B62794900407C38 /* URLSession+OutgoingRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+OutgoingRequest.swift"; sourceTree = ""; }; C61A7E2B2B62798800407C38 /* URLRequest+CURLCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+CURLCommand.swift"; sourceTree = ""; }; @@ -137,7 +143,13 @@ C6513B802BF76C7000A19EBC /* WebSocketClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketClient.swift; sourceTree = ""; }; C6554A2A2AD5BBB60090DD3A /* RESTClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTClient.swift; sourceTree = ""; }; C6554A2C2AD5C1560090DD3A /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; + C68FB9BF2C0229B900A52FC5 /* Int64+FormattedSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int64+FormattedSize.swift"; sourceTree = ""; }; + C68FB9C12C0229E600A52FC5 /* URLRequest+HTTPHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+HTTPHeaders.swift"; sourceTree = ""; }; + C68FB9C32C022A0800A52FC5 /* FileManager+SizeOfFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+SizeOfFile.swift"; sourceTree = ""; }; + C68FB9C52C022A7A00A52FC5 /* String+SplitNameAndExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SplitNameAndExtension.swift"; sourceTree = ""; }; + C68FB9C72C024FEC00A52FC5 /* HTTPUploadLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPUploadLogger.swift; sourceTree = ""; }; C69A78552ACEFF3200ECF092 /* Docs.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Docs.docc; sourceTree = ""; }; + C69BDD072BFD0817007B4CEB /* HTTPUploadRequestFormFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPUploadRequestFormFile.swift; sourceTree = ""; }; C6A9BEF92A93F16200459E32 /* URLSessionConfiguration+setAdditionalHTTPHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+setAdditionalHTTPHeader.swift"; sourceTree = ""; }; C6A9BEFC2A93FAF100459E32 /* URLSessionConfiguration+setUserAgentHTTPHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+setUserAgentHTTPHeader.swift"; sourceTree = ""; }; C6A9BEFE2A93FB1D00459E32 /* ProcessInfo+operatingSystemName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+operatingSystemName.swift"; sourceTree = ""; }; @@ -251,7 +263,8 @@ C61A7E222B6276D900407C38 /* Logger */ = { isa = PBXGroup; children = ( - C61A7E232B6276F800407C38 /* HTTPRequestLogger.swift */, + C61A7E232B6276F800407C38 /* HTTPLogger.swift */, + C68FB9C72C024FEC00A52FC5 /* HTTPUploadLogger.swift */, ); path = Logger; sourceTree = ""; @@ -360,9 +373,10 @@ C6554A2E2AD5C4F30090DD3A /* Parameters */ = { isa = PBXGroup; children = ( + 0B77E06229D965D30077FBC0 /* HTTPMIMEType.swift */, 0B77E05329D965D30077FBC0 /* HTTPUploadRequestFile.swift */, 0B77E05229D965D30077FBC0 /* HTTPUploadRequestFormData.swift */, - 0B77E06229D965D30077FBC0 /* HTTPMIMEType.swift */, + C69BDD072BFD0817007B4CEB /* HTTPUploadRequestFormFile.swift */, ); path = Parameters; sourceTree = ""; @@ -409,6 +423,8 @@ children = ( 0B77E05529D965D30077FBC0 /* Data+AppendString.swift */, C61A7E2D2B6279C700407C38 /* Data+JSON.swift */, + C68FB9C32C022A0800A52FC5 /* FileManager+SizeOfFile.swift */, + C68FB9BF2C0229B900A52FC5 /* Int64+FormattedSize.swift */, 0B77E08029D965D30077FBC0 /* Observable+Decodable.swift */, 0B77E07F29D965D30077FBC0 /* Observable+Retry.swift */, 0B77E08429D965D30077FBC0 /* Reactive+Curl.swift */, @@ -416,7 +432,9 @@ 0B77E05629D965D30077FBC0 /* Reactive+URLSessionAdaptedUploadResponse.swift */, 0B77E04D29D965D30077FBC0 /* Reactive+URLSessionDownloadResponse.swift */, 0B77E05829D965D30077FBC0 /* Reactive+URLSessionUploadResponse.swift */, + C68FB9C52C022A7A00A52FC5 /* String+SplitNameAndExtension.swift */, C61A7E2B2B62798800407C38 /* URLRequest+CURLCommand.swift */, + C68FB9C12C0229E600A52FC5 /* URLRequest+HTTPHeaders.swift */, 0B77E04F29D965D30077FBC0 /* URLSession+DownloadTask.swift */, C61A7E312B62885F00407C38 /* URLSession+LogRequests.swift */, C61A7E292B62794900407C38 /* URLSession+OutgoingRequest.swift */, @@ -660,12 +678,14 @@ C6EAFAED2BF77B00008D3C2B /* HTTPMethod.swift in Sources */, C6C643092BE6C9340071C2CC /* SecCertificate+Bundle.swift in Sources */, 0B77E0B429D965D30077FBC0 /* HTTPDownloadRequestRouter.swift in Sources */, + C68FB9C42C022A0800A52FC5 /* FileManager+SizeOfFile.swift in Sources */, C6C643132BE6CA080071C2CC /* Set+SecKey.swift in Sources */, 0B77E09A29D965D30077FBC0 /* HTTPMIMEType.swift in Sources */, C6A9BEFF2A93FB1D00459E32 /* ProcessInfo+operatingSystemName.swift in Sources */, C6EAFAE72BF77B00008D3C2B /* HTTPError.swift in Sources */, C6EAFAF02BF77B00008D3C2B /* HTTPURLResponse+StatusCode.swift in Sources */, C6C643032BE6C8580071C2CC /* TLSTrustEvaluatorConfiguration.swift in Sources */, + C68FB9C22C0229E600A52FC5 /* URLRequest+HTTPHeaders.swift in Sources */, 0B77E08E29D965D30077FBC0 /* HTTPUploadRequestFile.swift in Sources */, 0B77E08D29D965D30077FBC0 /* HTTPUploadRequestFormData.swift in Sources */, C61A7E262B62782D00407C38 /* Reactive+RESTResponse.swift in Sources */, @@ -692,12 +712,14 @@ C6EAFAE32BF77B00008D3C2B /* DefaultHTTPBodyError.swift in Sources */, C6EAFAEF2BF77B00008D3C2B /* HTTPStatusCode.swift in Sources */, C6BDFFF32ACDF5100022F675 /* WebSocketMessage.swift in Sources */, - C61A7E242B6276F800407C38 /* HTTPRequestLogger.swift in Sources */, + C69BDD082BFD0817007B4CEB /* HTTPUploadRequestFormFile.swift in Sources */, + C61A7E242B6276F800407C38 /* HTTPLogger.swift in Sources */, C6C6430B2BE6C9510071C2CC /* SecCertificate+PublicKey.swift in Sources */, C61A7E302B627B3000407C38 /* SessionConfiguration.swift in Sources */, C6EAFAE42BF77B00008D3C2B /* HTTPAPIError.swift in Sources */, C6B4B4C42AD47A2F009073ED /* WebSocketError.swift in Sources */, 0B77E0A429D965D30077FBC0 /* NetworkReachability.swift in Sources */, + C68FB9C62C022A7A00A52FC5 /* String+SplitNameAndExtension.swift in Sources */, C6EAFAE62BF77B00008D3C2B /* HTTPClientError.swift in Sources */, 0B77E0B229D965D30077FBC0 /* Completable+Retry.swift in Sources */, 0B77E0B029D965D30077FBC0 /* Observable+Retry.swift in Sources */, @@ -721,11 +743,13 @@ 0B77E09229D965D30077FBC0 /* HTTPUploadRequestEvent.swift in Sources */, 0B77E09029D965D30077FBC0 /* Data+AppendString.swift in Sources */, C6BDFFE82ACDF3830022F675 /* Reactive+WebSocketReceive.swift in Sources */, + C68FB9C02C0229B900A52FC5 /* Int64+FormattedSize.swift in Sources */, C61A7E322B62885F00407C38 /* URLSession+LogRequests.swift in Sources */, 0B77E08929D965D30077FBC0 /* Reactive+URLSessionDownloadResponse.swift in Sources */, C6554A2D2AD5C1560090DD3A /* HTTPClient.swift in Sources */, C61A7E2A2B62794900407C38 /* URLSession+OutgoingRequest.swift in Sources */, C6EAFAE92BF77B00008D3C2B /* HTTPRequestAdapter.swift in Sources */, + C68FB9C82C024FEC00A52FC5 /* HTTPUploadLogger.swift in Sources */, C6BDFFEC2ACDF4100022F675 /* Reactive+WebSocketPing.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Source/HTTP/Extensions/FileManager+SizeOfFile.swift b/Source/HTTP/Extensions/FileManager+SizeOfFile.swift new file mode 100644 index 0000000..f2216a7 --- /dev/null +++ b/Source/HTTP/Extensions/FileManager+SizeOfFile.swift @@ -0,0 +1,24 @@ +// +// FileManager+SizeOfFile.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 25/05/2024. +// + +import Foundation + +extension FileManager { + func sizeOfFile(atPath path: String) -> Int64? { + guard let attrs = try? attributesOfItem(atPath: path) else { + return nil + } + return attrs[.size] as? Int64 + } + + func sizeOfFile(atURL url: URL) -> Int64? { + guard let attributes = try? attributesOfItem(atPath: url.path) else { + return nil + } + return attributes[.size] as? Int64 + } +} diff --git a/Source/HTTP/Extensions/Int64+FormattedSize.swift b/Source/HTTP/Extensions/Int64+FormattedSize.swift new file mode 100644 index 0000000..fa726f4 --- /dev/null +++ b/Source/HTTP/Extensions/Int64+FormattedSize.swift @@ -0,0 +1,18 @@ +// +// Int64+FormattedSize.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 25/05/2024. +// + +import Foundation + +extension Int64 { + var formattedSize: String { + let bcf = ByteCountFormatter() + bcf.allowedUnits = [.useKB, .useMB, .useGB] + bcf.countStyle = .file + let size = bcf.string(fromByteCount: self) + return size + } +} diff --git a/Source/HTTP/Extensions/Reactive+URLSessionDownloadResponse.swift b/Source/HTTP/Extensions/Reactive+URLSessionDownloadResponse.swift index 89a664f..881d829 100644 --- a/Source/HTTP/Extensions/Reactive+URLSessionDownloadResponse.swift +++ b/Source/HTTP/Extensions/Reactive+URLSessionDownloadResponse.swift @@ -25,7 +25,7 @@ extension Reactive where Base: URLSession { let task = self.base.fileDownloadTask(with: request) { data, response, error in #if DEBUG if URLSession.logRequests { - HTTPRequestLogger.shared.log(response: (request.url, response, data, error), bodyPlaceholder: "[File Body]") + HTTPLogger.shared.log(responseArguments: (request.url, data, response, error), bodyLogMessage: "[File Body]") } #endif guard let response = response, let data = data else { @@ -66,7 +66,7 @@ extension Reactive where Base: URLSession { let task = self.base.fileDownloadTask(with: request, saveTo: url) { data, response, error in #if DEBUG if URLSession.logRequests { - HTTPRequestLogger.shared.log(response: (request.url, response, data, error), bodyPlaceholder: "[File Body]") + HTTPLogger.shared.log(responseArguments: (request.url, data, response, error), bodyLogMessage: "[File Body]") } #endif guard let response = response, let data = data else { diff --git a/Source/HTTP/Extensions/Reactive+URLSessionUploadResponse.swift b/Source/HTTP/Extensions/Reactive+URLSessionUploadResponse.swift index faa4410..8309832 100644 --- a/Source/HTTP/Extensions/Reactive+URLSessionUploadResponse.swift +++ b/Source/HTTP/Extensions/Reactive+URLSessionUploadResponse.swift @@ -17,16 +17,32 @@ extension Reactive where Base: URLSession { /// - request: `URLRequest` used to create upload task and its observables. /// - file: `HTTPUploadRequestFile` object to be uploaded. /// - /// - Returns: a tuple of progress `PublishSubject` and response and data `Single`. + /// - Returns: a tuple of progress `PublishSubject` and response and data `Singsle`. func uploadResponse(request: URLRequest, file: HTTPUploadRequestFile) -> (PublishSubject, Single<(response: HTTPURLResponse, data: Data)>) { - // we must keep refernce to task progress observation object + let adaptedRequest = adaptUploadRequest(originalRequest: request, withFile: file) + let uploadRequestObservables = makeUploadRequestObservables(request: adaptedRequest, completion: { + self.logUploadResponse(url: adaptedRequest.url, data: $0, urlResponse: $1, error: $2) + }) + logUploadRequest(request: adaptedRequest, file: file) + return uploadRequestObservables + } + /// Creates a tuple that includes progress `PublishSubject` and response and data `Single`. + /// This method is inspired by RxCocoa's `response` method. + /// + /// - Parameters: + /// - request: `URLRequest` used to create upload task and its observables. + /// - formData: `HTTPUploadRequestFormData` object that includes parameters and files to be uploaded. + /// + /// - Returns: a tuple of progress `PublishSubject` and response and data `Single`. + func uploadResponse(request: URLRequest, formData: HTTPUploadRequestFormData) -> (PublishSubject, Single<(response: HTTPURLResponse, data: Data)>) { + // we must keep reference to task progress observation object var taskProgressObservation: NSKeyValueObservation? let taskProgressSubject = PublishSubject() let taskResponseSingle = Single<(response: HTTPURLResponse, data: Data)>.create { single in - let task = self.base.fileUploadTask(with: request, from: file) { data, response, error in + let task = self.base.formDataUploadTask(with: request, from: formData) { data, response, error in #if DEBUG if URLSession.logRequests { - HTTPRequestLogger.shared.log(response: (request.url, response, data, error)) + HTTPLogger.shared.log(responseArguments: (request.url, data, response, error)) } #endif guard let response = response, let data = data else { @@ -51,34 +67,67 @@ extension Reactive where Base: URLSession { } return (taskProgressSubject, taskResponseSingle) } - /// Creates a tuple that includes progress `PublishSubject` and response and data `Single`. - /// This method is inspired by RxCocoa's `response` method. +} + +extension Reactive where Base: URLSession { + /// Adapts upload request by applying 'Content-Type' HTTP header and HTTP body. /// /// - Parameters: - /// - request: `URLRequest` used to create upload task and its observables. - /// - formData: `HTTPUploadRequestFormData` object that includes parameters and files to be uploaded. + /// - originalRequest: original `URLRequest`. + /// - file: `HTTPUploadRequestFile` to be added to the request. /// - /// - Returns: a tuple of progress `PublishSubject` and response and data `Single`. - func uploadResponse(request: URLRequest, formData: HTTPUploadRequestFormData) -> (PublishSubject, Single<(response: HTTPURLResponse, data: Data)>) { - // we must keep refernce to task progress observation object + /// - Returns: Adapted `URLRequest`. + fileprivate func adaptUploadRequest(originalRequest: URLRequest, withFile file: HTTPUploadRequestFile) -> URLRequest { + var request = originalRequest + request.setValue(file.name, forHTTPHeaderField: "File-Name") + request.setValue(file.mimeType.rawValue, forHTTPHeaderField: "Content-Type") + request.setValue(file.size, forHTTPHeaderField: "Content-Length") + if let data = file.data { + request.httpBody = data + } else if let inputStream = file.inputStream { + request.httpBodyStream = inputStream + } + return request + } + + fileprivate func logUploadRequest(request: URLRequest, file: HTTPUploadRequestFile) { +#if DEBUG + if URLSession.logRequests { + let finalRequest = base.finalRequest(for: request) + HTTPUploadLogger.shared.log(request: finalRequest, file: file) + } +#endif + } + + fileprivate func logUploadResponse(url: URL?, data: Data?, urlResponse: URLResponse?, error: Error?) { +#if DEBUG + if URLSession.logRequests { + HTTPUploadLogger.shared.log(responseArguments: (url, data, urlResponse, error)) + } +#endif + } + + fileprivate func handleRequestCompletion(using single: (Result<(response: HTTPURLResponse, data: Data), any Error>) -> Void, + with arguments: (Data?, URLResponse?, Error?)) { + guard let response = arguments.1, + let data = arguments.0 else { + single(.failure(arguments.2 ?? RxCocoaURLError.unknown)) + return + } + guard let httpResponse = arguments.1 as? HTTPURLResponse else { + single(.failure(RxCocoaURLError.nonHTTPResponse(response: response))) + return + } + single(.success((httpResponse, data))) + } + + fileprivate func makeUploadRequestObservables(request: URLRequest, completion: @escaping (Data?, URLResponse?, Error?) -> Void) -> (PublishSubject, Single<(response: HTTPURLResponse, data: Data)>) { var taskProgressObservation: NSKeyValueObservation? let taskProgressSubject = PublishSubject() let taskResponseSingle = Single<(response: HTTPURLResponse, data: Data)>.create { single in - let task = self.base.formDataUploadTask(with: request, from: formData) { data, response, error in -#if DEBUG - if URLSession.logRequests { - HTTPRequestLogger.shared.log(response: (request.url, response, data, error)) - } -#endif - guard let response = response, let data = data else { - single(.failure(error ?? RxCocoaURLError.unknown)) - return - } - guard let httpResponse = response as? HTTPURLResponse else { - single(.failure(RxCocoaURLError.nonHTTPResponse(response: response))) - return - } - single(.success((httpResponse, data))) + let task = self.base.dataTask(with: request) { data, response, error in + completion(data, response, error) + self.handleRequestCompletion(using: single, with: (data, response, error)) } taskProgressObservation = task.progress.observe(\.fractionCompleted) { progress, _ in taskProgressSubject.onNext(progress) diff --git a/Source/HTTP/Extensions/String+SplitNameAndExtension.swift b/Source/HTTP/Extensions/String+SplitNameAndExtension.swift new file mode 100644 index 0000000..c841707 --- /dev/null +++ b/Source/HTTP/Extensions/String+SplitNameAndExtension.swift @@ -0,0 +1,18 @@ +// +// String+SplitNameAndExtension.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 25/05/2024. +// + +import Foundation + +extension String { + func splitNameAndExtension() -> (String, String) { + var components = self.components(separatedBy: ".") + guard components.count > 1 else { return (self, "") } + let `extension` = components.removeLast() + let name = components.joined(separator: ".") + return (name, `extension`) + } +} diff --git a/Source/HTTP/Extensions/URLRequest+CURLCommand.swift b/Source/HTTP/Extensions/URLRequest+CURLCommand.swift index 8b8a0e8..dadcee0 100644 --- a/Source/HTTP/Extensions/URLRequest+CURLCommand.swift +++ b/Source/HTTP/Extensions/URLRequest+CURLCommand.swift @@ -30,7 +30,38 @@ extension URLRequest { } } - if let data = httpBody, let body = String(data: data, encoding: .utf8) { + if let data = httpBody, + let body = String(data: data, encoding: .utf8) { + command.append("-d '\(body)'") + } + + return command.joined(separator: " \\\n\t") + } + + func curlCommand(bodyOption: String? = nil) -> String { + guard let url = url else { return "" } + var baseCommand = #"curl "\#(url.absoluteString)""# + + if httpMethod == "HEAD" { + baseCommand += " --head" + } + + var command = [baseCommand] + + if let method = httpMethod, method != "GET" && method != "HEAD" { + command.append("-X \(method)") + } + + if let headers = allHTTPHeaderFields { + for (key, value) in headers where key != "Cookie" { + command.append("-H '\(key): \(value)'") + } + } + + if let bodyOption = bodyOption { + command.append(bodyOption) + } else if let data = httpBody, + let body = String(data: data, encoding: .utf8) { command.append("-d '\(body)'") } diff --git a/Source/HTTP/Extensions/URLRequest+HTTPHeaders.swift b/Source/HTTP/Extensions/URLRequest+HTTPHeaders.swift new file mode 100644 index 0000000..c7bc1c9 --- /dev/null +++ b/Source/HTTP/Extensions/URLRequest+HTTPHeaders.swift @@ -0,0 +1,15 @@ +// +// URLRequest+HTTPHeaders.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 25/05/2024. +// + +import Foundation + +extension URLRequest { + mutating func setValue(_ value: Int64?, forHTTPHeaderField field: String) { + guard let value = value else { return } + setValue("\(value)", forHTTPHeaderField: field) + } +} diff --git a/Source/HTTP/Extensions/URLSession+DownloadTask.swift b/Source/HTTP/Extensions/URLSession+DownloadTask.swift index ccb1bc3..fd47754 100644 --- a/Source/HTTP/Extensions/URLSession+DownloadTask.swift +++ b/Source/HTTP/Extensions/URLSession+DownloadTask.swift @@ -18,8 +18,8 @@ extension URLSession { func fileDownloadTask(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { #if DEBUG if URLSession.logRequests { - let outgoingRequest = outgoingRequest(for: request) - HTTPRequestLogger.shared.log(request: outgoingRequest) + let finalRequest = finalRequest(for: request) + HTTPLogger.shared.log(request: finalRequest) } #endif let task = dataTask(with: request, completionHandler: completionHandler) @@ -36,8 +36,8 @@ extension URLSession { func fileDownloadTask(with request: URLRequest, saveTo url: URL, completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { #if DEBUG if URLSession.logRequests { - let outgoingRequest = outgoingRequest(for: request) - HTTPRequestLogger.shared.log(request: outgoingRequest) + let finalRequest = finalRequest(for: request) + HTTPLogger.shared.log(request: finalRequest) } #endif let task = dataTask(with: request) { data, response, error in diff --git a/Source/HTTP/Extensions/URLSession+OutgoingRequest.swift b/Source/HTTP/Extensions/URLSession+OutgoingRequest.swift index 37e54cf..2830303 100644 --- a/Source/HTTP/Extensions/URLSession+OutgoingRequest.swift +++ b/Source/HTTP/Extensions/URLSession+OutgoingRequest.swift @@ -14,7 +14,7 @@ extension URLSession { /// - Parameter initialRequest: Initial `URLRequest` object. /// /// - Returns: Actual outgoing `URLRequest` after applying additional HTTP headers. - func outgoingRequest(for initialRequest: URLRequest) -> URLRequest { + func finalRequest(for initialRequest: URLRequest) -> URLRequest { var finalRequest = initialRequest let initialRequestHTTPHeaders = initialRequest.allHTTPHeaderFields ?? [:] let configurationAdditionalHTTPHeaders = (configuration.httpAdditionalHeaders) as? [String: String] ?? [:] diff --git a/Source/HTTP/Extensions/URLSession+UploadTask.swift b/Source/HTTP/Extensions/URLSession+UploadTask.swift index fa2821e..f0597ee 100644 --- a/Source/HTTP/Extensions/URLSession+UploadTask.swift +++ b/Source/HTTP/Extensions/URLSession+UploadTask.swift @@ -8,27 +8,6 @@ import Foundation extension URLSession { - /// Creates a data task with HTTP body of given file data. - /// - /// - Parameters: - /// - request: `URLRequest` used to create data task. - /// - file: `HTTPUploadRequestFile` object that includes name, data, url and HTTP MIME type. - /// - completionHandler: completion handler to be called on task completion. - /// - /// - Returns: upload`URLSessionDataTask` created using given request and file. - func fileUploadTask(with request: URLRequest, from file: HTTPUploadRequestFile, completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { - let fileData = extractFileData(file) - let request = adaptUploadRequest(originalRequest: request, withContentType: file.mimeType.rawValue, withBody: fileData) -#if DEBUG - if URLSession.logRequests { - let outgoingRequest = outgoingRequest(for: request) - HTTPRequestLogger.shared.log(request: outgoingRequest, bodyPlaceholder: "[File Body]") - - } -#endif - let task = dataTask(with: request, completionHandler: completionHandler) - return task - } /// Creates a data task with HTTP body of given form data. /// /// - Parameters: @@ -43,8 +22,8 @@ extension URLSession { let request = adaptUploadRequest(originalRequest: request, withContentType: "multipart/form-data; boundary=\(boundary)", withBody: dataBody) #if DEBUG if URLSession.logRequests { - let outgoingRequest = outgoingRequest(for: request) - HTTPRequestLogger.shared.log(request: outgoingRequest) + let finalRequest = finalRequest(for: request) + HTTPLogger.shared.log(request: finalRequest) } #endif let task = dataTask(with: request, completionHandler: completionHandler) @@ -73,7 +52,7 @@ extension URLSession { /// - Parameter file: `HTTPUploadRequestFile` object that includes data or url. /// /// - Returns: `Data` object representing the file. - fileprivate func extractFileData(_ file: HTTPUploadRequestFile) -> Data? { + fileprivate func extractFileData(_ file: HTTPUploadRequestFormFile) -> Data? { var data: Data? = nil if let fileData = file.data { data = fileData diff --git a/Source/HTTP/Types/Client/HTTPClient.swift b/Source/HTTP/Types/Client/HTTPClient.swift index 093f5a3..1e006cc 100644 --- a/Source/HTTP/Types/Client/HTTPClient.swift +++ b/Source/HTTP/Types/Client/HTTPClient.swift @@ -93,15 +93,9 @@ public class HTTPClient { public func upload(_ router: HTTPUploadRequestRouter, _ file: HTTPUploadRequestFile, _ httpErrorType: E.Type = DefaultHTTPBodyError.self, _ apiErrorType: AE.Type = DefaultHTTPAPIError.self) -> Observable> { let originalRequest = router.asURLRequest() let adaptedRequest = requestInterceptor.adapt(originalRequest, for: urlSession) - let retryMaxAttempts = requestInterceptor.retryMaxAttempts(adaptedRequest, for: urlSession) - let retryPolicy = requestInterceptor.retryPolicy(adaptedRequest, for: urlSession) - let shouldRetry = { (error: HTTPError) in - self.requestInterceptor.shouldRetry(adaptedRequest, for: self.urlSession, dueTo: error) - } let observable = urlSession .rx .uploadResponse(request: adaptedRequest, file: file, modelType: T.self, httpErrorType: E.self, apiErrorType: AE.self) - .retry(retryMaxAttempts, delay: retryPolicy, shouldRetry: shouldRetry) return observable } @@ -118,15 +112,9 @@ public class HTTPClient { public func upload(_ router: HTTPUploadRequestRouter, _ formData: HTTPUploadRequestFormData, _ httpErrorType: E.Type = DefaultHTTPBodyError.self, _ apiErrorType: AE.Type = DefaultHTTPAPIError.self) -> Observable> { let originalRequest = router.asURLRequest() let adaptedRequest = requestInterceptor.adapt(originalRequest, for: urlSession) - let retryMaxAttempts = requestInterceptor.retryMaxAttempts(adaptedRequest, for: urlSession) - let retryPolicy = requestInterceptor.retryPolicy(adaptedRequest, for: urlSession) - let shouldRetry = { (error: HTTPError) in - self.requestInterceptor.shouldRetry(adaptedRequest, for: self.urlSession, dueTo: error) - } let observable = urlSession .rx .uploadResponse(request: adaptedRequest, formData: formData, modelType: T.self, httpErrorType: E.self, apiErrorType: AE.self) - .retry(retryMaxAttempts, delay: retryPolicy, shouldRetry: shouldRetry) return observable } diff --git a/Source/HTTP/Types/Request/Logger/HTTPRequestLogger.swift b/Source/HTTP/Types/Request/Logger/HTTPLogger.swift similarity index 71% rename from Source/HTTP/Types/Request/Logger/HTTPRequestLogger.swift rename to Source/HTTP/Types/Request/Logger/HTTPLogger.swift index c18d721..8dadc58 100644 --- a/Source/HTTP/Types/Request/Logger/HTTPRequestLogger.swift +++ b/Source/HTTP/Types/Request/Logger/HTTPLogger.swift @@ -1,5 +1,5 @@ // -// HTTPRequestLogger.swift +// HTTPLogger.swift // RxNetworkKit // // Created by Loay Ashraf on 25/01/2024. @@ -8,10 +8,10 @@ import Foundation /// Object responsible for logging outgoing requests and incoming responses. -class HTTPRequestLogger { +final class HTTPLogger { - /// Shared `HTTPRequestLogger` instance. - static let shared: HTTPRequestLogger = .init() + /// Shared `HTTPLogger` instance. + static let shared: HTTPLogger = .init() /// Private initializer to ensure only one instance is created. private init() { } @@ -21,29 +21,29 @@ class HTTPRequestLogger { /// - Parameters: /// - request: `URLRequest` to be printed to console. /// - bodyPlaceholder: `String?` placeholder to be printed in place of actual body. - func log(request: URLRequest, bodyPlaceholder: String? = nil) { - let logMessage = makeLogMessage(for: request, bodyPlaceholder: bodyPlaceholder) + func log(request: URLRequest, bodyLogMessage: String? = nil, curlBodyOption: String? = nil) { + let logMessage = makeLogMessage(for: request, bodyLogMessage: bodyLogMessage, curlBodyOption: curlBodyOption) print(logMessage) } /// Prints incoming response to console. /// /// - Parameters: - /// - response: `(URL?, URLResponse?, Data?, Error?)` to be printed to console. + /// - responseArguments: `(URL?, URLResponse?, Data?, Error?)` to be printed to console. /// - bodyPlaceholder: `String?` placeholder to be printed in place of actual body. - func log(response: (URL?, URLResponse?, Data?, Error?), bodyPlaceholder: String? = nil) { - let logMessage = makeLogMessage(for: response, bodyPlaceholder: bodyPlaceholder) + func log(responseArguments: (URL?, Data?, URLResponse?, Error?), bodyLogMessage: String? = nil) { + let logMessage = makeLogMessage(for: responseArguments, bodyLogMessage: bodyLogMessage) print(logMessage) } - /// Make console message for outgoing request. - /// + /// Makes console message for outgoing request. + /// /// - Parameters: /// - request: `URLRequest` to be included in message. - /// - bodyPlaceholder: `String?` placeholder to be included in message in place of actual body. + /// - bodyLogMessage: `String?` placeholder to be included in message in place of actual body. /// /// - Returns: `String` of outgoing request message. - private func makeLogMessage(for request: URLRequest, bodyPlaceholder: String?) -> String { + private func makeLogMessage(for request: URLRequest, bodyLogMessage: String?, curlBodyOption: String? = nil) -> String { var logMessage: String = "" logMessage += "* * * * * * * * * * OUTGOING REQUEST * * * * * * * * * *\n" @@ -64,8 +64,8 @@ class HTTPRequestLogger { for (key,value) in request.allHTTPHeaderFields ?? [:] { requestDetails += "\(key): \(value) \n" } - if let bodyPlaceholder = bodyPlaceholder { - requestDetails += "\n\(bodyPlaceholder)\n" + if let bodyLogMessage = bodyLogMessage { + requestDetails += "\n\(bodyLogMessage)\n" } else if let body = request.httpBody { if let jsonString = body.json { requestDetails += "\n\(jsonString)\n" @@ -77,7 +77,7 @@ class HTTPRequestLogger { var curlCommand = """ \n- - - - - - - - - - - CURL COMMAND - - - - - - - - - - -\n """ - curlCommand += "\n\(request.curlCommand)\n" + curlCommand += "\n\(request.curlCommand(bodyOption: curlBodyOption))\n" logMessage += requestDetails logMessage += curlCommand @@ -86,22 +86,22 @@ class HTTPRequestLogger { return logMessage } - /// Make console message for incoming response. + /// Makes console message for incoming response. /// /// - Parameters: /// - response: `(URL?, URLResponse?, Data?, Error?)` to be included in message. - /// - bodyPlaceholder: `String?` placeholder to be included in message in place of actual body. + /// - bodyLogMessage: `String?` placeholder to be included in message in place of actual body. /// /// - Returns: `String` of incoming response message. - private func makeLogMessage(for response: (URL?, URLResponse?, Data?, Error?), bodyPlaceholder: String?) -> String { + private func makeLogMessage(for responseArguments: (URL?, Data?, URLResponse?, Error?), bodyLogMessage: String?) -> String { var logMessage: String = "" logMessage += "* * * * * * * * * * INCOMING RESPONSE * * * * * * * * * *\n" - let url = response.0 - let httpResponse = response.1 as? HTTPURLResponse - let responseBody = response.2 - let responseError = response.3 + let url = responseArguments.0 + let httpResponse = responseArguments.2 as? HTTPURLResponse + let responseBody = responseArguments.1 + let responseError = responseArguments.3 let urlString = url?.absoluteString ?? "" @@ -124,9 +124,9 @@ class HTTPRequestLogger { for (key, value) in httpResponse?.allHeaderFields ?? [:] { responseDetails += "\(key): \(value)\n" } - if let bodyPlaceholder = bodyPlaceholder, + if let bodyLogMessage = bodyLogMessage, responseError == nil { - responseDetails += "\n\(bodyPlaceholder)\n" + responseDetails += "\n\(bodyLogMessage)\n" } else if let body = responseBody { if let jsonString = body.json { responseDetails += "\n\(jsonString)\n" diff --git a/Source/HTTP/Types/Request/Logger/HTTPUploadLogger.swift b/Source/HTTP/Types/Request/Logger/HTTPUploadLogger.swift new file mode 100644 index 0000000..663eb98 --- /dev/null +++ b/Source/HTTP/Types/Request/Logger/HTTPUploadLogger.swift @@ -0,0 +1,84 @@ +// +// HTTPUploadLogger.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 25/05/2024. +// + +import Foundation + +final class HTTPUploadLogger { + + /// Shared `HTTPUploadLogger` instance. + static let shared: HTTPUploadLogger = .init() + + /// Private initializer to ensure only one instance is created. + private init() { } + + /// Prints outgoing request to console. + /// + /// - Parameters: + /// - request: `URLRequest` to be printed to console. + /// - file: `HTTPUploadRequestFile` file details to be printed in place of body. + func log(request: URLRequest, file: HTTPUploadRequestFile) { + let bodyLogMessage = makeBodyLogMessage(for: file) + let curlBodyOption = makeCURLBodyOption(for: file) + HTTPLogger.shared.log(request: request, bodyLogMessage: bodyLogMessage, curlBodyOption: curlBodyOption) + } + + /// Prints incoming response to console. + /// + /// - Parameters: + /// - responseArguments: `(URL?, Data?, URLResponse?, Error?)` to be printed to console. + func log(responseArguments: (URL?, Data?, URLResponse?, Error?)) { + HTTPLogger.shared.log(responseArguments: responseArguments) + } + + /// Makes console body message for outgoing request. + /// + /// - Parameters: + /// - file: `HTTPUploadRequestFile` file details to be printed in place of body. + /// + /// - Returns: `String` of outgoing request body message. + private func makeBodyLogMessage(for file: HTTPUploadRequestFile) -> String { + var bodyLogMessage: String = "" + + if let filePath = file.path { + let fileName = file.name + let fileType = file.mimeType.rawValue + let fileSize = file.size.formattedSize + bodyLogMessage += "{ File From Disk }\n" + bodyLogMessage += "- Name: \(fileName)\n" + bodyLogMessage += "- Type: \(fileType)\n" + bodyLogMessage += "- Size: \(fileSize)\n" + bodyLogMessage += "- Path: \(filePath)" + } else if file.data != nil { + let fileName = file.name + let fileType = file.mimeType.rawValue + let fileSize = file.size + bodyLogMessage += "{ File From Memory }\n" + bodyLogMessage += "- Name: \(fileName)\n" + bodyLogMessage += "- Type: \(fileType)\n" + bodyLogMessage += "- Size: \(fileSize)" + } + + return bodyLogMessage + } + + /// Makes cURL body option for outgoing request. + /// + /// - Parameters: + /// - file: `HTTPUploadRequestFile` file details to be printed in place of body. + /// + /// - Returns: `String` of outgoing request cURL command option. + private func makeCURLBodyOption(for file: HTTPUploadRequestFile) -> String { + var curlBodyOption = "" + + if let filePath = file.path { + curlBodyOption += "--upload-file \(filePath)" + } + + return curlBodyOption + } + +} diff --git a/Source/HTTP/Types/Request/Parameters/HTTPUploadRequestFile.swift b/Source/HTTP/Types/Request/Parameters/HTTPUploadRequestFile.swift index 58b0fa0..f2e393f 100644 --- a/Source/HTTP/Types/Request/Parameters/HTTPUploadRequestFile.swift +++ b/Source/HTTP/Types/Request/Parameters/HTTPUploadRequestFile.swift @@ -10,45 +10,61 @@ import Foundation /// Holds file details for upload request. public struct HTTPUploadRequestFile { - /// key used for file record. - let key: String - /// name of file in the file record. + /// name of the file. let name: String - /// local url of the file. - let url: URL? + /// absolute path of the file. + let path: String? /// data of the file. let data: Data? + /// input stream of the file. + let inputStream: InputStream? /// MIME type of the file. let mimeType: HTTPMIMEType + /// size of the file. + let size: Int64 - /// Creates `File` instance, use this initializer for relativley small files. + /// Creates `File` instance, use this initializer for relativley small files (< 20MB). /// /// - Parameters: - /// - key: file key or id. /// - name: file name. + /// - extension: file extension. /// - data: `Data` object for file. - public init?(forKey key: String, withName name: String, withData data: Data) { - self.key = key + public init?(withName name: String, withExtension `extension`: String, withData data: Data) { self.name = name - self.url = nil + self.path = nil self.data = data - guard let mime = HTTPMIMEType(fileName: name) else { return nil } + self.inputStream = nil + guard let mime = HTTPMIMEType(fileExtension: `extension`) else { return nil } self.mimeType = mime + self.size = Int64(data.count) +#if DEBUG + if size > 20_971_520 { + print("* * * * * * * * * * MEMORY WARNING * * * * * * * * * *\n") + print("Holding a large file for upload in the device memory (> 20MB)\nPerformance may be reduced if the available memory is low.") + print("\n* * * * * * * * * * * * * END * * * * * * * * * * * * *\n") + } +#endif } - /// Creates `File` instance, use this initializer for relativley large files. + /// Creates `File` instance, use this initializer for relativley large files (> 20MB). /// /// - Parameters: - /// - key: file key or id. /// - url: local `URL` for the file. - public init?(forKey key: String, withURL url: URL) { - let name = url.lastPathComponent - self.key = key + public init?(withURL url: URL) { + let fileName = url.lastPathComponent + let (name, `extension`) = fileName.splitNameAndExtension() self.name = name - self.url = url + if #available(macOS 13, *) { + self.path = url.path() + } else { + self.path = url.path + } self.data = nil - guard let mime = HTTPMIMEType(fileName: name) else { return nil } + self.inputStream = .init(url: url) + guard let mime = HTTPMIMEType(fileExtension: `extension`) else { return nil } self.mimeType = mime + guard let size = FileManager.default.sizeOfFile(atURL: url) else { return nil } + self.size = size } } diff --git a/Source/HTTP/Types/Request/Parameters/HTTPUploadRequestFormData.swift b/Source/HTTP/Types/Request/Parameters/HTTPUploadRequestFormData.swift index 3fd387e..6789f2a 100644 --- a/Source/HTTP/Types/Request/Parameters/HTTPUploadRequestFormData.swift +++ b/Source/HTTP/Types/Request/Parameters/HTTPUploadRequestFormData.swift @@ -13,14 +13,14 @@ public struct HTTPUploadRequestFormData { /// parameters (text data fields) to be included in the form HTTP body. let parameters: [String: String] /// files to be included in the form HTTP body. - let files: [HTTPUploadRequestFile] + let files: [HTTPUploadRequestFormFile] /// Creates `HTTPUploadRequestFormData` instance. /// /// - Parameters: /// - parameters: `[String: String]` parameters (text data fields) to be included in the form HTTP body. - /// - files: `[HTTPUploadRequestFile]` files to be included in the form HTTP body. - public init(parameters: [String: String], files: [HTTPUploadRequestFile]) { + /// - files: `[HTTPUploadRequestFormFile]` files to be included in the form HTTP body. + public init(parameters: [String: String], files: [HTTPUploadRequestFormFile]) { self.parameters = parameters self.files = files } diff --git a/Source/HTTP/Types/Request/Parameters/HTTPUploadRequestFormFile.swift b/Source/HTTP/Types/Request/Parameters/HTTPUploadRequestFormFile.swift new file mode 100644 index 0000000..a03ca36 --- /dev/null +++ b/Source/HTTP/Types/Request/Parameters/HTTPUploadRequestFormFile.swift @@ -0,0 +1,68 @@ +// +// HTTPUploadRequestFormFile.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 21/05/2024. +// + +import Foundation + +/// Holds file details for multipart upload request. +public struct HTTPUploadRequestFormFile { + + /// key used for file record. + let key: String + /// name of the file. + let name: String + /// data of the file. + let data: Data? + /// local url of the file. + let url: URL? + /// MIME type of the file. + let mimeType: HTTPMIMEType + /// size of the file. + let size: Int64 + + /// Creates `File` instance, use this initializer for relativley small files (< 20MB). + /// + /// - Parameters: + /// - key: file key or id. + /// - name: file name. + /// - extension: file extension. + /// - data: `Data` object for file. + public init?(forKey key: String, withName name: String, withExtension `extension`: String, withData data: Data) { + self.key = key + self.name = name + self.data = data + self.url = nil + guard let mime = HTTPMIMEType(fileExtension: `extension`) else { return nil } + self.mimeType = mime + self.size = Int64(data.count) +#if DEBUG + if size > 20_971_520 { + print("* * * * * * * * * * MEMORY WARNING * * * * * * * * * *\n") + print("Holding a large file for upload in the device memory (> 20MB)\nPerformance may be reduced if the available memory is low.") + print("\n* * * * * * * * * * * * * END * * * * * * * * * * * * *\n") + } +#endif + } + + /// Creates `File` instance, use this initializer for relativley large files (> 20MB). + /// + /// - Parameters: + /// - key: file key or id. + /// - url: local `URL` for the file. + public init?(forKey key: String, withURL url: URL) { + let fileName = url.lastPathComponent + let (name, `extension`) = fileName.splitNameAndExtension() + self.key = key + self.name = name + self.data = nil + self.url = url + guard let mime = HTTPMIMEType(fileExtension: `extension`) else { return nil } + self.mimeType = mime + guard let size = FileManager.default.sizeOfFile(atURL: url) else { return nil } + self.size = size + } + +} diff --git a/Source/REST/Extensions/Reactive+RESTResponse.swift b/Source/REST/Extensions/Reactive+RESTResponse.swift index 4da068e..90237be 100644 --- a/Source/REST/Extensions/Reactive+RESTResponse.swift +++ b/Source/REST/Extensions/Reactive+RESTResponse.swift @@ -10,32 +10,26 @@ import RxSwift import RxCocoa extension Reactive where Base: URLSession { - /** - Observable sequence of responses for URL request. - - Performing of request starts after observer is subscribed and not after invoking this method. - - **URL requests will be performed per subscribed observer.** - - Any error during fetching of the response will cause observed sequence to terminate with error. - - - parameter request: URL request. - - returns: Observable sequence of URL responses. - */ + + /// Observable sequence of responses for request. + /// + /// - Parameter request: `URLRequest` object. + /// + /// - Returns: Observable sequence of response. func restResponse(request: URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> { return Observable.create { observer in #if DEBUG if URLSession.logRequests { - let outgoingRequest = base.outgoingRequest(for: request) - HTTPRequestLogger.shared.log(request: outgoingRequest) + let finalRequest = base.finalRequest(for: request) + HTTPLogger.shared.log(request: finalRequest) } #endif let task = self.base.dataTask(with: request) { data, response, error in #if DEBUG if URLSession.logRequests { - HTTPRequestLogger.shared.log(response: (request.url, response, data, error)) + HTTPLogger.shared.log(responseArguments: (request.url, data, response, error)) } #endif diff --git a/Source/REST/Extensions/Single+Decode.swift b/Source/REST/Extensions/Single+Decode.swift index 28806c3..3efadf2 100644 --- a/Source/REST/Extensions/Single+Decode.swift +++ b/Source/REST/Extensions/Single+Decode.swift @@ -54,14 +54,24 @@ extension PrimitiveSequence where Trait == SingleTrait, Element == (response: HT /// - Returns: `Single` observable to be observed for values. func decode(_ modelType: M.Type, errorType: E.Type) -> Single { map { - let jsonDecoder = JSONDecoder() - do { - let model = try jsonDecoder.decode(modelType.self, from: $0.data) - return model - } catch { - let apiError = try jsonDecoder.decode(errorType.self, from: $0.1) - let networkError = HTTPError.api(apiError) - throw networkError + if let contentType = $0.response.allHeaderFields["Content-Type"] as? String, + contentType.contains("text/plain") { + guard let string = String(data: $0.data, encoding: .utf8) else { + let decodingError = DecodingError.dataCorrupted(.init(codingPath: [], + debugDescription: "Corrupt body provided.")) + throw decodingError + } + return string as! M + } else { + let jsonDecoder = JSONDecoder() + do { + let model = try jsonDecoder.decode(modelType.self, from: $0.data) + return model + } catch { + let apiError = try jsonDecoder.decode(errorType.self, from: $0.1) + let networkError = HTTPError.api(apiError) + throw networkError + } } } } diff --git a/Source/Session/Extensions/URLSessionConfiguration+setUserAgentHTTPHeader.swift b/Source/Session/Extensions/URLSessionConfiguration+setUserAgentHTTPHeader.swift index 03c5ba8..28a7152 100644 --- a/Source/Session/Extensions/URLSessionConfiguration+setUserAgentHTTPHeader.swift +++ b/Source/Session/Extensions/URLSessionConfiguration+setUserAgentHTTPHeader.swift @@ -11,11 +11,10 @@ extension URLSessionConfiguration { /// Sets `User-Agent` header as an additional HTTP header. func setUserAgentHTTPHeader() { let mainBundle = Bundle.main - let frameworkBundle = Bundle.init(for: HTTPClient.self) let mainBundleIndentifier = mainBundle.bundleIdentifier ?? "Unknown Client Identifier" - let frameworkBundleVersion = frameworkBundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String + let frameworkVersion = "3.0.1" let osName = ProcessInfo.processInfo.operatingSystemName let osVersion = ProcessInfo.processInfo.operatingSystemVersionString - setAdditionalHTTPHeader("User-Agent", value: "RxNetworkKit/\(frameworkBundleVersion) (\(osName) \(osVersion)) (\(mainBundleIndentifier))") + setAdditionalHTTPHeader("User-Agent", value: "RxNetworkKit/\(frameworkVersion) (\(osName) \(osVersion)) (\(mainBundleIndentifier))") } }