332 lines
14 KiB
Swift
332 lines
14 KiB
Swift
//
|
|
// FileProvider.swift
|
|
// FileProvider
|
|
//
|
|
// Created by Amir Abbas Mousavian.
|
|
// Copyright © 2016 Mousavian. Distributed under MIT license.
|
|
//
|
|
|
|
import Foundation
|
|
#if os(iOS)
|
|
import UIKit
|
|
public typealias ImageClass = UIImage
|
|
#elseif os(OSX)
|
|
import Cocoa
|
|
public typealias ImageClass = NSImage
|
|
#endif
|
|
|
|
public enum FileType: String {
|
|
case Directory
|
|
case Regular
|
|
case SymbolicLink
|
|
case Socket
|
|
case CharacterSpecial
|
|
case BlockSpecial
|
|
case NamedPipe
|
|
case Unknown
|
|
|
|
init(urlResourceTypeValue: String) {
|
|
switch urlResourceTypeValue {
|
|
case NSURLFileResourceTypeNamedPipe: self = .NamedPipe
|
|
case NSURLFileResourceTypeCharacterSpecial: self = .CharacterSpecial
|
|
case NSURLFileResourceTypeDirectory: self = .Directory
|
|
case NSURLFileResourceTypeBlockSpecial: self = .BlockSpecial
|
|
case NSURLFileResourceTypeRegular: self = .Regular
|
|
case NSURLFileResourceTypeSymbolicLink: self = .SymbolicLink
|
|
case NSURLFileResourceTypeSocket: self = .Socket
|
|
case NSURLFileResourceTypeUnknown: self = .Unknown
|
|
default: self = .Unknown
|
|
}
|
|
}
|
|
|
|
init(fileTypeValue: String) {
|
|
switch fileTypeValue {
|
|
case NSFileTypeCharacterSpecial: self = .CharacterSpecial
|
|
case NSFileTypeDirectory: self = .Directory
|
|
case NSFileTypeBlockSpecial: self = .BlockSpecial
|
|
case NSFileTypeRegular: self = .Regular
|
|
case NSFileTypeSymbolicLink: self = .SymbolicLink
|
|
case NSFileTypeSocket: self = .Socket
|
|
case NSFileTypeUnknown: self = .Unknown
|
|
default: self = .Unknown
|
|
}
|
|
}
|
|
}
|
|
|
|
protocol FoundationErrorEnum {
|
|
init? (rawValue: Int)
|
|
var rawValue: Int { get }
|
|
}
|
|
|
|
extension NSURLError: FoundationErrorEnum {}
|
|
extension NSCocoaError: FoundationErrorEnum {}
|
|
|
|
public class FileObject {
|
|
let absoluteURL: NSURL?
|
|
let name: String
|
|
let path: String
|
|
let size: Int64
|
|
let createdDate: NSDate?
|
|
let modifiedDate: NSDate?
|
|
let fileType: FileType
|
|
let isHidden: Bool
|
|
let 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
|
|
self.fileType = fileType
|
|
self.isHidden = isHidden
|
|
self.isReadOnly = isReadOnly
|
|
}
|
|
|
|
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
|
|
self.fileType = .Regular
|
|
self.isHidden = isHidden
|
|
self.isReadOnly = isReadOnly
|
|
}
|
|
|
|
var isDirectory: Bool {
|
|
return self.fileType == .Directory
|
|
}
|
|
|
|
var isSymLink: Bool {
|
|
return self.fileType == .SymbolicLink
|
|
}
|
|
}
|
|
|
|
|
|
public typealias SimpleCompletionHandler = ((error: ErrorType?) -> Void)?
|
|
|
|
public protocol FileProviderBasic: class {
|
|
var type: String { get }
|
|
var isPathRelative: Bool { get }
|
|
var baseURL: NSURL? { get }
|
|
var currentPath: String { get set }
|
|
var dispatch_queue: dispatch_queue_t { get set }
|
|
var delegate: FileProviderDelegate? { get set }
|
|
var credential: NSURLCredential? { get }
|
|
|
|
/**
|
|
*
|
|
*/
|
|
func contentsOfDirectoryAtPath(path: String, completionHandler: ((contents: [FileObject], error: ErrorType?) -> Void))
|
|
func attributesOfItemAtPath(path: String, completionHandler: ((attributes: FileObject?, error: ErrorType?) -> Void))
|
|
}
|
|
|
|
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)
|
|
func copyItemAtPath(path: String, toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler)
|
|
func removeItemAtPath(path: String, completionHandler: SimpleCompletionHandler)
|
|
|
|
func copyLocalFileToPath(localFile: NSURL, toPath: String, completionHandler: SimpleCompletionHandler)
|
|
func copyPathToLocalFile(path: String, toLocalURL: NSURL, completionHandler: SimpleCompletionHandler)
|
|
}
|
|
|
|
public protocol FileProviderReadWrite: FileProviderBasic {
|
|
func contentsAtPath(path: String, completionHandler: ((contents: NSData?, error: ErrorType?) -> Void))
|
|
func contentsAtPath(path: String, offset: Int64, length: Int, completionHandler: ((contents: NSData?, error: ErrorType?) -> Void))
|
|
func writeContentsAtPath(path: String, contents data: NSData, atomically: Bool, completionHandler: SimpleCompletionHandler)
|
|
|
|
func searchFilesAtPath(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: ((files: [FileObject], error: ErrorType?) -> Void))
|
|
}
|
|
|
|
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 {
|
|
|
|
}
|
|
|
|
extension FileProviderBasic {
|
|
var bareCurrentPath: String {
|
|
return currentPath.stringByTrimmingCharactersInSet(NSCharacterSet(charactersInString: ". /"))
|
|
}
|
|
|
|
public func absoluteURL(path: String? = nil) -> NSURL {
|
|
let rpath: String
|
|
if let path = path {
|
|
rpath = path
|
|
} else {
|
|
rpath = self.currentPath
|
|
}
|
|
if isPathRelative, let baseURL = baseURL {
|
|
if rpath.hasPrefix("/") && baseURL.absoluteString.hasSuffix("/") {
|
|
var npath = rpath
|
|
npath.removeAtIndex(npath.startIndex)
|
|
return baseURL.URLByAppendingPathComponent(npath)
|
|
} else {
|
|
return baseURL.URLByAppendingPathComponent(rpath)
|
|
}
|
|
} else {
|
|
return NSURL(fileURLWithPath: rpath).URLByStandardizingPath!
|
|
}
|
|
}
|
|
|
|
public func relativePathOf(url url: NSURL) -> String {
|
|
guard let baseURL = self.baseURL else { return url.absoluteString }
|
|
return url.URLByStandardizingPath!.absoluteString.stringByReplacingOccurrencesOfString(baseURL.absoluteString, withString: "/").stringByRemovingPercentEncoding!
|
|
}
|
|
|
|
public func fileByUniqueName(filePath: String) -> String {
|
|
let fileUrl = NSURL(fileURLWithPath: filePath)
|
|
let dirPath = fileUrl.URLByDeletingLastPathComponent?.path ?? ""
|
|
guard let fileName = fileUrl.URLByDeletingPathExtension?.lastPathComponent else {
|
|
return filePath
|
|
}
|
|
let fileExt = fileUrl.pathExtension ?? ""
|
|
var result = fileName
|
|
let group = dispatch_group_create()
|
|
dispatch_group_enter(group)
|
|
self.contentsOfDirectoryAtPath(dirPath) { (contents, error) in
|
|
var bareFileName = fileName
|
|
let number = Int(fileName.componentsSeparatedByString(" ").filter {
|
|
!$0.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()).isEmpty
|
|
}.last ?? "noname")
|
|
if let _ = number {
|
|
result = fileName.componentsSeparatedByString(" ").filter {
|
|
!$0.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()).isEmpty
|
|
}.dropLast().joinWithSeparator(" ")
|
|
bareFileName = result
|
|
}
|
|
var i = number ?? 2
|
|
let similiar = contents.map {
|
|
$0.absoluteURL?.lastPathComponent ?? $0.name
|
|
}.filter {
|
|
$0.hasPrefix(result) && (!fileExt.isEmpty && $0.hasSuffix("." + fileExt))
|
|
}
|
|
while similiar.contains(result + (!fileExt.isEmpty ? "." + fileExt : "")) {
|
|
result = "\(bareFileName) \(i)"
|
|
i += 1
|
|
}
|
|
dispatch_group_leave(group)
|
|
}
|
|
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
|
|
let finalFile = result + (!fileExt.isEmpty ? "." + fileExt : "")
|
|
return (dirPath as NSString).stringByAppendingPathComponent(finalFile)
|
|
}
|
|
|
|
internal func throwError(path: String, code: FoundationErrorEnum) -> NSError {
|
|
let fileURL = self.absoluteURL(path)
|
|
let domain: String
|
|
switch code {
|
|
case is NSURLError:
|
|
domain = NSURLErrorDomain
|
|
default:
|
|
domain = NSCocoaErrorDomain
|
|
}
|
|
return NSError(domain: domain, code: code.rawValue, userInfo: [NSURLErrorFailingURLErrorKey: fileURL, NSURLErrorFailingURLStringErrorKey: fileURL.absoluteString])
|
|
}
|
|
|
|
internal func NotImplemented() {
|
|
assert(false, "method not implemented")
|
|
}
|
|
|
|
internal func resolveRFCDate(httpDateString: String) -> NSDate? {
|
|
let dateFor: NSDateFormatter = NSDateFormatter()
|
|
dateFor.locale = NSLocale(localeIdentifier: "en_US")
|
|
dateFor.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss zzz"
|
|
if let rfc1123 = dateFor.dateFromString(httpDateString) {
|
|
return rfc1123
|
|
}
|
|
dateFor.dateFormat = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
|
|
if let rfc850 = dateFor.dateFromString(httpDateString) {
|
|
return rfc850
|
|
}
|
|
dateFor.dateFormat = "EEE MMM d HH':'mm':'ss yyyy"
|
|
if let asctime = dateFor.dateFromString(httpDateString) {
|
|
return asctime
|
|
}
|
|
//self.init()
|
|
return nil
|
|
}
|
|
|
|
internal func jsonToDictionary(jsonString: String) -> [String: AnyObject]? {
|
|
guard let data = jsonString.dataUsingEncoding(NSUTF8StringEncoding) else {
|
|
return nil
|
|
}
|
|
if let dic = try? NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as? [String: AnyObject] {
|
|
return dic
|
|
}
|
|
return nil
|
|
}
|
|
|
|
internal func dictionaryToJSON(dictionary: [String: AnyObject]) -> String? {
|
|
if let data = try? NSJSONSerialization.dataWithJSONObject(dictionary, options: NSJSONWritingOptions()) {
|
|
return String(data: data, encoding: NSUTF8StringEncoding)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
|
|
public protocol ExtendedFileProvider: FileProvider {
|
|
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))
|
|
}
|
|
|
|
public enum FileOperation {
|
|
case Create (path: String)
|
|
case Copy (source: String, destination: String)
|
|
case Move (source: String, destination: String)
|
|
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: 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
|
|
} |