Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c34a4e9a8 | |||
| 37ce9c95fc | |||
| b39c1c4e82 | |||
| 8f0cbf8513 | |||
| 2690551b7f | |||
| a6550b0ec3 | |||
| 4b0fffc691 |
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user