はじめに#
「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/passwd | etc_passwd | |
hello world!.jpg | hello_world.jpg | |
my<script>.png | my_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"}
動作例#
初期表示は以下のような感じ。
ファイルも何も選択せずに「送信」を押すとエラーメッセージが表示される。
次に、サーバーでは許容されていない拡張子のファイルを選択する。
これで「送信」を押すと、やはりエラーメッセージが表示されるが、先ほどと違いメッセージの数が変わっていることがわかる。
次は PDF ファイルを選択する。
これで「送信」を押すと、エラーメッセージは1つだけ表示される。
最後に、ファイル送信ができる条件を整える。
これで「送信」を押すと、成功のメッセージが表示される。
ちゃんと document
フォルダ配下にも文書が格納されることが確認できた。