メインコンテンツへスキップ

Flask でファイルアップロード機能を実装&フォームにメッセージを表示する

·3 分
ひとりアドベントカレンダー2024 Python Flask

はじめに
#

「OKAZAKI Shogo のひとりアドベントカレンダー2024」の8日目です。 今日は、昨日作成したフォームを拡張してファイルのアップロード機能を実装します。ついでに、フォーム送信時の成功/エラー時にメッセージを表示する機能を実装します。

完成系
#

完成した時には以下のような構成になる。 ドキュメントの格納先として document フォルダを作成しておく。

 1.
 2|-- Makefile
 3|-- app
 4|   |-- __init__.py
 5|   |-- app.py
 6|   |-- index.cgi
 7|   |-- models
 8|   |   `-- file.py
 9|   |-- static
10|   |   `-- css
11|   |       `-- default.css
12|   |-- templates
13|   |   |-- files.html
14|   |   |-- index.html
15|   |   |-- layout.html
16|   |   `-- regist_file_form.html  <-- 修正
17|   `-- views
18|       |-- files.py
19|       |-- index.py
20|       |-- regist_file.py  <-- 修正
21|       `-- regist_file_form.py 
22|-- db
23|   `-- bshssa_member_sys.db
24|-- documents  <-- 新規作成
25|-- instance
26|   `-- config
27|       `-- dev.py <-- 修正
28|-- poetry.lock
29`-- pyproject.toml

フォームを修正して、成功・エラー時にメッセージが表示できるようにする
#

テンプレートを以下のように修正して処理の成功時/失敗時にメッセージを表示する領域を作る。

template/regist_file_form.html
#

 1{% extends "layout.html" %}
 2
 3{% block content %}
 4<form method="post" action="/regist-file" enctype=multipart/form-data class="regist-file-form">
 5  {% if filename %}
 6  <div> 
 7    ファイル: {{ filename }} のアップロードが完了しました!     
 8  </div>        
 9  {% endif %}
10
11  {% with messages = get_flashed_messages() %}
12  {% if messages %}
13  <ul>
14    {% for message in messages %}
15    <li>{{ message }}</li>
16    {% endfor %}
17  </ul>
18  {% endif %}
19  {% endwith %}
20
21  <div class="regist-file-form">
22    <input type="file" name="file" id="file" />
23    <br>アップロードするファイル名を入力するか、ボタンを押してファイルを選択してください
24  </div>
25  <div class="regist-file-form">
26    <label for="title">タイトル: </label>
27    <input type="text" name="title" id="title" />
28  </div>
29  <div class="regist-file-form">
30    <label for="description">説明: </label>
31    <input type="text" name="description" id="description" />
32    <br>タイトルで内容が十分わかるときは、説明を入力しなくても構いません。
33  </div>
34  <div class="regist-file-form">
35    <input type="submit" value="送信" />
36  </div>
37</form>
38{% endblock %}

get_flashed_messages() について
#

Flask はフラッシュ表示の仕組み(flashing system)によって、ユーザへフィードバックを与える。サーバサイドで設定したフラッシュメッセージは、リクエストの最後にメッセージを記録し、次のリクエストでだけでそのメッセージにアクセスできるようにする。メッセージのフラッシュ表示はレイアウトのテンプレートと組み合わされる。ブラウザや Web サーバが Cookie のサイズに制限を課している場合、大き過ぎるフラッシュ表示用のメッセージでは、何も反応せずにメッセージのフラッシュ表示が失敗する。

テンプレートで get_flashed_messages() を呼び出すことでフラッシュメッセージ受け取ることができる。

ファイル登録処理の実装
#

昨日実装した、フォームからのリクエストを受けて動作する処理を拡張して、ファイル登録処理を実装する。

view/regist_file.py
#

送信された内容の検証を行い、不敵であれば、エラーメッセージを追加する。エラーメッセージは flask.flush() を呼び出すことで追加することが可能。

成功した場合は、元の画面にファイル名を私、成功メッセージと共に表示できるようにする。

クライアントから送信されたファイル自体は、 request.files を利用して取得することができる。その後、file.save() で所定の場所に保存する。

 1import os
 2from flask import Blueprint, render_template, request, flash, redirect
 3from werkzeug.utils import secure_filename
 4from instance.config import dev
 5
 6
 7regist_file_bp = Blueprint("regist_file", __name__, url_prefix="/regist-file")
 8
 9
10def __allowed_file(filename):
11    return '.' in filename and \
12           filename.rsplit('.', 1)[1].lower() in dev.ALLOWED_EXTENSIONS
13
14
15@regist_file_bp.route("/", methods=["POST"])
16def index():
17    is_file_check_ok = True
18    file = request.files["file"]
19
20    if not file:
21        flash("ファイルが選択されていません")
22        is_file_check_ok = False
23
24    if request.form["title"] == "":
25        flash("タイトルが入力されていません")
26        is_file_check_ok = False
27
28    origin_filename = file.filename
29    if __allowed_file(origin_filename) == False:
30        flash("ファイルの拡張子が正しくありません")
31        is_file_check_ok = False
32
33    if is_file_check_ok:
34        filename = secure_filename(datetime.now().strftime("%Y%m%d_%H%M%S_") + origin_filename)
35        file.save(os.path.join(dev.UPLOAD_FOLDER, filename))
36        return render_template("regist_file_form.html", filename=origin_filename)

secure_filename() について
#

secure_filename() は、ファイル名を安全な形式に変換してくれる関数。以下のように、特殊文字やディレクトリトラバーサル攻撃を防ぐ役割がある。

変換例:

元のファイル名変換後のファイル名
../../etc/passwdetc_passwd
hello world!.jpghello_world.jpg
my<script>.pngmy_script_.png

今回の処理の場合、日本語を含むファイル名は全て消えてしまうため、プレフィックスとして日時をつけたファイル名を secure_filename() に渡して変換することとした。

secret_key の設定
#

ファイルをアップロードするにあたって、セッション情報を暗号化する必要があるため、 SECRET_KEY の設定を追加する。

 1import os
 2
 3DEBUG = True
 4SECRET_KEY = "d56ec64032a3582931e560067426db08" # 新規追加
 5
 6# プロジェクトのルートディレクトリを基準にパスを解決
 7BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
 8DATABASE_PATH = os.path.join(BASE_DIR, "db", "bshssa_member_sys.db")
 9
10# SQLAlchemyの設定
11SQLALCHEMY_DATABASE_URI = f"sqlite:///{DATABASE_PATH}"
12SQLALCHEMY_TRACK_MODIFICATIONS = False
13
14# ファイルアップロード機能のための設定
15UPLOAD_FOLDER = os.path.join(BASE_DIR, "documents")
16ALLOWED_EXTENSIONS = {"pdf", "docx", "doc", "xlsx", "xls"}

動作例
#

初期表示は以下のような感じ。

動作例1

ファイルも何も選択せずに「送信」を押すとエラーメッセージが表示される。

動作例2

次に、サーバーでは許容されていない拡張子のファイルを選択する。

動作例3

これで「送信」を押すと、やはりエラーメッセージが表示されるが、先ほどと違いメッセージの数が変わっていることがわかる。

動作例4

次は PDF ファイルを選択する。

動作例5

これで「送信」を押すと、エラーメッセージは1つだけ表示される。

動作例6

最後に、ファイル送信ができる条件を整える。

動作例7

これで「送信」を押すと、成功のメッセージが表示される。

動作例8

ちゃんと document フォルダ配下にも文書が格納されることが確認できた。

動作例8

参考資料
#