コンテンツにスキップ

Djangoのファイルアップロード実装ガイド|FileField・バリデーション・S3対応

カテゴリ:Django・Python

Django はファイルアップロードのための FileFieldImageField、フォームベースのバリデーション、django-storages による S3 連携まで、充実したエコシステムを持っています。本記事では models.py のフィールド定義から、forms.py でのバリデーション、settings.pyMEDIA_ROOT/MEDIA_URL 設定、django-storages を使った AWS S3 への保存まで、実務レベルのファイルアップロード実装を体系的に解説します。

FileField と ImageField の定義

FileField はあらゆるファイルに対応し、ImageField は画像専用(Pillow が必要)です。どちらもデータベースには実際のファイルではなくファイルパス(文字列)が保存され、実ファイルは MEDIA_ROOT 以下に格納されます。

# models.py
from django.db import models


def upload_to_documents(instance, filename):
    """ユーザーIDごとにサブディレクトリを作成するアップロードパス関数"""
    return f'documents/{instance.user.id}/{filename}'


class UserProfile(models.Model):
    user = models.OneToOneField('auth.User', on_delete=models.CASCADE)

    # 画像フィールド(Pillowが必要: pip install Pillow)
    avatar = models.ImageField(
        upload_to='avatars/',   # MEDIA_ROOT/avatars/ 以下に保存
        blank=True,
        null=True,
        verbose_name='プロフィール画像',
    )

    def __str__(self):
        return str(self.user)


class Document(models.Model):
    title = models.CharField(max_length=255)
    file = models.FileField(
        upload_to=upload_to_documents,  # 関数でパスを動的に決定
        verbose_name='添付ファイル',
    )
    uploaded_at = models.DateTimeField(auto_now_add=True)
    file_size = models.PositiveIntegerField(default=0)
    mime_type = models.CharField(max_length=100, blank=True)

    class Meta:
        ordering = ['-uploaded_at']

    def save(self, *args, **kwargs):
        # 保存時にファイルサイズを記録
        if self.file:
            self.file_size = self.file.size
        super().save(*args, **kwargs)

    def delete(self, *args, **kwargs):
        # レコード削除時に実ファイルも削除
        storage = self.file.storage
        path = self.file.name
        super().delete(*args, **kwargs)
        storage.delete(path)

forms.py でのバリデーション実装

Django のフォームを使うと、ファイルのMIMEタイプ・サイズ・拡張子などを clean_フィールド名() メソッドで検証できます。カスタムバリデーターをモデルフィールドに付けることも可能です。

# forms.py
from django import forms
from django.core.exceptions import ValidationError
from .models import Document

# 許可するMIMEタイプの定義
ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
ALLOWED_DOC_TYPES = ['application/pdf', 'application/msword',
                     'application/vnd.openxmlformats-officedocument.wordprocessingml.document']

MAX_UPLOAD_SIZE = 10 * 1024 * 1024  # 10MB


class AvatarUploadForm(forms.Form):
    avatar = forms.ImageField(
        label='プロフィール画像',
        help_text='JPEG・PNG・GIF・WebP、最大5MB',
    )

    def clean_avatar(self):
        image = self.cleaned_data.get('avatar')
        if not image:
            return image

        # ファイルサイズの検証
        max_size = 5 * 1024 * 1024  # 5MB
        if image.size > max_size:
            raise ValidationError(
                f'ファイルサイズは5MB以下にしてください(現在: {image.size // 1024 // 1024}MB)'
            )

        # MIMEタイプの検証(content_type はクライアント申告値なので補助的に使用)
        if image.content_type not in ALLOWED_IMAGE_TYPES:
            raise ValidationError(
                f'許可されていないファイル形式です({image.content_type})。'
                'JPEG・PNG・GIF・WebP のみ使用できます。'
            )

        return image


class DocumentUploadForm(forms.ModelForm):
    class Meta:
        model = Document
        fields = ['title', 'file']

    def clean_file(self):
        file = self.cleaned_data.get('file')
        if not file:
            return file

        # ファイルサイズ検証
        if file.size > MAX_UPLOAD_SIZE:
            raise ValidationError(
                f'ファイルサイズは{MAX_UPLOAD_SIZE // 1024 // 1024}MB以下にしてください。'
            )

        # MIMEタイプ検証
        if file.content_type not in ALLOWED_DOC_TYPES:
            raise ValidationError('PDF または Word ファイルのみアップロードできます。')

        # 拡張子の検証
        ext = file.name.rsplit('.', 1)[-1].lower()
        if ext not in ['pdf', 'doc', 'docx']:
            raise ValidationError('ファイルの拡張子が不正です。')

        return file

views.py でのアップロード処理

Django ではマルチパートフォームのファイルは request.FILES から取得します。フォームクラスに request.FILES を渡すだけでバリデーションが実行されます。

# views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from django.views import View
from .forms import DocumentUploadForm


class DocumentUploadView(View):
    template_name = 'documents/upload.html'

    def get(self, request):
        form = DocumentUploadForm()
        return render(request, self.template_name, {'form': form})

    def post(self, request):
        # request.POST と request.FILES の両方を渡す
        form = DocumentUploadForm(request.POST, request.FILES)

        if form.is_valid():
            document = form.save(commit=False)
            document.user = request.user  # ログインユーザーを設定
            document.save()
            messages.success(request, 'ファイルをアップロードしました。')
            return redirect('documents:list')

        return render(request, self.template_name, {'form': form})


# REST API(Django REST Framework)の場合
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework import status


class DocumentUploadAPIView(APIView):
    parser_classes = [MultiPartParser, FormParser]

    def post(self, request):
        file = request.FILES.get('file')
        if not file:
            return Response({'error': 'ファイルが見つかりません。'}, status=status.HTTP_400_BAD_REQUEST)

        # バリデーション
        if file.size > 10 * 1024 * 1024:
            return Response({'error': 'ファイルが大きすぎます(上限10MB)。'}, status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE)

        # 保存処理(forms.py のロジックを再利用することを推奨)
        from django.core.files.storage import default_storage
        path = default_storage.save(f'uploads/{file.name}', file)

        return Response({'path': path, 'url': default_storage.url(path)})

settings.py の MEDIA_ROOT と MEDIA_URL 設定

開発環境でメディアファイルを配信するには MEDIA_ROOT(保存先のディレクトリ)と MEDIA_URL(URL のプレフィックス)を設定し、urls.py に静的ファイル配信を追加します。

# settings.py
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

# メディアファイルの保存先(開発環境)
MEDIA_ROOT = BASE_DIR / 'media'

# メディアファイルの URL プレフィックス
MEDIA_URL = '/media/'

# ファイルアップロードのデフォルト設定
# 2.5MB 以下はメモリ上で処理、それ以上は一時ファイルに書き込む
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440  # 2.5MB

# Django が受け付けるリクエストボディの上限
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024  # 10MB

# アップロードハンドラーのカスタマイズ(デフォルト)
FILE_UPLOAD_HANDLERS = [
    'django.core.files.uploadhandler.MemoryFileUploadHandler',
    'django.core.files.uploadhandler.TemporaryFileUploadHandler',
]
# urls.py(開発環境でのメディアファイル配信)
from django.conf import settings
from django.conf.urls.static import static
from django.urls import path, include

urlpatterns = [
    # ... アプリの URL
]

# 開発環境のみメディアファイルを Django で配信
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

django-storages を使った S3 設定

django-storagesboto3 を使うと、settings.py に数行追加するだけでメディアファイルの保存先を S3 に切り替えられます。

pip install django-storages[s3] boto3
# settings.py(S3 設定)
import os

INSTALLED_APPS = [
    # ...
    'storages',
]

# S3 バックエンドを使用
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'

# 静的ファイルも S3 に配置する場合
# STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage'

AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME')
AWS_S3_REGION_NAME = os.environ.get('AWS_S3_REGION_NAME', 'ap-northeast-1')

# S3 の URL 設定
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/media/'

# CloudFront を使う場合
# AWS_S3_CUSTOM_DOMAIN = 'xxxxxxxx.cloudfront.net'

# ファイルの公開設定(デフォルトは private)
AWS_DEFAULT_ACL = 'private'
AWS_S3_OBJECT_PARAMETERS = {
    'CacheControl': 'max-age=86400',  # 1日キャッシュ
}

# アップロードファイルのプレフィックス
AWS_LOCATION = 'media'

# 署名付きURL の有効期間(秒)
AWS_QUERYSTRING_EXPIRE = 3600  # 1時間

# HTTPS を強制
AWS_S3_USE_SSL = True

ファイルサイズ制限のカスタムバリデーター

モデルフィールドに validators を指定することで、フォームとモデル両方でバリデーションが適用されます。

# validators.py
from django.core.exceptions import ValidationError


def validate_file_size(value, max_mb=10):
    """ファイルサイズを検証するバリデーター"""
    max_bytes = max_mb * 1024 * 1024
    if value.size > max_bytes:
        raise ValidationError(
            f'ファイルサイズは {max_mb}MB 以下にしてください。'
            f'(現在: {value.size / 1024 / 1024:.1f}MB)'
        )


def validate_file_extension(value, allowed_extensions=None):
    """ファイル拡張子を検証するバリデーター"""
    if allowed_extensions is None:
        allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf']

    ext = value.name.rsplit('.', 1)[-1].lower()
    if ext not in allowed_extensions:
        raise ValidationError(
            f'許可されていない拡張子です(.{ext})。'
            f'使用可能: {", ".join(allowed_extensions)}'
        )


# models.py での使用例
from django.db import models
from .validators import validate_file_size, validate_file_extension


class Attachment(models.Model):
    file = models.FileField(
        upload_to='attachments/',
        validators=[
            validate_file_size,
            lambda v: validate_file_extension(v, ['pdf', 'docx', 'xlsx']),
        ],
    )

この記事で使えるテストファイル(無料)

📚 関連記事

PNG vs WebP vs AVIF|画像フォーマットの選び方と変換方法

PNG / JPEG / WebP / AVIF の特徴・用途・ブラウザ対応状況を比較。picture 要素での出し分け、DevLab の画像フォーマット変換ツールの使い方も解説。

2026-04-18

Whois でドメイン情報を調べる方法|有効期限・ネームサーバー・登録者

Whois でわかること (登録者・有効期限・レジストラ・NS)、GDPR によるプライバシー保護の影響、ドメイン管理の実務的な使い方を解説。

2026-04-18

HTTP ステータスコード完全ガイド|よくあるエラーの原因と対処法

開発者が頻出する HTTP ステータスコード (200/301/302/400/401/403/404/413/422/429/500/502/503/504) の意味・原因・対処法を解説。301 vs 302 の SEO 影響、400 vs 422 の使い分けも。

2026-04-18

cURL コマンドを JavaScript fetch・Python requests に変換する方法|DevTools 連携

Chrome DevTools の Copy as cURL を fetch / axios / Python requests / PHP cURL / Go net/http に変換する手順を解説。主要 cURL オプション (-X / -H / -d / -F / -u / -b / -L) の変換パターン、認証トークンの扱い、注意点まで。

2026-04-16

Cookie のセキュリティフラグ完全ガイド|Secure / HttpOnly / SameSite / __Host-

Cookie のセキュリティ属性 Secure / HttpOnly / SameSite (Strict/Lax/None) / __Host- __Secure- プレフィックス / 4096 バイト制限を解説。CSRF / XSS / セッションハイジャック対策と、Laravel / Express の実装例。

2026-04-16

JWT のセキュリティベストプラクティス|alg none 攻撃 / 有効期限 / 署名検証

JWT (JSON Web Token) の代表的な脆弱性 6 種類 (alg none 攻撃 / 鍵混同 / 無期限トークン / payload への機密情報 / 失効不可 / 弱いシークレット) と対策。リフレッシュトークンパターン、失効リスト、HttpOnly Cookie 格納まで。

2026-04-16