publish_via_obs.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. #!/usr/bin/env python3
  2. """publish_via_obs.py — publish.py 的跳板机备用通道。
  3. 适用场景: /api/storage/credentials 与 /api/storage/upload 均 404(未部署)、
  4. 容器无 CLOUD_SDK_AK/SK、无 obsutil 时 —— 借道 server.fmode.cn(云服务器)上的
  5. obsutil(自带 nkkj 主账套 AK/SK)上传到 storage-s3-nkkj 桶。
  6. 用法: python3 publish_via_obs.py --workdir /tmp/lapian [--date 20260829]
  7. 产物清单与 publish.py 完全一致, 上传后逐个 HEAD 校验, 写 workdir/publish.json。
  8. """
  9. import argparse, datetime, json, os, sys, tarfile, time, urllib.request
  10. import paramiko
  11. CLOUD = {"host": "139.159.253.131", "user": "root", "password": "bofang666."}
  12. OBSUTIL = "/opt/obsutil/obsutil_linux_amd64_5.4.11/obsutil"
  13. BUCKET = "storage-s3-nkkj"
  14. ENDPOINT = "obs.cn-north-4.myhuaweicloud.com"
  15. # 对外链接 host 规范(2026-08-30 硬性): 必须用 s3.fmode.cn, 禁用 OBS 原生域名
  16. # (浏览器只有经 s3.fmode.cn 反代才能正常加载报告的相对路径资源)
  17. PUB = "https://s3.fmode.cn"
  18. CT = {".html": "text/html; charset=utf-8", ".json": "application/json",
  19. ".mp4": "video/mp4", ".wav": "audio/wav", ".jpg": "image/jpeg"}
  20. def main():
  21. ap = argparse.ArgumentParser()
  22. ap.add_argument("--workdir", required=True)
  23. ap.add_argument("--date", default=datetime.date.today().strftime("%Y%m%d"))
  24. ap.add_argument("--sub", default="", help="视频子目录(默认取meta.json的aweme_id, 防同日多视频覆盖)")
  25. args = ap.parse_args()
  26. wd = args.workdir
  27. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  28. from common import get_userid
  29. userid = get_userid()
  30. sub = args.sub
  31. if not sub:
  32. try:
  33. sub = (json.load(open(f"{wd}/meta.json")) or {}).get("aweme_id") or "video"
  34. except Exception:
  35. sub = "video"
  36. base = f"user/{userid}/report/lapian/douyin/{args.date}/{sub}"
  37. # 打包清单(与 publish.py 一致)
  38. files = ["report.html", "video.mp4", "audio.wav", "transcript.json"]
  39. files = [f for f in files if os.path.exists(f"{wd}/{f}")]
  40. files += [f"frames/{f}" for f in sorted(os.listdir(f"{wd}/frames"))] if os.path.isdir(f"{wd}/frames") else []
  41. pkg = "/tmp/lapian-pkg.tar.gz"
  42. asset_map = []
  43. for n in ("analysis.json", "comments.json", "meta.json", "llm_out.json"):
  44. if os.path.exists(f"{wd}/{n}"):
  45. asset_map.append((f"{wd}/{n}", f"assets/{n}"))
  46. with tarfile.open(pkg, "w:gz") as tf:
  47. for rel in files:
  48. tf.add(f"{wd}/{rel}", arcname=rel)
  49. for local, arc in asset_map:
  50. tf.add(local, arcname=arc)
  51. files += [arc for _, arc in asset_map]
  52. print(f"[pkg] {len(files)} files, {os.path.getsize(pkg)/1048576:.1f} MB")
  53. # SFTP(带重试, banner 抖动常见)
  54. ssh = paramiko.SSHClient()
  55. ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
  56. for attempt in range(4):
  57. try:
  58. ssh.connect(CLOUD["host"], 22, CLOUD["user"], CLOUD["password"],
  59. timeout=30, banner_timeout=30)
  60. break
  61. except Exception as ex:
  62. print(f"[ssh] attempt {attempt+1} failed: {type(ex).__name__}, retry 15s")
  63. time.sleep(15)
  64. else:
  65. sys.exit("[ssh] all retries failed")
  66. sftp = ssh.open_sftp()
  67. sftp.put(pkg, "/tmp/lapian-pkg.tar.gz")
  68. sftp.close()
  69. remote = f"""
  70. set -e
  71. rm -rf /tmp/lapian-pkg && mkdir -p /tmp/lapian-pkg
  72. tar -xzf /tmp/lapian-pkg.tar.gz -C /tmp/lapian-pkg
  73. cd /tmp/lapian-pkg
  74. for f in {' '.join(files)}; do
  75. if {OBSUTIL} cp "$f" "obs://{BUCKET}/{base}/$f" -acl=public-read -e={ENDPOINT} -j=5 > /tmp/obs_last.log 2>&1; then
  76. echo "UP OK $f"
  77. else
  78. echo "UP FAIL $f: $(tail -2 /tmp/obs_last.log)"
  79. fi
  80. done"""
  81. _, stdout, stderr = ssh.exec_command(remote, timeout=600)
  82. out = stdout.read().decode(errors="replace")
  83. print(out)
  84. ssh.close()
  85. fails = [l for l in out.splitlines() if "UP FAIL" in l]
  86. # 公网校验
  87. bad = []
  88. for rel in files:
  89. try:
  90. req = urllib.request.Request(f"{PUB}/{base}/{rel}", method="HEAD")
  91. with urllib.request.urlopen(req, timeout=25) as r:
  92. if r.status != 200:
  93. bad.append((rel, r.status))
  94. except Exception as ex:
  95. bad.append((rel, str(ex)[:60]))
  96. report_url = f"{PUB}/{base}/report.html"
  97. print(f"\n[verify] {len(files)-len(bad)}/{len(files)} OK", f"fails={bad}" if bad else "ALL PASS")
  98. json.dump({"report_url": report_url, "prefix": base, "fails": bad + fails},
  99. open(f"{wd}/publish.json", "w"), ensure_ascii=False, indent=2)
  100. print("REPORT URL:", report_url)
  101. if __name__ == "__main__":
  102. main()