注意

这是 weichsel 的 ZIPFoundation 的一个分支,有两个目标:

  1. 通过简单地将 zlib C 源代码作为包依赖项提供,从而消除对系统安装的 zlib 的需求,增加对非 Apple 操作系统的可移植性。
  2. 通过移除底层 POSIX/Linux 系统调用(例如 fopen、funopen 等),并用 Foundation 的高级 FileHandle 替换它们,以及(对于内存中的归档文件)为 Data 编写文件句柄包装器,从而增加对非 POSIX/Linux 系统(目前仅测试 Windows)的兼容性。

我尽可能保留了原始测试,并在测试套件中添加了 Windows 环境。 假设可能存在损坏的东西,但就我的用例而言,它似乎至少在 macOS、Linux 和 Windows 上与原始 ZIPFoundation 一样稳定。

以下是原始仓库中修改后的自述文件。

ZIPFoundation

Swift Package Manager compatible Platform

ZIP Foundation 是一个用于创建、读取和修改 ZIP 归档文件的库。
它用 Swift 编写,并且基于 Apple 的 libcompression,以实现高性能和高能效。
要了解有关该框架的性能特征的更多信息,您可以阅读这篇博客文章

特性

安装

Swift Package Manager

Swift Package Manager 是一个与 Swift 构建系统集成的依赖项管理器。要了解如何为您的项目使用 Swift Package Manager,请阅读官方文档
要将 ZIP Foundation 添加为依赖项,您必须将其添加到 Package.swift 文件的 dependencies 中,并在您的 target 中引用该依赖项。

// swift-tools-version:5.7
import PackageDescription
let package = Package(
    name: "<Your Product Name>",
    dependencies: [
		.package(url: "https://github.com/gregcotten/ZIPFoundationModern", .upToNextMajor(from: "0.0.1")),
    ],
    targets: [
        .target(
		name: "<Your Target Name>",
		dependencies: [
			.product(name: "ZIPFoundation", package: "ZIPFoundationModern")
		]),
    ]
)

添加依赖项后,您可以使用以下命令获取库:

$ swift package resolve

用法

ZIP Foundation 提供了两种高级方法来压缩和解压缩项目。 两者都实现为 FileManager 的扩展。
这些方法的功能模仿了 macOS 中“归档实用程序”的行为。

压缩文件和目录

要压缩单个文件,您只需传递一个表示要压缩的项的文件 URL 和一个目标 URL 到 FileManager.zipItem(at sourceURL: URL, to destinationURL: URL)

let fileManager = FileManager()
let currentWorkingPath = fileManager.currentDirectoryPath
var sourceURL = URL(fileURLWithPath: currentWorkingPath)
sourceURL.appendPathComponent("file.txt")
var destinationURL = URL(fileURLWithPath: currentWorkingPath)
destinationURL.appendPathComponent("archive.zip")
do {
    try fileManager.zipItem(at: sourceURL, to: destinationURL)
} catch {
    print("Creation of ZIP archive failed with error:\(error)")
}

默认情况下,创建的归档文件没有任何压缩。 要创建压缩的 ZIP 归档文件,必须将可选的 compressionMethod 参数设置为 .deflate
相同的方法也接受表示目录项的 URL。 在这种情况下,zipItemsourceURL 的目录内容添加到归档文件中。
默认情况下,一个以 sourceURLlastPathComponent 命名的根目录条目会被添加到目标归档文件中。 如果您不想在归档文件中保留源的父目录,则可以传递 shouldKeepParent: false

解压归档文件

要解压缩现有归档文件,您可以使用 FileManager.unzipItem(at sourceURL: URL, to destinationURL: URL)
这会将归档文件中的所有条目递归提取到目标 URL

let fileManager = FileManager()
let currentWorkingPath = fileManager.currentDirectoryPath
var sourceURL = URL(fileURLWithPath: currentWorkingPath)
sourceURL.appendPathComponent("archive.zip")
var destinationURL = URL(fileURLWithPath: currentWorkingPath)
destinationURL.appendPathComponent("directory")
do {
    try fileManager.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil)
    try fileManager.unzipItem(at: sourceURL, to: destinationURL)
} catch {
    print("Extraction of ZIP archive failed with error:\(error)")
}

高级用法

ZIP Foundation 还允许您单独访问特定的条目,而无需提取整个归档文件。 此外,它还具有增量更新归档文件内容的功能。

访问单个条目

要访问特定的 ZIP 条目,您必须使用表示现有归档文件的文件 URL 初始化一个 Archive 对象。 完成此操作后,可以通过它们的相对路径检索条目。 Archive 符合 Sequence 协议,因此支持下标访问

let fileManager = FileManager()
let currentWorkingPath = fileManager.currentDirectoryPath
var archiveURL = URL(fileURLWithPath: currentWorkingPath)
archiveURL.appendPathComponent("archive.zip")
guard let archive = Archive(url: archiveURL, accessMode: .read) else  {
    return
}
guard let entry = archive["file.txt"] else {
    return
}
var destinationURL = URL(fileURLWithPath: currentWorkingPath)
destinationURL.appendPathComponent("out.txt")
do {
    try archive.extract(entry, to: destinationURL)
} catch {
    print("Extracting entry from archive failed with error:\(error)")
}

extract 方法接受可选参数,允许您控制压缩和内存消耗。
您可以在该方法的文档中找到有关这些参数的详细信息。

创建归档文件

要创建一个新的 Archive,传入一个不存在的文件 URL 和 AccessMode.create

let currentWorkingPath = fileManager.currentDirectoryPath
var archiveURL = URL(fileURLWithPath: currentWorkingPath)
archiveURL.appendPathComponent("newArchive.zip")
guard let archive = Archive(url: archiveURL, accessMode: .create) else  {
    return
}

添加和删除条目

您可以将条目添加到已使用 .create.update AccessMode 打开的归档文件,或从中删除条目。 要从现有文件中添加条目,您可以将相对路径和基本 URL 传递给 addEntry。 相对路径标识 ZIP 归档文件中的条目。 相对路径和基本 URL 必须形成一个绝对文件 URL,该 URL 指向您要添加到归档文件中的文件

let fileManager = FileManager()
let currentWorkingPath = fileManager.currentDirectoryPath
var archiveURL = URL(fileURLWithPath: currentWorkingPath)
archiveURL.appendPathComponent("archive.zip")
guard let archive = Archive(url: archiveURL, accessMode: .update) else  {
    return
}
var fileURL = URL(fileURLWithPath: currentWorkingPath)
fileURL.appendPathComponent("file.txt")
do {
    try archive.addEntry(with: fileURL.lastPathComponent, relativeTo: fileURL.deletingLastPathComponent())
} catch {
    print("Adding entry to ZIP archive failed with error:\(error)")
}

或者,可以使用 addEntry(with path: String, fileURL: URL) 方法添加共享公共基本目录的文件。 fileURL 参数必须包含一个绝对文件 URL,该 URL 指向任意文件系统位置上的文件、符号链接或目录。

addEntry 方法接受几个可选参数,允许您控制压缩、内存消耗和文件属性。
您可以在该方法的文档中找到有关这些参数的详细信息。

要删除条目,您需要引用归档文件中的条目,然后将其传递给 removeEntry

guard let entry = archive["file.txt"] else {
    return
}
do {
    try archive.remove(entry)
} catch {
    print("Removing entry from ZIP archive failed with error:\(error)")
}

基于闭包的读取和写入

ZIP Foundation 还允许您使用 ZIP 条目内容,而无需将其写入文件系统。 extract 方法接受 Consumer 类型的闭包。 在提取过程中,会调用此闭包,直到条目的内容耗尽为止

try archive.extract(entry, consumer: { (data) in
    print(data.count)
})

传递到闭包中的 data 包含当前条目的块。 您可以通过提供可选的 bufferSize 参数来控制条目的块大小。

您还可以从内存中的数据源添加条目。 为此,您必须向 addEntry 方法提供 Provider 类型的闭包

let string = "abcdefghijkl"
guard let data = string.data(using: .utf8) else { return }
try? archive.addEntry(with: "fromMemory.txt", type: .file, uncompressedSize: UInt64(data.count), bufferSize: 4, provider: { (position, size) -> Data in
    // This will be called until `data` is exhausted (3x in this case).
    return data.subdata(in: position..<position+size)
})

调用闭包,直到提供了足够的数据以创建 uncompressedSize 的条目。 闭包接收 positionsize 参数,以便您可以管理数据源的状态。

内存中的归档文件

除了基于闭包的文件归档文件的读取和写入之外,ZIP Foundation 还提供处理内存中归档文件的功能。 这允许创建或提取仅驻留在 RAM 中的归档文件。 此功能的一个用例是动态创建稍后发送到客户端的 ZIP 归档文件 - 无需执行任何磁盘 IO。

要使用内存中的归档文件,必须使用 init(data: Data, accessMode: AccessMode) 初始化器。
读取更新内存中的归档文件,传入的 data 必须包含有效 ZIP 归档文件的表示形式。
创建内存中的归档文件,可以省略 data 参数

let string = "Some string!"
guard let archive = Archive(accessMode: .create),
        let data = string.data(using: .utf8) else { return }
    try? archive.addEntry(with: "inMemory.txt", type: .file, uncompressedSize: UInt64(data.count), bufferSize: 4, provider: { (position, size) -> Data in
        return data.subdata(in: position..<position+size)
    })
let archiveData = archive.data

进度跟踪和取消

所有 Archive 操作都接受可选的 progress 参数。 通过传入 Progress 的实例,您可以表明要跟踪当前 ZIP 操作的进度。 ZIP Foundation 会自动配置 progress 对象的 totalUnitCount,并持续更新其 completedUnitCount
要获取有关当前操作已完成工作的通知,您可以将键值观察器附加到 progress 对象的 fractionCompleted 属性。
ZIP Foundation FileManager 扩展方法也接受可选的 progress 参数。 zipItemunzipItem 都自动创建进度对象层次结构,这些对象反映目录中包含的所有项目或包含多个项目的归档文件的进度。

可以使用 Progresscancel() 方法终止未完成的 ZIP 操作。 如果取消,当前操作将抛出 ArchiveError.cancelledOperation 异常。

鸣谢

此分支 ZIPFoundationModernGreg Cotten 适配和维护。
原始的 ZIPFoundation 包由 Thomas Zoechling 编写和维护。

许可证

ZIP Foundation 在 MIT 许可证下发布。
有关详细信息,请参阅 LICENSE