Flaskでのファイルアップロード(単一ファイルと複数ファイル)

ファイルアップロード

HTMLでファイルアップロードフィールドをレンダリングするには、<input>タグのtype属性をfileに設定します。

<input type="file">

これにより、ブラウザにファイルアップロードフィールドが表示され、ファイル選択ボタンをクリックするとファイル選択ウィンドウが開きます。選択したファイル名は、ファイル選択ボタンの横に表示されます。

サーバーサイドでは、通常のデータと同じようにアップロードされたファイルを取得し保存することができます。ただし、セキュリティ上の問題を考慮する必要があります。特に重要なのは、ファイルタイプの検証、ファイルサイズの検証、ファイル名のフィルタリングです。

アップロードフォームの定義

Pythonのフォームクラスでファイルアップロードフィールドを作成するには、Flask-WTFが提供するFileFieldクラスを使用します。これはWTFormsのFileFieldを拡張し、Flaskとの統合を追加しています。

例えば、以下のようにフォームを作成します:

from flask_wtf.file import FileField, FileRequired, FileAllowed
from flask_wtf import FlaskForm
from wtforms import SubmitField

class UploadForm(FlaskForm):
    image = FileField('画像をアップロード', validators=[FileRequired(), FileAllowed(['jpg', 'jpeg', 'png', 'gif'], '画像ファイルのみ許可されています')])
    submit = SubmitField('送信')

このUploadFormクラスでは、FileFieldクラスを使用してimageフィールドを作成し、画像ファイルのアップロードを可能にしています。FileRequiredとFileAllowedバリデータを使用して、必須性とファイルタイプを検証します。

また、クライアントサイドでの簡単なタイプフィルタリングを行うために、HTML5のaccept属性を使用することもできます。

<input type="file" id="profile_pic" name="profile_pic" accept=".jpg, .jpeg, .png, .gif">

ただし、ユーザーは依然として制限外のファイルを選択できるため、サーバーサイドでの検証は必要です。

ファイルサイズの検証は、Flaskの構成変数MAX_CONTENT_LENGTHを設定することで行います。これはリクエストの最大長をバイト単位で指定します。

app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024

ファイルサイズがこの制限を超える場合、Flaskの組み込みサーバーは接続を中断し、プロダクション環境では413エラーを返します。

アップロードフォームのレンダリング

新しいビュー関数uploadで、UploadFormクラスをインスタンス化し、テンプレートに渡します。

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    form = UploadForm()
    if form.validate_on_submit():
        file = form.image.data
        filename = generate_secure_filename(file.filename)
        file.save(os.path.join(app.config['UPLOAD_PATH'], filename))
        flash('アップロード成功')
        session['filenames'] = [filename]
        return redirect(url_for('show_images'))
    return render_template('upload.html', form=form)

upload.htmlテンプレートでは、以下のようになります。

{% from 'macros.html' import form_field %}

{% extends 'base.html' %}
{% block content %}
    <form method="post" enctype="multipart/form-data">
        {{ form.csrf_token }}
        {{ form_field(form.image) }}<br>
        {{ form.submit }}<br>
    </form>
{% endblock %}

注意点として、フォームにファイルアップロードフィールドが含まれる場合、enctype属性をmultipart/form-dataに設定する必要があります。これにより、ブラウザはファイルデータをサーバーに送信します。

アップロードファイルの処理

ファイルアップロードフィールドを含むフォームが送信された場合、アップロードされたファイルはrequest.filesから取得します。これはWerkzeugが提供するImmutableMultiDictオブジェクトで、フィールド名とファイルオブジェクトのマッピングを保持します。

ImmutableMultiDict([('image', <FileStorage: u'example.jpg' (image/jpeg)>)])

アップロードされたファイルはWerkzeugのFileStorageオブジェクトとして解析されます。手動で処理する場合は、ファイルアップロードフィールドのname属性を使用して対応するファイルオブジェクトを取得します。

file = request.files.get('image')

Flask-WTFを使用する場合、自動的にファイルオブジェクトを取得してくれます。以下の例では、form.image.dataを使用してファイルオブジェクトを取得します。

import os
import uuid

def generate_secure_filename(filename):
    ext = os.path.splitext(filename)[1]
    new_filename = uuid.uuid4().hex + ext
    return new_filename

app.config['UPLOAD_PATH'] = os.path.join(app.root_path, 'uploads')

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    form = UploadForm()
    if form.validate_on_submit():
        file = form.image.data
        filename = generate_secure_filename(file.filename)
        file.save(os.path.join(app.config['UPLOAD_PATH'], filename))
        flash('アップロード成功')
        session['filenames'] = [filename]
        return redirect(url_for('show_images'))
    return render_template('upload.html', form=form)

ファイル名の処理方法は以下の通りです。

  1. 元のファイル名を使用する:安全なソースからのファイルの場合、直接使用できます。
filename = file.filename
  1. フィルタリング後のファイル名を使用する:Werkzeugのsecure_filename関数を使用して危険な文字を削除します。
from werkzeug.utils import secure_filename
filename = secure_filename(file.filename)
  1. 一意のファイル名を使用する:UUIDを使用してランダムなファイル名を生成します。
def generate_secure_filename(filename):
    ext = os.path.splitext(filename)[1]
    new_filename = uuid.uuid4().hex + ext
    return new_filename

ファイルを保存後、アップロードされたファイルを表示するためのビュー関数を作成します。

@app.route('/uploads/<path:filename>')
def get_file(filename):
    return send_from_directory(app.config['UPLOAD_PATH'], filename)

このビュー関数は、Flaskのsend_from_directory関数を使用してファイルを返します。

uploaded.htmlテンプレートでは、以下のようになります。

{% extends 'base.html' %}
{% from 'macros.html' import form_field %}

{% block title %}Home{% endblock %}

{% block content %}
{% if session.filenames %}
{% for filename in session.filenames %}
<a href="{{ url_for('get_file', filename=filename) }}" target="_blank">
    ![]({{ url_for('get_file', filename=filename) }})
</a>
{% endfor %}
{% endif %}
{% endblock %}
複数ファイルのアップロード

Flask-WTFの現在のバージョンでは、複数ファイルのアップロードをサポートしていないため、手動でファイルを取得し検証する必要があります。

クライアントサイドで複数ファイルを選択するには、<input>タグにmultiple属性を追加します。

<input type="file" id="files" multiple>

フォームクラスでは、WTFormsのMultipleFileFieldを使用します。

from wtforms import MultipleFileField

class MultiUploadForm(FlaskForm):
    images = MultipleFileField('画像をアップロード', validators=[DataRequired()])
    submit = SubmitField('送信')

サーバーサイドでは、request.filesのgetlistメソッドを使用して全てのファイルオブジェクトを取得し、それぞれを処理します。

from flask import url_for, request, session, flash, redirect
from flask_wtf.csrf import validate_csrf
from wtforms import ValidationError

@app.route('/multi-upload', methods=['GET', 'POST'])
def multi_upload():
    form = MultiUploadForm()
    if request.method == 'POST':
        filenames = []
        try:
            validate_csrf(form.csrf_token.data)
        except ValidationError:
            flash('CSRFトークンエラー')
            return redirect(url_for('multi_upload'))

        if 'images' not in request.files:
            flash('ファイルが必要です')
            return redirect(url_for('multi_upload'))

        for f in request.files.getlist('images'):
            if f and allowed_file(f.filename):
                filename = generate_secure_filename(f.filename)
                f.save(os.path.join(app.config['UPLOAD_PATH'], filename))
                filenames.append(filename)
            else:
                flash('無効なファイルタイプ')
                return redirect(url_for('multi_upload'))

        flash('アップロード成功')
        session['filenames'] = filenames
        return redirect(url_for('show_images'))

    return render_template('upload.html', form=form)

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']

app.config['ALLOWED_EXTENSIONS'] = ['png', 'jpg', 'jpeg', 'gif']

このビュー関数では、以下のステップでアップロードデータを検証します。

  1. CSRFトークンの検証
  2. フィールドにファイルが含まれているか確認
  3. ファイルオブジェクトの存在確認
  4. ファイルタイプの検証

これらのステップを通じて、安全なファイルアップロードを実現します。

タグ: flask WTForms Flask-WTF ファイルアップロード Python

9月30日 19:46 投稿