スマホからPCにUSBを介さずにファイルを送りたい


USBって便利ですよね。でも繋ぐのが面倒だったり、ケーブルがないときもあります。クラウドストレージも便利ですが、セキュリティやプライバシーの観点から使いたくない人もいるでしょう。そんなとき、自分で簡単なファイル転送サーバーを立てる方法があります。

import http.server
import cgi

class UploadHandler(http.server.SimpleHTTPRequestHandler):
    def do_POST(self):
        # フォームデータの解析
        form = cgi.FieldStorage(
            fp=self.rfile,
            headers=self.headers,
            environ={'REQUEST_METHOD': 'POST'}
        )
        
        # 'file' という名前で送られてきたデータを保存
        if "file" in form:
            file_item = form["file"]
            with open(file_item.filename, "wb") as f:
                f.write(file_item.file.read())
            
            # 送信後のレスポンス
            self.send_response(200)
            self.end_headers()
            self.wfile.write(b"Upload Successful!")

    # ブラウザに表示するHTML(簡易的なアップロードボタン)
    def do_GET(self):
        if self.path == '/':
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            html = """
            <html><body>
            <form method="POST" enctype="multipart/form-data">
                <input type="file" name="file" style="font-size: 2em;"><br><br>
                <input type="submit" value="Upload to PC" style="font-size: 2em;">
            </form>
            </body></html>
            """
            self.wfile.write(html.encode())
        else:
            super().do_GET()

if __name__ == "__main__":
    http.server.test(HandlerClass=UploadHandler, port=8000)

このコードをPCで実行すると、ローカルホストの8000番ポートでファイル転送サーバーが立ち上がります。スマホのブラウザから http://<PCのIPアドレス>:8000/ にアクセスすると、ファイルを選択してアップロードできるフォームが表示されます。アップロードされたファイルは、PCの同じディレクトリに保存されるはずでしたが、Python 3.13以降では、cgi モジュールが、ついに**「完全に削除(Removed)」**されてしまったそうです。

Traceback (most recent call last):
  File "/home/miru/tools/server.py", line 2, in <module>
    import cgi
ModuleNotFoundError: No module named 'cgi'

それでも代わりの方法はあります。標準の email モジュール(マルチパート解析用)を使うのが、今の「枯れた」モダンなやり方です。

--- /home/miru/tools/server.py	2026-02-28 20:04:37.811910926 +0900
+++ /home/miru/tools/server2.py	2026-02-28 20:08:51.009089661 +0900
@@ -1,27 +1,34 @@
 import http.server
-import cgi
+import email.message
+import io
 
 class UploadHandler(http.server.SimpleHTTPRequestHandler):
     def do_POST(self):
-        # フォームデータの解析
-        form = cgi.FieldStorage(
-            fp=self.rfile,
-            headers=self.headers,
-            environ={'REQUEST_METHOD': 'POST'}
-        )
+        # ヘッダーから境界文字列(boundary)を取得
+        content_type = self.headers.get('Content-Type')
+        if not content_type or 'multipart/form-data' not in content_type:
+            self.send_error(400, "Expected multipart/form-data")
+            return
+
+        # ボディを解析してファイルを取り出す
+        length = int(self.headers.get('content-length'))
+        body = self.rfile.read(length)
         
-        # 'file' という名前で送られてきたデータを保存
-        if "file" in form:
-            file_item = form["file"]
-            with open(file_item.filename, "wb") as f:
-                f.write(file_item.file.read())
-            
-            # 送信後のレスポンス
-            self.send_response(200)
-            self.end_headers()
-            self.wfile.write(b"Upload Successful!")
+        # メッセージオブジェクトとしてパース
+        msg = email.message_from_bytes(b'Content-Type: ' + content_type.encode() + b'\r\n\r\n' + body)
+        
+        for part in msg.walk():
+            if part.get_filename():
+                filename = part.get_filename()
+                payload = part.get_payload(decode=True)
+                with open(filename, 'wb') as f:
+                    f.write(payload)
+                print(f"Saved: {filename}")
+
+        self.send_response(200)
+        self.end_headers()
+        self.wfile.write(b"Upload Successful!")
 
-    # ブラウザに表示するHTML(簡易的なアップロードボタン)
     def do_GET(self):
         if self.path == '/':
             self.send_response(200)
@@ -29,9 +36,10 @@
             self.end_headers()
             html = """
             <html><body>
+            <h2>Upload to My PC</h2>
             <form method="POST" enctype="multipart/form-data">
-                <input type="file" name="file" style="font-size: 2em;"><br><br>
-                <input type="submit" value="Upload to PC" style="font-size: 2em;">
+                <input type="file" name="file" style="font-size: 1.5em;"><br><br>
+                <input type="submit" value="Upload" style="font-size: 1.5em;">
             </form>
             </body></html>
             """

でもこれでは一回に一つのファイルしか送れません。複数ファイルを同時に送るには、HTMLフォームを少し変更して、複数選択を許可する必要があります。

--- /home/miru/tools/server2.py	2026-02-28 20:08:51.009089661 +0900
+++ /home/miru/tools/server3.py	2026-02-28 20:25:15.813981867 +0900
@@ -1,51 +1,51 @@
 import http.server
 import email.message
-import io
 
 class UploadHandler(http.server.SimpleHTTPRequestHandler):
     def do_POST(self):
-        # ヘッダーから境界文字列(boundary)を取得
         content_type = self.headers.get('Content-Type')
-        if not content_type or 'multipart/form-data' not in content_type:
-            self.send_error(400, "Expected multipart/form-data")
-            return
-
-        # ボディを解析してファイルを取り出す
         length = int(self.headers.get('content-length'))
         body = self.rfile.read(length)
         
-        # メッセージオブジェクトとしてパース
+        # マルチパートデータをパース
         msg = email.message_from_bytes(b'Content-Type: ' + content_type.encode() + b'\r\n\r\n' + body)
         
+        count = 0
         for part in msg.walk():
-            if part.get_filename():
-                filename = part.get_filename()
+            filename = part.get_filename()
+            if filename:
                 payload = part.get_payload(decode=True)
                 with open(filename, 'wb') as f:
                     f.write(payload)
                 print(f"Saved: {filename}")
+                count += 1
 
         self.send_response(200)
         self.end_headers()
-        self.wfile.write(b"Upload Successful!")
+        self.wfile.write(f"{count} files uploaded successfully!".encode())
 
     def do_GET(self):
         if self.path == '/':
             self.send_response(200)
             self.send_header("Content-type", "text/html")
             self.end_headers()
+            # 'multiple' 属性を追加し、少しスマホで押しやすくUIを調整
             html = """
-            <html><body>
-            <h2>Upload to My PC</h2>
-            <form method="POST" enctype="multipart/form-data">
-                <input type="file" name="file" style="font-size: 1.5em;"><br><br>
-                <input type="submit" value="Upload" style="font-size: 1.5em;">
-            </form>
-            </body></html>
+            <html>
+            <meta name="viewport" content="width=device-width, initial-scale=1">
+            <body style="font-family: sans-serif; text-align: center; padding-top: 50px;">
+                <h2>File Uploader</h2>
+                <form method="POST" enctype="multipart/form-data">
+                    <input type="file" name="file" multiple style="font-size: 1.2em;"><br><br>
+                    <input type="submit" value="Upload All" style="font-size: 1.2em; padding: 10px 20px;">
+                </form>
+            </body>
+            </html>
             """
             self.wfile.write(html.encode())
         else:
             super().do_GET()
 
 if __name__ == "__main__":
+    print("Server started at http://0.0.0.0:8000")
     http.server.test(HandlerClass=UploadHandler, port=8000)

で、最終的にこうなりました。

import http.server
import email.message

class UploadHandler(http.server.SimpleHTTPRequestHandler):
    def do_POST(self):
        content_type = self.headers.get('Content-Type')
        length = int(self.headers.get('content-length'))
        body = self.rfile.read(length)
        
        # マルチパートデータをパース
        msg = email.message_from_bytes(b'Content-Type: ' + content_type.encode() + b'\r\n\r\n' + body)
        
        count = 0
        for part in msg.walk():
            filename = part.get_filename()
            if filename:
                payload = part.get_payload(decode=True)
                with open(filename, 'wb') as f:
                    f.write(payload)
                print(f"Saved: {filename}")
                count += 1

        self.send_response(200)
        self.end_headers()
        self.wfile.write(f"{count} files uploaded successfully!".encode())

    def do_GET(self):
        if self.path == '/':
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            # 'multiple' 属性を追加し、少しスマホで押しやすくUIを調整
            html = """
            <html>
            <meta name="viewport" content="width=device-width, initial-scale=1">
            <body style="font-family: sans-serif; text-align: center; padding-top: 50px;">
                <h2>File Uploader</h2>
                <form method="POST" enctype="multipart/form-data">
                    <input type="file" name="file" multiple style="font-size: 1.2em;"><br><br>
                    <input type="submit" value="Upload All" style="font-size: 1.2em; padding: 10px 20px;">
                </form>
            </body>
            </html>
            """
            self.wfile.write(html.encode())
        else:
            super().do_GET()

if __name__ == "__main__":
    print("Server started at http://0.0.0.0:8000")
    http.server.test(HandlerClass=UploadHandler, port=8000)

aliasを作っておくと最高に「楽」になります。

# .bashrc や NixOSの shellAliases に
alias share-on='nix-shell -p python3 --run "python3 ~/bin/upload-server.py"'

これで、PCの前で「あ、写真送りたい」と思った瞬間に share-on と打つだけ。スマホからPCにファイルを送るのが、USBもクラウドも使わずに、超簡単になります。セキュリティ的には、同じネットワーク内でしかアクセスできないので、そこまで心配する必要はないでしょう。ただし、公共のWi-Fiなど不特定多数がアクセスできるネットワークではドキドキしながら使うことをおすすめします。