\ ポイント最大11倍! /

【Django REST framework】画像ファイルをアップロードする機能の実装方法

  • Django REST framework で画像アップロード機能を実装したい!

Django REST framework(以下、「DRF」とします)で画像アップロード機能を実装するには、いくつかのステップが必要になります。

これらのステップをできる限り単純化して、わかりやすく解説していきます。

DRF で画像アップロード機能を実装するまでの流れ

実装の全体的な流れから確認していきます。

  1. モデルの作成
  2. シリアライザの作成
  3. ビューの作成
  4. URL のルーティング
  5. フロントエンドの実装
  6. settings.py の整備

それぞれの手順について、必要性もあわせて詳しく解説します。

1. モデルの作成

画像ファイルを格納するモデルを用意します。

from django.db import models

class ImageModel(models.Model):
    # 画像ファイルのパスが格納される
    image = models.ImageField(upload_to="images/")

今回は画像を保存するので ImageField とし、upload_to という引数を設定しておきます。
この upload_to には、画像を格納するディレクトリを指定できます。

上記のサンプルコードでは image/ としています。この場合は MEDIA_ROOT で設定したディレクトリの下に image/ ディレクトリを掘って、その下に画像ファイルを保存されます。

2. シリアライザの作成

まずは serializers.py をアプリディレクトリ配下に作成します。

myproject/
    app/
        __init__.py
        models.py
        views.py
        serializers.py   # 追加
        urls.py
        ...
    myproject/
        settings.py
        urls.py
        ...
    manage.py

ここで気になるのが、「シリアライザって何者?」ということだと思います。

シリアライザの主な役割は次の二つです。

  1. JSON からモデルインスタンスに変換
  2. モデルインスタンスから JSON に変換

今回はアップロードされた画像を受け取るケースなので、JSONからモデルインスタンスに変換する時に使われることになります。

他にもバリデーションができたり Form 的な要素もあるのですが、この段階では上記の理解で十分です。

では、serializers.py の中身を見ていきましょう。

from rest_framework import serializers
from .models import ImageModel

class ImageModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = ImageModel
        fields = ["image"]

シリアライザにはいくつかの種類がありますが、今回はモデルインスタンスに対応するシリアライザである ModelSerializer を採用しました。

なお、今回のようなモデルベースのシリアライザでは Meta クラスの設定が必須になります。

model には対応するモデルを指定してください。

fields にはシリアライザとして受け入れたいフィールドの種類を指定します。例えば、シリアライザで画像データを受け入れつつ、他にもフィールドにも登録をしたい場合には登録したいフィールドをリストに渡しておくこととなります。

すべてのフィールドをシリアライザとして受け入れたい場合には、fields = “__all__” として指定できます。

3. ビューの作成

画像データを受け入れる際のロジックを views.py に書いていきましょう。ここでは最も基本的な APIView クラスを継承した例を掲載します。

from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import ImageModel
from .serializers import ImageModelSerializer

class ImageUploadView(APIView):
    def post(self, request, *args, **kwargs):
        file_serializer = ImageModelSerializer(data=request.data)
        if file_serializer.is_valid():
            file_serializer.save()
            return Response(file_serializer.data, status=status.HTTP_201_CREATED)
        else:
            return Response(file_serializer.errors, status=status.HTTP_400_BAD_REQUEST)

request.data は DRF ならではの属性で、リクエストボディが格納された辞書のようなオブジェクトです。これをシリアライザに渡すことで、画像ファイルを読み込むことができます。

ここで生成した file_serializer に対してバリデーション(is_valid)をしたり、モデルインスタンスを保存(save)したりすることができるようになります。

ここでは file_serializer.data をレスポンスとして返しています。この中には、例えば画像ファイルへのURLなどが含まれることになります。

file_serializers.errors のようにすると、バリデーションで失敗した内容が返されます。

4. URL のルーティング

urls.py 内でエンドポイントの URL を定義します。

from django.urls import path
from .views import ImageUploadView

urlpatterns = [
    path("upload/", ImageUploadView.as_view(), name="file-upload"),
]

# 開発用
if settings.DEBUG:
    urlpatterns += static(
        settings.MEDIA_URL,
        document_root=settings.MEDIA_ROOT,
    )

DEBUG 時にだけ if 文を組んでいるのは、本番ではアプリケーションサーバーを介さずにウェブサーバーから配信するからです。

また、MEDIA_ROOT と MEDIA_URL についても少し補足します。MEDIA_ROOT は、画像ファイルが物理的にサーバー上のどこに保存されるかを指定するものです。一方で MEDIA_URL は、MEDIA_ROOT で指定されたディレクトリにアクセスするための URL になります。

static() 関数は、静的ファイルへのアクセスに使われるメソッドぐらいに考えておけば OK です。

5. フロントエンドの実装

フロント側は色々な実装が考えられると思いますが、fetch 関数を使った例を挙げます。

const formData = new FormData();
formData.append('image', fileInput.files[0]);

fetch('http://yourserver.com/upload/', {
    method: 'POST',
    body: formData
})
.then(response => response.json())
.then(data => console.log(data))
.then(error => console.error(error));

6. settings.py の整備

最後に settings.py に以下の項目を追記します。

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

前述の通り、MEDIA_ROOT はサーバー上の保存場所を、MEDIA_URL は、MEDIA_ROOT で指定されたディレクトリにアクセスするための URL を制御するものになります。

より良い画像アップロード機能実装のためのヒント

これまでの手順で画像アップロード機能の実装は問題なくできますが、もう一歩進んだ実装を組み込みたい方向けにアドバンス的なテクニックをご紹介します。

画像のファイル名をUUIDにする

先ほどの方法では、保存するディレクトリは指定できてもファイル名の指定はできませんでした。

この場合、次のような問題が起こる可能性があります。

  • ファイル名が衝突する
  • URLが予測されてしまう

ユーザーがアップロードした画像の名前がそのままサーバー上でのファイル名になってしまうので、画像のファイル名が衝突を起こす可能性があります。
そして URL にファイル名が含まれることになるので、ファイル名が予測されることによりセキュリティが甘くなってしまう可能性を孕みます。

これを回避するために使われるのが、upload_to オプションです。

from uuid import uuid4
from django.db import models

def upload_to(instance, filename):
    # 拡張子を取得
    ext = filename.split(".")[-1]  
    # ファイル名としてUUIDを生成し、元の拡張子を維持
    filename = f"{uuid4()}.{ext}"
    # images/ディレクトリに保存されるようにパスを返す
    return f"images/{filename}"

class ImageModel(models.Model):
    # upload_toにupload_to関数を指定
    image = models.ImageField(upload_to=upload_to)

上記のように upload_to() 関数を用意して、ImageField(upload_to=upload_to) のように渡してあげます。これにより uuid4 で生成された文字列でファイル名が決まるので、ファイル名の衝突を避けてセキュリティも担保できるようになります。

画像の大きさを小さくする

クライアントから送られてくる画像は、極端にサイズの大きなものもあります。
そのような場合に無条件にデータを受け入れてしまうと、サーバーのストレージを圧迫したり、APIの通信速度にも影響が出てくるでしょう。

そこで、画像を受け入れるときにファイルサイズを小さくするロジックを組むと良いです。

方法はいくつかありますが、ここでは storage_backend.py を使った方法をご紹介します。

まず、storage_backend.py を以下の場所に作成します。

myproject/
    app/
        __init__.py
        models.py
        views.py
        serializers.py
        storage_backend.py   # 追加
        urls.py
        ...
    myproject/
        settings.py
        urls.py
        ...
    manage.py

このモジュールの中に、画像を save した後にサイズを縮小するというロジックを書いていきます。

from django.core.files.storage import FileSystemStorage
from PIL import Image

class ImageResizeStorage(FileSystemStorage):
    def save(self, name, content, *args, **kwargs)
        name = super().save(name, content, *args, **kwargs)

        max_size = 300
        full_path = self.path(name)
        with Image.open(full_path) as img:
            img.thumbnail((max_size, max_size))
            img.save(full_path)
        return name

さらに、このロジックを models.py で定義した ImageField に加えていきます。

image = models.ImageField(
    upload_to=upload_to,
    storage = ImageResizeStorage(),
)

以上で、画像を保存するたびに大きい画像であればリサイズしてくれる機能を実装することができます。

ここでの注意点は、この処理は画像保存のたびに同期的に行われるということです。つまり、トラフィックの多いサイトや、画像が頻繁に飛んでくるようなサイトの場合にはパフォーマンスが低下するおそれがあります。

そのような場合には Celery などを使って非同期で処理する仕組みを採用するのが良いと思われます。

この記事が気に入ったら
フォローしてね!

シェア・記事の保存はこちら!

この記事を書いた人

karo@プログラマのアバター karo@プログラマ プログラマ

「書くことで人の役にたつ」をモットーに活動中。
本職はプログラマで、Python(Django)が得意。最近ではフロント側の技術に触れる機会も増えてきました。
基本情報技術者試験合格。

コメント

コメントする

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)