コンテンツにスキップ

ファイルアップロード#

2024 iThome鉄人レース

これはDjango Ninjaシリーズチュートリアルの第23回です。ここから高度な機能について紹介を始めます!

現代のWebサービスにおいて、ファイルのアップロードは非常によく見られるシナリオです。

ユーザーが写真をアップロードするにせよ、添付ファイルを付けるにせよ、ファイルのアップロードは不可欠な機能です。

この記事では、Django Ninjaで画像のアップロード機能を実現する方法を、ユーザーの「プロフィール画像アップロード」(大頭貼と言うと可愛すぎる気がするので、以下ではすべてavatarと呼びます🥹)APIを例に、そのプロセスをステップバイステップで説明します。

この記事のすべてのコード変更は、このPRを参照してください。

GitHubサンプルプロジェクト#

👉 Django-Ninja-Tutorial


しかしその前に、この章にどのようなテーマがあるのかを理解しておく必要があります。

第6章「APIの高度な機能」概要#

APIプロジェクトにとって、高度な機能は私たちが複雑なシナリオ大規模プロジェクトの課題に対処するのに役立ちます。

これは入門ガイドではありますが、いくつかの一般的な高度な機能も取り上げます。これらの機能はAPIの柔軟性を高めるだけでなく、システムのパフォーマンスとユーザー体験も向上させます。

この章は5つの記事からなり、3つの一般的な高度な機能を紹介します:

これらの技術は大規模プロジェクトにとって極めて重要であるだけでなく、API開発において変化する要件に効果的に対応できるようにします。


この章の要点を理解したところで、最初の機能であるファイルのアップロードについて説明を始めます。

ファイルアップロードの主役——UploadedFile#

Django Ninjaでは、アップロードされたファイルを受け取るためにUploadedFileを使用できます。これはDjangoのUploadedFile再カプセル化したもので、両者は基本的にほぼ同じです。

UploadedFileの概要#

UploadedFileは、Djangoがファイルアップロードを処理するための中心的なオブジェクトです。ファイルがアップロードされると、Djangoは自動的にUploadedFileインスタンスとして扱い、後続の処理や保存をしやすくします。

UploadedFileオブジェクトには多くの属性がありますが、その中でよく使われるものは以下の通りです:

  • name:アップロードされたファイルの名前。これを使用してファイルの元のファイル名にアクセスできます。
  • size:ファイルのサイズ(バイト単位)。これを使用してファイルサイズの検証を行うことができます。
  • content_type:ファイルのMIMEタイプ。アップロードされたファイルの形式を検証する際に役立ちます。例えば、ファイルが画像形式であることを確認できます。この機能は後ほど使用します!
  • read()ファイルの内容を読み取るために使用します。カスタムのファイル処理を行う必要がある場合、このメソッドを使用してファイルのバイナリデータを取得できます。
  • chunks():ファイルが非常に大きい場合、このメソッドを使用してファイルを分割して読み取ることで、過剰なメモリの占有を避けることができます。

これらの特性により、UploadedFileは非常に柔軟であり、シンプルな画像のアップロードから大きなファイルの処理まで、様々なアップロード要件に対応できます。

OK、ファイルのアップロードについては、UploadedFileというコアコンポーネントを理解しておけば十分です。

「avatarアップロード」APIのコードを実装し始める前に、いくつか「事前準備」を行う必要があります。


ファイルのアップロードについては、多くの部分が実はDjango Ninjaの範疇ではなくDjangoに強く関連しているため、要点を絞って説明します。

Djangoプロジェクトの関連設定#

ファイルのアップロード機能を実装する前に、まずDjangoにアップロードされたファイルをどのように処理するかを伝える必要があります。これにはMEDIA_URLMEDIA_ROOTの設定が関わります。

MEDIA_URLMEDIA_ROOTの設定#

  • MEDIA_URL:これはファイルのURLプレフィックスであり、アップロードされたすべてのファイルはこのURLを通じてアクセスされます。
  • MEDIA_ROOT:これはDjangoサーバー内部で、アップロードされたファイルを実際に保存するパスです。

プロジェクトのsettings.pyに以下のコードを追加します:

# NinjaForum/settings.py
...

MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

これにより、アップロードされたファイルはプロジェクトのルートディレクトリ内のmediaフォルダに保存され、/media/パスを通じてアクセスできるようになります。

開発環境でのファイルアクセス#

開発環境でこれらのファイルに直接アクセスできるようにするために、Djangoが提供するstaticメソッドを使用する必要があります。

プロジェクトのurls.pyにこの行を追加します:

# NinjaForum/urls.py
from django.conf import settings
from django.conf.urls.static import static
...

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', api.urls),
    # 開発環境でアップロードされたファイルにアクセスできるようにする(開発環境専用)
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

このコードは、Djangoが開発環境で静的ファイルへのアクセスを提供することを許可します。

ImageFieldフィールドの作成#

ImageFieldはDjangoが画像を保存するために特化したフィールドであり、実際には画像のファイルパスを保存します。似たようなフィールドにFileFieldがあります。

コードは以下の通りです:

# user/models.py
class User(AbstractUser):
    ...
    avatar = models.ImageField(upload_to='avatars/', null=True)

先ほどのsettings.pyの設定と組み合わせることで、avatarフィールドはアップロードされた画像をmedia/avatars/フォルダに保存します。このパスはMEDIA_ROOTとフィールドのupload_toによって共同で決定されます。

このブランチに移動した後、プロジェクトにはすでに新しいマイグレーションファイルがあるので、データベースのマイグレーションを忘れないでください:

python manage.py migrate
# または
make migrate

ちなみに、ImageFieldはサードパーティのパッケージであるPillowに依存しています。このパッケージはフィールドに画像処理機能を提供するため、フィールドを正常に動作させるにはまずインストールする必要があります:

pip install Pillow
# または
poetry add pillow

Poetryを使用している読者は、マージ後のブランチで直接poetry installを実行すればOKです。


実装:avatarのアップロード#

事前準備が終わり、ついに本番に入ることができます。

以下は完全な「avatarアップロード」機能です:

from ninja import File, Router, UploadedFile
from ninja.errors import HttpError
...

@router.post('/users/{int:user_id}/avatar/',summary='avatarのアップロード')
def upload_avatar(
    request: HttpRequest,
    user_id: int,
    avatar_file: UploadedFile = File()
) -> dict[str, str]:
    """
    avatarのアップロード
    """
    # ファイルタイプのチェック
    if not avatar_file.content_type.startswith('image/'):
        raise HttpError(400, 'ファイルは画像フォーマットである必要があります')

    user = User.objects.get(id=user_id)
    user.avatar = avatar_file
    user.save()
    return {'detail': '画像のアップロードに成功しました'}

以下はこのコードの重要なポイントの解説です。

1. UploadedFileパラメータの定義#

view関数のシグネチャにおいて、UploadedFileは型ヒント(型ヒント)として使用され、avatar_fileパラメータはアップロードされたファイルを表します。

avatar_fileというパラメータ名は任意に命名できます。例えば、ドキュメントの例ではfileと呼ばれています。私がわざわざ別の名前を付けたのは、その名前が完全にカスタマイズ可能であることを強調したかったからです。

しかし!どんな名前を付けたとしても、リクエストを送信する際、body内のkey同じ名前を使用する必要があります。

この時のHTTPリクエスト以下のようになっているはずです:(body内のavatar_fileに注目してください)

POST /users/1/avatar/
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

# 以下はbodyの内容です
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar_file"; filename="example.jpg"
Content-Type: image/jpeg

(binary image data here)
------WebKitFormBoundary7MA4YWxkTrZu0gW--

Header内のContent-Typemultipart/form-dataであり、また各key-valueペアはContent-Disposition: form-data;で始まっています。このフォーマットにより、1つのリクエストでテキストやバイナリファイルを含む複数の異なる種類のデータを同時に送信することができます。

詳しくは、こちらの記事〈multipart/form-data入門〉を参照してください。

2. File関数#

=File()を定義する目的は、Django Ninjaに対して、このパラメータをHTTPリクエストの「アップロードファイル」部分から取得すべきであると伝えることです。似たようなやり方に、第11回で言及したQueryがあります。

このマーカーがないと、フレームワークはアップロードされた内容を正しく認識して処理できない可能性があります。

3. ファイルタイプのチェック#

UploadedFilecontent_type属性を使用して、このbody内容のファイルタイプを取得し、それが画像であることを確認してから処理を進めます。

# ファイルタイプのチェック
if not avatar_file.content_type.startswith('image/'):
    raise HttpError(400, 'ファイルは画像フォーマットである必要があります')

この方法は粗削りですが、シンプルな画像のアップロード機能には十分です。

プレーンテキストファイルをアップロードするテスト結果:

// 400 Bad Request
{
    "detail": "ファイルは画像フォーマットである必要があります"
}

本番環境では、例えば専用の画像処理パッケージを使用してファイル内容を検証するなど、より厳密なチェックが必要です。

4. 画像の保存#

最後に、画像をUseravatarフィールドに割り当て、save()メソッドを呼び出します。

Djangoはファイルの保存を自動的に処理し、もし名前が重複していれば、自動的に一意のファイル名を生成し、私たちが前に指定した場所にファイルを配置します。

APIを通じて全く同じavatarを2回アップロードした後、プロジェクトのルートディレクトリでtreeコマンドを使用して結果を見てみましょう:

 tree media
media
└── avatars
    ├── my-avatar.png
    └── my-avatar_gVwgCiG.png  # 同じファイル名で2回目アップロード、自動で名前変更

2 directories, 2 files

2回目にアップロードされた画像が「自動的に名前が変更されている」ことがわかります。これにより、ファイル名の一意性保証されます。

本番環境では、より良い管理とセキュリティを確保するために、ファイルの統一された命名規則を独自に定義することが最善です。


まとめと次のステップ#

この記事では、Django Ninjaでファイルのアップロード機能を実装する方法を、事前設定からAPIの実装まで、UploadedFileの使用方法を詳細に説明しました。

次は、もう一つの高度な機能であるページネーション(Pagination)を紹介します。これは、大量のデータをレスポンスとして返す際に、パフォーマンスとユーザー体験を効果的に向上させるのに役立ちます。