Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ccb7961171 | |||
| e4fc6b24c0 | |||
| f22af8d002 | |||
| e5e5faa4e8 | |||
| f2cd571d7a | |||
| 4202f5e1bd | |||
| 3040215ce3 | |||
| bd2f2b3954 | |||
| 9d19768e1c | |||
| bcc774d3a8 | |||
| ad9768a584 | |||
| a089cbc21c | |||
| 090baa3b61 | |||
| fc75c85b14 |
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
|
||||
#
|
||||
|
||||
s.name = "FilesProvider"
|
||||
s.version = "0.24.0"
|
||||
s.version = "0.25.0"
|
||||
s.summary = "FileManager replacement for Local and Remote (WebDAV/FTP/Dropbox/OneDrive/SMB2) files on iOS and macOS."
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
@@ -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,7 +65,7 @@ Pod::Spec.new do |s|
|
||||
# the deployment target. You can optionally include the target after the platform.
|
||||
#
|
||||
|
||||
s.swift_version = "4.0"
|
||||
s.swift_version = "4.1"
|
||||
s.ios.deployment_target = "8.0"
|
||||
s.osx.deployment_target = "10.10"
|
||||
# s.watchos.deployment_target = "2.0"
|
||||
@@ -117,7 +116,10 @@ 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 = "xml2"
|
||||
# s.libraries = "iconv", "xml2"
|
||||
|
||||
@@ -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.24.0;
|
||||
BUNDLE_VERSION_STRING = 0.25.0;
|
||||
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.24.0;
|
||||
BUNDLE_VERSION_STRING = 0.25.0;
|
||||
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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ 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 {
|
||||
@@ -114,6 +114,8 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "\(queueLabel).Operation"
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,13 +137,13 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else { return nil }
|
||||
let mode: Mode
|
||||
if let modeStr = aDecoder.decodeObject(forKey: "mode") as? String, let mode_v = Mode(rawValue: modeStr) {
|
||||
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(forKey: "credential") as? URLCredential)
|
||||
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")
|
||||
@@ -248,7 +250,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
}
|
||||
|
||||
|
||||
let files: [FileObject] = contents.flatMap {
|
||||
let files: [FileObject] = contents.compactMap {
|
||||
rfc3659enabled ? self.parseMLST($0, in: path) : (self.parseUnixList($0, in: path) ?? self.parseDOSList($0, in: path))
|
||||
}
|
||||
|
||||
@@ -305,7 +307,7 @@ 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)
|
||||
}
|
||||
@@ -559,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)
|
||||
@@ -643,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
|
||||
@@ -705,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
|
||||
}
|
||||
|
||||
|
||||
+57
-12
@@ -178,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
|
||||
@@ -319,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
|
||||
@@ -326,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 {
|
||||
@@ -337,17 +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 {
|
||||
error_lock.unlock()
|
||||
if (error as? URLError)?.code != .cancelled {
|
||||
throw error
|
||||
}
|
||||
return
|
||||
}
|
||||
error_lock.unlock()
|
||||
|
||||
if waitResult == .timedOut {
|
||||
throw self.urlError(path, code: .timedOut)
|
||||
@@ -359,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)
|
||||
@@ -382,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 {
|
||||
@@ -477,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()
|
||||
@@ -491,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))
|
||||
@@ -538,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
|
||||
@@ -552,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)
|
||||
@@ -637,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)
|
||||
@@ -655,6 +675,7 @@ internal extension FTPFileProvider {
|
||||
}
|
||||
|
||||
let chunkSize = 4096
|
||||
let lock = NSLock()
|
||||
var eof = false
|
||||
var sent: Int64 = 0
|
||||
repeat {
|
||||
@@ -673,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 {
|
||||
@@ -822,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
|
||||
@@ -836,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 {
|
||||
@@ -899,7 +944,7 @@ internal extension FTPFileProvider {
|
||||
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
|
||||
@@ -947,7 +992,7 @@ internal extension FTPFileProvider {
|
||||
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
dateFormatter.dateFormat = "M-d-y hh:mma"
|
||||
|
||||
let components = text.components(separatedBy: " ").flatMap { $0.isEmpty ? nil : $0 }
|
||||
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: " ")
|
||||
@@ -968,7 +1013,7 @@ internal extension FTPFileProvider {
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -985,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: "=")
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
+87
-17
@@ -569,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 {
|
||||
|
||||
@@ -764,27 +830,19 @@ public extension FileProviderBasic {
|
||||
|
||||
internal func urlError(_ path: String, code: URLError.Code) -> Error {
|
||||
let fileURL = self.url(of: path)
|
||||
var userInfo: [String: Any] = [NSURLErrorKey: fileURL,
|
||||
let userInfo: [String: Any] = [NSURLErrorKey: fileURL,
|
||||
NSURLErrorFailingURLErrorKey: fileURL,
|
||||
NSURLErrorFailingURLStringErrorKey: fileURL.absoluteString,
|
||||
]
|
||||
let error = NSError(domain: NSURLErrorDomain, code: code.rawValue, userInfo: nil)
|
||||
for (key, value) in error.userInfo {
|
||||
userInfo[key] = value
|
||||
}
|
||||
return URLError(code, userInfo: userInfo)
|
||||
}
|
||||
|
||||
internal func cocoaError(_ path: String, code: CocoaError.Code) -> Error {
|
||||
let fileURL = self.url(of: path)
|
||||
var userInfo: [String: Any] = [NSFilePathErrorKey: path,
|
||||
let userInfo: [String: Any] = [NSFilePathErrorKey: path,
|
||||
NSURLErrorKey: fileURL,
|
||||
]
|
||||
let error = NSError(domain: NSCocoaErrorDomain, code: code.rawValue, userInfo: nil)
|
||||
for (key, value) in error.userInfo {
|
||||
userInfo[key] = value
|
||||
}
|
||||
return cocoaError(fileURL.path, code: code)
|
||||
return CocoaError(code, userInfo: userInfo)
|
||||
}
|
||||
|
||||
internal func NotImplemented(_ fn: String = #function, file: StaticString = #file) {
|
||||
@@ -890,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
|
||||
@@ -898,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 {
|
||||
|
||||
@@ -14,7 +14,7 @@ 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?
|
||||
open var dispatch_queue: DispatchQueue
|
||||
@@ -97,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) {
|
||||
@@ -285,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
self.testRemoveFile(provider, filePath: self.testFolderName)
|
||||
}
|
||||
testBasic(provider)
|
||||
testArchiving(provider)
|
||||
testOperations(provider)
|
||||
}
|
||||
|
||||
@@ -41,6 +42,7 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
}
|
||||
let provider = WebDAVFileProvider(baseURL: url, credential: cred)!
|
||||
provider.delegate = self
|
||||
testArchiving(provider)
|
||||
addTeardownBlock {
|
||||
self.testRemoveFile(provider, filePath: self.testFolderName)
|
||||
}
|
||||
@@ -54,6 +56,7 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
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)
|
||||
}
|
||||
@@ -72,6 +75,7 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
}
|
||||
let provider = FTPFileProvider(baseURL: url, mode: .extendedPassive, credential: cred)!
|
||||
provider.delegate = self
|
||||
testArchiving(provider)
|
||||
addTeardownBlock {
|
||||
self.testRemoveFile(provider, filePath: self.testFolderName)
|
||||
}
|
||||
@@ -85,10 +89,11 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -207,7 +212,12 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
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).")
|
||||
}
|
||||
|
||||
@@ -247,7 +257,6 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
|
||||
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)
|
||||
@@ -310,6 +319,21 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
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) {
|
||||
let filepath = "/test/file.txt"
|
||||
let fileurl = provider.url(of: filepath)
|
||||
|
||||
Reference in New Issue
Block a user