なんちゃってP2Pファイル共有ソフト(笑)作った話

昨年度にとある理由があって分散システム関係で映画『Winny』を見たときに似たようなものを作ってみたく挑戦したお話です。

https://www.youtube.com/watch?v=kJRrn_tgxy8

本編

https://watch.amazon.co.jp/detail?gti=amzn1.dv.gti.48c9649b-d715-478b-9708-218fc7176f11&ref_=atv_dp_share_mv&r=web

※なお、私は凡人以下なのでlocalhostでしか使えませんし通信も暗号化していませんしファイルはどこも経由せずそのまま送られますし、匿名通信でもありません
# サーバサイド
私は学生時代にFlaskを使ったWebサービスやマッシュアップという他のサイトにある情報を組み合わせてサービスを作る仕組みについて学んだ事があり、DBPediaのJSONを用いて簡易辞書を作った事があります。

## サーバサイド内クライアントサイド
マッシュアップをするにはサーバ側から他のサイトにリクエストを送りHTMLを取得したりJSONなどWebAPIを取得します。

# サーバ×クライアントの合体
ここで考えたのがサーバとしてプログラムを起動する事でブラウザでリソースを得ることができます。
そして全員が同じサーバサイドプログラムを使うことで同じ機能を使いサーバサイドからクライアント機能としてリクエストの機能を実装してサーバサイドとしてファイル一覧とリンクを共有する事でファイル共有をできるのではないかと考えたわけです。

# 必要なファイル構成
まず、サーバサイドのプログラムとしてapp.pyを用意します(最後にコード載せています)。そして連絡先の「node.csv」を用意し、ブラウザに表示するHTMLを入れる「templates」のフォルダにPythonのFlaskで使用するrender_templateに対応するHTMLファイルを用意します。次に自分が強制送信と相手のHTMLにファイル一覧を表示する用に「uploads」というフォルダを用意して、ここに共有するファイルを入れます。また、強制送信されたファイルを保存する「downloads」というフォルダを用意します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2782889/388ea927-d49e-5c91-015f-b1acf8af9796.png)
## node.csv
ここには連絡先一覧が入ります。pandasでPythonでは読み込みます。
```
ユーザ名,IPアドレス,ポート番号
自分,127.0.0.1,5024
test2,192.168.123.130,5010
test3,192.168.128.130,5024
```
# コードの説明
## 強制送信
これは(悪意のあるファイルは送っちゃダメ)通信が成り立つかのテストを行うのに使います。
### home画面から
formタグで相手先のIPアドレスとポート番号、また、自分の「uploads」に入っている任意のファイル名を指名します。
```HTML
<!DOCTYPE html>
{{ err | safe }}
{{ result | safe }}
<h2>あなたのIPアドレス</h2>
<h3>{{ ip | safe }}</h3>
<h2>送信情報</h2>
<form action="upload" method="GET">
    <table>
        <tr><td>相手IPアドレス</td><td><input type="text" name="address" required></td></tr>
        <tr><td>相手ポート番号</td><td><input type="number" name="toport" required></td></tr>
        <tr><td>ファイル名</td><td><input type="text" name="path" required></td></tr>
        <tr><td colspan="2" align="right"><input type="submit" value="送信"></td></tr>
    </table>
</form>
<h2>アドレス集</h2>
{{ node | safe }}
```
「upload」にGETで情報が渡されて今度は「download」にPOSTでファイルを送信します。
```Python3
@app.route("/upload", methods=["GET"])
def upload():
    path = request.args.get("path")
    address = request.args.get("address")
    toport = request.args.get("toport")
    url = "http://" + address + ":" + str(toport) + "/download"
    try:
        files = {'file': open("uploads/"+path, "rb")}
    except:
        return redirect("home?err=openfile")
    response = requests.post(url=url, files=files, data={"filename":path})
    if response.status_code == 200:
        return redirect("home?result="+response.text)
    elif response.status_code == 404:
        return redirect("home?result=アドレスが間違っています")
```
```Python3
@app.route("/download", methods=["POST"])
def download():
    if "file" not in request.files:
        return "ファイルは送信されていません"
    file = request.files["file"]
    if file.filename == "":
        return "ファイル名が有りません"
    try:
        file.save("downloads/"+os.path.basename(request.form["filename"]))
        return "ファイルが保存されました"
    except:
        return "ファイルの保存に失敗しました"
```
この時、正常に動作するとファイルが保存されます。
また、uploadからクライアントとしての処理を行っているため画面は遷移しません。

## ファイルの共有
homeから相手のファイルを参照するにはアクセス先で情報を開示してもらう必要があります。
そこでPandasで相手先のURLを読み込み、読み込まれた相手側はファイルのリンクを送ります。
```Python3
@app.route("/home", methods=["GET"])
def home():
    host = socket.gethostname()
    ip = socket.gethostbyname(host)
    df = pd.read_csv("node.csv")
    cols = df.columns
    val = df.values
    node = "<table border=\"1\">\n\t<tr>"
    for col in cols:
        node = node + "<th>" + html.escape(col) + "</th>"
    node = node + "</tr>\n"
    for i in range(len(val)):
        node = node + "\t<tr>"
        for j in range(len(val[i])):
            node = node + "<td>" + html.escape(str(val[i][j])) + "</td>"
        node = node + "</tr>\n"
        node = node + "\t<tr>"
        try:
            response = requests.get("http://"+str(val[i][1])+":"+str(val[i][2])+"/files",
                                    timeout=(1.0, 2.5))
            res = response.text
            node = node + "<td colspan=\"3\">\n"
            node = node + res
            node = node + "\t</td>"
            node = node + "</tr>"
        except Timeout:
            node = node + "\n\t</tr>\n"
            pass
        try:
            response = requests.get("http://"+str(val[i][1])+":"+str(val[i][2])+"/json",
                                   timeout=2.0)
            jsons = json.loads(response.text)
            node = node + "\n\t<tr>\n\t\t<td colspan=\"2\">連絡先アドレス</td><td>ファイル</td>\n\t</tr>\n"
            for col in jsons:
                node = node + "\t<tr>\n\t\t<td colspan=\"2\">" + col + "</td><td>\n"
                for raw in jsons[col]:
                    node = node + "\t\t\t<a href=\""+ col + "/file?name=" + raw + "\">" + raw + "</a><br>\n"
                node = node + "\t\t</td>\n\t</tr>\n"
        except Timeout:
            pass
    node = node + "</table>"
    err = ""
    result = ""
    if request.args.get("err") is not None:
        err = "ファイルがありません"
    if request.args.get("result") is not None:
        result = request.args.get("result")
    return render_template("home.html", ip=ip, err=err, result=result, node=node)
```
ここでfilesにアクセスしてファイルの情報を得ます。
```Python3
@app.route("/files", methods=["GET"])
def datas():
    host = socket.gethostname()
    ip = socket.gethostbyname(host)
    files = glob.glob("uploads/*")
    res = ""
    for file in files:
        res = res + "\t\t<a href=\"http://" + ip + ":" + str(port) + "/file?name=" + os.path.basename(file) + "\">" + html.escape(os.path.basename(file)) + "</a><br>\n"
    return res

@app.route("/file", methods=["GET"])
def data():
    name = request.args.get("name")
    return send_file("uploads/"+name)
```
最後にfilesで指定されたURLでfileにアクセスしてsend_fileで相手にファイルを送信します。
当然ですが普通にダウンロードするとブラウザの機能でダウンロードされます。

# 全体コード
リポジトリ(ここからCloneすれば使えます)

https://github.com/NTanaka1994/p2ptest

- app.py
46行目から57行目は他人のPCでテストしていないので動く保証はないので消した方が良いかもしれません(他はテストして成功しています)。
```Python3
from flask import Flask, redirect, render_template, request, send_file, jsonify
from requests.exceptions import Timeout
import pandas as pd
import requests
import socket
import os
import html
import glob
import json

port = 5024
app = Flask("__main__")

@app.route("/", methods=["GET"])
def route():
    return redirect("home")

@app.route("/home", methods=["GET"])
def home():
    host = socket.gethostname()
    ip = socket.gethostbyname(host)
    df = pd.read_csv("node.csv")
    cols = df.columns
    val = df.values
    node = "<table border=\"1\">\n\t<tr>"
    for col in cols:
        node = node + "<th>" + html.escape(col) + "</th>"
    node = node + "</tr>\n"
    for i in range(len(val)):
        node = node + "\t<tr>"
        for j in range(len(val[i])):
            node = node + "<td>" + html.escape(str(val[i][j])) + "</td>"
        node = node + "</tr>\n"
        node = node + "\t<tr>"
        try:
            response = requests.get("http://"+str(val[i][1])+":"+str(val[i][2])+"/files",
                                    timeout=(1.0, 2.5))
            res = response.text
            node = node + "<td colspan=\"3\">\n"
            node = node + res
            node = node + "\t</td>"
            node = node + "</tr>"
        except Timeout:
            node = node + "\n\t</tr>\n"
            pass
        try:
            response = requests.get("http://"+str(val[i][1])+":"+str(val[i][2])+"/json",
                                   timeout=2.0)
            jsons = json.loads(response.text)
            node = node + "\n\t<tr>\n\t\t<td colspan=\"2\">連絡先アドレス</td><td>ファイル</td>\n\t</tr>\n"
            for col in jsons:
                node = node + "\t<tr>\n\t\t<td colspan=\"2\">" + col + "</td><td>\n"
                for raw in jsons[col]:
                    node = node + "\t\t\t<a href=\""+ col + "/file?name=" + raw + "\">" + raw + "</a><br>\n"
                node = node + "\t\t</td>\n\t</tr>\n"
        except Timeout:
            pass
    node = node + "</table>"
    err = ""
    result = ""
    if request.args.get("err") is not None:
        err = "ファイルがありません"
    if request.args.get("result") is not None:
        result = request.args.get("result")
    return render_template("home.html", ip=ip, err=err, result=result, node=node)

@app.route("/upload", methods=["GET"])
def upload():
    path = request.args.get("path")
    address = request.args.get("address")
    toport = request.args.get("toport")
    url = "http://" + address + ":" + str(toport) + "/download"
    try:
        files = {'file': open("uploads/"+path, "rb")}
    except:
        return redirect("home?err=openfile")
    response = requests.post(url=url, files=files, data={"filename":path})
    if response.status_code == 200:
        return redirect("home?result="+response.text)
    elif response.status_code == 404:
        return redirect("home?result=アドレスが間違っています")
    
@app.route("/download", methods=["POST"])
def download():
    if "file" not in request.files:
        return "ファイルは送信されていません"
    file = request.files["file"]
    if file.filename == "":
        return "ファイル名が有りません"
    try:
        file.save("downloads/"+os.path.basename(request.form["filename"]))
        return "ファイルが保存されました"
    except:
        return "ファイルの保存に失敗しました"

@app.route("/files", methods=["GET"])
def datas():
    host = socket.gethostname()
    ip = socket.gethostbyname(host)
    files = glob.glob("uploads/*")
    res = ""
    for file in files:
        res = res + "\t\t<a href=\"http://" + ip + ":" + str(port) + "/file?name=" + os.path.basename(file) + "\">" + html.escape(os.path.basename(file)) + "</a><br>\n"
    return res

@app.route("/file", methods=["GET"])
def data():
    name = request.args.get("name")
    return send_file("uploads/"+name)

@app.route("/json", methods=["GET"])
def getjson():
    df = pd.read_csv("node.csv")
    val = df.values
    jsons = {}
    for i in range(len(val)):
        try:
            response = requests.get("http://"+str(val[i][1])+":"+str(val[i][2])+"/file-json",
                                   timeout=(1.0, 2.5))
            jsons["http://"+str(val[i][1])+":"+str(val[i][2])] = json.loads(response.text)
        except Timeout:
            pass
    return jsonify(jsons)

@app.route("/file-json", methods=["GET"])
def file_json():
    files = glob.glob("uploads/*")
    res = []
    for file in files:
        res.append(os.path.basename(file))
    return jsonify(res)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=port)
```

# まとめ
YoutubeでP2Pの仕組みを見たらこのプログラムと似ていてサーバとクライアントの両方の機能を持った「サーバント」という名称で実装するとの事でした。
それを見る前から仕組みに気付けたのは大学で学んだ甲斐があったと思っても過言ではないでしょう。