Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save amosavian/bf5c9f106dc31551c06e2d201c3cb878 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 UnrarKit
// TODO: Progress
class RarFileProvider: FileProvider {
open class var type: String { return "RarCompressed" }
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: URKArchive
/// 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? URKArchive(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 = RarFileProvider(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 { RarFileObject(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: Int64 = list.reduce(0) { $0 + $1.uncompressedSize }
var allValues: [URLResourceKey: Any] = [.volumeAvailableCapacityKey: -used]
allValues[.volumeURLKey] = self.archive.fileURL
allValues[.volumeNameKey] = self.archive.fileURL?.lastPathComponent
allValues[.volumeIsReadOnlyKey] = true
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({ RarFileObject(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: URKFileInfo) {
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), .remove(path: let path):
let errorpath = self.baseURL!.lastPathComponent.appendingPathComponent(path)
let error = CocoaError(.fileWriteVolumeReadOnly, userInfo: [NSFilePathErrorKey: errorpath])
throw error
case .copy(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 {
let errorpath = self.baseURL!.lastPathComponent.appendingPathComponent(dest)
let error = CocoaError(.fileWriteVolumeReadOnly, userInfo: [NSFilePathErrorKey: errorpath])
throw error
}
case .move(source: _, destination: let dest):
let errorpath = self.baseURL!.lastPathComponent.appendingPathComponent(dest)
let error = CocoaError(.fileWriteVolumeReadOnly, userInfo: [NSFilePathErrorKey: errorpath])
throw error
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 RarFileObject: FileObject {
init(baseURL: URL, info: URKFileInfo) {
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
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment