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

978 lines
43 KiB
Swift

//
// FTPFileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2017 Mousavian. Distributed under MIT license.
//
import Foundation
/**
Allows accessing to FTP files and directories. This provider doesn't cache or save files internally.
It's a complete reimplementation and doesn't use CFNetwork deprecated API.
*/
open class FTPFileProvider: NSObject, FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite, FileProviderReadWriteProgressive {
/// FTP data connection mode.
public enum Mode: String {
/// Passive mode for FTP and Extended Passive mode for FTP over TLS.
case `default`
/// Data connection would establish by client to determined server host/port.
case passive
/// Data connection would establish by server to determined client's port.
case active
/// Data connection would establish by client to determined server host/port, with IPv6 support. (RFC 2428)
case extendedPassive
}
open class var type: String { return "FTP" }
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
/// Determine either FTP session is in passive or active mode.
public let mode: Mode
fileprivate var _session: URLSession!
internal var sessionDelegate: SessionDelegate?
public var session: URLSession {
get {
if _session == nil {
self.sessionDelegate = SessionDelegate(fileProvider: self)
let config = URLSessionConfiguration.default
_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!)
}
}
#if os(macOS) || os(iOS) || os(tvOS)
open var undoManager: UndoManager? = nil
#endif
/**
Initializer for FTP provider with given username and password.
- Note: `passive` value should be set according to server settings and firewall presence.
- Parameter baseURL: a url with `ftp://hostaddress/` format.
- Parameter mode: FTP server data connection type.
- Parameter credential: a `URLCredential` object contains user and password.
- Parameter cache: A URLCache to cache downloaded files and contents. (unimplemented for FTP and should be nil)
- Important: Extended Passive or Active modes will fallback to normal Passive or Active modes if your server
does not support extended modes.
*/
public init? (baseURL: URL, mode: Mode = .default, credential: URLCredential? = nil, cache: URLCache? = nil) {
guard ["ftp", "ftps", "ftpes"].contains(baseURL.uw_scheme.lowercased()) else {
return nil
}
guard baseURL.host != nil else { return nil }
var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: true)!
let defaultPort: Int = baseURL.scheme?.lowercased() == "ftps" ? 990 : 21
urlComponents.port = urlComponents.port ?? defaultPort
urlComponents.scheme = urlComponents.scheme ?? "ftp"
urlComponents.path = urlComponents.path.hasSuffix("/") ? urlComponents.path : urlComponents.path + "/"
self.baseURL = urlComponents.url!.absoluteURL
self.mode = mode
self.useCache = false
self.validatingCache = true
self.cache = cache
self.credential = credential
self.supportsRFC3659 = true
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()
}
/**
**DEPRECATED** Initializer for FTP provider with given username and password.
- Note: `passive` value should be set according to server settings and firewall presence.
- Parameter baseURL: a url with `ftp://hostaddress/` format.
- Parameter passive: FTP server data connection, `true` means passive connection (data connection created by client)
and `false` means active connection (data connection created by server). Default is `true` (passive mode).
- Parameter credential: a `URLCredential` object contains user and password.
- Parameter cache: A URLCache to cache downloaded files and contents. (unimplemented for FTP and should be nil)
*/
@available(*, deprecated, renamed: "init(baseURL:mode:credential:cache:)")
public convenience init? (baseURL: URL, passive: Bool, credential: URLCredential? = nil, cache: URLCache? = nil) {
self.init(baseURL: baseURL, mode: passive ? .passive : .active, credential: credential, cache: cache)
}
public required convenience init?(coder aDecoder: NSCoder) {
guard let baseURL = aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL? else {
if #available(macOS 10.11, iOS 9.0, tvOS 9.0, *) {
aDecoder.failWithError(CocoaError(.coderValueNotFound,
userInfo: [NSLocalizedDescriptionKey: "Base URL is not set."]))
}
return nil
}
let mode: Mode
if let modeStr = aDecoder.decodeObject(of: NSString.self, forKey: "mode") as String?, let mode_v = Mode(rawValue: modeStr) {
mode = mode_v
} else {
let passiveMode = aDecoder.decodeBool(forKey: "passiveMode")
mode = passiveMode ? .passive : .active
}
self.init(baseURL: baseURL, mode: mode, credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential"))
self.useCache = aDecoder.decodeBool(forKey: "useCache")
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
self.supportsRFC3659 = aDecoder.decodeBool(forKey: "supportsRFC3659")
self.securedDataConnection = aDecoder.decodeBool(forKey: "securedDataConnection")
}
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")
aCoder.encode(self.mode.rawValue, forKey: "mode")
aCoder.encode(self.supportsRFC3659, forKey: "supportsRFC3659")
aCoder.encode(self.securedDataConnection, forKey: "securedDataConnection")
}
public static var supportsSecureCoding: Bool {
return true
}
open func copy(with zone: NSZone? = nil) -> Any {
let copy = FTPFileProvider(baseURL: self.baseURL!, mode: self.mode, credential: self.credential, cache: self.cache)!
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
copy.useCache = self.useCache
copy.validatingCache = self.validatingCache
copy.securedDataConnection = self.securedDataConnection
copy.supportsRFC3659 = self.supportsRFC3659
return copy
}
deinit {
if let sessionuuid = _session?.sessionDescription {
removeSessionHandler(for: sessionuuid)
}
if fileProviderCancelTasksOnInvalidating {
_session?.invalidateAndCancel()
} else {
_session?.finishTasksAndInvalidate()
}
}
internal var supportsRFC3659: Bool
/**
Uploads files in chunk if `true`, Otherwise It will uploads entire file/data as single stream.
- Note: Due to an internal bug in `NSURLSessionStreamTask`, it must be `true` when using Apple's stream task,
otherwise it will occasionally throw `Assertion failed: (_writeBufferAlreadyWrittenForNextWrite == 0)`
fatal error. My implementation of `FileProviderStreamTask` doesn't have this bug.
- Note: Disabling this option will increase upload speed.
*/
public var uploadByREST: Bool = false
/**
Determines data connection must TLS or not. `false` value indicates to use `PROT C` and
`true` value indicates to use `PROT P`. Default is `true`.
*/
public var securedDataConnection: Bool = true
/**
Trust all certificates if `disableEvaluation`, Otherwise validate certificate chain.
Default is `performDefaultEvaluation`.
*/
public var serverTrustPolicy: ServerTrustPolicy = .performDefaultEvaluation(validateHost: true)
open func contentsOfDirectory(path: String, completionHandler: @escaping ([FileObject], Error?) -> Void) {
self.contentsOfDirectory(path: path, rfc3659enabled: supportsRFC3659, completionHandler: completionHandler)
}
/**
Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler.
If the directory contains no entries or an error is occured, this method will return the empty array.
- Parameter path: path to target directory. If empty, root will be iterated.
- Parameter rfc3659enabled: uses MLST command instead of old LIST to get files attributes, default is `true`.
- Parameter completionHandler: a closure with result of directory entries or error.
- Parameter contents: An array of `FileObject` identifying the the directory entries.
- Parameter error: Error returned by system.
*/
open func contentsOfDirectory(path apath: String, rfc3659enabled: Bool , completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
let path = ftpPath(apath)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
task.serverTrustPolicy = serverTrustPolicy
task.taskDescription = FileOperationType.fetch(path: path).json
self.ftpLogin(task) { (error) in
if let error = error {
self.dispatch_queue.async {
completionHandler([], error)
}
return
}
self.ftpList(task, of: self.ftpPath(path), useMLST: rfc3659enabled, completionHandler: { (contents, error) in
defer {
self.ftpQuit(task)
}
if let error = error {
if let uerror = error as? URLError, uerror.code == .unsupportedURL {
self.contentsOfDirectory(path: path, rfc3659enabled: false, completionHandler: completionHandler)
return
}
self.dispatch_queue.async {
completionHandler([], error)
}
return
}
let files: [FileObject] = contents.compactMap {
rfc3659enabled ? self.parseMLST($0, in: path) : (self.parseUnixList($0, in: path) ?? self.parseDOSList($0, in: path))
}
self.dispatch_queue.async {
completionHandler(files, nil)
}
})
}
}
open func attributesOfItem(path: String, completionHandler: @escaping (FileObject?, Error?) -> Void) {
self.attributesOfItem(path: path, rfc3659enabled: supportsRFC3659, completionHandler: completionHandler)
}
/**
Returns a `FileObject` containing the attributes of the item (file, directory, symlink, etc.) at the path in question via asynchronous completion handler.
If the directory contains no entries or an error is occured, this method will return the empty `FileObject`.
- Parameter path: path to target directory. If empty, attributes of root will be returned.
- Parameter rfc3659enabled: uses MLST command instead of old LIST to get files attributes, default is true.
- Parameter completionHandler: a closure with result of directory entries or error.
`attributes`: A `FileObject` containing the attributes of the item.
`error`: Error returned by system.
*/
open func attributesOfItem(path apath: String, rfc3659enabled: Bool, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
let path = ftpPath(apath)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
task.serverTrustPolicy = serverTrustPolicy
task.taskDescription = FileOperationType.fetch(path: path).json
self.ftpLogin(task) { (error) in
if let error = error {
self.dispatch_queue.async {
completionHandler(nil, error)
}
return
}
let command = rfc3659enabled ? "MLST \(path)" : "LIST \(path)"
self.execute(command: command, on: task, completionHandler: { (response, error) in
defer {
self.ftpQuit(task)
}
do {
if let error = error {
throw error
}
guard let response = response, response.hasPrefix("250") || (response.hasPrefix("50") && rfc3659enabled) else {
throw URLError(.badServerResponse, url: self.url(of: path))
}
if response.hasPrefix("500") {
self.supportsRFC3659 = false
self.attributesOfItem(path: path, rfc3659enabled: false, completionHandler: completionHandler)
}
let lines = response.components(separatedBy: "\n").compactMap { $0.isEmpty ? nil : $0.trimmingCharacters(in: .whitespacesAndNewlines) }
guard lines.count > 2 else {
throw URLError(.badServerResponse, url: self.url(of: path))
}
let dirPath = path.deletingLastPathComponent
let file: FileObject? = rfc3659enabled ?
self.parseMLST(lines[1], in: dirPath) :
(self.parseUnixList(lines[1], in: dirPath) ?? self.parseDOSList(lines[1], in: dirPath))
self.dispatch_queue.async {
completionHandler(file, nil)
}
} catch {
self.dispatch_queue.async {
completionHandler(nil, error)
}
}
})
}
}
open func storageProperties(completionHandler: @escaping (_ volume: VolumeObject?) -> Void) {
dispatch_queue.async {
completionHandler(nil)
}
}
@discardableResult
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: -1)
if recursive {
return self.recursiveList(path: path, useMLST: true, foundItemsHandler: { items in
if let foundItemHandler = foundItemHandler {
for item in items where query.evaluate(with: item.mapPredicate()) {
foundItemHandler(item)
}
progress.totalUnitCount = Int64(items.count)
}
}, completionHandler: {files, error in
if let error = error {
completionHandler([], error)
return
}
let foundFiles = files.filter { query.evaluate(with: $0.mapPredicate()) }
completionHandler(foundFiles, nil)
})
} else {
self.contentsOfDirectory(path: path, completionHandler: { (items, error) in
if let error = error {
completionHandler([], error)
return
}
var result = [FileObject]()
for item in items where query.evaluate(with: item.mapPredicate()) {
foundItemHandler?(item)
result.append(item)
}
completionHandler(result, nil)
})
}
return progress
}
open func url(of path: String?) -> URL {
let path = path?.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? (path ?? "")
var baseUrlComponent = URLComponents(url: self.baseURL!, resolvingAgainstBaseURL: true)
baseUrlComponent?.user = credential?.user
baseUrlComponent?.password = credential?.password
return URL(string: path, relativeTo: baseUrlComponent?.url ?? baseURL) ?? baseUrlComponent?.url ?? baseURL!
}
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)
}
if !relativePath.isEmpty, self.baseURL == self.url(of: "/") {
return (relativePath.removingPercentEncoding ?? relativePath).replacingOccurrences(of: "/", with: "", options: .anchored)
}
return relativePath.replacingOccurrences(of: "/", with: "", options: .anchored)
}
open func isReachable(completionHandler: @escaping (_ success: Bool, _ error: Error?) -> Void) {
self.attributesOfItem(path: "/") { (file, error) in
completionHandler(file != nil, error)
}
}
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), completionHandler: completionHandler)
}
@discardableResult
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.copy(source: path, destination: toPath), 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? {
guard (try? localFile.checkResourceIsReachable()) ?? false else {
dispatch_queue.async {
completionHandler?(URLError(.fileDoesNotExist, url: localFile))
}
return nil
}
// 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 progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
task.serverTrustPolicy = serverTrustPolicy
task.taskDescription = operation.json
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
self.ftpLogin(task) { (error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
return
}
guard let stream = InputStream(url: localFile) else {
return
}
let size = localFile.fileSize
self.ftpStore(task, filePath: self.ftpPath(toPath), from: stream, size: size, onTask: { task in
weak var weakTask = task
progress.cancellationHandler = {
weakTask?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { bytesSent, totalSent, expectedBytes in
progress.totalUnitCount = expectedBytes
progress.completedUnitCount = totalSent
self.delegateNotify(operation, progress: progress.fractionCompleted)
}, completionHandler: { (error) in
if error != nil {
progress.cancel()
}
self.ftpQuit(task)
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
})
}
return progress
}
@discardableResult
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
task.serverTrustPolicy = serverTrustPolicy
task.taskDescription = operation.json
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
self.ftpLogin(task) { (error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
}
return
}
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory().appendingPathComponent(UUID().uuidString))
guard let stream = OutputStream(url: tempURL, append: false) else {
completionHandler?(CocoaError(.fileWriteUnknown, path: destURL.path))
return
}
self.ftpDownload(task, filePath: self.ftpPath(path), to: stream, onTask: { task in
weak var weakTask = task
progress.cancellationHandler = {
weakTask?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { recevied, totalReceived, totalSize in
progress.totalUnitCount = totalSize
progress.completedUnitCount = totalReceived
self.delegateNotify(operation, progress: progress.fractionCompleted)
}) { (error) in
if error != nil {
progress.cancel()
}
do {
try FileManager.default.moveItem(at: tempURL, to: destURL)
} catch {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
try? FileManager.default.removeItem(at: tempURL)
return
}
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
}
}
return progress
}
@discardableResult
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
let operation = FileOperationType.fetch(path: path)
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
self.delegateNotify(operation)
}
return nil
}
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
task.serverTrustPolicy = serverTrustPolicy
task.taskDescription = operation.json
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
self.ftpLogin(task) { (error) in
if let error = error {
self.dispatch_queue.async {
completionHandler(nil, error)
}
return
}
let stream = OutputStream.toMemory()
self.ftpDownload(task, filePath: self.ftpPath(path), from: offset, length: length, to: stream, onTask: { task in
weak var weakTask = task
progress.cancellationHandler = {
weakTask?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { recevied, totalReceived, totalSize in
progress.totalUnitCount = totalSize
progress.completedUnitCount = totalReceived
self.delegateNotify(operation, progress: progress.fractionCompleted)
}) { (error) in
if let error = error {
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, error)
self.delegateNotify(operation, error: error)
}
return
}
if let data = stream.property(forKey: .dataWrittenToMemoryStreamKey) as? Data {
self.dispatch_queue.async {
completionHandler(data, nil)
self.delegateNotify(operation)
}
}
}
}
return progress
}
@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 progress = Progress(totalUnitCount: Int64(data?.count ?? -1))
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
task.serverTrustPolicy = serverTrustPolicy
task.taskDescription = operation.json
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
self.ftpLogin(task) { (error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
return
}
let storeHandler = {
let data = data ?? Data()
let stream = InputStream(data: data)
self.ftpStore(task, filePath: self.ftpPath(path), from: stream, size: Int64(data.count), onTask: { task in
weak var weakTask = task
progress.cancellationHandler = {
weakTask?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { bytesSent, totalSent, expectedBytes in
progress.completedUnitCount = totalSent
self.delegateNotify(operation, progress: progress.fractionCompleted)
}, completionHandler: { (error) in
if error != nil {
progress.cancel()
}
self.ftpQuit(task)
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
})
}
if overwrite {
storeHandler()
} else {
self.attributesOfItem(path: path, completionHandler: { (file, erroe) in
if file == nil {
storeHandler()
}
})
}
}
return progress
}
public func contents(path: String, offset: Int64, length: Int, responseHandler: ((URLResponse) -> Void)?, progressHandler: @escaping (Int64, Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.fetch(path: path)
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler?(nil)
self.delegateNotify(operation)
}
return nil
}
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
task.serverTrustPolicy = serverTrustPolicy
task.taskDescription = operation.json
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
self.ftpLogin(task) { (error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
}
return
}
self.ftpDownloadData(task, filePath: self.ftpPath(path), from: offset, length: length, onTask: { task in
weak var weakTask = task
progress.cancellationHandler = {
weakTask?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { data, recevied, totalReceived, totalSize in
progressHandler(totalReceived - recevied, data)
progress.totalUnitCount = totalSize
progress.completedUnitCount = totalReceived
self.delegateNotify(operation, progress: progress.fractionCompleted)
}) { (data, error) in
if let error = error {
progress.cancel()
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
return
}
self.dispatch_queue.async {
completionHandler?(nil)
self.delegateNotify(operation)
}
}
}
return progress
}
/**
Creates a symbolic link at the specified path that points to an item at the given path.
This method does not traverse symbolic links contained in destination path, making it possible
to create symbolic links to locations that do not yet exist.
Also, if the final path component is a symbolic link, that link is not followed.
- Note: Many servers does't support this functionality.
- Parameters:
- symbolicLink: The file path at which to create the new symbolic link. The last component of the path issued as the name of the link.
- withDestinationPath: The path that contains the item to be pointed to by the link. In other words, this is the destination of the link.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
*/
open func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
let operation = FileOperationType.link(link: path, target: destPath)
_=self.doOperation(operation, completionHandler: completionHandler)
}
}
extension FTPFileProvider {
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let sourcePath = operation.source
let destPath = operation.destination
let command: String
switch operation {
case .create: command = "MKD \(ftpPath(sourcePath))"
case .copy: command = "SITE CPFR \(ftpPath(sourcePath))\r\nSITE CPTO \(ftpPath(destPath!))"
case .move: command = "RNFR \(ftpPath(sourcePath))\r\nRNTO \(ftpPath(destPath!))"
case .remove: command = "DELE \(ftpPath(sourcePath))"
case .link: command = "SITE SYMLINK \(ftpPath(sourcePath)) \(ftpPath(destPath!))"
default: return nil // modify, fetch
}
let progress = Progress(totalUnitCount: 1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
task.serverTrustPolicy = serverTrustPolicy
task.taskDescription = operation.json
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
self.ftpLogin(task) { (error) in
if let error = error {
completionHandler?(error)
self.delegateNotify(operation, error: error)
return
}
self.execute(command: command, on: task, completionHandler: { (response, error) in
if let error = error {
completionHandler?(error)
self.delegateNotify(operation, error: error)
return
}
guard let response = response else {
completionHandler?(error)
self.delegateNotify(operation, error: URLError(.badServerResponse, url: self.url(of: sourcePath)))
return
}
let codes: [Int] = response.components(separatedBy: .newlines).compactMap({ $0.isEmpty ? nil : $0})
.compactMap {
let code = $0.components(separatedBy: .whitespaces).compactMap({ $0.isEmpty ? nil : $0}).first
return code != nil ? Int(code!) : nil
}
if codes.filter({ (450..<560).contains($0) }).count > 0 {
let errorCode: URLError.Code
switch operation {
case .create: errorCode = .cannotCreateFile
case .modify: errorCode = .cannotWriteToFile
case .copy:
self.fallbackCopy(operation, progress: progress, completionHandler: completionHandler)
return
case .move: errorCode = .cannotMoveFile
case .remove:
self.fallbackRemove(operation, progress: progress, on: task, completionHandler: completionHandler)
return
case .link: errorCode = .cannotWriteToFile
default: errorCode = .cannotOpenFile
}
let error = URLError(errorCode, url: self.url(of: sourcePath))
progress.cancel()
completionHandler?(error)
self.delegateNotify(operation, error: error)
return
}
#if os(macOS) || os(iOS) || os(tvOS)
self._registerUndo(operation)
#endif
progress.completedUnitCount = progress.totalUnitCount
completionHandler?(nil)
self.delegateNotify(operation)
})
}
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
return progress
}
private func fallbackCopy(_ operation: FileOperationType, progress: Progress, completionHandler: SimpleCompletionHandler) {
let sourcePath = operation.source
guard let destPath = operation.destination else { return }
let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension("tmp")
progress.becomeCurrent(withPendingUnitCount: 1)
_ = self.copyItem(path: sourcePath, toLocalURL: localURL) { (error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
return
}
progress.becomeCurrent(withPendingUnitCount: 1)
_ = self.copyItem(localFile: localURL, to: destPath) { error in
completionHandler?(nil)
self.delegateNotify(operation)
}
progress.resignCurrent()
}
progress.resignCurrent()
return
}
private func fallbackRemove(_ operation: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
let sourcePath = operation.source
self.execute(command: "SITE RMDIR \(ftpPath(sourcePath))", on: task) { (response, error) in
do {
if let error = error {
throw error
}
guard let response = response else {
throw URLError(.badServerResponse, url: self.url(of: sourcePath))
}
if response.hasPrefix("50") {
self.fallbackRecursiveRemove(operation, progress: progress, on: task, completionHandler: completionHandler)
return
}
if !response.hasPrefix("2") {
throw URLError(.cannotRemoveFile, url: self.url(of: sourcePath))
}
self.dispatch_queue.async {
completionHandler?(nil)
}
self.delegateNotify(operation)
} catch {
progress.cancel()
self.dispatch_queue.async {
completionHandler?(error)
}
self.delegateNotify(operation, error: error)
}
}
}
private func fallbackRecursiveRemove(_ operation: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
let sourcePath = operation.source
_ = self.recursiveList(path: sourcePath, useMLST: true, completionHandler: { (contents, error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
return
}
progress.becomeCurrent(withPendingUnitCount: 1)
let recursiveProgress = Progress(totalUnitCount: Int64(contents.count))
let sortedContents = contents.sorted(by: {
$0.path.localizedStandardCompare($1.path) == .orderedDescending
})
progress.resignCurrent()
var command = ""
for file in sortedContents {
command += (file.isDirectory ? "RMD \(self.ftpPath(file.path))" : "DELE \(self.ftpPath(file.path))") + "\r\n"
}
command += "RMD \(self.ftpPath(sourcePath))"
self.execute(command: command, on: task, completionHandler: { (response, error) in
recursiveProgress.completedUnitCount += 1
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
// TODO: Digest response
})
})
}
}
extension FTPFileProvider: FileProvider { }
#if os(macOS) || os(iOS) || os(tvOS)
extension FTPFileProvider: FileProvideUndoable { }
#endif