diff --git a/Zip/QuickZip.swift b/Zip/QuickZip.swift index 779912a8..0d40b5f3 100644 --- a/Zip/QuickZip.swift +++ b/Zip/QuickZip.swift @@ -40,6 +40,42 @@ extension Zip { return try quickUnzipFile(path, progress: nil) } + /** + Quick unzip a file. Unzips to a new folder inside the app's documents folder with the zip file's name. + + - parameter path: Path of zipped file. NSURL. + - parameter progress: A Cancellable progress + + - throws: Error if unzipping fails or if file is not found. Can be printed with a description variable. + + - notes: Supports implicit progress composition + + - returns: NSURL of the destination folder. + */ + public class func quickUnzipFile(_ path: URL, progress: Progress?) throws -> URL { + let fileManager = FileManager.default + + let fileExtension = path.pathExtension + let fileName = path.lastPathComponent + + let directoryName = fileName.replacingOccurrences(of: ".\(fileExtension)", with: "") + + #if os(Linux) + // urls(for:in:) is not yet implemented on Linux + // See https://github.com/apple/swift-corelibs-foundation/blob/swift-4.2-branch/Foundation/FileManager.swift#L125 + let documentsUrl = fileManager.temporaryDirectory + #else + let documentsUrl = fileManager.urls(for: self.searchPathDirectory(), in: .userDomainMask)[0] + #endif + do { + let destinationUrl = documentsUrl.appendingPathComponent(directoryName, isDirectory: true) + try self.unzipFile(path, destination: destinationUrl, overwrite: true, password: nil, progress: progress) + return destinationUrl + }catch{ + throw(ZipError.unzipFail) + } + } + /** Quick unzip a file. Unzips to a new folder inside the app's documents folder with the zip file's name. @@ -52,7 +88,7 @@ extension Zip { - returns: NSURL of the destination folder. */ - public class func quickUnzipFile(_ path: URL, progress: ((_ progress: Double) -> ())?) throws -> URL { + public class func quickUnzipFile(_ path: URL, progressCallback: ((_ progress: Double) -> ())?) throws -> URL { let fileManager = FileManager.default let fileExtension = path.pathExtension @@ -69,7 +105,7 @@ extension Zip { #endif do { let destinationUrl = documentsUrl.appendingPathComponent(directoryName, isDirectory: true) - try self.unzipFile(path, destination: destinationUrl, overwrite: true, password: nil, progress: progress) + try self.unzipFile(path, destination: destinationUrl, overwrite: true, password: nil, progressCallback: progressCallback) return destinationUrl }catch{ throw(ZipError.unzipFail) diff --git a/Zip/Zip.swift b/Zip/Zip.swift index 9269429b..26e63cd5 100644 --- a/Zip/Zip.swift +++ b/Zip/Zip.swift @@ -17,6 +17,8 @@ public enum ZipError: Error { case unzipFail /// Zip fail case zipFail + /// User Cancelled + case userCancelled /// User readable description public var description: String { @@ -24,6 +26,7 @@ public enum ZipError: Error { case .fileNotFound: return NSLocalizedString("File not found.", comment: "") case .unzipFail: return NSLocalizedString("Failed to unzip file.", comment: "") case .zipFail: return NSLocalizedString("Failed to zip file.", comment: "") + case .userCancelled: return NSLocalizedString("Cancelled", comment: "") } } } @@ -96,7 +99,7 @@ public class Zip { - notes: Supports implicit progress composition */ - public class func unzipFile(_ zipFilePath: URL, destination: URL, overwrite: Bool, password: String?, progress: ((_ progress: Double) -> ())? = nil, fileOutputHandler: ((_ unzippedFile: URL) -> Void)? = nil) throws { + public class func unzipFile(_ zipFilePath: URL, destination: URL, overwrite: Bool, password: String?, progressCallback: ((_ progress: Double) -> ())? = nil, fileOutputHandler: ((_ unzippedFile: URL) -> Void)? = nil) throws { // File manager let fileManager = FileManager.default @@ -107,172 +110,180 @@ public class Zip { if fileManager.fileExists(atPath: path) == false || fileExtensionIsInvalid(zipFilePath.pathExtension) { throw ZipError.fileNotFound } - - // Unzip set up - var ret: Int32 = 0 - var crc_ret: Int32 = 0 - let bufferSize: UInt32 = 4096 - var buffer = Array(repeating: 0, count: Int(bufferSize)) - - // Progress handler set up - var totalSize: Double = 0.0 - var currentPosition: Double = 0.0 - let fileAttributes = try fileManager.attributesOfItem(atPath: path) - if let attributeFileSize = fileAttributes[FileAttributeKey.size] as? Double { - totalSize += attributeFileSize - } + let totalSize = try getTotalSize(path: path) let progressTracker = Progress(totalUnitCount: Int64(totalSize)) progressTracker.isCancellable = false progressTracker.isPausable = false progressTracker.kind = ProgressKind.file - // Begin unzipping + try unzip(zipFilePath, + destination: destination, + overwrite: overwrite, + password: password, + progressCallback: progressCallback) + + progressTracker.completedUnitCount = Int64(totalSize) + } + + /** + Unzip file + + - parameter zipFilePath: Local file path of zipped file. NSURL. + - parameter destination: Local file path to unzip to. NSURL. + - parameter overwrite: Overwrite bool. + - parameter password: Optional password if file is protected. + - parameter progress: A cancellable progress. + + - throws: Error if unzipping fails or if fail is not found. Can be printed with a description variable. + + - notes: Supports implicit progress composition + */ + + public class func unzipFile(_ zipFilePath: URL, destination: URL, overwrite: Bool, password: String?, progress: Progress? = nil, fileOutputHandler: ((_ unzippedFile: URL) -> Void)? = nil) throws { + + let path = zipFilePath.path + let totalSize = try getTotalSize(path: path) + progress?.totalUnitCount = Int64(totalSize) + + try unzip(zipFilePath, + destination: destination, + overwrite: overwrite, + password: password, + progress: progress, + progressCallback: nil) + + progress?.completedUnitCount = Int64(totalSize) + } + + /// check if zip is password protected + /// - Parameter path: path to zip file + /// - Returns: true if file at path is password protected + public class func zipIsPasswordProtected(at path: String) -> Bool { let zip = unzOpen64(path) - defer { - unzClose(zip) - } - if unzGoToFirstFile(zip) != UNZ_OK { - throw ZipError.unzipFail - } - repeat { - if let cPassword = password?.cString(using: String.Encoding.ascii) { - ret = unzOpenCurrentFilePassword(zip, cPassword) - } - else { - ret = unzOpenCurrentFile(zip); - } - if ret != UNZ_OK { - throw ZipError.unzipFail - } - var fileInfo = unz_file_info64() - memset(&fileInfo, 0, MemoryLayout.size) - ret = unzGetCurrentFileInfo64(zip, &fileInfo, nil, 0, nil, 0, nil, 0) - if ret != UNZ_OK { - unzCloseCurrentFile(zip) - throw ZipError.unzipFail - } - currentPosition += Double(fileInfo.compressed_size) - let fileNameSize = Int(fileInfo.size_filename) + 1 - //let fileName = UnsafeMutablePointer(allocatingCapacity: fileNameSize) - let fileName = UnsafeMutablePointer.allocate(capacity: fileNameSize) - - unzGetCurrentFileInfo64(zip, &fileInfo, fileName, UInt(fileNameSize), nil, 0, nil, 0) - fileName[Int(fileInfo.size_filename)] = 0 - - var pathString = String(cString: fileName) - - guard pathString.count > 0 else { - throw ZipError.unzipFail - } + var ret: Int32 = unzGoToFirstFile( zip ) + if ret == UNZ_OK { + while( ret==UNZ_OK && UNZ_OK != UNZ_END_OF_LIST_OF_FILE ) { + ret = unzOpenCurrentFile( zip ) + if( ret != UNZ_OK ) { + return false; + } + var fileInfo: unz_file_info = unz_file_info() + ret = unzGetCurrentFileInfo(zip, &fileInfo, nil, 0, nil, 0, nil, 0) + if (ret != UNZ_OK) { + return false + } else if((fileInfo.flag & 1) == 1) { + return true + } - var isDirectory = false - let fileInfoSizeFileName = Int(fileInfo.size_filename-1) - if (fileName[fileInfoSizeFileName] == "/".cString(using: String.Encoding.utf8)?.first || fileName[fileInfoSizeFileName] == "\\".cString(using: String.Encoding.utf8)?.first) { - isDirectory = true; + unzCloseCurrentFile( zip ) + ret = unzGoToNextFile( zip ) } - free(fileName) - if pathString.rangeOfCharacter(from: CharacterSet(charactersIn: "/\\")) != nil { - pathString = pathString.replacingOccurrences(of: "\\", with: "/") - } - - let fullPath = destination.appendingPathComponent(pathString).path - - let creationDate = Date() - - let directoryAttributes: [FileAttributeKey: Any]? - #if os(Linux) - // On Linux, setting attributes is not yet really implemented. - // In Swift 4.2, the only settable attribute is `.posixPermissions`. - // See https://github.com/apple/swift-corelibs-foundation/blob/swift-4.2-branch/Foundation/FileManager.swift#L182-L196 - directoryAttributes = nil - #else - directoryAttributes = [.creationDate : creationDate, - .modificationDate : creationDate] - #endif + } + return false + } + + // MARK: Zip + + /** + Zip files. + + - parameter paths: Array of NSURL filepaths. + - parameter zipFilePath: Destination NSURL, should lead to a .zip filepath. + - parameter password: Password string. Optional. + - parameter compression: Compression strategy + - parameter progress: A progress closure called after unzipping each file in the archive. Double value betweem 0 and 1. + + - throws: Error if zipping fails. + + - notes: Supports implicit progress composition + */ + public class func zipFiles(paths: [URL], zipFilePath: URL, password: String?, compression: ZipCompression = .DefaultCompression, progress: Progress) throws { + + // File manager + let fileManager = FileManager.default + + // Check whether a zip file exists at path. + let destinationPath = zipFilePath.path + + // Process zip paths + let processedPaths = ZipUtilities().processZipPaths(paths) + + // Zip set up + let chunkSize: Int = 16384 + + // Progress handler set up + var currentPosition: Double = 0.0 + var totalSize: Double = 0.0 + // Get totalSize for progress handler + for path in processedPaths { do { - if isDirectory { - try fileManager.createDirectory(atPath: fullPath, withIntermediateDirectories: true, attributes: directoryAttributes) - } - else { - let parentDirectory = (fullPath as NSString).deletingLastPathComponent - try fileManager.createDirectory(atPath: parentDirectory, withIntermediateDirectories: true, attributes: directoryAttributes) + let filePath = path.filePath() + let fileAttributes = try fileManager.attributesOfItem(atPath: filePath) + let fileSize = fileAttributes[FileAttributeKey.size] as? Double + if let fileSize = fileSize { + totalSize += fileSize } - } catch {} - if fileManager.fileExists(atPath: fullPath) && !isDirectory && !overwrite { - unzCloseCurrentFile(zip) - ret = unzGoToNextFile(zip) } - - var writeBytes: UInt64 = 0 - var filePointer: UnsafeMutablePointer? - filePointer = fopen(fullPath, "wb") - while filePointer != nil { - let readBytes = unzReadCurrentFile(zip, &buffer, bufferSize) - if readBytes > 0 { - guard fwrite(buffer, Int(readBytes), 1, filePointer) == 1 else { - throw ZipError.unzipFail + catch {} + } + progress.totalUnitCount = Int64(totalSize) + + // Begin Zipping + let zip = zipOpen(destinationPath, APPEND_STATUS_CREATE) + for path in processedPaths { + let filePath = path.filePath() + var isDirectory: ObjCBool = false + _ = fileManager.fileExists(atPath: filePath, isDirectory: &isDirectory) + if !isDirectory.boolValue { + let input = fopen(filePath, "r") + if input == nil { + throw ZipError.zipFail + } + let fileName = path.fileName + var zipInfo: zip_fileinfo = zip_fileinfo(tmz_date: tm_zip(tm_sec: 0, tm_min: 0, tm_hour: 0, tm_mday: 0, tm_mon: 0, tm_year: 0), dosDate: 0, internal_fa: 0, external_fa: 0) + do { + let fileAttributes = try fileManager.attributesOfItem(atPath: filePath) + if let fileDate = fileAttributes[FileAttributeKey.modificationDate] as? Date { + let components = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: fileDate) + zipInfo.tmz_date.tm_sec = UInt32(components.second!) + zipInfo.tmz_date.tm_min = UInt32(components.minute!) + zipInfo.tmz_date.tm_hour = UInt32(components.hour!) + zipInfo.tmz_date.tm_mday = UInt32(components.day!) + zipInfo.tmz_date.tm_mon = UInt32(components.month!) - 1 + zipInfo.tmz_date.tm_year = UInt32(components.year!) + } + if let fileSize = fileAttributes[FileAttributeKey.size] as? Double { + currentPosition += fileSize } - writeBytes += UInt64(readBytes) + } + catch {} + let buffer = malloc(chunkSize) + if let password = password, let fileName = fileName { + zipOpenNewFileInZip3(zip, fileName, &zipInfo, nil, 0, nil, 0, nil,Z_DEFLATED, compression.minizipCompression, 0, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, password, 0) + } + else if let fileName = fileName { + zipOpenNewFileInZip3(zip, fileName, &zipInfo, nil, 0, nil, 0, nil,Z_DEFLATED, compression.minizipCompression, 0, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, nil, 0) } else { - break + throw ZipError.zipFail } - } - - if let fp = filePointer { fclose(fp) } - - crc_ret = unzCloseCurrentFile(zip) - if crc_ret == UNZ_CRCERROR { - throw ZipError.unzipFail - } - guard writeBytes == fileInfo.uncompressed_size else { - throw ZipError.unzipFail - } - - //Set file permissions from current fileInfo - if fileInfo.external_fa != 0 { - let permissions = (fileInfo.external_fa >> 16) & 0x1FF - //We will devifne a valid permission range between Owner read only to full access - if permissions >= 0o400 && permissions <= 0o777 { - do { - try fileManager.setAttributes([.posixPermissions : permissions], ofItemAtPath: fullPath) - } catch { - print("Failed to set permissions to file \(fullPath), error: \(error)") - } + var length: Int = 0 + while (feof(input) == 0) { + length = fread(buffer, 1, chunkSize, input) + zipWriteInFileInZip(zip, buffer, UInt32(length)) } + progress.completedUnitCount = Int64(currentPosition) + zipCloseFileInZip(zip) + free(buffer) + fclose(input) } - - ret = unzGoToNextFile(zip) - - // Update progress handler - if let progressHandler = progress{ - progressHandler((currentPosition/totalSize)) - } - - if let fileHandler = fileOutputHandler, - let encodedString = fullPath.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), - let fileUrl = URL(string: encodedString) { - fileHandler(fileUrl) - } - - progressTracker.completedUnitCount = Int64(currentPosition) - - } while (ret == UNZ_OK && ret != UNZ_END_OF_LIST_OF_FILE) - - // Completed. Update progress handler. - if let progressHandler = progress{ - progressHandler(1.0) } - - progressTracker.completedUnitCount = Int64(totalSize) - + zipClose(zip, nil) + progress.completedUnitCount = Int64(totalSize) } - // MARK: Zip - - /** Zip files. @@ -532,4 +543,186 @@ public class Zip { return validFileExtensions.contains(fileExtension) } + // MARK: - private methods + + private class func unzip(_ zipFilePath: URL, destination: URL, + overwrite: Bool, password: String?, + progress: Progress? = nil, + progressCallback: ((Double) -> Void)?, + fileOutputHandler: ((_ unzippedFile: URL) -> Void)? = nil) throws { + + // File manager + let fileManager = FileManager.default + + // Check whether a zip file exists at path. + let path = zipFilePath.path + + if fileManager.fileExists(atPath: path) == false || fileExtensionIsInvalid(zipFilePath.pathExtension) { + throw ZipError.fileNotFound + } + + // Unzip set up + var ret: Int32 = 0 + var crc_ret: Int32 = 0 + let bufferSize: UInt32 = 4096 + var buffer = Array(repeating: 0, count: Int(bufferSize)) + + // Progress handler set up + var totalSize: Double = 0.0 + var currentPosition: Double = 0.0 + let fileAttributes = try fileManager.attributesOfItem(atPath: path) + if let attributeFileSize = fileAttributes[FileAttributeKey.size] as? Double { + totalSize += attributeFileSize + } + + // Begin unzipping + let zip = unzOpen64(path) + defer { + unzClose(zip) + } + if unzGoToFirstFile(zip) != UNZ_OK { + throw ZipError.unzipFail + } + repeat { + if let cPassword = password?.cString(using: String.Encoding.ascii) { + ret = unzOpenCurrentFilePassword(zip, cPassword) + } + else { + ret = unzOpenCurrentFile(zip); + } + if ret != UNZ_OK { + throw ZipError.unzipFail + } + var fileInfo = unz_file_info64() + memset(&fileInfo, 0, MemoryLayout.size) + ret = unzGetCurrentFileInfo64(zip, &fileInfo, nil, 0, nil, 0, nil, 0) + if ret != UNZ_OK { + unzCloseCurrentFile(zip) + throw ZipError.unzipFail + } + currentPosition += Double(fileInfo.compressed_size) + let fileNameSize = Int(fileInfo.size_filename) + 1 + //let fileName = UnsafeMutablePointer(allocatingCapacity: fileNameSize) + let fileName = UnsafeMutablePointer.allocate(capacity: fileNameSize) + + unzGetCurrentFileInfo64(zip, &fileInfo, fileName, UInt(fileNameSize), nil, 0, nil, 0) + fileName[Int(fileInfo.size_filename)] = 0 + + var pathString = String(cString: fileName) + + guard pathString.count > 0 else { + throw ZipError.unzipFail + } + + var isDirectory = false + let fileInfoSizeFileName = Int(fileInfo.size_filename-1) + if (fileName[fileInfoSizeFileName] == "/".cString(using: String.Encoding.utf8)?.first || fileName[fileInfoSizeFileName] == "\\".cString(using: String.Encoding.utf8)?.first) { + isDirectory = true; + } + free(fileName) + if pathString.rangeOfCharacter(from: CharacterSet(charactersIn: "/\\")) != nil { + pathString = pathString.replacingOccurrences(of: "\\", with: "/") + } + + let fullPath = destination.appendingPathComponent(pathString).path + + let creationDate = Date() + + let directoryAttributes: [FileAttributeKey: Any]? + #if os(Linux) + // On Linux, setting attributes is not yet really implemented. + // In Swift 4.2, the only settable attribute is `.posixPermissions`. + // See https://github.com/apple/swift-corelibs-foundation/blob/swift-4.2-branch/Foundation/FileManager.swift#L182-L196 + directoryAttributes = nil + #else + directoryAttributes = [.creationDate : creationDate, + .modificationDate : creationDate] + #endif + + do { + if isDirectory { + try fileManager.createDirectory(atPath: fullPath, withIntermediateDirectories: true, attributes: directoryAttributes) + } + else { + let parentDirectory = (fullPath as NSString).deletingLastPathComponent + try fileManager.createDirectory(atPath: parentDirectory, withIntermediateDirectories: true, attributes: directoryAttributes) + } + } catch {} + if fileManager.fileExists(atPath: fullPath) && !isDirectory && !overwrite { + unzCloseCurrentFile(zip) + ret = unzGoToNextFile(zip) + } + + var writeBytes: UInt64 = 0 + var filePointer: UnsafeMutablePointer? + filePointer = fopen(fullPath, "wb") + while filePointer != nil { + let readBytes = unzReadCurrentFile(zip, &buffer, bufferSize) + if readBytes > 0 { + guard fwrite(buffer, Int(readBytes), 1, filePointer) == 1 else { + throw ZipError.unzipFail + } + writeBytes += UInt64(readBytes) + } + else { + break + } + } + + if let fp = filePointer { fclose(fp) } + + crc_ret = unzCloseCurrentFile(zip) + if crc_ret == UNZ_CRCERROR { + throw ZipError.unzipFail + } + guard writeBytes == fileInfo.uncompressed_size else { + throw ZipError.unzipFail + } + + //Set file permissions from current fileInfo + if fileInfo.external_fa != 0 { + let permissions = (fileInfo.external_fa >> 16) & 0x1FF + //We will devifne a valid permission range between Owner read only to full access + if permissions >= 0o400 && permissions <= 0o777 { + do { + try fileManager.setAttributes([.posixPermissions : permissions], ofItemAtPath: fullPath) + } catch { + print("Failed to set permissions to file \(fullPath), error: \(error)") + } + } + } + + ret = unzGoToNextFile(zip) + + // Update progress handler + if let progressHandler = progressCallback { + progressHandler((currentPosition/totalSize)) + } + + if let fileHandler = fileOutputHandler, + let encodedString = fullPath.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let fileUrl = URL(string: encodedString) { + fileHandler(fileUrl) + } + + progress?.completedUnitCount = Int64(currentPosition) + + } while (ret == UNZ_OK && ret != UNZ_END_OF_LIST_OF_FILE) + + // Completed. Update progress handler. + if let progressHandler = progressCallback { + progressHandler(1.0) + } + } + + private class func getTotalSize(path: String) throws -> Double { + // Progress handler set up + var totalSize: Double = 0.0 + let fileAttributes = try FileManager.default.attributesOfItem(atPath: path) + if let attributeFileSize = fileAttributes[FileAttributeKey.size] as? Double { + totalSize += attributeFileSize + } + return totalSize + } + } diff --git a/ZipTests/ZipTests.swift b/ZipTests/ZipTests.swift index 70f689ff..d861f0ef 100644 --- a/ZipTests/ZipTests.swift +++ b/ZipTests/ZipTests.swift @@ -85,7 +85,7 @@ class ZipTests: XCTestCase { func testQuickUnzipProgress() throws { let filePath = url(forResource: "bb8", withExtension: "zip")! - let destinationURL = try Zip.quickUnzipFile(filePath, progress: { progress in + let destinationURL = try Zip.quickUnzipFile(filePath, progressCallback: { progress in XCTAssertFalse(progress.isNaN) }) addTeardownBlock { @@ -102,7 +102,7 @@ class ZipTests: XCTestCase { let filePath = url(forResource: "bb8", withExtension: "zip")! let destinationPath = try autoRemovingSandbox() - try Zip.unzipFile(filePath, destination: destinationPath, overwrite: true, password: "password", progress: nil) + try Zip.unzipFile(filePath, destination: destinationPath, overwrite: true, password: "password", progressCallback: nil) XCTAssertTrue(FileManager.default.fileExists(atPath: destinationPath.path)) }