Compare commits

...

22 Commits

Author SHA1 Message Date
Amir Abbas e72d7ff088 Fixed podspec Swift version 2018-05-04 01:12:52 +04:30
Amir Abbas ccb7961171 Minor performance improvement on iCloud file progress monitor 2018-05-04 00:42:25 +04:30
Amir Abbas e4fc6b24c0 Updated to version 0.24.1, Pods Swift version to 4.1 2018-05-04 00:25:27 +04:30
Amir Abbas f22af8d002 Fixed possible leak in iCloud provider when uploading file 2018-05-03 19:48:24 +04:30
Amir Abbas e5e5faa4e8 Fixed iCloud download/upload progress report, Fix #93
- Fixed FileObject comparison
- Fixed image GPS metadata population in ExtendedLocalFileProvider
- More items for image metadata in ExtendedLocalFileProvider
2018-05-03 19:39:18 +04:30
Amir Abbas f2cd571d7a Fixed LocalFileMonitor dir not refreshing, Fixed FTP connection refused error by retrying 2018-04-25 13:23:15 +04:30
Amir Abbas 4202f5e1bd FPStreamTask returning real error instead of timeout 2018-04-22 17:50:42 +04:30
Amir Abbas 3040215ce3 Fixed race conditions, Possible fix #79 2018-04-22 14:17:42 +04:30
Amir Abbas bd2f2b3954 Addded progressive read to FTP and HTTP providers
- Fix OneDive returned code 200 as Error
2018-04-16 11:06:54 +04:30
Amir Abbas 9d19768e1c Added test for NSCoding, fixed archiving 2018-04-08 10:22:59 +04:30
Amir Abbas bcc774d3a8 Fix podspec error 2018-04-07 21:52:49 +04:30
Amir Abbas ad9768a584 Secure encoding enabled, Fixed podspec 2018-04-07 12:19:45 +04:30
Amir Abbas a089cbc21c Fixed Swift 3 build error 2018-04-01 11:29:21 +04:30
Amir Abbas 090baa3b61 Fix Swift 4.0 compile error 2018-03-31 22:22:36 +04:30
Amir Abbas fc75c85b14 Fixed warnings on Swift 4.1, LocalFileMonitor is now public 2018-03-31 19:19:25 +04:30
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
29 changed files with 896 additions and 281 deletions
-1
View File
@@ -1 +0,0 @@
4.0
+8 -9
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
#
s.name = "FilesProvider"
s.version = "0.23.0"
s.version = "0.25.1"
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.
@@ -55,9 +55,8 @@ Pod::Spec.new do |s|
# profile URL.
#
s.author = { "Amir Abbas Mousavian" => "a.mosavian@gmail.com" }
# Or just: s.author = "Amir Abbas Mousavian"
# s.authors = { "Amir Abbas Mousavian" => "a.mosavian@gmail.com" }
s.authors = { "Amir Abbas Mousavian" => "a.mosavian@gmail.com" }
s.social_media_url = "https://twitter.com/amosavian"
# ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
@@ -66,10 +65,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.1"
s.ios.deployment_target = "8.0"
s.osx.deployment_target = "10.10"
# s.watchos.deployment_target = "2.0"
@@ -120,9 +116,12 @@ Pod::Spec.new do |s|
#
# s.framework = "SomeFramework"
# s.frameworks = "SomeFramework", "AnotherFramework"
s.frameworks = "AVFoundation", "ImageIO", "CoreGraphics"
s.ios.framework = "UIKit"
s.tvos.framework = "UIKit"
s.osx.framework = "AppKit"
# s.library = "iconv"
s.library = "xml2"
# s.libraries = "iconv", "xml2"
+7 -5
View File
@@ -496,7 +496,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0910;
LastUpgradeCheck = 0900;
LastUpgradeCheck = 0930;
TargetAttributes = {
799396661D48B7F600086753 = {
CreatedOnToolsVersion = 7.3.1;
@@ -720,16 +720,18 @@
799396601D48B7BF00086753 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.22.0;
BUNDLE_VERSION_STRING = 0.25.1;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
@@ -759,16 +761,18 @@
799396611D48B7BF00086753 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.22.0;
BUNDLE_VERSION_STRING = 0.25.1;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
@@ -796,7 +800,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 +835,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;
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0920"
LastUpgradeVersion = "0930"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -26,7 +26,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
@@ -37,7 +36,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0920"
LastUpgradeVersion = "0930"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -26,7 +26,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
@@ -37,7 +36,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0920"
LastUpgradeVersion = "0930"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -26,7 +26,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
@@ -37,7 +36,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
+1 -1
View File
@@ -471,7 +471,7 @@ We would love for you to contribute to **FileProvider**, check the `LICENSE` fil
Things you may consider to help us:
- [ ] Implement request/response stack for `SMBClient`
- [ ] Implement Test-case (`XCTest`)
- [x] Implement Test-case (`XCTest`)
- [ ] Add Sample project for iOS
- [ ] Add Sample project for macOS
+77 -60
View File
@@ -93,11 +93,11 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
}
public required convenience init?(coder aDecoder: NSCoder) {
if let containerId = aDecoder.decodeObject(forKey: "containerId") as? String,
let scopeString = aDecoder.decodeObject(forKey: "scope") as? String,
if let containerId = aDecoder.decodeObject(of: NSString.self, forKey: "containerId") as String?,
let scopeString = aDecoder.decodeObject(of: NSString.self, forKey: "scope") as String?,
let scope = UbiquitousScope(rawValue: scopeString) {
self.init(containerId: containerId, scope: scope)
} else if let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL {
} else if let baseURL = aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL? {
self.init(baseURL: baseURL)
} else {
return nil
@@ -583,16 +583,84 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
return file
}
lazy fileprivate var observer: KVOObserver = KVOObserver()
fileprivate func monitorFile(path: String, operation: FileOperationType, progress: Progress?) {
var isDownloadingOperation: Bool
let isUploadingOperation: Bool
switch operation {
case .copy(_, destination: let dest) where dest.hasPrefix("file://"), .move(_, destination: let dest) where dest.hasPrefix("file://"):
fallthrough
case .fetch:
isDownloadingOperation = true
isUploadingOperation = false
case .copy(source: let source, _) where source.hasPrefix("file://"), .move(source: let source, _) where source.hasPrefix("file://"):
fallthrough
case .modify, .create:
isDownloadingOperation = false
isUploadingOperation = true
default:
return
}
let pathURL = self.url(of: path).standardizedFileURL
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K LIKE[CD] %@", NSMetadataItemPathKey, pathURL.path)
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataUbiquitousItemPercentDownloadedKey, NSMetadataUbiquitousItemPercentUploadedKey, NSMetadataUbiquitousItemDownloadingStatusKey, NSMetadataItemFSSizeKey]
query.predicate = NSPredicate(format: "(%K LIKE[CD] %@)", NSMetadataItemPathKey, pathURL.path)
query.valueListAttributes = [NSMetadataUbiquitousItemPercentDownloadedKey,
NSMetadataUbiquitousItemPercentUploadedKey,
NSMetadataUbiquitousItemIsUploadedKey,
NSMetadataUbiquitousItemDownloadingStatusKey,
NSMetadataItemFSSizeKey]
query.searchScopes = [self.scope.rawValue]
var context = QueryProgressWrapper(provider: self, progress: progress, operation: operation)
query.addObserver(self.observer, forKeyPath: "results", options: [.initial, .new, .old], context: &context)
var observer: NSObjectProtocol?
observer = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: .main) { [weak self] (notification) in
guard let items = notification.userInfo?[NSMetadataQueryUpdateChangedItemsKey] as? NSArray,
let item = items.firstObject as? NSMetadataItem else {
return
}
func terminateAndRemoveObserver() {
guard observer != nil else { return }
query.stop()
observer.flatMap(NotificationCenter.default.removeObserver)
observer = nil
}
func updateProgress(_ percent: NSNumber) {
let fraction = percent.doubleValue / 100
self?.delegateNotify(operation, progress: fraction)
if let progress = progress {
if progress.totalUnitCount < 1, let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? NSNumber {
progress.totalUnitCount = size.int64Value
}
progress.completedUnitCount = progress.totalUnitCount > 0 ? Int64(Double(progress.totalUnitCount) * fraction) : 0
}
if percent.doubleValue == 100.0 {
terminateAndRemoveObserver()
}
}
for attrName in item.attributes {
switch attrName {
case NSMetadataUbiquitousItemPercentDownloadedKey:
guard isDownloadingOperation, let percent = item.value(forAttribute: attrName) as? NSNumber else { break }
updateProgress(percent)
case NSMetadataUbiquitousItemPercentUploadedKey:
guard isUploadingOperation, let percent = item.value(forAttribute: attrName) as? NSNumber else { break }
updateProgress(percent)
case NSMetadataUbiquitousItemDownloadingStatusKey:
if isDownloadingOperation, let value = item.value(forAttribute: attrName) as? String,
value == NSMetadataUbiquitousItemDownloadingStatusDownloaded {
terminateAndRemoveObserver()
}
case NSMetadataUbiquitousItemIsUploadedKey:
if isUploadingOperation, let value = item.value(forAttribute: attrName) as? NSNumber, value.boolValue {
terminateAndRemoveObserver()
}
default:
break
}
}
}
DispatchQueue.main.async {
query.start()
@@ -691,57 +759,6 @@ public enum UbiquitousScope: RawRepresentable {
}
}
}
struct QueryProgressWrapper {
weak var provider: CloudFileProvider?
weak var progress: Progress?
let operation: FileOperationType
}
fileprivate class KVOObserver: NSObject {
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let query = object as? NSMetadataQuery else {
return
}
guard let wrapper = context?.load(as: QueryProgressWrapper.self) else {
query.stop()
query.removeObserver(self, forKeyPath: "results")
return
}
let provider = wrapper.provider
let progress = wrapper.progress
let operation = wrapper.operation
guard let results = change?[.newKey], let item = (results as? [NSMetadataItem])?.first else {
return
}
query.disableUpdates()
var size = progress?.totalUnitCount ?? -1
if size < 0, let size_d = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 {
size = size_d
progress?.totalUnitCount = size
}
let downloadStatus = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? String ?? ""
let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0
let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0
if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) {
progress?.completedUnitCount = Int64(uploaded / 100 * Double(size))
provider?.delegateNotify(operation, progress: uploaded / 100)
} else if (uploaded == 0 || uploaded == 100) && downloadStatus != NSMetadataUbiquitousItemDownloadingStatusCurrent {
progress?.completedUnitCount = Int64(downloaded / 100 * Double(size))
provider?.delegateNotify(operation, progress: downloaded / 100)
} else if uploaded == 100 || downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent {
progress?.completedUnitCount = size
query.stop()
query.removeObserver(self, forKeyPath: "results")
provider?.delegateNotify(operation)
}
query.enableUpdates()
}
}
/*
func getMetadataItem(url: URL) -> NSMetadataItem? {
let query = NSMetadataQuery()
+1 -1
View File
@@ -44,7 +44,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
}
public required convenience init?(coder aDecoder: NSCoder) {
self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
self.init(credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential"))
self.useCache = aDecoder.decodeBool(forKey: "useCache")
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
}
+1 -1
View File
@@ -94,7 +94,7 @@ internal extension DropboxFileProvider {
request.httpMethod = "POST"
request.setValue(authentication: self.credential, with: .oAuth2)
request.setValue(contentType: .json)
var requestDictionary: [String: AnyObject] = ["path": self.correctPath(path) as NSString!]
var requestDictionary: [String: AnyObject] = ["path": self.correctPath(path)! as NSString]
requestDictionary["query"] = queryStr as NSString
requestDictionary["start"] = NSNumber(value: (token.flatMap( { Int($0) } ) ?? 0))
request.httpBody = Data(jsonDictionary: requestDictionary)
+38 -11
View File
@@ -282,26 +282,31 @@ public struct LocalFileInformationGenerator {
let imageDict = cfImageDict as NSDictionary
let tiffDict = imageDict[kCGImagePropertyTIFFDictionary as String] as? NSDictionary ?? [:]
let exifDict = imageDict[kCGImagePropertyExifDictionary as String] as? NSDictionary ?? [:]
let gpsDict = imageDict[kCGImagePropertyGPSDictionary as String] as? NSDictionary ?? [:]
if let pixelWidth = imageDict.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight = imageDict.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber {
add(key: "Dimensions", value: "\(pixelWidth)x\(pixelHeight)")
}
add(key: "DPI", value: imageDict[kCGImagePropertyDPIWidth as String])
add(key: "Device make", value: tiffDict[kCGImagePropertyTIFFMake as String])
add(key: "Device maker", value: tiffDict[kCGImagePropertyTIFFMake as String])
add(key: "Device model", value: tiffDict[kCGImagePropertyTIFFModel as String])
add(key: "Lens model", value: exifDict[kCGImagePropertyExifLensModel as String])
add(key: "Artist", value: tiffDict[kCGImagePropertyTIFFArtist as String] as? String)
add(key: "Copyright", value: tiffDict[kCGImagePropertyTIFFCopyright as String] as? String)
add(key: "Date taken", value: tiffDict[kCGImagePropertyTIFFDateTime as String] as? String)
if let latitude = tiffDict[kCGImagePropertyGPSLatitude as String] as? NSNumber, let longitude = tiffDict[kCGImagePropertyGPSLongitude as String] as? NSNumber {
add(key: "Location", value: "\(latitude), \(longitude)")
if let latitude = gpsDict[kCGImagePropertyGPSLatitude as String] as? NSNumber,
let longitude = gpsDict[kCGImagePropertyGPSLongitude as String] as? NSNumber {
let altitudeDesc = (gpsDict[kCGImagePropertyGPSAltitude as String] as? NSNumber).map({ " at \($0.format(precision: 0))m" }) ?? ""
add(key: "Location", value: "\(latitude.format()), \(longitude.format())\(altitudeDesc)")
}
add(key: "Altitude", value: tiffDict[kCGImagePropertyGPSAltitude as String] as? NSNumber)
add(key: "Area", value: tiffDict[kCGImagePropertyGPSAreaInformation as String])
add(key: "Area", value: gpsDict[kCGImagePropertyGPSAreaInformation as String])
add(key: "Color space", value: imageDict[kCGImagePropertyColorModel as String])
add(key: "Color depth", value: (imageDict[kCGImagePropertyDepth as String] as? NSNumber).map({ "\($0) bits" }))
add(key: "Color profile", value: imageDict[kCGImagePropertyProfileName as String])
add(key: "Focal length", value: exifDict[kCGImagePropertyExifFocalLength as String])
add(key: "White banance", value: exifDict[kCGImagePropertyExifWhiteBalance as String])
add(key: "F number", value: exifDict[kCGImagePropertyExifFNumber as String])
add(key: "Exposure program", value: exifDict[kCGImagePropertyExifExposureProgram as String])
@@ -325,7 +330,7 @@ public struct LocalFileInformationGenerator {
}
}
func makeDescription(_ key: String?) -> String? {
func makeKeyDescription(_ key: String?) -> String? {
guard let key = key else {
return nil
}
@@ -336,6 +341,18 @@ public struct LocalFileInformationGenerator {
return newKey.capitalized
}
func parseLocationData(_ value: String) -> (latitude: Double, longitude: Double, height: Double?)? {
let scanner = Scanner.init(string: value)
var latitude: Double = 0.0, longitude: Double = 0.0, height: Double = 0
if scanner.scanDouble(&latitude), scanner.scanDouble(&longitude) {
scanner.scanDouble(&height)
return (latitude, longitude, height)
} else {
return nil
}
}
guard fileURL.fileExists else {
return (dic, keys)
}
@@ -347,10 +364,20 @@ public struct LocalFileInformationGenerator {
#else
let commonKey = item.commonKey
#endif
if let description = makeDescription(commonKey) {
if let value = item.stringValue {
keys.append(description)
dic[description] = value
if let key = makeKeyDescription(commonKey) {
if commonKey == "location", let value = item.stringValue, let loc = parseLocationData(value) {
keys.append(key)
let heightStr: String = (loc.height as NSNumber?).map({ ", \($0.format(precision: 0))m" }) ?? ""
dic[key] = "\((loc.latitude as NSNumber).format())°, \((loc.longitude as NSNumber).format())°\(heightStr)"
} else if let value = item.dateValue {
keys.append(key)
dic[key] = value
} else if let value = item.numberValue {
keys.append(key)
dic[key] = value
} else if let value = item.stringValue {
keys.append(key)
dic[key] = value
}
}
}
+34 -12
View File
@@ -410,12 +410,6 @@ internal extension Data {
guard self.count >= start + length else { return nil }
return String(data: self.subdata(in: start..<start+length), encoding: encoding)
}
static func mapMemory<T, U>(from: T) -> U? {
guard MemoryLayout<T>.size >= MemoryLayout<U>.size else { return nil }
let data = Data(value: from)
return data.scanValue()
}
}
internal extension String {
@@ -447,14 +441,14 @@ internal extension String {
}
}
#if swift(>=4.0)
#else
extension String {
var count: Int {
return self.characters.count
internal extension NSNumber {
internal func format(precision: Int = 2, style: NumberFormatter.Style = .decimal) -> String {
let formatter = NumberFormatter()
formatter.maximumFractionDigits = precision
formatter.numberStyle = style
return formatter.string(from: self)!
}
}
#endif
internal extension TimeInterval {
internal var formatshort: String {
@@ -592,3 +586,31 @@ func hasSuffix(_ suffix: String) -> (_ value: String) -> Bool {
value.hasSuffix(suffix)
}
}
// Legacy Swift versions support
#if swift(>=4.0)
#else
extension String {
var count: Int {
return self.characters.count
}
}
#endif
#if swift(>=4.1)
#else
extension Array {
func compactMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] {
return try self.flatMap(transform)
}
}
extension ArraySlice {
func compactMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] {
return try self.flatMap(transform)
}
}
#endif
+5 -2
View File
@@ -410,9 +410,12 @@ fileprivate func arrayOfBytes<T>(_ value:T, length:Int? = nil) -> [UInt8] {
bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee
}
valuePointer.deinitialize()
valuePointer.deinitialize(count: 1)
#if swift(>=4.1)
valuePointer.deallocate()
#else
valuePointer.deallocate(capacity: 1)
#endif
return bytes
}
+150 -27
View File
@@ -8,14 +8,67 @@
import Foundation
private var lasttaskIdAssociated = 1_000_000_000
private var _lasttaskIdAssociated = 1_000_000_000
let _lasttaskIdAssociated_lock: NSLock = NSLock()
var lasttaskIdAssociated: Int {
get {
_lasttaskIdAssociated_lock.try()
defer {
_lasttaskIdAssociated_lock.unlock()
}
return _lasttaskIdAssociated
}
set {
_lasttaskIdAssociated_lock.try()
defer {
_lasttaskIdAssociated_lock.unlock()
}
_lasttaskIdAssociated = newValue
}
}
// codebeat:disable[TOTAL_LOC,TOO_MANY_IVARS]
/// This class is a replica of NSURLSessionStreamTask with same api for iOS 7/8
/// while it can actually fallback to NSURLSessionStreamTask in iOS 9.
public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
fileprivate var inputStream: InputStream?
fileprivate var outputStream: OutputStream?
fileprivate var _inputStream: InputStream?
fileprivate var _outputStream: OutputStream?
let _inputStream_lock: NSLock = NSLock()
var inputStream: InputStream? {
get {
_inputStream_lock.try()
defer {
_inputStream_lock.unlock()
}
return _inputStream
}
set {
_inputStream_lock.try()
defer {
_inputStream_lock.unlock()
}
_inputStream = newValue
}
}
let _outputStream_lock: NSLock = NSLock()
var outputStream: OutputStream? {
get {
_outputStream_lock.try()
defer {
_outputStream_lock.unlock()
}
return _outputStream
}
set {
_outputStream_lock.try()
defer {
_outputStream_lock.unlock()
}
_outputStream = newValue
}
}
fileprivate var operation_queue: OperationQueue!
internal var _underlyingSession: URLSession
@@ -331,17 +384,18 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
}
}
self.operation_queue.isSuspended = true
self._state = .canceling
self.inputStream?.delegate = nil
self.outputStream?.delegate = nil
self.inputStream?.close()
self.outputStream?.close()
self.inputStream?.remove(from: RunLoop.main, forMode: .defaultRunLoopMode)
self.outputStream?.remove(from: RunLoop.main, forMode: .defaultRunLoopMode)
self.inputStream?.delegate = nil
self.outputStream?.delegate = nil
self.inputStream = nil
self.outputStream = nil
@@ -350,7 +404,25 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
self._countOfBytesRecieved = 0
}
var _error: Error? = nil
var __error: Error? = nil
let _error_lock: NSLock = NSLock()
var _error: Error? {
get {
_error_lock.try()
defer {
_error_lock.unlock()
}
return __error
}
set {
_error_lock.try()
defer {
_error_lock.unlock()
}
__error = newValue
}
}
/**
* An error object that indicates why the task failed.
@@ -398,29 +470,29 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
var readStream : Unmanaged<CFReadStream>?
var writeStream : Unmanaged<CFWriteStream>?
if inputStream == nil || outputStream == nil {
if let host = host {
if self.inputStream == nil || self.outputStream == nil {
if let host = self.host {
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, host.hostname as CFString, UInt32(host.port), &readStream, &writeStream)
} else if let service = service {
} else if let service = self.service {
let cfnetService = CFNetServiceCreate(kCFAllocatorDefault, service.domain as CFString, service.type as CFString, service.name as CFString, Int32(service.port))
CFStreamCreatePairWithSocketToNetService(kCFAllocatorDefault, cfnetService.takeRetainedValue(), &readStream, &writeStream)
}
inputStream = readStream?.takeRetainedValue()
outputStream = writeStream?.takeRetainedValue()
guard let inputStream = inputStream, let outputStream = outputStream else {
self.inputStream = readStream?.takeRetainedValue()
self.outputStream = writeStream?.takeRetainedValue()
guard let inputStream = self.inputStream, let outputStream = self.outputStream else {
return
}
streamDelegate?.urlSession?(self._underlyingSession, streamTask: self, didBecome: inputStream, outputStream: outputStream)
self.streamDelegate?.urlSession?(self._underlyingSession, streamTask: self, didBecome: inputStream, outputStream: outputStream)
}
guard let inputStream = inputStream, let outputStream = outputStream else {
guard let inputStream = self.inputStream, let outputStream = self.outputStream else {
return
}
if isSecure {
inputStream.setProperty(securityLevel.rawValue, forKey: .socketSecurityLevelKey)
outputStream.setProperty(securityLevel.rawValue, forKey: .socketSecurityLevelKey)
if self.isSecure {
inputStream.setProperty(self.securityLevel.rawValue, forKey: .socketSecurityLevelKey)
outputStream.setProperty(self.securityLevel.rawValue, forKey: .socketSecurityLevelKey)
} else {
inputStream.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey)
outputStream.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey)
@@ -435,12 +507,37 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
inputStream.open()
outputStream.open()
operation_queue.isSuspended = false
_state = .running
self.operation_queue.isSuspended = false
self._state = .running
}
fileprivate var dataToBeSent: Data = Data()
fileprivate var dataReceived: Data = Data()
fileprivate var _dataReceived: Data = Data()
// We are updating `dataReceived` from main thread while reading it from operation_queue.
let _dataReceived_lock: NSLock = NSLock()
var dataReceived: Data {
get {
if !_dataReceived_lock.try() {
print("not locked")
}
defer {
_dataReceived_lock.unlock()
}
return _dataReceived
}
set {
if !_dataReceived_lock.try() {
print("not locked")
}
defer {
_dataReceived_lock.unlock()
}
_dataReceived = newValue
}
}
/**
* Asynchronously reads a number of bytes from the stream, and calls a handler upon completion.
@@ -471,6 +568,11 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
operation_queue.addOperation {
var timedOut: Bool = false
while (self.dataReceived.count == 0 || self.dataReceived.count < minBytes) && !timedOut {
if let error = inputStream.streamError {
completionHandler(nil, inputStream.streamStatus == .atEnd, error)
return
}
Thread.sleep(forTimeInterval: 0.1)
timedOut = expireDate < Date()
}
@@ -584,6 +686,10 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
var byteSent: Int = 0
let expireDate = Date(timeIntervalSinceNow: timeout)
while self.dataToBeSent.count > 0 && (timeout == 0 || expireDate > Date()) {
if let _ = outputStream.streamError {
return byteSent == 0 ? -1 : byteSent
}
let bytesWritten = self.dataToBeSent.withUnsafeBytes {
outputStream.write($0, maxLength: self.dataToBeSent.count)
}
@@ -684,18 +790,35 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
}
}
private var _retriesForInputStream: Int = 0
private var _retriesForOutputStream: Int = 0
open func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
if eventCode.contains(.errorOccurred) {
self._error = aStream.streamError
streamDelegate?.urlSession?(_underlyingSession, task: self, didCompleteWithError: error)
let retries: Int
if aStream == self.inputStream {
retries = _retriesForInputStream
_retriesForInputStream += 1
} else if aStream == self.outputStream {
retries = _retriesForOutputStream
_retriesForOutputStream += 1
} else {
return
}
if retries < 3 {
aStream.open()
} else {
self._error = aStream.streamError
streamDelegate?.urlSession?(_underlyingSession, task: self, didCompleteWithError: error)
}
}
if aStream == inputStream && eventCode.contains(.hasBytesAvailable) {
while (inputStream!.hasBytesAvailable) {
if aStream == self.inputStream && eventCode.contains(.hasBytesAvailable) {
while (self.inputStream!.hasBytesAvailable) {
var buffer = [UInt8](repeating: 0, count: 2048)
let len = inputStream!.read(&buffer, maxLength: buffer.count)
let len = self.inputStream!.read(&buffer, maxLength: buffer.count)
if len > 0 {
dataReceived.append(&buffer, count: len)
self.dataReceived.append(&buffer, count: len)
self._countOfBytesRecieved += Int64(len)
}
}
+127 -21
View File
@@ -12,7 +12,20 @@ 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: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite {
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" }
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
@@ -99,14 +114,40 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
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(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(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) {
@@ -114,8 +155,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 +165,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 +192,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 +200,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)
}
@@ -201,8 +250,8 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
}
let files: [FileObject] = contents.flatMap {
rfc3659enabled ? self.parseMLST($0, in: path) : self.parseUnixList($0, in: path)
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 {
@@ -258,11 +307,14 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
self.attributesOfItem(path: path, rfc3659enabled: false, completionHandler: completionHandler)
}
let lines = response.components(separatedBy: "\n").flatMap { $0.isEmpty ? nil : $0.trimmingCharacters(in: .whitespacesAndNewlines) }
let lines = response.components(separatedBy: "\n").compactMap { $0.isEmpty ? nil : $0.trimmingCharacters(in: .whitespacesAndNewlines) }
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)
}
@@ -509,7 +561,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
weakTask?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { recevied, totalReceived, totalSize in
}, onProgress: { _, recevied, totalReceived, totalSize in
progress.totalUnitCount = totalSize
progress.completedUnitCount = totalReceived
self.delegateNotify(operation, progress: progress.fractionCompleted)
@@ -593,6 +645,60 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
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: 0)
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!)
self.ftpLogin(task) { (error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
}
return
}
self.ftpFileData(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
@@ -655,9 +761,9 @@ extension FTPFileProvider {
return
}
let codes: [Int] = response.components(separatedBy: .newlines).flatMap({ $0.isEmpty ? nil : $0})
.flatMap {
let code = $0.components(separatedBy: .whitespaces).flatMap({ $0.isEmpty ? nil : $0}).first
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
}
+174 -25
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)
}
@@ -158,13 +178,13 @@ internal extension FTPFileProvider {
throw self.urlError("", code: .badServerResponse)
}
let destArray = destString.components(separatedBy: ",").flatMap({ UInt32(trimmedNumber($0)) })
let destArray = destString.components(separatedBy: ",").compactMap({ UInt32(trimmedNumber($0)) })
guard destArray.count == 6 else {
throw self.urlError("", code: .badServerResponse)
}
// first 4 elements are ip, 2 next are port, as byte
var host = destArray.prefix(4).flatMap(String.init).joined(separator: ".")
var host = destArray.prefix(4).compactMap(String.init).joined(separator: ".")
let portHi = Int(destArray[4]) << 8
let portLo = Int(destArray[5])
let port = portHi + portLo
@@ -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)
}
@@ -246,6 +319,7 @@ internal extension FTPFileProvider {
return
}
let success_lock = NSLock()
var success = false
let command = useMLST ? "MLSD \(path)" : "LIST \(path)"
self.execute(command: command, on: task, minLength: 20, afterSend: { error in
@@ -253,6 +327,7 @@ internal extension FTPFileProvider {
let timeout = self.session.configuration.timeoutIntervalForRequest
var finalData = Data()
var eof = false
let error_lock = NSLock()
var error: Error?
do {
@@ -264,18 +339,22 @@ internal extension FTPFileProvider {
finalData.append(data)
}
eof = seof
error_lock.try()
error = serror
error_lock.unlock()
group.leave()
}
let waitResult = group.wait(timeout: .now() + timeout)
error_lock.try()
if let error = error {
if !((error as NSError).domain == URLError.errorDomain
&& (error as NSError).code == URLError.cancelled.rawValue) {
error_lock.unlock()
if (error as? URLError)?.code != .cancelled {
throw error
}
return
}
error_lock.unlock()
if waitResult == .timedOut {
throw self.urlError(path, code: .timedOut)
@@ -287,8 +366,10 @@ internal extension FTPFileProvider {
}
let contents: [String] = response.components(separatedBy: "\n")
.flatMap({ $0.trimmingCharacters(in: .whitespacesAndNewlines) })
.compactMap({ $0.trimmingCharacters(in: .whitespacesAndNewlines) })
success_lock.try()
success = true
success_lock.unlock()
completionHandler(contents, nil)
} catch {
completionHandler([], error)
@@ -310,8 +391,12 @@ internal extension FTPFileProvider {
throw self.urlError(path, code: .unsupportedURL)
}
success_lock.try()
if !success && !(response.hasPrefix("25") || response.hasPrefix("15")) {
success_lock.unlock()
throw FileProviderFTPError(message: response, path: path)
} else {
success_lock.unlock()
}
} catch {
self.dispatch_queue.async {
@@ -326,16 +411,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
}
@@ -405,6 +490,7 @@ internal extension FTPFileProvider {
DispatchQueue.global().async {
var totalReceived: Int64 = 0
var eof = false
let error_lock = NSLock()
var error: Error?
while !eof {
let group = DispatchGroup()
@@ -419,15 +505,20 @@ internal extension FTPFileProvider {
onProgress(data, totalReceived, totalSize)
}
eof = segeof || (length > 0 && totalReceived >= Int64(length))
error_lock.try()
error = segerror
error_lock.unlock()
group.leave()
}
let waitResult = group.wait(timeout: .now() + timeout)
error_lock.try()
if let error = error {
error_lock.unlock()
completionHandler(error)
return
}
error_lock.unlock()
if waitResult == .timedOut {
completionHandler(self.urlError(filePath, code: .timedOut))
@@ -466,7 +557,7 @@ internal extension FTPFileProvider {
func ftpFileData(_ task: FileProviderStreamTask, filePath: String, from position: Int64 = 0, length: Int = -1,
onTask: ((_ task: FileProviderStreamTask) -> Void)?,
onProgress: ((_ bytesReceived: Int64, _ totalReceived: Int64, _ expectedBytes: Int64) -> Void)?,
onProgress: ((_ data: Data, _ bytesReceived: Int64, _ totalReceived: Int64, _ expectedBytes: Int64) -> Void)?,
completionHandler: @escaping (_ data: Data?, _ error: Error?) -> Void) {
// Check cache
@@ -480,7 +571,7 @@ internal extension FTPFileProvider {
var finalData = Data()
self.ftpRetrieve(task, filePath: filePath, from: position, length: length, onTask: onTask, onProgress: { (data, total, expected) in
finalData.append(data)
onProgress?(Int64(data.count), total, expected)
onProgress?(data, Int64(data.count), total, expected)
}) { (error) in
if let error = error {
completionHandler(nil, error)
@@ -565,6 +656,7 @@ internal extension FTPFileProvider {
return
}
let len = 19 /* TYPE response */ + 44 + filePath.count /* STOR open response */ + 10 /* RETR Transfer complete message. */
let success_lock = NSLock()
var success = false
self.execute(command: "TYPE I" + "\r\n" + "STOR \(filePath)", on: task, minLength: len, afterSend: { error in
onTask?(dataTask)
@@ -583,6 +675,7 @@ internal extension FTPFileProvider {
}
let chunkSize = 4096
let lock = NSLock()
var eof = false
var sent: Int64 = 0
repeat {
@@ -601,33 +694,49 @@ internal extension FTPFileProvider {
let group = DispatchGroup()
group.enter()
dataTask.write(subdata, timeout: timeout, completionHandler: { (serror) in
lock.try()
error = serror
sent += Int64(subdata.count)
let totalsent = sent
let sentbytes = Int64(subdata.count)
lock.unlock()
group.leave()
onProgress?(Int64(subdata.count), sent, size)
onProgress?(sentbytes, totalsent, size)
//print("ftp \(filePath): \(subdata.count), \(sent), \(size)")
})
let waitResult = group.wait(timeout: .now() + timeout)
lock.try()
if waitResult == .timedOut {
error = self.urlError(filePath, code: .timedOut)
}
if let error = error {
lock.unlock()
completionHandler(error)
return
}
if let data = fromData {
lock.try()
let endIndex = min(data.count, Int(sent) + chunkSize)
eof = endIndex == data.count
lock.unlock()
} else if let fileHandle = fileHandle {
eof = Int64(fileHandle.offsetInFile) == size
}
} while !eof
success_lock.try()
success = true
success_lock.unlock()
}) { (response, error) in
guard success else { return }
success_lock.try()
guard success else {
success_lock.unlock()
return
}
success_lock.unlock()
do {
if let error = error {
@@ -750,6 +859,7 @@ internal extension FTPFileProvider {
}
// Send retreive command
let success_lock = NSLock()
var success = false
let len = 19 /* TYPE response */ + 65 + String(position).count /* REST Response */ + 44 + filePath.count /* STOR open response */ + 10 /* RETR Transfer complete message. */
self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "STOR \(filePath)", on: task, minLength: len, afterSend: { error in
@@ -764,13 +874,20 @@ internal extension FTPFileProvider {
completionHandler(error)
return
}
success_lock.try()
success = true
success_lock.unlock()
dataTask.closeRead()
dataTask.closeWrite()
})
}) { (response, error) in
guard success else { return }
success_lock.try()
guard success else {
success_lock.unlock()
return
}
success_lock.unlock()
do {
if let error = error {
@@ -804,6 +921,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,14 +937,14 @@ 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")
farDateFormatter.dateFormat = "MMM dd yyyy"
let thisYear = gregorian.component(.year, from: Date())
let components = text.components(separatedBy: " ").flatMap { $0.isEmpty ? nil : $0 }
let components = text.components(separatedBy: " ").compactMap { $0.isEmpty ? nil : $0 }
guard components.count >= 9 else { return nil }
let posixPermission = components[0]
let linksCount = Int(components[1]) ?? 0
@@ -867,8 +985,35 @@ 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: " ").compactMap { $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 }
var components = text.components(separatedBy: ";").compactMap { $0.isEmpty ? nil : $0 }
guard components.count > 1 else { return nil }
let nameOrPath = components.removeLast().trimmingCharacters(in: .whitespacesAndNewlines)
@@ -885,7 +1030,7 @@ internal extension FTPFileProvider {
var attributes = [String: String]()
for component in components {
let keyValue = component.components(separatedBy: "=") .flatMap { $0.isEmpty ? nil : $0 }
let keyValue = component.components(separatedBy: "=").compactMap { $0.isEmpty ? nil : $0 }
guard keyValue.count >= 2, !keyValue[0].isEmpty else { continue }
attributes[keyValue[0].lowercased()] = keyValue.dropFirst().joined(separator: "=")
}
@@ -950,7 +1095,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
+23 -3
View File
@@ -9,7 +9,7 @@
import Foundation
/// Containts path, url and attributes of a file or resource.
open class FileObject: Equatable {
open class FileObject: NSObject {
/// A `Dictionary` contains file information, using `URLResourceKey` keys.
open internal(set) var allValues: [URLResourceKey: Any]
@@ -19,6 +19,7 @@ open class FileObject: Equatable {
internal init(url: URL?, name: String, path: String) {
self.allValues = [URLResourceKey: Any]()
super.init()
if let url = url {
self.url = url
}
@@ -147,6 +148,18 @@ open class FileObject: Equatable {
open var isSymLink: Bool {
return self.type == .symbolicLink
}
}
extension FileObject {
open override var hashValue: Int {
let hashURL = self.url.hashValue
let hashSize = self.size.hashValue
return (hashURL << 7) &+ hashURL &+ hashSize
}
open override var hash: Int {
return self.hashValue
}
/// Check `FileObject` equality
public static func ==(lhs: FileObject, rhs: FileObject) -> Bool {
@@ -159,7 +172,7 @@ open class FileObject: Equatable {
}
#else
if type(of: lhs) != type(of: rhs) {
return false
return false
}
#endif
@@ -169,6 +182,13 @@ open class FileObject: Equatable {
return rhs.path == lhs.path && rhs.size == lhs.size && rhs.modifiedDate == lhs.modifiedDate
}
open override func isEqual(_ object: Any?) -> Bool {
guard let object = object as? FileObject else { return false }
return self == object
}
}
extension FileObject {
internal func mapPredicate() -> [String: Any] {
let mapDict: [URLResourceKey: String] = [.fileURLKey: "url", .nameKey: "name", .pathKey: "path",
.fileSizeKey: "fileSize", .creationDateKey: "creationDate",
@@ -225,7 +245,7 @@ open class FileObject: Equatable {
}
/// Containts attributes of a provider.
open class VolumeObject {
open class VolumeObject: NSObject {
/// A `Dictionary` contains volume information, using `URLResourceKey` keys.
open internal(set) var allValues: [URLResourceKey: Any]
+88 -19
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)
@@ -577,6 +569,72 @@ extension FileProviderReadWrite {
}
}
/// Defines method for fetching file contents progressivly
public protocol FileProviderReadWriteProgressive {
/**
Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler.
If path specifies a directory, or if some other error occurs, data will be nil.
- Parameters:
- path: Path of file.
- progressHandler: a closure which will be called every time a new data received from server.
- position: Start position of returned data, indexed from zero.
- data: returned `Data` from server.
- completionHandler: a closure which will be called after receiving is completed or an error occureed.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func contents(path: String, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler.
If path specifies a directory, or if some other error occurs, data will be nil.
- Parameters:
- path: Path of file.
- responseHandler: a closure which will be called after fetching server response.
- response: `URLResponse` returned from server.
- progressHandler: a closure which will be called every time a new data received from server.
- position: Start position of returned data, indexed from zero.
- data: returned `Data` from server.
- completionHandler: a closure which will be called after receiving is completed or an error occureed.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func contents(path: String, responseHandler: ((_ response: URLResponse) -> Void)?, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler.
If path specifies a directory, or if some other error occurs, data will be nil.
- Parameters:
- path: Path of file.
- offset: First byte index which should be read. **Starts from 0.**
- length: Bytes count of data. Pass `-1` to read until the end of file.
- responseHandler: a closure which will be called after fetching server response.
- response: `URLResponse` returned from server.
- progressHandler: a closure which will be called every time a new data received from server.
- position: Start position of returned data, indexed from offset.
- data: returned `Data` from server.
- completionHandler: a closure which will be called after receiving is completed or an error occureed.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func contents(path: String, offset: Int64, length: Int, responseHandler: ((_ response: URLResponse) -> Void)?, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress?
}
public extension FileProviderReadWriteProgressive {
@discardableResult
public func contents(path: String, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? {
return contents(path: path, offset: 0, length: -1, responseHandler: nil, progressHandler: progressHandler, completionHandler: completionHandler)
}
@discardableResult
public func contents(path: String, responseHandler: ((_ response: URLResponse) -> Void)?, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? {
return contents(path: path, offset: 0, length: -1, responseHandler: responseHandler, progressHandler: progressHandler, completionHandler: completionHandler)
}
}
/// Allows a file provider to notify changes occured
public protocol FileProviderMonitor: FileProviderBasic {
@@ -781,7 +839,10 @@ public extension FileProviderBasic {
internal func cocoaError(_ path: String, code: CocoaError.Code) -> Error {
let fileURL = self.url(of: path)
return CocoaError(code, userInfo: [NSFilePathErrorKey: path, NSURLErrorKey: fileURL])
let userInfo: [String: Any] = [NSFilePathErrorKey: path,
NSURLErrorKey: fileURL,
]
return CocoaError(code, userInfo: userInfo)
}
internal func NotImplemented(_ fn: String = #function, file: StaticString = #file) {
@@ -887,7 +948,11 @@ extension ExtendedFileProvider {
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
#if os(macOS)
#if swift(>=3.2)
#if swift(>=4.0)
let ppp = Int(NSScreen.main?.backingScaleFactor ?? 1) // fetch device is retina or not
#elseif swift(>=3.3)
let ppp = Int(NSScreen.main()?.backingScaleFactor ?? 1) // fetch device is retina or not
#elseif swift(>=3.2)
let ppp = Int(NSScreen.main?.backingScaleFactor ?? 1) // fetch device is retina or not
#else
let ppp = Int(NSScreen.main()?.backingScaleFactor ?? 1) // fetch device is retina or not
@@ -895,15 +960,23 @@ extension ExtendedFileProvider {
size.width *= CGFloat(ppp)
size.height *= CGFloat(ppp)
#if swift(>=3.2)
#if swift(>=4.0)
let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .calibratedRGB,
bytesPerRow: 0, bitsPerPixel: 0)
#elseif swift(>=3.3)
let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSCalibratedRGBColorSpace,
bytesPerRow: 0, bitsPerPixel: 0)
#elseif swift(>=3.2)
let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .calibratedRGB,
bytesPerRow: 0, bitsPerPixel: 0)
#else
let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSCalibratedRGBColorSpace,
bytesPerRow: 0, bitsPerPixel: 0)
let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSCalibratedRGBColorSpace,
bytesPerRow: 0, bitsPerPixel: 0)
#endif
guard let context = NSGraphicsContext(bitmapImageRep: rep!) else {
@@ -1070,10 +1143,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 -10
View File
@@ -14,14 +14,9 @@ import Foundation
No instance of this class should (and can) be created. Use derived classes instead. It leads to a crash with `fatalError()`.
*/
open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite {
open class HTTPFileProvider: NSObject, FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite, FileProviderReadWriteProgressive {
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 {
@@ -102,6 +97,8 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
operation_queue = OperationQueue()
operation_queue.name = "\(queueLabel).Operation"
super.init()
}
public required convenience init?(coder aDecoder: NSCoder) {
@@ -290,12 +287,10 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
- Returns: An `Progress` to get progress or cancel progress.
*/
@discardableResult
open func contents(path: String, offset: Int64 = 0, responseHandler: ((_ response: URLResponse) -> Void)? = nil, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? {
open func contents(path: String, offset: Int64 = 0, length: Int = -1, responseHandler: ((_ response: URLResponse) -> Void)? = nil, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.fetch(path: path)
var request = self.request(for: operation)
if offset > 0 {
request.addValue("bytes \(offset)-", forHTTPHeaderField: "Range")
}
request.setValue(rangeWithOffset: offset, length: length)
var position: Int64 = offset
return download_progressive(path: path, request: request, operation: operation, responseHandler: responseHandler, progressHandler: { data in
progressHandler(position, data)
+9 -11
View File
@@ -14,7 +14,7 @@ import Foundation
it uses `FileManager` foundation class with some additions like searching and reading a portion of file.
*/
open class LocalFileProvider: FileProvider, FileProviderMonitor {
open class LocalFileProvider: NSObject, FileProvider, FileProviderMonitor {
open class var type: String { return "Local" }
open fileprivate(set) var baseURL: URL?
open var dispatch_queue: DispatchQueue
@@ -114,12 +114,14 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
operation_queue = OperationQueue()
operation_queue.name = "\(queueLabel).Operation"
super.init()
fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self)
opFileManager.delegate = fileProviderManagerDelegate
}
public required convenience init?(coder aDecoder: NSCoder) {
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else {
guard let baseURL = aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL? else {
return nil
}
self.init(baseURL: baseURL)
@@ -158,7 +160,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
dispatch_queue.async {
do {
let contents = try self.fileManager.contentsOfDirectory(at: self.url(of: path), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants)
let filesAttributes = contents.flatMap({ (fileURL) -> LocalFileObject? in
let filesAttributes = contents.compactMap({ (fileURL) -> LocalFileObject? in
let path = self.relativePathOf(url: fileURL)
return LocalFileObject(fileWithPath: path, relativeTo: self.baseURL)
})
@@ -580,16 +582,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
return self.doOperation(operation, data: data ?? Data(), atomically: atomically, completionHandler: completionHandler)
}
fileprivate var monitors = [LocalFolderMonitor]()
fileprivate var monitors = [LocalFileMonitor]()
open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
self.unregisterNotifcation(path: path)
let dirurl = self.url(of: path)
let isdir = (try? dirurl.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
if !isdir {
return
}
let monitor = LocalFolderMonitor(url: dirurl) {
let url = self.url(of: path)
let monitor = LocalFileMonitor(url: url) {
eventHandler()
}
monitor.start()
@@ -597,7 +595,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
}
open func unregisterNotifcation(path: String) {
var removedMonitor: LocalFolderMonitor?
var removedMonitor: LocalFileMonitor?
for (i, monitor) in monitors.enumerated() {
if self.relativePathOf(url: monitor.url) == path {
removedMonitor = monitors.remove(at: i)
+9 -7
View File
@@ -89,19 +89,21 @@ public final class LocalFileObject: FileObject {
}
}
internal final class LocalFolderMonitor {
public final class LocalFileMonitor {
fileprivate let source: DispatchSourceFileSystemObject
fileprivate let descriptor: CInt
fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: .default)
fileprivate var state: Bool = false
fileprivate var monitoredTime: TimeInterval = Date().timeIntervalSinceReferenceDate
var url: URL
public var url: URL
/// Creates a folder monitor object with monitoring enabled.
init(url: URL, handler: @escaping ()->Void) {
public init(url: URL, handler: @escaping ()->Void) {
self.url = url
descriptor = open((url as NSURL).fileSystemRepresentation, O_EVTONLY)
source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: .write, queue: qq)
descriptor = open((url.absoluteURL as NSURL).fileSystemRepresentation, O_EVTONLY)
let event: DispatchSource.FileSystemEvent = url.fileIsDirectory ? [.write] : .all
source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: event, queue: qq)
// Folder monitoring is recursive and deep. Monitoring a root folder may be very costly
// We have a 0.2 second delay to ensure we wont call handler 1000s times when there is
// a huge file operation. This ensures app will work smoothly while this 250 milisec won't
@@ -126,7 +128,7 @@ internal final class LocalFolderMonitor {
}
/// Starts sending notifications if currently stopped
func start() {
public func start() {
if !state {
state = true
source.resume()
@@ -134,7 +136,7 @@ internal final class LocalFolderMonitor {
}
/// Stops sending notifications if currently enabled
func stop() {
public func stop() {
if state {
state = false
source.suspend()
+16 -17
View File
@@ -146,13 +146,13 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
public required convenience init?(coder aDecoder: NSCoder) {
let route: Route
if let driveId = aDecoder.decodeObject(forKey: "drive") as? String, let uuid = UUID(uuidString: driveId) {
if let driveId = aDecoder.decodeObject(of: NSString.self, forKey: "drive") as String?, let uuid = UUID(uuidString: driveId) {
route = .drive(uuid: uuid)
} else {
route = (aDecoder.decodeObject(forKey: "route") as? String).flatMap({ Route(rawValue: $0) }) ?? .me
}
self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential,
serverURL: aDecoder.decodeObject(forKey: "baseURL") as? URL,
self.init(credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential"),
serverURL: aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL?,
route: route)
self.useCache = aDecoder.decodeBool(forKey: "useCache")
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
@@ -227,12 +227,12 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderHTTPError?
var fileObject: OneDriveFileObject?
if let response = response as? HTTPURLResponse {
if let response = response as? HTTPURLResponse, response.statusCode >= 400 {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
if let json = data?.deserializeJSON(), let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: json) {
fileObject = file
}
}
if let json = data?.deserializeJSON(), let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: json) {
fileObject = file
}
completionHandler(fileObject, serverError ?? error)
})
@@ -290,7 +290,7 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
*/
@discardableResult
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
let queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first
let queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).compactMap { $0.value as? String }.first
return paginated(path, requestHandler: { [weak self] (token) -> URLRequest? in
guard let `self` = self else { return nil }
@@ -555,16 +555,15 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderHTTPError?
var link: URL?
if let response = response as? HTTPURLResponse {
if let response = response as? HTTPURLResponse, response.statusCode >= 400 {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
if let json = data?.deserializeJSON() {
if let linkDic = json["link"] as? NSDictionary, let linkStr = linkDic["webUrl"] as? String {
link = URL(string: linkStr)
}
}
if let json = data?.deserializeJSON() {
if let linkDic = json["link"] as? NSDictionary, let linkStr = linkDic["webUrl"] as? String {
link = URL(string: linkStr)
}
}
completionHandler(link, nil, nil, serverError ?? error)
})
task.resume()
@@ -589,9 +588,9 @@ extension OneDriveFileProvider: ExtendedFileProvider {
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
if let json = data?.deserializeJSON() {
(dic, keys) = self.mapMediaInfo(json)
}
}
if let json = data?.deserializeJSON() {
(dic, keys) = self.mapMediaInfo(json)
}
completionHandler(dic, keys, serverError ?? error)
})
+4 -4
View File
@@ -53,7 +53,7 @@ public final class OneDriveFileObject: FileObject {
self.entryTag = json["eTag"] as? String
let hashes = json["file"]?["hashes"] as? NSDictionary
// checks for both sha1 or quickXor. First is available in personal drives, second in business one.
self.hash = (hashes?["sha1Hash"] as? String) ?? (hashes?["quickXorHash"] as? String)
self.fileHash = (hashes?["sha1Hash"] as? String) ?? (hashes?["quickXorHash"] as? String)
}
/// The document identifier is a value assigned by the OneDrive to a file.
@@ -88,7 +88,7 @@ public final class OneDriveFileObject: FileObject {
}
/// Calculated hash from OneDrive server. Hex string SHA1 in personal or Base65 string [QuickXOR](https://dev.onedrive.com/snippets/quickxorhash.htm) in business drives.
open internal(set) var hash: String? {
open internal(set) var fileHash: String? {
get {
return allValues[.documentIdentifierKey] as? String
}
@@ -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 {
+2 -2
View File
@@ -28,7 +28,7 @@ class SMBClient: NSObject, StreamDelegate {
public var timeout: TimeInterval = 30
internal private(set) var messageId: UInt64 = 0
private func createMessageId() -> UInt64 {
fileprivate func createMessageId() -> UInt64 {
defer {
messageId += 1
}
@@ -36,7 +36,7 @@ class SMBClient: NSObject, StreamDelegate {
}
internal private(set) var credit: UInt16 = 0
private func consumeCredit() -> UInt16 {
fileprivate func consumeCredit() -> UInt16 {
if credit > 0 {
credit -= 1
return credit
+2 -2
View File
@@ -310,11 +310,11 @@ extension SMB2 {
static let ipv6: sa_family_t = 0x17
var sockaddr: sockaddr_in {
return Data.mapMemory(from: self.sockaddrStorage)!
return unsafeBitCast(self.sockaddrStorage, to: sockaddr_in.self)
}
var sockaddr6: sockaddr_in6 {
return Data.mapMemory(from: self.sockaddrStorage)!
return unsafeBitCast(self.sockaddrStorage, to: sockaddr_in6.self)
}
}
}
+18 -9
View File
@@ -72,17 +72,23 @@ extension SMB2 {
let header: SMB2FilesInformationHeader
switch type {
case .fileDirectoryInformation:
header = buffer.scanValue(start: offset) as FileDirectoryInformationHeader!
let tHeader: FileDirectoryInformationHeader = buffer.scanValue(start: offset)!
header = tHeader
case .fileFullDirectoryInformation:
header = buffer.scanValue(start: offset) as FileFullDirectoryInformationHeader!
let tHeader: FileFullDirectoryInformationHeader = buffer.scanValue(start: offset)!
header = tHeader
case .fileIdFullDirectoryInformation:
header = buffer.scanValue(start: offset) as FileIdFullDirectoryInformationHeader!
let tHeader: FileIdFullDirectoryInformationHeader = buffer.scanValue(start: offset)!
header = tHeader
case .fileBothDirectoryInformation:
header = buffer.scanValue(start: offset) as FileBothDirectoryInformationHeader!
let tHeader: FileBothDirectoryInformationHeader = buffer.scanValue(start: offset)!
header = tHeader
case .fileIdBothDirectoryInformation:
header = buffer.scanValue(start: offset) as FileIdBothDirectoryInformationHeader!
let tHeader: FileIdBothDirectoryInformationHeader = buffer.scanValue(start: offset)!
header = tHeader
case .fileNamesInformation:
header = buffer.scanValue(start: offset) as FileNamesInformationHeader!
let tHeader: FileNamesInformationHeader = buffer.scanValue(start: offset)!
header = tHeader
default:
return []
}
@@ -98,8 +104,10 @@ extension SMB2 {
}
init? (data: Data) {
let offset = Int(data.scanValue(start: 2) as UInt16!)
let length = Int(data.scanValue(start: 4) as UInt32!)
let tOffset: UInt16 = data.scanValue(start: 2)!
let offset = Int(tOffset)
let tLength: UInt32 = data.scanValue(start: 4)!
let length = Int(tLength)
guard data.count > offset + length else {
return nil
}
@@ -200,7 +208,8 @@ extension SMB2 {
/*let offsetData = data.subdataWithRange(NSRange(location: 2, length: 2))
let offset: UInt16 = decode(offsetData)*/
let length = Int(data.scanValue(start: 4) as UInt32!)
let tLength: UInt32 = data.scanValue(start: 4)!
let length = Int(tLength)
guard data.count >= 8 + length else {
return nil
+2 -2
View File
@@ -46,11 +46,11 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing {
}
public required convenience init?(coder aDecoder: NSCoder) {
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else {
guard let baseURL = aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL? else {
return nil
}
self.init(baseURL: baseURL,
credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential"))
self.useCache = aDecoder.decodeBool(forKey: "useCache")
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
}
+84 -10
View File
@@ -8,7 +8,7 @@
import XCTest
import FilesProvider
class FilesProviderTests: XCTestCase {
class FilesProviderTests: XCTestCase, FileProviderDelegate {
override func setUp() {
super.setUp()
@@ -27,6 +27,7 @@ class FilesProviderTests: XCTestCase {
self.testRemoveFile(provider, filePath: self.testFolderName)
}
testBasic(provider)
testArchiving(provider)
testOperations(provider)
}
@@ -40,6 +41,8 @@ class FilesProviderTests: XCTestCase {
cred = nil
}
let provider = WebDAVFileProvider(baseURL: url, credential: cred)!
provider.delegate = self
testArchiving(provider)
addTeardownBlock {
self.testRemoveFile(provider, filePath: self.testFolderName)
}
@@ -52,6 +55,8 @@ class FilesProviderTests: XCTestCase {
}
let cred = URLCredential(user: "testuser", password: pass, persistence: .forSession)
let provider = DropboxFileProvider(credential: cred)
provider.delegate = self
testArchiving(provider)
addTeardownBlock {
self.testRemoveFile(provider, filePath: self.testFolderName)
}
@@ -68,7 +73,9 @@ 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
testArchiving(provider)
addTeardownBlock {
self.testRemoveFile(provider, filePath: self.testFolderName)
}
@@ -81,10 +88,12 @@ class FilesProviderTests: XCTestCase {
}
let cred = URLCredential(user: "testuser", password: pass, persistence: .forSession)
let provider = OneDriveFileProvider(credential: cred)
provider.delegate = self
testBasic(provider)
testArchiving(provider)
addTeardownBlock {
self.testRemoveFile(provider, filePath: self.testFolderName)
}
testBasic(provider)
testOperations(provider)
}
@@ -106,16 +115,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 +140,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 +158,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 +188,49 @@ 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)
if provider is FTPFileProvider {
// FTP will need to download and upload file again.
wait(for: [expectation], timeout: timeout * 6)
} else {
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 {
@@ -227,8 +257,8 @@ class FilesProviderTests: XCTestCase {
fileprivate func testUploadFile(_ provider: FileProvider, filePath: String) {
// 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 +266,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 +284,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 +304,34 @@ 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 testArchiving(_ provider: FileProvider) {
let archivedData = NSKeyedArchiver.archivedData(withRootObject: provider)
let unarchived = NSKeyedUnarchiver.unarchiveObject(with: archivedData) as? FileProvider
XCTAssertNotNil(unarchived)
XCTAssertEqual(unarchived?.baseURL, provider.baseURL, "archived provider is not same with original")
XCTAssertEqual(unarchived?.credential, provider.credential, "archived provider is not same with original")
if let provider = provider as? FileProviderBasicRemote, let unarchived_r = unarchived as? FileProviderBasicRemote {
XCTAssertEqual(unarchived_r.useCache, provider.useCache, "archived provider is not same with original")
XCTAssertEqual(unarchived_r.validatingCache, provider.validatingCache, "archived provider is not same with original")
}
if let provider = provider as? OneDriveFileProvider, let unarchived_o = unarchived as? OneDriveFileProvider {
XCTAssertEqual(unarchived_o.route.rawValue, provider.route.rawValue, "archived provider is not same with original")
}
}
fileprivate func testBasic(_ provider: FileProvider) {
@@ -300,6 +351,7 @@ class FilesProviderTests: XCTestCase {
}
fileprivate func testOperations(_ provider: FileProvider) {
// Test file operations
testReachability(provider)
testCreateFolder(provider, folderName: testFolderName)
testContentsOfDirectory(provider)
@@ -317,4 +369,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
}
}