#!/usr/bin/env python3 """Serve public voice assets with MP3 MIME/range support; keep app services running. Run on the existing Ubuntu host using sudo. Only manifest-listed public files are copied. Nginx config is backed up and rolled back if validation/reload fails. """ import json from pathlib import Path import re import shutil import subprocess SOURCE = Path('/home/ubuntu/juzi/h5/assets/voice') PUBLIC = Path('/var/www/juzi-voice-20261102') CONFIG = Path('/etc/nginx/sites-available/juzi') BACKUP = CONFIG.with_name('juzi.voice-20261102.bak') MARKER = '# BEGIN juzi-voice-media 20261102' BLOCK = ''' # BEGIN juzi-voice-media 20261102 location ^~ /state/assets/voice/ { alias /var/www/juzi-voice-20261102/; types { audio/mpeg mp3; application/json json; } default_type application/octet-stream; add_header Cache-Control "public, max-age=3600"; limit_except GET HEAD { deny all; } } # END juzi-voice-media 20261102 ''' def checked(command): result = subprocess.run(command, capture_output=True, text=True) if result.returncode: raise RuntimeError('command failed: ' + ' '.join(command) + '\n' + result.stderr) def main(): manifest = json.loads((SOURCE / 'manifest.json').read_text()) names = manifest['files'] assert names and all(isinstance(name, str) and re.fullmatch(r'[a-z0-9-]+', name) for name in names) files = [SOURCE / (name + '.mp3') for name in names] + [SOURCE / 'manifest.json'] assert all(file.is_file() and not file.is_symlink() and file.resolve().parent == SOURCE.resolve() for file in files) original = CONFIG.read_text() if MARKER not in original: assert not BACKUP.exists(), 'existing backup must be preserved' assert original.count(' location /state/ {') == 1 PUBLIC.mkdir(mode=0o755, parents=True, exist_ok=True) assert PUBLIC.resolve() == Path('/var/www/juzi-voice-20261102') for file in files: target = PUBLIC / file.name assert not target.is_symlink() shutil.copyfile(file, target) target.chmod(0o644) if MARKER not in original: shutil.copy2(CONFIG, BACKUP) CONFIG.write_text(original.replace(' location /state/ {', BLOCK + ' location /state/ {', 1)) try: checked(['nginx', '-t']) checked(['systemctl', 'reload', 'nginx']) except Exception: shutil.copy2(BACKUP, CONFIG) checked(['nginx', '-t']) checked(['systemctl', 'reload', 'nginx']) raise else: checked(['nginx', '-t']) print(json.dumps({'publicFiles': len(files), 'nginxConfigValid': True, 'backup': str(BACKUP), 'businessServicesRestarted': False})) if __name__ == '__main__': main()