65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 原生多文件压缩

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (16投票s)

2012 年 1 月 12 日

CPOL

5分钟阅读

viewsIcon

38493

downloadIcon

2070

.NET 原生实现的多文件、可搜索、流式压缩库。

引言

.NET 内置的压缩库有一个精妙的 Deflate 算法实现。它通过使用 Stream 提供了简单的压缩和解压缩工具。

如果你的应用程序需要与使用压缩的网站进行通信,或者你想压缩单个文件,这些工具都非常有用。

但是,如果你想将多个文件压缩到一个存档中,要么需要做大量工作,要么需要使用第三方库。

我最近遇到一个情况,需要将许多小文件压缩到一个文件中——我当时也无法使用第三方库。

这给我留下了唯一的选择:在 .NET 中完全实现自己的存档文件系统。我需要能够搜索存档中的文件并单独提取它们。我不必使用其他系统来打开存档,因此我对任何现有文件格式都没有依赖,这让我可以自由地开发自己的格式。

现在我有一些空闲时间,我想分享一下我的成果。

背景

我的第一个想法是创建一个类,其中包含一个 ArchiveFile 对象列表。ArchiveFile 对象将包含文件的详细信息,以及一个包含文件内容的字节数组。我将通过压缩流对类进行序列化来存储和压缩数据。

我甚至实现了这个解决方案(我将其保留在库中,作为 TinyArchive 类)——我本打算使用它,但意识到它有一个致命的缺陷:整个对象和所有未压缩的数据都需要加载到内存中。这为存档的最大尺寸设定了明确的限制。

我重新回到绘图板。我不能使用我编写的压缩二进制序列化代码来处理文件结构,因为它无法选择性地加载存档文件的部分。我需要自己创建。

我希望能够只读取存档中文件的详细信息,然后使用这些详细信息来选择性地读取文件的特定部分。我还意识到,我不能将所有索引放在文件开头,因为这将使向存档添加更多文件变得非常困难。

我决定从两个字节开始,这两个字节包含存档下一部分的长度。下一部分将是压缩文件的详细信息:文件名和在存档中的长度,然后紧接着是文件的压缩内容。

下一个文件将以完全相同的方式添加。代码将读取前两个字节,将其转换为 ushort,并使用该值来指定从存档中读取的下一组字节的长度,这些字节是索引详细信息。从索引中,它获取压缩数据块的长度,然后使用该长度跳到下一个索引。

通过这种方式,读取器可以非常快速地编目整个存档。它会构建一个可搜索的索引,该索引指定了压缩到存档中的每个文件的起始字节索引号和长度。

提取压缩文件只需打开存档文件的文件流,定位到要提取的文件的索引位置,然后读取正确的字节数。然后,通过 DeflateStream 类将压缩字节解压缩回其原始大小。

第二个最困难的部分是从存档中删除文件。这对于我最初的应用程序来说并不是必需的,但我认为为了使库完整,这是必需的。

我在这个上面遇到了很多麻烦,而且我仍然对我的解决方案不完全满意。删除方法基本上是在临时文件位置创建当前存档的副本,跳过被告知要删除的文件。然后删除原始文件并将副本移到其位置。

示例应用程序

NativeFileArchive2/ScreenShot.png

在附带的 Visual Studio 项目中提供了一个演示存档表单应用程序。这是一个快速、基本的多文件存档实现。您可以创建存档、向其中添加文件、提取它们以及删除它们。您可以使用树视图控件在存档中的文件之间导航。

Using the Code

处理存档的主要类是 StreamingArchiveFile

下面的代码展示了如何创建一个新的存档,并将一个文件夹的文件添加到其中。

// create a new or access an existing streaming archive file:
StreamingArchiveFile arc = new StreamingArchiveFile(@"C:\Temp\streamingArchive.arc");

// now iterate through the files in a specific directory and add them to the archive:
foreach (string fileName in Directory.GetFiles(@"C:\Temp\Test\", "*.*"))
{
    // add the file
    arc.AddFile(new ArchiveFile(fileName));
}

提取文件:这段代码展示了如何枚举存档中的文件,将它们提取到 temp 文件夹,然后打开它们。

// open the existing archive file:
StreamingArchiveFile arc = new StreamingArchiveFile(@"C:\Temp\streamingArchive.arc");

// iterate the files in the archive:
foreach (string fileName in arc.FileIndex.IndexedFileNames)
{
    // write the name of the file
    Debug.Print("File: " + fileName);

    // extract the file:
    ArchiveFile file = arc.GetFile(fileName);

    // save it to disk:
    String tempFileName = Path.GetTempPath() + "\\" + file.Name;
    file.SaveAs(tempFileName);

    // open the file:
    Process.Start(tempFileName);
}

还有一个使用正则表达式和 LINQ 在存档中查找文件的方法。

/// <summary>
/// search the index for a file matching the expression.
/// </summary>
/// <param name="fileNameExpression"></param>
/// <returns></returns>
public IEnumerable<ArchiveFileIndex> Search(String fileNameExpression)
{
    return (from file in _fileIndex
            where Regex.IsMatch(file.FileName, fileNameExpression)
            select file);
}

要真正了解存档的用途,请查看 FrmStreamingArchiveUI 表单中的代码。

关注点

.NET 提供了两个压缩类:GZipStreamDeflateStream。它们都使用 deflate 算法,GZipStream 类实际上是建立在 DeflateStream 之上的,并添加了一些额外的头部信息和 CRC 校验。

我使用了 DeflateStream 类,因为它更快,占用的空间更小。

文件以 ArchiveFile 对象的格式存储在存档中。该对象存储文件的属性,如创建日期和大小,以及一个包含文件实际内容的字节数组。

ArchiveFile 对象使用 TinySerializer 类序列化到存档中。这是为了产生最小的类序列化。它有一个可选的自定义 SerializationBinder,可以去除 AssemblyNameTypeName(这意味着你要序列化的对象必须只包含简单的对象作为字段或属性),并且它可以与 Deflate 流类进行序列化或反序列化,以提供快速简便的压缩/解压缩。

/// <summary>
/// deserialize an object from compressed data.
/// </summary>
/// <typeparam name="T">the type of object to deserialize</typeparam>
/// <param name="compressedInputStream">stream of compressed
///         data containing an object to deserialize</param>
/// <returns></returns>
public static T DeSerializeCompressed<T>(Stream compressedInputStream, bool useCustomBinder = false)
{
    // construct the binary formatter and assign the custom binder:
    BinaryFormatter formatter = new BinaryFormatter();
    if (useCustomBinder)
        formatter.Binder = new TinySerializer(typeof(T));
            
    // read the stream through a GZip decompression stream.
    using (DeflateStream decompressionStream = 
           new DeflateStream(compressedInputStream, CompressionMode.Decompress, true))
    {
        // deserialize to an object:
        object graph = formatter.Deserialize(decompressionStream);

        // check the type is correct and return.
        if (graph is T)
            return (T)graph;
        else
            throw new ArgumentException("Invalid Type!");
    }
}
© . All rights reserved.