在 Django 中延迟文件下载的正确方法



我有一个基于类的视图,它触发了用户的报表的撰写和下载。

通常,在类def get中,我只是编译报告,添加response['Content-Disposition'] = 'attachment; filename="somefilename.pdf"'并将响应返回给用户。

问题是某些报告很大,并且在编译时会发生请求超时。

我知道处理此问题的正确方法是将其委托给后台进程(如芹菜)。但问题是,这意味着我必须将这些报告存储在某个地方,并编写一个将定期清理报告目录的 cronjob,而不是创建一个在用户下载报告时不复存在的临时文件。

在 Django 中是否有更优雅的方式来解决这个问题?

一个比使用芹菜更不花哨的解决方案是使用Django的StreamingHttpResponse

(https://docs.djangoproject.com/en/2.0/ref/request-response/#django.http.StreamingHttpResponse

有了这个,你使用一个生成器函数,这是一个 python 函数,它使用yield作为迭代器返回其结果。 这允许您在生成数据时返回数据,而不是在完成后一次返回所有数据。 您可以在报告的每一行或部分之后yield。从而保持数据流返回到浏览器。

但是..这只有在您一点一点地构建完成的文件时才有效......例如,CSV文件。 如果您要返回需要一次格式化的内容,例如,如果您在完成后使用wkhtmltopdf之类的东西生成 pdf 文件,那么就没有那么容易了。

但是仍然有一个解决方案:

在这种情况下,您可以做的是,使用StreamingHttpReponse和生成器函数将报告生成到临时文件中,而不是返回到浏览器。 但是当你这样做时,yieldHTML片段回到浏览器,让用户知道进度,例如:

def get(self, request, **kwargs):
# first you need a tempfile name.. do that however you like
tempfile = "kfjkdsjfksjfks"
# then you need to create a view which will open that file and serve it
# but I won't show that here.  
# For security reasons it has to serve only out of one directory 
# that is dedicated to this.
fetchurl = reverse('reportgetter_url') + '?file=' + tempfile
def reportgen():
yield 'Starting report generation..<br>'
# do some stuff to generate your report into the tempfile
yield 'Doing this..<br>'
# do this
yield 'Doing that..<br>'
# do that
yield 'Finished.<br>'
# when the browser receives this script, it'll go to fetchurl where
# you will send them the finished report.
yield '<script>document.location="%s";</script>' % fetchurl
return http.StreamingHttpResponse(reportgen())

这显然不是一个完整的示例,但应该给你一个想法。

当用户获取此视图时,他们将看到报表的进度。 最后,您将发送javacript,它将浏览器重定向到您必须编写的另一个视图,该视图返回包含已完成文件的响应。 当浏览器得到这个javacript时,如果返回临时文件的视图在返回之前将响应Content-Disposition设置为附件,例如:

response['Content-Disposition'] = 'attachment; filename="%s"' % filename

..然后浏览器将停留在显示您的进度的当前页面上。并简单地为用户弹出一个文件保存对话框。

对于清理,无论如何你都需要一个cron工作......因为如果人们不等待,他们永远不会拿起报告。 有时事情不会成功... 因此,您可以只清理超过 1 小时的文件。 对于许多系统,这是可以接受的。

但是如果你想立即清理,如果你在 unix/linux 上,你可以做的是使用旧的 unix 文件系统技巧:在打开时被删除的文件在关闭之前不会真正消失。 所以,打开你的临时文件..然后删除它。 然后返回您的回复。 一旦响应完成发送,文件使用的空间将被释放。

PS:我应该补充一点......如果你采取第二种方法,你可以使用一个视图来完成两个工作。

if `file` in request.GET:
# file= was in the url.. they are trying to get an already generated report
with open(thepathname) as f:
os.unlink(f)
# file has been 'deleted' but f is still a valid open file
response = HttpResponse( etc etc etc)
response['Content-Disposition'] = 'attachment; filename="thereport"'
response.write(f)
return response
else:
# generate the report
# as above

这不是一个真正的Django问题,而是一个一般的架构问题。

您始终可以增加服务器超时时间,但如果用户必须坐下来观看浏览器旋转,IMO 仍然会给您带来糟糕的用户体验。

在后台任务上执行此操作是正确执行此操作的唯一方法。我不知道报告有多大,但使用电子邮件可能是一个很好的解决方案。后台任务只是生成报告,通过电子邮件发送并删除它。

如果文件太大而无法通过电子邮件发送,那么您将不得不存储它们。也许发送一封带有链接的电子邮件和一条消息,指示该链接在 X 天/小时后不起作用。拥有后台辅助角色后,创建每日或每小时清理任务将非常容易。

希望对你有帮助

最新更新