web-dev-qa-db-ja.com

HttpContext OutputStreamを使用してZipArchiveに書き込む

.NET 4.5(System.IO.Compression.ZipArchive)に含まれる「新しい」ZipArchiveをASP.NETサイトで動作させるように努めています。しかし、HttpContext.Response.OutputStreamのストリームに書き込むのは好きではないようです。

次のコード例はスローされます

System.NotSupportedException:指定されたメソッドはサポートされていません

ストリームで書き込みが試行されるとすぐに。

ストリームのCanWriteプロパティはtrueを返します。

ローカルディレクトリを指すOutputStreamをfilestreamと交換すると、機能します。何ができますか?

ZipArchive archive = new ZipArchive(HttpContext.Response.OutputStream, ZipArchiveMode.Create, false);

ZipArchiveEntry entry = archive.CreateEntry("filename");

using (StreamWriter writer = new StreamWriter(entry.Open()))
{
    writer.WriteLine("Information about this package.");
    writer.WriteLine("========================");
}

スタックトレース:

[NotSupportedException: Specified method is not supported.]
System.Web.HttpResponseStream.get_Position() +29
System.IO.Compression.ZipArchiveEntry.WriteLocalFileHeader(Boolean isEmptyFile) +389
System.IO.Compression.DirectToArchiveWriterStream.Write(Byte[] buffer, Int32 offset, Int32 count) +94
System.IO.Compression.WrappedStream.Write(Byte[] buffer, Int32 offset, Int32 count) +41
36

注:これは.Net Core 2.0で修正されました。 .Net Frameworkの修正のステータスがどうなるかわかりません。


カルベルトフェレイラの回答にはいくつかの有用な情報がありますが、結論はほとんど間違っています。アーカイブを作成するには、シークは必要ありませんが、Positionを読み取ることができる必要があります。

ドキュメント によると、Positionの読み取りはシーク可能なストリームに対してのみサポートされるべきですが、ZipArchiveはシークできないストリームからでもこれを必要とするようです バグ

したがって、OutputStreamへのZipファイルの直接書き込みをサポートするために必要なことは、Streamの取得をサポートするカスタムPositionにラップすることです。何かのようなもの:

class PositionWrapperStream : Stream
{
    private readonly Stream wrapped;

    private int pos = 0;

    public PositionWrapperStream(Stream wrapped)
    {
        this.wrapped = wrapped;
    }

    public override bool CanSeek { get { return false; } }

    public override bool CanWrite { get { return true; } }

    public override long Position
    {
        get { return pos; }
        set { throw new NotSupportedException(); }
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        pos += count;
        wrapped.Write(buffer, offset, count);
    }

    public override void Flush()
    {
        wrapped.Flush();
    }

    protected override void Dispose(bool disposing)
    {
        wrapped.Dispose();
        base.Dispose(disposing);
    }

    // all the other required methods can throw NotSupportedException
}

これを使用して、次のコードはZipアーカイブをOutputStreamに書き込みます。

using (var outputStream = new PositionWrapperStream(Response.OutputStream))
using (var archive = new ZipArchive(outputStream, ZipArchiveMode.Create, false))
{
    var entry = archive.CreateEntry("filename");

    using (var writer = new StreamWriter(entry.Open()))
    {
        writer.WriteLine("Information about this package.");
        writer.WriteLine("========================");
    }
}
45
svick

2014年2月2日のsvickの回答の改良。Stream抽象クラスのメソッドとプロパティをさらに実装し、posメンバーを長く宣言する必要があることがわかりました。その後、それは魅力のように働きました。私はこのクラスを広範囲にテストしていませんが、HttpResponseでZipArchiveを返す目的で機能します。私はSeek and Readを正しく実装していると思いますが、調整が必要な場合があります。

class PositionWrapperStream : Stream
{
    private readonly Stream wrapped;

    private long pos = 0;

    public PositionWrapperStream(Stream wrapped)
    {
        this.wrapped = wrapped;
    }

    public override bool CanSeek
    {
        get { return false; }
    }

    public override bool CanWrite
    {
        get { return true; }
    }

    public override long Position
    {
        get { return pos; }
        set { throw new NotSupportedException(); }
    }

    public override bool CanRead
    {
        get { return wrapped.CanRead; }
    }

    public override long Length
    {
        get { return wrapped.Length; }
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        pos += count;
        wrapped.Write(buffer, offset, count);
    }

    public override void Flush()
    {
        wrapped.Flush();
    }

    protected override void Dispose(bool disposing)
    {
        wrapped.Dispose();
        base.Dispose(disposing);
    }

    public override long Seek(long offset, SeekOrigin Origin)
    {
        switch (Origin)
        {
            case SeekOrigin.Begin:
                pos = 0;
                break;
            case SeekOrigin.End:
                pos = Length - 1;
                break;
        }
        pos += offset;
        return wrapped.Seek(offset, Origin);
    }

    public override void SetLength(long value)
    {
        wrapped.SetLength(value);
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        pos += offset;
        int result = wrapped.Read(buffer, offset, count);
        pos += count;
        return result;
    }
}
6
flayman

コードの適応を MSDNページ に示されているバージョンと比較すると、ZipArchiveMode.Createが使用されないことがわかります。使用されているのはZipArchiveMode.Updateです。

それにもかかわらず、主な問題は、更新モードでZipArchiveに必要なRead and SeekをサポートしないOutputStreamです

モードを更新に設定する場合、基になるファイルまたはストリームは、読み取り、書き込み、およびシークをサポートする必要があります。アーカイブ全体の内容はメモリに保持され、アーカイブが破棄されるまで、基になるファイルまたはストリームにデータは書き込まれません。

ソース:MSDN

次のように書くだけでよいので、作成モードで例外が発生することはありません。

モードをCreateに設定すると、基になるファイルまたはストリームは書き込みをサポートする必要がありますが、シークをサポートする必要はありません。アーカイブ内の各エントリは、書き込みのために一度だけ開くことができます。単一のエントリを作成する場合、データは利用可能になるとすぐに、基になるストリームまたはファイルに書き込まれます。 CreateFromDirectoryメソッドを呼び出すなどして複数のエントリを作成する場合、すべてのエントリが作成された後で、基になるストリームまたはファイルにデータが書き込まれます。

ソース:MSDN

Zipファイルはネットワークストリームであり、シークがサポートされていないため、OutputStreamで直接作成できないと思います。

ストリームはシークをサポートできます。シークとは、ストリーム内の現在の位置を照会および変更することを指します。シーク機能は、ストリームが持つバッキングストアの種類によって異なります。たとえば、ネットワークストリームには現在の位置に関する統一された概念がないため、通常はシークをサポートしません。

別の方法としては、メモリストリームに書き込み、次にOutputStream.Writeメソッドを使用してZipファイルを送信することもできます。

MemoryStream ZipInMemory = new MemoryStream();

    using (ZipArchive UpdateArchive = new ZipArchive(ZipInMemory, ZipArchiveMode.Update))
    {
        ZipArchiveEntry Zipentry = UpdateArchive.CreateEntry("filename.txt");

        foreach (ZipArchiveEntry entry in UpdateArchive.Entries)
        {
            using (StreamWriter writer = new StreamWriter(entry.Open()))
            {
                writer.WriteLine("Information about this package.");
                writer.WriteLine("========================");
            }
        }
    }
    byte[] buffer = ZipInMemory.GetBuffer();
    Response.AppendHeader("content-disposition", "attachment; filename=Zip_" + DateTime.Now.ToString() + ".Zip");
    Response.AppendHeader("content-length", buffer.Length.ToString());
    Response.ContentType = "application/x-compressed";
    Response.OutputStream.Write(buffer, 0, buffer.Length);

EDIT:コメントからのフィードバックとさらに読むと、大きなZipファイルを作成している可能性があるため、メモリストリームが問題を引き起こす可能性があります。

この場合、Webサーバー上にZipファイルを作成し、Response.WriteFileを使用してファイルを出力することをお勧めします。

3
Carlos Ferreira

おそらくこれはMVCアプリではなく、FileStreamResultクラスを簡単に使用できます。

私は現在、これをZipArchiveを使用して作成されたMemoryStreamで使用しているので、動作することがわかります。

それを念頭に置いて、FileStreamResult.WriteFile()メソッドを見てください。

protected override void WriteFile(HttpResponseBase response)
{
    // grab chunks of data and write to the output stream
    Stream outputStream = response.OutputStream;
    using (FileStream)
    {
        byte[] buffer = newbyte[_bufferSize];
        while (true)
        {
            int bytesRead = FileStream.Read(buffer, 0, _bufferSize);
            if (bytesRead == 0)
            {
                // no more data
                break;
            }
            outputStream.Write(buffer, 0, bytesRead);
        }
    }
}

CodePlexのFileStreamResult全体

これがZipArchiveを生成して返す方法です。
FSRを上記のWriteFileメソッドの根性で置き換えても問題はありません。ここで、FileStreamは以下のコードからresultStreamになります。

var resultStream = new MemoryStream();

using (var zipArchive = new ZipArchive(resultStream, ZipArchiveMode.Create, true))
{
    foreach (var doc in req)
    {
        var fileName = string.Format("Install.Rollback.{0}.v{1}.docx", doc.AppName, doc.Version);
        var xmlData = doc.GetXDocument();
        var fileStream = WriteWord.BuildFile(templatePath, xmlData);

        var docZipEntry = zipArchive.CreateEntry(fileName, CompressionLevel.Optimal);
        using (var entryStream = docZipEntry.Open())
        {
            fileStream.CopyTo(entryStream);
        }
    }
}
resultStream.Position = 0;

// add the Response Header for downloading the file
var cd = new ContentDisposition
    {
        FileName = string.Format(
            "{0}.{1}.{2}.{3}.Install.Rollback.Documents.Zip",
            DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, (long)DateTime.Now.TimeOfDay.TotalSeconds),
        // always Prompt the user for downloading, set to true if you want 
        // the browser to try to show the file inline
        Inline = false,
    };
Response.AppendHeader("Content-Disposition", cd.ToString());

// stuff the Zip package into a FileStreamResult
var fsr = new FileStreamResult(resultStream, MediaTypeNames.Application.Zip);    
return fsr;

最後に、大規模なストリーム(または任意の時点でより多くのストリーム)を書き込む場合は、匿名パイプを使用してZipファイルの基になるストリームに書き込んだ直後に、データを出力ストリームに書き込みます。サーバー上のメモリにすべてのファイルの内容を保持するためです。 この回答の終わり 同様の質問に、それを行う方法の素晴らしい説明があります。

0
JoeBrockhaus

サーバー側のファイルを圧縮してOutputStream経由で送信するためのsvickの簡易バージョン:

using (var outputStream = new PositionWrapperStream(Response.OutputStream))
using (var archive = new ZipArchive(outputStream, ZipArchiveMode.Create, false))
{
    var entry = archive.CreateEntryFromFile(fullPathOfFileOnDisk, fileNameAppearingInZipArchive);
}

(これが明白に思われる場合は、私にはわかりませんでした!)

0
Ed Graham