【Django】models.FileFieldにカスタムメソッドを加えてファイルのアップロード&ダウンロード


投稿日 2020年8月21日 >> 更新日 2023年3月1日

今回は、モデルフィールドの「FileField」についてご紹介していこうと思います。

DjangoのモデルフィールドでFileFieldを設定しておけば、Webサイト上でファイルをアップロードして管理することができます。

画像のアップロードでいうと「ImageFiled」のようなものです。

開発環境と本番環境ではファイルの保存先の設定が多少異なりますが、簡単に設定することができます。

ファイルの管理場所は「media」ディレクトリ内なので、Imagefiledを設定済みであればモデルフィールドの設定だけで大丈夫です。

その他の設定では、様々なファイルに対応した保存方法と、カスタムメソッドを使用した特定の要素の呼び出しです。

基本設定から行っていくので、興味がありましたら進んでみて下さい。

実行環境&使用ライブラリ

実行環境
Windows Subsystem for Linux
Python 3.6.9
pip 9.0.1
使用ライブラリ ライセンス
Django==3.1 BSD
django-cleanup==5.0.0 MIT

ファイルを扱う際に便利なアイテム

まずDjangoはインストール済みとして、ファイルを扱う際に便利なアイテムをインストールします。

それは「django-cleanup」ライブラリです。

django-cleanupを使用することで、ファイルが削除・更新された際に実際のファイルも削除してくれます。

通常ですとデータを削除したと思っても、実際保存されているデータはファイルの「パス」、つまりURLに過ぎないので、ファイル本体は削除されていません。

もしファイルの本体も削除するのであれば手動で消すことになるので大変面倒になります。

なので、django-cleanupを使用する場合はインストールしておきましょう。

# 実行

$ pip install django-cleanup

settings.pyのINSTALLED_APPSにも記述しておきます。

# config/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_cleanup.apps.CleanupConfig',  # django-cleanup
    'test_app.apps.TestAppConfig',  # app名
]

FileFieldの設定

ではモデルを構築していきます。

最初は単純な設定から、徐々に複雑な処理へとカスタマイズしていきます。

まずは単純な例から試してみます。

# test_app/models.py

from django.db import models


class FileUpload(models.Model):
    upload = models.FileField(upload_to='file/%Y/%m/%d')

公式はこちら

Django --FileField

FileFieldの引数である「upload_to」には、ファイルの保存先であるディレクトリを指定しています。

Djangoでは通常画像などのファイル類は「media」ディレクトリ内で管理され、上記の記述通りだと、「media/file/%Y/%m/%d/ファイル」へとファイルがアップロードされます。

そしてupload_toに記述した「%Y/%m/%d」ですが、Python標準ライブラリのdatetimeオブジェクトで表現される日付・時刻等のファーマットです。

実際に記述される値は、「file/2020/08/20/ファイル」のような形です。

次に、管理画面で扱えるようにしておきます。

# test_app/admin.py

from django.contrib import admin
from .models import FileUpload


admin.site.register(FileUpload)

最後に、開発環境下でファイル管理が行えるように「media」ディレクトリを設定します。

# config/settings.py

import os  # インポート

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

...

STATIC_URL = '/static/'

""" --------------追記----------------"""
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# config/urls.py

from django.contrib import admin
from django.urls import path, include

""" -----------インポート------------"""
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('test_app.urls')),
]

""" ----------------デバッグがTrueだった場合----------------------"""
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

これでアップロードされるファイルは、mediaディレクトリ内に作成されます。

では、データベースを反映させてから管理画面にてアップロードしていきます。

# 実行

$ python3 manage.py makemigrations test_app

$ python3 manage.py migrate test_app

# スーパーユーザー
$ python3 manage.py createsuperuser
....
....
....

test.csvというファイルをアップロードしてみました。

URLの部分「file/2020/08/20/test.csv」をクリックすると、ダウンロードすることができます。

ファイルは以下のような階層で管理されています。

media
   |--file
        |--2020
             |--08
                 |--20
                     |--test.csv

次に、テンプレートファイルに記述して要素を表示してみます。

まずはアプリディレクトリ内の「urls.py」と「views.py」を編集します。

# test_app/urls.py

from django.urls import path
from . import views

app_name = 'test_app'

urlpatterns = [
        path('', views.index, name='index'),
]
# test_app/views.py

from django.shortcuts import render
from .models import FileUpload


def index(request):
    file_obj = FileUpload.objects.all()
    return render(request, 'test_app/index.html', {'file_obj': file_obj})

次にテンプレートファイル(templates/test_app/index.html)を作成します。

<!-- test_app/templates/test_app/index.html -->

<!doctype html>
<html>
        <head>
                <title>テストアプリ</title>
        </head>
        <body>
                <a href="#">ファイルのアップロード</a>
                <br>

                <h2>ダウンロードファイル一覧</h2>
                <br>

                {% for file in file_obj %}
                    <p><a href="{{ file.upload.url }}">{{ file.upload.name }}</a></p>
                {% endfor %}
        </body>
</html>

file_objの要素をそれぞれ取り出すと

属性 要素
file.upload.url http://127.0.0.1:8000/media/file/2020/08/20/test.csv
file.upload.name file/2020/08/20/test.csv

となります。

ではrunserverを実行して表示してみましょう。

リンクをクリックすると管理されているファイルがダウンロードできるようになります。

次に、Formクラスを使ったファイルのアップロード画面を作成していきます。

Formクラスの設定

DjangoのFormクラスを継承する事によって、管理画面で追加・編集を行うチェンジフォームをテンプレートファイルで表現することができます。

そうするには、アプリディレクトリ内に「forms.py」という分かりやすいファイル作成し、以下のように記述します。

# test_app/forms.py

from django import forms
from .models import FileUpload


class FileUploadForm(forms.ModelForm):

    class Meta:
        model = FileUpload
        fields = ('upload',)

FormクラスにMetaオプションを設定して、model属性に使用したいモデルを当てはめます。

モデルを当てはめたら、fields属性にモデルで定義されているフィールド名を設定します。

今回は一つだけですが、複数のフィールドを持つ場合は、1つ以上のフィールドを当てはめ、リストもしくわタプルで設定した順序通り(左からトップフォーム)に並べられます。

詳しくはこちらにあります。

ではアプリディレクトリ内のurls.pyにアップロードページ用のURLパターンを記述して、views.pyにてアップロードの処理を追記します。

# test_app/urls.py

from django.urls import path
from . import views

app_name = 'test_app'

urlpatterns = [
        path('', views.index, name='index'),
        path('new_file', views.new_file, name='new_file'),  # 追加
]
# test_app/views.py

from django.shortcuts import render, redirect  # 追記
from .forms import FileUploadForm  # 追記
from .models import FileUpload


....

""" -------------アップロードページ-------------"""
def new_file(request):

    if request.method == "POST":
        form = FileUploadForm(request.POST, request.FILES)
        if form.is_valid():
            form.save()
            return redirect('test_app:index')
    else:
        form = FileForm()
    return render(request, 'test_app/new_file.html', {'form': form })

ファイルをアップロードする場合は、FileUploadFormの引数に「request.FILES」を設定します。

この中に、HTMLのformタグから受け取った値を辞書のような形で格納されます。

上手くアップロードできれば「redirect」によりトップページへ遷移されます。

次にアップロードするためのテンプレートファイルを「new_file.html」として作成します。

<!-- test_app/templates/test_app/new_file.html -->

<!doctype html>
<html>
        <head>
        </head>
        <body>
            <a href="{% url 'test_app:index' %}">トップ</a>
            <br>

            <form enctype="multipart/form-data" action="{% url 'test_app:new_file' %}" method="POST">
                {% csrf_token %}
                {{ form }}
                <button type="submit">保存</button>
            </form>
        </body>
</html>

Django公式にもありますが、formタグの「enctype」を定義しなければ空のフォームとなってしまいアップロードできません。

<form enctype="multipart/form-data" action="{% url 'test_app:new_file' %}" method="POST">

ではアップロードページに遷移できるよう「index.html」を編集して表示させてみます。

<!-- test_app/templates/test_app/index.html -->

<!doctype html>
<html>
        <head>
                <title>テストアプリ</title>
        </head>
        <body>
                <a href="{% url 'test_app:new_file' %}">ファイルのアップロード</a>
                <br>

                <h2>ダウンロードファイル一覧</h2>
                <br>

                ...
        </body>
</html>

空のファイルをアップロードしようとすると、受け付けないようになっています。

これで自作したフォームからファイルのアップロードが可能となりましたが、ファイル名が同じだった場合Djangoはファイル名の後ろに適当なローマ字をつけ足します。

個人的に少し見栄えが悪いので、ファイル名が被っても意味のあるファイル名に置き換えられるよう設定していきたいと思います。

ファイル名の設定

ファイルをアップロードするとき、同じファイル名で合った場合Djangoは被ったファイル名の後ろに適当なローマ字を付け加えます。

media
   |--test.csv
   |--test_EIagWrf.csv  # 既に同じファイル名が存在している場合

これは少し見栄えが悪く思うので、ファイル先のディレクトリが保存される前にファイル名を予め設定してから保存されるようにしていきたいと思います。

モデルフィールドの「FileFiled」引数に「upload_to」を設定しています。

この「upload_to」に独自で定義した関数を当てはめられるようになっているため、ファイルの行き先が保存される前にアップロードされてくるファイル名に任意の数字、または文字を付け加えます。

ここでは「年/月/日」というディレクトリ構造でファイルが保存されるので、ファイル名の前にその日の「時-分-秒」をタイムスタンプとして付け加えたいと思います。

ではmodels.pyを編集します。

# test_app/models.py

from django.db import models
import os
import datetime

""" ----upload_toに渡すディレクトリ用の関数---- """
def dir_path_name(instance, filename):
    date_time = datetime.datetime.now()  # 現在の時刻を取得
    date_dir = date_time.strftime('%Y/%m/%d')  # 年/月/日のフォーマットの作成
    time_stamp = date_time.strftime('%H-%M-%S')  # 時-分-秒のフォーマットを作成
    new_filename = time_stamp + filename  # 実際のファイル名と結合
    dir_path = os.path.join('file', date_dir, new_filename)  # 階層構造にする
    return dir_path

class FileUpload(models.Model):
    upload = models.FileField(upload_to=dir_path_name)

dir_path_name関数が最後に出力する要素は、「file/%Y/%m/%d/%H-%M-%Sファイル」となります。

関数の設定は公式ドキュメントにもあります。

モデルのフィールドを編集したので、マイグレーションコマンドで反映させてからサーバーを再起動させます。

# 実行

$ python3 manage.py makemigrations test_app

$ python3 manage.py migrate test_app

$ python3 manage.py runserver

ファイルをアップロードしてみると

上図のようにタイムスタンプ付きのファイルが作成されます。

他にも色々な方法でファイル名を変更させていくやり方があると思うので、余力がある方は探してみて下さい。

次は、ファイルの拡張子に従って保存先のディレクトリ構造を変更させていく処理を行いたいと思います。

ファイルの拡張子によるディレクトリ構造の指定

これまでは、「file/...」に続くディレクトリ構造でしたが、拡張子に従って、例えばCSVファイルをアップロードした場合は「csv/...」、PDFファイルをアップロードした場合は「pdf/...」といった具合にディレクトリを決めていきたいと思います。

先ほどのファイル名の設定で関数を定義しましたが、その関数で条件分岐を記述して処理を実行させます。

# test_app/models.py

from django.db import models
import os
import datetime


def dir_path_name(instance, filename):

    file_type = os.path.splitext(filename)  # ファイル名と拡張子を分ける
    date_time = datetime.datetime.now()
    date_dir = date_time.strftime('%Y/%m/%d')
    time_stamp = date_time.strftime('%H-%M-%S-')
    new_filename = time_stamp + filename

    """ -------追記--------- """
    # 1番目の要素である拡張子と同じであれば処理が実行される。
    if file_type[1] == '.csv':
        path = os.path.join('csv', date_dir, new_filename)
    elif file_type[1] == '.pdf':
        path = os.path.join('pdf', date_dir, new_filename)
    elif file_type[1] == '.txt':
        path = os.path.join('txt', date_dir, new_filename)
    return path

class FileUpload(models.Model):
    upload = models.FileField(upload_to=dir_path_name)

上記の処理では、「CSV」「PDF」「TEXT」ファイルのみそれぞれの処理が実行されるようになっているので、Pythonファイルの「PY」ファイルがアップロードされるとエラーとなってしまいます。

それを防ぐ為にFIleFiledにオプションを設定し、「CSV」「PDF」「TEXT」ファイル以外は受け付けないようにします。

# test_app/models.py

from django.db import models
import os
import datetime
from django.core.validators import FileExtensionValidator  # インポート


....

class FileUpload(models.Model):
    upload = models.FileField(upload_to=dir_path_name, validators=[FileExtensionValidator(['csv', 'pdf', 'txt'])])

FileField引数に「validators」を設定することで、アップロードされる値に対して何かしらの制御をすることができます。

validatorsに定義している「FileExtensionValidator」メソッドはファイルに関する制御を実行してくれて、他にも色々なメソッドが用意されています。

モデルフィールドを編集したらマイグレーションを実行し反映させます。

これで拡張子による振り分けと、アップロードの制御を終えたので、それぞれのタイプをアップロードしてみます。

※既に作成済みのファイルは削除しています。

それぞれ拡張子に合わせてディレクトリが割り当てられました。

ではPythonファイルをアップロードしてみます。

すると

上図のように指定した拡張子以外は受け付けないようになります。

カスタムメソッドで要素をファイル名のみに置き換える

ここまでアップロードされた要素は「file/2020/08/20/test.csv」のようにディレクトリ構造となっています。

もちろんデータベースには「file/2020/08/20/test.csv」のように保存されているためデータそのものを取得していることになります。

できれば「test.csv」のようにディレクトリ構造を抜いたファイル名を要素として取得するのが理想かと思います。

そのような処理を行うにも色々な処理の仕方がありますが、ここではモデルにカスタムメソッドを定義してファイル名だけを取得できるオブジェクトの属性を作成したいと思います。

# test_app/models.py

from django.db import models
import os
...


...

class FileUpload(models.Model):
    upload = models.FileField(upload_to=dir_path_name, validators=[FileExtensionValidator(['csv', 'pdf', 'txt'])])

    """ -----file_name属性として作成----- """
    def file_name(self):
        path = os.path.basename(self.upload.name)  # ファイル名のみ抽出
        return path

このカスタムメソッドは、オブジェクトの属性として取得することができます。

Djangoのshellコマンドを開いて確かめてみます。

# 実行

$ python3 manage.py shell
>>>
>>> from test_app.models import FileUpload
>>>
# オブジェクトを変数に格納
>>> file_data = FileUpload.objects.all()
>>>
# それぞれのフィールド属性やカスタムメソッドの属性を取り出す。
>>> for row in file_data:
...     print(row.upload.url)
...     print(row.upload.name)
...     print(row.upload.size)
...     print(row.file_name())
...
/media/csv/2020/08/20/22-14-30-test.csv
csv/2020/08/20/22-14-30-test.csv
24
22-14-30-test.csv
/media/pdf/2020/08/20/22-14-38-test.pdf
pdf/2020/08/20/22-14-38-test.pdf
15
22-14-38-test.pdf
/media/txt/2020/08/20/22-14-47-test.txt
txt/2020/08/20/22-14-47-test.txt
15
22-14-47-test.txt
>>>
>>> quit()

実際保存されているデータはアクセス先のpathに過ぎないので、ファイル名のみを取得する場合は独自で処理を行う必要があります。

もちろん他にも方法はありますが、参考にしていただけたら幸いです。

テンプレートファイルの「index.html」を編集して表示してみます。

<!-- test_app/templates/test_app/index.html -->

<!doctype html>
<html>
        <head>
                <title>テストアプリ</title>
        </head>
        <body>
                ...
                ...

                {% for file in file_obj %}
                    <p><a href="{{ file.upload.url }}">{{ file.file_name }}</a></p>
                {% endfor %}
        </body>
</html>

ちなみに、ファイルの容量(サイズ)を取得するには「file.upload.size」属性を取得します。

単位はbytesサイズです。

<!-- test_app/templates/test_app/index.html -->

<!doctype html>
<html>
        <head>
                <title>テストアプリ</title>
        </head>
        <body>
                ...
                ...

                {% for file in file_obj %}
                    <p><a href="{{ file.upload.url }}">{{ file.file_name }}</a> {{ file.upload.size }}B</p>
                {% endfor %}
        </body>
</html>

それでは最後に、ダウンロードファイル一覧の要素をテーブルタグで囲んでいきたいと思います。

<!-- test_app/templates/test_app/index.html -->

<!doctype html>
<html>
        <head>
                <title>テストアプリ</title>
        </head>
        <body>
                ...
                ...

                <table border="1" cellpadding="10">
                    <tr>
                        <th>ファイル名</th>
                        <th>サイズ(bytes)</th>
                    </tr>
                    {% for file in file_obj %}
                    <tr>
                        <td><a href="{{ file.upload_to.url }}">{{ file.file_name }}</a></td>
                        <td>{{ file.upload.size }}B</td>
                    </tr>
                    {% endfor %}
                </table>
        </body>
</html>

それでは以上となります。

最後までご覧いただきありがとうございました。