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

ZipFS:在 ASP.NET 中使用 ZIP 文件作为虚拟目录或只读资源容器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.16/5 (12投票s)

2008年5月14日

CPOL

9分钟阅读

viewsIcon

52625

downloadIcon

410

一个库,可以在 ASP.NET 应用程序中像使用虚拟目录一样使用 ZIP 存档(包含图像、脚本文件、资源等)。

引言

在您的 ASP.NET 应用程序中,您可能有一组静态文件和目录,其中包含许多图像、脚本、资源文件。特别是具有插件支持的大型 JavaScript 库(如 TinyMCE)可能包含数百个文件。除非您升级到新版本的库,否则您可能永远不会修改这些文件中的任何一个。

当您创建一个使用相同库集的新项目时,您必须将整个目录和文件集复制到新的项目文件夹中(如果您想要简单的“复制部署”方式,您必须这样做),最终导致数千个重复文件污染您的硬盘驱动器。(如果您像我一样,并且使用类似 SVN 的版本控制系统(对我来说是必须的),那么在存储库方面您的工作会更容易,因为复制一个目录在 SVN 中只意味着创建一个链接,但这并不能免去在您的工作副本中拥有每个文件的全新本地副本。)

这就是我的解决方案建议:为什么不将这些库(或其他资源)保存在它们自己的 ZIP 文件中(因为它们中的大多数已经以 ZIP 文件形式分发),并在 ASP.NET 服务器上像使用该 ZIP 文件作为虚拟目录一样使用它们?(注意:“虚拟目录”在这里与 IIS 虚拟目录无关)

背景

首先,为什么选择 ZIP?

  • ZIP 是一种简单且标准的格式,几乎可以被任何存档软件或 Windows 本身处理。使用非标准格式(如虚拟文件系统)会使准备和修改所需文件更加困难。
  • 几乎所有的 JavaScript 库都以 ZIP 存档的形式提供(如果它们没有,您可以轻松地自己创建一个)。
  • 得益于强大而小巧的 Ionic .NET ZIP 库(DotNetZip,http://www.codeplex.com/DotNetZip),在 ASP.NET 应用程序中读取 ZIP 文件内容非常容易,无需任何第三方依赖(只需一个 35KB 的 DLL 文件就足够了)。

注意:在 1.3 版本之前,打开 ZIP 存档时,ZipFile 对象会将整个 ZIP 文件加载到内存中,但从 1.4 版本开始,通过使用流,它只加载文件头条目,这在内存方面更有效率,速度也更快。

我们对完成的系统的要求是:

  • ZIP 文件应该被视为虚拟目录,并且不应该在 IIS 端进行任何配置(因为我们可能无法访问 IIS 配置)。
  • 访问 ZIP 存档中的文件应该与直接访问物理文件一样快(特别是当文件以无压缩方式打包时。如果您关心性能和服务器负载,您可能希望使用无压缩选项)。
  • 尽管 DotNetZip 库只加载 ZIP 文件的文件头(而不是文件数据本身),但定位和读取这些信息需要一些时间。每次请求 ZIP 存档中的文件时,我们应该避免再次打开该 ZIP 文件并加载相同的头信息。因此,我们应该在第一次请求时打开 ZIP 文件并加载头信息,并在后续请求中通过缓存来重用这些信息。
  • 既然我们要缓存 ZIP 文件头,我们就应该找到一种方法来识别 ZIP 文件本身何时被修改(通过加载新的 ZIP 文件?),并自动更新我们的缓存(使用 ZIP 文件的最后修改时间、大小应该足够)。
  • 只要 ZIP 文件本身不变,我们就应该帮助客户端浏览器缓存文件,并尽可能减轻我们服务器的负载。
  • 在 ZIP 存档中引用文件应该尽可能简单。在此解决方案中,URL 如“http://www.site.com/zip.axd/js/tiny_mce.zip/license.txt”将等同于“http://www.site.com/js/tiny_mce.zip”存档中的“license.txt”(其中 zip.axd 是一个虚拟页面,将由我们将要开发的 IHttpHandler 实现来处理)。
  • 像 TinyMCE 这样的库会从其主 .js 文件所在的子文件夹加载其附加文件,因此解决方案不应该导致它们停止工作。例如,TinyMCE 在磁盘上的目录结构如下:
\js\tiny_mce\
    tiny_mce.js
    simple\
        editor_template.js
    advanced\
        editor_template.js
    plugins\
        advhdr\
            editor_plugin.js
    ...        

当 TinyMCE 的主文件 *tiny_mce.js* 被加载时,它会自己加载 *advanced\editor_template.js* 和 *plugins\advhdr\editor_plugin.js*。所以,如果我们使用基于查询字符串的系统来实现我们的虚拟文件系统,比如“http://www.site.com/zip.axd?zip=/js/tiny_mce.zip&file=tiny_mce.js”,那么浏览器将尝试从类似“http://www.site.com/advanced/editor_template.js”的位置加载 editor_template.js,因为浏览器会认为它已经从我们应用程序的根目录加载了 tiny_mce.js(因为 zip.axd 虚拟页面似乎位于根目录),而这将会失败。

所以我们将依赖 ASP.NET 一个有用的功能,称为 PathInfo。对于像“http://www.site.com/zip.axd/js/tiny_mce.zip/tiny_mce.js”这样的请求 URL,处理程序页面 URL 仍然是“http://www.site.com/zip.axd”,而 Request.PathInfo 将返回“/js/tiny_mce.zip/tiny_mce.js”。取其扩展名为“.zip”的部分,我们可以说我们要处理的 ZIP 存档是“/js/tiny_mce.zip”,而请求的文件是存档根目录下的“tiny_mce.js”。但这次浏览器认为 tiny_mce.js 位于“http://www.site.com/zip.axd/js/tiny_mce.zip/”,所以当它请求“advanced/editor_template.js”时,它将使用“http://www.site.com/zip.axd/js/tiny_mce.zip/advanced/editor_template.js”,而我们的虚拟页面(http://www.site.com/zip.axd)将愉快地处理该请求...

  • 还有一个可能令人困惑但非常实用的功能。我可能想修改存档中的一些文件,或者用其他内容完全替换它们,但不想每次都修改原始 ZIP 文件。我想将我的修改保存在一个特殊的文件夹中,并希望我的处理程序自动加载它们,而不是 ZIP 存档中的文件。例如,您只修改了 TinyMCE 发行版根目录中的 tiny_mce.js 文件。现在,而不是在“tiny_mce.zip”中重新存档它,您创建一个名为“tiny_mce”的文件夹,并将您修改过的“tiny_mce.js”放在那里。当请求“tiny_mce.zip”中的“tiny_mce.js”文件时,我们的处理程序将首先在该文件夹(没有“.zip”扩展名,与“.zip”文件在同一文件夹)下查找名为“tiny_mce”的文件夹,并搜索那里的“tiny_mce.js”文件。如果它存在(在我们的示例中是存在的),它将简单地返回其内容,并忽略存档中的内容。如果不存在,则回退到从 ZIP 文件本身加载。这对于跟踪您在基础库中所做更改的记录非常有帮助,并简化了补丁。

使用代码

我们的解决方案完全由一个 IHttpHandler 实现(Poligon.ZipFS.ZipFSHandler)组成,该实现处理以“~/zip.axd”开头的请求。此处理程序位于“Poligon.ZipFS.dll”文件中,该文件可在示例存档中找到。将其放置在您的网站 BIN 文件夹中,与 Ionic.Utils.Zip.dll 一起,并在您的 web.config 文件中“system.web\httpHandlers”部分下添加以下行:

<configuration>
  <system.web>
    <httpHandlers>
      <add verb="GET,HEAD" path="zip.axd" validate="false" 
         type="Poligon.ZipFS.ZipFSHandler"/>
    </httpHandlers>
  </system.web>
</configuration>        

如果您使用的是 ASP.NET 开发服务器而不是 IIS,它可能无法按预期工作,因为 ASP.NET 开发服务器对于带有路径信息的 URL 的行为与 IIS 不同。要解决此问题,请在 system.web\httpModules 部分添加以下行:

<configuration>
  <system.web>
    <httpModules>
      <add name="InternalServerFix" type="Poligon.ZipFS.InternalServerFix" />
    </httpModules>
  </system.web>
</configuration>       

请注意,第二个更改仅在使用 ASP.NET 开发服务器时需要。使用 IIS 6 时无需使用它(未在其他 IIS 版本上测试过...)

为了演示其工作原理,我创建了一个包含单个页面的示例网站(default.aspx)。该页面包含一个指向“beatiful.zip”中“index.html”的链接,该 ZIP 文件与该页面位于同一文件夹下。“Beatiful.zip”包含一个简单的网站模板,其中包含一些 html、css、gif、jpg 文件。因此,当您单击链接时,Beatiful Day 网站模板将直接从 zip 存档中启动。

让我们简要了解一下 Poligon.ZipFS 库中的类...

ZipFileCache 表示单个 ZIP 存档,其文件头条目缓存在内存中。它会自动检测底层 ZIP 存档中的更改(通过最后修改日期和大小),并在需要时重新加载头信息。它还维护一个 [文件名 -> zip 文件条目] 对的字典,以便通过文件名快速访问 zip 文件条目。您可以通过向其构造函数提供 ZIP 文件的绝对路径来创建它的一个实例。ZipFileCache 仅提供一个值得注意的公共函数:

public bool ExtractStream(string filePath, Stream stream)

给定 ZIP 存档中文件的名称,它首先检查 ZIP 存档自上次加载头信息以来是否已被修改,如果已修改则重新加载它们,然后将文件内容提取到给定的流中。如果在存档中找不到该名称的文件,则返回 false。

ZipFileCache 保持 ZIP 文件打开,但在提取期间仅锁定它进行写入。在没有提取操作进行时,可以安全地替换 ZIP 存档。

ZipFileCache 也是线程安全的,因为它通过缓存同步访问。我使用了 Jeffrey Richter(Wintellect)的 OneManyResourceLock 进行同步,它本质上是一个允许多个读取者和一个写入者访问的锁。当多个读取者可以同时访问资源时,它比互斥锁(C# lock 关键字)更有效。在我们的例子中,多个线程可以同时读取存档,但当存档被修改(这很少发生)并且需要重新加载时,其他线程应该等待重新加载完成。

请注意,filePath 应该使用正斜杠(而不是反斜杠)指定,例如“folder/subfolder/file.txt”。

ZipFSCache 只是 ZipFileCache 对象的静态集合,每个 ZIP 文件一个。它还允许对此集合进行线程安全的访问。当使用其 ExtractStream 函数首次访问 ZIP 文件时,它会创建一个 ZipFileCache 对象实例。

public static bool ExtractStream(string zipFilePath, 
    string filePath, Stream outputStream)

除了 ZipFileCache.ExtractStream 方法的两个参数外,它还需要第三个参数(第一个参数)。这是 ZIP 存档文件的完整路径(这次使用反斜杠...)。

ZipFSHandler 是我们的 IHttpHandler 实现,它处理以“~/zip.axd”开头的请求。它使用 ZipFSCache 从 ZIP 存档中提取文件,并且如果文件位于特殊位置(没有“.zip”扩展名的文件夹,如我之前解释的),它还可以选择直接从物理文件发送内容。

由于库代码文档记录良好,您可以对其进行检查以了解其工作原理。

关注点

ZipFSHandler 工作得很好,而且很简单,但可能比应该的更简单。有时,在不检查用户是否有权访问的情况下发送所有 ZIP 文件内容可能存在安全风险。因此,如果您有一些不想让所有人访问的 ZIP 文件,您可能需要以某种方式保护它们,或者为 ZipFSHandler 添加一些配置选项。我建议的一个简单方法是重命名您希望公开访问的 ZIP 文件为 .zipfs 扩展名,并修改 ZipFSHandler 以使用该扩展名而不是所有“.zip”文件。

它还保持 ZIP 文件打开,并且从不关闭它们。我认为,当成千上万的用户请求 ZIP 文件中的文件时,为每个请求重新打开它们会比为每个 ZIP 文件保持一个打开的文件句柄更慢,并消耗更多资源。您可以选择在每次提取后关闭文件...

历史

  • 2008 年 5 月 14 日 - 第一个版本在 CodeProject 上发布。
© . All rights reserved.