我有一个基于类的视图,可以触发用户组合和下载报告。
通常在类的def get
中我只编译报告,添加response['Content-Disposition'] = 'attachment; filename="somefilename.pdf"'
并将响应返回给用户。
问题是某些报告大,并且在编译请求超时时发生。
我知道正确处理此问题的方法是将其委托给后台进程(如Celery)。但问题在于,这意味着在用户下载报告的那一刻,我不得不创建一个不再存在的临时文件,而是必须将这些报告存储在某处,并编写一个定期清理报告目录的cronjob。
Django有没有更优雅的方式来处理这个问题?
答案 0 :(得分:3)
使用芹菜的一种解决方案比使用芹菜更少:StreamingHttpResponse
:
(https://docs.djangoproject.com/en/2.0/ref/request-response/#django.http.StreamingHttpResponse
有了这个,你使用了一个生成器函数,它是一个python函数,它使用yield
将结果作为迭代器返回。这允许您在生成数据时返回数据,而不是在完成后立即返回数据。您可以在报告的每一行或每个部分之后yield
,从而将数据流保留回浏览器。
但是..这只有在你逐步构建完成的文件时才有效..例如,一个CSV文件。如果您要返回一些需要一次格式化的内容,例如,如果您在完成后使用wkhtmltopdf
之类的内容生成pdf文件,那么就不那么容易了。
但仍有解决方案:
在这种情况下,您可以使用StreamingHttpReponse
和生成器函数将报告生成临时文件,而不是返回浏览器。但是当你这样做时,yield
HTML片段回到浏览器,让用户知道进度,例如:
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时,如果返回tempfile的视图在返回之前将响应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
答案 1 :(得分:2)
这不是真正的Django问题,而是一般架构问题。
您可以随时增加服务器超时时间,但如果用户必须坐在观看浏览器旋转状态,IMO仍然会给您带来糟糕的用户体验。
在后台任务上执行此操作是正确执行此操作的唯一方法。我不知道报告有多大,但使用电子邮件可能是一个很好的解决方案。后台任务只生成报告,通过电子邮件发送并删除它。
如果文件太大而无法通过电子邮件发送,则您必须存储它们。也许发送一封电子邮件,其中包含一个链接和一条消息,表明该链接在X天/小时后无效。一旦有了后台工作人员,创建每日或每小时清理任务将非常容易。
希望有所帮助