1
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

P2Pファイル共有ソフトを作る

1
Posted at

教材の一つ(複雑すぎて提案する前に自発的にボツ)で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でブラウザに文字列をそのまま表示できるようになります。

1
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?