在Django中通过哈希值高效地保存文件

8
我正在开发一个Django项目。我希望用户能够通过表单上传文件,并将文件保存到自定义路径和自定义文件名(使用哈希值)。我想到的唯一解决方案是使用我正在使用的FileField的"upload_to"参数。这个参数的作用是:

1)将文件写入磁盘

2)计算哈希值

3)返回路径+哈希值作为文件名

问题在于有两个写操作:一个是将文件从内存保存到磁盘以计算哈希值,另一个是将文件保存到指定位置。

有没有办法覆盖FileField的保存到磁盘方法(或者我应该去哪里查找背后的具体情况),以便我可以使用临时名称保存文件,然后将其重命名为哈希值,而不是让它被保存两次。

谢谢。

2个回答

7
FileFieldupload_to参数接受一个可调用对象,该对象返回的字符串将与MEDIA_ROOT设置拼接以获得最终的文件名(来自文档):

这也可以是一个可调用对象,比如函数,它将被调用以获取上传路径,包括文件名。这个可调用对象必须能够接受两个参数,并返回一个Unix风格(使用正斜杠)的路径,以传递给存储系统。将传递的两个参数分别为:

  • instance: 文件字段所在模型的实例。更具体地说,这是当前正在附加文件的特定实例。在大多数情况下,此对象尚未保存到数据库中,因此如果使用默认的AutoField,则可能尚未为其主键字段设置值。
  • filename: 最初赋予文件的文件名。在确定最终目标路径时,可能会考虑或不考虑此文件名。
此外,当您访问model.my_file_field时,它会解析为FieldFile的实例,它就像一个文件。因此,您应该能够编写以下样式的upload_to:
def hash_upload(instance, filename):
    instance.my_file.open() # make sure we're at the beginning of the file
    contents = instance.my_file.read() # get the contents
    fname, ext = os.path.splitext(filename)
    return "{0}_{1}{2}".format(fname, hash_function(contents), ext) # assemble the filename

替换您想使用的适当哈希函数。根本不需要保存到磁盘(实际上,文件通常已经上传到临时存储中,或者在较小的文件的情况下只需在内存中保留即可)。

您可以像这样使用:

class MyModel(models.Model):
    my_file = models.FileField(upload_to=hash_upload,...)

我还没有测试过这个功能,所以你可能需要检查读取整个文件的那一行代码(你可能只想散列文件的第一个块,以防止恶意用户上传大型文件并导致拒绝服务攻击)。您可以使用以下代码获取第一个块:
instance.my_file.read(instance.my_file.DEFAULT_CHUNK_SIZE)


这有点像我之前所做的,但是我并没有将文件视为已经写入磁盘,而是保存它(根据文档逐块保存),然后计算哈希值。我不知道我可以将其视为已经保存。 - Puscasu Emanuel
3
当字段不包含图像时,这种方法似乎无法工作。在这种情况下,哈希值似乎会从先前的内容中派生而来,而不是新内容。查看save方法可以发现,在调用generate_filename之前,内容并没有绑定到FieldFile上。不确定如何从上传函数获取内容的哈希值…最好的方法可能是子类化FieldFile::save - FMCorz

6

至少1.10更新的答案:

  • 您的instance.my_file_fieldUploadedFile的实例,而不是类似文件的对象
  • 它无法打开或关闭,只能读取并可能分成块
  • 无条件调用read()可能会消耗所有可用的物理内存

在下面的示例中,该实例具有类方法“get_image_basedir”,因为有几个模型都使用相同的函数,但需要不同的基本目录。 我保留了这一点,因为这是一种常见的模式。 HASH_CHUNK_SIZE是我设置的一个变量,选择优化磁盘读取(即匹配文件系统的块大小或其倍数)。

def get_image_path(instance, filename):
    import os.path
    import hashlib
    base = instance.get_image_basedir()
    parts = os.path.splitext(filename)
    ctx = hashlib.sha256()
    if instance.img.multiple_chunks():
        for data in instance.img.chunks(HASH_CHUNK_SIZE):
            ctx.update(data)
    else:
        ctx.update(instance.img.read())
    return os.path.join(base, ctx.hexdigest() + parts[1])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接