2021年7月20日火曜日

Flask でファイルのアップロード

Flask でファイルのアップロード

概要

Flask でファイルのアップロードをやってみました

環境

  • macOS 11.4
  • Python 3.8.3
  • flask 1.1.2

サンプルコード

import os
from flask import Flask, flash, request, redirect, url_for, send_from_directory, render_template
from werkzeug.utils import secure_filename

UPLOAD_FOLDER = '/tmp'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.secret_key = 'randomstringyoulike'

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

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

@app.route('/', methods=['POST'])
def upload_file():
    # file パートがない場合はアップロード画面にリダイレクト
    if 'file' not in request.files:
        flash('No file part')
        return redirect(request.url)
    file = request.files['file']
    # ファイルが選択されていない場合はアップロード画面にリダイレクト
    if not file.filename:
        flash('No selected file')
        return redirect(request.url)
    if file and allowed_file(file.filename):
        # マルチバイトなど XSS の可能性のある文字列を変換
        filename = secure_filename(file.filename)
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        return redirect(url_for('uploaded_file', filename=filename))

@app.route('/', methods=['GET'])
def upload_file_view():
    return render_template('index.html')
  • vim templates/index.html
<!doctype html>
<title>Upload new File</title>
{% with messages = get_flashed_messages() %}
  {% if messages %}
    <ul class=flashes>
    {% for message in messages %}
      <li>{{ message }}</li>
    {% endfor %}
    </ul>
  {% endif %}
{% endwith %}
<h1>Upload new File</h1>
<form method=post enctype=multipart/form-data>
  <input type=file name=file>
  <input type=submit value=Upload>
</form>

解説

「/」にブラウザでアクセスした場合にはアップロードフォームを表示します フォームはテンプレートとして管理します

ファイルは multipart/form-data としてアップロードします Flask では file パートがある場合に request.files に格納されるためここをチェックします そもそも file パートがない場合とアップロードフォームでファイルを指定しないでアップロードした場合はエラーとしトップのアップロードフォーム画面にリダイレクトします

今回は flash メソッドを使ってエラー分をテンプレートに表示するような仕組みにしていますがここは好きなようにしてもらって大丈夫です

RuntimeError: The session is unavailable because no secret key was set. が発生する場合は app.secret_key に好きな暗号化文字列を入力します flush が session を使う仕組みなので secret_key を設定していない場合に上記のエラーが発生します

アップロードされたファイルは「/tmp」に保存されます 保存する際に XSS 対策としてファイル名を半角英数字のみのファイル名に変換してあげています ファイルのアップロードに成功した場合はアップロードしたファイルを返しています flask でファイルの内容をそのまま返したい場合には send_from_directory を使うのが便利です

もう少し考えるべきこと

今はサイズ制限などしていません 数GBのファイルをアップロードされてサーバ側がパンクする可能性もあるのでファイルサイズのチェックはサーバ側でもするべきです ちなみにファイルサイズも含めてリクエストのペイロード全体のサイズを制限する場合は app.config['MAX_CONTENT_LENGTH'] を使います このサイズを超えてアップロードしようとした場合は 413 エラーが flask から返却されます

また拡張子のチェックはしていますが正確なファイルの内容を見ているわけではないので本来であればファイルの内容まで確認した上でアップロードさせるべきです

あとは自分で実装せずともそのあたりのお作法などを実装してくれている Flask-Uploads というモジュールがあるのでこれを使うのも手です

動作確認

  • pipenv run flask run

でアプリが起動した localhost:5000 にアクセスするとアップロードフォームが表示されます また curl の場合は以下のようにアップロードできます

  • curl -X POST -F file=@./test.txt localhost:5000

参考サイト

0 件のコメント:

コメントを投稿