文件上传后如果不设置会使用默认名称进行保存,重复存储后会在原名称后添加_1、_2等避免重名,那么如何将文件存储名称和其在数据库的保存数据联系上是一个问题。
文件字段为FileField
,其特有的参数为upload_to和storage,upload_to决定文件保存地址,比如当upload_to="uploads/"
时,实际地址为settings.MEDIA_ROOT/uploads
,MEDIA_ROOT
可以在配置文件中设置;storage为存储操作,默认的为FileStorageSystem
。
其中upload_to还有一种定义方式就是函数返回式定义,其原理为:FileField
字段对象中存在generate_filename
方法,其中就对upload_to
的类型进行判断,如果是callable(self.upload_to)
,则传入相关参数调用重新赋值,而在FieldFile
中的save
方法会调用FileField
中的generate_filename
方法生成给storage
所保存的文件地址。
FieldFile
和FileField
是两个东西。
如官方文档中的例子:
# 定义函数user_directory_path接收两个参数。
def user_directory_path(instance, filename):
# file will be uploaded to MEDIA_ROOT/user_<id>/<filename>
return 'user_{0}/{1}'.format(instance.user.id, filename)
class MyModel(models.Model):
upload = models.FileField(upload_to=user_directory_path)
文档的例子有个问题就是模型在迁移的时候会对字段进行序列化转为migrations文件,但是这里所使用的函数返回式的定义也会被添加到迁移文件中,所以要使用deconstructible来避免这个问题,因为一旦被加入迁移文件,而迁移文件存在迭代更新的逻辑,之后更改文件存储方式则极其困难。
EvalAI中存在大量的文件字段,当文件多了起来,要对文件名称进行一定的规范才更容易管理,比如将比赛的pk作为文件名的一部分,这就让文件名更加的合理以及规范。
# 添加装饰器deconstructible避免加入迁移文件的问题
@deconstructible
class RandomFileName(object):
def __init__(self, path):
self.path = path
def __call__(self, instance, filename):
# split ext 分离出文件后缀名
extension = os.path.splitext(filename)[1]
path = self.path
# 如果传入的地址中存在id字符,则加入实例保存后的pk
if "id" in self.path and instance.pk:
path = self.path.format(id=instance.pk)
# 随机生成文件地址
filename = "{}{}".format(uuid.uuid4(), extension)
filename = os.path.join(path, filename)
return filename
# Model
# 模型文件中添加RandomFileName作为覆盖
... = models.FileField(upload_to=RandomFileName("..."), ...)