实现全新部署数据清理保护
部署脚本、外部数据库初始化保护及统一证书域名配置已完成;不保留旧运行数据。
This commit is contained in:
parent
a6e55db4f5
commit
e3c6717cbb
@ -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")
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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{}
|
||||
|
||||
@ -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() }
|
||||
}
|
||||
|
||||
@ -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;启动后请按《生产资源部署说明》执行只读资源预检。"
|
||||
|
||||
@ -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('\')) {
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user