Raspberry Pi でファイルアップローダ 【Flask】

Raspberry Pi

ローカルネットワーク内の Raspberry Pi を使って、ファイルアップローダの Web アプリを作ってみます。
機能としては地味ですが、Web サーバとクライアント間でのファイル送受信の基本機能を作ってみます。応用範囲は広いと思います。

以下の環境で動作確認をしています。
環境:
・ Raspberry Pi (bullseye、Web サーバーとして使用)
・ Windows パソコン(クライアントとして使用)
・ Wi-Fi ネットワーク(Raspberry Pi と Windows パソコンはローカルネットワークに接続済み)

背景 ローカルネットワークでアップローダ!

これまで、このサイトでは、できるだけ最短で Raspberry Pi を Web サーバー化する手順などをまとめてきました(下記の関連リンク参照)。
クライアント側からサーバーに文字列を送り、何らかの処理をして返すことが可能となっています。
サーバーとのデータのやり取りという観点では、文字列以外にファイルの送受信があります。ファイルの送受信ができれば、サーバーとクライアント間でのデータのやり取りは、ほぼ網羅できると思われます。

そこで今回は、画像や動画などの任意のファイルをアップロードできる Web アプリを作ってみることにします。画像検索サイトなどでよく見かける機能です。

Raspberry Pi に標準でインストールされている Flask を使うことで、余計なソフトウェアをインストールすることなく、最小限の手間で作ってみます。

ローカルのパソコンにあるファイルや、ネットの画像検索で見つけた画像などを、かんたんにラズパイのサーバーにアップロードして保存できるようになります。
アップロードされたファイルについて機能を追加していくことで、任意の画像処理や物体認識などの機能を発展させていくことが可能になります。

設定方法

① 以下を参考に、Raspberry Pi にフォルダを作成してください。このフォルダをドキュメントルート(公開フォルダ)にします。
例: /home/pi/flask1/uploader1/
② ①のフォルダ内に、”app.py” という名前でテキストファイルを作成し、下記のサンプルスクリプトを貼りつけて保存してください。
③ ①のフォルダ内に、”upload1″ という名前でフォルダを作ってください。
例: /home/pi/flask1/uploader1/upload1/
アップロードしたファイルは、この中に保存されます。
④ ①のフォルダ内に “templates” という名前でフォルダを作ってください。
例: /home/pi/flask1/uploader1/templates/
⑤ ④の “templates” フォルダの中に、“index.html”、”uploader01.html” という名前で2つのテキストファイルを作成し、以下のサンプルスクリプトをそれぞれ貼りつけて保存してください。それぞれ、メイン画面、アップロード画面になっています。

使い方

Web サーバーを動かす

⑥ 以下のコマンドを実行し、Raspberry Pi の IP アドレス(ローカルIPアドレス)を確認してください。

hostname -I

例: 192.168.1.33

※ 詳細は、”ip addr” を実行すると確認できます。Wi-Fi 接続であれば、wan0、LAN ケーブル接続であれば、eth0 の項目で、ローカルネットワーク内の IP アドレスを確認できます。ネットワークに正常に接続されていれば、192.*.*.*、172.*.*.*、10.*.*.* となるはずです。

⑦ Raspberry Pi のターミナルを起動し、①のフォルダ(ドキュメントルート)に移動してください。
例:

cd /home/pi/flask1/uploader1

⑧ 以下のコマンドを実行してください。

flask run --host=0.0.0.0 --port=5000

→ ”Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)” という文字列が表示されたら、Web サーバーの起動成功です。
※ Web サーバーを停止する場合は、[ctrl] と [c] のキーを同時に押してください。
※ ここで、”5000″ の数値(ポート番号)は、他の数値に変更しても問題ありませんが、以下でブラウザで参照する際に、同じ値となるようにしてください。

ブラウザから Web サーバーにアクセスする

⑨ ⑧の Web サーバーが動いている状態で、Raspberry Pi と同じネットワーク内の別のパソコン(Windows など)でブラウザを起動し、以下を参考に、Webサーバー(Raspberry Pi のIPアドレス + ポート番号)にアクセスしてみてください。
例: http://192.168.1.33:5000

→ Web サーバーのトップ画面が表示されたら、Web サーバーへのアクセス成功です!

※ ここで、IP アドレス(192.168.1.33) は、⑥で確認した値と一致させてください。
また、”5000″ の数値(ポート番号)は、⑧で設定・表示された値と一致させてください。

⑩ トップ画面で、アップローダのリンク(”uploader01″)をクリックしてください。
ファイルアップロードの画面に遷移します。
⑪ 「ここにドラッグ&ドロップ」となっている領域に、jpg 画像、その他のファイルをドラッグしてみてください。ファイルをサーバーにアップロードできます。
⑫ Raspberry Pi 側で作成した③のフォルダ(upload1)内を確認してください。

→ ドラッグしたファイルがアップされていたら、アップロード成功です。

うまく動いたら

・ うまく動いたら、ローカルのファイルや、Google 検索などで表示された画像を上記の欄にドラッグしてみてください。画像などを簡単にアップロードできると思います。PDFファイル、動画ファイルなどもアップロードしてみてください。
・ また、メイン画面やアップロード画面の HTML を修正し、デザインや表示などを変えてみてください。HTML などを修正した場合は、⑧のサーバーを再起動して、修正を反映させてください。
・ app.py のスクリプトなども修正してみてください。
・ また、アップロードフォルダ内のファイルについて、Python を使ってリンクの一覧を表示する HTML を生成することで、アップロードした動画などを Web アプリ上でいつでも視聴できるようにもできます。自宅のローカルネットワーク内で、YouTube のようなサイトを自作することも可能です。
・ アップロードした画像について画像処理をすることで、物体認識など機械学習関連の機能の追加も可能です。
・ アップロードの要素技術を発展させて、どのような応用が考えられるか、案出しや機能確認版の試作などもしてみてください。

スクリプトの説明

Flask のスクリプト app.py について

・ 冒頭の import と app = Flask(…) のあたりは、標準的な Flask の書き方に沿って記載しています。お決まりの定型文です。
・ @app.route(“/”) のところで、サイトのトップにアクセスすると index1() 関数が実行されるよう紐づけしています。また、index1() が実行されると、テンプレート “index.html” を返します。
これにより、サイトのトップにアクセスすると、トップ画面 index.html が表示されます。render_template() を使うというところと、”templates” フォルダを参照しにいくというところは、Flask の仕様です。
・ トップ画面の index.html には、HTML でリンクが記載してあります。リンクをクリックすると “/uploader01” にジャンプするよう記載しています。プログラムを追加する場合は、この記載を参考にリンクを追加してください。
・ @app.route(“/uploader01″) とした部分で、”/uploader01” にアクセスすると “uploader01.html” を返すよう記載しています。これにより、アップローダの画面 uploader01.html が表示されます。
・ アップローダ画面 uploader01.html 内では、ファイルをドロップすると、ドロップされたファイルを取得して、”/uploader01_1″ に POST でアクセスするよう設定しています。これにより、ファイルがサーバーに送信されます。
・ @app.route(“/uploader01_1”) とした部分で、この URL にアクセスがあったときの処理 uploader01_1() を記載しています。
・ 関数 uploader01_1() では、送られてきたファイルを file1 = request.files[…] として取得し、”upload1″ フォルダに保存します。
ext1 = … としたところで、ファイルが jpg かどうかを判定し、jpg でなければそのまま保存、jpg であれば exif 情報も保存するようにしています。これは、たまたま、このスクリプトを発展させて、今後、画像処理などをやりたいと思っているためです。元ファイルの情報をできる限り維持したいため、サンプルとして記載しています。不要であれば、この部分の処理は、自由に削除、修正してください。
・ また、現在のスクリプトでは、ファイル名の重複チェックなどをしていません。つまり、ドロップしたファイルのファイル名が過去に保存されているファイル名と同一であるとき、上書き保存となります。
もし、過去のファイルを消したくない場合は、同一ファイルがないかどうかを事前にチェックし、あれば付番をするなど、機能追加を検討してみてください。
・ 関数 uploader01_1() では、ファイルの保存が完了すると、文字列をクライアント側に返すようにしています。
具体的には、アップロードが完了したときの文字列 “uploaded!” と、受領したファイルのファイル名を送信しています。文字列を2つ送りたいため、セパレータとして “\n\n” を間に入れて文字列を結合しています。

メイン画面 index.html について

・ メイン画面 index.html は、Flask のサーバーのトップ画面の一例として作成しています。
この中で、アップロード画面のリンク先を記載しています。
・ デザインや文字列の内容はあまり考えずに作っていますので、うまく動いたら、HTML や CSS などを工夫するなど、もっと洗練したデザインに修正してください。
・ また、新たな Web アプリを作成して追加したい場合は、このアップローダの記載と同様に、リンク先(uploader02、uploader03、…)と、対応するページ(uploader02.html、uploader03.html、…)を追加していけばよいです。リンク先の名称等は自由にアレンジしてください。

アップロード画面 uploader01.html について

・ アップロード画面 uploader01.html では、冒頭の <style> … </style> としたところで、ドラッグエリアの大きさや色を記載しています。
・ <body> … </body> 内で、アップロードに必要な各要素を定義しています。
具体的には、ドロップするエリア “area1” と、サーバーから返ってきた結果を表示するための要素 “div1″、”div2” の計3つを定義しています。
・ <script> … </script> 内で、ドラッグ中の処理と、ドロップ時の処理、ドロップ後にサーバーから応答が返ってきたときの処理を記載しています。
・ ドロップエリア “area1” において、addEventListener() を追加することで、領域内に入ったか、ドラッグしているところか、領域から離れたか、ドロップされたか、各イベントを監視してイベント発生時の処理を定義しています。
・ ファイルがドロップされると、addEventListener(“drop” … ) としたところで、function が実行されるようにしています。
この関数内で、ドロップされたときにもしファイルが見つかったら、その最初のファイル files1[0] について、request1() 関数を実行するようにしています。
・ request1() 関数では、ファイルを送信し、サーバーからの応答を取得する処理を記載しています。
サーバーとの送受信は、Fetch API という処理を使っています。典型的な記載としています。取得したファイルを data1 にぶら下げて送信をトライ(try)し、サーバーからの応答が返ってきたら(resp1.ok)、応答時の処理 response1() を実行します。
Fetch API を使うことで、画面遷移をせず、現在表示している画面を部分的に書き換えることで、受け取ったデータを表示させるようにしています。
・ response1() 関数では、サーバーから受け取った文字列を分けて、div1、div2 の領域に表示しています。文字列の分割は app.py 側で “\n\n” とすることにしています。そこで、”\n\n” で split しています。この部分は、送受信したいデータの型や文字列などにより、自由に修正してください。

まとめ

Raspberry Pi に標準で入っている Flask を使って、ファイルのアップローダを作ってみました。
Flask のスクリプトも40行程度で書けるということがわかります。
HTML 側のファイルアップロードの処理は、今回、Fetch API という機能を使ってみましたが、特に問題なく動くことがわかります。

アップロードしたファイルについて、Python などで解析し、クライアント側に処理結果を返す、などとすれば、機械学習でのオブジェクト認識など、Web サーバー側での任意の画像処理、データ処理が実現できることになります。
今回のアップローダーを応用して、バーコードを読み取る Web アプリなども作成しています。関心があるようでしたら、関連リンクなども参照してみてください。

関連リンク
・ Raspberry Pi で Flask を動かしてみる 【Python】
・ Raspberry Pi でローカルWebサーバー 【Python 活用】
・ バーコードリーダ Web アプリ 【Raspberry Pi & Flask】
・ 固定IPアドレスを設定する 【Raspberry Pi】
・ サーバーのエラーコードのまとめ 【Webサーバー】

外部リンク [PR]  
・ Flask Development Server
・ Raspberry Pi 5 (8GB RAM) 技適対応品
・ Raspberry Pi 5 8GB 技適対応品/アクティブクーラー
・ Raspberry Pi 5用 27W USB-C PD(電源アダプタ)

サンプルスクリプト

Flask のサンプル:app.py

from flask import Flask, render_template, request 
import os 
from PIL import Image as im1 

app = Flask(__name__) 

@app.route("/") 
def index1():
    return render_template( "index.html" ) 

@app.route("/uploader01")                                # file uploader
def uploader01(): 
  return render_template( "uploader01.html" ) 

@app.route( "/uploader01_1", methods=["POST"] )          # drag and drop uploader 
def uploader01_1(): 
  path1 = "./upload1"
  file1 = request.files["file1"]
  file2 = path1 + "/" + file1.filename 
  ext1 = os.path.splitext( file2 )[1].lower() 
  if ext1 == ".jpg": 
    save_file1( file1, file2 )
  else: 
    with open( file2, "wb" ) as f1:
      f1.write( file1.read()) 
  str1 = "uploaded!" + "\n\n" + file1.filename
  return str1

def save_file1( file1, file2 ): 
  image1 = im1.open( file1 ) 
  exif1 = image1.info.get( "exif" ) 
  if exif1: 
    image1.save( file2, exif=exif1 ) 
  else:
    image1.save( file2 ) 
  return "" 

メイン画面の例 index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>main page</title>
</head>
<body style="background-color:#efefef;" >
<h1>main page</h1> 

<a href="./uploader01">uploader01</a> : file uploader<br>

</body>
</html>


アップロード画面の例 uploader01.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <title>file uploader</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        #area1 {
            width: 200px;
            height: 70px;
            border: 3px dashed #808080;
            margin: 10px auto 10px 0;
            text-align: center;
            line-height: 18px;
            background-color: #ffffff; 
        }
        #area1.dragover1 {
            border-color: #0000ff;
            background-color: #cfdfef;
        }
    </style>
</head>

<body style="background-color:#efefef;" >
    <h1>file uploader v0.1</h1>
    <div id="area1"><br>ここにファイルを<br>ドラッグ&ドロップ</div>
    <div id="div1"></div> 
    <div id="div2"></div> 
    <script>
        const area11 = document.getElementById( "area1" );
        area11.addEventListener("dragenter", function( event1 ) {
            event1.preventDefault();
            area11.classList.add( "dragover1" );
        });
        area11.addEventListener("dragover", function( event1 ) {
            event1.preventDefault();
        });
        area11.addEventListener("dragleave", function( event1 ) {
            event1.preventDefault();
            area11.classList.remove( "dragover1" );
        });

        area11.addEventListener("drop", function( event1 ) {
            event1.preventDefault();
            area11.classList.remove( "dragover1" );
            const files1 = event1.dataTransfer.files;
            if (files1.length > 0) {
                request1( files1[0] ); 
            }
        });

        async function request1( file1 ) {     // Fetch API   request & response 
            const url1 = "/uploader01_1";
            const data1 = new FormData();
            data1.append("file1", file1);
            try {
                const resp1 = await fetch(url1, { method: "POST", body: data1 });
                if (resp1.ok) { 
                    const str1 = await resp1.text();
                    response1(str1);
                } 
            } catch ( error1 ) {console.error("Fetch error:", error1);}
        } 

        function response1( str1 ) {
            const a1 = str1.split("\n\n"); 
            const div11 = document.getElementById("div1");
            const div21 = document.getElementById("div2");
            div11.innerHTML = a1[0];
            div21.innerHTML = a1[1];
        }

    </script>
</body>
</html>

 

タイトルとURLをコピーしました