web-dev-qa-db-ja.com

C#少ないメモリ消費でサーバーから大きなファイルをダウンロードする

メモリサイズが42 MBの大きなファイルがあります。少ないメモリ消費量でファイルをダウンロードしたい。
コントローラーコード

public ActionResult Download()
{
    var filePath = "file path in server";
    FileInfo file = new FileInfo(filePath);
    Response.ContentType = "application/Zip";                        
    Response.AppendHeader("Content-Disposition", "attachment; filename=folder.Zip");                   
    Response.TransmitFile(file.FullName);
    Response.End(); 
}

Streamで試した代替方法

public ActionResult Download()
{           
    string failure = string.Empty;
    Stream stream = null;
    int bytesToRead = 10000;


    long LengthToRead;
    try
    {
        var path = "file path from server";
        FileWebRequest fileRequest = (FileWebRequest)FileWebRequest.Create(path);
        FileWebResponse fileResponse = (FileWebResponse)fileRequest.GetResponse();

        if (fileRequest.ContentLength > 0)
            fileResponse.ContentLength = fileRequest.ContentLength;

        //Get the Stream returned from the response
        stream = fileResponse.GetResponseStream();

        LengthToRead = stream.Length;

        //Indicate the type of data being sent
        Response.ContentType = "application/octet-stream";

        //Name the file 
        Response.AddHeader("Content-Disposition", "attachment; filename=SolutionWizardDesktopClient.Zip");
        Response.AddHeader("Content-Length", fileResponse.ContentLength.ToString());

        int length;
        do
        {
            // Verify that the client is connected.
            if (Response.IsClientConnected)
            {
                byte[] buffer = new Byte[bytesToRead];

                // Read data into the buffer.
                length = stream.Read(buffer, 0, bytesToRead);

                // and write it out to the response's output stream
                Response.OutputStream.Write(buffer, 0, length);

                // Flush the data
                Response.Flush();

                //Clear the buffer
                LengthToRead = LengthToRead - length;
            }
            else
            {
                // cancel the download if client has disconnected
                LengthToRead = -1;
            }
        } while (LengthToRead > 0); //Repeat until no data is read

    }
    finally
    {
        if (stream != null)
        {
            //Close the input stream                   
            stream.Close();
        }
        Response.End();
        Response.Close();
    }
    return View("Failed");
}

ファイルのサイズのため、パフォーマンスの問題につながるより多くのメモリを消費しています。
iisログをチェックした後、ダウンロードプロセスはそれぞれ42 mbと64 mbを使用しています。
前もって感謝します

6
anand

より良いオプションは、ActionResultの代わりにFileResultを使用することです:

この方法を使用すると、提供する前にファイル/バイトをメモリにロードする必要がありません。

public FileResult Download()
{
     var filePath = "file path in server";
     return new FilePathResult(Server.MapPath(filePath), "application/Zip");
}

編集:ファイルが大きい場合、FilePathResultも失敗します。

あなたの最善の策はおそらくResponse.TransmitFile()です。これを大きなファイル(GB)で使用したことがあり、以前は問題がなかった

public ActionResult Download()
{

    var filePath = @"file path from server";

    Response.Clear();
    Response.ContentType = "application/octet-stream";
    Response.AppendHeader("Content-Disposition", "filename=" + filePath);

    Response.TransmitFile(filePath);

    Response.End();

    return Index();
}

MSDNから:

メモリにバッファリングせずに、指定したファイルを直接HTTP応答出力ストリームに書き込みます。

23
Janus Pienaar

同様の問題がありましたが、ローカルディスクにファイルがなく、APIからダウンロードする必要がありました(私のMVCはプロキシのようなものでした)。重要なことは、MVCアクションにResponse.Buffer=false;を設定することです。 @JanusPienaarの最初のソリューションはこれで動作するはずです。私のMVCアクションは:

public class HomeController : Controller
{
    public async Task<FileStreamResult> Streaming(long RecordCount)
    {
        HttpClient Client;
        System.IO.Stream Stream;

        //This is the key thing
        Response.Buffer=false;

        Client = new HttpClient() { BaseAddress=new Uri("http://MyApi", };
        Stream = await Client.GetStreamAsync("api/Streaming?RecordCount="+RecordCount);
        return new FileStreamResult(Stream, "text/csv");
    }
}

そして、私のテストWebApi(ファイルを生成します)は次のとおりです。

public class StreamingController : ApiController
{
    // GET: api/Streaming/5
    public HttpResponseMessage Get(long RecordCount)
    {
        var response = Request.CreateResponse();

        response.Content=new PushStreamContent((stream, http, transport) =>
        {
            RecordsGenerator Generator = new RecordsGenerator();
            long i;

            using(var writer = new System.IO.StreamWriter(stream, System.Text.Encoding.UTF8))
            {
                for(i=0; i<RecordCount; i++)
                {
                    writer.Write(Generator.GetRecordString(i));

                    if(0==(i&0xFFFFF))
                        System.Diagnostics.Debug.WriteLine($"Record no: {i:N0}");
                    }
                }
            });

            return response;
        }

        class RecordsGenerator
        {
            const string abc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
            char[] Chars = new char[14];//Ceiling(log26(2^63))

            public string GetRecordString(long Record)
            {
                int iLength = 0;
                long Div = Record, Mod;

                do
                {
                    iLength++;
                    Div=Math.DivRem(Div, abc.Length, out Mod);
                    //Save from backwards
                    Chars[Chars.Length-iLength]=abc[(int)Mod];
                }
                while(Div!=0);

                return $"{Record} {new string(Chars, Chars.Length-iLength, iLength)}\r\n";
            }
        }
    }
}

RecordCountが100000000の場合、TestApiによって生成されるファイルは1.56 GBです。 WebApiもMVCもそれほど多くのメモリを消費しません。

Transfer-Encodingヘッダーをチャンクに設定し、PushStreamContentを使用してHttpResponseMessageを返します。チャンクのTransfer-Encodingは、HTTP応答にContent-Lengthヘッダーがないため、クライアントはHTTP応答のチャンクをストリームとして解析する必要があることを意味します。注、チャンクされた転送エンコーディングを処理しなかったクライアント(ブラウザなど)に出くわしたことはありません。詳しくは、以下のリンクをご覧ください。

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding

    [HttpGet]
    public async Task<HttpResponseMessage> Download(CancellationToken token)
    {
        var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
        {
            Content = new PushStreamContent(async (stream, context, transportContext) =>
            {
                try
                {
                    using (var fileStream = System.IO.File.OpenRead("some path to MyBigDownload.Zip"))
                    {
                        await fileStream.CopyToAsync(stream);
                    }
                }
                finally
                {
                    stream.Close();
                }
            }, "application/octet-stream"),
        };
        response.Headers.TransferEncodingChunked = true;
        response.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
        {
            FileName = "MyBigDownload.Zip"
        };
        return response;
    }
4

私のために働いた Rizwan Ansari の投稿があります:

サーバー上のどこかにある、または実行時に生成される大きなファイルのダウンロードオプションを提供する必要がある場合があります。以下の機能を使用して、任意のサイズのファイルをダウンロードできます。大きなファイルをダウンロードすると、「プログラムの実行を続行するにはメモリが不足しています」という例外OutOfMemoryExceptionがスローされる場合があります。したがって、この関数は、ファイルを1 MBのチャンクに分割することによってもこの状況を処理します(bufferSize変数を変更してカスタマイズできます)。

使用法:

DownloadLargeFile("A big file.pdf", "D:\\Big Files\\Big File.pdf", "application/pdf", System.Web.HttpContext.Current.Response);

右で「application/pdf」を変更できます MIMEタイプ

ダウンロード機能:

public static void DownloadLargeFile(string DownloadFileName, string FilePath, string ContentType, HttpResponse response)
    {
        Stream stream = null;

        // read buffer in 1 MB chunks
        // change this if you want a different buffer size
        int bufferSize = 1048576;

        byte[] buffer = new Byte[bufferSize];

        // buffer read length
        int length;
        // Total length of file
        long lengthToRead;

        try
        {
            // Open the file in read only mode 
            stream = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read);

            // Total length of file
            lengthToRead = stream.Length;
            response.ContentType = ContentType;
            response.AddHeader("Content-Disposition", "attachment; filename=" + HttpUtility.UrlEncode(DownloadFileName, System.Text.Encoding.UTF8));

            while (lengthToRead > 0)
            {
                // Verify that the client is connected.
                if (response.IsClientConnected)
                {
                    // Read the data in buffer
                    length = stream.Read(buffer, 0, bufferSize);

                    // Write the data to output stream.
                    response.OutputStream.Write(buffer, 0, length);

                    // Flush the data 
                    response.Flush();

                    //buffer = new Byte[10000];
                    lengthToRead = lengthToRead - length;
                }
                else
                {
                    // if user disconnects stop the loop
                    lengthToRead = -1;
                }
            }
        }
        catch (Exception exp)
        {
            // handle exception
            response.ContentType = "text/html";
            response.Write("Error : " + exp.Message);
        }
        finally
        {
            if (stream != null)
            {
                stream.Close();
            }
            response.End();
            response.Close();
        }
    }
3
A. Morel

IISを使用してHTTPダウンロードを有効にする必要があります。これを見てください link

そして、ダウンロードするファイルのHTTPパスをすばやく簡単に返すだけです。

1
Shahrooz Ansari