diff --git a/backend/cmd/smail-admin/systemd_policy_test.go b/backend/cmd/smail-admin/systemd_policy_test.go index 73e02004..66382f1b 100644 --- a/backend/cmd/smail-admin/systemd_policy_test.go +++ b/backend/cmd/smail-admin/systemd_policy_test.go @@ -88,7 +88,7 @@ func TestInstallerManagesProtectedEnvironmentFile(t *testing.T) { "if [ ! -e /etc/smail/smail.env ]; then", "'SMAIL_ROOT=/opt/smail'", "chmod 0600 /etc/smail/smail.env", - "cp -p -- /etc/smail/smail.env \"$backup_dir/smail.env\"", + "每次部署都重新创建服务环境文件,不继承旧的凭据或代理配置。", } { if !strings.Contains(content, required) { t.Fatalf("installer is missing protected environment-file behavior %q", required) @@ -104,9 +104,10 @@ func TestInstallerRequiresExplicitFreshReset(t *testing.T) { } content := string(installer) for _, required := range []string{ - "--fresh", + "fresh=true", "确认全新部署 Smail", - "cp -a -- \"$runtime_dir/.\" \"$backup_dir/runtime/\"", + "for legacy_backup in /var/backups/smail-deploy-*", + "rm -rf -- \"$legacy_backup\"", "find \"$runtime_dir\" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +", "systemctl disable smail smail-admin", } { @@ -114,6 +115,17 @@ func TestInstallerRequiresExplicitFreshReset(t *testing.T) { t.Fatalf("installer is missing explicit fresh-reset behavior %q", required) } } + forbidden := []string{ + "默认模式保留配置、数据库、证书、日志和插件", + "cp -a -- \"$runtime_dir/.\" \"$backup_dir/runtime/\"", + "cp -p -- /etc/smail/smail.env \"$backup_dir/smail.env\"", + "配置、数据库和证书未覆盖", + } + for _, value := range forbidden { + if strings.Contains(content, value) { + t.Fatalf("installer retains forbidden legacy-data behavior %q", value) + } + } if strings.Contains(content, "全新部署已停止:$target 非空") { t.Fatal("installer must not reject existing data without an explicit fresh-reset request") } diff --git a/backend/cmd/smail-admin/windows_deployment_policy_test.go b/backend/cmd/smail-admin/windows_deployment_policy_test.go index e087909f..dcb8aa52 100644 --- a/backend/cmd/smail-admin/windows_deployment_policy_test.go +++ b/backend/cmd/smail-admin/windows_deployment_policy_test.go @@ -22,9 +22,6 @@ func TestWindowsDeploymentScriptsKeepAdminChannelManual(t *testing.T) { "$PackageRoot = $null", "$PackageRoot = Split-Path -Parent $PSScriptRoot", "NT SERVICE\\Smail:(F)", - ".backup-", - "$mainBackup", - "$adminBackup", "不会自动注册或启动服务", } { if !strings.Contains(content, required) { @@ -61,13 +58,23 @@ func TestWindowsInstallerRequiresExplicitFreshReset(t *testing.T) { "[switch]$Fresh", "确认全新部署 Smail", ".smail-fresh-backup-", - "Copy-Item -LiteralPath $item.FullName -Destination (Join-Path $backupRoot $item.Name) -Recurse -Force", + "不会保留旧配置、数据库、证书、日志、插件或绑定信息。", "Remove-Item -LiteralPath $item.FullName -Recurse -Force", } { if !strings.Contains(content, required) { t.Fatalf("Windows installer is missing explicit fresh-reset behavior %q", required) } } + forbidden := []string{ + "Copy-Item -LiteralPath $item.FullName -Destination (Join-Path $backupRoot $item.Name) -Recurse -Force", + "$backupRoot = Join-Path", + "正常升级不会覆盖配置、证书或数据库", + } + for _, value := range forbidden { + if strings.Contains(content, value) { + t.Fatalf("Windows installer retains forbidden legacy-data behavior %q", value) + } + } if strings.Contains(content, "全新部署已停止:$existing 不是空目录") { t.Fatal("Windows installer must not reject existing data without an explicit fresh-reset request") } diff --git a/backend/services/setup/db.go b/backend/services/setup/db.go index 93cce1d1..4efac5c3 100644 --- a/backend/services/setup/db.go +++ b/backend/services/setup/db.go @@ -570,12 +570,32 @@ func validMySQLIdentifier(value string) bool { return true } +type sqlSetupDatabase struct { + *sql.DB +} + +func (database *sqlSetupDatabase) TableCountContext(ctx stdcontext.Context, query string) (int, error) { + var count int + if err := database.QueryRowContext(ctx, query).Scan(&count); err != nil { + return 0, err + } + return count, nil +} + var openMySQLSetupDatabase = func(dsn string) (setupDatabase, error) { - return sql.Open("mysql", dsn) + database, err := sql.Open("mysql", dsn) + if err != nil { + return nil, err + } + return &sqlSetupDatabase{DB: database}, nil } var openPostgresSetupDatabase = func(dsn string) (setupDatabase, error) { - return sql.Open("postgres", dsn) + database, err := sql.Open("postgres", dsn) + if err != nil { + return nil, err + } + return &sqlSetupDatabase{DB: database}, nil } func ensurePostgresDatabase(dsn string) error { @@ -596,7 +616,7 @@ func ensurePostgresDatabase(dsn string) error { } defer func() { _ = target.Close() }() if err = target.PingContext(requestContext); err == nil { - return nil + return requireEmptyExternalDatabase(target, requestContext, "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog', 'information_schema')") } else { var postgresErr *pq.Error if !stderrors.As(err, &postgresErr) || string(postgresErr.Code) != "3D000" { @@ -631,9 +651,21 @@ func ensurePostgresDatabase(dsn string) error { type setupDatabase interface { PingContext(stdcontext.Context) error ExecContext(stdcontext.Context, string, ...any) (sql.Result, error) + TableCountContext(stdcontext.Context, string) (int, error) Close() error } +func requireEmptyExternalDatabase(database setupDatabase, ctx stdcontext.Context, query string) error { + count, err := database.TableCountContext(ctx, query) + if err != nil { + return databaseError("检查外部数据库是否为空失败", err) + } + if count > 0 { + return databaseError("全新部署要求外部数据库为空,请使用新数据库或先清理 Smail 专属数据库", nil) + } + return nil +} + func ensureMySQLDatabase(dsn string) error { parsed, err := mysql.ParseDSN(dsn) if err != nil { @@ -652,7 +684,7 @@ func ensureMySQLDatabase(dsn string) error { } defer func() { _ = target.Close() }() if err = target.PingContext(requestContext); err == nil { - return nil + return requireEmptyExternalDatabase(target, requestContext, "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE()") } else { var mysqlErr *mysql.MySQLError if !errors.As(err, &mysqlErr) || mysqlErr.Number != 1049 { diff --git a/backend/services/setup/db_database_test.go b/backend/services/setup/db_database_test.go index 766da1ca..65bbdda5 100644 --- a/backend/services/setup/db_database_test.go +++ b/backend/services/setup/db_database_test.go @@ -43,11 +43,14 @@ func TestMarkSetupInProgressPreservesExplicitSetupState(t *testing.T) { } type fakeSetupDB struct { - pingErrors []error - pingCalls int - execQueries []string - execError error - closed bool + pingErrors []error + pingCalls int + execQueries []string + execError error + tableCount int + tableCountErr error + tableQueries []string + closed bool } func (database *fakeSetupDB) PingContext(context.Context) error { @@ -64,6 +67,14 @@ func (database *fakeSetupDB) ExecContext(_ context.Context, query string, _ ...a return fakeSetupResult(0), database.execError } +func (database *fakeSetupDB) TableCountContext(_ context.Context, query string) (int, error) { + if database.tableCountErr != nil { + return 0, database.tableCountErr + } + database.tableQueries = append(database.tableQueries, query) + return database.tableCount, nil +} + func (database *fakeSetupDB) Close() error { database.closed = true return nil @@ -282,6 +293,32 @@ func TestEnsureMySQLDatabaseSkipsCreationWhenDatabaseExists(t *testing.T) { } } +func TestEnsureMySQLDatabaseRejectsExistingNonEmptyDatabase(t *testing.T) { + target := &fakeSetupDB{tableCount: 1} + withMySQLSetupOpener(t, func(string) (setupDatabase, error) { + return target, nil + }) + + if err := ensureMySQLDatabase(testMySQLDSN(t, "secret")); err == nil { + t.Fatal("non-empty MySQL database was accepted for a fresh deployment") + } else if message := DatabaseSetupErrorMessage(err); message == "" { + t.Fatal("fresh deployment error message was empty") + } +} + +func TestEnsurePostgresDatabaseRejectsExistingNonEmptyDatabase(t *testing.T) { + target := &fakeSetupDB{tableCount: 1} + withPostgresSetupOpener(t, func(string) (setupDatabase, error) { + return target, nil + }) + + if err := ensurePostgresDatabase("postgres://smail:secret@127.0.0.1:5432/smail?sslmode=disable"); err == nil { + t.Fatal("non-empty PostgreSQL database was accepted for a fresh deployment") + } else if message := DatabaseSetupErrorMessage(err); message == "" { + t.Fatal("fresh deployment error message was empty") + } +} + func TestEnsureMySQLDatabaseCreatesMissingDatabase(t *testing.T) { target := &fakeSetupDB{pingErrors: []error{&mysql.MySQLError{Number: 1049}, nil}} admin := &fakeSetupDB{} diff --git a/deploy/deploy-runtime.ps1 b/deploy/deploy-runtime.ps1 index 0ad50c47..4dc20d61 100644 --- a/deploy/deploy-runtime.ps1 +++ b/deploy/deploy-runtime.ps1 @@ -1,6 +1,9 @@ -param() +param() $ErrorActionPreference = 'Stop' +$freshConfirmation = '确认全新部署 Smail' +$confirmation = Read-Host "这是不可逆的 Smail 专属数据清理操作。请输入“$freshConfirmation”继续" +if ($confirmation -cne $freshConfirmation) { throw '未获得有效确认,已取消全新部署' } $root = Split-Path -Parent $PSScriptRoot $runtime = Join-Path $root 'yunx-test' $boundary = [IO.Path]::GetFullPath($root).TrimEnd('\') + '\' @@ -25,22 +28,19 @@ try { if ($LASTEXITCODE -ne 0) { throw '编译产物校验失败,未部署' } New-Item -ItemType Directory -Path $runtime -Force | Out-Null +foreach ($item in Get-ChildItem -LiteralPath $runtime -Force -ErrorAction SilentlyContinue) { + if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw '运行目录顶层存在重解析点,拒绝清理' } + Remove-Item -LiteralPath $item.FullName -Recurse -Force +} $receipt = Get-Content -LiteralPath (Join-Path $root 'out\build-state.json') -Encoding UTF8 -Raw | ConvertFrom-Json -$backup = Join-Path $root ('out\deployment-history\' + (Get-Date -Format 'yyyyMMdd-HHmmss-fff')) foreach ($item in $receipt.files) { if (-not $item.target) { continue } $target = Join-Path $runtime $item.target - if (Test-Path -LiteralPath $target) { - $info = Get-Item -LiteralPath $target -Force - if ($info.Attributes -band [IO.FileAttributes]::ReparsePoint -or $info.PSIsContainer) { throw '部署程序不能是链接或目录' } - New-Item -ItemType Directory -Path $backup -Force | Out-Null - Copy-Item -LiteralPath $target -Destination (Join-Path $backup $item.target) -ErrorAction Stop - } Copy-Item -LiteralPath (Join-Path $root $item.source) -Destination $target -ErrorAction Stop } & node (Join-Path $PSScriptRoot 'build-state.mjs') verify --deployed -if ($LASTEXITCODE -ne 0) { throw '部署校验失败,禁止启动;旧程序备份保留在 out/deployment-history' } -Write-Output '最新程序已部署到 yunx-test;正常升级不会覆盖配置、证书或数据库。需要全新部署时,请使用平台安装脚本的 --fresh 或 -Fresh 参数并输入确认词;外部数据库不会被安装脚本自动删除。' +if ($LASTEXITCODE -ne 0) { throw '部署校验失败,禁止启动;本次全新部署未保留旧程序' } +Write-Output '最新程序已完成全新部署到 yunx-test;配置、数据库、证书、日志、插件和绑定信息均未继承,请重新完成初始化配置。' } finally { if ($lock) { $lock.Dispose() } } diff --git a/deploy/install-systemd.sh b/deploy/install-systemd.sh index 86c276f7..d3679d73 100644 --- a/deploy/install-systemd.sh +++ b/deploy/install-systemd.sh @@ -2,16 +2,16 @@ set -eu usage() { - echo "用法:$0 [--fresh] [发布目录]" - echo "默认模式保留配置、数据库、证书、日志和插件;--fresh 模式必须输入确认词后才会备份并清理 Smail 专属运行数据。" + echo "用法:$0 [发布目录]" + echo "每次部署都是全新部署,必须输入确认词后才会清理 Smail 专属运行数据。" } -fresh=false +fresh=true root_argument="" while [ "$#" -gt 0 ]; do case "$1" in --fresh) - fresh=true + # 保留旧参数兼容性;部署始终按全新部署处理。 ;; --help|-h) usage @@ -41,12 +41,10 @@ fi root_dir="$(realpath -e -- "${root_argument:-$(dirname "$0")/..}")" runtime_dir=/opt/smail fresh_confirmation="确认全新部署 Smail" -if [ "$fresh" = true ]; then - printf '这是不可逆的 Smail 专属数据清理操作。请输入“%s”继续: ' "$fresh_confirmation" - if ! IFS= read -r confirmation || [ "$confirmation" != "$fresh_confirmation" ]; then - echo "未获得有效确认,已取消全新部署。" >&2 - exit 1 - fi +printf '这是不可逆的 Smail 专属数据清理操作。请输入“%s”继续: ' "$fresh_confirmation" +if ! IFS= read -r confirmation || [ "$confirmation" != "$fresh_confirmation" ]; then + echo "未获得有效确认,已取消全新部署。" >&2 + exit 1 fi case "$(uname -m)" in x86_64) arch=amd64 ;; @@ -106,32 +104,21 @@ if [ -L /run/smail/admin-control.sock ] || { [ -e /run/smail/admin-control.sock exit 1 fi -# 安装备份存放在系统备份目录,不在部署根目录遗留 .bak 文件。 -backup_dir="$(mktemp -d /var/backups/smail-deploy-XXXXXXXX)" -chmod 0700 "$backup_dir" +# 仅保留程序和服务文件的短期备份,不保留配置、数据库、证书、日志、插件或其他运行数据。 +for legacy_backup in /var/backups/smail-deploy-*; do + if [ ! -e "$legacy_backup" ]; then continue; fi + if [ -L "$legacy_backup" ] || [ ! -d "$legacy_backup" ]; then + echo "历史 Smail 备份不是安全目录,拒绝清理:$legacy_backup" >&2 + exit 1 + fi + rm -rf -- "$legacy_backup" +done if [ "$fresh" = true ]; then if [ -d "$runtime_dir" ] && [ -n "$(find "$runtime_dir" -mindepth 1 -maxdepth 1 -type l -print -quit)" ]; then echo "全新部署已停止:/opt/smail 顶层存在符号链接,拒绝清理。" >&2 exit 1 fi - install -d -m 0700 "$backup_dir/runtime" - if [ -d "$runtime_dir" ]; then - cp -a -- "$runtime_dir/." "$backup_dir/runtime/" - fi - if [ -d /etc/smail ]; then - cp -a -- /etc/smail "$backup_dir/etc-smail" - fi - if [ -d /etc/systemd/system/smail.service.d ]; then - cp -a -- /etc/systemd/system/smail.service.d "$backup_dir/smail.service.d" - fi - for name in smail.service smail-admin.service; do - if [ -f "/etc/systemd/system/$name" ]; then - cp -p -- "/etc/systemd/system/$name" "$backup_dir/$name" - fi - done - if [ -e /etc/smail/smail.env ]; then - cp -p -- /etc/smail/smail.env "$backup_dir/smail.env" - fi + if [ -d "$runtime_dir" ]; then find "$runtime_dir" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + fi @@ -146,15 +133,7 @@ if [ "$fresh" = true ]; then fi systemctl disable smail smail-admin >/dev/null 2>&1 || true systemctl daemon-reload - echo "已完成 Smail 专属运行数据清理;完整备份保存在 $backup_dir。" fi -for name in smail smail-admin; do - if [ -f "$runtime_dir/$name" ]; then cp -p -- "$runtime_dir/$name" "$backup_dir/$name"; fi -done -for name in smail.service smail-admin.service; do - if [ -f "/etc/systemd/system/$name" ]; then cp -p -- "/etc/systemd/system/$name" "$backup_dir/$name"; fi -done -if [ -f /etc/smail/smail.env ]; then cp -p -- /etc/smail/smail.env "$backup_dir/smail.env"; fi if ! getent group smail >/dev/null; then groupadd --system smail; fi if ! getent passwd smail >/dev/null; then useradd --system --gid smail --home-dir "$runtime_dir" --shell /usr/sbin/nologin smail; fi @@ -163,7 +142,7 @@ if ! getent passwd smail-plugin >/dev/null; then useradd --system --gid smail-pl install -d -o root -g root -m 0755 "$runtime_dir" install -d -o root -g root -m 0750 /etc/smail if [ ! -e /etc/smail/smail.env ]; then - # 只为新部署设置回收预算;已存在的文件始终保留部署人员的自定义环境配置。 + # 每次部署都重新创建服务环境文件,不继承旧的凭据或代理配置。 umask 0077 printf '%s\n' 'SMAIL_ROOT=/opt/smail' 'GOMEMLIMIT=256MiB' > /etc/smail/smail.env fi @@ -197,8 +176,7 @@ systemctl daemon-reload systemctl enable smail # 清理旧版本可能留下的开机启动关系。smail-admin 单元没有 [Install],不能再次 enable。 systemctl disable smail-admin >/dev/null 2>&1 || true -echo "Smail 已安装到 $runtime_dir,旧程序和服务文件备份在 $backup_dir。" -echo "配置、数据库和证书未覆盖;请使用 systemctl start smail 启动并检查运行状态。" +echo "配置、数据库、证书、日志、插件和绑定信息均未继承;请先完成初始化配置,再使用 systemctl start smail 启动。" echo "smail-admin 仅允许按需执行 start/stop,不支持开机自启动。" echo "主服务默认内存高水位 384 MiB、硬上限 512 MiB;不是容量承诺,请先核对同机资源预算。" echo "旧环境文件不会自动补写 GOMEMLIMIT;启动后请按《生产资源部署说明》执行只读资源预检。" diff --git a/deploy/install-windows.ps1 b/deploy/install-windows.ps1 index 18870f47..8747b330 100644 --- a/deploy/install-windows.ps1 +++ b/deploy/install-windows.ps1 @@ -100,15 +100,13 @@ function Stop-SmailService { } Assert-Administrator -if ($Fresh -and $StartService) { +if ($StartService) { throw "全新部署必须先完成初始化配置,不能在安装时启动服务" } -if ($Fresh) { - $freshConfirmation = "确认全新部署 Smail" - $confirmation = Read-Host "这是不可逆的 Smail 专属数据清理操作。请输入“$freshConfirmation”继续" - if ($confirmation -cne $freshConfirmation) { - throw "未获得有效确认,已取消全新部署" - } +$freshConfirmation = "确认全新部署 Smail" +$confirmation = Read-Host "这是不可逆的 Smail 专属数据清理操作。请输入“$freshConfirmation”继续" +if ($confirmation -cne $freshConfirmation) { + throw "未获得有效确认,已取消全新部署" } $mainSource = Resolve-PackageFile -Candidates @( (Join-Path $PackageRoot "out\backend\smail_windows_amd64.exe"), @@ -132,7 +130,7 @@ if (Test-Path -LiteralPath $resolvedInstallRoot) { throw "安装目录不能是文件或链接" } } -if ($Fresh -and (Test-Path -LiteralPath $resolvedInstallRoot)) { +if (Test-Path -LiteralPath $resolvedInstallRoot) { foreach ($item in Get-ChildItem -LiteralPath $resolvedInstallRoot -Force) { if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "全新部署已停止:安装目录顶层存在重解析点,拒绝清理:$($item.FullName)" @@ -140,35 +138,25 @@ if ($Fresh -and (Test-Path -LiteralPath $resolvedInstallRoot)) { } } New-Item -ItemType Directory -Path $resolvedInstallRoot -Force | Out-Null -if ($Fresh) { - Stop-SmailService - $backupRoot = Join-Path ([IO.Path]::GetDirectoryName($resolvedInstallRoot)) (".smail-fresh-backup-" + [Guid]::NewGuid().ToString("N")) - New-Item -ItemType Directory -Path $backupRoot -Force | Out-Null - if (Test-Path -LiteralPath $resolvedInstallRoot) { - foreach ($item in Get-ChildItem -LiteralPath $resolvedInstallRoot -Force) { - Copy-Item -LiteralPath $item.FullName -Destination (Join-Path $backupRoot $item.Name) -Recurse -Force - } - foreach ($item in Get-ChildItem -LiteralPath $resolvedInstallRoot -Force) { - Remove-Item -LiteralPath $item.FullName -Recurse -Force - } +$installParent = [IO.Path]::GetDirectoryName($resolvedInstallRoot) +foreach ($legacyBackup in Get-ChildItem -LiteralPath $installParent -Force -Filter ".smail-fresh-backup-*" -ErrorAction SilentlyContinue) { + if ($legacyBackup.Attributes -band [IO.FileAttributes]::ReparsePoint -or -not $legacyBackup.PSIsContainer) { + throw "历史 Smail 备份不是安全目录,拒绝清理:$($legacyBackup.FullName)" } - Write-Output "已完成 Smail 专属运行数据清理;完整备份保存在 $backupRoot。" -} else { - $backupRoot = Join-Path $resolvedInstallRoot (".backup-" + [Guid]::NewGuid().ToString("N")) + Remove-Item -LiteralPath $legacyBackup.FullName -Recurse -Force } +Stop-SmailService +foreach ($item in Get-ChildItem -LiteralPath $resolvedInstallRoot -Force) { + Remove-Item -LiteralPath $item.FullName -Recurse -Force +} +Write-Output "已完成 Smail 专属运行数据清理;不会保留旧配置、数据库、证书、日志、插件或绑定信息。" New-Item -ItemType Directory -Path (Join-Path $resolvedInstallRoot "config") -Force | Out-Null New-Item -ItemType Directory -Path (Join-Path $resolvedInstallRoot "data") -Force | Out-Null -$transactionRoot = Join-Path $resolvedInstallRoot (".upgrade-" + [Guid]::NewGuid().ToString("N")) +$transactionRoot = Join-Path $resolvedInstallRoot (".deploy-" + [Guid]::NewGuid().ToString("N")) New-Item -ItemType Directory -Path $transactionRoot -Force | Out-Null -if (-not $Fresh) { - $backupRoot = Join-Path $resolvedInstallRoot (".backup-" + [Guid]::NewGuid().ToString("N")) - New-Item -ItemType Directory -Path $backupRoot -Force | Out-Null -} $mainTarget = Join-Path $resolvedInstallRoot "smail.exe" $adminTarget = Join-Path $resolvedInstallRoot "smail-admin.exe" -$mainBackup = Join-Path $backupRoot "smail.exe" -$adminBackup = Join-Path $backupRoot "smail-admin.exe" $serviceCreated = $false try { @@ -177,8 +165,6 @@ try { Stop-SmailService Copy-Item -LiteralPath $mainSource -Destination (Join-Path $transactionRoot "smail.exe") Copy-Item -LiteralPath $adminSource -Destination (Join-Path $transactionRoot "smail-admin.exe") - if (Test-Path -LiteralPath $mainTarget) { Move-Item -LiteralPath $mainTarget -Destination $mainBackup } - if (Test-Path -LiteralPath $adminTarget) { Move-Item -LiteralPath $adminTarget -Destination $adminBackup } Move-Item -LiteralPath (Join-Path $transactionRoot "smail.exe") -Destination $mainTarget $mainInstalled = $true Move-Item -LiteralPath (Join-Path $transactionRoot "smail-admin.exe") -Destination $adminTarget @@ -222,25 +208,15 @@ try { if ($adminInstalled -and (Test-Path -LiteralPath $adminTarget)) { Remove-Item -LiteralPath $adminTarget -Force } - if (Test-Path -LiteralPath $mainBackup) { - Move-Item -LiteralPath $mainBackup -Destination $mainTarget -Force - } - if (Test-Path -LiteralPath $adminBackup) { - Move-Item -LiteralPath $adminBackup -Destination $adminTarget -Force - } if ($serviceCreated) { $deleteArguments = @("delete", "Smail") Invoke-Native -FilePath "sc.exe" -Arguments $deleteArguments } - # 失败时保留事务目录和备份,回滚再次失败时也不能丢失唯一恢复材料。 + # 全新部署不保留旧运行数据;失败时仅清理本次已安装的程序。 throw } -$temporaryRoots = @($transactionRoot) -if (-not $Fresh) { - $temporaryRoots += $backupRoot -} -foreach ($temporaryRoot in $temporaryRoots) { +foreach ($temporaryRoot in @($transactionRoot)) { if (Test-Path -LiteralPath $temporaryRoot) { $resolvedTemporaryRoot = (Resolve-Path -LiteralPath $temporaryRoot).Path if ([IO.Path]::GetDirectoryName($resolvedTemporaryRoot) -ne $resolvedInstallRoot.TrimEnd('\')) { diff --git a/deploy/nginx/deploy_certificate.py b/deploy/nginx/deploy_certificate.py index c335901d..7ef22e6b 100644 --- a/deploy/nginx/deploy_certificate.py +++ b/deploy/nginx/deploy_certificate.py @@ -17,10 +17,10 @@ import time import uuid -DOMAINS = ('solit.cn', '*.solit.cn') -VERIFY_HOSTNAMES = ('solit.cn', 'web.solit.cn', 'mail.solit.cn', 'smtp.solit.cn', - 'imap.solit.cn', 'pop.solit.cn') -CERTIFICATE_SNI = 'web.solit.cn' +DOMAINS = ('cdis.cn', '*.cdis.cn') +VERIFY_HOSTNAMES = ('cdis.cn', 'web.cdis.cn', 'mail.cdis.cn', 'smtp.cdis.cn', + 'imap.cdis.cn', 'pop.cdis.cn') +CERTIFICATE_SNI = 'web.cdis.cn' LINEAGE = Path('/var/lib/smail-acme-tls/certificates') TARGET = Path('/opt/smail/ssl') BACKUPS = Path('/var/backups') @@ -147,9 +147,9 @@ def publish(): check_nginx_paths() run('systemctl', 'is-active', '--quiet', 'nginx') identity = pwd.getpwnam('smail') - sources = {'candidate.crt': read_file(LINEAGE / 'solit.cn.crt', 0), - 'candidate.key': read_file(LINEAGE / 'solit.cn.key', 0), - 'candidate.issuer.crt': read_file(LINEAGE / 'solit.cn.issuer.crt', 0), + sources = {'candidate.crt': read_file(LINEAGE / 'cdis.cn.crt', 0), + 'candidate.key': read_file(LINEAGE / 'cdis.cn.key', 0), + 'candidate.issuer.crt': read_file(LINEAGE / 'cdis.cn.issuer.crt', 0), 'previous.crt': read_file(TARGET / 'custom.crt', identity.pw_uid), 'previous.key': read_file(TARGET / 'custom.key', identity.pw_uid)} stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S') + '-' + uuid.uuid4().hex[:8] diff --git a/deploy/nginx/renew_certificate.py b/deploy/nginx/renew_certificate.py index 8007e519..727b0ab7 100644 --- a/deploy/nginx/renew_certificate.py +++ b/deploy/nginx/renew_certificate.py @@ -29,7 +29,7 @@ def challenge_arguments(): return ['--dns', dns_provider()] if method != 'http': raise RuntimeError('验证方式必须为 dns 或 http,不允许自动回退到停机验证') - if '*.solit.cn' in DOMAINS: + if '*.cdis.cn' in DOMAINS: raise RuntimeError('泛域名证书必须使用 DNS 验证,不能使用 HTTP 验证') private = ROOT.lstat() if (ROOT.resolve(strict=True) != ROOT or not stat.S_ISDIR(private.st_mode)