自動化無しに生活無し

WEBとかAIとかLinux関係をひたすら書く備忘録系ブログ

Djangoで画像及びファイルをアップロードする方法【ImageFieldとFileField】【python-magicでMIMEの判定あり】

thumbnail

Djangoで画像やファイルをアップロードする方法をまとめる。

40分Djangoを理解している方向け。

流れ

  1. 必要なライブラリのインストール
  2. アプリの作成
  3. settings.pyの編集
  4. urls.pyの編集
  5. models.pyでフィールドの定義
  6. forms.pyでフォームを作る
  7. views.pyで受け取り処理
  8. templatesにフォームを設置
  9. マイグレーション
  10. 開発用サーバーの立ち上げ

必要なライブラリのインストール

pip install Pillow
pip install python-magic

Pillowは画像を保存するために必要なライブラリ。画像の加工もできる。後述のImageFieldを使用するために必須のライブラリである。

python-magicはアップロードされたファイルのMIME値を取得するためのライブラリ。

MIMEとはファイルの種類のこと。このMIMEの値をチェックすることでアップロードされたファイルがPDFか、MP4か、EXEかなどを調べられる。

アプリの作成

本記事では、uploadアプリを作った上で作業を行っている。

python manage.py startapp upload

こちらを実行しておく。

settings.pyの編集

INSTALLED_APPS にupload アプリを追加しておく。

INSTALLED_APPS = [

    # 省略

    "upload",
]

ファイルをアップロードするには、『アップロード先となるディレクトリ』、『アップロードしたファイルを公開するパス』を設定する必要がある。

以下をsettings.pyに書き込む。

MEDIA_URL   = "/media/"
MEDIA_ROOT  = BASE_DIR / "media"

MEDIA_URLはサイトにアクセスするクライアント側から見たURLを指定する。例えば、test.pngの場合、URLは127.0.0.1:8000/media/test.pngになる。

仮にMEDIA_URL"/mediafile/"の場合、URLは127.0.0.1:8000/mediafile/test.pngになる。

MEDIA_ROOTはサーバー側から見た画像ファイルの在り処を指定する。プロジェクトディレクトリ直下のmediaディレクトリを指定する。

【補足1】Django2.x以前の書き方

Django2.x以前の場合は下記のように書く

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

urls.pyの編集

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('upload.urls')),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

やっていることは、settings.pyで指定したMEDIA_URLMEDIA_ROOTに倣って、urlpatternsにパスを追加している。

ファイルアップロード時の保存先と公開先の指定はこれでOK

upload/urls.py

from django.urls import path
from . import views

app_name    = "upload"
urlpatterns = [
    path('album/', views.album, name="album"),
    path('document/', views.document, name="document"),
]

models.pyのモデルクラスにてImageField、FileFieldを追加

今回は、画像とファイルのアップロード機能を搭載させるため、下記のようになった。

from django.db import models

class Album(models.Model):

    photo       = models.ImageField(verbose_name="フォト",upload_to="upload/album/photo/")

class Document(models.Model):

    file        = models.FileField(verbose_name="ファイル",upload_to="upload/document/file/")

modelsImageFieldFileFieldを使用する。upload_to属性を指定してアップロード先を分けている。

【補足1】upload_toで指定するパスについて

upload_toが未指定もしくは空文字列であれば、settings.pyMEDIA_ROOTに基づき、プロジェクトディレクトリ直下のmediaに保存される。

ただ、全てのファイルがmediaディレクトリに保存されてしまうと、バックアップの作業が大変になる。そこで、upload_toを指定して適宜ディレクトリ分けをすることを推奨する。

可能であれば、パスは

[アプリ名]/[モデルクラス名]/[フィールド名]/

とすれば、重複することはないだろう。その場合、スネークケースで書いたほうが無難。

【補足2】ImageFieldはFileFieldを継承して作られている。

ImageFieldはもともとFileFieldを継承して作られている。

仕組みは同じだが、画像だけを受け取る仕様になっているため、全てのファイルを受け取るFileFieldとは違う。

画像かどうか判定するためにPillowを使用している。

だから、例えば同じようにPDFだけを受け取るフィールド、動画だけを受け取るフィールドを作って、複数のモデルで使いまわしたい場合。自前でFileFieldを継承して、それぞれPDFField、VideoFieldとしても良いかもしれない。

もっとも、PDFや動画を受け取るフィールドを、複数のモデルクラスで使いまわすほどの案件は無いと思われるが。

forms.pyでフォームを作る

from django import forms
from .models import Album,Document

class AlbumForm(forms.ModelForm):

    class Meta:
        model   = Album
        fields  = ['photo']

class DocumentForm(forms.ModelForm):

    class Meta:
        model   = Document
        fields  = ['file']

モデル利用して作る。フィールドを指定するだけでいい。

views.pyで受け取り処理

from django.shortcuts import render,redirect
from django.views import View

from .models import Album,Document
from .forms import AlbumForm,DocumentForm

import magic

ALLOWED_MIME    = [ "application/pdf" ]

class AlbumView(View):

    def get(self, request, *args, **kwargs):

        context             = {}
        context["albums"]   = Album.objects.all()

        return render(request,"upload/album.html",context)

    def post(self, request, *args, **kwargs):

        form    = AlbumForm(request.POST, request.FILES)
        
        if not form.is_valid():
            print("バリデーションNG")
            print(form.errors)
            return redirect("upload:album")

        print("バリデーションOK")
        form.save()

        return redirect("upload:album")

album   = AlbumView.as_view()

class DocumentView(View):

    def get(self, request, *args, **kwargs):

        context                 = {}
        context["documents"]    = Document.objects.all()

        return render(request,"upload/document.html",context)

    def post(self, request, *args, **kwargs):

        form        = DocumentForm(request.POST,request.FILES)

        if not form.is_valid():
            print("バリデーションNG")
            print(form.errors)
            return redirect("upload:document")

        mime_type   = magic.from_buffer(request.FILES["file"].read(1024) , mime=True)
        
        if not mime_type in ALLOWED_MIME:
            print("このファイルのMIMEは許可されていません。")
            print(mime_type)
            return redirect("upload:document")


        print("バリデーションOK")
        form.save()

        return redirect("upload:document")

document    = DocumentView.as_view()

アップロードされたファイルを保存する時、フォームクラスの第二引数にrequest.FILESをセットする必要がある。

また、その際にFileFieldの場合はpython-magicを使用して、ファイルのMIMEタイプを調べている。許可されていないMIMEタイプであれば、保存はしない。

templatesにフォームを設置

注意するべきことは、formタグ内にenctype="multipart/form-data"を書いておくこと。

画像、ファイルいずれもenctype="multipart/form-data"がなければデータがアップロードされない。

画像アップロード用テンプレート

templates/upload/album.htmlを作る。これが画像ファイルのアップロードページ。内容は下記。

<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
	<title>画像アップロードのテスト</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>

    <h1 class="bg-primary text-center text-white">画像アップロードのテスト</h1>
    
    <main class="container">

        <p><a href="{% url 'upload:document' %}">ファイルのアップロードはこちら</a></p>

        <form method="POST" enctype="multipart/form-data">
            {% csrf_token %}
            <input type="file" name="photo">
            <input class="form-control" type="submit" value="送信">
        </form>

        {% for album in albums %}
        <div class="my-2">
            <img class="img-fluid" src="{{ album.photo.url }}" alt="投稿された画像">
        </div>
        {% endfor %}
    
    </main>

</body>
</html>

ファイルのアップロード用テンプレート

templates/upload/document.htmlを作る。これがファイルアップロードページ。

<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
	<title>ファイルアップロードのテスト</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>
    <h1 class="bg-primary text-center text-white">ファイルアップロードのテスト</h1>
    <main class="container">
    
        <p><a href="{% url 'upload:album' %}">画像のアップロードはこちら</a></p>

        <form method="POST" enctype="multipart/form-data">
            {% csrf_token %}
            <input type="file" name="file">
            <input class="form-control" type="submit" value="送信">
        </form>
    
        {% for document in documents %}
        <div class="my-2">
            <a href="{{ document.file.url }}">{{ document.file }}</a>
        </div>
        {% endfor %}

    </main>
</body>
</html>

【補足1】ファイル名だけを取り出したい場合は?

upload_toのフィールドオプションにはファイルの名前だけでなくmediaディレクトリ以下のファイルパスまで含まれている。

そのため、リンクタグの表示としてはあまり適切ではないかもしれない。そこでファイル名のみを表示させる必要が出てくる。

具体的には、モデルにメソッドを追加し、テンプレート側からそのメソッドを呼び出す。

import os

class Document(models.Model):
    file    = models.FileField(verbose_name="ファイル",upload_to="app/document/file/")

    def file_name(self):
        return os.path.basename(self.file.name)

詳細は下記記事にて解説されている。

【Django】FilefieldやImageFieldでファイル名だけを表示させる方法【モデルにメソッドを追加】

マイグレーション

マイグレーションを実行し、DBにモデルの内容を反映させる。

python manage.py makemigrations
python manage.py migrate 

【補足1】マイグレーション時の警告が出る場合はどうする?

今回の場合は新しくモデルを作っているので問題はないが、既存のモデルに上記のフィールドを追加する時、マイグレーション時に警告(You are Trying to add a non-nullable field)が出る。

ImageField及びFileFieldはDB上は文字列型扱い(格納されているのはファイルパス)なので、null=True,blank=Trueのフィールドオプションを追加するか、1度限りのデフォルト値として任意の文字列を指定すると良いだろう。

参照: 【Django】models.pyにフィールドを追加・削除する【マイグレーションできないときの原因と対策も】

開発用サーバーの立ち上げ

開発サーバーを立ち上げる。

python3 manage.py runserver 127.0.0.1:8000

こんなふうになればOK。

画像ファイルアップロード時の挙動

ファイルアップロードのページでは、pdfファイルのみ受け付ける。

PDFファイルのみ受け付けている

結論

今回ファイルアップロード時にMIMEタイプを調べたが、拡張子を調べる方法もある。

しかし、拡張子を調べる方法では、拡張子を書き換えられると想定外のファイルがアップロードされるリスクがある。

例えば、本来であれば.exeファイルを.pdfに書き換えてアップロードする場合。拡張子を調べる方法では、本来は.exe.pdfファイルのアップロードを許してしまう。

これはセキュリティ上、非常に好ましくない。

故に、今回ファイルタイプであるMIMEを元にバリデーションを行った。

ソースコード

https://github.com/seiya0723/django_fileupload

スポンサーリンク

シェアボタン

Twitter LINEで送る Facebook はてなブログ