154 lines
4.6 KiB
Python
154 lines
4.6 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
从宝塔正式机拉取线上运行目录到本地镜像(与 soul-api/master.py、soul-admin/master.py 同源 SSH 配置)。
|
|||
|
|
|
|||
|
|
说明:
|
|||
|
|
- 服务器上一般是「二进制 + .env + 日志」与「静态 dist」,不包含完整 Go/React 源码。
|
|||
|
|
- 默认解压到仓库根目录 _server_live/soul-api、_server_live/soul-admin,不覆盖本地工程源码。
|
|||
|
|
|
|||
|
|
环境变量与 master.py 一致:DEPLOY_HOST、DEPLOY_USER、DEPLOY_PASSWORD、DEPLOY_SSH_KEY、
|
|||
|
|
DEPLOY_PROJECT_PATH、DEPLOY_BASE_PATH。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import print_function
|
|||
|
|
|
|||
|
|
import argparse
|
|||
|
|
import importlib.util
|
|||
|
|
import os
|
|||
|
|
import shutil
|
|||
|
|
import sys
|
|||
|
|
import tarfile
|
|||
|
|
import tempfile
|
|||
|
|
import threading
|
|||
|
|
|
|||
|
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|||
|
|
SOUL_API_DIR = os.path.join(ROOT, "soul-api")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _load_api_master():
|
|||
|
|
path = os.path.join(SOUL_API_DIR, "master.py")
|
|||
|
|
spec = importlib.util.spec_from_file_location("soul_api_deploy_master", path)
|
|||
|
|
mod = importlib.util.module_from_spec(spec)
|
|||
|
|
spec.loader.exec_module(mod)
|
|||
|
|
return mod
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _pull_dir_tar(client, remote_dir, local_dir, mod, timeout=600):
|
|||
|
|
"""远端 tar czf 流式下载并解压到 local_dir。"""
|
|||
|
|
import shlex
|
|||
|
|
|
|||
|
|
remote_q = shlex.quote(remote_dir)
|
|||
|
|
cmd = "tar czf - -C %s . 2>/dev/null" % remote_q
|
|||
|
|
stdin, stdout, stderr = client.exec_command(cmd, timeout=timeout)
|
|||
|
|
|
|||
|
|
err_holder = []
|
|||
|
|
|
|||
|
|
def _drain():
|
|||
|
|
try:
|
|||
|
|
err_holder.append(stderr.read())
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
t = threading.Thread(target=_drain)
|
|||
|
|
t.daemon = True
|
|||
|
|
t.start()
|
|||
|
|
|
|||
|
|
fd, tmp_path = tempfile.mkstemp(suffix=".tar.gz")
|
|||
|
|
os.close(fd)
|
|||
|
|
try:
|
|||
|
|
with open(tmp_path, "wb") as out:
|
|||
|
|
while True:
|
|||
|
|
chunk = stdout.read(256 * 1024)
|
|||
|
|
if not chunk:
|
|||
|
|
break
|
|||
|
|
out.write(chunk)
|
|||
|
|
t.join(timeout=5)
|
|||
|
|
exit_status = stdout.channel.recv_exit_status()
|
|||
|
|
if exit_status != 0:
|
|||
|
|
print(" [警告] 远端 tar 退出码: %s" % exit_status)
|
|||
|
|
if os.path.isdir(local_dir):
|
|||
|
|
shutil.rmtree(local_dir)
|
|||
|
|
os.makedirs(local_dir, exist_ok=True)
|
|||
|
|
with tarfile.open(tmp_path, "r:gz") as tf:
|
|||
|
|
tf.extractall(local_dir)
|
|||
|
|
print(" [成功] 已同步到: %s" % local_dir)
|
|||
|
|
return True
|
|||
|
|
finally:
|
|||
|
|
try:
|
|||
|
|
os.remove(tmp_path)
|
|||
|
|
except OSError:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
parser = argparse.ArgumentParser(description="从宝塔拉取 soul-api / soul-admin 线上目录")
|
|||
|
|
parser.add_argument("--api-only", action="store_true", help="仅拉 soul-api")
|
|||
|
|
parser.add_argument("--admin-only", action="store_true", help="仅拉 soul-admin")
|
|||
|
|
args = parser.parse_args()
|
|||
|
|
|
|||
|
|
mod = _load_api_master()
|
|||
|
|
cfg = mod.get_cfg()
|
|||
|
|
if not cfg.get("password") and not cfg.get("ssh_key"):
|
|||
|
|
print("[失败] 请设置 DEPLOY_PASSWORD 或 DEPLOY_SSH_KEY")
|
|||
|
|
return 1
|
|||
|
|
|
|||
|
|
pull_api = not args.admin_only
|
|||
|
|
pull_admin = not args.api_only
|
|||
|
|
if args.api_only and args.admin_only:
|
|||
|
|
print("[失败] 不能同时指定 --api-only 与 --admin-only")
|
|||
|
|
return 1
|
|||
|
|
|
|||
|
|
client = None
|
|||
|
|
try:
|
|||
|
|
client = mod._connect_ssh(cfg)
|
|||
|
|
live_root = os.path.join(ROOT, "_server_live")
|
|||
|
|
os.makedirs(live_root, exist_ok=True)
|
|||
|
|
|
|||
|
|
print("=" * 60)
|
|||
|
|
print(" 从宝塔拉取线上目录 → %s" % live_root)
|
|||
|
|
print(" 主机: %s@%s:%s" % (cfg["user"], cfg["host"], mod.DEFAULT_SSH_PORT))
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
if pull_api:
|
|||
|
|
print("[1] soul-api: %s" % cfg["project_path"])
|
|||
|
|
_pull_dir_tar(
|
|||
|
|
client,
|
|||
|
|
cfg["project_path"],
|
|||
|
|
os.path.join(live_root, "soul-api"),
|
|||
|
|
mod,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if pull_admin:
|
|||
|
|
admin_base = os.environ.get("DEPLOY_BASE_PATH", "/www/wwwroot/self/soul-admin").rstrip("/")
|
|||
|
|
print("[2] soul-admin: %s" % admin_base)
|
|||
|
|
# 复用同一连接;若仅拉 admin,上面未开新连接也行
|
|||
|
|
if not pull_api:
|
|||
|
|
pass
|
|||
|
|
_pull_dir_tar(
|
|||
|
|
client,
|
|||
|
|
admin_base,
|
|||
|
|
os.path.join(live_root, "soul-admin"),
|
|||
|
|
mod,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
print("")
|
|||
|
|
print(" 完成。镜像根目录: %s" % live_root)
|
|||
|
|
return 0
|
|||
|
|
except Exception as e:
|
|||
|
|
print("[失败] %s" % e)
|
|||
|
|
import traceback
|
|||
|
|
|
|||
|
|
traceback.print_exc()
|
|||
|
|
return 1
|
|||
|
|
finally:
|
|||
|
|
if client:
|
|||
|
|
try:
|
|||
|
|
client.close()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
sys.exit(main() or 0)
|