Files
FileProvider/Sources/HTTPFileProvider.swift
2019-04-04 12:42:17 +04:30

718 lines
34 KiB
Swift

//
// HTTPFileProvider.swift
// FilesProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2017 Mousavian. Distributed under MIT license.
//
import Foundation
/**
The abstract base class for all REST/Web based providers such as WebDAV, Dropbox, OneDrive, Google Drive, etc. and encapsulates basic
functionalitis such as downloading/uploading.
No instance of this class should (and can) be created. Use derived classes instead. It leads to a crash with `fatalError()`.
*/
open class HTTPFileProvider: NSObject, FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite, FileProviderReadWriteProgressive {
open class var type: String { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") }
public let baseURL: URL?
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue {
willSet {
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
}
}
open weak var delegate: FileProviderDelegate?
open var credential: URLCredential? {
didSet {
sessionDelegate?.credential = self.credential
}
}
open private(set) var cache: URLCache?
public var useCache: Bool
public var validatingCache: Bool
fileprivate var _session: URLSession!
internal fileprivate(set) var sessionDelegate: SessionDelegate?
public var session: URLSession {
get {
if _session == nil {
self.sessionDelegate = SessionDelegate(fileProvider: self)
let config = URLSessionConfiguration.default
config.urlCache = cache
config.requestCachePolicy = .returnCacheDataElseLoad
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue)
_session.sessionDescription = UUID().uuidString
initEmptySessionHandler(_session.sessionDescription!)
}
return _session
}
set {
assert(newValue.delegate is SessionDelegate, "session instances should have a SessionDelegate instance as delegate.")
_session = newValue
if _session.sessionDescription?.isEmpty ?? true {
_session.sessionDescription = UUID().uuidString
}
self.sessionDelegate = newValue.delegate as? SessionDelegate
initEmptySessionHandler(_session.sessionDescription!)
}
}
fileprivate var _longpollSession: URLSession?
/// This session has extended timeout up to 10 minutes, suitable for monitoring.
internal var longpollSession: URLSession {
if _longpollSession == nil {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 600
_longpollSession = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
}
return _longpollSession!
}
#if os(macOS) || os(iOS) || os(tvOS)
open var undoManager: UndoManager? = nil
#endif
/**
This is parent initializer for subclasses. Using this method on `HTTPFileProvider` will fail as `type` is not implemented.
- Parameters:
- baseURL: Location of server.
- credential: An `URLCredential` object with `user` and `password`.
- cache: A URLCache to cache downloaded files and contents.
*/
public init(baseURL: URL?, credential: URLCredential?, cache: URLCache?) {
// Make base url absolute and path as directory
let urlStr = baseURL?.absoluteString
self.baseURL = urlStr.flatMap { $0.hasSuffix("/") ? URL(string: $0) : URL(string: $0 + "/") }
self.useCache = false
self.validatingCache = true
self.cache = cache
self.credential = credential
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
operation_queue = OperationQueue()
operation_queue.name = "\(queueLabel).Operation"
super.init()
}
public required convenience init?(coder aDecoder: NSCoder) {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
deinit {
if let sessionuuid = _session?.sessionDescription {
removeSessionHandler(for: sessionuuid)
}
if fileProviderCancelTasksOnInvalidating {
_session?.invalidateAndCancel()
} else {
_session?.finishTasksAndInvalidate()
}
longpollSession.invalidateAndCancel()
}
public func encode(with aCoder: NSCoder) {
aCoder.encode(self.baseURL, forKey: "baseURL")
aCoder.encode(self.credential, forKey: "credential")
aCoder.encode(self.useCache, forKey: "useCache")
aCoder.encode(self.validatingCache, forKey: "validatingCache")
}
public static var supportsSecureCoding: Bool {
return true
}
open func copy(with zone: NSZone? = nil) -> Any {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
open func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
open func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
@discardableResult
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
open func isReachable(completionHandler: @escaping (_ success: Bool, _ error: Error?) -> Void) {
self.storageProperties { volume in
if volume != nil {
completionHandler(volume != nil, nil)
return
} else {
self.contentsOfDirectory(path: "", completionHandler: { (files, error) in
completionHandler(false, error)
})
}
}
}
// Nothing special for these two funcs, just reimplemented to workaround a bug in swift to allow override in subclasses!
open func url(of path: String) -> URL {
var rpath: String = path
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? rpath
if let baseURL = baseURL {
if rpath.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
}
return URL(string: rpath, relativeTo: baseURL) ?? baseURL
} else {
return URL(string: rpath) ?? URL(string: "/")!
}
}
open func relativePathOf(url: URL) -> String {
// check if url derieved from current base url
let relativePath = url.relativePath
if !relativePath.isEmpty, url.baseURL == self.baseURL {
return (relativePath.removingPercentEncoding ?? relativePath).replacingOccurrences(of: "/", with: "", options: .anchored)
}
// resolve url string against baseurl
guard let baseURL = self.baseURL else { return url.absoluteString }
let standardRelativePath = url.absoluteString.replacingOccurrences(of: baseURL.absoluteString, with: "/").replacingOccurrences(of: "/", with: "", options: .anchored)
if URLComponents(string: standardRelativePath)?.host?.isEmpty ?? true {
return standardRelativePath.removingPercentEncoding ?? standardRelativePath
} else {
return relativePath.replacingOccurrences(of: "/", with: "", options: .anchored)
}
}
open weak var fileOperationDelegate: FileOperationDelegate?
@discardableResult
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let path = atPath.appendingPathComponent(folderName) + "/"
return doOperation(.create(path: path), completionHandler: completionHandler)
}
@discardableResult
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.move(source: path, destination: toPath), overwrite: overwrite, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.copy(source: path, destination: toPath), overwrite: overwrite, completionHandler: completionHandler)
}
@discardableResult
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
@discardableResult
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
// check file is not a folder
guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else {
dispatch_queue.async {
completionHandler?(URLError(.fileIsDirectory, url: localFile))
}
return nil
}
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let request = self.request(for: operation, overwrite: overwrite)
return upload_file(toPath, request: request, localFile: localFile, operation: operation, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.copy(source: path, destination: destURL.absoluteString)
let request = self.request(for: operation)
let cantLoadError = URLError(.cannotLoadFromNetwork, url: self.url(of: path))
return self.download_file(path: path, request: request, operation: operation, completionHandler: { [weak self] (tempURL, error) in
do {
if let error = error {
throw error
}
guard let tempURL = tempURL else {
throw cantLoadError
}
#if os(macOS) || os(iOS) || os(tvOS)
var coordError: NSError?
NSFileCoordinator().coordinate(writingItemAt: tempURL, options: .forMoving, writingItemAt: destURL, options: .forReplacing, error: &coordError, byAccessor: { (tempURL, destURL) in
do {
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
self?.delegateNotify(operation)
} catch {
completionHandler?(error)
self?.delegateNotify(operation, error: error)
}
})
if let error = coordError {
throw error
}
#else
do {
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
self?.delegateNotify(operation)
} catch {
completionHandler?(error)
self?.delegateNotify(operation, error: error)
}
#endif
} catch {
completionHandler?(error)
self?.delegateNotify(operation, error: error)
}
})
}
/**
Progressively fetch data of file and returns fetched data in `progressHandler`.
If path specifies a directory, or if some other error occurs, data will be nil.
- Parameters:
- path: Path of file.
- progressHandler: a closure called every time a new `Data` is available.
- position: start position of data fetched.
- data: a portion of contents of file in a `Data` object.
- completionHandler: a closure with result of file contents or error.
- error: `Error` returned by system if occured.
- Returns: An `Progress` to get progress or cancel progress.
*/
@discardableResult
open func contents(path: String, offset: Int64 = 0, length: Int = -1, responseHandler: ((_ response: URLResponse) -> Void)? = nil, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.fetch(path: path)
var request = self.request(for: operation)
request.setValue(rangeWithOffset: offset, length: length)
var position: Int64 = offset
return download_progressive(path: path, request: request, operation: operation, responseHandler: responseHandler, progressHandler: { data in
progressHandler(position, data)
position += Int64(data.count)
}, completionHandler: (completionHandler ?? { _ in return }))
}
@discardableResult
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
}
return nil
}
let operation = FileOperationType.fetch(path: path)
var request = self.request(for: operation)
let cantLoadError = URLError(.cannotLoadFromNetwork, url: self.url(of: path))
request.setValue(rangeWithOffset: offset, length: length)
let stream = OutputStream.toMemory()
return self.download(path: path, request: request, operation: operation, stream: stream) { (error) in
do {
if let error = error {
throw error
}
guard let data = stream.property(forKey: .dataWrittenToMemoryStreamKey) as? Data else {
throw cantLoadError
}
completionHandler(data, nil)
} catch {
completionHandler(nil, error)
}
}
}
@discardableResult
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let data = data ?? Data()
let request = self.request(for: operation, overwrite: overwrite, attributes: [.contentModificationDateKey: Date()])
let stream = InputStream(data: data)
return upload(path, request: request, stream: stream, size: Int64(data.count), operation: operation, completionHandler: completionHandler)
}
internal func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey: Any] = [:]) -> URLRequest {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
internal func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
internal func multiStatusError(operation: FileOperationType, data: Data) -> FileProviderHTTPError? {
// WebDAV will override this function
return nil
}
/**
This is the main function to init urlsession task for specified file operation.
You won't need to override this function unless another network request must be done before intended operation,
such as retrieving file id from file path. Then you must call `super.doOperation()`
In case you have to call super method asyncronously, create a `Progress` object and pass ot to `progress` parameter.
*/
@discardableResult
internal func doOperation(_ operation: FileOperationType, overwrite: Bool = false, progress: Progress? = nil,
completionHandler: SimpleCompletionHandler) -> Progress? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let progress = progress ?? Progress(totalUnitCount: 1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let request = self.request(for: operation, overwrite: overwrite)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
do {
if let error = error {
throw error
}
if let response = response as? HTTPURLResponse {
if response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
throw self.serverError(with: code, path: operation.source, data: data)
}
if FileProviderHTTPErrorCode(rawValue: response.statusCode) == .multiStatus, let data = data,
let ms_error = self.multiStatusError(operation: operation, data: data) {
throw ms_error
}
}
#if os(macOS) || os(iOS) || os(tvOS)
self._registerUndo(operation)
#endif
progress.completedUnitCount = 1
completionHandler?(nil)
self.delegateNotify(operation)
} catch {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
})
task.taskDescription = operation.json
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return progress
}
// codebeat:disable[ARITY]
/**
This method should be used in subclasses to fetch directory content from servers which support paginated results.
Almost all HTTP based provider, except WebDAV, supports this method.
- Important: Please use `[weak self]` when implementing handlers to prevent retain cycles. In these cases,
return `nil` as the result of handler as the operation will be aborted.
- Parameters:
- path: path of directory which enqueued for listing, for informational use like errpr reporting.
- requestHandler: Get token of next page and returns appropriate `URLRequest` to be sent to server.
handler can return `nil` to cancel entire operation.
- token: Token of the page which `URLRequest` is needed, token will be `nil` for initial page.
- pageHandler: Handler which is called after fetching results of a page to parse data. will return parse result as
array of `FileObject` or error if data is nil or parsing is failed. Method will not continue to next page if
`error` is returned, otherwise `nextToken` will be used for next page. `nil` value for `newToken` will indicate
last page of directory contents.
- data: Raw data returned from server. Handler should parse them and return files.
- progress: `Progress` object that `completedUnits` will be increased when a new `FileObject` is parsed in method.
- completionHandler: All file objects returned by `pageHandler` will be passed to this handler, or error if occured.
This handler will be called when `pageHandler` returns `nil for `newToken`.
- contents: all files parsed via `pageHandler` will be return aggregated.
- error: `Error` returned by server. `nil` means success. If exists, it means `contents` are incomplete.
*/
internal func paginated(_ path: String, requestHandler: @escaping (_ token: String?) -> URLRequest?,
pageHandler: @escaping (_ data: Data?, _ progress: Progress) -> (files: [FileObject], error: Error?, newToken: String?),
completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress {
let progress = Progress(totalUnitCount: -1)
self.paginated(path, startToken: nil, currentProgress: progress, previousResult: [], requestHandler: requestHandler, pageHandler: pageHandler, completionHandler: completionHandler)
return progress
}
private func paginated(_ path: String, startToken: String?, currentProgress progress: Progress,
previousResult: [FileObject], requestHandler: @escaping (_ token: String?) -> URLRequest?,
pageHandler: @escaping (_ data: Data?, _ progress: Progress) -> (files: [FileObject], error: Error?, newToken: String?),
completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
guard !progress.isCancelled, let request = requestHandler(startToken) else {
return
}
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
do {
if let error = error {
throw error
}
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
throw self.serverError(with: rCode, path: path, data: data)
}
let (newFiles, err, newToken) = pageHandler(data, progress)
if let error = err {
throw error
}
let files = previousResult + newFiles
if let newToken = newToken, !progress.isCancelled {
_ = self.paginated(path, startToken: newToken, currentProgress: progress, previousResult: files, requestHandler: requestHandler, pageHandler: pageHandler, completionHandler: completionHandler)
} else {
completionHandler(files, nil)
}
} catch {
completionHandler(previousResult, error)
}
})
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
}
// codebeat:enable[ARITY]
internal var maxUploadSimpleSupported: Int64 { return Int64.max }
func upload_task(_ targetPath: String, progress: Progress, task: URLSessionTask, operation: FileOperationType,
completionHandler: SimpleCompletionHandler) -> Void {
var allData = Data()
dataCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { data in
allData.append(data)
}
completionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
var responseError: FileProviderHTTPError?
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = self?.serverError(with: rCode, path: targetPath, data: allData)
}
if !(responseError == nil && error == nil) {
progress.cancel()
}
completionHandler?(responseError ?? error)
self?.delegateNotify(operation, error: responseError ?? error)
}
task.taskDescription = operation.json
sessionDelegate?.observerProgress(of: task, using: progress, kind: .upload)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
}
func upload(_ targetPath: String, request: URLRequest, stream: InputStream, size: Int64, operation: FileOperationType,
completionHandler: SimpleCompletionHandler) -> Progress? {
if size > maxUploadSimpleSupported {
let error = self.serverError(with: .payloadTooLarge, path: targetPath, data: nil)
completionHandler?(error)
self.delegateNotify(operation, error: error)
return nil
}
let progress = Progress(totalUnitCount: size)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
var request = request
request.httpBodyStream = stream
let task = session.uploadTask(withStreamedRequest: request)
self.upload_task(targetPath, progress: progress, task: task, operation: operation, completionHandler: completionHandler)
return progress
}
func upload_file(_ targetPath: String, request: URLRequest, localFile: URL, operation: FileOperationType,
completionHandler: SimpleCompletionHandler) -> Progress? {
let fSize = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.allValues[.fileSizeKey] as? Int64
let size = Int64(fSize ?? -1)
if size > maxUploadSimpleSupported {
let error = self.serverError(with: .payloadTooLarge, path: targetPath, data: nil)
completionHandler?(error)
self.delegateNotify(operation, error: error)
return nil
}
let progress = Progress(totalUnitCount: size)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
#if os(macOS) || os(iOS) || os(tvOS)
var error: NSError?
NSFileCoordinator().coordinate(readingItemAt: localFile, options: .forUploading, error: &error, byAccessor: { (url) in
let task = self.session.uploadTask(with: request, fromFile: localFile)
self.upload_task(targetPath, progress: progress, task: task, operation: operation, completionHandler: completionHandler)
})
if let error = error {
completionHandler?(error)
}
#else
self.upload_task(targetPath, progress: progress, task: task, operation: operation, completionHandler: completionHandler)
#endif
return progress
}
internal func download(path: String, request: URLRequest, operation: FileOperationType,
responseHandler: ((_ response: URLResponse) -> Void)? = nil,
stream: OutputStream,
completionHandler: @escaping (_ error: Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.dataTask(with: request)
if let responseHandler = responseHandler {
responseCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { response in
responseHandler(response)
}
}
stream.open()
dataCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak task, weak self] data in
guard !data.isEmpty else { return }
task.flatMap { self?.delegateNotify(operation, progress: Double($0.countOfBytesReceived) / Double($0.countOfBytesExpectedToReceive)) }
let result = (try? stream.write(data: data)) ?? -1
if result < 0 {
completionHandler(stream.streamError!)
self?.delegateNotify(operation, error: stream.streamError!)
task?.cancel()
}
}
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
stream.close()
completionHandler(error)
self.delegateNotify(operation, error: error)
}
task.taskDescription = operation.json
sessionDelegate?.observerProgress(of: task, using: progress, kind: .download)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return progress
}
internal func download_progressive(path: String, request: URLRequest, operation: FileOperationType,
responseHandler: ((_ response: URLResponse) -> Void)? = nil,
progressHandler: @escaping (_ data: Data) -> Void,
completionHandler: @escaping (_ error: Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.dataTask(with: request)
if let responseHandler = responseHandler {
responseCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { response in
responseHandler(response)
}
}
dataCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak task, weak self] data in
task.flatMap { self?.delegateNotify(operation, progress: Double($0.countOfBytesReceived) / Double($0.countOfBytesExpectedToReceive)) }
progressHandler(data)
}
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
completionHandler(error)
self.delegateNotify(operation, error: error)
}
task.taskDescription = operation.json
sessionDelegate?.observerProgress(of: task, using: progress, kind: .download)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return progress
}
internal func download_file(path: String, request: URLRequest, operation: FileOperationType,
completionHandler: @escaping ((_ tempURL: URL?, _ error: Error?) -> Void)) -> Progress? {
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.downloadTask(with: request)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if let error = error {
progress.cancel()
completionHandler(nil, error)
self.delegateNotify(operation, error: error)
}
}
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1)
let errorData : Data? = try? Data(contentsOf: tempURL)
let serverError = code.flatMap { self.serverError(with: $0, path: path, data: errorData) }
if serverError != nil {
progress.cancel()
}
completionHandler(nil, serverError)
self.delegateNotify(operation)
return
}
completionHandler(tempURL, nil)
}
task.taskDescription = operation.json
sessionDelegate?.observerProgress(of: task, using: progress, kind: .download)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return progress
}
}
extension HTTPFileProvider: FileProvider { }
#if os(macOS) || os(iOS) || os(tvOS)
extension HTTPFileProvider: FileProvideUndoable { }
#endif