将Django的FileField设置为现有文件

111

我在磁盘上有一个现有文件(例如 /folder/file.txt),以及Django中的FileField模型字段。

当我执行以下操作时:

instance.field = File(file('/folder/file.txt'))
instance.save()

当再次保存文件时,它会将文件另存为file_1.txt(下一次是_2,以此类推)。

我理解原因,但我不想这样做 - 我知道我想让该字段关联的文件确实已经等待着我,我只想让Django指向它。

如何实现?


1
不确定您是否可以在不修改Django或子类化FileField的情况下获得所需内容。每当保存FileField时,都会创建文件的新副本。添加一个选项以避免这种情况将是相当简单的。 - Michael Mior
好的,看起来我需要创建子类并添加一个参数。我不想为这个简单的任务创建额外的表格。 - Guard
1
将文件放置在不同的位置,使用该路径创建您的字段,保存它,然后您就可以将文件保存到 upload_to 目的地。 - benjaoming
7个回答

157

只需将instance.field.name设置为您文件的路径即可。

例如:

class Document(models.Model):
    file = FileField(upload_to=get_document_path)
    description = CharField(max_length=100)


doc = Document()
doc.file.name = 'path/to/file'  # must be relative to MEDIA_ROOT
doc.file
<FieldFile: path/to/file>

17
你的“MEDIA_ROOT”相对路径,也就是说。 - mgalgs
8
在这个例子中,我认为你也可以只使用doc.file = '文件路径' - Andrew Swihart

25

如果您想永久地做到这一点,您需要创建自己的FileStorage类。

import os
from django.conf import settings
from django.core.files.storage import FileSystemStorage

class MyFileStorage(FileSystemStorage):

    # This method is actually defined in Storage
    def get_available_name(self, name):
        if self.exists(name):
            os.remove(os.path.join(settings.MEDIA_ROOT, name))
        return name # simply returns the name passed

现在在你的模型中,你使用了修改过的MyFileStorage。

from mystuff.customs import MyFileStorage

mfs = MyFileStorage()

class SomeModel(model.Model):
   my_file = model.FileField(storage=mfs)

1
哦,看起来很有前途。因为FileField的代码有点不直观。 - Guard
但是...是否可以按请求基础更改存储,例如:instance.field.storage = mfs; instance.field.save(name, file); 但不在我的代码的不同分支中执行它。 - Guard
2
不可以,因为存储引擎与模型绑定在一起。你可以通过将文件路径存储在FilePathField或纯文本中来避免所有这些问题。 - Burhan Khalid
你不能只返回一个名称,你需要先删除现有的文件。 - Alexander Shpindler
2
此解决方案仅表面上是正确的,因为它实际上删除了已经存在的文件并创建了一个同名的新文件。最终,它并没有像作者所写的那样“指向它”。想象一下用户想要指向一个大型文件的情况,但实际上却误删了它并从头重新上传。 - Static.Mike

24

请尝试这个 (文档):

instance.field.name = <PATH RELATIVE TO MEDIA_ROOT> 
instance.save()

5
写自己的存储类是正确的。然而,覆盖 get_available_name 方法是不正确的。
当 Django 发现同名文件并尝试获取新的可用文件名时,会调用 get_available_name 方法。它不是导致重命名的方法。导致重命名的方法是 _save。在 _save 中有很好的注释,您可以轻松找到它使用标志 os.O_EXCL 打开文件进行写入,如果相同的文件名已经存在,则会抛出 OSError。Django 捕获此错误,然后调用 get_available_name 获取新名称。
因此,我认为正确的方法是覆盖 _save 并调用 os.open() 而不使用标志 os.O_EXCL。修改非常简单,但是该方法有点长,所以我不在这里粘贴它。如果您需要更多帮助,请告诉我 :)

这是50行代码需要复制,这很糟糕。覆盖get_available_name似乎更加隔离、更短、更安全,比如将来升级到较新版本的Django。 - Michael Gendin
2
仅覆盖get_available_name的问题在于,当您上传具有相同名称的文件时,服务器将陷入无限循环。由于_save检查文件名并决定获取新文件名,但是get_available_name仍然返回重复的文件名。因此,您需要同时覆盖两者。 - x1a0
1
哎呀,我们在两个问题中讨论这个问题,但是现在我才注意到它们略有不同)所以我在那个问题中是正确的,而你在这个问题中是正确的) - Michael Gendin

3

如果您使用应用程序的文件系统来存储文件,则答案可以正常工作。但是,如果您正在使用boto3并上载到诸如AWS S3之类的东西,并且可能希望将已存在于S3存储桶中的文件设置为模型的FileField,则需要执行以下操作。

我们有一个带有filefield的简单模型类:

class Image(models.Model):
    
    img = models.FileField()
    owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='images')

    date_added = models.DateTimeField(editable=False)
    date_modified = models.DateTimeField(editable=True)

from botocore.exceptions import ClientError
import boto3
    
s3 = boto3.client(
    's3',
    aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
    aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY")
)

s3_key = S3_DIR + '/' + filename
bucket_name = os.getenv("AWS_STORAGE_BUCKET_NAME")

try:
    s3.upload_file(local_file_path, bucket_name, s3_key)
    # we want to store it to our db model called **Image** after s3 upload is complete so,
    image_data = Image()
    image_data.img.name = s3_key # this does it !!
    image_data.owner = get_user_model().objects.get(id=owner_id)
    image_data.save()
except ClientError as e:
    print(f"failed uploading to s3 {e}")


S3 KEY设置到FileFieldname字段中即可完成操作。 我已经测试了所有相关内容,例如在Django管理界面中预览图像文件,从数据库获取图像也会将根S3存储桶前缀(或CloudFront CDN前缀)附加到文件的S3键中。当然,前提是我已经为boto和S3的django settings.py设置工作正常。

1
你应该定义自己的存储方式,从FileSystemStorage继承,并重写OS_OPEN_FLAGS类属性和get_available_name()方法: Django版本:3.1

Project/core/files/storages/backends/local.py

import os

from django.core.files.storage import FileSystemStorage


class OverwriteStorage(FileSystemStorage):
    """
    FileSystemStorage subclass that allows overwrite the already existing
    files.
    
    Be careful using this class, as user-uploaded files will overwrite
    already existing files.
    """

    # The combination that don't makes os.open() raise OSError if the
    # file already exists before it's opened.
    OS_OPEN_FLAGS = os.O_WRONLY | os.O_TRUNC | os.O_CREAT | getattr(os, 'O_BINARY', 0)

    def get_available_name(self, name, max_length=None):
        """
        This method will be called before starting the save process.
        """
        return name

在您的模型中,使用您自定义的 OverwriteStorage。

myapp/models.py

from django.db import models

from core.files.storages.backends.local import OverwriteStorage


class MyModel(models.Model):
   my_file = models.FileField(storage=OverwriteStorage())

1

我曾经遇到过完全相同的问题!后来我意识到是我的模型引起了这个问题。例如,我的模型就像这样:

class Tile(models.Model):
  image = models.ImageField()

接下来,我想让多个瓷砖引用磁盘上的同一个文件!我找到的解决方法是改变我的模型结构如下:

class Tile(models.Model):
  image = models.ForeignKey(TileImage)

class TileImage(models.Model):
  image = models.ImageField()

之后我意识到这更有意义,因为如果我想要同一文件在我的数据库中保存多次,我必须为其创建另一个表格!

我猜你也可以像这样解决问题,只是希望你能改变模型!

编辑

此外,我猜你也可以使用不同的存储方式,比如 SymlinkOrCopyStorage。

http://code.welldev.org/django-storages/src/11bef0c2a410/storages/backends/symlinkorcopy.py


在你的情况下有意义,但不适用于我的情况。我不希望它被多次引用。我创建一个引用文件的对象,然后我发现其他属性中存在错误,我重新打开创建表单。在重新提交时,我不想丢失已经保存在磁盘上的文件。 - Guard
所以我猜你可以使用我的方法!因为你将拥有一个名为FormFile的表,它将仅保存文件,那么在你的Form表中,你将有一个该文件的FK!因此,您可以为同一文件更改/创建新表单!(顺便说一下,在我的主要示例中,我正在更改FK的顺序) - Arthur Neves
这里的限制可能是因为在Django中保存FileField时,它总是通过Django存储传递!所以强制文件路径是没有意义的!此外,Django如何知道文件已经存在于路径中?另一种方法是使用FilePathField!这样,您可以在数据库中设置路径,并以您认为最好的方式进行查找! - Arthur Neves
我猜我找到了一个Django存储库,可以帮助你实现你想要的目标。请检查我的帖子编辑! - Arthur Neves
谢谢你的努力,但这看起来真的很复杂。我可能只需要子类化ImageField(我实际使用的那个)并给它一个选项,强制它不重新保存文件。 - Guard
显示剩余3条评论

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