Skip to content

Commit

Permalink
wip(client): add support for background upload
Browse files Browse the repository at this point in the history
  • Loading branch information
ThibaultBee committed Nov 28, 2023
1 parent 47552ef commit d00607d
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
{
"pins" : [
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire",
"state" : {
"revision" : "d120af1e8638c7da36c8481fd61a66c0c08dc4fc",
"version" : "5.4.4"
}
},
{
"identity" : "anycodable",
"kind" : "remoteSourceControl",
Expand Down
2 changes: 2 additions & 0 deletions Sources/APIs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class ApiVideoClient {
internal static var requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory()
public static var apiResponseQueue: DispatchQueue = .main
public static var timeout: TimeInterval = 60
public static var backgroundIdentifier: String = "video.api.upload.background"
internal static var customHeaders:[String: String] {
var headers = defaultHeaders
if let apiKey = apiKey {
Expand Down Expand Up @@ -135,4 +136,5 @@ open class RequestBuilder<T> {
public protocol RequestBuilderFactory {
func getNonDecodableBuilder<T>() -> RequestBuilder<T>.Type
func getBuilder<T: Decodable>() -> RequestBuilder<T>.Type
func getBackgroundBuilder<T: Decodable>() -> RequestBuilder<T>.Type
}
6 changes: 3 additions & 3 deletions Sources/APIs/VideosAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ The latter allows you to split a video source into X chunks and send those chunk

let localVariableHeaderParameters = APIHelper.rejectNilHeaders(localVariableNillableHeaders)

let localVariableRequestBuilder: RequestBuilder<Video>.Type = ApiVideoClient.requestBuilderFactory.getBuilder()
let localVariableRequestBuilder: RequestBuilder<Video>.Type = ApiVideoClient.requestBuilderFactory.getBackgroundBuilder()

return localVariableRequestBuilder.init(method: "POST", URLString: (localVariableUrlComponents?.string ?? localVariableURLString), parameters: localVariableParameters, headers: localVariableHeaderParameters, onProgressReady: onProgressReady)
}
Expand Down Expand Up @@ -248,7 +248,7 @@ The latter allows you to split a video source into X chunks and send those chunk

let localVariableHeaderParameters = APIHelper.rejectNilHeaders(localVariableNillableHeaders)

let localVariableRequestBuilder: RequestBuilder<Video>.Type = ApiVideoClient.requestBuilderFactory.getBuilder()
let localVariableRequestBuilder: RequestBuilder<Video>.Type = ApiVideoClient.requestBuilderFactory.getBackgroundBuilder()

return localVariableRequestBuilder.init(method: "POST", URLString: (localVariableUrlComponents?.string ?? localVariableURLString), parameters: localVariableParameters, headers: localVariableHeaderParameters, onProgressReady: onProgressReady)
}
Expand Down Expand Up @@ -395,7 +395,7 @@ The latter allows you to split a video source into X chunks and send those chunk

let localVariableHeaderParameters = APIHelper.rejectNilHeaders(localVariableNillableHeaders)

let localVariableRequestBuilder: RequestBuilder<Video>.Type = ApiVideoClient.requestBuilderFactory.getBuilder()
let localVariableRequestBuilder: RequestBuilder<Video>.Type = ApiVideoClient.requestBuilderFactory.getBackgroundBuilder()

return localVariableRequestBuilder.init(method: "POST", URLString: (localVariableUrlComponents?.string ?? localVariableURLString), parameters: localVariableParameters, headers: localVariableHeaderParameters, onProgressReady: onProgressReady)
}
Expand Down
29 changes: 29 additions & 0 deletions Sources/FileHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// APIHelper.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
//

import Foundation

public struct FileHelper {
/**
This function will copy a video file to a temporary location so that it remains accessbile for further handling such as an upload to S3.
- Parameter url: This is the url of the media item.
- Returns: Return a new URL for the local copy of the vidoe file.
*/
static func createTemporaryURLfrom(_ url: URL) -> URL {
/// Create the temporary directory.
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
/// create a temporary file for us to copy the video to.
let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(url.lastPathComponent)
/// Attempt the copy.
do {
try FileManager().copyItem(at: url.absoluteURL, to: temporaryFileURL)
} catch {
fatalError("There was an error copying the video file to the temporary location.")
}

return temporaryFileURL
}
}
114 changes: 92 additions & 22 deletions Sources/URLSessionImplementations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import Foundation
import MobileCoreServices
#endif

public protocol URLSessionProtocol {
func dataTask(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
}

extension URLSession: URLSessionProtocol {}

class URLSessionRequestBuilderFactory: RequestBuilderFactory {
func getNonDecodableBuilder<T>() -> RequestBuilder<T>.Type {
return URLSessionRequestBuilder<T>.self
Expand All @@ -17,6 +23,10 @@ class URLSessionRequestBuilderFactory: RequestBuilderFactory {
func getBuilder<T: Decodable>() -> RequestBuilder<T>.Type {
return URLSessionDecodableRequestBuilder<T>.self
}

func getBackgroundBuilder<T: Decodable>() -> RequestBuilder<T>.Type {
return URLSessionBackgroundUploadRequestBuilder<T>.self
}
}

public typealias ApiVideoClientChallengeHandler = ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))
Expand Down Expand Up @@ -62,7 +72,7 @@ open class URLSessionRequestBuilder<T>: RequestBuilder<T> {
May be overridden by a subclass if you want to control the URLSession
configuration.
*/
open func createURLSession() -> URLSession {
open func createURLSession() -> URLSessionProtocol {
return defaultURLSession
}

Expand All @@ -81,7 +91,7 @@ open class URLSessionRequestBuilder<T>: RequestBuilder<T> {
May be overridden by a subclass if you want to control the URLRequest
configuration (e.g. to override the cache policy).
*/
open func createURLRequest(urlSession: URLSession, method: HTTPMethod, encoding: ParameterEncoding, headers: [String: String]) throws -> URLRequest {
open func createURLRequest(urlSession: URLSessionProtocol, method: HTTPMethod, encoding: ParameterEncoding, headers: [String: String]) throws -> URLRequest {

guard let url = URL(string: URLString) else {
throw DownloadException.requestMissingURL
Expand Down Expand Up @@ -144,26 +154,9 @@ open class URLSessionRequestBuilder<T>: RequestBuilder<T> {
}

let dataTask = urlSession.dataTask(with: request) { data, response, error in

if let taskCompletionShouldRetry = self.taskCompletionShouldRetry {

taskCompletionShouldRetry(data, response, error) { shouldRetry in

if shouldRetry {
cleanupRequest()
self.execute(apiResponseQueue, completion)
} else {
apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
cleanupRequest()
}
}
}
} else {
apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
cleanupRequest()
}
apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
cleanupRequest()
}
}

Expand Down Expand Up @@ -372,6 +365,68 @@ open class URLSessionDecodableRequestBuilder<T: Decodable>: URLSessionRequestBui
}
}

class BackgroundUploadTaskURLSession<T>: NSObject, URLSessionProtocol {
private let file: URL
private let sessionDelegate = BackgroundUploadSessionDelegate()

private lazy var urlSession = URLSession(configuration: .background(withIdentifier: ApiVideoClient.backgroundIdentifier), delegate: sessionDelegate, delegateQueue: nil)

init(_ file: URL) {
self.file = FileHelper.createTemporaryURLfrom(file)
}

func dataTask(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
sessionDelegate.completionHandler = completionHandler
return urlSession.uploadTask(with: request, fromFile: file)
}
}

open class URLSessionBackgroundUploadRequestBuilder<T: Decodable>: URLSessionDecodableRequestBuilder<T> {
private var completion: ((Result<Response<T>, ErrorResponse>) -> Void)? = nil

override open func createURLSession() -> URLSessionProtocol {
guard let parameters = parameters else { fatalError("No parameters found") }

// Find file URL
var file: URL? = nil
for (_, value) in parameters {
if let fileURL = value as? URL {
file = fileURL
break
}
}
guard let fileURL = file else {
fatalError("No file URL found")
}
return BackgroundUploadTaskURLSession<T>(fileURL)
}

/**
May be overridden by a subclass if you want to control the URLRequest
configuration (e.g. to override the cache policy).
*/
override open func createURLRequest(urlSession: URLSessionProtocol, method: HTTPMethod, encoding: ParameterEncoding, headers: [String: String]) throws -> URLRequest {

guard let url = URL(string: URLString) else {
throw DownloadException.requestMissingURL
}

var originalRequest = URLRequest(url: url)
originalRequest.timeoutInterval = ApiVideoClient.timeout
originalRequest.httpMethod = method.rawValue

headers.forEach { key, value in
originalRequest.setValue(value, forHTTPHeaderField: key)
}

buildHeaders().forEach { key, value in
originalRequest.setValue(value, forHTTPHeaderField: key)
}

return originalRequest
}
}

private class SessionDelegate: NSObject, URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

Expand All @@ -397,6 +452,21 @@ private class SessionDelegate: NSObject, URLSessionTaskDelegate {
}
}

private class BackgroundUploadSessionDelegate: SessionDelegate, URLSessionDataDelegate {
var completionHandler: ((Data?, URLResponse?, Error?) -> Void)? = nil

func urlSession(_ session: URLSession, task: URLSessionTask, didBecomeInvalidWithError error: Error?) {
guard let error = error else {
return
}
completionHandler?(nil, task.response, error)
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
completionHandler?(data, dataTask.response, nil)
}
}

public enum HTTPMethod: String {
case options = "OPTIONS"
case get = "GET"
Expand Down

0 comments on commit d00607d

Please sign in to comment.