从流中创建Zip文件并下载

50

我有一个 DataTable,想要将其转换为 XML,然后使用 DotNetZip 进行压缩。最后用户可以通过 Asp.Net 网页下载它。以下是我的代码:

    dt.TableName = "Declaration";

    MemoryStream stream = new MemoryStream();
    dt.WriteXml(stream);

    ZipFile zipFile = new ZipFile();
    zipFile.AddEntry("Report.xml", "", stream);
    Response.ClearContent();
    Response.ClearHeaders();
    Response.AppendHeader("content-disposition", "attachment; filename=Report.zip");

    zipFile.Save(Response.OutputStream);
    //Response.Write(zipstream);
    zipFile.Dispose();

压缩文件中的 XML 文件为空。


3
那么,哪一部分不起作用呢? :) - Rob
压缩文件中的 XML 文件为空。 - Navid Farhadi
8个回答

71

有两件事。首先,如果您保留当前的代码设计,则需要在将MemoryStream写入条目之前执行Seek()。

dt.TableName = "Declaration"; 

MemoryStream stream = new MemoryStream(); 
dt.WriteXml(stream); 
stream.Seek(0,SeekOrigin.Begin);   // <-- must do this after writing the stream!

using (ZipFile zipFile = new ZipFile())
{
  zipFile.AddEntry("Report.xml", "", stream); 
  Response.ClearContent(); 
  Response.ClearHeaders(); 
  Response.AppendHeader("content-disposition", "attachment; filename=Report.zip"); 

  zipFile.Save(Response.OutputStream); 
}

即使你保留这个设计,我建议使用一个using()从句来替代调用Dispose(),如我所示,并且在DotNetZip examples中描述的。在面对失败时,using()从句更加可靠。
现在你可能会想,为什么在调用AddEntry()之前需要在MemoryStream中寻找?原因是,AddEntry()被设计为支持那些传递了流并且位置很重要的调用者。在这种情况下,调用者需要从流中读取条目数据,使用流的当前位置。AddEntry()支持此操作。因此,在调用AddEntry()之前设置流中的位置。
但是,更好的选择是修改您的代码以使用接受WriteDelegate的overload of AddEntry()。它专门为将数据集添加到zip文件中而设计。您的原始代码将数据集写入内存流,然后在流上查找,并将流的内容写入zip。如果您只写入一次数据,那么使用WriteDelegate更快,更容易。代码如下:
dt.TableName = "Declaration"; 
Response.ClearContent(); 
Response.ClearHeaders(); 
Response.ContentType = "application/zip";
Response.AppendHeader("content-disposition", "attachment; filename=Report.zip"); 

using(Ionic.Zip.ZipFile zipFile = new Ionic.Zip.ZipFile())
{
    zipFile.AddEntry("Report.xml", (name,stream) => dt.WriteXml(stream) );
    zipFile.Save(Response.OutputStream); 
}

这将数据集直接写入zipfile中的压缩流中。非常高效!没有双缓冲。匿名委托在ZipFile.Save()时被调用。仅执行一次写入(+压缩)。


@Cheeso:zipFile.AddEntry没有这个重载! - Meysam Javadi
3
在DotNetZip库的v1.9版本中可以实现。我保证。请查看.chm文件。 - Cheeso

6
为什么你没有关闭MemoryStream,我会把它包装在一个using子句中,同样的方法也适用于zipFile。此外,dt我认为是一个DataTable...放入错误检查以查看是否有行,请参见下面的代码...

编辑:再次查看此内容后,我意识到使用了来自 Codeplex 的 Ionic.Ziplib,稍微更改了代码,不再使用 zipFile.Save(Response.OutputStream);,而是使用 zipFile.Save(stream);,其中使用了 MemoryStream 类的 stream 实例,并使用 Response.Write(stream); 将其写出。

编辑#2:感谢Cheeso + Mikael 指出了显而易见的缺陷——我错过了它,没有理解他们的评论,直到我意识到流已经结束...


有用的提示,但关键缺失点是在调用AddEntry()之前,原始代码需要使用Seek()将MemoryStream定位到开头。 - Cheeso
我之所以说这是真的,是因为当在原始(不起作用的)代码中调用AddEntry()时,MemoryStream上的光标位于EOS位置。为了使AddEntry()能够按预期成功读取流中的内容,它需要位于流的开头。AddEntry在读取之前不会在流中后退。调用者必须准备流并根据需要设置光标。 - Cheeso
就像 @cheeso 所说的那样。在将其写出之前,将 memstream 的位置设置为 0。 - Mikael Svenson
@Cheeso,@Mikael:啊啊啊啊啊!!!谢谢你们提醒我——我错过了那个问题,没错。你们两个都是对的!!!但是……如果流被定位回开头,那么dt.WriteXml(stream)不就变得无用了吗?也就是说,它会被覆盖掉……? - t0mm13b
dt.WriteXml()会写入流中。在调用它之后,位置位于流的末尾。从这样的流中读取将得不到任何内容。如果您然后寻回到位置0,并尝试读取流,您将获得通过WriteXml编写的所有内容。即使如此,这是将DataSet存储到ZIP文件中的错误方法。有一种更简单的方法,没有中间流。https://dev59.com/d3E95IYBdhLWcg3wi-ee#2267750 - Cheeso

1

好的。看起来我们在这里没有取得太大进展,所以你需要更深入地调试一下。

更新你的代码,执行以下操作:

dt.WriteXml(stream);
stream.Seek(0, SeekOrigin.Begin);
File.WriteAllBytes("c:\test.xml", stream.GetBuffer());

检查您是否有有效的XML文件。如果是,则继续执行相同的操作,检查您的ZipFile。将其保存到本地文件中。检查它是否存在,其中包含您的xml文件以及您的xml文件是否具有内容。

如果成功,请尝试将内存流作为响应发送回来,看看是否有效。

然后,您应该能够进一步跟踪问题。


你是正确的;在写入和读取之间,只需要在MemoryStream上执行Seek()操作即可。 - Cheeso

1
你有没有尝试在压缩之前刷新流?
dt.WriteXml(stream);
stream.Flush();
ZipFile zipFile = new ZipFile();

0
从流创建zip文件并下载。以下是代码。
FileStream stream=File.OpenRead(@"D:\FileDownLoad\DeskTop\1.txt");
MemoryStream MS=new MemoryStream();

ZipOutputStream zipOutputStream = new ZipOutputStream(MS);
zipOutputStream.SetLevel(9);
ZipEntry entry = new ZipEntry("1.txt");
zipOutputStream.PutNextEntry(entry);

byte[] buffer = new byte[stream.Length];
int byteRead = 0;

while ((byteRead = stream.Read(buffer, 0, buffer.Length)) > 0) 
    zipOutputStream.Write(buffer, 0, byteRead);

    zipOutputStream.IsStreamOwner = false;
    stream.Close();
    zipOutputStream.Close();
    MS.Position = 0;

    Response.ContentType = "application/application/octet-stream";
    Response.AppendHeader("content-disposition", "attachment; filename=\"Download.zip\"");
    Response.BinaryWrite(MS.ToArray());

0

这段代码将帮助您从流中下载文件。

using (var outStream = new MemoryStream())
{
    using (var archive = new ZipArchive(outStream, ZipArchiveMode.Create, true))
    {
        var fileInArchive = archive.CreateEntry("FileName.pdf", CompressionLevel.Optimal);
        using (var entryStream = fileInArchive.Open())
        using (WebResponse response = req.GetResponse())
        {
            using (var fileToCompressStream = response.GetResponseStream())
            {
                fileToCompressStream.CopyTo(entryStream);
            }
        }                       
    }
    using (var fileStream = new FileStream(@"D:\test.zip", FileMode.Create))
    {
        outStream.Seek(0, SeekOrigin.Begin);
        outStream.CopyTo(fileStream);
    }
}

所需命名空间:

using System.IO.Compression;
using System.IO.Compression.ZipArchive;

0

添加ContentType头:

Response.ContentType = "application/zip";

这将允许浏览器检测您正在发送的内容。


0

请仔细检查您要返回的流。在下面的示例中:

zipFile.Save(Response.OutputStream);
Response.Write(zipstream);
zipFile.Dispose();

你正在使用Save方法将zipFile保存到响应流中,但是你还在使用zipstream变量调用Response.Write()。那么zipstream是什么?请检查它是否为空流。

很抱歉,'Response.Write(zipstream);' 这行代码必须被注释掉。 - Navid Farhadi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接