Compare commits

...

6 Commits

Author SHA1 Message Date
Amir Abbas 5be8c4ef5e podspec and version update 2017-06-27 14:02:00 +04:30
Amir Abbas 0374fd7688 Making ftp active data task creation async 2017-06-24 12:09:16 +04:30
Amir Abbas 5ddfa43555 Fix #49 (Workaround colon in url path bug in NSURL relative url) 2017-06-24 12:04:50 +04:30
Amir Abbas 21850bb548 Probable fix for #51 2017-06-24 11:44:12 +04:30
Amir Abbas b166e111e0 FTP Active mode, FTPS bugfix, possible fix for #47 2017-05-25 17:33:56 +04:30
Amir Abbas 1dd7561215 Caching ftp server support for RFC3659, Possible fix for #47 2017-05-23 19:10:04 +04:30
7 changed files with 89 additions and 67 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
#
s.name = "FileProvider"
s.version = "0.16.2"
s.version = "0.17.0"
s.summary = "FileManager replacement for Local and Remote (WebDAV/FTP/Dropbox/OneDrive/SMB2) files on iOS and macOS."
# This description is used to generate tags and improve search results.
+4 -8
View File
@@ -621,7 +621,7 @@
799396601D48B7BF00086753 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.16.2;
BUNDLE_VERSION_STRING = 0.17.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -645,6 +645,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = FileProvider;
SWIFT_VERSION = 3.0;
};
name = Debug;
@@ -652,7 +653,7 @@
799396611D48B7BF00086753 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.16.2;
BUNDLE_VERSION_STRING = 0.17.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -672,6 +673,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
PRODUCT_NAME = FileProvider;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 3.0;
};
@@ -719,7 +721,6 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-iOS";
PRODUCT_NAME = FileProvider;
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -766,7 +767,6 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-iOS";
PRODUCT_NAME = FileProvider;
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -823,7 +823,6 @@
MACOSX_DEPLOYMENT_TARGET = 10.10;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-OSX";
PRODUCT_NAME = FileProvider;
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -874,7 +873,6 @@
MACOSX_DEPLOYMENT_TARGET = 10.10;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-OSX";
PRODUCT_NAME = FileProvider;
SDKROOT = macosx;
SKIP_INSTALL = YES;
VERSIONING_SYSTEM = "apple-generic";
@@ -926,7 +924,6 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-tvOS";
PRODUCT_NAME = FileProvider;
SDKROOT = appletvos;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -976,7 +973,6 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-tvOS";
PRODUCT_NAME = FileProvider;
SDKROOT = appletvos;
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = 3;
+1
View File
@@ -65,6 +65,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
return _taskDescription
}
@objc(setTaskDescription:)
set {
if #available(iOS 9.0, OSX 10.11, *) {
if self.useURLSession {
+12 -6
View File
@@ -35,9 +35,7 @@ open class FTPFileProvider: FileProviderBasicRemote {
public var validatingCache: Bool
/// Determine either FTP session is in passive or active mode.
/// - Note: Due to `URLSessionStreamTask` restrictions for determining listening port,
/// only passive sessions are available in current implementation.
public let passiveMode = true
public let passiveMode: Bool
/// Force to use URLSessionDownloadTask/URLSessionDataTask when possible
public var useAppleImplementation = true
@@ -72,11 +70,15 @@ open class FTPFileProvider: FileProviderBasicRemote {
/**
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)
*/
public init? (baseURL: URL, credential: URLCredential? = nil, cache: URLCache? = nil) {
public init? (baseURL: URL, passive: Bool = true, credential: URLCredential? = nil, cache: URLCache? = nil) {
guard (baseURL.scheme ?? "ftp").lowercased().hasPrefix("ftp") else { return nil }
guard baseURL.host != nil else { return nil }
var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: true)!
@@ -84,6 +86,7 @@ open class FTPFileProvider: FileProviderBasicRemote {
urlComponents.scheme = urlComponents.scheme ?? "ftp"
self.baseURL = (urlComponents.url!.path.hasSuffix("/") ? urlComponents.url! : urlComponents.url!.appendingPathComponent("")).absoluteURL
self.passiveMode = passive
self.currentPath = ""
self.useCache = false
self.validatingCache = true
@@ -140,8 +143,10 @@ open class FTPFileProvider: FileProviderBasicRemote {
}
}
internal var serverSupportsRFC3659: Bool = true
open func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) {
self.contentsOfDirectory(path: path, rfc3659enabled: true, completionHandler: completionHandler)
self.contentsOfDirectory(path: path, rfc3659enabled: serverSupportsRFC3659, completionHandler: completionHandler)
}
/**
@@ -197,7 +202,7 @@ open class FTPFileProvider: FileProviderBasicRemote {
}
open func attributesOfItem(path: String, completionHandler: @escaping ((FileObject?, Error?) -> Void)) {
self.attributesOfItem(path: path, rfc3659enabled: true, completionHandler: completionHandler)
self.attributesOfItem(path: path, rfc3659enabled: serverSupportsRFC3659, completionHandler: completionHandler)
}
/**
@@ -243,6 +248,7 @@ open class FTPFileProvider: FileProviderBasicRemote {
}
if response.hasPrefix("500") {
self.serverSupportsRFC3659 = false
self.attributesOfItem(path: path, rfc3659enabled: false, completionHandler: completionHandler)
}
+68 -51
View File
@@ -65,7 +65,7 @@ internal extension FTPFileProvider {
}
if let data = data, let response = String(data: data, encoding: .utf8) {
completionHandler(response.trimmingCharacters(in: CharacterSet(charactersIn: "\r\n")), nil)
completionHandler(response.trimmingCharacters(in: .whitespacesAndNewlines), nil)
} else {
completionHandler(nil, self.throwError("", code: URLError.cannotParseResponse))
return
@@ -94,10 +94,7 @@ internal extension FTPFileProvider {
}
guard response.hasPrefix("22") else {
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
let error = FileProviderFTPError(message: response)
completionHandler(error)
return
}
@@ -123,7 +120,7 @@ internal extension FTPFileProvider {
// needs password
if response.hasPrefix("33") {
self.execute(command: "PASS \(credential?.password ?? "fileprovider@")", on: task) { (response, error) in
if response?.hasPrefix("2") ?? false {
if response?.hasPrefix("23") ?? false {
completionHandler(nil)
} else {
completionHandler(self.throwError("", code: URLError.userAuthenticationRequired))
@@ -132,10 +129,7 @@ internal extension FTPFileProvider {
return
}
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
let error = FileProviderFTPError(message: response)
completionHandler(error)
return
}
@@ -143,8 +137,23 @@ internal extension FTPFileProvider {
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
self.execute(command: "AUTH TLS", on: task, minLength: 0, completionHandler: { (response, error) in
task.startSecureConnection()
loginHandle()
if let error = error {
completionHandler(error)
return
}
if let response = response, response.hasPrefix("23") {
task.startSecureConnection()
self.execute(command: "PBSZ 0\r\nPROT P", on: task, completionHandler: { (response, error) in
if let error = error {
completionHandler(error)
return
}
loginHandle()
})
}
})
} else {
loginHandle()
@@ -170,10 +179,7 @@ internal extension FTPFileProvider {
}
// not logged in
else if response.hasPrefix("55") {
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
let error = FileProviderFTPError(message: response)
completionHandler(error)
return
}
@@ -214,26 +220,25 @@ internal extension FTPFileProvider {
let passiveTask = self.session.fpstreamTask(withHostName: host, port: port)
passiveTask.resume()
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
task.startSecureConnection()
passiveTask.startSecureConnection()
}
completionHandler(passiveTask, nil)
}
}
func ftpActive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
var port: Int32 = 0
var _activeTask: FileProviderStreamTask?
while (_activeTask?.state ?? .suspended) == .suspended {
port = 32000 + Int32(arc4random_uniform(16384))
let service = NetService(domain: "", type: "_tcp.", name: "", port: port)
_activeTask = self.session.fpstreamTask(withNetService: service)
_activeTask?.resume()
let service = NetService(domain: "", type: "_tcp.", name: "", port: 0)
service.publish(options: .listenForConnections)
let startTime = Date()
while service.port < 1 && startTime.timeIntervalSinceNow > -self.session.configuration.timeoutIntervalForRequest {
usleep(100_000)
}
guard let activeTask = _activeTask else { return }
let activeTask = self.session.fpstreamTask(withNetService: service)
activeTask.resume()
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
task.startSecureConnection()
activeTask.startSecureConnection()
}
self.execute(command: "PORT \(port)", on: task) { (response, error) in
self.execute(command: "PORT \(service.port)", on: task) { (response, error) in
if let error = error {
activeTask.cancel()
completionHandler(nil, error)
@@ -241,11 +246,13 @@ internal extension FTPFileProvider {
}
guard let response = response else {
activeTask.cancel()
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
return
}
guard !response.hasPrefix("5") else {
activeTask.cancel()
completionHandler(nil, self.throwError("", code: URLError.cannotConnectToHost))
return
}
@@ -258,7 +265,9 @@ internal extension FTPFileProvider {
if self.passiveMode {
self.ftpPassive(task, completionHandler: completionHandler)
} else {
self.ftpActive(task, completionHandler: completionHandler)
dispatch_queue.async {
self.ftpActive(task, completionHandler: completionHandler)
}
}
}
@@ -270,13 +279,15 @@ internal extension FTPFileProvider {
}
// Successful
if response?.hasPrefix("35") ?? false {
guard let response = response else {
completionHandler(self.throwError("", code: URLError.badServerResponse))
return
}
if response.hasPrefix("35") {
completionHandler(nil)
} else {
let spaceIndex = response?.characters.index(of: "-") ?? response?.startIndex
let code = Int((response?.substring(to: spaceIndex!).trimmingCharacters(in: .whitespacesAndNewlines))!) ?? -1
let description = response?.substring(from: spaceIndex!).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
let error = FileProviderFTPError(message: response, path: "")
completionHandler(error)
return
}
@@ -352,18 +363,15 @@ internal extension FTPFileProvider {
return
}
if response.hasPrefix("50") && useMLST {
if response.hasPrefix("500") && useMLST {
dataTask.cancel()
self.serverSupportsRFC3659 = false
completionHandler([], self.throwError(path, code: URLError.unsupportedURL))
return
}
if !success && !(response.hasPrefix("25") || response.hasPrefix("15")) {
let spaceIndex = response.characters.index(of: " ") ?? response.startIndex
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: path, errorDescription: description)
let error = FileProviderFTPError(message: response, path: path)
self.dispatch_queue.async {
completionHandler([], error)
}
@@ -504,10 +512,7 @@ internal extension FTPFileProvider {
}
if !(response.hasPrefix("1") || !response.hasPrefix("2")) {
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
let error = FileProviderFTPError(message: response)
self.dispatch_queue.async {
completionHandler(nil, error)
@@ -620,10 +625,7 @@ internal extension FTPFileProvider {
}
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
let error = FileProviderFTPError(message: response)
self.dispatch_queue.async {
completionHandler(nil, error)
@@ -748,10 +750,7 @@ internal extension FTPFileProvider {
}
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
let error = FileProviderFTPError(message: response)
self.dispatch_queue.async {
completionHandler(error)
@@ -921,4 +920,22 @@ public struct FileProviderFTPError: Error {
public let path: String
/// Contents returned by server as error description
public let errorDescription: String?
init(code: Int, path: String, errorDescription: String?) {
self.code = code
self.path = path
self.errorDescription = errorDescription
}
init(message response: String, path: String = "") {
let message = response.components(separatedBy: .newlines).last ?? "No Response"
let spaceIndex = message.characters.index(of: "-") ?? message.characters.index(of: " ") ?? message.startIndex
self.code = Int(message.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
self.path = path
if code > 0 {
self.errorDescription = message.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
} else {
self.errorDescription = message
}
}
}
+2
View File
@@ -72,6 +72,8 @@ open class RemoteOperationHandle: OperationHandle {
/// A protocol defines properties for errors returned by HTTP/S based providers.
/// Including Dropbox, OneDrive and WebDAV.
public protocol FileProviderHTTPError: Error, CustomStringConvertible {
/// HTTP status codes as an enum.
typealias Code = FileProviderHTTPErrorCode
/// HTTP status code returned for error by server.
var code: FileProviderHTTPErrorCode { get }
/// Path of file/folder casued that error
+1 -1
View File
@@ -660,7 +660,7 @@ struct DavResponse {
func standardizePath(_ str: String) -> String {
let trimmedStr = str.hasPrefix("/") ? str.substring(from: str.index(after: str.startIndex)) : str
return trimmedStr.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? str
return trimmedStr.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed.subtracting(CharacterSet(charactersIn: ":"))) ?? str
}
// find node names with namespace