首页 > 解决方案 > Django:批量上传并确认

问题描述

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

由于这个问题有点模糊,我会在最后给出一些观点。

所以,用例。

我有一个包含这些项目的网站。有一个功能可以按用户添加项目。现在我想要一个通过 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})
  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"
  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})
  1. forms.py 中:表格非常明显,但要清楚
class UploadItemCsvFileForm(forms.Form):
    item_csv_file = forms.FileField()

以下是问题/要点。

a) 即使显然它可能会更好,这种解决方案是可以接受的还是根本不可以接受的?

b)我使用“request.session”将“uploaded_file”从一个视图传递到另一个视图,这是一个好习惯吗?在不使用 GET 变量的情况下,还有其他方法吗?

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

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

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

也欢迎所有其他评论。

标签: pythondjangodjango-viewsbulk

解决方案


a) 即使显然它可能会更好,这种解决方案是可以接受的还是根本不可以接受的?

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

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

b)我使用“request.session”将“uploaded_file”从一个视图传递到另一个视图,这是一个好习惯吗?在不使用 GET 变量的情况下,还有其他方法吗?

使用 request.session 有好处也有坏处。

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

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

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

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

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

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

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

一些想法:

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

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

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

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

请记住:如果您仅将 csv 存储为您在每次访问某个视图时加载和解析的文本数据,这可能是攻击者耗尽您的服务器的一种简单方法,从而为他们提供简单的 DoS 攻击。


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

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


推荐阅读