You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
899 lines
37 KiB
899 lines
37 KiB
// |
|
// SessionManager.swift |
|
// |
|
// Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/) |
|
// |
|
// Permission is hereby granted, free of charge, to any person obtaining a copy |
|
// of this software and associated documentation files (the "Software"), to deal |
|
// in the Software without restriction, including without limitation the rights |
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
// copies of the Software, and to permit persons to whom the Software is |
|
// furnished to do so, subject to the following conditions: |
|
// |
|
// The above copyright notice and this permission notice shall be included in |
|
// all copies or substantial portions of the Software. |
|
// |
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
// THE SOFTWARE. |
|
// |
|
|
|
import Foundation |
|
|
|
/// Responsible for creating and managing `Request` objects, as well as their underlying `NSURLSession`. |
|
open class SessionManager { |
|
|
|
// MARK: - Helper Types |
|
|
|
/// Defines whether the `MultipartFormData` encoding was successful and contains result of the encoding as |
|
/// associated values. |
|
/// |
|
/// - Success: Represents a successful `MultipartFormData` encoding and contains the new `UploadRequest` along with |
|
/// streaming information. |
|
/// - Failure: Used to represent a failure in the `MultipartFormData` encoding and also contains the encoding |
|
/// error. |
|
public enum MultipartFormDataEncodingResult { |
|
case success(request: UploadRequest, streamingFromDisk: Bool, streamFileURL: URL?) |
|
case failure(Error) |
|
} |
|
|
|
// MARK: - Properties |
|
|
|
/// A default instance of `SessionManager`, used by top-level Alamofire request methods, and suitable for use |
|
/// directly for any ad hoc requests. |
|
public static let `default`: SessionManager = { |
|
let configuration = URLSessionConfiguration.default |
|
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders |
|
|
|
return SessionManager(configuration: configuration) |
|
}() |
|
|
|
/// Creates default values for the "Accept-Encoding", "Accept-Language" and "User-Agent" headers. |
|
public static let defaultHTTPHeaders: HTTPHeaders = { |
|
// Accept-Encoding HTTP Header; see https://tools.ietf.org/html/rfc7230#section-4.2.3 |
|
let acceptEncoding: String = "gzip;q=1.0, compress;q=0.5" |
|
|
|
// Accept-Language HTTP Header; see https://tools.ietf.org/html/rfc7231#section-5.3.5 |
|
let acceptLanguage = Locale.preferredLanguages.prefix(6).enumerated().map { index, languageCode in |
|
let quality = 1.0 - (Double(index) * 0.1) |
|
return "\(languageCode);q=\(quality)" |
|
}.joined(separator: ", ") |
|
|
|
// User-Agent Header; see https://tools.ietf.org/html/rfc7231#section-5.5.3 |
|
// Example: `iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0` |
|
let userAgent: String = { |
|
if let info = Bundle.main.infoDictionary { |
|
let executable = info[kCFBundleExecutableKey as String] as? String ?? "Unknown" |
|
let bundle = info[kCFBundleIdentifierKey as String] as? String ?? "Unknown" |
|
let appVersion = info["CFBundleShortVersionString"] as? String ?? "Unknown" |
|
let appBuild = info[kCFBundleVersionKey as String] as? String ?? "Unknown" |
|
|
|
let osNameVersion: String = { |
|
let version = ProcessInfo.processInfo.operatingSystemVersion |
|
let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" |
|
|
|
let osName: String = { |
|
#if os(iOS) |
|
return "iOS" |
|
#elseif os(watchOS) |
|
return "watchOS" |
|
#elseif os(tvOS) |
|
return "tvOS" |
|
#elseif os(macOS) |
|
return "OS X" |
|
#elseif os(Linux) |
|
return "Linux" |
|
#else |
|
return "Unknown" |
|
#endif |
|
}() |
|
|
|
return "\(osName) \(versionString)" |
|
}() |
|
|
|
let alamofireVersion: String = { |
|
guard |
|
let afInfo = Bundle(for: SessionManager.self).infoDictionary, |
|
let build = afInfo["CFBundleShortVersionString"] |
|
else { return "Unknown" } |
|
|
|
return "Alamofire/\(build)" |
|
}() |
|
|
|
return "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(alamofireVersion)" |
|
} |
|
|
|
return "Alamofire" |
|
}() |
|
|
|
return [ |
|
"Accept-Encoding": acceptEncoding, |
|
"Accept-Language": acceptLanguage, |
|
"User-Agent": userAgent |
|
] |
|
}() |
|
|
|
/// Default memory threshold used when encoding `MultipartFormData` in bytes. |
|
public static let multipartFormDataEncodingMemoryThreshold: UInt64 = 10_000_000 |
|
|
|
/// The underlying session. |
|
public let session: URLSession |
|
|
|
/// The session delegate handling all the task and session delegate callbacks. |
|
public let delegate: SessionDelegate |
|
|
|
/// Whether to start requests immediately after being constructed. `true` by default. |
|
open var startRequestsImmediately: Bool = true |
|
|
|
/// The request adapter called each time a new request is created. |
|
open var adapter: RequestAdapter? |
|
|
|
/// The request retrier called each time a request encounters an error to determine whether to retry the request. |
|
open var retrier: RequestRetrier? { |
|
get { return delegate.retrier } |
|
set { delegate.retrier = newValue } |
|
} |
|
|
|
/// The background completion handler closure provided by the UIApplicationDelegate |
|
/// `application:handleEventsForBackgroundURLSession:completionHandler:` method. By setting the background |
|
/// completion handler, the SessionDelegate `sessionDidFinishEventsForBackgroundURLSession` closure implementation |
|
/// will automatically call the handler. |
|
/// |
|
/// If you need to handle your own events before the handler is called, then you need to override the |
|
/// SessionDelegate `sessionDidFinishEventsForBackgroundURLSession` and manually call the handler when finished. |
|
/// |
|
/// `nil` by default. |
|
open var backgroundCompletionHandler: (() -> Void)? |
|
|
|
let queue = DispatchQueue(label: "org.alamofire.session-manager." + UUID().uuidString) |
|
|
|
// MARK: - Lifecycle |
|
|
|
/// Creates an instance with the specified `configuration`, `delegate` and `serverTrustPolicyManager`. |
|
/// |
|
/// - parameter configuration: The configuration used to construct the managed session. |
|
/// `URLSessionConfiguration.default` by default. |
|
/// - parameter delegate: The delegate used when initializing the session. `SessionDelegate()` by |
|
/// default. |
|
/// - parameter serverTrustPolicyManager: The server trust policy manager to use for evaluating all server trust |
|
/// challenges. `nil` by default. |
|
/// |
|
/// - returns: The new `SessionManager` instance. |
|
public init( |
|
configuration: URLSessionConfiguration = URLSessionConfiguration.default, |
|
delegate: SessionDelegate = SessionDelegate(), |
|
serverTrustPolicyManager: ServerTrustPolicyManager? = nil) |
|
{ |
|
self.delegate = delegate |
|
self.session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil) |
|
|
|
commonInit(serverTrustPolicyManager: serverTrustPolicyManager) |
|
} |
|
|
|
/// Creates an instance with the specified `session`, `delegate` and `serverTrustPolicyManager`. |
|
/// |
|
/// - parameter session: The URL session. |
|
/// - parameter delegate: The delegate of the URL session. Must equal the URL session's delegate. |
|
/// - parameter serverTrustPolicyManager: The server trust policy manager to use for evaluating all server trust |
|
/// challenges. `nil` by default. |
|
/// |
|
/// - returns: The new `SessionManager` instance if the URL session's delegate matches; `nil` otherwise. |
|
public init?( |
|
session: URLSession, |
|
delegate: SessionDelegate, |
|
serverTrustPolicyManager: ServerTrustPolicyManager? = nil) |
|
{ |
|
guard delegate === session.delegate else { return nil } |
|
|
|
self.delegate = delegate |
|
self.session = session |
|
|
|
commonInit(serverTrustPolicyManager: serverTrustPolicyManager) |
|
} |
|
|
|
private func commonInit(serverTrustPolicyManager: ServerTrustPolicyManager?) { |
|
session.serverTrustPolicyManager = serverTrustPolicyManager |
|
|
|
delegate.sessionManager = self |
|
|
|
delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] session in |
|
guard let strongSelf = self else { return } |
|
DispatchQueue.main.async { strongSelf.backgroundCompletionHandler?() } |
|
} |
|
} |
|
|
|
deinit { |
|
session.invalidateAndCancel() |
|
} |
|
|
|
// MARK: - Data Request |
|
|
|
/// Creates a `DataRequest` to retrieve the contents of the specified `url`, `method`, `parameters`, `encoding` |
|
/// and `headers`. |
|
/// |
|
/// - parameter url: The URL. |
|
/// - parameter method: The HTTP method. `.get` by default. |
|
/// - parameter parameters: The parameters. `nil` by default. |
|
/// - parameter encoding: The parameter encoding. `URLEncoding.default` by default. |
|
/// - parameter headers: The HTTP headers. `nil` by default. |
|
/// |
|
/// - returns: The created `DataRequest`. |
|
@discardableResult |
|
open func request( |
|
_ url: URLConvertible, |
|
method: HTTPMethod = .get, |
|
parameters: Parameters? = nil, |
|
encoding: ParameterEncoding = URLEncoding.default, |
|
headers: HTTPHeaders? = nil) |
|
-> DataRequest |
|
{ |
|
var originalRequest: URLRequest? |
|
|
|
do { |
|
originalRequest = try URLRequest(url: url, method: method, headers: headers) |
|
let encodedURLRequest = try encoding.encode(originalRequest!, with: parameters) |
|
return request(encodedURLRequest) |
|
} catch { |
|
return request(originalRequest, failedWith: error) |
|
} |
|
} |
|
|
|
/// Creates a `DataRequest` to retrieve the contents of a URL based on the specified `urlRequest`. |
|
/// |
|
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. |
|
/// |
|
/// - parameter urlRequest: The URL request. |
|
/// |
|
/// - returns: The created `DataRequest`. |
|
@discardableResult |
|
open func request(_ urlRequest: URLRequestConvertible) -> DataRequest { |
|
var originalRequest: URLRequest? |
|
|
|
do { |
|
originalRequest = try urlRequest.asURLRequest() |
|
let originalTask = DataRequest.Requestable(urlRequest: originalRequest!) |
|
|
|
let task = try originalTask.task(session: session, adapter: adapter, queue: queue) |
|
let request = DataRequest(session: session, requestTask: .data(originalTask, task)) |
|
|
|
delegate[task] = request |
|
|
|
if startRequestsImmediately { request.resume() } |
|
|
|
return request |
|
} catch { |
|
return request(originalRequest, failedWith: error) |
|
} |
|
} |
|
|
|
// MARK: Private - Request Implementation |
|
|
|
private func request(_ urlRequest: URLRequest?, failedWith error: Error) -> DataRequest { |
|
var requestTask: Request.RequestTask = .data(nil, nil) |
|
|
|
if let urlRequest = urlRequest { |
|
let originalTask = DataRequest.Requestable(urlRequest: urlRequest) |
|
requestTask = .data(originalTask, nil) |
|
} |
|
|
|
let underlyingError = error.underlyingAdaptError ?? error |
|
let request = DataRequest(session: session, requestTask: requestTask, error: underlyingError) |
|
|
|
if let retrier = retrier, error is AdaptError { |
|
allowRetrier(retrier, toRetry: request, with: underlyingError) |
|
} else { |
|
if startRequestsImmediately { request.resume() } |
|
} |
|
|
|
return request |
|
} |
|
|
|
// MARK: - Download Request |
|
|
|
// MARK: URL Request |
|
|
|
/// Creates a `DownloadRequest` to retrieve the contents the specified `url`, `method`, `parameters`, `encoding`, |
|
/// `headers` and save them to the `destination`. |
|
/// |
|
/// If `destination` is not specified, the contents will remain in the temporary location determined by the |
|
/// underlying URL session. |
|
/// |
|
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. |
|
/// |
|
/// - parameter url: The URL. |
|
/// - parameter method: The HTTP method. `.get` by default. |
|
/// - parameter parameters: The parameters. `nil` by default. |
|
/// - parameter encoding: The parameter encoding. `URLEncoding.default` by default. |
|
/// - parameter headers: The HTTP headers. `nil` by default. |
|
/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default. |
|
/// |
|
/// - returns: The created `DownloadRequest`. |
|
@discardableResult |
|
open func download( |
|
_ url: URLConvertible, |
|
method: HTTPMethod = .get, |
|
parameters: Parameters? = nil, |
|
encoding: ParameterEncoding = URLEncoding.default, |
|
headers: HTTPHeaders? = nil, |
|
to destination: DownloadRequest.DownloadFileDestination? = nil) |
|
-> DownloadRequest |
|
{ |
|
do { |
|
let urlRequest = try URLRequest(url: url, method: method, headers: headers) |
|
let encodedURLRequest = try encoding.encode(urlRequest, with: parameters) |
|
return download(encodedURLRequest, to: destination) |
|
} catch { |
|
return download(nil, to: destination, failedWith: error) |
|
} |
|
} |
|
|
|
/// Creates a `DownloadRequest` to retrieve the contents of a URL based on the specified `urlRequest` and save |
|
/// them to the `destination`. |
|
/// |
|
/// If `destination` is not specified, the contents will remain in the temporary location determined by the |
|
/// underlying URL session. |
|
/// |
|
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. |
|
/// |
|
/// - parameter urlRequest: The URL request |
|
/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default. |
|
/// |
|
/// - returns: The created `DownloadRequest`. |
|
@discardableResult |
|
open func download( |
|
_ urlRequest: URLRequestConvertible, |
|
to destination: DownloadRequest.DownloadFileDestination? = nil) |
|
-> DownloadRequest |
|
{ |
|
do { |
|
let urlRequest = try urlRequest.asURLRequest() |
|
return download(.request(urlRequest), to: destination) |
|
} catch { |
|
return download(nil, to: destination, failedWith: error) |
|
} |
|
} |
|
|
|
// MARK: Resume Data |
|
|
|
/// Creates a `DownloadRequest` from the `resumeData` produced from a previous request cancellation to retrieve |
|
/// the contents of the original request and save them to the `destination`. |
|
/// |
|
/// If `destination` is not specified, the contents will remain in the temporary location determined by the |
|
/// underlying URL session. |
|
/// |
|
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. |
|
/// |
|
/// On the latest release of all the Apple platforms (iOS 10, macOS 10.12, tvOS 10, watchOS 3), `resumeData` is broken |
|
/// on background URL session configurations. There's an underlying bug in the `resumeData` generation logic where the |
|
/// data is written incorrectly and will always fail to resume the download. For more information about the bug and |
|
/// possible workarounds, please refer to the following Stack Overflow post: |
|
/// |
|
/// - http://stackoverflow.com/a/39347461/1342462 |
|
/// |
|
/// - parameter resumeData: The resume data. This is an opaque data blob produced by `URLSessionDownloadTask` |
|
/// when a task is cancelled. See `URLSession -downloadTask(withResumeData:)` for |
|
/// additional information. |
|
/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default. |
|
/// |
|
/// - returns: The created `DownloadRequest`. |
|
@discardableResult |
|
open func download( |
|
resumingWith resumeData: Data, |
|
to destination: DownloadRequest.DownloadFileDestination? = nil) |
|
-> DownloadRequest |
|
{ |
|
return download(.resumeData(resumeData), to: destination) |
|
} |
|
|
|
// MARK: Private - Download Implementation |
|
|
|
private func download( |
|
_ downloadable: DownloadRequest.Downloadable, |
|
to destination: DownloadRequest.DownloadFileDestination?) |
|
-> DownloadRequest |
|
{ |
|
do { |
|
let task = try downloadable.task(session: session, adapter: adapter, queue: queue) |
|
let download = DownloadRequest(session: session, requestTask: .download(downloadable, task)) |
|
|
|
download.downloadDelegate.destination = destination |
|
|
|
delegate[task] = download |
|
|
|
if startRequestsImmediately { download.resume() } |
|
|
|
return download |
|
} catch { |
|
return download(downloadable, to: destination, failedWith: error) |
|
} |
|
} |
|
|
|
private func download( |
|
_ downloadable: DownloadRequest.Downloadable?, |
|
to destination: DownloadRequest.DownloadFileDestination?, |
|
failedWith error: Error) |
|
-> DownloadRequest |
|
{ |
|
var downloadTask: Request.RequestTask = .download(nil, nil) |
|
|
|
if let downloadable = downloadable { |
|
downloadTask = .download(downloadable, nil) |
|
} |
|
|
|
let underlyingError = error.underlyingAdaptError ?? error |
|
|
|
let download = DownloadRequest(session: session, requestTask: downloadTask, error: underlyingError) |
|
download.downloadDelegate.destination = destination |
|
|
|
if let retrier = retrier, error is AdaptError { |
|
allowRetrier(retrier, toRetry: download, with: underlyingError) |
|
} else { |
|
if startRequestsImmediately { download.resume() } |
|
} |
|
|
|
return download |
|
} |
|
|
|
// MARK: - Upload Request |
|
|
|
// MARK: File |
|
|
|
/// Creates an `UploadRequest` from the specified `url`, `method` and `headers` for uploading the `file`. |
|
/// |
|
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. |
|
/// |
|
/// - parameter file: The file to upload. |
|
/// - parameter url: The URL. |
|
/// - parameter method: The HTTP method. `.post` by default. |
|
/// - parameter headers: The HTTP headers. `nil` by default. |
|
/// |
|
/// - returns: The created `UploadRequest`. |
|
@discardableResult |
|
open func upload( |
|
_ fileURL: URL, |
|
to url: URLConvertible, |
|
method: HTTPMethod = .post, |
|
headers: HTTPHeaders? = nil) |
|
-> UploadRequest |
|
{ |
|
do { |
|
let urlRequest = try URLRequest(url: url, method: method, headers: headers) |
|
return upload(fileURL, with: urlRequest) |
|
} catch { |
|
return upload(nil, failedWith: error) |
|
} |
|
} |
|
|
|
/// Creates a `UploadRequest` from the specified `urlRequest` for uploading the `file`. |
|
/// |
|
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. |
|
/// |
|
/// - parameter file: The file to upload. |
|
/// - parameter urlRequest: The URL request. |
|
/// |
|
/// - returns: The created `UploadRequest`. |
|
@discardableResult |
|
open func upload(_ fileURL: URL, with urlRequest: URLRequestConvertible) -> UploadRequest { |
|
do { |
|
let urlRequest = try urlRequest.asURLRequest() |
|
return upload(.file(fileURL, urlRequest)) |
|
} catch { |
|
return upload(nil, failedWith: error) |
|
} |
|
} |
|
|
|
// MARK: Data |
|
|
|
/// Creates an `UploadRequest` from the specified `url`, `method` and `headers` for uploading the `data`. |
|
/// |
|
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. |
|
/// |
|
/// - parameter data: The data to upload. |
|
/// - parameter url: The URL. |
|
/// - parameter method: The HTTP method. `.post` by default. |
|
/// - parameter headers: The HTTP headers. `nil` by default. |
|
/// |
|
/// - returns: The created `UploadRequest`. |
|
@discardableResult |
|
open func upload( |
|
_ data: Data, |
|
to url: URLConvertible, |
|
method: HTTPMethod = .post, |
|
headers: HTTPHeaders? = nil) |
|
-> UploadRequest |
|
{ |
|
do { |
|
let urlRequest = try URLRequest(url: url, method: method, headers: headers) |
|
return upload(data, with: urlRequest) |
|
} catch { |
|
return upload(nil, failedWith: error) |
|
} |
|
} |
|
|
|
/// Creates an `UploadRequest` from the specified `urlRequest` for uploading the `data`. |
|
/// |
|
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. |
|
/// |
|
/// - parameter data: The data to upload. |
|
/// - parameter urlRequest: The URL request. |
|
/// |
|
/// - returns: The created `UploadRequest`. |
|
@discardableResult |
|
open func upload(_ data: Data, with urlRequest: URLRequestConvertible) -> UploadRequest { |
|
do { |
|
let urlRequest = try urlRequest.asURLRequest() |
|
return upload(.data(data, urlRequest)) |
|
} catch { |
|
return upload(nil, failedWith: error) |
|
} |
|
} |
|
|
|
// MARK: InputStream |
|
|
|
/// Creates an `UploadRequest` from the specified `url`, `method` and `headers` for uploading the `stream`. |
|
/// |
|
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. |
|
/// |
|
/// - parameter stream: The stream to upload. |
|
/// - parameter url: The URL. |
|
/// - parameter method: The HTTP method. `.post` by default. |
|
/// - parameter headers: The HTTP headers. `nil` by default. |
|
/// |
|
/// - returns: The created `UploadRequest`. |
|
@discardableResult |
|
open func upload( |
|
_ stream: InputStream, |
|
to url: URLConvertible, |
|
method: HTTPMethod = .post, |
|
headers: HTTPHeaders? = nil) |
|
-> UploadRequest |
|
{ |
|
do { |
|
let urlRequest = try URLRequest(url: url, method: method, headers: headers) |
|
return upload(stream, with: urlRequest) |
|
} catch { |
|
return upload(nil, failedWith: error) |
|
} |
|
} |
|
|
|
/// Creates an `UploadRequest` from the specified `urlRequest` for uploading the `stream`. |
|
/// |
|
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. |
|
/// |
|
/// - parameter stream: The stream to upload. |
|
/// - parameter urlRequest: The URL request. |
|
/// |
|
/// - returns: The created `UploadRequest`. |
|
@discardableResult |
|
open func upload(_ stream: InputStream, with urlRequest: URLRequestConvertible) -> UploadRequest { |
|
do { |
|
let urlRequest = try urlRequest.asURLRequest() |
|
return upload(.stream(stream, urlRequest)) |
|
} catch { |
|
return upload(nil, failedWith: error) |
|
} |
|
} |
|
|
|
// MARK: MultipartFormData |
|
|
|
/// Encodes `multipartFormData` using `encodingMemoryThreshold` and calls `encodingCompletion` with new |
|
/// `UploadRequest` using the `url`, `method` and `headers`. |
|
/// |
|
/// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative |
|
/// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most |
|
/// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to |
|
/// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory |
|
/// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be |
|
/// used for larger payloads such as video content. |
|
/// |
|
/// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory |
|
/// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, |
|
/// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk |
|
/// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding |
|
/// technique was used. |
|
/// |
|
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. |
|
/// |
|
/// - parameter multipartFormData: The closure used to append body parts to the `MultipartFormData`. |
|
/// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes. |
|
/// `multipartFormDataEncodingMemoryThreshold` by default. |
|
/// - parameter url: The URL. |
|
/// - parameter method: The HTTP method. `.post` by default. |
|
/// - parameter headers: The HTTP headers. `nil` by default. |
|
/// - parameter encodingCompletion: The closure called when the `MultipartFormData` encoding is complete. |
|
open func upload( |
|
multipartFormData: @escaping (MultipartFormData) -> Void, |
|
usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold, |
|
to url: URLConvertible, |
|
method: HTTPMethod = .post, |
|
headers: HTTPHeaders? = nil, |
|
queue: DispatchQueue? = nil, |
|
encodingCompletion: ((MultipartFormDataEncodingResult) -> Void)?) |
|
{ |
|
do { |
|
let urlRequest = try URLRequest(url: url, method: method, headers: headers) |
|
|
|
return upload( |
|
multipartFormData: multipartFormData, |
|
usingThreshold: encodingMemoryThreshold, |
|
with: urlRequest, |
|
queue: queue, |
|
encodingCompletion: encodingCompletion |
|
) |
|
} catch { |
|
(queue ?? DispatchQueue.main).async { encodingCompletion?(.failure(error)) } |
|
} |
|
} |
|
|
|
/// Encodes `multipartFormData` using `encodingMemoryThreshold` and calls `encodingCompletion` with new |
|
/// `UploadRequest` using the `urlRequest`. |
|
/// |
|
/// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative |
|
/// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most |
|
/// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to |
|
/// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory |
|
/// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be |
|
/// used for larger payloads such as video content. |
|
/// |
|
/// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory |
|
/// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, |
|
/// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk |
|
/// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding |
|
/// technique was used. |
|
/// |
|
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. |
|
/// |
|
/// - parameter multipartFormData: The closure used to append body parts to the `MultipartFormData`. |
|
/// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes. |
|
/// `multipartFormDataEncodingMemoryThreshold` by default. |
|
/// - parameter urlRequest: The URL request. |
|
/// - parameter encodingCompletion: The closure called when the `MultipartFormData` encoding is complete. |
|
open func upload( |
|
multipartFormData: @escaping (MultipartFormData) -> Void, |
|
usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold, |
|
with urlRequest: URLRequestConvertible, |
|
queue: DispatchQueue? = nil, |
|
encodingCompletion: ((MultipartFormDataEncodingResult) -> Void)?) |
|
{ |
|
DispatchQueue.global(qos: .utility).async { |
|
let formData = MultipartFormData() |
|
multipartFormData(formData) |
|
|
|
var tempFileURL: URL? |
|
|
|
do { |
|
var urlRequestWithContentType = try urlRequest.asURLRequest() |
|
urlRequestWithContentType.setValue(formData.contentType, forHTTPHeaderField: "Content-Type") |
|
|
|
let isBackgroundSession = self.session.configuration.identifier != nil |
|
|
|
if formData.contentLength < encodingMemoryThreshold && !isBackgroundSession { |
|
let data = try formData.encode() |
|
|
|
let encodingResult = MultipartFormDataEncodingResult.success( |
|
request: self.upload(data, with: urlRequestWithContentType), |
|
streamingFromDisk: false, |
|
streamFileURL: nil |
|
) |
|
|
|
(queue ?? DispatchQueue.main).async { encodingCompletion?(encodingResult) } |
|
} else { |
|
let fileManager = FileManager.default |
|
let tempDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()) |
|
let directoryURL = tempDirectoryURL.appendingPathComponent("org.alamofire.manager/multipart.form.data") |
|
let fileName = UUID().uuidString |
|
let fileURL = directoryURL.appendingPathComponent(fileName) |
|
|
|
tempFileURL = fileURL |
|
|
|
var directoryError: Error? |
|
|
|
// Create directory inside serial queue to ensure two threads don't do this in parallel |
|
self.queue.sync { |
|
do { |
|
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) |
|
} catch { |
|
directoryError = error |
|
} |
|
} |
|
|
|
if let directoryError = directoryError { throw directoryError } |
|
|
|
try formData.writeEncodedData(to: fileURL) |
|
|
|
let upload = self.upload(fileURL, with: urlRequestWithContentType) |
|
|
|
// Cleanup the temp file once the upload is complete |
|
upload.delegate.queue.addOperation { |
|
do { |
|
try FileManager.default.removeItem(at: fileURL) |
|
} catch { |
|
// No-op |
|
} |
|
} |
|
|
|
(queue ?? DispatchQueue.main).async { |
|
let encodingResult = MultipartFormDataEncodingResult.success( |
|
request: upload, |
|
streamingFromDisk: true, |
|
streamFileURL: fileURL |
|
) |
|
|
|
encodingCompletion?(encodingResult) |
|
} |
|
} |
|
} catch { |
|
// Cleanup the temp file in the event that the multipart form data encoding failed |
|
if let tempFileURL = tempFileURL { |
|
do { |
|
try FileManager.default.removeItem(at: tempFileURL) |
|
} catch { |
|
// No-op |
|
} |
|
} |
|
|
|
(queue ?? DispatchQueue.main).async { encodingCompletion?(.failure(error)) } |
|
} |
|
} |
|
} |
|
|
|
// MARK: Private - Upload Implementation |
|
|
|
private func upload(_ uploadable: UploadRequest.Uploadable) -> UploadRequest { |
|
do { |
|
let task = try uploadable.task(session: session, adapter: adapter, queue: queue) |
|
let upload = UploadRequest(session: session, requestTask: .upload(uploadable, task)) |
|
|
|
if case let .stream(inputStream, _) = uploadable { |
|
upload.delegate.taskNeedNewBodyStream = { _, _ in inputStream } |
|
} |
|
|
|
delegate[task] = upload |
|
|
|
if startRequestsImmediately { upload.resume() } |
|
|
|
return upload |
|
} catch { |
|
return upload(uploadable, failedWith: error) |
|
} |
|
} |
|
|
|
private func upload(_ uploadable: UploadRequest.Uploadable?, failedWith error: Error) -> UploadRequest { |
|
var uploadTask: Request.RequestTask = .upload(nil, nil) |
|
|
|
if let uploadable = uploadable { |
|
uploadTask = .upload(uploadable, nil) |
|
} |
|
|
|
let underlyingError = error.underlyingAdaptError ?? error |
|
let upload = UploadRequest(session: session, requestTask: uploadTask, error: underlyingError) |
|
|
|
if let retrier = retrier, error is AdaptError { |
|
allowRetrier(retrier, toRetry: upload, with: underlyingError) |
|
} else { |
|
if startRequestsImmediately { upload.resume() } |
|
} |
|
|
|
return upload |
|
} |
|
|
|
#if !os(watchOS) |
|
|
|
// MARK: - Stream Request |
|
|
|
// MARK: Hostname and Port |
|
|
|
/// Creates a `StreamRequest` for bidirectional streaming using the `hostname` and `port`. |
|
/// |
|
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. |
|
/// |
|
/// - parameter hostName: The hostname of the server to connect to. |
|
/// - parameter port: The port of the server to connect to. |
|
/// |
|
/// - returns: The created `StreamRequest`. |
|
@discardableResult |
|
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *) |
|
open func stream(withHostName hostName: String, port: Int) -> StreamRequest { |
|
return stream(.stream(hostName: hostName, port: port)) |
|
} |
|
|
|
// MARK: NetService |
|
|
|
/// Creates a `StreamRequest` for bidirectional streaming using the `netService`. |
|
/// |
|
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. |
|
/// |
|
/// - parameter netService: The net service used to identify the endpoint. |
|
/// |
|
/// - returns: The created `StreamRequest`. |
|
@discardableResult |
|
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *) |
|
open func stream(with netService: NetService) -> StreamRequest { |
|
return stream(.netService(netService)) |
|
} |
|
|
|
// MARK: Private - Stream Implementation |
|
|
|
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *) |
|
private func stream(_ streamable: StreamRequest.Streamable) -> StreamRequest { |
|
do { |
|
let task = try streamable.task(session: session, adapter: adapter, queue: queue) |
|
let request = StreamRequest(session: session, requestTask: .stream(streamable, task)) |
|
|
|
delegate[task] = request |
|
|
|
if startRequestsImmediately { request.resume() } |
|
|
|
return request |
|
} catch { |
|
return stream(failedWith: error) |
|
} |
|
} |
|
|
|
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *) |
|
private func stream(failedWith error: Error) -> StreamRequest { |
|
let stream = StreamRequest(session: session, requestTask: .stream(nil, nil), error: error) |
|
if startRequestsImmediately { stream.resume() } |
|
return stream |
|
} |
|
|
|
#endif |
|
|
|
// MARK: - Internal - Retry Request |
|
|
|
func retry(_ request: Request) -> Bool { |
|
guard let originalTask = request.originalTask else { return false } |
|
|
|
do { |
|
let task = try originalTask.task(session: session, adapter: adapter, queue: queue) |
|
|
|
if let originalTask = request.task { |
|
delegate[originalTask] = nil // removes the old request to avoid endless growth |
|
} |
|
|
|
request.delegate.task = task // resets all task delegate data |
|
|
|
request.retryCount += 1 |
|
request.startTime = CFAbsoluteTimeGetCurrent() |
|
request.endTime = nil |
|
|
|
task.resume() |
|
|
|
return true |
|
} catch { |
|
request.delegate.error = error.underlyingAdaptError ?? error |
|
return false |
|
} |
|
} |
|
|
|
private func allowRetrier(_ retrier: RequestRetrier, toRetry request: Request, with error: Error) { |
|
DispatchQueue.utility.async { [weak self] in |
|
guard let strongSelf = self else { return } |
|
|
|
retrier.should(strongSelf, retry: request, with: error) { shouldRetry, timeDelay in |
|
guard let strongSelf = self else { return } |
|
|
|
guard shouldRetry else { |
|
if strongSelf.startRequestsImmediately { request.resume() } |
|
return |
|
} |
|
|
|
DispatchQueue.utility.after(timeDelay) { |
|
guard let strongSelf = self else { return } |
|
|
|
let retrySucceeded = strongSelf.retry(request) |
|
|
|
if retrySucceeded, let task = request.task { |
|
strongSelf.delegate[task] = request |
|
} else { |
|
if strongSelf.startRequestsImmediately { request.resume() } |
|
} |
|
} |
|
} |
|
} |
|
} |
|
}
|
|
|