Потоковый zip-файл без AllowSynchronousIO = true - PullRequest
1 голос
/ 04 мая 2020

Я использую . NET Core 3.1 . Я следовал за этой записью в блоге от Стивена Клири, чтобы добиться потоковой передачи файла .zip непосредственно клиенту (без предварительного сохранения в памяти). Я добавил много комментариев, чтобы код было легче понять.

AttachmentsController.cs

public IActionResult DownloadAll(int commentId)
{
    var syncIOFeature = HttpContext.Features.Get<IHttpBodyControlFeature>();
    if (syncIOFeature != null)
    {
        syncIOFeature.AllowSynchronousIO = true; // DOESN'T WORK IF I REMOVE THIS
    }

    List<Attachment> attachments = _attachmentsService.GetAll(commentId);

    return new FileCallbackResult(new MediaTypeHeaderValue("application/octet-stream"), async (outputStream, _) =>
    {
        using (var zipArchive = new ZipArchive(outputStream, ZipArchiveMode.Create))
        {
            foreach (Attachment attachment in attachments)
            {
                var zipEntry = zipArchive.CreateEntry(attachment.FileName));
                using (var zipStream = zipEntry.Open())
                {
                    using (var transaction = _dbContext.Database.BeginTransaction())
                    {
                        try
                        {
                            using (var stream = _dbContext.GetLargeObjectStream(attachment.Id))
                            {
                                await stream.CopyToAsync(zipStream);
                            }

                            transaction.Commit();
                        }
                        catch
                        {
                            transaction.Rollback();
                        }
                    }
                }
            }
        }
    })
    {
        FileDownloadName = "Attachments.zip"
    };
}

FileCallbackResult.cs

/// <summary>
/// Represents an <see cref="ActionResult"/> that when executed will
/// execute a callback to write the file content out as a stream.
/// </summary>
public class FileCallbackResult : FileResult
{
    private Func<Stream, ActionContext, Task> _callback;

    /// <summary>
    /// Creates a new <see cref="FileCallbackResult"/> instance.
    /// </summary>
    /// <param name="contentType">The Content-Type header of the response.</param>
    /// <param name="callback">The stream with the file.</param>
    public FileCallbackResult(string contentType, Func<Stream, ActionContext, Task> callback)
        : this(MediaTypeHeaderValue.Parse(contentType), callback)
    { }

    /// <summary>
    /// Creates a new <see cref="FileCallbackResult"/> instance.
    /// </summary>
    /// <param name="contentType">The Content-Type header of the response.</param>
    /// <param name="callback">The stream with the file.</param>
    public FileCallbackResult(MediaTypeHeaderValue contentType, Func<Stream, ActionContext, Task> callback)
        : base(contentType?.ToString())
    {
        Callback = callback ?? throw new ArgumentNullException(nameof(callback));
    }

    /// <summary>
    /// Gets or sets the callback responsible for writing the file content to the output stream.
    /// </summary>
    public Func<Stream, ActionContext, Task> Callback
    {
        get
        {
            return _callback;
        }
        set
        {
            _callback = value ?? throw new ArgumentNullException(nameof(value));
        }
    }

    /// <inheritdoc />
    public override Task ExecuteResultAsync(ActionContext context)
    {
        if (context == null)
            throw new ArgumentNullException(nameof(context));

        var executor = new FileCallbackResultExecutor(context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>());
        return executor.ExecuteAsync(context, this);
    }
}

/// <summary>
/// An action result handler of type file
/// </summary>
internal sealed class FileCallbackResultExecutor : FileResultExecutorBase
{
    /// <summary>
    /// Creating an instance of a class <see cref="FileCallbackResultExecutor"/>
    /// </summary>
    /// <param name="loggerFactory"></param>
    public FileCallbackResultExecutor(ILoggerFactory loggerFactory)
        : base(CreateLogger<FileCallbackResultExecutor>(loggerFactory))
    { }

    /// <summary>
    /// Handler execution
    /// </summary>
    /// <param name="context">Action context</param>
    /// <param name="result">Action result</param>
    /// <returns><see cref="Task"/></returns>
    public Task ExecuteAsync(ActionContext context, FileCallbackResult result)
    {
        SetHeadersAndLog(context, result, null, result.EnableRangeProcessing);
        return result.Callback(context.HttpContext.Response.Body, context);
    }
}

Как я могу заставить его работать без установки syncIOFeature.AllowSynchronousIO = true? Я предполагаю, что эта строка заставляет код работать медленнее? Я заметил, что загрузка zip примерно в 4 раза медленнее, чем загрузка только одного файла без его архивирования.

...