Djangoで受信したファイルは request.FILES からフォームへ渡し、名前・サイズ等を確認してから保存します。画面に見せる申告名と、サーバーが管理する保存名を分けることが出発点です。
まず分ける:受信・検証・保存・公開
| 段階 | 確認すること |
|---|---|
| 入口 | 認証済み利用者のアップロード権限とCSRF。さらにプロキシ等でリクエスト容量・受信時間・頻度を制限する。 |
| 受信後 | 必須ファイルの有無、空ファイル、1ファイルのサイズ、個数、利用者別の容量枠。フォームの成功を内容検査と同一視しない。 |
| 保存 | 元名とは別の名前、上書きしないストレージ、実際に採用された名前、失敗時の解放。 |
| 公開 | 非公開保存を維持し、別途ダウンロード時にも対象ファイルへの権限を確認。UUIDは認可の代用ではない。 |
元の名前・拡張子の取得例
まず名前と拡張子を調べる元の例を示します。保存先の決定や内容の安全判定には使いません。完成例は「POSTフォームと保存をつなぐ例」を参照してください。
| この記事の要点 |
|---|
|
インポート
|
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、内容の無害化の実証ではありません。
- Django公式:アップロードとフォームへのバインド
- Django公式:UploadedFileとハンドラー
- Django公式:フォームFileField
- Django公式:ストレージと保存名
- Django公式:受信と一時保存の設定
- Django 5.2.18:ハンドラーの実装
- Django公式:CSRF
- Django公式:アップロード内容のリスク
- Python公式:splitext
入力検証や所有者ごとの操作権限の考え方はビューの入力検証と権限、実配信前に分ける設定は本番配備の前提を参照してください。
子ページはありません
- 環境構築とプロジェクト/アプリの作成
- MVC(MVT)のそれぞれの使い方と説明
- データベースへの接続と操作
- Django Administration
- git管理
- エラー一覧
- バージョンの確認方法
- ログ出力方法
- SQLのログ出力方法
- ログのローテート設定
- settings.pyの定数にアクセスする方法
- 本番環境へのインストールとアプリのデプロイ(apache編)
- 本番環境へのインストールとアプリのデプロイ(nginx編)
- djangoアプリの本番の開始URLを変更する
- 静的(static)ファイルの置き場所と読み込み(画像、css、js )
- CSRFトークンをAjaxで使用する方法
- ajaxの使用例(POST編)
- ファイルのアップロードとファイルの名前
- クイックスタート/チュートリアル
- ログイン機能
- テンプレート側のログイン判定
- ビュー側のログイン判定
- 管理者ユーザーの作成/判定と管理画面
- モデルのjson化とレスポンス
- runserverでポートを指定する方法
- cronによるバッチ実行
- テンプレートで利用する共通のcontextを定義する方法
- プログラムが本番サーバーで反映されない場合の対処法
- APIの作成
- cron用コマンド・ファイルの作成
人気ページ
- 1 Eclipseで「サーバーに追加または除去できるリソースがありません。」の原因と対処法
- 2 tomcat の起動 / 停止ログと catalina.log・catalina.out の違い
- 3 JavaScript で base URL を取得する方法|window.location.origin
- 4 YouTube Data API v3 エラー一覧|403・400・404 の原因と対処
- 5 Laravel エラー一覧|500/Blade/DB 接続/ルーティングの代表エラー
- 6 3Dグラフィックスのソフト一覧|Blender・Metasequoiaと用途別の選び方
- 7 Spring Frameworkのアノテーション一覧
- 8 【Spring】@Valueアノテーションとは
- 9 CATALINA_HOME の確認方法 (Linux / Mac)
- 10 【Spring】@Autowiredアノテーションとは
最近更新/作成されたページ
- Visual Studio「デザイナーを表示できません」エラーの原因と対処 2026-10-28 12:06:00
- SSH 認証に失敗しました。再試行してください の対処 2026-10-28 12:06:00
- SSH2 秘密鍵の読み込みに失敗しました の原因と対処 2026-10-28 12:06:00
- TeraTerm のよくあるエラーと対処法完全ガイド — Connection refused / 2026-10-28 12:06:00
- Visual Studio エラー一覧|コンパイルエラー・警告の確認方法と対処 2026-10-28 11:39:57
- Whitelabel Error Page の原因と消し方|Spring Boot のエラーページ 2026-10-28 11:39:57
- Eclipseで「サーバーに追加または除去できるリソースがありません。」の原因と対処法 2026-10-28 11:39:57
- SQL WHERE 句完全ガイド 2026-10-28 11:39:57
- SQL JOIN 完全ガイド (INNER/LEFT/RIGHT/FULL OUTER) 2026-10-28 11:39:57
- tomcat の起動 / 停止ログと catalina.log・catalina.out の違い 2026-10-28 10:24:23
- Tomcat の起動確認方法 5 つ|localhost:8080・ポート・プロセス・ログ 2026-10-28 10:24:23
- Oracle 19c COMMITの使い方: 暗黙コミット・自動確定・応答不明時の確認 2026-10-28 09:59:09
- Oracle 19c DELETE文|削除対象の確認・COMMIT前の判断・ROLLBACKの条件 2026-10-28 09:59:09
- Oracle 19c INSERT文|INSERT ALL・SELECT・RETURNINGとMERGEの使い分け 2026-10-28 09:59:09
- Oracle 19cのトランザクション制御|COMMIT・ROLLBACK・分離レベルと接続設定 2026-10-28 09:59:09