This commit is contained in:
eric
2026-06-07 23:57:52 -05:00
parent 6d479c9f94
commit 47b1ae8514
56 changed files with 4163 additions and 874 deletions

152
scripts/dev-tunnel.py Normal file
View File

@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""
Windows / macOS 本地开发SSH 隧道把线上 MinIO/PocketBase 映射到本机。
默认映射:
localhost:9100 -> 服务器 127.0.0.1:9100 (MinIO S3 API)
localhost:8090 -> 服务器 127.0.0.1:8090 (PocketBase可选)
用法:
python scripts/dev-tunnel.py
环境变量(可写在 .env.local
DEV_SSH_HOST=82.157.112.245
DEV_SSH_USER=ubuntu
DEV_SSH_PASSWORD=你的密码
DEV_TUNNEL_MINIO_PORT=9100
DEV_TUNNEL_POCKETBASE=0 # 设为 1 时同时映射 8090
"""
from __future__ import annotations
import os
import socket
import sys
import threading
import time
from pathlib import Path
try:
import paramiko
except ImportError:
print("请先安装 paramiko: pip install paramiko", file=sys.stderr)
raise
ROOT = Path(__file__).resolve().parent.parent
def _read_env_file(path: Path) -> dict[str, str]:
values: dict[str, str] = {}
if not path.exists():
return values
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
values[key.strip()] = value.strip().strip('"').strip("'")
return values
def load_env() -> None:
merged: dict[str, str] = {}
for path in [ROOT / ".env.local", ROOT / ".env", ROOT / "backend" / ".env"]:
merged.update(_read_env_file(path))
for key, value in merged.items():
os.environ.setdefault(key, value)
def forward_tunnel(local_port: int, remote_host: str, remote_port: int, transport) -> None:
def handle(client: socket.socket, addr) -> None:
try:
chan = transport.open_channel("direct-tcpip", (remote_host, remote_port), addr)
except Exception as exc:
print(f"[tunnel] open_channel failed {local_port}->{remote_host}:{remote_port}: {exc}")
client.close()
return
def relay(src, dst) -> None:
try:
while True:
data = src.recv(1024 * 64)
if not data:
break
dst.sendall(data)
except Exception:
pass
finally:
try:
src.close()
except Exception:
pass
try:
dst.close()
except Exception:
pass
threading.Thread(target=relay, args=(client, chan), daemon=True).start()
threading.Thread(target=relay, args=(chan, client), daemon=True).start()
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", local_port))
server.listen(32)
print(f"[tunnel] listening 127.0.0.1:{local_port} -> {remote_host}:{remote_port}")
while True:
client_sock, addr = server.accept()
threading.Thread(
target=handle,
args=(client_sock, addr),
daemon=True,
).start()
def main() -> None:
load_env()
host = os.getenv("DEV_SSH_HOST", "82.157.112.245")
user = os.getenv("DEV_SSH_USER", "ubuntu")
password = os.getenv("DEV_SSH_PASSWORD", "")
if not password:
print("缺少 DEV_SSH_PASSWORD请在 .env.local 中配置", file=sys.stderr)
sys.exit(1)
minio_local = int(os.getenv("DEV_TUNNEL_MINIO_PORT", "9100"))
pb_enabled = os.getenv("DEV_TUNNEL_POCKETBASE", "0").lower() in {"1", "true", "yes"}
pb_local = int(os.getenv("DEV_TUNNEL_POCKETBASE_PORT", "8090"))
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
print(f"[tunnel] connecting {user}@{host} ...")
client.connect(host, username=user, password=password, timeout=20)
transport = client.get_transport()
if transport is None:
print("SSH transport unavailable", file=sys.stderr)
sys.exit(1)
threading.Thread(
target=forward_tunnel,
args=(minio_local, "127.0.0.1", 9100, transport),
daemon=True,
).start()
if pb_enabled:
threading.Thread(
target=forward_tunnel,
args=(pb_local, "127.0.0.1", 8090, transport),
daemon=True,
).start()
print("[tunnel] ready. Ctrl+C to stop.")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n[tunnel] stopping")
finally:
client.close()
if __name__ == "__main__":
main()