From f0cd7846d84fcc054a34d47e21780dde9337938c Mon Sep 17 00:00:00 2001 From: Amir Abbas Mousavian Date: Mon, 11 Jul 2016 13:31:22 +0430 Subject: [PATCH] FileOpreationDelegate, delegate strong reference bug, fileByUniqueName method --- Source/FileProvider.swift | 99 ++++++++++++++++++---- Source/LocalFileProvider.swift | 142 +++++++++++++++++++++++++++----- Source/SMBFileProvider.swift | 9 +- Source/WebDAVFileProvider.swift | 12 +-- 4 files changed, 218 insertions(+), 44 deletions(-) diff --git a/Source/FileProvider.swift b/Source/FileProvider.swift index f8891d1..7bc0b26 100644 --- a/Source/FileProvider.swift +++ b/Source/FileProvider.swift @@ -7,11 +7,12 @@ // import Foundation -#if (iOS) +#if os(iOS) import UIKit -#endif -#if (OSX) -import AppKit +public typealias ImageClass = UIImage +#elseif os(OSX) +import Cocoa +public typealias ImageClass = NSImage #endif public enum FileType: String { @@ -63,6 +64,7 @@ extension NSCocoaError: FoundationErrorEnum {} public class FileObject { let absoluteURL: NSURL? let name: String + let path: String let size: Int64 let createdDate: NSDate? let modifiedDate: NSDate? @@ -70,9 +72,10 @@ public class FileObject { let isHidden: Bool let isReadOnly: Bool - init(absoluteURL: NSURL, name: String, size: Int64, createdDate: NSDate?, modifiedDate: NSDate?, fileType: FileType, isHidden: Bool, isReadOnly: Bool) { + init(absoluteURL: NSURL?, name: String, path: String, size: Int64, createdDate: NSDate?, modifiedDate: NSDate?, fileType: FileType, isHidden: Bool, isReadOnly: Bool) { self.absoluteURL = absoluteURL self.name = name + self.path = path self.size = size self.createdDate = createdDate self.modifiedDate = modifiedDate @@ -81,9 +84,10 @@ public class FileObject { self.isReadOnly = isReadOnly } - init(name: String, createdDate: NSDate?, modifiedDate: NSDate?, isHidden: Bool, isReadOnly: Bool) { - self.absoluteURL = NSURL() + init(name: String, path: String, createdDate: NSDate?, modifiedDate: NSDate?, isHidden: Bool, isReadOnly: Bool) { + self.absoluteURL = nil self.name = name + self.path = path self.size = -1 self.createdDate = createdDate self.modifiedDate = modifiedDate @@ -113,6 +117,8 @@ public protocol FileProviderBasic: class { } public protocol FileProviderOperations: FileProviderBasic { + var fileOperationDelegate : FileOperationDelegate? { get set } + func createFolder(folderName: String, atPath: String, completionHandler: SimpleCompletionHandler) func createFile(fileAttribs: FileObject, atPath: String, contents data: NSData?, completionHandler: SimpleCompletionHandler) func moveItemAtPath(path: String, toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) @@ -134,6 +140,7 @@ public protocol FileProviderReadWrite: FileProviderBasic { public protocol FileProviderMonitor: FileProviderBasic { func registerNotifcation(path: String, eventHandler: (() -> Void)) func unregisterNotifcation(path: String) + func isRegisteredForNotification(path: String) -> Bool } public protocol FileProvider: FileProviderBasic, FileProviderOperations, FileProviderReadWrite { @@ -145,7 +152,7 @@ extension FileProviderBasic { return currentPath.stringByTrimmingCharactersInSet(NSCharacterSet(charactersInString: ". /")) } - func absoluteURL(path: String? = nil) -> NSURL { + public func absoluteURL(path: String? = nil) -> NSURL { let rpath: String if let path = path { rpath = path @@ -165,6 +172,36 @@ extension FileProviderBasic { } } + public func relativePathOf(url url: NSURL) -> String { + guard let baseURL = self.baseURL else { return url.absoluteString } + return url.absoluteString.stringByReplacingOccurrencesOfString(baseURL.absoluteString, withString: "/").stringByRemovingPercentEncoding! + } + + public func fileByUniqueName(filePath: String) -> String { + let dirPath = (filePath as NSString).stringByDeletingLastPathComponent + let fileName = ((filePath as NSString).lastPathComponent as NSString).stringByDeletingPathExtension + var result = fileName + let group = dispatch_group_create() + dispatch_group_enter(group) + self.contentsOfDirectoryAtPath(dirPath) { (contents, error) in + var i = Int(fileName.componentsSeparatedByString(" ").filter { + !$0.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()).isEmpty + }.last ?? "noname") ?? 2 + let similiar = contents.map { + $0.absoluteURL?.lastPathComponent ?? $0.name + }.filter { + $0.hasPrefix(fileName) && $0.hasSuffix("." + (filePath as NSString).pathExtension) + } + while similiar.contains(result) { + result = ((((fileName as NSString).stringByDeletingPathExtension + " \(i)") as NSString).pathExtension as NSString).stringByAppendingPathExtension((filePath as NSString).pathExtension)! + i += 1 + } + dispatch_group_leave(group) + } + dispatch_group_wait(group, DISPATCH_TIME_FOREVER) + return (dirPath as NSString).stringByAppendingPathComponent(result) + } + internal func throwError(path: String, code: FoundationErrorEnum) -> NSError { let fileURL = self.absoluteURL(path) let domain: String @@ -218,17 +255,13 @@ extension FileProviderBasic { } } -#if (iOS) + public protocol ExtendedFileProvider: FileProvider { - func thumbnailOfFileAtPath(path: String, dimension: CGSize, completionHandler: ((image: UIImage?, error: ErrorType?) -> Void)) + func thumbnailOfFileSupported(path: String) -> Bool + func propertiesOfFileSupported(path: String) -> Bool + func thumbnailOfFileAtPath(path: String, dimension: CGSize, completionHandler: ((image: ImageClass?, error: ErrorType?) -> Void)) func propertiesOfFileAtPath(path: String, completionHandler: ((propertiesDictionary: [String: AnyObject], keys: [String], error: ErrorType?) -> Void)) } -#elseif (OSX) -public protocol ExtendedFileProvider: FileProvider { - func thumbnailOfFileAtPath(path: String, dimension: CGSize, completionHandler: ((image: NSImage?, error: ErrorType?) -> Void)) - func propertiesOfFileAtPath(path: String, completionHandler: ((propertiesDictionary: [String: AnyObject], keys: [String], error: ErrorType?) -> Void)) -} -#endif public enum FileOperation { case Create (path: String) @@ -237,12 +270,42 @@ public enum FileOperation { case Modify (path: String) case Remove (path: String) case Link (link: String, target: String) + + var description: String { + switch self { + case .Create(path: _): return "Create" + case .Copy(source: _, destination: _): return "Copy" + case .Move(source: _, destination: _): return "Move" + case .Modify(path: _): return "Modify" + case .Remove(path: _): return "Remove" + case .Link(link: _, target: _): return "Link" + } + } + + var actionDescription: String { + switch self { + case .Create(path: _): return "Creating" + case .Copy(source: _, destination: _): return "Copying" + case .Move(source: _, destination: _): return "Moving" + case .Modify(path: _): return "Modifying" + case .Remove(path: _): return "Removing" + case .Link(link: _, target: _): return "Linking" + } + } + } -public protocol FileProviderDelegate { +public protocol FileProviderDelegate: class { func fileproviderSucceed(fileProvider: FileProviderOperations, operation: FileOperation) func fileproviderFailed(fileProvider: FileProviderOperations, operation: FileOperation) func fileproviderProgress(fileProvider: FileProviderOperations, operation: FileOperation, progress: Float) } - +public protocol FileOperationDelegate: class { + + /// fileProvider(_:shouldOperate:) gives the delegate an opportunity to filter the file operation. Returning true from this method will allow the copy to happen. Returning false from this method causes the item in question to be skipped. If the item skipped was a directory, no children of that directory will be subject of the operation, nor will the delegate be notified of those children. + func fileProvider(fileProvider: FileProviderOperations, shouldDoOperation operation: FileOperation) -> Bool + + /// fileProvider:shouldProceedAfterError:copyingItemAtPath:toPath: gives the delegate an opportunity to recover from or continue copying after an error. If an error occurs, the error object will contain an ErrorType indicating the problem. The source path and destination paths are also provided. If this method returns true, the FileProvider instance will continue as if the error had not occurred. If this method returns false, the NSFileManager instance will stop copying, return false from copyItemAtPath:toPath:error: and the error will be provied there. + func fileProvider(fileProvider: FileProviderOperations, shouldProceedAfterError error: ErrorType, operation: FileOperation) -> Bool +} \ No newline at end of file diff --git a/Source/LocalFileProvider.swift b/Source/LocalFileProvider.swift index c88c2ff..4ecca96 100644 --- a/Source/LocalFileProvider.swift +++ b/Source/LocalFileProvider.swift @@ -11,30 +11,35 @@ import Foundation public final class LocalFileObject: FileObject { let allocatedSize: Int64 - init(absoluteURL: NSURL, name: String, size: Int64, allocatedSize: Int64, createdDate: NSDate?, modifiedDate: NSDate?, fileType: FileType, isHidden: Bool, isReadOnly: Bool) { + init(absoluteURL: NSURL, name: String, path: String, size: Int64, allocatedSize: Int64, createdDate: NSDate?, modifiedDate: NSDate?, fileType: FileType, isHidden: Bool, isReadOnly: Bool) { self.allocatedSize = allocatedSize - super.init(absoluteURL: absoluteURL, name: name, size: size, createdDate: createdDate, modifiedDate: modifiedDate, fileType: fileType, isHidden: isHidden, isReadOnly: isReadOnly) + super.init(absoluteURL: absoluteURL, name: name, path: path, size: size, createdDate: createdDate, modifiedDate: modifiedDate, fileType: fileType, isHidden: isHidden, isReadOnly: isReadOnly) } } public class LocalFileProvider: FileProvider, FileProviderMonitor { - public let type = "NSFileManager" + public let type = "Local" public var isPathRelative: Bool = true public var baseURL: NSURL? = LocalFileProvider.defaultBaseURL() public var currentPath: String = "" public var dispatch_queue: dispatch_queue_t - public var delegate: FileProviderDelegate? + public weak var delegate: FileProviderDelegate? public let credential: NSURLCredential? = nil - let fileManager = NSFileManager() + public let fileManager = NSFileManager() + private var fileProviderManagerDelegate: LocalFileProviderManagerDelegate? = nil init () { - dispatch_queue = dispatch_queue_create("FileProvider.\(type)", DISPATCH_QUEUE_SERIAL) + dispatch_queue = dispatch_queue_create("FileProvider.\(type)", DISPATCH_QUEUE_CONCURRENT) + fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self) + fileManager.delegate = fileProviderManagerDelegate } init (baseURL: NSURL) { self.baseURL = baseURL - dispatch_queue = dispatch_queue_create("FileProvider.\(type)", DISPATCH_QUEUE_SERIAL) + dispatch_queue = dispatch_queue_create("FileProvider.\(type)", DISPATCH_QUEUE_CONCURRENT) + fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self) + fileManager.delegate = fileProviderManagerDelegate } private static func defaultBaseURL() -> NSURL { @@ -66,7 +71,13 @@ public class LocalFileProvider: FileProvider, FileProviderMonitor { _ = try? fileURL.getResourceValue(&filetypev, forKey: NSURLFileResourceTypeKey) _ = try? fileURL.getResourceValue(&hiddenv, forKey: NSURLIsHiddenKey) _ = try? fileURL.getResourceValue(&readonlyv, forKey: NSURLVolumeIsReadOnlyKey) - let fileAttr = LocalFileObject(absoluteURL: fileURL, name: namev as! String, size: sizev?.longLongValue ?? -1, allocatedSize: allocated?.longLongValue ?? -1, createdDate: creationDatev as? NSDate, modifiedDate: modifiedDatev as? NSDate, fileType: FileType(urlResourceTypeValue: filetypev as? String ?? ""), isHidden: hiddenv?.boolValue ?? false, isReadOnly: readonlyv?.boolValue ?? false) + let path: String + if isPathRelative { + path = self.relativePathOf(url: fileURL) + } else { + path = fileURL.path! + } + let fileAttr = LocalFileObject(absoluteURL: fileURL, name: namev as! String, path: path, size: sizev?.longLongValue ?? -1, allocatedSize: allocated?.longLongValue ?? -1, createdDate: creationDatev as? NSDate, modifiedDate: modifiedDatev as? NSDate, fileType: FileType(urlResourceTypeValue: filetypev as? String ?? ""), isHidden: hiddenv?.boolValue ?? false, isReadOnly: readonlyv?.boolValue ?? false) return fileAttr } @@ -76,6 +87,8 @@ public class LocalFileProvider: FileProvider, FileProviderMonitor { } } + public weak var fileOperationDelegate : FileOperationDelegate? + public func createFolder(folderName: String, atPath: String, completionHandler: SimpleCompletionHandler) { dispatch_async(dispatch_queue) { do { @@ -282,31 +295,38 @@ public class LocalFileProvider: FileProvider, FileProviderMonitor { } } - private var monitorDictionary = [String : LocalFolderMonitor]() + private var monitors = [LocalFolderMonitor]() public func registerNotifcation(path: String, eventHandler: (() -> Void)) { self.unregisterNotifcation(path) + let absurl = self.absoluteURL(path) var isdirv: AnyObject? do { - try absoluteURL(path).getResourceValue(&isdirv, forKey: NSURLIsDirectoryKey) + try absurl.getResourceValue(&isdirv, forKey: NSURLIsDirectoryKey) } catch _ { } if !(isdirv?.boolValue ?? false) { return } - let monitor = LocalFolderMonitor(url: absoluteURL(path)) { + let monitor = LocalFolderMonitor(url: absurl) { eventHandler() } monitor.start() - monitorDictionary[path] = monitor + monitors.append(monitor) } public func unregisterNotifcation(path: String) { - if let prevMonitor = monitorDictionary[path] { - prevMonitor.stop() - monitorDictionary.removeValueForKey(path) + for (i, monitor) in monitors.enumerate() { + if self.relativePathOf(url: monitor.url) == path { + monitor.stop() + monitors.removeAtIndex(i) + } } } + + public func isRegisteredForNotification(path: String) -> Bool { + return monitors.map( { self.relativePathOf(url: $0.url) } ).contains(path) + } } extension LocalFileProvider { @@ -328,11 +348,90 @@ extension LocalFileProvider { } } -private class LocalFolderMonitor { +class LocalFileProviderManagerDelegate: NSObject, NSFileManagerDelegate { + weak var provider: LocalFileProvider? + + init(provider: LocalFileProvider) { + self.provider = provider + } + + func fileManager(fileManager: NSFileManager, shouldCopyItemAtURL srcURL: NSURL, toURL dstURL: NSURL) -> Bool { + guard let provider = self.provider, delegate = provider.fileOperationDelegate else { + return true + } + let srcPath = provider.relativePathOf(url: srcURL) + let dstPath = provider.relativePathOf(url: dstURL) + return delegate.fileProvider(provider, shouldDoOperation: .Copy(source: srcPath, destination: dstPath)) + } + + func fileManager(fileManager: NSFileManager, shouldMoveItemAtURL srcURL: NSURL, toURL dstURL: NSURL) -> Bool { + guard let provider = self.provider, delegate = provider.fileOperationDelegate else { + return true + } + let srcPath = provider.relativePathOf(url: srcURL) + let dstPath = provider.relativePathOf(url: dstURL) + return delegate.fileProvider(provider, shouldDoOperation: .Move(source: srcPath, destination: dstPath)) + } + + func fileManager(fileManager: NSFileManager, shouldRemoveItemAtURL URL: NSURL) -> Bool { + guard let provider = self.provider, delegate = provider.fileOperationDelegate else { + return true + } + let path = provider.relativePathOf(url: URL) + return delegate.fileProvider(provider, shouldDoOperation: .Remove(path: path)) + } + + func fileManager(fileManager: NSFileManager, shouldLinkItemAtURL srcURL: NSURL, toURL dstURL: NSURL) -> Bool { + guard let provider = self.provider, delegate = provider.fileOperationDelegate else { + return true + } + let srcPath = provider.relativePathOf(url: srcURL) + let dstPath = provider.relativePathOf(url: dstURL) + return delegate.fileProvider(provider, shouldDoOperation: .Link(link: srcPath, target: dstPath)) + } + + func fileManager(fileManager: NSFileManager, shouldProceedAfterError error: NSError, copyingItemAtURL srcURL: NSURL, toURL dstURL: NSURL) -> Bool { + guard let provider = self.provider, delegate = provider.fileOperationDelegate else { + return false + } + let srcPath = provider.relativePathOf(url: srcURL) + let dstPath = provider.relativePathOf(url: dstURL) + return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .Copy(source: srcPath, destination: dstPath)) + } + + func fileManager(fileManager: NSFileManager, shouldProceedAfterError error: NSError, movingItemAtURL srcURL: NSURL, toURL dstURL: NSURL) -> Bool { + guard let provider = self.provider, delegate = provider.fileOperationDelegate else { + return false + } + let srcPath = provider.relativePathOf(url: srcURL) + let dstPath = provider.relativePathOf(url: dstURL) + return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .Move(source: srcPath, destination: dstPath)) + } + + func fileManager(fileManager: NSFileManager, shouldProceedAfterError error: NSError, removingItemAtURL URL: NSURL) -> Bool { + guard let provider = self.provider, delegate = provider.fileOperationDelegate else { + return false + } + let path = provider.relativePathOf(url: URL) + return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .Remove(path: path)) + } + + func fileManager(fileManager: NSFileManager, shouldProceedAfterError error: NSError, linkingItemAtURL srcURL: NSURL, toURL dstURL: NSURL) -> Bool { + guard let provider = self.provider, delegate = provider.fileOperationDelegate else { + return false + } + let srcPath = provider.relativePathOf(url: srcURL) + let dstPath = provider.relativePathOf(url: dstURL) + return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .Link(link: srcPath, target: dstPath)) + } +} + +internal class LocalFolderMonitor { private let source: dispatch_source_t private let descriptor: CInt - private let qq: dispatch_queue_t = dispatch_get_main_queue() + private let qq: dispatch_queue_t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) private var state: Bool = false + var url: NSURL /// Creates a folder monitor object with monitoring enabled. init(url: NSURL, handler: ()->Void) { @@ -345,8 +444,13 @@ private class LocalFolderMonitor { DISPATCH_VNODE_WRITE, qq ) - - dispatch_source_set_event_handler(source, handler) + let main_handler: ()->Void = { + dispatch_async(dispatch_get_main_queue(), { + handler() + }) + } + dispatch_source_set_event_handler(source, main_handler) + self.url = url start() } diff --git a/Source/SMBFileProvider.swift b/Source/SMBFileProvider.swift index 0756108..79b7f66 100644 --- a/Source/SMBFileProvider.swift +++ b/Source/SMBFileProvider.swift @@ -14,7 +14,7 @@ class SMBFileProvider: FileProvider, FileProviderMonitor { var baseURL: NSURL? var currentPath: String = "" var dispatch_queue: dispatch_queue_t - var delegate: FileProviderDelegate? + weak var delegate: FileProviderDelegate? let credential: NSURLCredential? typealias FileObjectClass = FileObject @@ -40,6 +40,8 @@ class SMBFileProvider: FileProvider, FileProviderMonitor { NotImplemented() } + weak var fileOperationDelegate: FileOperationDelegate? + func createFolder(folderName: String, atPath: String, completionHandler: SimpleCompletionHandler) { NotImplemented() } @@ -91,7 +93,10 @@ class SMBFileProvider: FileProvider, FileProviderMonitor { func unregisterNotifcation(path: String) { NotImplemented() } - + + func isRegisteredForNotification(path: String) -> Bool { + return false + } } // MARK: basic CIFS interactivity diff --git a/Source/WebDAVFileProvider.swift b/Source/WebDAVFileProvider.swift index 5625920..38ea660 100644 --- a/Source/WebDAVFileProvider.swift +++ b/Source/WebDAVFileProvider.swift @@ -51,10 +51,10 @@ public final class WebDavFileObject: FileObject { let contentType: String let entryTag: String? - init(absoluteURL: NSURL, name: String, size: Int64, contentType: String, createdDate: NSDate?, modifiedDate: NSDate?, fileType: FileType, isHidden: Bool, isReadOnly: Bool, entryTag: String?) { + init(absoluteURL: NSURL, name: String, path: String, size: Int64, contentType: String, createdDate: NSDate?, modifiedDate: NSDate?, fileType: FileType, isHidden: Bool, isReadOnly: Bool, entryTag: String?) { self.contentType = contentType self.entryTag = entryTag - super.init(absoluteURL: absoluteURL, name: name, size: size, createdDate: createdDate, modifiedDate: modifiedDate, fileType: fileType, isHidden: isHidden, isReadOnly: isReadOnly) + super.init(absoluteURL: absoluteURL, name: name, path: path, size: size, createdDate: createdDate, modifiedDate: modifiedDate, fileType: fileType, isHidden: isHidden, isReadOnly: isReadOnly) } } @@ -71,14 +71,14 @@ public class WebDAVFileProvider: NSObject, FileProviderBasic { assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.") } } - public var delegate: FileProviderDelegate? + public weak var delegate: FileProviderDelegate? public let credential: NSURLCredential? private var _session: NSURLSession? private var session: NSURLSession { if _session == nil { let queue = NSOperationQueue() - queue.underlyingQueue = dispatch_queue + //queue.underlyingQueue = dispatch_queue _session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: self, delegateQueue: queue) } return _session! @@ -145,6 +145,8 @@ public class WebDAVFileProvider: NSObject, FileProviderBasic { } task.resume() } + + public weak var fileOperationDelegate: FileOperationDelegate? } extension WebDAVFileProvider: FileProviderOperations { @@ -479,7 +481,7 @@ internal extension WebDAVFileProvider { let contentType = davResponse.prop["getcontenttype"] ?? "octet/stream" let isDirectory = contentType == "httpd/unix-directory" let entryTag = davResponse.prop["getetag"] - return WebDavFileObject(absoluteURL: href, name: name, size: size, contentType: contentType, createdDate: createdDate, modifiedDate: modifiedDate, fileType: isDirectory ? .Directory : .Regular, isHidden: false, isReadOnly: false, entryTag: entryTag) + return WebDavFileObject(absoluteURL: href, name: name, path: href.path ?? name, size: size, contentType: contentType, createdDate: createdDate, modifiedDate: modifiedDate, fileType: isDirectory ? .Directory : .Regular, isHidden: false, isReadOnly: false, entryTag: entryTag) } private func delegateNotify(operation: FileOperation, error: ErrorType?) {