Files
kefu/im/deploy/baota_publish_h5.py
T
2026-09-03 08:38:17 +08:00

132 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""Publish a versioned uni-app H5 build through the existing BaoTa gateway."""
import argparse
import hashlib
import os
import pwd
import shutil
import subprocess
import sys
import time
import urllib.request
import zipfile
from pathlib import Path
parser = argparse.ArgumentParser()
parser.add_argument('archive')
parser.add_argument('sha256')
args = parser.parse_args()
archive = Path(args.archive).resolve()
assert archive.is_file() and archive.parent == Path('/tmp')
assert hashlib.sha256(archive.read_bytes()).hexdigest() == args.sha256.lower()
assert os.geteuid() == 0
root = Path('/www/wwwroot/im.bchongw.com')
go_config = Path('/www/server/panel/vhost/nginx/go_xingyu_im.conf')
main_config = go_config if go_config.is_file() else Path('/www/server/panel/vhost/nginx/html_im.bchongw.com.conf')
config = Path('/www/server/panel/vhost/nginx/extension/xingyu_im/xingyu_routes.conf') if go_config.is_file() else main_config
old_config = config.read_text()
assert 'server_name im.bchongw.com;' in main_config.read_text()
release = Path('/www/wwwroot/xingyu-h5/releases') / time.strftime('%Y%m%d-%H%M%S')
release.mkdir(mode=0o755, parents=True, exist_ok=False)
with zipfile.ZipFile(str(archive)) as package:
for info in package.infolist():
candidate = (release / info.filename.replace('\\', '/')).resolve()
assert os.path.commonpath([str(candidate), str(release)]) == str(release)
# Windows PowerShell archives may store backslash separators. Normalize
# them explicitly instead of creating literal backslashes on Linux.
if info.is_dir() or info.filename.endswith(('\\', '/')):
candidate.mkdir(parents=True, exist_ok=True)
else:
candidate.parent.mkdir(parents=True, exist_ok=True)
with package.open(info) as source, candidate.open('wb') as target:
shutil.copyfileobj(source, target)
www = pwd.getpwnam('www')
for parent, directories, files in os.walk(str(release)):
os.chown(parent, www.pw_uid, www.pw_gid)
os.chmod(parent, 0o755)
for filename in files:
path = os.path.join(parent, filename)
os.chown(path, www.pw_uid, www.pw_gid)
os.chmod(path, 0o644)
assert '/app/assets/' in (release / 'index.html').read_text()
assert (release / 'static/favicon.svg').is_file()
entry = root / 'app'
assert not entry.exists() or entry.is_symlink()
previous = os.readlink(str(entry)) if entry.is_symlink() else None
backup = Path('/www/backup/xingyu-h5-publish-' + time.strftime('%Y%m%d-%H%M%S'))
backup.mkdir(mode=0o700)
shutil.copy2(str(config), str(backup / config.name))
block = '''
# XINGYU_H5_START
location = /app { return 301 /app/; }
location = /app/ {
root /www/wwwroot/im.bchongw.com;
add_header Cache-Control "no-store, no-cache, must-revalidate";
try_files /app/index.html =404;
}
location = /app/index.html {
root /www/wwwroot/im.bchongw.com;
add_header Cache-Control "no-store, no-cache, must-revalidate";
try_files $uri =404;
}
location ^~ /app/assets/ {
root /www/wwwroot/im.bchongw.com;
expires 7d;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
location ^~ /app/static/ {
root /www/wwwroot/im.bchongw.com;
expires 1h;
try_files $uri =404;
}
location ^~ /app/ {
root /www/wwwroot/im.bchongw.com;
try_files $uri $uri/ /app/index.html;
}
# XINGYU_H5_END
'''
def switch(target):
next_link = root / 'app.next'
assert not next_link.exists() and not next_link.is_symlink()
os.symlink(target, str(next_link))
os.replace(str(next_link), str(entry))
try:
switch(str(release))
if '# XINGYU_H5_START' not in old_config:
marker = ' location = /admin {'
assert old_config.count(marker) == 1
config.write_text(old_config.replace(marker, block + marker))
subprocess.run(['/www/server/nginx/sbin/nginx', '-t'], check=True)
subprocess.run(['/etc/init.d/nginx', 'reload'], check=True)
assert entry.resolve() == release.resolve()
assert (release / 'index.html').is_file() and (release / 'static/favicon.svg').is_file()
assert (root / 'admin').resolve().joinpath('index.html').is_file()
with urllib.request.urlopen('http://127.0.0.1:18888/healthz', timeout=10) as response:
assert response.status == 200
os.chdir('/www/server/panel')
sys.path.insert(0, '/www/server/panel')
sys.path.insert(0, '/www/server/panel/class')
import public
project_name = 'xingyu_im' if go_config.is_file() else 'im.bchongw.com'
public.M('sites').where('name=?', (project_name,)).setField(
'ps', '星遇 IM · im.bchongw.com · 管理端 /admin/ · H5 /app/')
print('H5_PUBLISHED=' + str(release))
print('NGINX_BACKUP=' + str(backup))
except Exception:
config.write_text(old_config)
if previous:
switch(previous)
else:
# Remove only the symlink just created; retain all release files for diagnosis.
assert entry.is_symlink() and os.readlink(str(entry)) == str(release)
entry.unlink()
subprocess.run(['/www/server/nginx/sbin/nginx', '-t'], check=True)
subprocess.run(['/etc/init.d/nginx', 'reload'], check=True)
raise