#!/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