教材の一つ(複雑すぎて提案する前に自発的にボツ)でP2Pのファイル共有ソフトを作ったので、それの解説
全体コード
from flask import jsonify, Flask, send_file, request
from urllib.parse import quote
import glob
import pandas as pd
import httpx
import asyncio
import html
import socket
import os
app = Flask(__name__)
@app.route("/")
async def home():
df = pd.read_csv("node.csv")
res = "<table border=\"1\">"
async with httpx.AsyncClient(timeout=3.0) as client:
tasks = []
for i in range(len(df.values)):
tasks.append(client.get("http://"+df.values[i][0]+"/files"))
responses = await asyncio.gather(*tasks, return_exceptions=True)
for response in responses:
if isinstance(response, Exception):
continue
jsn = response.json()
res += "<tr><td>" + list(jsn)[0] + "</td><td>"
for i in range(len(jsn[list(jsn)[0]])):
res += "<a href=\"http://" + list(jsn)[0] + "/download?name=" + quote(jsn[list(jsn)[0]][i].replace("./files\\", "")) + "\">" + html.escape(jsn[list(jsn)[0]][i].replace("./files\\", "")) + "</a><br>\n"
res += "</td></tr>\n"
res += "</table>"
return res
@app.route("/files")
def files():
host = socket.gethostname()
ip = socket.gethostbyname(host)
jsn = {ip : glob.glob("./files/*")}
return jsonify(jsn)
@app.route("/download")
def download():
file = request.args.get("name")
return send_file("files/"+os.path.basename(file))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=80)
では部分的に見ていきましょう。
ファイル一覧API
@app.route("/files")
def files():
host = socket.gethostname()
ip = socket.gethostbyname(host)
jsn = {ip : glob.glob("./files/*")}
return jsonify(jsn)
ここではAPIとしてファイル一覧のAPIを出力します。
「files」というフォルダを用意してそこに共有したいファイルを入れます。
JSONのキーを自分のIPアドレスにしてリストにglobライブラリを使いファイル一覧が入るようになります。
ファイルダウンロード
@app.route("/download")
def download():
file = request.args.get("name")
return send_file("files/"+os.path.basename(file))
URLパラメータでファイル名を貰い、filesフォルダの中から当該ファイルをダウンロードします。
この時、ディレクトリトラバーサル対策でbasename(ファイル名だけを抽出)を使います。
ファイル一覧画面
@app.route("/")
async def home():
df = pd.read_csv("node.csv")
res = "<table border=\"1\">"
async with httpx.AsyncClient(timeout=3.0) as client:
tasks = []
for i in range(len(df.values)):
tasks.append(client.get("http://"+df.values[i][0]+"/files"))
responses = await asyncio.gather(*tasks, return_exceptions=True)
for response in responses:
if isinstance(response, Exception):
continue
jsn = response.json()
res += "<tr><td>" + list(jsn)[0] + "</td><td>"
for i in range(len(jsn[list(jsn)[0]])):
res += "<a href=\"http://" + list(jsn)[0] + "/download?name=" + quote(jsn[list(jsn)[0]][i].replace("./files\\", "")) + "\">" + html.escape(jsn[list(jsn)[0]][i].replace("./files\\", "")) + "</a><br>\n"
res += "</td></tr>\n"
res += "</table>"
return res
では部分的に見ていきます。
非同期通信
@app.route("/")
async def home():
...
async with httpx.AsyncClient(timeout=3.0) as client:
tasks = []
for i in range(len(df.values)):
tasks.append(client.get("http://"+df.values[i][0]+"/files"))
responses = await asyncio.gather(*tasks, return_exceptions=True)
for response in responses:
if isinstance(response, Exception):
continue
jsn = response.json()
...
return res
非同期通信にすることで一般的なリクエストである順次アクセスではなく並列アクセスにします。
イメージとしては
同期通信
====A====>===B===>======C======>
非同期通信
|====A====>
|===B===>
|======C======>
となります。
大量にユーザがいる場合はイメージ図から分かるように非同期通信が有利になります。
画面ミス対策
意図せずファイル名にHTMLで扱えない文字やURLで使う事ができない(というより使うと誤動作する)文字の対策をします。
for response in responses:
if isinstance(response, Exception):
continue
jsn = response.json()
res += "<tr><td>" + list(jsn)[0] + "</td><td>"
for i in range(len(jsn[list(jsn)[0]])):
res += "<a href=\"http://" + list(jsn)[0] + "/download?name=" + quote(jsn[list(jsn)[0]][i].replace("./files\\", "")) + "\">" + html.escape(jsn[list(jsn)[0]][i].replace("./files\\", "")) + "</a><br>\n"
res += "</td></tr>\n"
res += "</table>"
return res
quoteでファイル名に「&」等があってもURLでは正常に使えるようになり、html.escapeでブラウザに文字列をそのまま表示できるようになります。