delhi09の勉強日記

技術トピック専用のブログです。自分用のメモ書きの投稿が多いです。あくまで「勉強日記」なので記事の内容は鵜呑みにしないでください。

DjangoでModelのFileFieldを文字列やファイルから初期化する方法

概要

DjangoでModelのFileFieldを文字列やファイルで初期化する方法が分からなくて調べていた。

※ 以下のようにModelFormを使用してファイルアップロード機能を実現する方法はいくつかヒットしたが、FileFieldを自分で初期化する方法はほとんど出てこなかった。

blog.narito.ninja


やりかたが分かったのでメモしておく。

結論

以下のように書ける。

前提

Modelの定義は以下とする。

class TextFile(models.Model):
    class Meta:
        db_table = "text_file"

    file_title = models.CharField(max_length=32)
    file_path = models.FileField(max_length=256)

またMEDIA_ROOTを定義しておくこと。

[settings.py]

MEDIA_ROOT = os.path.join(BASE_DIR, "media/")

①文字列から初期化する場合

from django.core.files.base import ContentFile

# 省略

text_file = TextFile(
    file_title="sample1", file_path=ContentFile("aaa", "sample1.txt")
)
text_file.save()

②ファイルから初期化する場合

from django.core.files import File

# 省略

with open("/tmp/aaa.txt", "r", encoding="utf-8") as f:
    text_file = TextFile(
        file_title="sample2",
        file_path=File(f, "sample2.txt"),
    )
    text_file.save()

結果

以下のように期待通りの結果となった。

・mediaディレクトリに保存されたファイル

$ tree media
media
├── sample1.txt
└── sample2.txt

・text_fileテーブルに保存されたデータ

sqlite> select * from text_file;
1|sample1|sample1.txt
2|sample2|sample2.txt

根拠

公式ドキュメントに、上記の初期化方法について直接説明している箇所はなかったが、以下のFile Uploadsのセクションに記載されている内容から、上記の初期化方法が正しいといえるのではないかと考えた。
docs.djangoproject.com

このドキュメントには

If you are constructing an object manually, you can assign the file object from request.FILES to the file field in the model:

とあり、以下のようにModelFormを使わずにViewでFileFieldを持つModelを初期化する方法が記載されている。

[公式ドキュメントのサンプルコード]

from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import UploadFileForm
from .models import ModelWithFileField

def upload_file(request):
    if request.method == 'POST':
        form = UploadFileForm(request.POST, request.FILES)
        if form.is_valid():
            instance = ModelWithFileField(file_field=request.FILES['file'])
            instance.save()
            return HttpResponseRedirect('/success/url/')
    else:
        form = UploadFileForm()
    return render(request, 'upload.html', {'form': form})

ここで、以下のセクションより、request.FILES['file']の型はUploadedFileとのことである。
docs.djangoproject.com

※ UploadedFileのドキュメントは以下
docs.djangoproject.com


Django本体のソースコードを読むと、UploadedFileはdjango.core.files.base.Fileを継承していることが分かる。
github.com

ここは推論になってしまうが、以下の理由よりModelのFileFieldは、django.core.files.base.File(派生クラスも含む)で初期化できる仕様になっていると考えられる。

The File class is a thin wrapper around a Python file object with some Django-specific additions. Internally, Django uses this class when it needs to represent a file.

  • 一般的な設計原則として、派生クラスであるUploadedFileに依存しているとは考えにくい。

加えて、「①文字列から初期化する場合」に関しては、django.core.files.base.Fileをファイルではなく文字列やバイト列から生成したい場合は、Fileオブジェクトの派生クラスであるdjango.core.files.base.ContentFileを使うことができる。
docs.djangoproject.com

補足

今回はテキストファイルを例としたが、バイナリファイル(画像など)でも同じ方法で実現できることも確認した。

参考

以下の記事を参考にさせて頂いた。

djangobrothers.com
note.crohaco.net
blog.narito.ninja
stackoverflow.com