【问题标题】:Initiate file download in .Net ApiController在 .Net ApiController 中启动文件下载
【发布时间】:2015-01-30 23:21:33
【问题描述】:

我正在尝试生成一个包含 HTML 内容的 PDF 文件,该文件通过 AJAX 从 JavaScript 发送。

我已经正确发送了 HTML,并且使用 HTML 正确生成了 PDF,但我一直在试图弄清楚如何在用户的浏览器上启动文件下载。

这是我目前正在尝试的。当直接点击文件的 URL 时,这在 IHttpHandler 文件中有效,但在通过 AJAX 发布时在 ApiController 中无效。

我也尝试过发布到 IHttpHandler 文件,结果相同。一切正常,直到我尝试使用 BinaryWrite 启动下载。

public class DownloadScheduleController : ApiController
{
    public void Post(HtmlModel data)
    {
        var htmlContent = data.html;

        var pdfBytes = (new NReco.PdfGenerator.HtmlToPdfConverter()).GeneratePdf(htmlContent);

        using (var mstream = new System.IO.MemoryStream())
        {
            HttpContext.Current.Response.ContentType = "application/pdf";
            HttpContext.Current.Response.AppendHeader("content-disposition", "attachment; filename=UserSchedule.pdf");
            HttpContext.Current.Response.BinaryWrite(pdfBytes);
            HttpContext.Current.Response.End();
        }
    }
}

这是 Ajax 请求。它使用 Angular 的 $http。

$http({
    method: 'POST',
    url: 'api/downloadSchedule',
    dataType: "json",
    contentType: "application/json; charset=utf-8",
    data: data
});

或者,我可以将 PDF 二进制文件返回到 JavaScript,但是我如何使用 JavaScript 来保存 pdf?

任何帮助将不胜感激。

【问题讨论】:

  • 我只是通过实际重定向(通过 301)到返回字节流和content-disposition: attachment 的非 API 控制器来做到这一点。我不能 100% 确定它是否可以用 AJAX 完成(但我肯定是错的 - 在这里失去记忆)。
  • 是的,不幸的是我处于需要通过 AJAX 完成的情况。 PDF 生成器需要的 html 内容正在另一个页面上通过 Javascript 动态加载。当他们点击下载按钮时,我使用 iFrame 触发页面并获取内容,一旦内容加载,我就会有一个事件触发对 downloadSchedule ApiController 的 API 调用。
  • 我会尝试做两件事:Response.Flush();在您的 Response.End();和 Response.OutputStream.Write(pdfBytes,0,pdfBytes.Length);而不是 Response.BinaryWrite(pdfBytes)
  • 感谢 JFlox 的评论。我尝试了这两个,但仍然没有。
  • 不确定是否需要 using 语句,因为您似乎实际上并未在该块中的任何位置使用 mstream。

标签: javascript c# jquery .net


【解决方案1】:

出于安全原因,浏览器无法通过 AJAX 请求启动文件下载。只能通过导航到发送文件的页面来完成。

通常,如果您需要从 Javascript 启动下载,您可以设置 window.location.href = urlToFile,或者创建一个指向该 url 的新 iframe。

在你的情况下这不起作用,因为上述方法只执行一个 GET 请求,而你需要一个 POST,所以我只看到两种可能的解决方案:

  • 您可以将您的 JS 修改为 - 而不是使用 $http 提交请求 - 创建一个 HTML 表单,其中包含与您最初通过 AJAX 请求发布的内容相对应的字段,然后将表单附加到页面,填充字段并提交表格
  • 或者您可以更改服务器端代码以及 Javascript。

如果您选择第二种解决方案,您可以按照在服务器端使用两种方法的方法:

  • 一个 POST 生成文件并将其保存在缓存中(使用缓存可能有更好的解决方案,特别是如果您在服务器场上,但让我们保持简单)

  • 一个获取缓存内容并将其返回给用户的 GET。通过这种方式,您将能够通过 AJAX 调用发布数据,然后通过导航到正确的 url 来获取文件。

您的 C# 代码如下所示:

public class DownloadScheduleController : ApiController
{
    public object Post(HtmlModel data)
    {
        var htmlContent = data.html;

        var pdfBytes = (new NReco.PdfGenerator.HtmlToPdfConverter()).GeneratePdf(htmlContent);
        var policy = new CacheItemPolicy { AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(30), Priority = CacheItemPriority.NotRemovable };
        var cacheId = Guid.NewGuid().ToString();
        MemoryCache.Default.Add("pdfBytes_" + cacheId, pdfBytes, policy);
        return new { id = cacheId };
    }

    public void Get(string id)
    {
        var pdfBytes = MemoryCache.Default.Get("pdfBytes_" + id);
        MemoryCache.Default.Remove("pdfBytes_" + id);

        using (var mstream = new System.IO.MemoryStream())
        {
            HttpContext.Current.Response.ContentType = "application/pdf";
            HttpContext.Current.Response.AppendHeader("content-disposition", "attachment; filename=UserSchedule.pdf");
            HttpContext.Current.Response.BinaryWrite(pdfBytes);
            HttpContext.Current.Response.End();
        }
    }
}

你的前端可能是这样的:

$http({
    method: 'POST',
    url: 'api/downloadSchedule',
    dataType: "json",
    contentType: "application/json; charset=utf-8",
    data: data
}).success(function(response) {
    // note: the url path might be different depending on your route configuration
    window.location.href = 'api/downloadSchedule/' + response.id;
});

请记住,我的代码只是为了向您展示方法,而不是在生产中使用。例如,您绝对应该在使用后立即清除缓存。在使用之前,您还应该在 Get 方法中检查从缓存中检索到的内容。此外,您可能希望根据您的要求对存储和检索的数据采取额外的安全预防措施。

【讨论】:

  • 你知道第一种方法是否适用于网络表单吗?我考虑过使用第二种方法,但是数据可能非常大,并且是一个拥有大量用户的大型应用程序。在这种情况下,我认为任何服务器端存储选项都不起作用。
  • 您可以调整第一个解决方案以使用 webforms,但它可能需要 viewstate,因此您需要事先定义表单元素,然后从您的 JS 函数中填充它们并改为调用 __doPostBack()只需提交表单。至于第二种解决方案,您为什么认为它行不通?我认为如果你有大量数据不会有什么不同:一旦你创建了字节数组,你就已经把它全部放在内存中了,那么为下一个请求记住它有什么问题?
  • 我最终使用了第二种方法,效果很好,非常感谢。并不是我不认为它会起作用,我只是担心为 DOS(或类似)攻击打开一个漏洞。我明白你在说什么,多记住 30 秒并没有多大用处。 Get 函数运行后删除缓存怎么样?
  • 是的,您绝对应该这样做。我的代码只是为了向您展示方法,并不打算在生产中使用。您绝对应该在使用后立即清除缓存。在使用之前,您还应该在 Get 方法中检查从缓存中检索到的内容。此外,您可能需要采取额外的安全预防措施,因为这样可以访问存储在缓存中的任何内容。在您的情况下,使用缓存键的前缀可能就足够了。
  • 当我意识到这非常重要时,我编辑了我的答案以包含这些评论。我还对代码进行了一些简单的编辑:我为缓存键添加了一个前缀,并更改了策略以确保缓存项不会被应用程序自动删除。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-03-15
  • 2016-02-02
  • 2017-02-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-18
相关资源
最近更新 更多