SimplePack 库






4.93/5 (30投票s)
一种将数据打包到单个文件中的简单方法。
引言
SimplePack 是一个用于将多个文件存储到一个文件中的库。SimplePack 在某些方面类似于普通的 TAR 归档文件,因为这两种归档文件都允许将目录结构存储到一个文件中而不进行压缩。SimplePack 提供了此基本功能
Archive.cs
- 将文件/目录添加到归档
- 从归档中删除文件/目录
- 从归档中提取文件/目录
- 以同步和异步方式执行此操作(带进度信息)
ArchiveInfo.cs
- 计算归档的基本统计信息
ArchiveFileStream.cs
- 只读文件流,提供对文件内容的直接读取,无需提取
注意: Archive.cs 一次只能执行一项异步操作。更改此行为会有些棘手,并会导致 Archive 本身性能下降。此类中的所有异步操作都带有后缀 Async
。
注意 2: SimplePack 源代码包含库的文档。
优点和缺点
优点
- 在不使用临时文件的情况下工作
- 同步和异步工作方式(无需额外线程)
- 无数据压缩(更快的归档/提取数据,可能直接访问特定文件)
缺点
- 无数据压缩
背景
在 simplePack 归档中,物理存储的只有文件和序列化的 Footer。归档中的目录结构仅由对象表示。这种分层结构序列化在归档的末尾。根“目录”由 Footer
类型的对象表示。Footer
类与 DirectoryArchive
类一样实现了 IArchiveStructure
接口。Footer
和 DirectoryArchive
之间存在一些差异(例如,Footer
没有父目录),因此 Footer
不继承 DirectoryArchive
类。Footer
和 DirectoryArchive
类中有两个集合。第一个集合包含 FileArchive
类的对象,这些对象是 Footer
或目录直接包含的。第二个集合包含 DirectoryArchive
类的对象(因此是当前目录或 Footer
的嵌套目录)。FileArchive
类保存有关存储在归档中的文件信息。FileArchive
和 DirectoryArchive
还包含有关原始文件或目录属性的附加信息。这些属性在提取文件或目录后会恢复。FileArchive
和 DirectoryArchive
还引用父 IArchiveStructure
对象(Footer
或 DirectoryArchive
)。正如您所见,分层结构由嵌套对象表示,因此递归经常用于遍历此结构,但我认为这不会减慢归档速度。无论如何,SimplePack 还存储了归档中所有文件的列表,以加快文件操作的速度(我之所以这样做,主要原因是:我太懒了,不愿意编写几个额外的递归方法)。删除的文件被未使用的空间(Gap)替换。文件按顺序存储在归档中,一个文件紧接着另一个文件。当不是这样时,两个文件之间会有一个间隙。有一种非常简单的方法可以检测归档中是否存在间隙。您还记得,我们仍然有一个归档中所有文件的列表,所以让我们按在归档中的起始位置(StartSeek
)对该列表进行排序,其余的只是一个简单的算法。存储在 Footer 中的数据(FileArchive
和 DirectoryArchive
类型的对象)包含有关 parentDirectory
的信息。如果使用 XML 序列化器,此信息不会序列化到 Footer
中,因为这样会产生循环。此信息是从现有的分层结构计算得出的。每个目录还存储大小。在添加或删除文件或目录后,此 Size
信息会一直向上更新到根(在这里,存储有关 parentDirectory
的信息也很重要)。
我将通过一些示例解释 SimplePack 的工作原理。演示的示例仅处理文件,处理目录是类比的。所有文件和目录信息都存储在 Archive 的 Footer
部分。Footer
实际上是 Footer
类的序列化对象。Footer
的默认序列化器是 XML 序列化器,但用户可以实现自己的序列化器并将其放入构造函数中。此序列化器必须实现 IFooterStorage
接口。仅当调用 Close()
方法时,Footer
才会写入 Archive。如果将属性 Atomic
设置为 TRUE
,此行为可以更改,那么在每次写入 Archive 的操作后,footer
将会被写入。这也会稍微减慢 Archive 的速度。您可以指定 SimpePack 归档中文件和目录的虚拟路径。可以将相同文件的多个副本存储在一起,但每个文件将存储在不同的虚拟目录中(这也将在示例中进行演示)。Archive 的根是 arch:\\
。您不能删除根目录(基本上可以,但之后您会遇到异常)。
添加文件
图 1.A:这是在新文件插入之前的归档。Footer 位于文件末尾。
图 1.B:Footer 已从归档中移除(Footer 仍在内存中),因为新文件将写入归档末尾
图 1.C:新文件已插入到归档末尾
图 1.D:更新后的 Footer 已写入归档文件末尾
注意: 归档最多只能有一个 Footer!
删除文件
图 2.A:删除文件之前的归档
图 2.B:被删除的文件被空白数据替换,Footer 已更新并写入文件末尾
注意: 归档大小保持不变(如果我们不考虑 Footer
记录的小变化)。通过这种方式从归档中删除文件不需要临时文件,因为被删除的文件会被空字节(0x00
)覆盖。SimplePack 提供了方法(同步方法,当然也包括异步方法)来移除 Archive 中的未使用空间。
移除归档中的未使用空间
从归档中删除文件后,会创建未使用空间。即使您将新文件添加到目录中,这些未使用空间也不会改变,因为数据会被碎片整理,这会导致整个 SimplePack 速度变慢。我决定实现 VacuumArchive
方法(或 VacuumArchiveAsync
),它与 SQLite 中的 vacuum
方法非常相似。未使用空间会被移到归档的末尾,然后归档会被截断。
图 3.A:调用 VacuumArchive
方法之前的带有 2 个未使用空间的归档
图 3.B:文件 2 被移到文件 1 的后面,因此未使用空间移到了文件 2 的后面
图 3.C:归档被截断为所有文件的总和
图 3.D:Footer 被写入归档末尾
注意: 正如您在此处看到的,Footer
没有改变,因为 Footer
不包含有关未使用空间的信息。
Using the Code
第一个示例演示了如何以同步方式使用基类 Archive
。
using(Archive simpelPackTestArchive = new Archive(@"c:\myTestArchive.smp"))
{
//open archive
simpelPackTestArchive.Open();
//add file into archive
simpelPackTestArchive.AddFile(@"c:\test1.txt",
@"arch:\nameOfFileInArchive"); //second path is virtual path in archive
simpelPackTestArchive.AddFile(@"c:\test1.txt",
@"arch:\nameOfFileInArchive2"); //different virtual path but same source file
simpelPackTestArchive.AddFile(@"c:\test2.txt",
@"arch:\archiveDirectory\myArchivedFile"); //file is placed into
//virtual directory which is automatically created
simpelPackTestArchive.AddFile(@"c:\test3.txt",
@"arch:\archiveDirectory\myArchivedFile2");
//add directory into archive
simpelPackTestArchive.AddDirectory(@"c:\testDirectory\",
@"arch:\someDirInsideArchive\Additional Directory\myArchivedDirectory\");
//archive virtual directory MUST ends with \
//removing files from archive, file is specified with virtual path
simpelPackTestArchive.RemoveFile
(@"arch:\nameOfFileInArchive2"); //unused space is created
simpelPackTestArchive.RemoveFile
(@"arch:\archiveDirectory\myArchivedFile2"); //unused space is created
//remove directory from archive
simpelPackTestArchive.RemoveDirectory
(@"arch:\someDirInsideArchive\"); //used parent^2 directory for
//removing directory
//simpelPackTestArchive.RemoveDirectory
(@"arch:\"); //this will leads to exception (Root path of archive is mandatory)
//vacuuming archive
simpelPackTestArchive.VacuumArchive(); //removing unused space in archive
} //here is archive closed and disposed
这是如何以异步方式使用基类 Archive
的演示。此演示基于普通的 Form
类。所有方法都有同步和异步版本。大多数异步方法还提供进度信息(除了 OpenAsync
和 VacuumArchiveAsync
)。有关更多信息,请参阅文档。
private void Form2_Load(object sender, EventArgs e)
{
testArchive = new Archive(@"c:\testArchive");
testArchive.OpenCompleted +=
(testArchive_OpenCompleted); //assign method which will be called
//when opening of archive is completed
testArchive.OpenAsync(); //calling asynchronous method
}
void testArchive_OpenCompleted(Archive sender,
SimplePack.Events.OpenCompletedEventArgs openCompletedEventArgs)
{
if (openCompletedEventArgs.Error == null)
{
//no error occurred during opening archive
MessageBox.Show("Archive was opened correctly");
} else {
//omg, some error happened
MessageBox.Show(openCompletedEventArgs.Error.Message);
}
}
private void Form2_FormClosing(object sender, FormClosingEventArgs e)
{
//prevent close form until operation is not done
if (!testArchive.IsBusy)
return;
e.Cancel = true;
MessageBox.Show("Can not close form, operation in progress");
}
如何获取 Archive
的基本统计信息。
ArchiveInfo archiveInfo = new ArchiveInfo
(@"c:\testArchive"); //create ArchiveInfo object,
//path to SimplePack Archive is parameter in constructor
MessageBox.Show(archiveInfo.BiggestFile.Length.ToString()); //read and display
//statistic information
如何直接从 Archive
读取数据(无需提取)
private void Form1_Load(object sender, EventArgs e)
{
using (Archive testArchive = new Archive
(@"c:\testArchive")) //create SimplePack archive
{
testArchive.Open(); //open archive properly
//create ArchiveFileStream where file
//arch:\myPicture.jpg will be accessible via Stream (READ ONLY!)
using (ArchiveFileStream archiveFileStream =
new ArchiveFileStream(testArchive, @"arch:\myPicture.jpg"))
{
Image testImage = new Bitmap
(archiveFileStream); //create image object from stream
pictureBox1.Image = testImage; //display image in pictureBox1
}
}
}
关注点
我创建这个库的原因是我需要将几个目录存储到一个文件中。提供异步方法使代码易于阅读,并且您只需几行代码就可以实现操作进度的显示。您甚至不需要了解有关线程的任何知识,并且不必在正确的线程中重新调用事件,因此最终代码更易于阅读。不支持数据压缩,并且 Archive
永远不会以这种方式扩展(但您可以,如果您愿意:)。我之所以这样做,是因为这个归档文件的主要用途是存储多媒体数据,而 zipping 会使源数据变得更大。SimplePack 仅受文件系统能力的限制。
基准测试
我为支持目录打包的 3 个库(SimplePack,用于 tar 打包的 ChilkatDotNet2,用于 zip 打包且压缩级别为 0 的 SharpZipLib)进行了一个简单的基准测试。测试程序创建了 20 次相同的归档,并计算了毫秒级的最小、最大和平均时间。测试目录包含 5230 个文件和 1004 个子目录,总大小为 237,799,409 字节。结果显示在以下图表中。对于 SimplePack,使用了最新版本 1.0.3,该版本具有自定义二进制序列化 Footer
,这当然比 XML 序列化更快,并且占用的资源更少。
历史
- 版本 1.0.3 - Footer 的自定义二进制序列化/反序列化
- 版本 1.0.2.2 – 已实现基本异常
ArchiveException
+ 一些小型改进 - 版本 1.0.2 – 已实现
ArchiveFileStream
- 版本 1.0.1 – 文件和目录属性已保留
- 版本 1.0.0 – 异步方法调用实现