◀ 18.

Django のファイルアップロード|ファイル名と拡張子の取得

▶

Djangoで受信したファイルは request.FILES からフォームへ渡し、名前・サイズ等を確認してから保存します。画面に見せる申告名と、サーバーが管理する保存名を分けることが出発点です。

入口の認可とCSRF、フォームの検証、非公開保存を分けるDjangoアップロードの図

まず分ける:受信・検証・保存・公開

アップロードできることと、安全に公開できることは別です。名前をUUIDに変える、拡張子をbinにする、CSRFが通る、といった条件だけではファイルが無害とは分かりません。ここでは非公開領域への保存までを扱い、配信エンドポイントは作りません。
段階確認すること
入口認証済み利用者のアップロード権限とCSRF。さらにプロキシ等でリクエスト容量・受信時間・頻度を制限する。
受信後必須ファイルの有無、空ファイル、1ファイルのサイズ、個数、利用者別の容量枠。フォームの成功を内容検査と同一視しない。
保存元名とは別の名前、上書きしないストレージ、実際に採用された名前、失敗時の解放。
公開非公開保存を維持し、別途ダウンロード時にも対象ファイルへの権限を確認。UUIDは認可の代用ではない。

元の名前・拡張子の取得例

まず名前と拡張子を調べる元の例を示します。保存先の決定や内容の安全判定には使いません。完成例は「POSTフォームと保存をつなぐ例」を参照してください。

この記事の要点
  • Django でファイルのアップロードとアップロード後のファイル名取得
  • ビュー: request.FILES["upload_file"] でアップロードオブジェクトを取得
  • ファイル名: upload_file.name はクライアント申告名をDjangoが調整したメタ情報。保存パスに使わない
  • 保存先を分離: サーバー生成名を使い、storage.save() の戻り値を記録する。basenameやUUIDだけで内容の安全性は保証できない

 

インポート

import os

 

ビューの定義

以下は名前と拡張子だけを見る元の断片です。クラス本体・フォーム・応答・保存処理はなく、単独で動くビューではありません。request.FILES['upload_file'] は欠落時に例外になります。ファイルはPOSTかつmultipart/form-dataで送り、フォームへrequest.POSTとrequest.FILESの両方を渡します。元名を保存先へ連結したり、read()で無制限に全量を読み込んだりする処理は追加しません。

※テンプレート側のアップロードファイルのname属性を[upload_file]とする

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

    # ファイルの取得

    upload_file = request.FILES['upload_file']

 

    # ファイル名の取得

    upload_file_name = upload_file.name

 

    # 拡張子を除くファイル名(root)と拡張子(ext)の取得

    root, ext = os.path.splitext(upload_file_name)
 

 

POSTフォームと保存をつなぐ例

以下は同じソースを隔離テストで実行した教材です。upload_fixtureをPythonから読み込めるパッケージとして置き、テンプレート探索先へtemplatesを追加し、既存URL設定に接続します。既存の設定全体を置換しません。アップロード可能なのは認証済みかつ uploads.add_upload 権限を持つ利用者です。この権限名は連携アプリで定義・付与する前提で、ここではモデルや権限DBを作成しません。実環境ではSessionMiddleware・AuthenticationMiddleware・CsrfViewMiddleware等を適切に維持してください。

PRIVATE_UPLOAD_ROOT はWebサーバーの公開ディレクトリやMEDIA配信先から外れた専用の絶対パスへ設定します。教材の試験はOSが割り当てた一意なTemporaryDirectoryだけを使用します。テンプレートの自動エスケープを維持し、元名へsafe等を付けません。1ファイル1 MiB以下・空不可という教材上の方針で、拡張子やMIMEによる許可リスト、内容解析、マルウェア検査、総容量枠は実装していません。未信頼ファイルの受付を本番公開する完成品ではありません。

upload_fixture/forms.py

from django import forms


MAX_BYTES = 1024 * 1024


class UploadForm(forms.Form):
    upload_file = forms.FileField(max_length=200, allow_empty_file=False)

    def clean_upload_file(self):
        uploaded = self.cleaned_data["upload_file"]
        if uploaded.size > MAX_BYTES:
            raise forms.ValidationError("File must be at most 1 MiB.")
        return uploaded

upload_fixture/views.py

from uuid import uuid4

from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import render
from django.views.decorators.http import require_http_methods

from .forms import UploadForm


def save_upload(uploaded, storage):
    try:
        candidate = uuid4().hex + ".bin"
        return storage.save(candidate, uploaded)
    finally:
        uploaded.close()


@require_http_methods(["GET", "POST"])
def upload(request):
    # This permission must be defined by the integrating application.
    user = getattr(request, "user", None)
    if user is None or not user.is_authenticated or not user.has_perm("uploads.add_upload"):
        return HttpResponseForbidden("Upload permission required.")
    if request.method == "POST" and (
        set(request.FILES) - {"upload_file"}
        or len(request.FILES.getlist("upload_file")) > 1
    ):
        return HttpResponse("Exactly one upload_file is accepted.", status=400)
    form = UploadForm(request.POST, request.FILES) if request.method == "POST" else UploadForm()
    if request.method == "POST" and form.is_valid():
        uploaded = form.cleaned_data["upload_file"]
        original_name = uploaded.name
        storage = FileSystemStorage(
            location=settings.PRIVATE_UPLOAD_ROOT,
            allow_overwrite=False,
            file_permissions_mode=0o600,
            directory_permissions_mode=0o700,
        )
        try:
            saved_name = save_upload(uploaded, storage)
        except OSError:
            return HttpResponse("Storage unavailable.", status=503)
        return render(request, "uploaded.html", {
            "original_name": original_name, "saved_name": saved_name,
        }, status=201)
    status = 400 if request.method == "POST" else 200
    return render(request, "upload.html", {"form": form}, status=status)

upload_fixture/urls.py

from django.urls import path

from .views import upload


urlpatterns = [path("upload/", upload, name="upload")]

templates/upload.html

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Private upload fixture</title>
<form action="{% url 'upload' %}" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Upload</button>
</form>
</html>

templates/uploaded.html

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Stored privately</title>
<p>Original name (untrusted): {{ original_name }}</p>
<p>Storage key: {{ saved_name }}</p>
<p>No download URL is provided.</p>
</html>

request.FILES が空になるとき

この例のフィールド名は upload_file です。GETは未送信フォーム、POSTは空でも送信済みとしてバインドします。POSTでファイルを選ばない・nameが違う・multipart/form-dataでない場合は400になります。同名の複数ファイルと予期しないファイル項目も拒否します。通常のテキスト項目はrequest.POST、ファイルはrequest.FILESなので、UploadForm(request.POST)だけではファイルがバインドされません。フォーム側のmax_length=200はファイル名の長さで、ファイル容量の指定ではありません。複数受付へ拡張するならgetlist()で取り出すだけで済ませず、全件の検証と件数・合計サイズ・利用者のクォータを設計します。

メモリ閾値と拒否上限は違う

通常のアップロードハンドラーは小さな受信をメモリ、大きな受信を一時ファイルへ振り分けます。FILE_UPLOAD_MAX_MEMORY_SIZEはこの切替の閾値であり、超えたファイルを拒否する上限ではありません。この教材では閾値を1バイトへ下げても1024バイトのファイルがTemporaryUploadedFileとして保存できることを確認します。FILE_UPLOAD_TEMP_DIRは一時保存先、FILE_UPLOAD_PERMISSIONSは保存後の権限に関する設定で、容量枠ではありません。

5.2.18の標準MemoryFileUploadHandlerはリクエスト全体の長さを参照し、seek可能なストリームなら実サイズを調べます。単体ファイルのサイズだけでメモリ処理かどうかを決めるとは限りません。DATA_UPLOAD_MAX_NUMBER_FILESはmultipartのファイル個数の上限で、バイト数や利用者別の累積容量の上限とは別です。独自ハンドラーを入れる場合はrequest.POSTやrequest.FILESの解析前に設定する必要があり、CSRF middlewareが先に解析する順序にも注意します。制限のために安易にcsrf_exemptを付けず、ここでは標準ハンドラーのまま検証しています。

フォームのuploaded.size検証はDjangoが受信・解析した後です。1 MiBを超えるファイルを最終保存しない効果はありますが、転送・一時ディスク・メモリの消費を最初から防ぐものではありません。DATA_UPLOAD_MAX_MEMORY_SIZEも通常のmultipartファイル本文を含む一律のアップロード上限ではありません。プロキシやWebサーバーの本文上限・タイムアウト、ハンドラーの受信中の制限、個数やユーザー別容量を別層で設計します。上流上限はmultipartのヘッダー等も含むリクエスト全体なので、1ファイルの上限と同じ数値では境界の挙動が異なります。本記事は稼働中のサーバー設定を変更しません。

保存名・衝突・例外を扱う

save_upload()はUUIDから候補名を生成し、FileSystemStorageをallow_overwrite=Falseで使います。元名や申告拡張子は保存パスへ混ぜず、内容種別を主張しない.binを付けます。偶然の衝突や並行保存で別名になることがあるため、候補名ではなくstorage.save()の戻り値を使います。事前のexists()確認だけでは競合を防げません。別ストレージの衝突・上書き規約や、OSの権限設定は個別に確認が必要です。

ストレージは受信ファイルのchunks()を利用して保存し、例ではfinallyで受信ファイルをclose()します。read()で全体をbytesにしてから保存しません。OSErrorは固定文言の503へ変換し、内部パスや例外文字列を応答へ出しません。他の例外を一括成功扱いにはしません。ただし実際のI/O障害では部分ファイルが残る可能性があり、このコードはストレージ全体のトランザクションや自動巻戻しを保証しません。再試行時の重複、未参照ファイルの照合、保存期間を設計し、未知の既存ファイルを機械的に削除しないでください。

ファイル名と拡張子はメタ情報

upload_file.name はクライアント由来で、Djangoによるbasename化や長さ調整もあるため、ブラウザで選んだパスや完全な元名がそのまま届くとは限りません。os.path.basenameだけに依存した保存も、拡張子の保持やタイムスタンプの前置も安全性の証明にはなりません。元名を記録する場合は長さを制限した表示用メタ情報として保存し、HTML・ログ・HTTPヘッダー等の出力文脈ごとに扱います。

os.path.splitext("report.tar.gz")は("report.tar", ".gz")で、最後の拡張子を分けるだけです。拡張子なしや.envのような名前では拡張子が空になります。これらもOSのパス規則に従う文字列処理で、内容の検査ではありません。uploaded.content_typeは送信者が申告するMIMEで、そのまま信頼しません。教材は偽のimage/pngと.png名でも任意バイトが保存される試験を含み、画像検証を実装したとは扱いません。許可形式のパーサー検証・必要に応じた再エンコードや検疫を別途設計し、magic判定だけでも無害性を保証しません。

認証・CSRFとダウンロード権限

CSRFは他サイトからの意図しない操作への対策で、利用者がアップロードを許可されているか、内容が安全かの検査ではありません。画面を隠すだけでなくサーバー側で権限を確認します。この教材の権限検査は合成ユーザーで分岐を検証したもので、実ログイン・権限DB・全middleware構成の認証試験ではありません。

非公開パスへ保存しても、Webサーバーがその領域を配信する設定なら公開されます。ダウンロードは別の認可済み処理として設計し、ユーザー入力のパスではなく管理する保存キーから対象を決め、所有者や共有範囲を照合します。Content-Disposition、Content-Type、nosniffや別オリジン配信等の配信方針も検討し、未検査のHTML/SVGをアプリと同じオリジンへそのまま表示しません。教材はダウンロード処理も実配信設定も実装・検証していません。

検証環境と公式資料

2026年10月26日、Python 3.12.10 / Django 5.2.18で、DB接続禁止・独立設定・OSの一意な一時ディレクトリを使ったClient試験を実施しました。GET/POST、CSRF、multipart、必須・空・サイズ境界、申告名のエスケープ、衝突時の戻り値、保存例外とcloseを確認しています。偽MIMEの受付も意図した限界として確認しています。テストはHTTPサーバーを起動せず、既存ファイル・設定・DBを変更しません。実ブラウザ、実認証基盤、並行負荷、全OSのACL、プロキシ、WSGI/ASGI、本番サーバーやPHP-FPM、S3、内容の無害化の実証ではありません。

入力検証や所有者ごとの操作権限の考え方はビューの入力検証と権限、実配信前に分ける設定は本番配備の前提を参照してください。

子ページ

子ページはありません

同階層のページ
  1. 環境構築とプロジェクト/アプリの作成
  2. MVC(MVT)のそれぞれの使い方と説明
  3. データベースへの接続と操作
  4. Django Administration
  5. git管理
  6. エラー一覧
  7. バージョンの確認方法
  8. ログ出力方法
  9. SQLのログ出力方法
  10. ログのローテート設定
  11. settings.pyの定数にアクセスする方法
  12. 本番環境へのインストールとアプリのデプロイ(apache編)
  13. 本番環境へのインストールとアプリのデプロイ(nginx編)
  14. djangoアプリの本番の開始URLを変更する
  15. 静的(static)ファイルの置き場所と読み込み(画像、css、js )
  16. CSRFトークンをAjaxで使用する方法
  17. ajaxの使用例(POST編)
  18. ファイルのアップロードとファイルの名前
  19. クイックスタート/チュートリアル
  20. ログイン機能
  21. テンプレート側のログイン判定
  22. ビュー側のログイン判定
  23. 管理者ユーザーの作成/判定と管理画面
  24. モデルのjson化とレスポンス
  25. runserverでポートを指定する方法
  26. cronによるバッチ実行
  27. テンプレートで利用する共通のcontextを定義する方法
  28. プログラムが本番サーバーで反映されない場合の対処法
  29. APIの作成
  30. cron用コマンド・ファイルの作成

最近更新/作成されたページ