Skip to content

Instantly share code, notes, and snippets.

@amosavian
Created October 7, 2017 21:47
Show Gist options
  • Select an option

  • Save amosavian/f59138c4c876609ca4044485d8f349e2 to your computer and use it in GitHub Desktop.

Select an option

Save amosavian/f59138c4c876609ca4044485d8f349e2 to your computer and use it in GitHub Desktop.
//
// ZipFileProvider
// ExtDownloader
//
// Created by Amir Abbas on 1/30/1396 AP.
// Copyright © 1396 AP Mousavian. All rights reserved.
//
import Foundation
import FilesProvider
import UnzipKit
// TODO: Progress
class ZipFileProvider: FileProvider {
open class var type: String { return "Compressed" }
open fileprivate(set) var baseURL: URL?
open var currentPath: String
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue
open weak var delegate: FileProviderDelegate?
open var credential: URLCredential? {
didSet {
archive.password = credential?.password
}
}
fileprivate var archive: UZKArchive
/// Initializes provider for the specified zip file in local URL.
///
/// - Parameter baseURL: Local URL location for base directory.
public init?(baseURL: URL) {
guard baseURL.isFileURL || baseURL.scheme == "compressed" else {
fatalError("Cannot initialize a Local provider from remote URL.")
}
guard let archive = try? UZKArchive(path: baseURL.path) else {
return nil
}
self.archive = archive
let base = (baseURL.path.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")).absoluteURL
var components = URLComponents(url: base, resolvingAgainstBaseURL: true)
components?.scheme = "compressed"
self.baseURL = components?.url ?? base
self.currentPath = ""
self.credential = nil
dispatch_queue = DispatchQueue(label: "FileProvider.\(Swift.type(of: self).type)", attributes: .concurrent)
operation_queue = OperationQueue()
operation_queue.name = "FileProvider.\(Swift.type(of: self).type).Operation"
}
public required convenience init?(coder aDecoder: NSCoder) {
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else {
return nil
}
self.init(baseURL: baseURL)
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
}
open func encode(with aCoder: NSCoder) {
aCoder.encode(self.baseURL, forKey: "baseURL")
aCoder.encode(self.currentPath, forKey: "currentPath")
}
public static var supportsSecureCoding: Bool {
return true
}
public func copy(with zone: NSZone? = nil) -> Any {
let copy = ZipFileProvider(baseURL: self.baseURL!)!
copy.currentPath = self.currentPath
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
return copy
}
open func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
dispatch_queue.async {
do {
let barePath = path.trimmingCharacters(in: CharacterSet(charactersIn: "/ "))
let list = try self.archive.listFileInfo()
let contents = list.filter({ $0.filename.deletingLastPathComponent == barePath }).map { CompressedFileObject(baseURL: self.baseURL!, info: $0) }
completionHandler(contents, nil)
} catch {
completionHandler([], error)
}
}
}
open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
let barePath = path.trimmingCharacters(in: CharacterSet(charactersIn: "/ "))
let parentPath = path.deletingLastPathComponent.trimmingCharacters(in: CharacterSet(charactersIn: "/ "))
self.contentsOfDirectory(path: parentPath) { (contents, error) in
let file = contents.first(where: { $0.path == barePath })
completionHandler(file, error)
}
}
open func storageProperties(completionHandler: @escaping (_ volume: VolumeObject?) -> Void) {
dispatch_queue.async {
do {
let list = try self.archive.listFileInfo()
let used: UInt64 = list.reduce(0) { $0 + $1.uncompressedSize }
var allValues: [URLResourceKey: Any] = [.volumeAvailableCapacityKey: -Int64(used)]
allValues[.volumeURLKey] = self.archive.fileURL
allValues[.volumeNameKey] = self.archive.fileURL?.lastPathComponent
let volume = VolumeObject(allValues: allValues)
completionHandler(volume)
} catch {
completionHandler(nil)
}
}
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
dispatch_queue.async {
do {
let list = try self.archive.listFileInfo()
let contents = list.map({ CompressedFileObject(baseURL: self.baseURL!, info: $0) }).filter { query.evaluate(with: $0.mapPredicate()) }
completionHandler(contents, nil)
} catch {
completionHandler([], error)
}
}
return nil
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
dispatch_queue.async {
completionHandler(self.archive.filename != nil && self.archive.validatePassword())
}
}
open func isPasswordProtected() -> Bool {
return self.archive.isPasswordProtected() && !self.archive.validatePassword()
}
open weak var fileOperationDelegate : FileOperationDelegate?
@discardableResult
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
return self.doOperation(operation, completionHandler: completionHandler)
}
@discardableResult
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.move(source: path, destination: toPath)
if !overwrite && FileManager.default.fileExists(atPath: self.url(of: toPath).path) {
let errorpath = self.baseURL!.lastPathComponent.appendingPathComponent(toPath)
let error = CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: errorpath])
completionHandler?(error)
return nil
}
return self.doOperation(operation, overwrite: overwrite, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.copy(source: path, destination: toPath)
if !overwrite && FileManager.default.fileExists(atPath: self.url(of: toPath).path) {
self.dispatch_queue.async {
let errorpath = self.baseURL!.lastPathComponent.appendingPathComponent(toPath)
let error = CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: errorpath])
completionHandler?(error)
}
return nil
}
return self.doOperation(operation, overwrite: overwrite, completionHandler: completionHandler)
}
@discardableResult
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.remove(path: path)
return self.doOperation(operation, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
return self.doOperation(operation, overwrite: overwrite, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
return self.doOperation(operation, completionHandler: completionHandler)
}
@discardableResult
fileprivate func doOperation(_ operation: FileOperationType, data: Data? = nil, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
func askPassword(file: UZKFileInfo) {
if file.isEncryptedWithPassword {
let group = DispatchGroup()
let msg = "Please enter password to access \(self.type)"
let okBtn = Utility.UI.AlertButton(title: NSLocalizedString("OK", comment: "button"), buttonType: Utility.UI.ButtonType.default, buttonHandler: { fields in
let credential = URLCredential(user: "anonymous", password: fields[0], persistence: .forSession)
self.credential = credential
group.leave()
})
let cancel = Utility.UI.AlertButton(title: NSLocalizedString("Cancel", comment: "button"), buttonType: .cancel, buttonHandler: { _ in
group.leave()
})
let passBox = Utility.UI.AlertTextField(placeHolder: "password", defaultValue: "", textInputTraits: TextInputTraits())
group.enter()
DispatchQueue.main.async {
Utility.UI.askAlert(msg, withTitle: NSLocalizedString("\(self.type) Credential", comment: "Remote credential"), viewController: UIApplication.shared.mn_topViewController()!, buttons: [okBtn, cancel], textFields: [passBox])
}
group.wait()
}
}
let pathTrim = CharacterSet(charactersIn: "/")
operation_queue.addOperation {
do {
switch operation {
case .create(path: let path), .modify(path: let path):
var barePath = path.trimmingCharacters(in: pathTrim)
if path.hasSuffix("/") {
barePath.append("/")
}
try self.archive.write(data ?? Data(), filePath: barePath, fileDate: Date(), compressionMethod: .none, password: self.archive.password, overwrite: overwrite, progress: { progress in
self.delegateNotify(operation, progress: Double(progress))
})
case .copy(source: let source, destination: let dest), .move(source: let source, destination: let dest):
if dest.hasPrefix("file://"), let destURL = URL(string: dest) {
let files = try self.archive.listFileInfo().filter { $0.filename.hasPrefix(source.trimmingCharacters(in: pathTrim)) }
for (i, file) in files.enumerated() {
askPassword(file: file)
let finalRelPath = file.filename.replacingOccurrences(of: source.trimmingCharacters(in: pathTrim), with: "", options: .anchored).trimmingCharacters(in: pathTrim)
let finalDestURL = destURL.appendingPathComponent(finalRelPath)
if file.isDirectory {
try FileManager.default.createDirectory(atPath: finalDestURL.path, withIntermediateDirectories: true, attributes: [.modificationDate: file.timestamp])
} else {
let data = try self.archive.extractData(fromFile: file.filename, progress: { progress in
self.delegateNotify(operation, progress: (Double(i) + Double(progress)) / Double(files.count))
})
try data.write(to: finalDestURL)
try? FileManager.default.setAttributes([FileAttributeKey.modificationDate:file.timestamp], ofItemAtPath: finalDestURL.path)
}
}
} else if source.hasPrefix("file://"), let sourceURL = URL(string: source) {
let files: [URL] = [sourceURL].flatMap({ (url) -> [URL] in
if url.fileIsDirectory {
let subpaths: [String] = (try? FileManager.default.subpathsOfDirectory(atPath: url.path)) ?? []
let urlSubpaths: [URL] = subpaths.map({
let path = url.path.appendingPathComponent($0)
return URL(fileURLWithPath: path)
})
return [url] + urlSubpaths
} else {
return [url]
}
})
for (i, file) in files.enumerated() {
let isDir = file.fileIsDirectory
let data = isDir ? Data() : try Data(contentsOf: file)
let date = source.fileInfo.modificationDate
let relativePath = file.path.replacingOccurrences(of: sourceURL.deletingLastPathComponent().path, with: "", options: .anchored).trimmingCharacters(in: pathTrim)
let destPath = isDir ? relativePath + "/" : relativePath
try self.archive.write(data, filePath: destPath, fileDate: date, compressionMethod: .default, password: self.archive.password, overwrite: overwrite, progress: { progress in
let fraction = (Double(i) + Double(progress)) / Double(files.count)
self.delegateNotify(operation, progress: fraction)
})
}
} else {
let bareSource = source.trimmingCharacters(in: pathTrim)
let files = try self.archive.listFileInfo().filter { $0.filename.hasPrefix(source.trimmingCharacters(in: pathTrim)) }
for (i, file) in files.enumerated() {
let relativePath = file.filename.replacingOccurrences(of: bareSource, with: "", options: .anchored).trimmingCharacters(in: pathTrim)
var finalPath = dest + relativePath
let data: Data
if file.isDirectory {
finalPath.append("/")
data = Data()
} else {
askPassword(file: file)
data = try self.archive.extractData(fromFile: file.filename, progress: { progress in
let fraction = (Double(i) + Double(progress)) / Double(files.count * 2)
self.delegateNotify(operation, progress: fraction)
})
}
try self.archive.write(data, filePath: finalPath, fileDate: file.timestamp, compressionMethod: .default, password: self.archive.password, overwrite: overwrite, progress: { progress in
let fraction = (Double(files.count) + Double(i) + Double(progress)) / Double(files.count * 2)
self.delegateNotify(operation, progress: fraction)
})
}
}
if case .move = operation {
try self.archive.deleteFile(source.trimmingCharacters(in: pathTrim))
}
case .remove(path: let path):
try self.archive.deleteFile(path.trimmingCharacters(in: pathTrim))
default:
return
}
completionHandler?(nil)
self.delegateNotify(operation)
} catch {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
}
return nil
}
@discardableResult
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
let operation = FileOperationType.fetch(path: path)
let barePath = path.trimmingCharacters(in: CharacterSet(charactersIn: "/ "))
dispatch_queue.async {
do {
let data = try self.archive.extractData(fromFile: barePath, progress: { (progress) in
self.delegateNotify(operation, progress: Double(progress))
})
completionHandler(data, nil)
self.delegateNotify(operation)
} catch {
completionHandler(nil, error)
self.delegateNotify(operation, error: error)
}
}
return nil
}
@discardableResult
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
return self.contents(path: path) { (data, error) in
let range: Range<Int> = Int(offset)..<(Int(offset) + length)
completionHandler(data?.subdata(in: range), error)
}
}
@discardableResult
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation: FileOperationType = .modify(path: path)
return self.doOperation(operation, data: data ?? Data(), overwrite: overwrite, completionHandler: completionHandler)
}
fileprivate func delegateNotify(_ operation: FileOperationType, error: Error? = nil) {
DispatchQueue.main.async(execute: {
if let error = error {
self.delegate?.fileproviderFailed(self, operation: operation, error: error)
} else {
self.delegate?.fileproviderSucceed(self, operation: operation)
}
})
}
fileprivate func delegateNotify(_ operation: FileOperationType, progress: Double) {
DispatchQueue.main.async(execute: {
self.delegate?.fileproviderProgress(self, operation: operation, progress: Float(progress))
})
}
}
class CompressedFileObject: FileObject {
init(baseURL: URL, info: UZKFileInfo) {
var allValues = [URLResourceKey: Any]()
allValues = [URLResourceKey: Any]()
var path = info.filename.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
if info.isDirectory {
path.append("/")
}
allValues[.fileURLKey] = URL(string: path, relativeTo: baseURL)
allValues[.nameKey] = info.filename.lastPathComponent
allValues[.pathKey] = path
allValues[.fileSizeKey] = Int64(info.uncompressedSize)
allValues[.fileResourceTypeKey] = info.isDirectory ? URLFileResourceType.directory : URLFileResourceType.regular
allValues[.documentIdentifierKey] = info.crc
allValues[.fileAllocatedSizeKey] = Int64(info.compressedSize)
allValues[.isEncryptedKey] = info.isEncryptedWithPassword
super.init(allValues: allValues)
}
var compressedSize: Int64? {
get {
return allValues[.fileAllocatedSizeKey] as? Int64
}
}
var isEncrypted: Bool {
get {
return allValues[.isEncryptedKey] as? Bool ?? false
}
}
var crc: UInt? {
get {
return allValues[.documentIdentifierKey] as? UInt
}
}
}
extension FileObject {
func mapPredicate() -> [String: Any] {
let mapDict: [URLResourceKey: String] = [.fileURLKey: "url", .nameKey: "name", .pathKey: "path", .fileSizeKey: "filesize", .creationDateKey: "creationDate",
.contentModificationDateKey: "modifiedDate", .isHiddenKey: "isHidden", .isWritableKey: "isWritable", .serverDateKey: "serverDate", .entryTagKey: "entryTag", .mimeTypeKey: "mimeType"]
let typeDict: [URLFileResourceType: String] = [.directory: "directory", .regular: "regular", .symbolicLink: "symbolicLink", .unknown: "unknown"]
var result = [String: Any]()
for (key, value) in allValues {
if let convertkey = mapDict[key] {
result[convertkey] = value
}
}
result["eTag"] = result["entryTag"]
result["isReadOnly"] = self.isReadOnly
result["isDirectory"] = self.isDirectory
result["isRegularFile"] = self.isRegularFile
result["isSymLink"] = self.isSymLink
result["type"] = typeDict[self.type ] ?? "unknown"
return result
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment