From 1328a8e9e2ab93ed91c3eb63cb4e21cc430d501d Mon Sep 17 00:00:00 2001 From: Amir Abbas Mousavian Date: Sat, 3 Dec 2016 13:21:13 +0330 Subject: [PATCH] New FIleObject implementation, highlighted Readme --- FileProvider.podspec | 2 +- FileProvider.xcodeproj/project.pbxproj | 12 +- README.md | 250 ++++++++++++--------- Sources/DropboxHelper.swift | 57 +++-- Sources/FileProvider.swift | 127 +++++++++-- Sources/LocalFileProvider.swift | 295 ++---------------------- Sources/LocalHelper.swift | 298 +++++++++++++++++++++++++ Sources/WebDAVFileProvider.swift | 44 ++-- 8 files changed, 651 insertions(+), 434 deletions(-) create mode 100644 Sources/LocalHelper.swift diff --git a/FileProvider.podspec b/FileProvider.podspec index c6f2caa..cbf101d 100644 --- a/FileProvider.podspec +++ b/FileProvider.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = "FileProvider" - s.version = "0.7.2" + s.version = "0.8.0" s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/SMB2) files on iOS and macOS." # This description is used to generate tags and improve search results. diff --git a/FileProvider.xcodeproj/project.pbxproj b/FileProvider.xcodeproj/project.pbxproj index bc8808c..c3c0a9d 100644 --- a/FileProvider.xcodeproj/project.pbxproj +++ b/FileProvider.xcodeproj/project.pbxproj @@ -32,6 +32,9 @@ 7924B1B11D89F7DF00589DB7 /* FPSStreamTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */; }; 7924B1B21D89FCDA00589DB7 /* FPSStreamTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */; }; 7924B1B31D89FD6400589DB7 /* SMBClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396971D48C02300086753 /* SMBClient.swift */; }; + 792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; }; + 792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; }; + 792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; }; 794C21FE1D58912A00EC49B8 /* DropboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */; }; 794C21FF1D58912A00EC49B8 /* DropboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */; }; 794C22001D58912A00EC49B8 /* DropboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */; }; @@ -100,6 +103,7 @@ 7924B1911D89DAE000589DB7 /* Options.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Options.swift; sourceTree = ""; }; 7924B1921D89DAE000589DB7 /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = ""; }; 7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FPSStreamTask.swift; sourceTree = ""; }; + 792572401DF23BDA006A1526 /* LocalHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalHelper.swift; sourceTree = ""; }; 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropboxHelper.swift; sourceTree = ""; }; 794C22091D5893F800EC49B8 /* SMB2Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2Notification.swift; sourceTree = ""; }; 794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2QueryTypes.swift; sourceTree = ""; }; @@ -215,6 +219,7 @@ 799396941D48C02300086753 /* FileProvider.h */, 799396951D48C02300086753 /* FileProvider.swift */, 799396961D48C02300086753 /* LocalFileProvider.swift */, + 792572401DF23BDA006A1526 /* LocalHelper.swift */, 7902C0851D61B56D00564440 /* RemoteSession.swift */, 7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */, 799396971D48C02300086753 /* SMBClient.swift */, @@ -396,6 +401,7 @@ 799396B31D48C02300086753 /* LocalFileProvider.swift in Sources */, 799396D41D48C02300086753 /* SMB2Tree.swift in Sources */, 7924B1A21D89DAE000589DB7 /* Options.swift in Sources */, + 792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */, 7924B1991D89DAE000589DB7 /* Element.swift in Sources */, 799396C81D48C02300086753 /* SMB2IOCtl.swift in Sources */, 799396D71D48C02300086753 /* SMB2Types.swift in Sources */, @@ -430,6 +436,7 @@ 799396B41D48C02300086753 /* LocalFileProvider.swift in Sources */, 799396D51D48C02300086753 /* SMB2Tree.swift in Sources */, 7924B1A31D89DAE000589DB7 /* Options.swift in Sources */, + 792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */, 7924B1B01D89F7DE00589DB7 /* FPSStreamTask.swift in Sources */, 7924B19A1D89DAE000589DB7 /* Element.swift in Sources */, 799396C91D48C02300086753 /* SMB2IOCtl.swift in Sources */, @@ -464,6 +471,7 @@ 799396B51D48C02300086753 /* LocalFileProvider.swift in Sources */, 799396D61D48C02300086753 /* SMB2Tree.swift in Sources */, 7924B1A41D89DAE000589DB7 /* Options.swift in Sources */, + 792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */, 7924B1B11D89F7DF00589DB7 /* FPSStreamTask.swift in Sources */, 7924B19B1D89DAE000589DB7 /* Element.swift in Sources */, 799396CA1D48C02300086753 /* SMB2IOCtl.swift in Sources */, @@ -557,7 +565,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; APPLICATION_EXTENSION_API_ONLY = YES; - BUNDLE_VERSION_STRING = 0.7.2; + BUNDLE_VERSION_STRING = 0.8.0; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -610,7 +618,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; APPLICATION_EXTENSION_API_ONLY = YES; - BUNDLE_VERSION_STRING = 0.7.2; + BUNDLE_VERSION_STRING = 0.8.0; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; diff --git a/README.md b/README.md index 84a33f4..762cd77 100644 --- a/README.md +++ b/README.md @@ -47,23 +47,39 @@ FileProvider supports both CocoaPods. Add this line to your pods file: - pod "FileProvider" +```ruby +pod "FileProvider" +``` + +Or add this to cartfile: + +``` +github "amosavian/FileProvider" +``` ### Git To have latest updates with ease, use this command on terminal to get a clone: - git clone https://github.com/amosavian/FileProvider FileProvider - +```bash +git clone https://github.com/amosavian/FileProvider +``` + You can update your library using this command in FileProvider folder: - git pull +```bash +git pull +``` if you have a git based project, use this command in your projects directory to add this project as a submodule to your project: - git submodule add https://github.com/amosavian/FileProvider FileProvider +```bash +git submodule add https://github.com/amosavian/FileProvider +``` ### Manually -Copy Source folder to your project and Voila! +**First way:** Copy Source folder to your project and Voila! + +**Second way:** Drop FileProvider.xcodeproj to you Xcode workspace and add the framework to your Embeded Binaries in target. ## Usage @@ -73,20 +89,26 @@ Each provider has a specific class which conforms to FileProvider protocol and s For LocalFileProvider if you want to deal with `Documents` folder - let documentsProvider = LocalFileProvider() +``` swift +let documentsProvider = LocalFileProvider() +``` is equal to: - - let documentPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true); - let documentsURL = URL(fileURLWithPath: documentPath); - let documentsProvider = LocalFileProvider(baseURL: documentsURL) + +``` swift +let documentPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true); +let documentsURL = URL(fileURLWithPath: documentPath); +let documentsProvider = LocalFileProvider(baseURL: documentsURL) +``` You can't change the base url later. and all paths are related to this base url by default. For remote file providers authentication may be necessary: - let credential = URLCredential(user: "user", password: "pass", persistence: .permanent) - let webdavProvider = WebDAVFileProvider(baseURL: URL(string: "http://www.example.com/dav")!, credential: credential) +``` swift +let credential = URLCredential(user: "user", password: "pass", persistence: .permanent) +let webdavProvider = WebDAVFileProvider(baseURL: URL(string: "http://www.example.com/dav")!, credential: credential) +``` * In case you want to connect non-secure servers for WebDAV (http) in iOS 9+ / macOS 10.11+ you should disable App Transport Security (ATS) according to [this guide.](https://gist.github.com/mlynch/284699d676fe9ed0abfa) @@ -104,40 +126,42 @@ It's simply three method which indicated whether the operation failed, succeed a Your class should conforms `FileProviderDelegate` class: - override func viewDidLoad() { - documentsProvider.delegate = self as FileProviderDelegate - } +```swift +override func viewDidLoad() { + documentsProvider.delegate = self as FileProviderDelegate +} - func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperation) { - switch operation { - case .copy(source: let source, destination: let dest): - print("\(source) copied to \(dest).") - case .remove(path: let path): - print("\(path) has been deleted.") - default: - print("\(operation.actionDescription) from \(operation.source ?? "") to \(operation.destination) succeed") - } +func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperation) { + switch operation { + case .copy(source: let source, destination: let dest): + print("\(source) copied to \(dest).") + case .remove(path: let path): + print("\(path) has been deleted.") + default: + print("\(operation.actionDescription) from \(operation.source ?? "") to \(operation.destination) succeed") } - - func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperation) { - switch operation { - case .copy(source: let source, destination: let dest): - print("copy of \(source) failed.") - case .remove(path: let path): - print("\(path) can't be deleted.") - default: - print("\(operation.actionDescription) from \(operation.source ?? "") to \(operation.destination) failed") - } - } - - func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperation, progress: Float) { - switch operation { - case .copy(source: let source, destination: let dest): - print("Copy\(source) to \(dest): \(progress * 100) completed.") - default: - break - } +} + +func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperation) { + switch operation { + case .copy(source: let source, destination: let dest): + print("copy of \(source) failed.") + case .remove: + print("file can't be deleted.") + default: + print("\(operation.actionDescription) from \(operation.source ?? "") to \(operation.destination) failed") } +} + +func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperation, progress: Float) { + switch operation { + case .copy(source: let source, destination: let dest): + print("Copy\(source) to \(dest): \(progress * 100) completed.") + default: + break + } +} +``` **Note:** `fileproviderProgress()` delegate method is not called by `LocalFileProvider` currently. @@ -159,42 +183,50 @@ There is a `FileObject` class which holds file attributes like size and creation For a single file: - documentsProvider.attributesOfItem(path: "/file.txt", completionHandler: { - (attributes: LocalFileObject?, error: ErrorType?) -> Void in - if let attributes = attributes { - print("File Size: \(attributes.size)") - print("Creation Date: \(attributes.createdDate)") - print("Modification Date: \(attributes.modifiedDate)") - print("Is Read Only: \(attributes.isReadOnly)") - } - }) +```swift +documentsProvider.attributesOfItem(path: "/file.txt", completionHandler: { + attributes, error in + if let attributes = attributes { + print("File Size: \(attributes.size)") + print("Creation Date: \(attributes.creationDate)") + print("Modification Date: \(attributes.modifiedDate)") + print("Is Read Only: \(attributes.isReadOnly)") + } +}) +``` To get list of files in a directory: - documentsProvider.contentsOfDirectory(path: "/", completionHandler: { - (contents: [LocalFileObject], error: ErrorType?) -> Void in - for file in contents { - print("Name: \(attributes.name)") - print("Size: \(attributes.size)") - print("Creation Date: \(attributes.createdDate)") - print("Modification Date: \(attributes.modifiedDate)") - } - }) +```swift +documentsProvider.contentsOfDirectory(path: "/", completionHandler: { + contents, error in + for file in contents { + print("Name: \(attributes.name)") + print("Size: \(attributes.size)") + print("Creation Date: \(attributes.creationDate)") + print("Modification Date: \(attributes.modifiedDate)") + } +}) +``` To get size of strage and used/free space: - func storageProperties(completionHandler: {(total: Int64, used: Int64) -> Void in - print("Total Storage Space: \(total)") - print("Used Space: \(used)") - print("Free Space: \(total - used)") - }) +```swift +func storageProperties(completionHandler: { total, used in + print("Total Storage Space: \(total)") + print("Used Space: \(used)") + print("Free Space: \(total - used)") +}) +``` * if this function is unavailable on provider or an error has been occurred, total space will be reported `-1` and used space `0` ### Change current directory - documentsProvider.currentPath = "/New Folder" - // now path is ~/Documents/New Folder +```swift +documentsProvider.currentPath = "/New Folder" +// now path is ~/Documents/New Folder +``` You can then pass "" (empty string) to `contentsOfDirectory` method to list files in current directory. @@ -202,56 +234,71 @@ You can then pass "" (empty string) to `contentsOfDirectory` method to list file Creating new directory: - documentsProvider.create(folder: "new folder", at: "/", completionHandler: nil) +```swift +documentsProvider.create(folder: "new folder", at: "/", completionHandler: nil) +``` -Creating new file from data stream: +Creating new file from data: - let data = "hello world!".data(encoding: String.encoding.utf8) - let file = FileObject(name: "old.txt", createdDate: Date(), modifiedDate: Date(), isHidden: false, isReadOnly: true) - documentsProvider.create(file: file, at: "/", contents: data, completionHandler: nil) +```swift +let data = "hello world!".data(encoding: .utf8) +documentsProvider.create(file: "newFile.txt", at: "/", contents: data, completionHandler: nil) +``` ### Copy and Move/Rename Files Copy file old.txt to new.txt in current path: - documentsProvider.copyItem(path: "new folder/old.txt", to: "new.txt", overwrite: false, completionHandler: nil) +```swift +documentsProvider.copyItem(path: "new folder/old.txt", to: "new.txt", overwrite: false, completionHandler: nil) +``` Move file old.txt to new.txt in current path: - documentsProvider.moveItem(path: "new folder/old.txt", to: "new.txt", overwrite: false, completionHandler: nil) +```swift +documentsProvider.moveItem(path: "new folder/old.txt", to: "new.txt", overwrite: false, completionHandler: nil) +``` -**Note:** To have a consistent behaviour, create intermediate directories first if necessary. +**Note:** To have a consistent behavior, create intermediate directories first if necessary. ### Delete Files - documentsProvider.removeItem(path: "new.txt", completionHandler: nil) +```swift +documentsProvider.removeItem(path: "new.txt", completionHandler: nil) +``` -***Caution:*** This method will delete directories with all it's content recursively. +***Caution:*** This method will delete directories with all it's contents recursively. -### Retrieve Content of File +### Fetching Contents of File There is two method for this purpose, one of them loads entire file into NSData and another can load a portion of file. - documentsProvider.contents(path: "old.txt", completionHandler: { - (contents: Data?, error: ErrorType?) -> Void - if let contents = contents { - print(String(data: contents, encoding: String.encoding.utf8)) // "hello world!" - } - }) +```swift +documentsProvider.contents(path: "old.txt", completionHandler: { + contents, error in + if let contents = contents { + print(String(data: contents, encoding: .utf8)) // "hello world!" + } +}) +``` If you want to retrieve a portion of file you can use `contents` method with offset and length arguments. Please note first byte of file has offset: 0. - documentsProvider.contents(path: "old.txt", offset: 2, length: 5, completionHandler: { - (contents: Data?, error: ErrorType?) -> Void - if let contents = contents { - print(String(data: contents, encoding: String.encoding.utf8)) // "llo w" - } - }) +```swift +documentsProvider.contents(path: "old.txt", offset: 2, length: 5, completionHandler: { + contents, error in + if let contents = contents { + print(String(data: contents, encoding: .utf8)) // "llo w" + } +}) +``` ### Write Data To Files - let data = "What's up Newyork!".data(encoding: String.encoding.utf8) - documentsProvider.writeContents(path: "old.txt", content: data, atomically: true, completionHandler: nil) +```swift +let data = "What's up Newyork!".data(encoding: .utf8) +documentsProvider.writeContents(path: "old.txt", content: data, atomically: true, completionHandler: nil) +``` ### Operation Handle @@ -263,13 +310,16 @@ It's not supported by native `(NS)FileManager` so `LocalFileProvider`, but this You can monitor updates in some file system (Local and SMB2), there is three methods in supporting provider you can use to register a handler, to unregister and to check whether it's being monitored or not. It's useful to find out when new files added or removed from directory and update user interface. The handler will be dispatched to main threads to avoid UI bugs with a 0.25 sec delay. - documentsProvider.registerNotifcation(path: provider.currentPath) - { - // calling functions to update UI - } +```swift +// to register a new notification handler +documentsProvider.registerNotifcation(path: provider.currentPath) +{ + // calling functions to update UI +} - // To discontinue monitoring folders: - documentsProvider.unregisterNotifcation(path: provider.currentPath) +// To discontinue monitoring folders: +documentsProvider.unregisterNotifcation(path: provider.currentPath) +``` * **Please note** in LocalFileProvider it will also monitor changes in subfolders. This behaviour can varies according to file system specification. diff --git a/Sources/DropboxHelper.swift b/Sources/DropboxHelper.swift index 6e2cf11..11c1bc8 100644 --- a/Sources/DropboxHelper.swift +++ b/Sources/DropboxHelper.swift @@ -19,18 +19,36 @@ public struct FileProviderDropboxError: Error, CustomStringConvertible { } public final class DropboxFileObject: FileObject { - public let serverTime: Date? - public let id: String? - public let rev: String? - - // codebeat:disable[ARITY] - public init(name: String, path: String, size: Int64 = -1, serverTime: Date? = nil, modifiedDate: Date? = nil, fileType: FileType = .regular, isHidden: Bool = false, isReadOnly: Bool = false, id: String? = nil, rev: String? = nil) { - self.serverTime = serverTime - self.id = id - self.rev = rev - super.init(absoluteURL: URL(string: path), name: name, path: path, size: size, createdDate: nil, modifiedDate: modifiedDate, fileType: fileType, isHidden: isHidden, isReadOnly: isReadOnly) + public init(name: String, path: String) { + super.init(absoluteURL: URL(string: path), name: name, path: path) + } + + open internal(set) var serverTime: Date? { + get { + return allValues["NSURLServerDateKey"] as? Date + } + set { + allValues["NSURLServerDateKey"] = newValue + } + } + + open internal(set) var id: String? { + get { + return allValues["NSURLDropboxDocumentIdentifyKey"] as? String + } + set { + allValues["NSURLDropboxDocumentIdentifyKey"] = newValue + } + } + + open internal(set) var rev: String? { + get { + return allValues[URLResourceKey.generationIdentifierKey.rawValue] as? String + } + set { + allValues[URLResourceKey.generationIdentifierKey.rawValue] = newValue + } } - // codebeat:enable[ARITY] } // codebeat:disable[ARITY] @@ -158,14 +176,15 @@ internal extension DropboxFileProvider { func mapToFileObject(_ json: [String: AnyObject]) -> DropboxFileObject? { guard let name = json["name"] as? String else { return nil } guard let path = json["path_display"] as? String else { return nil } - let size = (json["size"] as? NSNumber)?.int64Value ?? -1 - let serverTime = resolve(dateString: json["server_modified"] as? String ?? "") - let modifiedDate = resolve(dateString: json["client_modified"] as? String ?? "") - let isDirectory = (json[".tag"] as? String) == "folder" - let isReadonly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false - let id = json["id"] as? String - let rev = json["id"] as? String - return DropboxFileObject(name: name, path: path, size: size, serverTime: serverTime, modifiedDate: modifiedDate, fileType: isDirectory ? .directory : .regular, isReadOnly: isReadonly, id: id, rev: rev) + let fileObject = DropboxFileObject(name: name, path: path) + fileObject.size = (json["size"] as? NSNumber)?.int64Value ?? -1 + fileObject.serverTime = resolve(dateString: json["server_modified"] as? String ?? "") + fileObject.modifiedDate = resolve(dateString: json["client_modified"] as? String ?? "") + fileObject.fileType = (json[".tag"] as? String) == "folder" ? .directory : .regular + fileObject.isReadOnly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false + fileObject.id = json["id"] as? String + fileObject.rev = json["id"] as? String + return fileObject } func delegateNotify(_ operation: FileOperationType, error: Error?) { diff --git a/Sources/FileProvider.swift b/Sources/FileProvider.swift index 0ef1868..372d7c1 100644 --- a/Sources/FileProvider.swift +++ b/Sources/FileProvider.swift @@ -51,6 +51,19 @@ public enum FileType: String { default: self = .unknown } } + + var resourceType: URLFileResourceType { + switch self { + case .namedPipe: return .namedPipe + case .characterSpecial: return .characterSpecial + case .directory: return .directory + case .blockSpecial: return .blockSpecial + case .regular: return .regular + case .symbolicLink: return .symbolicLink + case .socket: return .socket + case .unknown: return .unknown + } + } } public protocol FoundationErrorEnum { @@ -62,32 +75,112 @@ extension URLError.Code: FoundationErrorEnum {} extension CocoaError.Code: FoundationErrorEnum {} open class FileObject { - open let absoluteURL: URL? - open let name: String - open let path: String - open let size: Int64 - open let createdDate: Date? - open let modifiedDate: Date? - open let fileType: FileType - open let isHidden: Bool - open let isReadOnly: Bool + open internal(set) var allValues: [String: Any] - public init(absoluteURL: URL? = nil, name: String, path: String, size: Int64 = -1, createdDate: Date? = nil, modifiedDate: Date? = nil, fileType: FileType = .regular, isHidden: Bool = false, isReadOnly: Bool = false) { + internal init(allValues: [String: Any]) { + self.allValues = allValues + } + + internal init(absoluteURL: URL? = nil, name: String, path: String) { + self.allValues = [String: Any]() self.absoluteURL = absoluteURL self.name = name self.path = path - self.size = size - self.createdDate = createdDate - self.modifiedDate = modifiedDate - self.fileType = fileType - self.isHidden = isHidden - self.isReadOnly = isReadOnly + } + + open internal(set) var absoluteURL: URL? { + get { + return allValues["NSURLAbsoluteURLKey"] as? URL + } + set { + allValues["NSURLAbsoluteURLKey"] = newValue + } + } + + open internal(set) var name: String { + get { + return allValues[URLResourceKey.nameKey.rawValue] as! String + } + set { + allValues[URLResourceKey.nameKey.rawValue] = newValue + } + } + + open internal(set) var path: String { + get { + return allValues[URLResourceKey.pathKey.rawValue] as! String + } + set { + allValues[URLResourceKey.pathKey.rawValue] = newValue + } + } + + open internal(set) var size: Int64 { + get { + return allValues[URLResourceKey.fileSizeKey.rawValue] as? Int64 ?? -1 + } + set { + allValues[URLResourceKey.fileSizeKey.rawValue] = Int(exactly: newValue) ?? Int.max + } + } + + open internal(set) var creationDate: Date? { + get { + return allValues[URLResourceKey.creationDateKey.rawValue] as? Date + } + set { + allValues[URLResourceKey.creationDateKey.rawValue] = newValue + } + } + + open internal(set) var modifiedDate: Date? { + get { + return allValues[URLResourceKey.contentModificationDateKey.rawValue] as? Date + } + set { + allValues[URLResourceKey.contentModificationDateKey.rawValue] = newValue + } + } + + open internal(set) var fileType: FileType? { + get { + guard let typeString = allValues[URLResourceKey.fileResourceTypeKey.rawValue] as? String else { + return nil + } + let type = URLFileResourceType(rawValue: typeString) + return FileType(urlResourceTypeValue: type) + } + set { + allValues[URLResourceKey.fileResourceTypeKey.rawValue] = newValue?.resourceType.rawValue ?? FileType.unknown.resourceType.rawValue + } + } + + open internal(set) var isHidden: Bool { + get { + return allValues[URLResourceKey.isHiddenKey.rawValue] as? Bool ?? false + } + set { + allValues[URLResourceKey.isHiddenKey.rawValue] = newValue + } + } + + open internal(set) var isReadOnly: Bool { + get { + return !(allValues[URLResourceKey.isWritableKey.rawValue] as? Bool ?? true) + } + set { + allValues[URLResourceKey.isWritableKey.rawValue] = !newValue + } } open var isDirectory: Bool { return self.fileType == .directory } + open var isRegularFile: Bool { + return self.fileType == .regular + } + open var isSymLink: Bool { return self.fileType == .symbolicLink } @@ -247,7 +340,7 @@ extension FileProviderBasic { guard let path = path else { return nil } var p = path.hasPrefix("/") ? path : "/" + path if p.hasSuffix("/") { - p.remove(at: p.characters.index(before: p.endIndex)) + p.remove(at: p.endIndex) } return p } diff --git a/Sources/LocalFileProvider.swift b/Sources/LocalFileProvider.swift index 1366177..496fe4c 100644 --- a/Sources/LocalFileProvider.swift +++ b/Sources/LocalFileProvider.swift @@ -8,20 +8,10 @@ import Foundation -public final class LocalFileObject: FileObject { - public let allocatedSize: Int64 - // codebeat:disable[ARITY] - public init(absoluteURL: URL, name: String, path: String, size: Int64 = -1, allocatedSize: Int64 = 0, createdDate: Date? = nil, modifiedDate: Date? = nil, fileType: FileType = .regular, isHidden: Bool = false, isReadOnly: Bool = false) { - self.allocatedSize = allocatedSize - super.init(absoluteURL: absoluteURL, name: name, path: path, size: size, createdDate: createdDate, modifiedDate: modifiedDate, fileType: fileType, isHidden: isHidden, isReadOnly: isReadOnly) - } - // codebeat:enable[ARITY] -} - open class LocalFileProvider: FileProvider, FileProviderMonitor { open static let type = "Local" open var isPathRelative: Bool = true - open var baseURL: URL? = LocalFileProvider.defaultBaseURL() + open private(set) var baseURL: URL? = LocalFileProvider.defaultBaseURL() open var currentPath: String = "" open var dispatch_queue: DispatchQueue open var operation_queue: DispatchQueue @@ -47,17 +37,18 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { opFileManager.delegate = fileProviderManagerDelegate } - fileprivate static func defaultBaseURL() -> URL { - let paths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true); + open static func defaultBaseURL() -> URL { + let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true); return URL(fileURLWithPath: paths[0]) } open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) { dispatch_queue.async { do { - let contents = try self.fileManager.contentsOfDirectory(at: self.absoluteURL(path), includingPropertiesForKeys: [URLResourceKey.nameKey, URLResourceKey.fileSizeKey, URLResourceKey.fileAllocatedSizeKey, URLResourceKey.creationDateKey, URLResourceKey.contentModificationDateKey, URLResourceKey.isHiddenKey, URLResourceKey.volumeIsReadOnlyKey], options: FileManager.DirectoryEnumerationOptions.skipsSubdirectoryDescendants) - let filesAttributes = contents.map({ (fileURL) -> LocalFileObject in - return self.attributesOfItem(url: fileURL) + let contents = try self.fileManager.contentsOfDirectory(at: self.absoluteURL(path), includingPropertiesForKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .isHiddenKey, .volumeIsReadOnlyKey], options: .skipsSubdirectoryDescendants) + let filesAttributes = contents.flatMap({ (fileURL) -> LocalFileObject? in + let path = self.relativePathOf(url: fileURL) + return LocalFileObject(fileWithPath: path, relativeTo: self.baseURL) }) completionHandler(filesAttributes, nil) } catch let e as NSError { @@ -66,28 +57,16 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { } } - internal func attributesOfItem(url fileURL: URL) -> LocalFileObject { - let values = try? fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .volumeIsReadOnlyKey]) - let path: String - if isPathRelative { - path = self.relativePathOf(url: fileURL) - } else { - path = fileURL.path - } - let fileAttr = LocalFileObject(absoluteURL: fileURL, name: values?.name ?? fileURL.lastPathComponent, path: path, size: Int64(values?.fileSize ?? -1), allocatedSize: Int64(values?.fileAllocatedSize ?? -1), createdDate: values?.creationDate, modifiedDate: values?.contentModificationDate, fileType: FileType(urlResourceTypeValue: values?.fileResourceType ?? .unknown), isHidden: values?.isHidden ?? false, isReadOnly: values?.isWritable ?? false) - return fileAttr - } - open func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) { let dict = (try? FileManager.default.attributesOfFileSystem(forPath: baseURL?.path ?? "/")) - let totalSize = (dict?[FileAttributeKey.systemSize] as? NSNumber)?.int64Value ?? -1; - let freeSize = (dict?[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value ?? 0; + let totalSize = (dict?[.systemSize] as? NSNumber)?.int64Value ?? -1; + let freeSize = (dict?[.systemFreeSize] as? NSNumber)?.int64Value ?? 0; completionHandler(totalSize, totalSize - freeSize) } open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { dispatch_queue.async { - completionHandler(self.attributesOfItem(url: self.absoluteURL(path)), nil) + completionHandler(LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), nil) } } @@ -256,7 +235,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { let opType = FileOperationType.fetch(path: path) dispatch_queue.async { let aPath = self.absoluteURL(path).path - guard !self.attributesOfItem(url: self.absoluteURL(path)).isDirectory && self.fileManager.fileExists(atPath: aPath) else { + guard self.fileManager.fileExists(atPath: aPath) && !self.absoluteURL(path).fileIsDirectory else { completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum)) return } @@ -296,9 +275,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { var result = [LocalFileObject]() while let fileURL = iterator?.nextObject() as? URL { if fileURL.lastPathComponent.lowercased().contains(query.lowercased()) { - let fileObject = self.attributesOfItem(url: fileURL) - result.append(self.attributesOfItem(url: fileURL)) - foundItemHandler?(fileObject) + let path = self.relativePathOf(url: fileURL) + if let fileObject = LocalFileObject(fileWithPath: path, relativeTo: self.baseURL) { + result.append(fileObject) + foundItemHandler?(fileObject) + } } } completionHandler(result, nil) @@ -364,247 +345,3 @@ public extension LocalFileProvider { } } } - -internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate { - weak var provider: LocalFileProvider? - - init(provider: LocalFileProvider) { - self.provider = provider - } - - func fileManager(_ fileManager: FileManager, shouldCopyItemAt srcURL: URL, to dstURL: URL) -> Bool { - guard let provider = self.provider, let 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: FileManager, shouldMoveItemAt srcURL: URL, to dstURL: URL) -> Bool { - guard let provider = self.provider, let 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: FileManager, shouldRemoveItemAt URL: URL) -> Bool { - guard let provider = self.provider, let delegate = provider.fileOperationDelegate else { - return true - } - let path = provider.relativePathOf(url: URL) - return delegate.fileProvider(provider, shouldDoOperation: .remove(path: path)) - } - - func fileManager(_ fileManager: FileManager, shouldLinkItemAt srcURL: URL, to dstURL: URL) -> Bool { - guard let provider = self.provider, let 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: FileManager, shouldProceedAfterError error: Error, copyingItemAt srcURL: URL, to dstURL: URL) -> Bool { - guard let provider = self.provider, let 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: FileManager, shouldProceedAfterError error: Error, movingItemAt srcURL: URL, to dstURL: URL) -> Bool { - guard let provider = self.provider, let 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: FileManager, shouldProceedAfterError error: Error, removingItemAt URL: URL) -> Bool { - guard let provider = self.provider, let 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: FileManager, shouldProceedAfterError error: Error, linkingItemAt srcURL: URL, to dstURL: URL) -> Bool { - guard let provider = self.provider, let 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 { - fileprivate let source: DispatchSourceFileSystemObject - fileprivate let descriptor: CInt - fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.default) - fileprivate var state: Bool = false - fileprivate var monitoredTime: TimeInterval = Date().timeIntervalSinceReferenceDate - var url: URL - - /// Creates a folder monitor object with monitoring enabled. - init(url: URL, handler: @escaping ()->Void) { - self.url = url - descriptor = open((url as NSURL).fileSystemRepresentation, O_EVTONLY) - source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: DispatchSource.FileSystemEvent.write, 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 - // affect user experince much - let main_handler: ()->Void = { - if Date().timeIntervalSinceReferenceDate < self.monitoredTime + 0.2 { - return - } - self.monitoredTime = Date().timeIntervalSinceReferenceDate - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.25, execute: { - handler() - }) - } - source.setEventHandler(handler: main_handler) - source.setCancelHandler { - close(self.descriptor) - } - start() - } - - /// Starts sending notifications if currently stopped - func start() { - if !state { - state = true - source.resume() - } - } - - /// Stops sending notifications if currently enabled - func stop() { - if state { - state = false - source.suspend() - } - } - - deinit { - source.cancel() - } -} - -open class LocalOperationHandle: OperationHandle { - public let baseURL: URL - public let operationType: FileOperationType - - init (operationType: FileOperationType, baseURL: URL?) { - self.baseURL = baseURL ?? LocalFileProvider.defaultBaseURL() - self.operationType = operationType - } - - private var sourceURL: URL? { - guard let source = operationType.source else { return nil } - return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source) - } - - private var destURL: URL? { - guard let dest = operationType.destination else { return nil } - return dest.hasPrefix("file://") ? URL(fileURLWithPath: dest) : baseURL.appendingPathComponent(dest) - } - - /// Caution: may put pressure on CPU, may have latency - open var bytesSoFar: Int64 { - assert(!Thread.isMainThread, "Don't run \(#function) method on main thread") - switch operationType { - case .modify: - guard let url = sourceURL, url.isFileURL else { return 0 } - if url.fileIsDirectory { - return iterateDirectory(url, deep: true).totalsize - } else { - return url.fileSize - } - case .copy, .move: - guard let url = destURL, url.isFileURL else { return 0 } - if url.fileIsDirectory { - return iterateDirectory(url, deep: true).totalsize - } else { - return url.fileSize - } - default: - return 0 - } - - } - - /// Caution: may put pressure on CPU, may have latency - open var totalBytes: Int64 { - assert(!Thread.isMainThread, "Don't run \(#function) method on main thread") - switch operationType { - case .copy, .move: - guard let url = sourceURL, url.isFileURL else { return 0 } - if url.fileIsDirectory { - return iterateDirectory(url, deep: true).totalsize - } else { - return url.fileSize - } - default: - return 0 - } - } - - /// Not usable in local provider - open var inProgress: Bool { - return false - } - - /// Not usable in local provider - open func cancel() -> Bool{ - return false - } - - func iterateDirectory(_ pathURL: URL, deep: Bool) -> (folders: Int, files: Int, totalsize: Int64) { - var folders = 0 - var files = 0 - var totalsize: Int64 = 0 - let keys: [URLResourceKey] = [.isDirectoryKey, .fileSizeKey] - let enumOpt: FileManager.DirectoryEnumerationOptions = !deep ? [.skipsSubdirectoryDescendants, .skipsPackageDescendants] : [] - - let fp = FileManager() - let filesList = fp.enumerator(at: pathURL, includingPropertiesForKeys: keys, options: enumOpt, errorHandler: nil) - while let fileURL = filesList?.nextObject() as? URL { - do { - let values = try fileURL.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey]) - let isdir = values.isDirectory ?? false - let size = Int64(values.fileSize ?? 0) - if isdir { - folders += 1 - } else { - files += 1 - } - totalsize += size - } catch _ { - } - } - - return (folders, files, totalsize) - - } -} - -internal extension URL { - var fileIsDirectory: Bool { - return (try? self.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false - } - - var fileSize: Int64 { - return Int64((try? self.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1) - } - - var fileExists: Bool { - return self.isFileURL && FileManager.default.fileExists(atPath: self.path) - } -} diff --git a/Sources/LocalHelper.swift b/Sources/LocalHelper.swift new file mode 100644 index 0000000..657f834 --- /dev/null +++ b/Sources/LocalHelper.swift @@ -0,0 +1,298 @@ +// +// LocalFileProvider.swift +// FileProvider +// +// Created by Amir Abbas Mousavian. +// Copyright © 2016 Mousavian. Distributed under MIT license. +// + +import Foundation + +public final class LocalFileObject: FileObject { + internal init(absoluteURL: URL, name: String, path: String) { + super.init(absoluteURL: absoluteURL, name: name, path: path) + } + + public convenience init? (fileWithPath path: String, relativeTo relativeURL: URL?) { + let fileURL: URL + if let relativeURL = relativeURL { + fileURL = relativeURL.appendingPathComponent(path) + } else { + fileURL = URL(fileURLWithPath: path) + } + do { + let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey]) + self.init(absoluteURL: fileURL, name: values.name ?? fileURL.lastPathComponent, path: path) + for (key, value) in values.allValues { + self.allValues[key.rawValue] = value + } + } catch { + return nil + } + } + + public convenience init?(fileWithURL fileURL: URL) { + do { + let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey]) + self.init(absoluteURL: fileURL, name: values.name ?? fileURL.lastPathComponent, path: fileURL.path) + for (key, value) in values.allValues { + self.allValues[key.rawValue] = value + } + } catch { + return nil + } + } + + open internal(set) var allocatedSize: Int64 { + get { + return allValues[URLResourceKey.fileAllocatedSizeKey.rawValue] as? Int64 ?? 0 + } + set { + allValues[URLResourceKey.fileAllocatedSizeKey.rawValue] = Int(exactly: newValue) ?? Int.max + } + } +} + +internal class LocalFolderMonitor { + fileprivate let source: DispatchSourceFileSystemObject + fileprivate let descriptor: CInt + fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.default) + fileprivate var state: Bool = false + fileprivate var monitoredTime: TimeInterval = Date().timeIntervalSinceReferenceDate + var url: URL + + /// Creates a folder monitor object with monitoring enabled. + init(url: URL, handler: @escaping ()->Void) { + self.url = url + descriptor = open((url as NSURL).fileSystemRepresentation, O_EVTONLY) + source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: DispatchSource.FileSystemEvent.write, 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 + // affect user experince much + let main_handler: ()->Void = { + if Date().timeIntervalSinceReferenceDate < self.monitoredTime + 0.2 { + return + } + self.monitoredTime = Date().timeIntervalSinceReferenceDate + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.25, execute: { + handler() + }) + } + source.setEventHandler(handler: main_handler) + source.setCancelHandler { + close(self.descriptor) + } + start() + } + + /// Starts sending notifications if currently stopped + func start() { + if !state { + state = true + source.resume() + } + } + + /// Stops sending notifications if currently enabled + func stop() { + if state { + state = false + source.suspend() + } + } + + deinit { + source.cancel() + } +} + +internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate { + weak var provider: LocalFileProvider? + + init(provider: LocalFileProvider) { + self.provider = provider + } + + func fileManager(_ fileManager: FileManager, shouldCopyItemAt srcURL: URL, to dstURL: URL) -> Bool { + guard let provider = self.provider, let 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: FileManager, shouldMoveItemAt srcURL: URL, to dstURL: URL) -> Bool { + guard let provider = self.provider, let 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: FileManager, shouldRemoveItemAt URL: URL) -> Bool { + guard let provider = self.provider, let delegate = provider.fileOperationDelegate else { + return true + } + let path = provider.relativePathOf(url: URL) + return delegate.fileProvider(provider, shouldDoOperation: .remove(path: path)) + } + + func fileManager(_ fileManager: FileManager, shouldLinkItemAt srcURL: URL, to dstURL: URL) -> Bool { + guard let provider = self.provider, let 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: FileManager, shouldProceedAfterError error: Error, copyingItemAt srcURL: URL, to dstURL: URL) -> Bool { + guard let provider = self.provider, let 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: FileManager, shouldProceedAfterError error: Error, movingItemAt srcURL: URL, to dstURL: URL) -> Bool { + guard let provider = self.provider, let 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: FileManager, shouldProceedAfterError error: Error, removingItemAt URL: URL) -> Bool { + guard let provider = self.provider, let 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: FileManager, shouldProceedAfterError error: Error, linkingItemAt srcURL: URL, to dstURL: URL) -> Bool { + guard let provider = self.provider, let 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)) + } +} + +open class LocalOperationHandle: OperationHandle { + public let baseURL: URL + public let operationType: FileOperationType + + init (operationType: FileOperationType, baseURL: URL?) { + self.baseURL = baseURL ?? LocalFileProvider.defaultBaseURL() + self.operationType = operationType + } + + private var sourceURL: URL? { + guard let source = operationType.source else { return nil } + return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source) + } + + private var destURL: URL? { + guard let dest = operationType.destination else { return nil } + return dest.hasPrefix("file://") ? URL(fileURLWithPath: dest) : baseURL.appendingPathComponent(dest) + } + + /// Caution: may put pressure on CPU, may have latency + open var bytesSoFar: Int64 { + assert(!Thread.isMainThread, "Don't run \(#function) method on main thread") + switch operationType { + case .modify: + guard let url = sourceURL, url.isFileURL else { return 0 } + if url.fileIsDirectory { + return iterateDirectory(url, deep: true).totalsize + } else { + return url.fileSize + } + case .copy, .move: + guard let url = destURL, url.isFileURL else { return 0 } + if url.fileIsDirectory { + return iterateDirectory(url, deep: true).totalsize + } else { + return url.fileSize + } + default: + return 0 + } + + } + + /// Caution: may put pressure on CPU, may have latency + open var totalBytes: Int64 { + assert(!Thread.isMainThread, "Don't run \(#function) method on main thread") + switch operationType { + case .copy, .move: + guard let url = sourceURL, url.isFileURL else { return 0 } + if url.fileIsDirectory { + return iterateDirectory(url, deep: true).totalsize + } else { + return url.fileSize + } + default: + return 0 + } + } + + /// Not usable in local provider + open var inProgress: Bool { + return false + } + + /// Not usable in local provider + open func cancel() -> Bool{ + return false + } + + func iterateDirectory(_ pathURL: URL, deep: Bool) -> (folders: Int, files: Int, totalsize: Int64) { + var folders = 0 + var files = 0 + var totalsize: Int64 = 0 + let keys: [URLResourceKey] = [.isDirectoryKey, .fileSizeKey] + let enumOpt: FileManager.DirectoryEnumerationOptions = !deep ? [.skipsSubdirectoryDescendants, .skipsPackageDescendants] : [] + + let fp = FileManager() + let filesList = fp.enumerator(at: pathURL, includingPropertiesForKeys: keys, options: enumOpt, errorHandler: nil) + while let fileURL = filesList?.nextObject() as? URL { + do { + let values = try fileURL.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey]) + let isdir = values.isDirectory ?? false + let size = Int64(values.fileSize ?? 0) + if isdir { + folders += 1 + } else { + files += 1 + } + totalsize += size + } catch _ { + } + } + + return (folders, files, totalsize) + + } +} + +internal extension URL { + var fileIsDirectory: Bool { + return (try? self.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false + } + + var fileSize: Int64 { + return Int64((try? self.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1) + } + + var fileExists: Bool { + return self.isFileURL && FileManager.default.fileExists(atPath: self.path) + } +} diff --git a/Sources/WebDAVFileProvider.swift b/Sources/WebDAVFileProvider.swift index 282f175..634e7d8 100644 --- a/Sources/WebDAVFileProvider.swift +++ b/Sources/WebDAVFileProvider.swift @@ -9,16 +9,27 @@ import Foundation public final class WebDavFileObject: FileObject { - public let contentType: String - public let entryTag: String? - - // codebeat:disable[ARITY] - public init(absoluteURL: URL, name: String, path: String, size: Int64 = -1, contentType: String = "", createdDate: Date? = nil, modifiedDate: Date? = nil, fileType: FileType = .regular, isHidden: Bool = false, isReadOnly: Bool = false, entryTag: String? = nil) { - self.contentType = contentType - self.entryTag = entryTag - super.init(absoluteURL: absoluteURL, name: name, path: path, size: size, createdDate: createdDate, modifiedDate: modifiedDate, fileType: fileType, isHidden: isHidden, isReadOnly: isReadOnly) + public init(absoluteURL: URL, name: String, path: String) { + super.init(absoluteURL: absoluteURL, name: name, path: path) + } + + open internal(set) var contentType: String { + get { + return allValues["NSURLContentTypeKey"] as? String ?? "" + } + set { + allValues["NSURLContentTypeKey"] = newValue + } + } + + open internal(set) var entryTag: String? { + get { + return allValues["NSURLEntryTagKey"] as? String + } + set { + allValues["NSURLEntryTagKey"] = newValue + } } - // codebeat:enable[ARITY] } /// Because this class uses NSURLSession, it's necessary to disable App Transport Security @@ -537,13 +548,14 @@ internal extension WebDAVFileProvider { href = absoluteURL(href.path) } let name = davResponse.prop["displayname"] ?? (davResponse.hrefString.removingPercentEncoding! as NSString).lastPathComponent - let size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown - let createdDate = self.resolve(dateString: davResponse.prop["creationdate"] ?? "") - let modifiedDate = self.resolve(dateString: davResponse.prop["getlastmodified"] ?? "") - let contentType = davResponse.prop["getcontenttype"] ?? "octet/stream" - let isDirectory = contentType == "httpd/unix-directory" - let entryTag = davResponse.prop["getetag"] - return WebDavFileObject(absoluteURL: href, name: name, path: href.path, size: size, contentType: contentType, createdDate: createdDate, modifiedDate: modifiedDate, fileType: isDirectory ? .directory : .regular, isHidden: false, isReadOnly: false, entryTag: entryTag) + let fileObject = WebDavFileObject(absoluteURL: href, name: name, path: href.path) + fileObject.size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown + fileObject.creationDate = self.resolve(dateString: davResponse.prop["creationdate"] ?? "") + fileObject.modifiedDate = self.resolve(dateString: davResponse.prop["getlastmodified"] ?? "") + fileObject.contentType = davResponse.prop["getcontenttype"] ?? "octet/stream" + fileObject.fileType = fileObject.contentType == "httpd/unix-directory" ? .directory : .regular + fileObject.entryTag = davResponse.prop["getetag"] + return fileObject } fileprivate func delegateNotify(_ operation: FileOperationType, error: Error?) {