【脱クラウド】Python数十行で自作AirDrop!OSの壁を破壊する爆速ファイル転送ツールを作ってみた(Fletとの死闘編)

本ページはプロモーションが含まれています
国内のAI狂い

やっほー!国内のAI狂いだよ!✨

iPhoneで撮った動画をWindowsに送るの、面倒くさすぎない?ケーブル探すのもダルいし、クラウド経由だと画質落ちるし…。だからPythonで「自分専用のAirDrop」を作っちゃったよ!🐍🔥

目次

OSの壁を破壊したい全人類へ

iPhoneを使ってる人は「AirDrop最高!」って言うし、Android勢は「Nearby Share(クイック共有)便利!」って言うよね。
でもさ、「iPhoneからWindows PCへ」とか「AndroidからiPadへ」ってなった瞬間、急に原始時代に戻らない?🦍

  • LINEで送ると画質が死ぬ💀
  • Googleドライブに上げると「アップロード中…」で永遠に待たされる⏳
  • USBケーブル? どこ置いたっけ…あれ、Type-Cじゃない!?😱

この「ちょっとファイルを送りたいだけなのに、なんでこんなに苦労するの?」というストレスを、技術の力(Python)で物理的に粉砕することにしました。

開発秘話:GUIライブラリ「Flet」との敗北、そして原点回帰

最初はね、「今風のカッコいいアプリにしよう!」と思って、PythonのGUIライブラリ「Flet」を使って開発してたの。
画面にQRコードが出て、オシャレなボタンがあって…みたいな。

でも、そこで待ち受けていたのは地獄のエラー祭りでした😭

踏み抜いた地雷たち💣

  1. 画像パスの日本語問題: フォルダ名に日本語が入っていると、Fletが画像を読み込めずにエラーを吐く。
  2. セキュリティの壁: 「ローカルの画像を表示する」という行為が、ブラウザベースのFletではセキュリティ制限(CORS)に引っかかりまくる。
  3. 仕様変更の嵐: バージョンアップで ft.Html が消えたり、アイコンの指定方法が変わったり、昨日のコードが今日動かないレベル。

数時間格闘して、私は気づいてしまったんだね。

「あれ? 私がやりたいのって『ファイル転送』であって、『ウィンドウを出すこと』じゃなくない?」

そう、本質を見失っていたの。
だから、GUI(ウィンドウ)を完全に捨てました。
エンジニアなら黙って「黒い画面(ターミナル)」!!
これならライブラリ依存も減るし、起動も爆速だし、何より「動かない」というリスクが極限まで減る!

完成した「最強の自作AirDrop」がこちら

見てこれ!この硬派な画面!✨

黒い画面にQRコードが表示されている様子
起動するとターミナルに直接QRコードが描画される!画像ファイルなんていらんかったんや!

スマホでこのQRを読み込むと、こんな感じのWebアプリ(ブラウザ)が立ち上がります👇

スマホ側の送受信画面
シンプルで超高速!送信も受信もこれ1つ。

このツールの推しポイント💖

  1. 完全ローカル通信: インターネットを経由しないから、ギガが減らない!画質劣化もなし!4K動画も一瞬!🚀
  2. マルチスレッド対応: ThreadingHTTPServer を採用したから、家族みんなで同時にアクセスしてもサクサク動くよ!(ここ地味に苦労したの…シングルスレッドだと接続が詰まるからね💦)
  3. 双方向対応: 「スマホ → PC」のアップロードだけじゃなく、「PC → スマホ」のダウンロードもリストからワンタップで可能!
  4. インストール不要: 相手がiPhoneでもAndroidでもiPadでも、「ブラウザ」さえあればアプリのインストールなしで使える! これが最強。

全ソースコード公開(コピペで動くよ!)

今回は Flet すら使いません。必要なのはQRコード生成用のライブラリだけ!
以下のコマンドでライブラリを入れてね。

pip install qrcode

そして、これが苦難の末にたどり着いた最終コードです。
ファイル名は my_airdrop.py とかにして保存してね!

import qrcode
import socket
import os
import sys
import time
import logging
import email.parser 
import email.policy
import webbrowser
import urllib.parse
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler # マルチスレッド対応!

# ==========================================
# 0. 硬派なデバッグログ設定
# ==========================================
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(message)s',
    datefmt='%H:%M:%S'
)
logger = logging.getLogger(__name__)

# ==========================================
# 設定エリア
# ==========================================
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
PORT = 8000
UPLOAD_DIR = os.path.join(BASE_DIR, "uploads")

# フォルダ生成
os.makedirs(UPLOAD_DIR, exist_ok=True)

# ==========================================
# 1. サーバー機能 (バックエンド)
# ==========================================
class AirDropHandler(SimpleHTTPRequestHandler):
    def log_message(self, format, *args):
        # ログが流れすぎないように抑制
        pass

    def do_GET(self):
        # ---------------------------------------------------------
        # 【新機能】PC内のファイルダウンロード処理
        # /files/ファイル名 にアクセスが来たら、uploadsフォルダの中身を返す
        # ---------------------------------------------------------
        if self.path.startswith('/files/'):
            # ファイル名をURLデコードして取得
            filename = self.path[len('/files/'):]
            filename = urllib.parse.unquote(filename)
            # ディレクトリトラバーサル対策(ファイル名のみ抽出)
            filename = os.path.basename(filename)
            file_path = os.path.join(UPLOAD_DIR, filename)

            if os.path.exists(file_path) and os.path.isfile(file_path):
                try:
                    # ファイルサイズ取得
                    file_size = os.path.getsize(file_path)
                    
                    self.send_response(200)
                    self.send_header('Content-Type', 'application/octet-stream')
                    self.send_header('Content-Disposition', f'attachment; filename="{urllib.parse.quote(filename)}"')
                    self.send_header('Content-Length', str(file_size))
                    self.end_headers()
                    
                    # ファイルを読み込んで送信
                    with open(file_path, 'rb') as f:
                        self.wfile.write(f.read())
                    logger.info(f"📤 ダウンロード完了: {filename}")
                    return
                except Exception as e:
                    logger.error(f"ダウンロードエラー: {e}")
                    self.send_error(500)
                    return
            else:
                self.send_error(404, "File Not Found")
                return

        # ---------------------------------------------------------
        # トップページ表示(アップロードフォーム + ファイル一覧)
        # ---------------------------------------------------------
        if self.path == '/':
            self.send_response(200)
            self.send_header('Content-type', 'text/html; charset=utf-8')
            self.end_headers()

            # uploadsフォルダの中身をリストアップしてHTMLを作る
            files_list_html = ""
            try:
                files = sorted(os.listdir(UPLOAD_DIR)) # 名前順にソート
                if not files:
                    files_list_html = "<li style='color:#888;'>ファイルがありません</li>"
                else:
                    for f in files:
                        # ダウンロードリンクを作成
                        files_list_html += f"""
                        <li>
                            <a href="/files/{f}" class="file-link">
                                📄 {f} <span class="download-icon">⬇️</span>
                            </a>
                        </li>
                        """
            except Exception as e:
                files_list_html = f"<li>エラー: {e}</li>"

            # シンプルかつ高機能なUI
            html = f"""
            <!DOCTYPE html>
            <html lang="ja">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Python AirDrop V2</title>
                <style>
                    body {{ font-family: sans-serif; text-align: center; padding: 20px; background: #222; color: #fff; margin: 0; }}
                    .container {{ max-width: 600px; margin: 0 auto; background: #333; border-radius: 15px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.5); }}
                    
                    /* ヘッダーエリア */
                    .header {{ background: #00ff00; color: #000; padding: 15px; }}
                    h1 {{ margin: 0; font-family: monospace; font-size: 24px; }}
                    p {{ margin: 5px 0 0; font-size: 14px; opacity: 0.8; }}

                    /* エリア共通 */
                    .section {{ padding: 20px; border-bottom: 1px solid #444; }}
                    h2 {{ color: #00ff00; font-size: 18px; margin-top: 0; border-left: 4px solid #00ff00; padding-left: 10px; text-align: left; }}

                    /* アップロードフォーム */
                    input[type="file"] {{ display: block; width: 100%; padding: 10px; margin-bottom: 15px; background: #222; color: #fff; border: 1px dashed #666; box-sizing: border-box; }}
                    button {{ width: 100%; background-color: #00ff00; color: #000; border: none; padding: 15px; font-size: 16px; font-weight: bold; cursor: pointer; border-radius: 5px; transition: 0.2s; }}
                    button:active {{ transform: scale(0.98); background-color: #00cc00; }}

                    /* ダウンロードリスト */
                    ul {{ list-style: none; padding: 0; margin: 0; text-align: left; }}
                    li {{ border-bottom: 1px solid #444; }}
                    li:last-child {{ border-bottom: none; }}
                    .file-link {{ display: flex; justify-content: space-between; padding: 15px; color: #fff; text-decoration: none; transition: 0.2s; }}
                    .file-link:hover {{ background: #444; }}
                    .download-icon {{ font-size: 1.2em; }}
                </style>
            </head>
            <body>
                <div class="container">
                    <div class="header">
                        <h1>>> AIRDROP_V2</h1>
                        <p>Multi-Threaded Server Active</p>
                    </div>

                    <!-- エリア1: スマホ -> PC (アップロード) -->
                    <div class="section">
                        <h2>📤 スマホから送る</h2>
                        <form action="/" method="post" enctype="multipart/form-data">
                            <input type="file" name="file" multiple required>
                            <button type="submit">PCに送信する 🚀</button>
                        </form>
                    </div>

                    <!-- エリア2: PC -> スマホ (ダウンロード) -->
                    <div class="section">
                        <h2>📥 PCから受け取る</h2>
                        <p style="text-align:left; font-size:12px; color:#aaa;">※ uploadsフォルダ内のファイル一覧</p>
                        <ul>
                            {files_list_html}
                        </ul>
                    </div>
                </div>
            </body>
            </html>
            """
            self.wfile.write(html.encode('utf-8'))
        else:
            self.send_error(404)

    def do_POST(self):
        if self.path == '/':
            try:
                content_length = int(self.headers.get('Content-Length', 0))
                content_type = self.headers.get('Content-Type')
                if not content_type or 'multipart/form-data' not in content_type:
                    self.send_error(400, "Bad Request")
                    return
                
                # 大きなファイルも扱えるようにバッファ読み込みなどは改善の余地ありだが、
                # 今回はメモリ一括読み込みでシンプルに実装
                body = self.rfile.read(content_length)
                headers = f"Content-Type: {content_type}\r\n".encode('utf-8')
                msg = email.parser.BytesParser(policy=email.policy.default).parsebytes(headers + b'\r\n' + body)
                
                saved_files = []
                if msg.is_multipart():
                    for part in msg.iter_parts():
                        fn = part.get_filename()
                        if fn:
                            fn = os.path.basename(fn)
                            save_path = os.path.join(UPLOAD_DIR, fn)
                            file_data = part.get_payload(decode=True)
                            if file_data:
                                with open(save_path, 'wb') as f:
                                    f.write(file_data)
                                saved_files.append(fn)
                                logger.info(f"📥 受信完了: {fn}")
                
                # アップロード後のリダイレクト(再読み込み防止)
                self.send_response(303) # See Other
                self.send_header('Location', '/')
                self.end_headers()
                
            except Exception as e:
                logger.error(f"アップロードエラー: {e}")
                self.send_error(500)

def get_local_ip():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        return "127.0.0.1"

# ==========================================
# 2. メイン処理 (GUIなし!CUIのみ!)
# ==========================================
def main():
    os.system('cls' if os.name == 'nt' else 'clear')

    print(r"""
    =========================================
      PYTHON AIRDROP V2 (MULTI-THREAD) 🚀
    =========================================
    """)

    ip_address = get_local_ip()
    url = f"http://{ip_address}:{PORT}"
    
    print(f"[*] サーバーIP: {ip_address}")
    print(f"[*] ポート: {PORT}")
    print(f"[*] 保存先: {UPLOAD_DIR}")
    print("-" * 40)
    print("スマホで以下のQRコードを読み取ってください:")
    print("-" * 40)
    
    # QRコード表示
    qr = qrcode.QRCode()
    qr.add_data(url)
    qr.print_ascii(invert=True) 
    
    print("-" * 40)
    print(f"URL: {url}")
    print("サーバー起動中... (Ctrl+C で終了)")
    print("-" * 40)

    # ブラウザで開く
    webbrowser.open(url)

    try:
        # 【重要】ThreadingHTTPServerに変更!これで並列処理が可能に!
        server_address = ('0.0.0.0', PORT)
        httpd = ThreadingHTTPServer(server_address, AirDropHandler)
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\n\n[!] 終了します。お疲れ様でした!")
        sys.exit(0)
    except Exception as e:
        print(f"\n[!] 起動エラー: {e}")

if __name__ == "__main__":
    main()

使い方

  1. 上記のコードを実行する(python my_airdrop.py)。
  2. 黒い画面(ターミナル)にQRコードが表示される。
  3. スマホのカメラでQRコードを読み込む。
  4. ブラウザが開くので、ファイルを選んで送信!
  5. PCの uploads フォルダにファイルが保存される🎉
⚠️ 唯一の注意点(カフェとかでは気をつけて!)
記事に載せるのは安全だけど、このツールを使う場所には気をつけてね!
このツールは「同じWi-Fiにいる人なら誰でもアクセスできる」仕組みだから、スタバや空港のフリーWi-Fiでこのツールを起動するのはちょっと危険だよ。(同じカフェにいる悪い人が、あなたのPCにファイルを送りつけたりできちゃう可能性があるからね!)
「自宅のWi-Fi」で使う分には最強のセキュリティだから、堂々と記事にしちゃって!👍

まとめ

Python標準ライブラリの http.server って、実はめちゃくちゃ優秀なんだね。
GUIライブラリで消耗するよりも、HTMLテンプレートを直接Pythonコードに埋め込んで、ブラウザをUIとして使う方が、開発速度も安定性も段違いでした。

何より、「自分の手で作った道具で、日々の不便が解決される」って感覚が最高に気持ちいい!😆
みんなもぜひ、Pythonで生活をハックしてみてね!それじゃ、またね〜!👋

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