From b55c298ef45444c288a5d7a3b6f68f7092df6c8d Mon Sep 17 00:00:00 2001 From: zxyszx <299979470+zxyszx@users.noreply.github.com> Date: Mon, 3 Aug 2026 23:23:41 +0800 Subject: [PATCH] feat: add safe fresh reinstall option --- README.md | 5 +++-- install.sh | 38 ++++++++++++++++++++++++++++++++------ tests/install_test.sh | 21 +++++++++++++++++++++ 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ff708a9..53db4e1 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,9 @@ bash <(curl -fsSL https://raw.githubusercontent.com/zxyszx/NewSzxcn-Email/main/i ``` 空白服务器会进入防火墙、邮件域名、管理员账号和 Web 部署方式的引导。检测到 -`/opt/newszxcn-email/.env` 时,脚本会先询问是更新现有邮局、修复现有安装还是退出; -不会再直接跳过引导并重启服务。更新会先备份数据库,并在启动失败时自动回滚。 +`/opt/newszxcn-email/.env` 时,脚本会先询问是更新现有邮局、修复现有安装、退出, +还是完整备份旧目录后全新安装;不会再直接跳过引导并重启服务。更新会先备份数据库, +并在启动失败时自动回滚。 脚本会自动完成: diff --git a/install.sh b/install.sh index a4d05e7..8e31f56 100755 --- a/install.sh +++ b/install.sh @@ -146,20 +146,20 @@ prompt_value() { } prompt_choice() { - local variable="$1" prompt="$2" default_value="$3" value + local variable="$1" prompt="$2" default_value="$3" max_value="${4:-3}" value value="${!variable:-}" while true; do if [[ -z "${value}" ]] && has_tty; then read -r -p "${prompt}" value = 1 && value <= max_value )); then printf '%s' "${value}" return fi - prompt_text "[提示] 请输入 1、2 或 3。\n" + prompt_text "[提示] 请输入 1 至 ${max_value}。\n" value="" - has_tty || fail "${variable} 必须设置为 1、2 或 3。" + has_tty || fail "${variable} 必须设置为 1 至 ${max_value}。" done } @@ -170,11 +170,11 @@ prompt_existing_install_action() { if [[ -n "${public_url}" ]]; then prompt_text "[发现] 当前访问地址:${public_url}\n" fi - prompt_text '请选择操作 [1]:\n1. 更新现有邮局(推荐,自动备份并支持回滚)\n2. 修复现有安装(保留配置和数据)\n3. 退出,不做任何修改\n' + prompt_text '请选择操作 [1]:\n1. 更新现有邮局(推荐,自动备份并支持回滚)\n2. 修复现有安装(保留配置和数据)\n3. 退出,不做任何修改\n4. 备份现有安装并全新安装\n' if [[ -z "${LANQIN_EXISTING_ACTION:-}" ]] && ! has_tty; then fail "非交互环境不能选择已有安装操作;更新请执行 newszxcn-email update。" fi - action="$(prompt_choice LANQIN_EXISTING_ACTION "请选择 [1]: " "1")" + action="$(prompt_choice LANQIN_EXISTING_ACTION "请选择 [1]: " "1" "4")" printf '%s' "${action}" } @@ -612,6 +612,7 @@ do_install() { 1) do_update ;; 2) do_repair_install ;; 3) success "已退出,现有邮局未作修改。" ;; + 4) do_backup_reinstall ;; esac return fi @@ -723,6 +724,31 @@ do_uninstall() { success "容器和自动生成的 Nginx 配置已移除,${INSTALL_DIR} 中的邮件、证书、配置和数据库仍然保留。" } +do_backup_reinstall() { + local backup_dir + backup_dir="${INSTALL_DIR}.backup-$(date -u +%Y%m%dT%H%M%SZ)" + + if [[ -f "${INSTALL_DIR}/docker-compose.yml" ]] && command -v docker >/dev/null 2>&1; then + ensure_docker + compose down --remove-orphans + fi + if [[ -f "${NGINX_CONFIG}" ]]; then + rm -f "${NGINX_CONFIG}" + if command -v nginx >/dev/null 2>&1 && nginx -t >/dev/null 2>&1; then + if command -v systemctl >/dev/null 2>&1; then + systemctl reload nginx + else + nginx -s reload + fi + fi + fi + + mv "${INSTALL_DIR}" "${backup_dir}" + success "旧安装已完整备份到 ${backup_dir}。" + log "现在开始全新安装。" + do_install +} + if [[ "${LANQIN_SOURCE_ONLY:-false}" == "true" ]]; then if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then return 0 diff --git a/tests/install_test.sh b/tests/install_test.sh index 3a4342d..1b5821b 100644 --- a/tests/install_test.sh +++ b/tests/install_test.sh @@ -122,9 +122,29 @@ test_existing_install_action() { assert_eq "2" "$(prompt_existing_install_action)" "existing install repair action" export LANQIN_EXISTING_ACTION=3 assert_eq "3" "$(prompt_existing_install_action)" "existing install exit action" + export LANQIN_EXISTING_ACTION=4 + assert_eq "4" "$(prompt_existing_install_action)" "existing install fresh action" unset LANQIN_EXISTING_ACTION } +test_backup_reinstall_preserves_existing_directory() ( + local temp_dir backup_dir + temp_dir="$(mktemp -d)" + INSTALL_DIR="${temp_dir}/newszxcn-email" + NGINX_CONFIG="${temp_dir}/newszxcn-email.conf" + mkdir -p "${INSTALL_DIR}" + printf 'existing-data\n' > "${INSTALL_DIR}/marker" + + do_install() { + [[ ! -e "${INSTALL_DIR}" ]] || fail_test "fresh install started before old directory was moved" + } + + do_backup_reinstall + backup_dir="$(find "${temp_dir}" -maxdepth 1 -type d -name 'newszxcn-email.backup-*' -print -quit)" + [[ -n "${backup_dir}" ]] || fail_test "existing install backup directory missing" + grep -Fq 'existing-data' "${backup_dir}/marker" || fail_test "existing install data was not preserved" +) + test_hostname_validation test_password_validation test_install_configuration 1 1 "127.0.0.1:8088" "https://mail.example.com" "false" @@ -134,5 +154,6 @@ test_nginx_configuration test_compose_configuration test_legacy_configuration_is_preserved test_existing_install_action +test_backup_reinstall_preserves_existing_directory printf 'install.sh tests passed\n'