Compare commits

...

7 Commits

Author SHA1 Message Date
Amir Abbas Mousavian 6c34a4e9a8 Updated version, removed obsoleted properties 2018-03-27 20:56:16 +04:30
Amir Abbas 37ce9c95fc FTP provider fallbacks from EPSV to PASV if extended is not implmented 2018-03-13 10:53:26 +03:30
Amir Abbas b39c1c4e82 Fixed FTP issues. (Partial fix #88)
- Fixed FTPS connectivity on data connection
- Fixed FTP attributesOfItem()
- More verbose testing
2018-03-12 18:26:35 +03:30
Amir Abbas 8f0cbf8513 Support FTP EPSV mode, property to set data connection TLS 2018-03-09 02:24:56 +03:30
Amir Abbas 2690551b7f Fixed FTP/TLS, Support FTP listing on Windows servers 2018-03-08 11:50:14 +03:30
Amir Abbas a6550b0ec3 Fixed utf8 filenames issue in OneDrive 2018-03-07 09:56:53 +03:30
Amir Abbas 4b0fffc691 Fix Test building error 2018-03-06 20:24:00 +03:30
8 changed files with 259 additions and 66 deletions
+3 -6
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
#
s.name = "FilesProvider"
s.version = "0.23.0"
s.version = "0.24.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.
@@ -66,10 +66,7 @@ Pod::Spec.new do |s|
# the deployment target. You can optionally include the target after the platform.
#
# s.platform = :ios
# s.platform = :ios, "8.0"
# When using multiple platforms
s.swift_version = "4.0"
s.ios.deployment_target = "8.0"
s.osx.deployment_target = "10.10"
# s.watchos.deployment_target = "2.0"
@@ -122,7 +119,7 @@ Pod::Spec.new do |s|
# s.framework = "SomeFramework"
# s.frameworks = "SomeFramework", "AnotherFramework"
# s.library = "iconv"
s.library = "xml2"
# s.libraries = "iconv", "xml2"
+2 -4
View File
@@ -720,7 +720,7 @@
799396601D48B7BF00086753 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.22.0;
BUNDLE_VERSION_STRING = 0.24.0;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
@@ -759,7 +759,7 @@
799396611D48B7BF00086753 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.22.0;
BUNDLE_VERSION_STRING = 0.24.0;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
@@ -796,7 +796,6 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
APPLICATION_EXTENSION_API_ONLY = YES;
BUNDLE_VERSION_STRING = 0.23.0;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
@@ -832,7 +831,6 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
APPLICATION_EXTENSION_API_ONLY = YES;
BUNDLE_VERSION_STRING = 0.23.0;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
+64 -14
View File
@@ -13,6 +13,19 @@ import Foundation
It's a complete reimplementation and doesn't use CFNetwork deprecated API.
*/
open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite {
/// 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" }
open let baseURL: URL?
@@ -34,7 +47,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
public var validatingCache: Bool
/// Determine either FTP session is in passive or active mode.
public let passiveMode: Bool
public let mode: Mode
fileprivate var _session: URLSession!
internal var sessionDelegate: SessionDelegate?
@@ -67,12 +80,14 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
- 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 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, passive: Bool = true, credential: URLCredential? = nil, cache: URLCache? = nil) {
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
}
@@ -84,7 +99,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
urlComponents.path = urlComponents.path.hasSuffix("/") ? urlComponents.path : urlComponents.path + "/"
self.baseURL = urlComponents.url!.absoluteURL
self.passiveMode = passive
self.mode = mode
self.useCache = false
self.validatingCache = true
self.cache = cache
@@ -101,12 +116,36 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
operation_queue.name = "\(queueLabel).Operation"
}
/**
**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(forKey: "baseURL") as? URL else { return nil }
self.init(baseURL: baseURL, passive: aDecoder.decodeBool(forKey: "passiveMode"), credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
self.useCache = aDecoder.decodeBool(forKey: "useCache")
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
self.supportsRFC3659 = aDecoder.decodeBool(forKey: "supportsRFC3659")
let mode: Mode
if let modeStr = aDecoder.decodeObject(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(forKey: "credential") as? URLCredential)
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) {
@@ -114,8 +153,9 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
aCoder.encode(self.credential, forKey: "credential")
aCoder.encode(self.useCache, forKey: "useCache")
aCoder.encode(self.validatingCache, forKey: "validatingCache")
aCoder.encode(self.passiveMode, forKey: "passiveMode")
aCoder.encode(self.mode.rawValue, forKey: "mode")
aCoder.encode(self.supportsRFC3659, forKey: "supportsRFC3659")
aCoder.encode(self.securedDataConnection, forKey: "securedDataConnection")
}
public static var supportsSecureCoding: Bool {
@@ -123,11 +163,12 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
}
open func copy(with zone: NSZone? = nil) -> Any {
let copy = FTPFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)!
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
}
@@ -149,7 +190,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
/**
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,
- 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.
@@ -157,6 +198,12 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
*/
public var uploadByREST: Bool = FileProviderStreamTask.defaultUseURLSession
/**
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
open func contentsOfDirectory(path: String, completionHandler: @escaping ([FileObject], Error?) -> Void) {
self.contentsOfDirectory(path: path, rfc3659enabled: supportsRFC3659, completionHandler: completionHandler)
}
@@ -202,7 +249,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
let files: [FileObject] = contents.flatMap {
rfc3659enabled ? self.parseMLST($0, in: path) : self.parseUnixList($0, in: path)
rfc3659enabled ? self.parseMLST($0, in: path) : (self.parseUnixList($0, in: path) ?? self.parseDOSList($0, in: path))
}
self.dispatch_queue.async {
@@ -262,7 +309,10 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
guard lines.count > 2 else {
throw self.urlError(path, code: .badServerResponse)
}
let file: FileObject? = rfc3659enabled ? self.parseMLST(lines[1], in: path) : self.parseUnixList(lines[1], in: path)
let dirPath = (path as NSString).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)
}
+118 -14
View File
@@ -60,12 +60,13 @@ internal extension FTPFileProvider {
}
// needs password
if FileProviderFTPError(message: response).code == 331 {
if response.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("33") {
self.execute(command: "PASS \(self.credential?.password ?? "fileprovider@")", on: task) { (response, error) in
if response?.hasPrefix("23") ?? false {
completionHandler(nil)
} else {
completionHandler(self.urlError("", code: .userAuthenticationRequired))
let error: Error = response.flatMap(FileProviderFTPError.init(message:)) ?? self.urlError("", code: .userAuthenticationRequired)
completionHandler(error)
}
}
return
@@ -76,6 +77,25 @@ internal extension FTPFileProvider {
}
}
fileprivate func ftpEstablishSecureDataConnection(_ task: FileProviderStreamTask, completionHandler: @escaping (_ error: Error?) -> Void) {
self.execute(command: "PBSZ 0", on: task, completionHandler: { (response, error) in
if let error = error {
completionHandler(error)
return
}
let prot = self.securedDataConnection ? "PROT P" : "PROT C"
self.execute(command: prot, on: task, completionHandler: { (response, error) in
if let error = error {
completionHandler(error)
return
}
self.ftpUserPass(task, completionHandler: completionHandler)
})
})
}
func ftpLogin(_ task: FileProviderStreamTask, completionHandler: @escaping (_ error: Error?) -> Void) {
let timeout = session.configuration.timeoutIntervalForRequest
@@ -118,25 +138,25 @@ internal extension FTPFileProvider {
if let response = response, response.hasPrefix("23") {
task.startSecureConnection()
isSecure = true
self.execute(command: "PBSZ 0\r\nPROT P", on: task, completionHandler: { (response, error) in
self.ftpEstablishSecureDataConnection(task) { error in
if let error = error {
completionHandler(error)
return
}
self.ftpUserPass(task, completionHandler: completionHandler)
})
}
}
})
} else if isSecure {
self.execute(command: "PBSZ 0\r\nPROT P", on: task, completionHandler: { (response, error) in
self.ftpEstablishSecureDataConnection(task) { error in
if let error = error {
completionHandler(error)
return
}
self.ftpUserPass(task, completionHandler: completionHandler)
})
}
} else {
self.ftpUserPass(task, completionHandler: completionHandler)
}
@@ -177,6 +197,50 @@ internal extension FTPFileProvider {
if self.baseURL?.scheme == "ftps" || self.baseURL?.scheme == "ftpes" || self.baseURL?.port == 990 {
passiveTask.startSecureConnection()
}
passiveTask.securityLevel = .tlSv1
passiveTask.resume()
completionHandler(passiveTask, nil)
} catch {
completionHandler(nil, error)
return
}
}
}
func ftpExtendedPassive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
func trimmedNumber(_ s : String) -> String {
return s.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
}
self.execute(command: "EPSV", on: task) { (response, error) in
do {
if let error = error {
throw error
}
guard let response = response, let destString = response.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ").last else {
throw self.urlError("", code: .badServerResponse)
}
if response.trimmingCharacters(in: .whitespaces).hasPrefix("50") {
self.ftpPassive(task, completionHandler: completionHandler)
}
let destArray = destString.components(separatedBy: "|")
guard destArray.count >= 4, let port = Int(trimmedNumber(destArray[3])) else {
throw self.urlError("", code: .badServerResponse)
}
var host = destArray[2]
if host.isEmpty {
host = self.baseURL?.host ?? ""
}
let passiveTask = self.session.fpstreamTask(withHostName: host, port: port)
if self.baseURL?.scheme == "ftps" || self.baseURL?.scheme == "ftpes" || self.baseURL?.port == 990 {
passiveTask.startSecureConnection()
}
passiveTask.securityLevel = .tlSv1
passiveTask.resume()
completionHandler(passiveTask, nil)
} catch {
@@ -223,9 +287,18 @@ internal extension FTPFileProvider {
}
func ftpDataConnect(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
if self.passiveMode {
switch self.mode {
case .default:
if self.baseURL?.port == 990 || self.baseURL?.scheme == "ftps" || self.baseURL?.scheme == "ftpes" {
self.ftpExtendedPassive(task, completionHandler: completionHandler)
} else {
self.ftpPassive(task, completionHandler: completionHandler)
}
case .passive:
self.ftpPassive(task, completionHandler: completionHandler)
} else {
case .extendedPassive:
self.ftpExtendedPassive(task, completionHandler: completionHandler)
case .active:
dispatch_queue.async {
self.ftpActive(task, completionHandler: completionHandler)
}
@@ -270,8 +343,7 @@ internal extension FTPFileProvider {
let waitResult = group.wait(timeout: .now() + timeout)
if let error = error {
if !((error as NSError).domain == URLError.errorDomain
&& (error as NSError).code == URLError.cancelled.rawValue) {
if (error as? URLError)?.code != .cancelled {
throw error
}
return
@@ -326,16 +398,16 @@ internal extension FTPFileProvider {
completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: -1)
let queue = DispatchQueue(label: "\(self.type).recursiveList")
let group = DispatchGroup()
queue.async {
let group = DispatchGroup()
var result = [FileObject]()
var success = true
group.enter()
self.contentsOfDirectory(path: path, completionHandler: { (files, error) in
success = success && (error == nil)
if let error = error {
completionHandler([], error)
group.leave()
completionHandler([], error)
return
}
@@ -804,6 +876,7 @@ internal extension FTPFileProvider {
func ftpPath(_ apath: String) -> String {
// path of base url should be concreted into file path! And remove final slash
let apath = apath.replacingOccurrences(of: "/", with: "", options: [.anchored])
var path = baseURL!.appendingPathComponent(apath).path.replacingOccurrences(of: "/", with: "", options: [.anchored, .backwards])
// Fixing slashes
@@ -819,7 +892,7 @@ internal extension FTPFileProvider {
let nearDateFormatter = DateFormatter()
nearDateFormatter.calendar = gregorian
nearDateFormatter.locale = Locale(identifier: "en_US_POSIX")
nearDateFormatter.dateFormat = "MMM dd hh:ss yyyy"
nearDateFormatter.dateFormat = "MMM dd hh:mm yyyy"
let farDateFormatter = DateFormatter()
farDateFormatter.calendar = gregorian
farDateFormatter.locale = Locale(identifier: "en_US_POSIX")
@@ -867,6 +940,33 @@ internal extension FTPFileProvider {
return file
}
func parseDOSList(_ text: String, in path: String) -> FileObject? {
let gregorian = Calendar(identifier: .gregorian)
let dateFormatter = DateFormatter()
dateFormatter.calendar = gregorian
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "M-d-y hh:mma"
let components = text.components(separatedBy: " ").flatMap { $0.isEmpty ? nil : $0 }
guard components.count >= 4 else { return nil }
let size = Int64(components[2]) ?? -1
let date = components[0..<2].joined(separator: " ")
let name = components[3..<components.count].joined(separator: " ")
guard name != "." && name != ".." else { return nil }
let path = (path as NSString).appendingPathComponent(name).replacingOccurrences(of: "/", with: "", options: .anchored)
let file = FileObject(url: url(of: path), name: name, path: "/" + path)
file.type = components[2] == "<DIR>" ? .directory : .regular
file.size = size
if let parsedDate = dateFormatter.date(from: date) {
file.modifiedDate = parsedDate
}
return file
}
func parseMLST(_ text: String, in path: String) -> FileObject? {
var components = text.components(separatedBy: ";").flatMap { $0.isEmpty ? nil : $0 }
guard components.count > 1 else { return nil }
@@ -950,7 +1050,11 @@ public struct FileProviderFTPError: LocalizedError {
self.serverDescription = serverDescription
}
init(message response: String, path: String = "") {
init(message response: String) {
self.init(message: response, path: "")
}
init(message response: String, path: String) {
let message = response.components(separatedBy: .newlines).last ?? "No Response"
#if swift(>=4.0)
let startIndex = (message.index(of: "-") ?? message.index(of: " ")) ?? message.startIndex
+13 -14
View File
@@ -424,14 +424,6 @@ public protocol FileProviderOperations: FileProviderBasic {
}
public extension FileProviderOperations {
/// *OBSOLETED:* Use Use FileProviderReadWrite.writeContents(path:, data:, completionHandler:) method instead.
@available(*, obsoleted: 0.23, message: "Use FileProviderReadWrite.writeContents(path:, data:, completionHandler:) method instead.")
@discardableResult
public func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> Progress? {
let path = (at as NSString).appendingPathComponent(file)
return (self as? FileProviderReadWrite)?.writeContents(path: path, contents: data, completionHandler: completionHandler)
}
@discardableResult
public func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.moveItem(path: path, to: to, overwrite: false, completionHandler: completionHandler)
@@ -772,16 +764,27 @@ public extension FileProviderBasic {
internal func urlError(_ path: String, code: URLError.Code) -> Error {
let fileURL = self.url(of: path)
let userInfo: [String: Any] = [NSURLErrorKey: fileURL,
var userInfo: [String: Any] = [NSURLErrorKey: fileURL,
NSURLErrorFailingURLErrorKey: fileURL,
NSURLErrorFailingURLStringErrorKey: fileURL.absoluteString,
]
let error = NSError(domain: NSURLErrorDomain, code: code.rawValue, userInfo: nil)
for (key, value) in error.userInfo {
userInfo[key] = value
}
return URLError(code, userInfo: userInfo)
}
internal func cocoaError(_ path: String, code: CocoaError.Code) -> Error {
let fileURL = self.url(of: path)
return CocoaError(code, userInfo: [NSFilePathErrorKey: path, NSURLErrorKey: fileURL])
var userInfo: [String: Any] = [NSFilePathErrorKey: path,
NSURLErrorKey: fileURL,
]
let error = NSError(domain: NSCocoaErrorDomain, code: code.rawValue, userInfo: nil)
for (key, value) in error.userInfo {
userInfo[key] = value
}
return cocoaError(fileURL.path, code: code)
}
internal func NotImplemented(_ fn: String = #function, file: StaticString = #file) {
@@ -1070,10 +1073,6 @@ public enum FileOperationType: CustomStringConvertible {
}
}
/// Allows to get progress or cancel an in-progress operation, useful for remote providers
@available(*, obsoleted: 1.0, message: "Use Foudation.Progress class instead.")
public protocol OperationHandle {}
/// Delegate methods for reporting provider's operation result and progress, when it's ready to update
/// user interface.
/// All methods are called in main thread to avoids UI bugs.
-5
View File
@@ -17,11 +17,6 @@ import Foundation
open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite {
open class var type: String { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") }
open let baseURL: URL?
/// **OBSOLETED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method.
@available(*, obsoleted: 0.22, message: "This property is redundant with almost no use internally.")
open var currentPath: String = ""
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue {
willSet {
+2 -2
View File
@@ -134,9 +134,9 @@ public final class OneDriveFileObject: FileObject {
}
static func relativePathOf(url: URL, baseURL: URL?, route: OneDriveFileProvider.Route) -> String {
let base = baseURL?.appendingPathComponent(route.drivePath)
let base = baseURL?.appendingPathComponent(route.drivePath).path ?? ""
let crudePath = url.absoluteString.replacingOccurrences(of: base?.absoluteString ?? "", with: "", options: .anchored)
let crudePath = url.path.replacingOccurrences(of: base, with: "", options: .anchored)
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
switch crudePath {
+57 -7
View File
@@ -8,7 +8,7 @@
import XCTest
import FilesProvider
class FilesProviderTests: XCTestCase {
class FilesProviderTests: XCTestCase, FileProviderDelegate {
override func setUp() {
super.setUp()
@@ -40,6 +40,7 @@ class FilesProviderTests: XCTestCase {
cred = nil
}
let provider = WebDAVFileProvider(baseURL: url, credential: cred)!
provider.delegate = self
addTeardownBlock {
self.testRemoveFile(provider, filePath: self.testFolderName)
}
@@ -52,6 +53,7 @@ class FilesProviderTests: XCTestCase {
}
let cred = URLCredential(user: "testuser", password: pass, persistence: .forSession)
let provider = DropboxFileProvider(credential: cred)
provider.delegate = self
addTeardownBlock {
self.testRemoveFile(provider, filePath: self.testFolderName)
}
@@ -68,7 +70,8 @@ class FilesProviderTests: XCTestCase {
} else {
cred = nil
}
let provider = FTPFileProvider(baseURL: url, passive: true, credential: cred)!
let provider = FTPFileProvider(baseURL: url, mode: .extendedPassive, credential: cred)!
provider.delegate = self
addTeardownBlock {
self.testRemoveFile(provider, filePath: self.testFolderName)
}
@@ -81,6 +84,7 @@ class FilesProviderTests: XCTestCase {
}
let cred = URLCredential(user: "testuser", password: pass, persistence: .forSession)
let provider = OneDriveFileProvider(credential: cred)
provider.delegate = self
addTeardownBlock {
self.testRemoveFile(provider, filePath: self.testFolderName)
}
@@ -106,16 +110,19 @@ class FilesProviderTests: XCTestCase {
fileprivate func testCreateFolder(_ provider: FileProvider, folderName: String) {
let desc = "Creating folder at root in \(provider.type)"
print("Test started: \(desc).")
let expectation = XCTestExpectation(description: desc)
provider.create(folder: folderName, at: "/") { (error) in
XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")")
expectation.fulfill()
}
wait(for: [expectation], timeout: timeout)
print("Test fulfilled: \(desc).")
}
fileprivate func testContentsOfDirectory(_ provider: FileProvider) {
let desc = "Enumerating files list in \(provider.type)"
print("Test started: \(desc).")
let expectation = XCTestExpectation(description: desc)
provider.contentsOfDirectory(path: "/") { (files, error) in
XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")")
@@ -128,10 +135,12 @@ class FilesProviderTests: XCTestCase {
expectation.fulfill()
}
wait(for: [expectation], timeout: timeout)
print("Test fulfilled: \(desc).")
}
fileprivate func testAttributesOfFile(_ provider: FileProvider, filePath: String) {
let desc = "Attrubutes of file in \(provider.type)"
print("Test started: \(desc).")
let expectation = XCTestExpectation(description: desc)
provider.attributesOfItem(path: filePath) { (fileObject, error) in
XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")")
@@ -144,21 +153,25 @@ class FilesProviderTests: XCTestCase {
expectation.fulfill()
}
wait(for: [expectation], timeout: timeout)
print("Test fulfilled: \(desc).")
}
fileprivate func testCreateFile(_ provider: FileProvider, filePath: String) {
let desc = "Creating file in \(provider.type)"
print("Test started: \(desc).")
let expectation = XCTestExpectation(description: desc)
let data = sampleText.data(using: .ascii)
provider.writeContents(path: filePath, contents: data, overwrite: true) { (error) in
XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")")
expectation.fulfill()
}
wait(for: [expectation], timeout: timeout)
wait(for: [expectation], timeout: timeout * 3)
print("Test fulfilled: \(desc).")
}
fileprivate func testContentsFile(_ provider: FileProvider, filePath: String, hasSampleText: Bool = true) {
let desc = "Reading file in \(provider.type)"
print("Test started: \(desc).")
let expectation = XCTestExpectation(description: desc)
provider.contents(path: filePath) { (data, error) in
XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")")
@@ -170,37 +183,44 @@ class FilesProviderTests: XCTestCase {
}
expectation.fulfill()
}
wait(for: [expectation], timeout: timeout)
wait(for: [expectation], timeout: timeout * 3)
print("Test fulfilled: \(desc).")
}
fileprivate func testRenameFile(_ provider: FileProvider, filePath: String, to toPath: String) {
let desc = "Renaming file in \(provider.type)"
print("Test started: \(desc).")
let expectation = XCTestExpectation(description: desc)
provider.moveItem(path: filePath, to: toPath, overwrite: true) { (error) in
XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")")
expectation.fulfill()
}
wait(for: [expectation], timeout: timeout)
print("Test fulfilled: \(desc).")
}
fileprivate func testCopyFile(_ provider: FileProvider, filePath: String, to toPath: String) {
let desc = "Copying file in \(provider.type)"
print("Test started: \(desc).")
let expectation = XCTestExpectation(description: desc)
provider.copyItem(path: filePath, to: toPath, overwrite: true) { (error) in
XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")")
expectation.fulfill()
}
wait(for: [expectation], timeout: timeout)
print("Test fulfilled: \(desc).")
}
fileprivate func testRemoveFile(_ provider: FileProvider, filePath: String) {
let desc = "Deleting file in \(provider.type)"
print("Test started: \(desc).")
let expectation = XCTestExpectation(description: desc)
provider.removeItem(path: filePath) { (error) in
XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")")
expectation.fulfill()
}
wait(for: [expectation], timeout: timeout)
print("Test fulfilled: \(desc).")
}
private func randomData(size: Int = 262144) -> Data {
@@ -229,6 +249,7 @@ class FilesProviderTests: XCTestCase {
// test Upload/Download
let url = dummyFile()
let desc = "Uploading file in \(provider.type)"
print("Test started: \(desc).")
let expectation = XCTestExpectation(description: desc)
let dummy = dummyFile()
provider.copyItem(localFile: dummy, to: filePath) { (error) in
@@ -236,11 +257,13 @@ class FilesProviderTests: XCTestCase {
expectation.fulfill()
}
wait(for: [expectation], timeout: timeout * 3)
print("Test fulfilled: \(desc).")
}
fileprivate func testDownloadFile(_ provider: FileProvider, filePath: String) {
let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("downloadedfile.dat")
let desc = "Downloading file in \(provider.type)"
print("Test started: \(desc).")
let expectation = XCTestExpectation(description: desc)
provider.copyItem(path: filePath, toLocalURL: url) { (error) in
XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")")
@@ -252,10 +275,12 @@ class FilesProviderTests: XCTestCase {
expectation.fulfill()
}
wait(for: [expectation], timeout: timeout * 3)
print("Test fulfilled: \(desc).")
}
fileprivate func testStorageProperties(_ provider: FileProvider, isExpected: Bool) {
let desc = "Querying volume in \(provider.type)"
print("Test started: \(desc).")
let expectation = XCTestExpectation(description: desc)
provider.storageProperties { (volume) in
if !isExpected {
@@ -270,17 +295,19 @@ class FilesProviderTests: XCTestCase {
expectation.fulfill()
}
wait(for: [expectation], timeout: timeout)
print("Test fulfilled: \(desc).")
}
fileprivate func testReachability(_ provider: FileProvider) {
// Test file operations
let desc = "Reachability of \(provider.type)"
print("Test started: \(desc).")
let expectation = XCTestExpectation(description: desc)
provider.isReachable { (status) in
XCTAssertTrue(status, "\(provider.type) not reachable")
provider.isReachable { (status, error) in
XCTAssertTrue(status, "\(provider.type) not reachable: \(error?.localizedDescription ?? "no error desc")")
expectation.fulfill()
}
wait(for: [expectation], timeout: timeout)
print("Test fulfilled: \(desc).")
}
fileprivate func testBasic(_ provider: FileProvider) {
@@ -300,6 +327,7 @@ class FilesProviderTests: XCTestCase {
}
fileprivate func testOperations(_ provider: FileProvider) {
// Test file operations
testReachability(provider)
testCreateFolder(provider, folderName: testFolderName)
testContentsOfDirectory(provider)
@@ -317,4 +345,26 @@ class FilesProviderTests: XCTestCase {
testUploadFile(provider, filePath: uploadFilePath)
testDownloadFile(provider, filePath: uploadFilePath)
}
func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperationType) {
return
}
func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperationType, error: Error) {
return
}
func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperationType, progress: Float) {
switch operation {
case .copy(source: let source, destination: let dest) where dest.hasPrefix("file://"):
print("Downloading \(source) to \((dest as NSString).lastPathComponent): \(progress * 100) completed.")
case .copy(source: let source, destination: let dest) where source.hasPrefix("file://"):
print("Uploading \((source as NSString).lastPathComponent) to \(dest): \(progress * 100) completed.")
case .copy(source: let source, destination: let dest):
print("Copy \(source) to \(dest): \(progress * 100) completed.")
default:
break
}
return
}
}