diff --git a/cyberpanel_upgrade_monolithic.sh b/cyberpanel_upgrade_monolithic.sh index e5fbb8a26..0ef2f1526 100644 --- a/cyberpanel_upgrade_monolithic.sh +++ b/cyberpanel_upgrade_monolithic.sh @@ -1716,10 +1716,17 @@ Sync_CyberCP_To_Latest() { fi ) local sync_code=$? - # Restore production settings so panel keeps working (DB, secrets, etc.) - if [[ -f /tmp/cyberpanel_settings_backup.py ]]; then + # Merge production DATABASES into branch settings.py (preserve webmail/emailDelivery in INSTALLED_APPS) + if [[ -f /tmp/cyberpanel_settings_backup.py ]] && [[ -f /usr/local/CyberCP/upgrade_modules/merge_production_settings.py ]]; then + python3 /usr/local/CyberCP/upgrade_modules/merge_production_settings.py /tmp/cyberpanel_settings_backup.py /usr/local/CyberCP/CyberCP/settings.py 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + if [[ "${PIPESTATUS[0]}" -eq 0 ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Merged production DATABASES into branch settings.py (INSTALLED_APPS from branch preserved)" | tee -a /var/log/cyberpanel_upgrade_debug.log + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] WARNING: settings merge failed; keeping branch settings.py as-is" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + elif [[ -f /tmp/cyberpanel_settings_backup.py ]]; then cp /tmp/cyberpanel_settings_backup.py /usr/local/CyberCP/CyberCP/settings.py - echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Restored settings.py after sync" | tee -a /var/log/cyberpanel_upgrade_debug.log + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Restored settings.py after sync (merge script missing)" | tee -a /var/log/cyberpanel_upgrade_debug.log fi # LiteSpeed serves /static/ from public/static/; ensure it has latest baseTemplate static files (e.g. dashboard JS) if [[ -d /usr/local/CyberCP/public/static ]] && [[ -d /usr/local/CyberCP/baseTemplate/static/baseTemplate ]]; then diff --git a/upgrade_modules/09_sync.sh b/upgrade_modules/09_sync.sh index bd5730f7e..3d385e818 100644 --- a/upgrade_modules/09_sync.sh +++ b/upgrade_modules/09_sync.sh @@ -23,10 +23,18 @@ Sync_CyberCP_To_Latest() { fi ) local sync_code=$? - # Restore production settings so panel keeps working (DB, secrets, etc.) - if [[ -f /tmp/cyberpanel_settings_backup.py ]]; then + # Merge production DATABASES into branch settings.py so DB creds survive without stripping + # new INSTALLED_APPS (webmail, emailDelivery, etc.). Blind full restore broke integrated webmail. + if [[ -f /tmp/cyberpanel_settings_backup.py ]] && [[ -f /usr/local/CyberCP/upgrade_modules/merge_production_settings.py ]]; then + python3 /usr/local/CyberCP/upgrade_modules/merge_production_settings.py /tmp/cyberpanel_settings_backup.py /usr/local/CyberCP/CyberCP/settings.py 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + if [[ "${PIPESTATUS[0]}" -eq 0 ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Merged production DATABASES into branch settings.py (INSTALLED_APPS from branch preserved)" | tee -a /var/log/cyberpanel_upgrade_debug.log + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] WARNING: settings merge failed; keeping branch settings.py as-is" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + elif [[ -f /tmp/cyberpanel_settings_backup.py ]]; then cp /tmp/cyberpanel_settings_backup.py /usr/local/CyberCP/CyberCP/settings.py - echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Restored settings.py after sync" | tee -a /var/log/cyberpanel_upgrade_debug.log + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Restored settings.py after sync (merge script missing)" | tee -a /var/log/cyberpanel_upgrade_debug.log fi # LiteSpeed serves /static/ from public/static/; ensure it has latest baseTemplate static files (e.g. dashboard JS) if [[ -d /usr/local/CyberCP/public/static ]] && [[ -d /usr/local/CyberCP/baseTemplate/static/baseTemplate ]]; then diff --git a/upgrade_modules/merge_production_settings.py b/upgrade_modules/merge_production_settings.py new file mode 100644 index 000000000..be62e41e8 --- /dev/null +++ b/upgrade_modules/merge_production_settings.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Merge DATABASES from a backup settings.py into the branch checkout (in-place). + +Used after git sync so production DB credentials survive without replacing INSTALLED_APPS +from the branch (e.g. webmail, emailDelivery). See upgrade_modules/09_sync.sh.""" +from __future__ import annotations + +import sys + + +def _extract_assign_block(text: str, name: str) -> tuple[str | None, int | None, int | None]: + """Return (full_assignment, start, end_exclusive) for NAME = { ... } or None.""" + prefix = name + ' = ' + start = text.find(prefix) + if start == -1: + return None, None, None + brace = text.find('{', start) + if brace == -1: + return None, None, None + depth = 0 + for j in range(brace, len(text)): + if text[j] == '{': + depth += 1 + elif text[j] == '}': + depth -= 1 + if depth == 0: + end = j + 1 + # Include trailing comma/newline if present (keep style) + while end < len(text) and text[end] in ' \t': + end += 1 + if end < len(text) and text[end] == ',': + end += 1 + return text[start:end], start, end + return None, None, None + + +def merge_settings(old_path: str, branch_path: str) -> int: + try: + with open(old_path, 'r', encoding='utf-8', errors='replace') as f: + old_text = f.read() + with open(branch_path, 'r', encoding='utf-8', errors='replace') as f: + branch_text = f.read() + except OSError as e: + print('merge_production_settings: read error: %s' % e, file=sys.stderr) + return 2 + + old_db, _, _ = _extract_assign_block(old_text, 'DATABASES') + if not old_db: + print('merge_production_settings: no DATABASES in backup, leaving branch file', file=sys.stderr) + return 1 + + br_db, br_start, br_end = _extract_assign_block(branch_text, 'DATABASES') + if not br_db or br_start is None or br_end is None: + print('merge_production_settings: no DATABASES in branch settings', file=sys.stderr) + return 3 + + merged = branch_text[:br_start] + old_db + branch_text[br_end:] + try: + with open(branch_path, 'w', encoding='utf-8', newline='\n') as f: + f.write(merged) + except OSError as e: + print('merge_production_settings: write error: %s' % e, file=sys.stderr) + return 4 + + print('merge_production_settings: replaced DATABASES from backup into %s' % branch_path) + return 0 + + +if __name__ == '__main__': + if len(sys.argv) != 3: + print('Usage: merge_production_settings.py /path/to/backup_settings.py /path/to/CyberCP/settings.py', file=sys.stderr) + sys.exit(9) + sys.exit(merge_settings(sys.argv[1], sys.argv[2]))