【Python特化】おすすめのオンラインプログラミングスクール

【Flask】画像ファイルアップロード機能を実装した画像表示アプリ開発

flask-image-file-upload
本記事の要点
  • 画像ファイルアップロード機能の使い方を理解する
  • 画像と紐づくデータベース操作を理解する
  • 画像ファイルの表示方法と削除方法を理解する

上記をまとめ、具体的なコードや画像ファイルの使い方を重点的に解説します。

目次

画像表示アプリのディレクトリ構成

本記事では、以下のディレクトリ構成で画像表示アプリを開発します。

flask-image
├── .env
├── apps
│   ├── app.py
│   ├── config.py
│   ├── static
│   │   └── bootstrap.min.css
│   ├── image
│   │   ├── __init__.py
│   │   ├── forms.py
│   │   ├── models.py
│   │   ├── static/css
│   │   │   └── style.css
│   │   ├── templates
│   │   │   └── image
│   │   │       ├── base.html
│   │   │       ├── index.html
│   │   │       └── upload.html
│   │   └── views.py
|   └── images
├── local.sqlite
└── migrations
ディレクトリ構成
  • flask-image:開発用ディレクトリ名
  • apps:格納されたアプリを管理するディレクトリ
  • auth:認証アプリの関連ファイルを格納したディレクトリ
  • image:画像表示アプリの関連ファイルを格納したディレクトリ
  • images:画像ファイルを格納するディレクトリ
  • static/css:静的ファイルのディレクトリ
  • templates:HTMLファイルを格納したディレクトリ

各ファイルについては、後述の中で解説していきます。

画像表示アプリの完成画面は、以下になります。

flask-image-file-upload
flask-image-file-upload

Flaskを利用するためのPython環境構築

Pythonで開発を実施する際、開発プロジェクトごとに専用の実行環境を作成し切り替えることができます。

また、開発用として一時的に作成する実行環境を「仮想環境」と呼びます。

ローカル環境で開発を進めるにあたり、venvモジュールを利用し仮想環境を構築します。

venvは、Pythonに標準搭載された仮想環境用モジュールです。

venvを利用することで、プロジェクトごとに分離したPython実行環境を構築できるため、各実行環境でそれぞれのパッケージを管理できます。

Pythonによる仮想環境の構築方法が知りたい人は、「【Python】ダウンロードとインストール方法から開発環境構築まで解説!」を一読ください。

Flaskのインストール

本記事では、WindowsPCによる任意のディレクトリにてFlaskをインストールしています。

インストール自体は簡単で、以下のコマンドを実行しましょう。

pip install flask

上記のコマンドを実行すると、インストールが完了します。

FlaskによるWebアプリ開発の事前準備

任意で作成したディレクトリにcdコマンドで移動後、仮想環境を作成します。

py -m venv venv

次に、仮想環境をアクティベート(有効化)します。

./venv/Scripts/Activate

pip listを利用するとパッケージの状況が確認できます。

インストールしたのちにpip listでパッケージ状況を確認すると以下のように表示されます。

(venv) PS C:\Users\sugir\Documents\flask-image> pip list
Package      Version
------------ -------
blinker      1.8.2
click        8.1.7
colorama     0.4.6
Flask        3.0.3
itsdangerous 2.2.0
Jinja2       3.1.4
MarkupSafe   2.1.5
pip          24.0
Werkzeug     3.0.4

さらに、本プログラムでは環境変数の設定ファイルとして.envファイルを利用します。

.envファイルを利用すると、アプリ単位で環境変数を設定できます。

以下のコマンドでpython-dotenvをインストールします。

pip install python-dotenv

pip listで確認します。

(venv) PS C:\Users\sugir\Documents\flask-image> pip list
Package       Version
------------- -------
blinker       1.8.2
click         8.1.7
colorama      0.4.6
Flask         3.0.3
itsdangerous  2.2.0
Jinja2        3.1.4
MarkupSafe    2.1.5
pip           24.0
python-dotenv 1.0.1
Werkzeug      3.0.4

flaskのインストールが完了したので画像一覧表示アプリを開発する準備が整いました。

Blueprintによるアプリ登録

始めに、以下のディレクトリとファイルを作成します。

flask-image
├── .env
└── apps
    ├── app.py
    └── image
        └── views.py

Blueprintによるアプリ登録を実施します。

# Flaskのインポート
from flask import Flask

def create_app():
    # Flaskのインスタンス作成
    app = Flask(__name__)

    # imageパッケージからviewsをインポート
    from apps.image import views as image_views

    # register_blueprintを利用してviewsのimageをアプリへ登録
    app.register_blueprint(image_views.image, url_prefix="/image")

    return app
app.py
  • flaskのインポート
  • flaskのインスタンス作成
  • imageパッケージからviewsをインポート
  • register_blueprintを利用してviewsのimageをアプリへ登録
from flask import Flask

flaskをインポートし、Flaskフレームワークの機能を利用できるようにします。

def create_app():
    app = Flask(__name__)
    from apps.image import views as image_views
    app.register_blueprint(image_views.image, url_prefix="/image")
    return app

create_app関数を作成し、appへFlaskインスタンスを作成します。

本プログラムで開発するimageパッケージからviews.pyをインポートします。

Blueprint機能によるregister_blueprint関数を利用し、imageアプリを登録します。

また、url_prefixに/imageを指定し、views.pyのエンドポイント全てのURLがimageから始まるよう設定します。

次に、views.pyにてBlueprintによるアプリを作成します。

from flask import Blueprint

image = Blueprint(
    "image",
    __name__,
    template_folder = "templates",
    static_folder = "static",
)

@image.route("/")
def index():
    return 'Hello World!'
views.py
  • Blueprintのインポート
  • Blueprintにてimageを作成
  • デコレータ関数にてURLマッピング
  • index関数で任意の文字列を出力
from flask import Blueprint

作成するimageアプリのviews.pyでは、Blueprintをインポートします。

image = Blueprint(
    "image",
    __name__,
    template_folder = "templates",
    static_folder = "static",
)

imageアプリをBlueprintオブジェクトで生成します。

@image.route("/")
def index():
    return 'Hello World!'

ひとまず、デコレータ関数にてURLマッピングしており、任意の文字列を出力しておきます。

また、.envファイルにて環境変数を設定します。

FLASK_APP = apps.app:create_app
FLASK_DEBUG = 1

アプリを生成するのが関数の場合、「モジュール:関数」と指定します。

また、アプリを生成する関数名がcreate_app関数の場合、Flaskが自動でcreate_app関数を呼び出せます。

これらのディレクトリ/ファイルを起点にコードを追加していきます。

一度、flask routesコマンドでルーティングの確認します。

(venv) PS C:\Users\sugir\Documents\flask-image> flask routes
Endpoint      Methods  Rule
------------  -------  -----------------------------
image.index   GET      /image/
image.static  GET      /image/static/<path:filename>
static        GET      /static/<path:filename>

ルートが割り当てられていることが分かります。

次に、flask runコマンドを実行し127.0.0.5000/image/にアクセスしてみましょう。

Hello World!が左上に表示されたかと思います。

SQLAlchemyとMigrateの事前準備

次に、データベースを利用するための事前準備を行います。

データベースを利用するための事前準備
  • SQLAlchemyのインストール
  • Migrateのインストール

上記どちらもflaskの拡張機能としてインストールできます。

SQLAlchemyとMigrateのインストール

pip installコマンドでSQLAlchemyとMigrateをインストールします。

pip install flask-sqlalchemy
pip install flask-migrate

pip listコマンドでパッケージを確認します。

(venv) PS C:\Users\sugir\Documents\flask-image> pip list
Package           Version
----------------- -------
alembic           1.13.3
blinker           1.8.2
click             8.1.7
colorama          0.4.6
Flask             3.0.3
Flask-Migrate     4.0.7
Flask-SQLAlchemy  3.1.1
greenlet          3.1.1
itsdangerous      2.2.0
Jinja2            3.1.4
Mako              1.3.5
MarkupSafe        2.1.5
pip               24.0
python-dotenv     1.0.1
SQLAlchemy        2.0.35
typing_extensions 4.12.2
Werkzeug          3.0.4

インストールが完了したのでapp.pyにて利用準備を実施します。

SQLAlchemyとMigrateの利用準備

ここからはSQLAlchemyとMigrateの利用準備のため、app.pyにコードを追加します。

# Flaskのインポート
from flask import Flask
# SQLAlchemyのインポート
from flask_sqlalchemy import SQLAlchemy
# Migrateのインポート
from flask_migrate import Migrate

# SQLAlchemyのインスタンス作成
db = SQLAlchemy()

def create_app():
    # Flaskのインスタンス作成
    app = Flask(__name__)

    # SQLalchemyとアプリを連携
    db.init_app(app)
    # Migrateとアプリを連携
    Migrate(app, db)

    # imageパッケージからviewsをインポート
    from apps.image import views as image_views

    # register_blueprintを利用してviewsのimageappをアプリへ登録
    app.register_blueprint(image_views.image, url_prefix="/image")

    return app
app.py
  • SQLAlchemyのインポート
  • Migrateのインポート
  • SQLAlchemyのインスタンス作成
  • SQLalchemyとアプリを連携
  • Migrateとアプリを連携
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

先ほどインストールしたSQLAlchemyとMigrateをインポートします。

db = SQLAlchemy()

変数dbへSQLAlchemyをインスタンス化します。

db.init_app(app)
Migrate(app, db)

create_app関数内にて、SQLAlchemyとMigrateをアプリに連携させます。

models.pyの作成と実装

次に、以下のディレクトリに__init__.pyとmodels.pyを追加で作成します。

flask-image
├── .env
└── apps
    ├── app.py
    └── image
        ├── __init__.py
        ├── models.py
        └── views.py

ファイル作成が完了したら、models.pyにコードを追加します。

from apps.app import db

class UserImage(db.Model):
    __tablename__ = "user_images"
    id = db.Column(db.Integer, primary_key=True)
    image_path = db.Column(db.String)
models.py
  • apps.appからdbをインポート
  • db.Modelを継承したUserImageクラス作成
  • テーブル名の指定
  • カラムの定義
from apps.app import db

apps.appからSQLAlchemyをインスタンス化したdbをインポートします。

class UserImage(db.Model):
    __tablename__ = "user_images"
    id = db.Column(db.Integer, primary_key=True)
    image_path = db.Column(db.String)

db.Modelを継承したUserImageクラスを作成し、テーブル名を指定します。

作成したいカラム(列)を定義します。

__init__.pyによるモデルの宣言

モデルを作成できたら、__init__.pyにてmodels.pyをインポートします。

import apps.image.models

コンフィグ設定

次に、以下のディレクトリにconfig.pyと画像ファイル保存用のimagesディレクトリを追加で作成します。

flask-image
├── .env
└── apps
    ├── app.py
    ├── config.py
    ├── image
    │   ├── __init__.py
    │   ├── models.py
    │   └── views.py
    └── images

ファイル作成が完了したら、config.pyにコードを追加します。

from pathlib import Path

basedir = Path(__file__).parent.parent

# BaseConfigクラスを作成
class BaseConfig:
    SECRET_KEY = "{SECRET_KEY}"
    # 画像アップロード先にapps/imagesを指定
    UPLOAD_FOLDER = str(Path(basedir, "apps", "images"))

# BaseConfigクラスを継承しLocalConfigクラスを作成
class LocalConfig(BaseConfig):
    SQLALCHEMY_DATABASE_URI = f"sqlite:///{basedir/'local.sqlite'}"
    SQLALCHEMY_TRACK_MODIFICATIONS = False

# config辞書にマッピング
config = {
    "local": LocalConfig,
}
config.py
  • Pathをインポート
  • baseのパスになるbasedirを作成
  • BaseConfigクラス作成
  • 画像アップロード先にapps/imagesを指定
  • config辞書を作成
from pathlib import Path

Pythonの標準ライブラリであるPathをインポートします。

basedir = Path(__file__).parent.parent

basedirは、Pathによってflask-imageディレクトリのパスを格納しています。

class BaseConfig:
    SECRET_KEY = "{SECRET_KEY}"
    UPLOAD_FOLDER = str(Path(basedir, "apps", "images"))

BaseConfigクラスを作成し、SECRET_KEYとUPLOAD_FOLDERを定義しています。

Flaskには、画像ファイルアップロード先を指定するためにconfigとしてUPLOAD_FOLDERが用意されています。

UPLOAD_FOLDERにパスを指定することで指定したディレクトリに画像をアップロードできます。

class LocalConfig(BaseConfig):
    SQLALCHEMY_DATABASE_URI = f"sqlite:///{basedir/'local.sqlite'}"
    SQLALCHEMY_TRACK_MODIFICATIONS = False

BaseConfigクラスを継承しLocalConfigクラスを作成します。

config = {
    "local": LocalConfig,
}

最後に、config辞書を作成しcreate_app関数に引き渡せられるようlocalの値を持たせます。

config設定は、アプリにおいてどのように管理するか重要です。

開発環境・ステージング環境・本番環境など各環境で設定すべきコンフィグの値が変わるためです。

本記事では、環境を簡易に変更できるfrom_object形式で設定しています。

以下は、コンフィグ設定時に利用される環境設定の方法です。

コンフィグを設定する方法
  • from_objectを使う方法
  • from_mappingを使う方法
  • from_envvarを使う方法
  • from_pyfileを使う方法
  • from_fileを使う方法

また、データベースを利用する際にセッションを利用するため、SECRET_KEYの設定が必要になります。

そのため、config設定したあとにapp.pyにインポートしcreate_appへ引数を持たせ、コンフィグを読み込ませます。

# Flaskのインポート
from flask import Flask
# SQLAlchemyのインポート
from flask_sqlalchemy import SQLAlchemy
# Migrateのインポート
from flask_migrate import Migrate
# configをインポート
from apps.config import config

# SQLAlchemyのインスタンス作成
db = SQLAlchemy()

def create_app(config_key):
    # Flaskのインスタンス作成
    app = Flask(__name__)

    # config_keyにマッチする環境のコンフィグを読み込む
    app.config.from_object(config[config_key])

    # SQLalchemyとアプリを連携
    db.init_app(app)
    # Migrateとアプリを連携
    Migrate(app, db)

    # imageパッケージからviewsをインポート
    from apps.image import views as image_views

    # register_blueprintを利用してviewsのimageappをアプリへ登録
    app.register_blueprint(image_views.image, url_prefix="/image")

    return app
app.py
  • configのインポート
  • create_appに引数config_keyを持たせる
  • config_keyにマッチする環境のコンフィグを読み込む
from apps.config import config

ppsディレクトリに存在するconfig.pyをインポートします。

app.config.from_object(config[config_key])

create_app関数にconfig_keyといった引数を持たせ、config.pyで作成した辞書から指定の値を読み込めるよう設定しています。

次に、環境設定ファイルである.envファイルに変更を追加します。

FLASK_APP = apps.app:create_app("local")
FLASK_DEBUG = 1

config.pyで作成したconfig辞書からlocalを読み込むように設定しています。

データベースの初期化とマイグレーション

次に、データベースの初期化とマイグレーションファイルを作成します。

マイグレーションファイルは、データベースの設計書のようなものです。

マイグレーションファイルを実行することで記述してきた内容がデータベースに反映されます。

データベースの初期化とマイグレーションを実施するflask dbコマンドは以下になります。

flask dbコマンド
  • flask db init
  • flask db migrate
  • flask db upgrade
  • flask db downgrade

以下のコマンド例は環境変数FLASK_APPを適切に設定している場合になります。

flask db init

flask db initコマンドは、データベースを初期化します。

ここでは、flask db initコマンドを実行するとflask-imageディレクトリ配下にmigrationsディレクトリが作成されます。

(venv) PS C:\Users\sugir\Documents\flask-image> flask db init
Creating directory 'C:\\Users\\sugir\\Documents\\flask-image\\migrations' ...  done
Creating directory 'C:\\Users\\sugir\\Documents\\flask-image\\migrations\\versions' ...  done
Generating C:\Users\sugir\Documents\flask-image\migrations\alembic.ini ...  done
Generating C:\Users\sugir\Documents\flask-image\migrations\env.py ...  done
Generating C:\Users\sugir\Documents\flask-image\migrations\README ...  done
Generating C:\Users\sugir\Documents\flask-image\migrations\script.py.mako ...  done
Please edit configuration/connection/logging settings in 'C:\\Users\\sugir\\Documents\\flask-image\\migrations\\alembic.ini' before proceeding.

flask db migrate

flask db migrateコマンドは、データベースのマイグレーションファイルを作成します。

flask db migrateコマンドを実行すると、Model定義をもとにmigrations/versions配下にPythonファイルでデータベースに適用する前の情報が生成されます。

(venv) PS C:\Users\sugir\Documents\flask-image> flask db migrate
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'user_images'
Generating C:\Users\sugir\Documents\flask-image\migrations\versions\01c477262caa_.py ...  done

flask db upgrade

flask db upgradeコマンドは、マイグレーション情報を実際にデータベースに反映します。

ここでは、flask db upgradeコマンドを実行するとuser_imagesテーブルが生成されます。

(venv) PS C:\Users\sugir\Documents\flask-image> flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 01c477262caa, empty message

VSCodeでデータベースを確認する

VSCodeでデータベースの内容を確認できるように設定する方法です。

VSCodeの左サイドバーにある拡張機能アイコンをクリックし、検索バーで「SQLite」を検索します。

作成されたデータベースファイル(ここではlocal.sqlite)を右クリックし、「Open Database」を選択すると「SQLITE EXPLORER」が左下に表示されます。

データベースの作成と操作が実行できるようになりました。

DBを使った画像一覧表示・画像ファイルアップロード機能作成

ここでは、データベースを利用して以下の機能を作成していきます。

データベースを利用した各機能
  • 画像一覧表示機能の作成
  • 画像ファイルアップロード機能の作成
  • 削除機能の作成

画像一覧表示機能の作成

画像一覧表示機能作成における主な手順は以下になります。

画像一覧表示機能作成の手順
  • 画像一覧機能のエンドポイント作成
  • 画像表示機能のエンドポイント作成
  • 画像一覧表示機能のテンプレート作成
  • 動作確認

画像一覧表示機能のエンドポイント作成

まず始めに、画像一覧機能のエンドポイントを作成します。

from apps.app import db
from apps.image.models import UserImage
from flask import Blueprint, render_template

image = Blueprint(
    "image",
    __name__,
    template_folder = "templates",
    static_folder = "static",
)

@image.route("/")
def index():
    user_images = db.session.query(UserImage).all()
    return render_template("iamge/index.html", user_images=user_images)
views.py
  • dbのインポート
  • UserImageのインポート
  • render_templateのインポート
  • UserImage情報を参照
  • index.htmlファイル出力
from apps.app import db
from apps.image.models import UserImage
from flask import Blueprint, render_template

db、UserImage、テンプレートを利用するためのrender_templateをインポートします。

@image.route("/")
def index():
    user_images = db.session.query(UserImage).all()
    return render_template("iamge/index.html", user_images=user_images)

user_images変数にデータを格納し、index.htmlへuser_imagesとして値を渡しています。

画像表示機能のエンドポイント作成

次に、画像表示機能のエンドポイントを作成します。

実際に、指定されたディレクトリとファイルを読み込むために必要になります。

from apps.app import db
from apps.image.models import UserImage
from flask import Blueprint, render_template, send_from_directory, current_app

image = Blueprint(
    "image",
    __name__,
    template_folder = "templates",
    static_folder = "static",
)

@image.route("/")
def index():
    user_images = db.session.query(UserImage).all()
    return render_template("image/index.html", user_images=user_images)

@image.route("/images/<path:filename>")
def image_file(filename):
    return send_from_directory(current_app.config["UPLOAD_FOLDER"], filename)
views.py
  • flaskからsend_from_directory, current_appの追加インポート
  • image_fileエンドポイント作成
  • send_from_directoryで返す
from flask import Blueprint, render_template, send_from_directory, current_app

UPLOAD_FOLDERのパス取得とパス/画像ファイル名を返すsend_from_directoryを利用するため、flaskから追加インポートします。

@image.route("/images/<path:filename>")
def image_file(filename):
    return send_from_directory(current_app.config["UPLOAD_FOLDER"], filename)

デコレータ関数にて、image_fileエンドポイントを作成します。

引数にfilenameを持たせ、画像を右クリックでタブを開けば画像表示されるようになります。

画像一覧表示機能のテンプレート作成

次に、画像一覧表示機能を反映させたテンプレートを作成します。

また、apps/staticにはCSSフレームワークであるBootStrapを採用しています。

以下、BootStrapのダウンロードサイトになります。

flask-image
├── .env
└── apps
    ├── app.py
    ├── config.py
    ├── static/css
    │   └── bootstrap.min.css
    ├── image
    │   ├── __init__.py
    │   ├── models.py
    │   ├── static/css
    │   │   └── style.css
    │   ├── templates
    │   │   └── image
    │   │       ├── base.html
    │   │       └── index.html
    │   └── views.py
    └── images

テンプレート作成では、共通テンプレートとしてbase.htmlから作成します。

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ImageApp</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
    <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}" />
    <link rel="stylesheet" href="{{ url_for('image.static', filename='css/style.css') }}" />
  </head>
  <body>
    <nav class="navbar navbar-dark bg-dark">
      <div class="container-fluid">
        <ul class="navbar-nav flex-row">
          <li class="nav-item pe-2">
            <button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasDarkNavbar" aria-controls="offcanvasDarkNavbar" aria-label="Toggle navigation">
              <span class="navbar-toggler-icon"></span>
            </button>
          </li>
          <li class="nav-item mt-1">
            <a class="navbar-brand" href="{{ url_for('image.index') }}">ImageApp</a>
          </li>
        </ul>
        <div class="offcanvas offcanvas-start text-bg-dark" tabindex="-1" id="offcanvasDarkNavbar" aria-labelledby="offcanvasDarkNavbarLabel">
          <div class="offcanvas-header">
            <h5 class="offcanvas-title" id="offcanvasDarkNavbarLabel">ImageApp</h5>
            <button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
          </div>
          <div class="offcanvas-body">
            <ul class="navbar-nav justify-content-end flex-grow-1 pe-3">
              <li class="nav-item">
                <a class="nav-link d-flex align-items-center" href="/image">
                  <span class="material-symbols-outlined pe-2">image</span>
                  画像一覧
                </a>
              </li>
              <li class="nav-item">
                <a class="nav-link d-flex align-items-center" href="/image/upload">
                  <span class="material-symbols-outlined pe-2">upload</span>
                  画像アップロード
                </a>
              </li>
            </ul>
          </div>
        </div>
    </nav>
    {% block content %}{% endblock %}
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
  </body>
</html>

以下、index.htmlはbase.htmlを継承しています。

{% extends "image/base.html" %}
{% block content %}
<div class="row row-cols-1 row-cols-md-2 g-4 mx-2 my-2">
{% for user_image in user_images %}
  <div class="col">
    <div class="card">
      <img src="{{ url_for('image.image_file', filename=user_image.image_path) }}" class="card-img-top img-thumbnail" style="border: none;" alt="画像">
    </div>
  </div>
{% endfor %}
</div>
{% endblock %}

動作確認

flask runコマンドでアプリを起動し、127.0.0.1:5000/image/へアクセスします。

また、ナビゲーションバーのトグルを押下すると、左から各ページボタンが表示されます。

画像ファイルアップロード機能の作成

いよいよ本丸となる画像ファイルアップロード機能の実装を実施します。

画像ファイルアップロード機能作成における主な手順は以下になります。

画像ファイルアップロード機能作成の手順
  • 画像ファイルアップロード機能のフォームクラス作成
  • 画像ファイルアップロード機能のエンドポイント作成
  • 画像ファイルアップロード機能のテンプレート作成
  • 動作確認

画像ファイルアップロード機能のフォームクラス作成

forms.pyに画像ファイルアップロード機能のフォームクラスを追加します。

また、フォームの拡張機能であるflask-wtfを利用します。

flask-wtfはバリデーションチェックやCSRF対策の機能を持ったフォーム作成できます。

フォーム作成時のメリットがあるため、インストールしておきましょう。

pip install flask-wtf
(venv) PS C:\Users\sugir\Documents\flask-image> pip list
Package           Version
----------------- -------
alembic           1.13.3
blinker           1.8.2
click             8.1.7
colorama          0.4.6
Flask             3.0.3
Flask-Migrate     4.0.7
Flask-SQLAlchemy  3.1.1
Flask-WTF         1.2.1
greenlet          3.1.1
itsdangerous      2.2.0
Jinja2            3.1.4
Mako              1.3.5
MarkupSafe        2.1.5
pip               24.0
python-dotenv     1.0.1
SQLAlchemy        2.0.35
typing_extensions 4.12.2
Werkzeug          3.0.4
WTForms           3.1.2

インストールの確認ができたら、ファイルを作成します。

flask-image
├── .env
└── apps
    ├── app.py
    ├── config.py
    ├── static/css
    │   └── bootstrap.min.css
    ├── image
    │   ├── __init__.py
    │   ├── forms.py
    │   ├── models.py
    │   ├── static/css
    │   │   └── style.css
    │   ├── templates
    │   │   └── image
    │   │       ├── base.html
    │   │       └── index.html
    │   └── views.py
    └── images
# flask_wtfからFlaskFormクラスをインポート
from flask_wtf import FlaskForm
# flask_wtf.fileから各フィールドをインポート
from flask_wtf.file import FileAllowed, FileField, FileRequired
# wtformsからサブミットフィールドをインポート
from wtforms.fields.simple import SubmitField

class UploadImageForm(FlaskForm):
    # ファイルフィールドに必要なバリデーションを設定
    image = FileField(
        validators = [
            FileRequired("画像ファイルを指定してください"),
            FileAllowed(["png", "jpg", "jpeg"], "サポートされていない画像形式です"),
        ]
    )
    submit = SubmitField("アップロード")
forms.py
  • flask_wtfからFlaskFormクラスをインポート
  • flask_wtf.fileから各フィールドをインポート
  • wtformsからサブミットフィールドをインポート
  • アップロードイメージフォームクラス作成
from flask_wtf import FlaskForm
from flask_wtf.file import FileAllowed, FileField, FileRequired
from wtforms.fields.simple import SubmitField

フォーム拡張機能を利用するためにFlaskFormクラスをインポートします。

ファイルフィールドに必要な各フィールドをインポートします。

サブミットフィールドをインポートします。(<input type=”submit”>フィールドを生成します。)

class UploadImageForm(FlaskForm):
    # ファイルフィールドに必要なバリデーションを設定
    image = FileField(
        validators = [
            FileRequired("画像ファイルを指定してください"),
            FileAllowed(["png", "jpg", "jpeg"], "サポートされていない画像形式です"),
        ]
    )
    submit = SubmitField("アップロード")

FlaskFormを継承したUploadImageFormクラスを作成し、必要なバリデーションを設定します。

FileRequiredによるファイル指定、FileAllowedによる画像形式を設定します。

画像ファイルアップロード機能のエンドポイント作成

画像ファイルアップロード機能のエンドポイントを作成します。

from apps.app import db
from apps.image.models import UserImage
# flaskからcurrent_app, redirect, url_forを追加インポート
from flask import Blueprint, render_template, send_from_directory, current_app, redirect, url_for
# uuidをインポート
import uuid
# Pathをインポート
from pathlib import Path
# UploadImageFormをインポート
from apps.image.forms import UploadImageForm

image = Blueprint(
    "image",
    __name__,
    template_folder = "templates",
    static_folder = "static",
)

@image.route("/")
def index():
    user_images = db.session.query(UserImage).all()
    return render_template("image/index.html", user_images=user_images)

@image.route("/images/<path:filename>")
def image_file(filename):
    return send_from_directory(current_app.config["UPLOAD_FOLDER"], filename)

@image.route("/upload", methods=["GET", "POST"])
def upload_image():
    # UploadImageFormを利用してバリデーションチェック
    uploadimageform = UploadImageForm()
    if uploadimageform.validate_on_submit():
        # アップロードされた画像ファイルを取得
        file = uploadimageform.image.data
        # ファイルのファイル名と拡張子を取得しファイル名をuuidに変換
        ext = Path(file.filename).suffix
        image_uuid_file_name = str(uuid.uuid4()) + ext

        # 画像保存
        image_path = Path(current_app.config["UPLOAD_FOLDER"], image_uuid_file_name)
        file.save(image_path)

        # DB保存
        user_image = UserImage(
            image_path = image_uuid_file_name
        )
        db.session.add(user_image)
        db.session.commit()
        # アップロード後はリダイレクトで画像一覧画面へ
        return redirect(url_for("image.index"))
    
    return render_template("image/upload.html", form=uploadimageform)
forms.py
  • flaskからredirect, url_forの追加インポート
  • uuidのインポート
  • Pathのインポート
  • forms.pyからUploadImageFormクラスをインポート
  • uploadエンドポイント作成
  • UploadImageFormのインスタンス作成
  • バリデーションチェック
  • ファイル取得とファイル名変換
  • 画像保存とDB保存
  • リダイレクトで画像一覧表示画面へ
from flask import Blueprint, render_template, send_from_directory, current_app, redirect, url_for
import uuid
from pathlib import Path
from apps.image.forms import UploadImageForm

flaskからcurrent_app、redirect、url_forをそれぞれインポートします。

uuidは、ファイル名をそのままにするとセキュリティ上問題になる可能性があるため、ファイル名変換に利用します。

また、forms.pyで作成したUploadImageFormクラスをインポートします。

@image.route("/upload", methods=["GET", "POST"])
def upload_image():
    # UploadImageFormを利用してバリデーションチェック
    uploadimageform = UploadImageForm()
    if uploadimageform.validate_on_submit():
        # アップロードされた画像ファイルを取得
        file = uploadimageform.image.data
        # ファイルのファイル名と拡張子を取得しファイル名をuuidに変換
        ext = Path(file.filename).suffix
        image_uuid_file_name = str(uuid.uuid4()) + ext

        # 画像保存
        image_path = Path(current_app.config["UPLOAD_FOLDER"], image_uuid_file_name)
        file.save(image_path)

        # DB保存
        user_image = UserImage(
            image_path = image_uuid_file_name
        )
        db.session.add(user_image)
        db.session.commit()
        # アップロード後はリダイレクトで画像一覧画面へ
        return redirect(url_for("image.index"))
    
    return render_template("image/upload.html", form=uploadimageform)

デコレータ関数にて、uploadエンドポイントを作成します。

UploadImageFormクラスをインスタンス化します。

画像ファイルアップロード時のバリデーションチェックを実施します。

始めに、アップロードされた画像ファイルを取得します。

取得したファイルの拡張子を取得し格納します。

uuidにてファイル名を変換し、拡張子を付与します。

image_pathは、current_app.config[“UPLOAD_FOLDER”]でUPLOAD_FOLDERのパスを取得し、image_uuid_file_nameを格納します。

file.saveにて、画像保存します。

DBへの保存は、UserImageクラスのimage_path属性にimage_uuid_fole_nameを格納し、db.session.addと.commitで追加/登録します。

render_templateでformデータをテンプレートに渡しています。

画像ファイルアップロード機能のテンプレート作成

次に、画像ファイルアップロード機能を反映させたテンプレートを作成します。

flask-image
├── .env
└── apps
    ├── app.py
    ├── config.py
    ├── static/css
    │   └── bootstrap.min.css
    ├── image
    │   ├── __init__.py
    │   ├── forms.py
    │   ├── models.py
    │   ├── static/css
    │   │   └── style.css
    │   ├── templates
    │   │   └── image
    │   │       ├── base.html
    │   │       ├── index.html
    │   │       └── upload.html
    │   └── views.py
    └── images

upload.htmlを作成します。

{% extends "image/base.html" %}
{% block content %}
<div class="mx-2 my-2">
  <h5>画像アップロード</h5>
  <p>アップロードする画像を選択してください</p>
  <form action="{{ url_for('image.upload_image') }}" method="POST" enctype="multipart/form-data" novalidate="novalidate">
    {{ form.hidden_tag() }}
    <div>
      <label>
        <span>{{ form.image(class="form-control-file") }}</span>
      </label>
    </div>
    {% for error in form.image.errors %}
    <span style="color:red;">{{ error }}</span>
    {% endfor %}
    <hr/>
    <div>
      <label>{{ form.submit(class="btn btn-primary") }}</label>
    </div>
  </form>
</div>
{% endblock %}

動作確認

flask runコマンドでアプリを起動し、127.0.0.1:5000/image/へアクセスします。

サイドバーから画像アップロードページに遷移すると、以下のようなページが表示されます。

実際に、ファイル選択ボタンから画像ファイルを選択し、アップロードすると画像一覧ページにリダイレクトされます。

問題なければ、アップロードした画像は画像一覧ページで確認できます。

削除機能の作成

削除機能作成における主な手順は以下になります。

削除機能作成の手順
  • 削除機能のフォームクラス作成
  • 削除機能のエンドポイント作成
  • 画像一覧表示のテンプレートに削除機能追加
  • 動作確認

削除機能のフォームクラス作成

forms.pyに削除機能のフォームクラスを追加します。

# flask_wtfからFlaskFormクラスをインポート
from flask_wtf import FlaskForm
# flask_wtf.fileから各フィールドをインポート
from flask_wtf.file import FileAllowed, FileField, FileRequired
# wtformsからサブミットフィールドをインポート
from wtforms.fields.simple import SubmitField

class UploadImageForm(FlaskForm):
    # ファイルフィールドに必要なバリデーションを設定
    image = FileField(
        validators = [
            FileRequired("画像ファイルを指定してください"),
            FileAllowed(["png", "jpg", "jpeg"], "サポートされていない画像形式です"),
        ]
    )
    submit = SubmitField("アップロード")

# 画像削除フォームクラス作成
class DeleteImageForm(FlaskForm):
    submit = SubmitField("delete")
forms.py
  • DeleteImageFormクラスの作成
class DeleteImageForm(FlaskForm):
    submit = SubmitField("delete")

FlaskFormを継承したDeleteImageFormクラスを作成し、delete用サブミットフィールドを設定しています。

削除機能のエンドポイント作成

削除機能のエンドポイントを作成します。

from apps.app import db
from apps.image.models import UserImage
from flask import Blueprint, render_template, send_from_directory, current_app, redirect, url_for
# uuidをインポート
import uuid
# Pathをインポート
from pathlib import Path
# forms.pyからDeleteImageFormクラスを追加インポート
from apps.image.forms import UploadImageForm, DeleteImageForm
# osをインポート
import os

image = Blueprint(
    "image",
    __name__,
    template_folder = "templates",
    static_folder = "static",
)

@image.route("/")
def index():
    user_images = db.session.query(UserImage).all()
    deleteimageform = DeleteImageForm()
    return render_template("image/index.html", user_images=user_images, form=deleteimageform)

@image.route("/images/<path:filename>")
def image_file(filename):
    return send_from_directory(current_app.config["UPLOAD_FOLDER"], filename)

@image.route("/upload", methods=["GET", "POST"])
def upload_image():
    # UploadImageFormを利用してバリデーションチェック
    uploadimageform = UploadImageForm()
    if uploadimageform.validate_on_submit():
        # アップロードされた画像ファイルを取得
        file = uploadimageform.image.data
        print(file)
        # ファイルのファイル名と拡張子を取得しファイル名をuuidに変換
        ext = Path(file.filename).suffix
        print(Path(file.filename))
        print(ext)
        image_uuid_file_name = str(uuid.uuid4()) + ext

        # 画像保存
        image_path = Path(current_app.config["UPLOAD_FOLDER"], image_uuid_file_name)
        file.save(image_path)

        # DB保存
        user_image = UserImage(
            image_path = image_uuid_file_name
        )
        db.session.add(user_image)
        db.session.commit()
        # アップロード後はリダイレクトで画像一覧画面へ
        return redirect(url_for("image.index"))
    
    return render_template("image/upload.html", form=uploadimageform)

@image.route("/delete/<string:image_id>", methods=["POST"])
def delete_image(image_id):
    # user_imagesテーブルからレコードを削除
    delete_image = db.session.query(UserImage).filter(UserImage.id == image_id).first()
    os.remove(f"apps/images/{delete_image.image_path}")
    db.session.query(UserImage).filter(UserImage.id == image_id).delete()
    db.session.commit()

    return redirect(url_for("image.index"))
views.py
  • forms.pyからDeleteImageFormクラスを追加インポート
  • osのインポート
  • index関数にdeleteimageformを追加
  • delete_imageエンドポイント作成
  • imagesディレクトリから画像削除
  • DBから画像データ削除
  • 画像一覧ページにリダイレクト
from apps.image.forms import UploadImageForm, DeleteImageForm
import os

forms.pyで作成したDeleteImageFormクラスとimagesディレクトリから画像削除するためosライブラリをインポートします。

@image.route("/")
def index():
    user_images = db.session.query(UserImage).all()
    deleteimageform = DeleteImageForm()
    return render_template("image/index.html", user_images=user_images, form=deleteimageform)

index関数内にDeleteImageFormをインスタンス化し、テンプレートへformを渡しています。

@image.route("/delete/<string:image_id>", methods=["POST"])
def delete_image(image_id):
    # user_imagesテーブルからレコードを削除
    delete_image = db.session.query(UserImage).filter(UserImage.id == image_id).first()
    os.remove(f"apps/images/{delete_image.image_path}")
    db.session.query(UserImage).filter(UserImage.id == image_id).delete()
    db.session.commit()

    return redirect(url_for("image.index"))

delete_imageエンドポイントを作成しています。

idを利用してDBから一意の画像データを取得し、os.removeでimagesディレクトリから画像ファイルを削除しています。

また、画像削除後にDBからも一意のレコードを削除しています。

削除が完了したら画像一覧表示画面へリダイレクトします。

画像一覧表示のテンプレートに削除機能追加

次に、画像一覧表示画面のテンプレートファイルに削除ボタンを追加します。

{% extends "image/base.html" %}
{% block content %}
<div class="row row-cols-1 row-cols-md-2 g-4 mx-2 my-2">
{% for user_image in user_images %}
  <div class="col">
    <div class="card">
      <img src="{{ url_for('image.image_file', filename=user_image.image_path) }}" class="card-img-top img-thumbnail" style="border: none;" alt="画像">
      <div class="card-body">
        <div class="d-flex justify-content-end">
          <form action="{{ url_for('image.delete_image', image_id=user_image.id) }}" method="POST">
            {{ form.hidden_tag() }}
            {{ form.submit(class="form-delete-btn material-symbols-outlined")}}
          </form>
        </div>
      </div>
    </div>
  </div>
{% endfor %}
</div>
{% endblock %}

BootStrap以外にも、style.cssを記述しています。

.form-delete-btn {
    border: none;
    background:#fff;
}

動作確認

以下は削除ボタン追加後の画面になります。

画像右下部に追加した削除ボタンを押下すると、画像が削除されたことが分かります。

flask-loginによるログイン機能を実装した認証アプリ開発

flask-login
ログイン認証アプリ開発の工程
  • Blueprintによるアプリ登録
  • SQLAlchemyとMigrateの事前準備
  • コンフィグ設定
  • データベースを使ったCRUD機能作成
  • サインアップ機能の作成
  • ログイン機能の作成
  • ログアウト機能の作成

本記事では、メインとして画像表示アプリの開発を目的にしています。

そのため、ログイン機能を実装した認証アプリを合わせて開発したい場合は、「【Flask】flask-loginによるログイン機能を実装した認証アプリ開発」を一読ください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

sugiのアバター sugi SUGI

【経歴】玉川大学工学部卒業→新卒SIer企業入社→2年半後に独立→プログラミングスクール運営/受託案件→フリーランスエンジニア&SEOコンサル→Python特化のコンテンツサイトJob Code運営中

コメント

コメントする

目次