Django:批量上传并确认

Pau*_*rov 6 python django bulk django-views

还有一个关于风格和良好实践的问题。我将展示的代码可以工作并执行功能。但我想知道它是否可以作为解决方案,或者它可能太丑陋了?

由于问题有点晦涩难懂,我最后再给出一些观点。

那么,用例。

我有一个包含这些物品的网站。有一个按用户添加项目的功能。现在我想要一个通过 csv 文件添加多个项目的功能。

应该如何运作?

  1. 用户转到特殊上传页面。
  2. 用户选择 csv 文件,单击上传。
  3. 然后他被重定向到显示 csv 文件内容的页面(以表格形式)。
  4. 如果用户可以,他单击“是”(具有“confirm_items_upload”值的按钮),文件中的项目将添加到数据库中(如果可以)。

我已经看到了 django 批量上传的例子,它们看起来很清楚。但我没有找到带有中间“验证确认”页面的示例。那么我是怎么做到的:

  1. views.py中:上传csv文件页面的视图
def upload_item_csv_file(request):
    if request.method == 'POST':
        form = UploadItemCsvFileForm(request.POST, request.FILES)
        if form.is_valid():
            uploaded_file_name = handle_uploaded_item_csv_file(request.FILES['item_csv_file'])
            request.session['uploaded_file'] = uploaded_file_name
            return redirect('show_upload_csv_item')
    else:
        form = UploadItemCsvFileForm()
    return render(request, 'myapp/item_csv_upload.html', {'form': form})
Run Code Online (Sandbox Code Playgroud)
  1. 在utils.py中:handle_uploaded_item_csv_file - 只需保存文件并返回文件名
def handle_uploaded_item_csv_file(f):
    now = datetime.now()
    # YY_mm_dd_HH_MM
    dt_string = now.strftime("%Y_%m_%d_%H_%M")
    file_name = os.path.join(settings.MEDIA_ROOT, f"tmp_csv/item_csv_{dt_string}.csv")
    with open(file_name, 'wb+') as destination:
        for chunk in f.chunks():
            destination.write(chunk)

    return f"tmp_csv/item_csv_{dt_string}.csv"
Run Code Online (Sandbox Code Playgroud)
  1. views.py中:show_upload_csv_item的视图
@transaction.atomic
def show_uploaded_file(request):
    if 'uploaded_file' in request.session :
        file_name = request.session['uploaded_file']
    else :
        print("Something wrong : raise 404")
        raise Http404
    if not os.path.isfile(os.path.join(settings.MEDIA_ROOT, file_name)):
        print("Something wrong, file does not exist : raise 404")
        raise Http404

    with open(os.path.join(settings.MEDIA_ROOT, file_name)) as csvfile :
        fieldnames = ['serial_number', 'type', 'shipping_date', 'comments']
        csv_reader = csv.DictReader(csvfile, delimiter=';', fieldnames=fieldnames)
        list_items = list(csv_reader)

    if request.POST and ("confirm_items_upload" in request.POST) :
        if request.POST["confirm_items_upload"] == "yes" :
            for cur_item in list_items :
                if not cur_item['shipping_date'] :
                    cur_item.pop('shipping_date', None)

                try :
                    Item.objects.create(**cur_item)
                except IntegrityError :
                    messages.warning(request, f"This Item : {cur_item} - already exists. No items were added." )
            os.remove(os.path.join(settings.MEDIA_ROOT, file_name))
            return redirect('items')
    else :
        return render(request, 'myapp/item_csv_uploaded.html', {'items': list_items})
Run Code Online (Sandbox Code Playgroud)
  1. forms.py中:形式非常明显,但只是为了清楚
class UploadItemCsvFileForm(forms.Form):
    item_csv_file = forms.FileField()
Run Code Online (Sandbox Code Playgroud)

这是问题/要点。

a) 即使显然它可以更好,这个解决方案是否可以接受或根本不可以接受?

b)我使用“request.session”将“uploaded_file”从一个视图传递到另一个视图,这是一种好的做法吗?有没有另一种方法可以不使用 GET 变量来做到这一点?

c) 起初我的愿望是避免保存 csv 文件。但我不知道该怎么做?将所有文件读取到 request.session 对我来说似乎不是一个好主意。是否有可能将文件上传到 Django 内存中?

d) 如果我必须使用 tmp 文件。如果用户在中间放弃上传(例如,他看到确认页面,但没有单击“是”并决定重写他的文件),我应该如何处理这种情况。如何删除 tmp 文件?

e) 附加小问题:Django 对上传的文件进行了哪些检查?例如,我如何检查该文件至少是一个文本文件?我应该这样做吗?

也欢迎所有其他评论。

He3*_*xxx 4

a) 即使显然它可以更好,这个解决方案是否可以接受或根本不可以接受?

我认为它有一些您想要解决的问题,但是使用文件系统并仅存储文件名的一般想法是可以接受的,具体取决于您需要服务的用户数量以及您想要进行的数据一致性和并发访问的保证。

我会考虑上传的文件临时数据可能会因系统故障而丢失。如果您想提供不丢失数据的任何保证,您希望将其存储在数据库中而不是文件系统中。

b)我使用“request.session”将“uploaded_file”从一个视图传递到另一个视图,这是一种好的做法吗?有没有另一种方法可以不使用 GET 变量来做到这一点?

使用 request.session 有优点也有缺点。

  • 攻击者无法更改文件名,从而检索其他用户的数据。这也是您不应在此处使用 GET 参数的原因:如果您使用了 GET 参数,攻击者可以简单地更改该参数并访问其他用户的文件。
  • 用户可以上传文件,去做其他事情,然后再回来实际导入文件,但是:
  • 如果用户结束会话,您将丢失文件名。此外,用户无法在一台设备上上传文件,更改到另一台设备,然后继续导入,因为另一台设备将具有不同的会话。

最后一点与剩余文件问题相关:如果您丢失了有关仍需要哪些文件的信息,则清理工作会变得更加困难(尽管理论上,您可以从会话存储中检索仍需要哪些文件)。

如果由于用户清除其 cookie 或更改设备而导致会话可能结束或更改,您可以考虑将文件名添加到UserProfile数据库中。这样,它就不受会话的约束。

c) 起初我的愿望是避免保存 csv 文件。但我不知道该怎么做?将所有文件读取到 request.session 对我来说似乎不是一个好主意。是否有可能将文件上传到 Django 内存中?

你想存储状态。存储状态的首选方法是数据库或会话存储。您可以加载整个 CSVFile 并将其作为文本放入数据库中。这是否可以接受取决于您的数据库处理大型非结构化数据的能力。传统数据库最初并不是为此构建的,但是现在它们中的大多数都可以很好地处理小型二进制文件。数据库可以为您提供诸如 ACID 保证之类的优势,其中对文件系统上同一文件的并发写入可能会破坏该文件。请参阅dba stackexchange 上的讨论

您的数据库可能有关于该主题的文档,例如,有一个关于 postgres 中的二进制数据的页面

d) 如果我必须使用 tmp 文件。如果用户在中间放弃上传(例如,他看到确认页面,但没有单击“是”并决定重写他的文件),我应该如何处理这种情况。如何删除 tmp 文件?

一些想法:

  • 根据设计,将每个用户上传的文件数限制为 1。目前,您的文件名基于时间戳。如果两个用户同时决定上传文件,这种情况就会中断:他们都将获得相同的时间戳,并且磁盘上的文件可能已损坏。如果您使用用户的主键,则可以保证每个用户最多拥有一个文件。如果他们稍后上传另一个文件,他们的旧文件将被覆盖。如果您的用户数量足够少,您可以为每个用户存储一个剩余文件,则不需要额外的清理。但是,如果同一用户同时上传两个文件,这仍然会失败。
  • 使用唯一标识符(例如UUID ),并在用户上传新文件时删除旧存储的文件。这要求您仍然拥有旧文件名,因此会话存储不能与此一起使用。您仍然会在文件系统中保留用户的最后一个文件。
  • 使用文件名的唯一标识符并设置任意最大存储持续时间。设置一个 cronjob 或类似的程序,定期检查文件并删除存储时间超过指定最大持续时间的所有文件。如果用户上传文件,但没有尽快进行实际导入,他们的数据将被删除,并且他们必须再次上传。在这里,您的代码必须处理具有存储的文件名的文件不再存在的情况(甚至可能在您读取文件时被删除)。

您可能希望将服务器限制为每个用户存储一个文件,以便攻击者无法填充您的文件系统。

e) 附加小问题:Django 对上传的文件进行了哪些检查?例如,我如何检查该文件至少是一个文本文件?我应该这样做吗?

您肯定想为文件设置一些最大文件大小,如此处所述。您可以限制允许的文件扩展名,但这只是一个可用性问题。攻击者还可以向您提供任何可接受的扩展名的垃圾数据。

请记住:如果您仅将 csv 存储为每次访问特定视图时加载和解析的文本数据,那么这可能是攻击者耗尽您的服务器的一种简单方法,从而使他们能够轻松进行 DoS 攻击。


总的来说,这取决于您想要做出什么保证、您拥有多少用户以及他们的可信度。如果用户可能是恶意的,您需要牢记所有可能类型的数据提取和资源耗尽攻击。文件系统不会横向扩展(至少不像数据库那么容易)。

我知道在一个项目中有类似的设置,其中只允许少数特权用户上传内容,并且我们可以容忍在失败时删除所有临时文件。用户只需重新上传他们的文件。这很好用。