#!/usr/bin/env python3 """Deploy popadmin pull pages static + media-pull hotpatched jar to SG.""" from __future__ import annotations import os import time from pathlib import Path import paramiko HOST = "47.236.66.9" USER = "root" PEM = Path(os.path.expandvars(r"%USERPROFILE%\.ssh\wyrs_shanghai_deploy.pem")) LOCAL_JS = Path(r"F:\0000wyrs\Aliyun\src\main\resources\static\popadmin\js\popadmin.js") LOCAL_CSS = Path(r"F:\0000wyrs\Aliyun\src\main\resources\static\popadmin\css\popadmin.css") LOCAL_JAR = Path(r"F:\0000wyrs\Aliyun\target\remote-control-1.0.0.media-pull.jar") REMOTE_STATIC = "/opt/wyrs-admin/static-override" REMOTE_JAR = "/opt/wyrs-admin/remote-control-1.0.0.jar" CACHE = "20261102-pullpages" def run(client: paramiko.SSHClient, cmd: str, timeout: int = 180) -> tuple[int, str, str]: print("$", cmd[:240]) _, stdout, stderr = client.exec_command(cmd, timeout=timeout) out = stdout.read().decode("utf-8", "replace") err = stderr.read().decode("utf-8", "replace") code = stdout.channel.recv_exit_status() if out.strip(): print(out[:2000]) if err.strip(): print("[stderr]", err[:1000]) return code, out, err def main() -> None: if not PEM.is_file(): raise SystemExit(f"missing pem {PEM}") for p in (LOCAL_JS, LOCAL_CSS, LOCAL_JAR): if not p.is_file(): raise SystemExit(f"missing {p}") client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # Auto-detect key type (RSA / Ed25519 / ECDSA) key = None for loader in ( paramiko.Ed25519Key.from_private_key_file, paramiko.RSAKey.from_private_key_file, paramiko.ECDSAKey.from_private_key_file, ): try: key = loader(str(PEM)) break except Exception: continue if key is None: raise SystemExit(f"cannot load private key {PEM}") client.connect(HOST, username=USER, pkey=key, timeout=30) sftp = client.open_sftp() ts = time.strftime("%Y%m%d%H%M%S") print("=== backup static ===") run( client, f"cp -a {REMOTE_STATIC}/popadmin.js {REMOTE_STATIC}/popadmin.js.bak.pullpages.{ts} && " f"cp -a {REMOTE_STATIC}/popadmin.css {REMOTE_STATIC}/popadmin.css.bak.pullpages.{ts} && " f"cp -a {REMOTE_STATIC}/popadmin.index.html {REMOTE_STATIC}/popadmin.index.html.bak.pullpages.{ts}", ) print("=== upload static ===") sftp.put(str(LOCAL_JS), f"{REMOTE_STATIC}/popadmin.js") # Prefer remote CSS if it has nav-fix; merge by keeping remote if larger/newer features. # Still upload local CSS then restore nav scroll rules via sed if needed. sftp.put(str(LOCAL_CSS), f"{REMOTE_STATIC}/popadmin.css") # bump cache on index idx_cmd = f"""python3 - <<'PY' from pathlib import Path p = Path('{REMOTE_STATIC}/popadmin.index.html') s = p.read_text(encoding='utf-8') import re s2 = re.sub(r'popadmin\\.js\\?v=[^\"\\']+', 'popadmin.js?v={CACHE}', s) s2 = re.sub(r'popadmin\\.css\\?v=[^\"\\']+', 'popadmin.css?v={CACHE}', s2) p.write_text(s2, encoding='utf-8') print('cache', '{CACHE}') print('js', 'popadmin.js?v={CACHE}' in s2) print('css', 'popadmin.css?v={CACHE}' in s2) PY""" run(client, idx_cmd) # Ensure sidebar scroll rule exists (from earlier navfix) run( client, f"""python3 - <<'PY' from pathlib import Path p = Path('{REMOTE_STATIC}/popadmin.css') s = p.read_text(encoding='utf-8') needle = '.sidebar nav' rule = ''' /* pullpages: keep long nav scrollable */ .sidebar {{ display:flex; flex-direction:column; min-height:0; }} .sidebar nav {{ flex:1; overflow-y:auto; overscroll-behavior:contain; }} ''' if 'overscroll-behavior:contain' not in s: p.write_text(s + '\\n' + rule, encoding='utf-8') print('css nav scroll appended') else: print('css nav scroll already present') PY""", ) print("=== markers in deployed js ===") run( client, f"python3 - <<'PY'\n" f"s=open('{REMOTE_STATIC}/popadmin.js',encoding='utf-8',errors='replace').read()\n" f"for k in ['async contacts(','contactsConsented','albumConsented','documentsConsented','用户未授权','mediaPullQId']:\n" f" print(k, s.count(k))\n" f"PY", ) print("=== deploy jar ===") run(client, f"cp -a {REMOTE_JAR} {REMOTE_JAR}.bak.pullpages.{ts}") remote_tmp = f"/tmp/remote-control-1.0.0.media-pull.{ts}.jar" print("uploading jar...", LOCAL_JAR.stat().st_size) sftp.put(str(LOCAL_JAR), remote_tmp) run(client, f"mv {remote_tmp} {REMOTE_JAR} && chmod 644 {REMOTE_JAR} && ls -lh {REMOTE_JAR}") print("=== restart wyrs-admin ===") run(client, "systemctl restart wyrs-admin", timeout=60) # wait health for i in range(30): time.sleep(2) code, out, _ = run( client, "curl -sS -m 3 -o /dev/null -w '%{http_code}' http://127.0.0.1:8090/api/admin/auth/login || true", timeout=20, ) # login endpoint should be 405/400/200 not 000 if out.strip() and out.strip() != "000": print("health http", out.strip(), "attempt", i) break else: print("WARN: health check timed out") print("=== verify APIs ===") verify = r''' python3 - <<'PY' import json, os, urllib.request env={} for line in open('/etc/wyrs-admin/app.env',encoding='utf-8',errors='replace'): line=line.strip() if not line or line.startswith('#') or '=' not in line: continue k,v=line.split('=',1); env[k.strip()]=v.strip().strip('"').strip("'") password=env.get('WYRS_ADMIN_PASSWORD') or '' body=json.dumps({'username':'admin','password':password}).encode() req=urllib.request.Request('http://127.0.0.1:8090/api/admin/auth/login',data=body,headers={'Content-Type':'application/json'},method='POST') with urllib.request.urlopen(req,timeout=15) as resp: token=json.loads(resp.read().decode())['data']['token'] for url in [ 'http://127.0.0.1:8090/api/admin/spark/media-pull?userId=1017', 'http://127.0.0.1:8090/api/admin/spark/media-pull?email=spark.test.01@wyrs.test.local', 'http://127.0.0.1:8090/api/admin/spark/contacts/summary', ]: req=urllib.request.Request(url,headers={'Authorization':'Bearer '+token}) try: with urllib.request.urlopen(req,timeout=15) as resp: raw=resp.read().decode() print('OK', url, raw[:400]) except Exception as e: body='' if hasattr(e,'read'): try: body=e.read().decode() except Exception: pass print('ERR', url, e, body[:300]) # PUT pullContacts false should work req=urllib.request.Request( 'http://127.0.0.1:8090/api/admin/spark/media-pull?email=spark.test.01@wyrs.test.local', data=json.dumps({'pullContacts':False,'count':25}).encode(), headers={'Authorization':'Bearer '+token,'Content-Type':'application/json'}, method='PUT') with urllib.request.urlopen(req,timeout=15) as resp: print('PUT', resp.read().decode()[:500]) PY''' run(client, verify, timeout=60) print("=== nginx reload (noop if unchanged) ===") run(client, "nginx -t && nginx -s reload") sftp.close() client.close() print("DEPLOY DONE cache=", CACHE) if __name__ == "__main__": main()