Django ImageField上传时更改文件名

47
在保存“产品”模型时,我希望上传的图像以pk相同的名称命名,例如22.png或34.gif。我不想更改图像的格式,只需更改名称。如何实现?以下是我目前的模型示例...
image = models.ImageField(
        upload_to="profiles",
        height_field="image_height",
        width_field="image_width",
        null=True,
        blank=True,
        editable=True,
        help_text="Profile Picture",
        verbose_name="Profile Picture"
    )
    image_height = models.PositiveIntegerField(null=True, blank=True, editable=False, default="100")
    image_width = models.PositiveIntegerField(null=True, blank=True, editable=False, default="100")
7个回答

104
你可以将一个函数作为参数传递到upload_to字段中:
def f(instance, filename):
    ext = filename.split('.')[-1]
    if instance.pk:
        return '{}.{}'.format(instance.pk, ext)
    else:
        pass
        # do something if pk is not there yet

我的建议是随机生成一个文件名,而不是使用{pk}.{ext}。这样做还可以提高安全性。

Django会调用该函数来确定文件应该上传到哪里。这意味着您的函数负责返回包括文件名在内的完整文件路径。以下是修改后的函数,您可以指定上传位置以及如何使用它:

import os
from uuid import uuid4

def path_and_rename(path):
    def wrapper(instance, filename):
        ext = filename.split('.')[-1]
        # get filename
        if instance.pk:
            filename = '{}.{}'.format(instance.pk, ext)
        else:
            # set filename as random string
            filename = '{}.{}'.format(uuid4().hex, ext)
        # return the whole path to the file
        return os.path.join(path, filename)
    return wrapper

FileField(upload_to=path_and_rename('upload/here/'), ...)

4
不,不要道歉。我没有发布那个函数,而那正是OP所需要的。你的回答比我更好,并且发布得更快。无论如何我会给你点赞。 - Aidan Ewen
3
你正在传递被调用的函数,而应该传递函数本身 - upload_to=path_and_rename, ... - miki725
6
解决方案可以正常工作,但是当我执行迁移操作时,会出现值错误,指出“找不到包装器”。 - Rohan
4
当我实现新函数时,出现了“无法找到函数包装器”的错误提示。我做错了什么? - Redgren Grumbholdt
4
对于仍然遇到“Could not find function wrapper”问题的任何人:https://dev59.com/tl8e5IYBdhLWcg3wb52S#25768034 - Baily
显示剩余8条评论

32

根据@miki725的答案和问题,Django 1.7及更高版本不会使用这样的函数进行迁移,您需要将您的函数更改为如下形式:

import os
from uuid import uuid4
from django.utils.deconstruct import deconstructible

@deconstructible
class UploadToPathAndRename(object):

    def __init__(self, path):
        self.sub_path = path

    def __call__(self, instance, filename):
        ext = filename.split('.')[-1]
        # get filename
        if instance.pk:
            filename = '{}.{}'.format(instance.pk, ext)
        else:
            # set filename as random string
            filename = '{}.{}'.format(uuid4().hex, ext)
        # return the whole path to the file
        return os.path.join(self.sub_path, filename)

FileField(upload_to=UploadToPathAndRename(os.path.join(MEDIA_ROOT, 'upload', 'here'), ...)

1
这也适用于Python 2.7(测试Django 1.9),而被接受的答案仅适用于>= 3.0,引发错误“请注意,由于Python 2的限制,您无法序列化未绑定的方法函数(例如,在同一类体中声明和使用的方法)。请将该函数移动到主模块体中以使用迁移。 有关更多信息,请参见https://docs.djangoproject.com/en/1.9/topics/migrations/#serializing-values - dotcomly
1
在UploadToPathAndRename中不需要MEDIA_ROOT(os.path.join(MEDIA_ROOT...),因为FileField会自动添加。 - dmitri
instancefilename是如何传递到__call__方法的呢?难道不需要编写upload_to=UploadToPathAndRename(...)(obj_instance, 'some_file_name')吗?请帮我理解这个Python魔法哈哈。 - Eduardo Gomes

7

您可以按照文档中所述,将赋给upload_to的字符串替换为可调用对象。但是,我怀疑在使用upload_to参数时可能还没有主键可用。


5

默认情况下,Django会保留上传文件的原始名称,但很可能您希望将其重命名为其他名称(例如对象ID)。 幸运的是,使用Django表单的ImageField或FileField,您可以将可调用函数分配给upload_to参数以进行重命名。 例如:

from django.db import models
from django.utils import timezone
import os
from uuid import uuid4

def path_and_rename(instance, filename):
    upload_to = 'photos'
    ext = filename.split('.')[-1]
    # get filename
    if instance.pk:
        filename = '{}.{}'.format(instance.pk, ext)
    else:
        # set filename as random string
        filename = '{}.{}'.format(uuid4().hex, ext)
    # return the whole path to the file
    return os.path.join(upload_to, filename)

在模型字段中:

class CardInfo(models.Model):
    ...
    photo = models.ImageField(upload_to=path_and_rename, max_length=255, null=True, blank=True)

在这个例子中,上传的每张图片都将被重新命名为 CardInfo 对象的主键 id_number。

你能帮我理解在这个赋值语句中upload_to=path_and_rename是如何传递instancefilename的值的吗?我不太明白这个Python的魔法。谢谢。 - Eduardo Gomes

1

另一个选项是,按照这个答案https://dev59.com/_WUp5IYBdhLWcg3wo4ud#15141228的建议,我们发现当我们需要返回路径与%Y/%m/%d一起使用时会出现问题,例如:

FileField(upload_to=path_and_rename('upload/here/%Y/%m/%d'), ...)

所以,我们用这个处理它:
FileField(upload_to=path_and_rename('upload/here/{}'.format(time.strftime("%Y/%m/%d"))), ...)

确保已经导入了模块time

0

我有一个更可定制的Aidan Ewen解决方案实现。

有什么新东西?

  • 你可以将要在文件名中使用的字段作为列表发送(按预定顺序)
  • ^ 其中之一必须是唯一的
  • ^ 否则,此列表必须包括唯一字段元组之一
  • ^ 否则,你发送的字段将被忽略,并将使用uuid4作为文件名

示例1:

image = models.ImageField(upload_to=PathAndRename('images/').wrapper)

filename = {pk}.{ext}
# default is pk for filenames

例子2:

name = models.CharField(max_length=20)  # not unique
image = models.ImageField(upload_to=PathAndRename('images/', ['name']).wrapper)

filename = {uuid4}.{ext}
# if given fields are did not accepted will use the uuid4

例子 3:

name = models.CharField(max_length=20, unique=True)
no = models.CharField(max_length=10)
image = models.ImageField(upload_to=PathAndRename('images/', ['name','no']).wrapper)

filename = {name}_{no}.{ext}
# one unique field is enough to use all of the given fields in the filename

例子 4:

name = models.CharField(max_length=20)  # not unique
no = models.CharField(max_length=10)  # not unique
image = models.ImageField(upload_to=PathAndRename('images/', ['name','no']).wrapper)

class Meta:
    unique_together = ('name', 'no')
    # (('name', 'no'),) is acceptable too or multiple unique togethers

filename = {name}_{no}.{ext}
# if one of the unique together fields exists in the given fields, will use all of the given fields in the filename

我可能忘记了给出更多的例子,但你可以从下面的代码中理解:

class PathAndRename:
    """
    fields to use for naming, order is important
    """

    def __init__(self, path, fields_to_use=('pk',)):
        self.path = path
        self.fields_to_use = fields_to_use

    def wrapper(self, instance, filename):
        # multiple extensions
        ext = '.'.join(filename.split('.')[1:])

        # check the uniqueness of the fields given for filename
        if self.is_any_unique_exist(instance):
            # if any unique field exist in the given list
            # create filename by using given field values
            filename = '{}.{}'.format(self.get_filename_by_fields(instance), ext)
        # else check the existence of at least one unique together
        elif self.is_any_unique_together_exist(instance):
            # create filename by using given field values
            filename = '{}.{}'.format(self.get_filename_by_fields(instance), ext)
        # if any unique or unique together not exists
        else:
            # then create a filename by using uuid4
            filename = '{}.{}'.format(uuid4().hex, ext)

        # return the whole path to the file
        return os.path.join(self.path, filename)

    def is_any_unique_exist(self, instance):
        if 'pk' in self.fields_to_use:
            return True
        return any([instance._meta.get_field(field).unique for field in self.fields_to_use if hasattr(instance, field)])

    def is_any_unique_together_exist(self, instance):
        if hasattr(instance._meta, 'unique_together'):
            if isinstance(instance._meta.unique_together, (list, tuple)):
                for uniques in instance._meta.unique_together:
                    # if any one of the unique together set is exists in the fields to use
                    if all(map(lambda field: field in self.fields_to_use, uniques)):
                        return True
            else:
                if all(map(lambda field: field in self.fields_to_use, instance._meta.unique_together)):
                    return True
        return False

    def get_filename_by_fields(self, instance):
        return '_'.join([str(getattr(instance, field)) for field in self.fields_to_use])

警告:对于此 upload_to 问题的每种基于方法的解决方案,当您放弃使用这些解决方案时,都会对已执行的迁移文件造成问题。如果您使用这些解决方案一段时间后将其删除,则旧的迁移将因为这些方法不存在而失败。(当然,您可以通过修改旧的迁移文件来解决此问题)


0

我稍微修改了被接受的答案,以便更容易理解。

def wrapper(instance, filename):
    ext = filename.split('.')[-1]
    # get filename
    if instance.pk:
        filename = '{}.{}'.format(instance.pk, ext) # do instance.username 
                                                    # if you want to save as username
    else:
        # set filename as random string
        filename = '{}.{}'.format(uuid4().hex, ext)
    # return the whole path to the file
    return os.path.join('path/to/save/', filename)

image = models.ImageField(upload_to=wrapper, default="/user.png", null=True, blank=True)

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