增加需确认的全新部署流程
默认升级保留 Smail 运行数据;Linux 和 Windows 的全新部署均要求输入确认词,先备份后仅清理 Smail 专属目录,并补充部署策略回归用例。
This commit is contained in:
parent
0c4ad939aa
commit
9eabdd2049
@ -96,6 +96,29 @@ func TestInstallerManagesProtectedEnvironmentFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallerRequiresExplicitFreshReset(t *testing.T) {
|
||||
root := filepath.Clean(filepath.Join("..", "..", ".."))
|
||||
installer, err := os.ReadFile(filepath.Join(root, "deploy", "install-systemd.sh"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := string(installer)
|
||||
for _, required := range []string{
|
||||
"--fresh",
|
||||
"确认全新部署 Smail",
|
||||
"cp -a -- \"$runtime_dir/.\" \"$backup_dir/runtime/\"",
|
||||
"find \"$runtime_dir\" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +",
|
||||
"systemctl disable smail smail-admin",
|
||||
} {
|
||||
if !strings.Contains(content, required) {
|
||||
t.Fatalf("installer is missing explicit fresh-reset behavior %q", required)
|
||||
}
|
||||
}
|
||||
if strings.Contains(content, "全新部署已停止:$target 非空") {
|
||||
t.Fatal("installer must not reject existing data without an explicit fresh-reset request")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSmailServiceRetainsPluginIsolationCapabilities(t *testing.T) {
|
||||
root := filepath.Clean(filepath.Join("..", "..", ".."))
|
||||
unit, err := os.ReadFile(filepath.Join(root, "deploy", "systemd", "smail.service"))
|
||||
|
||||
@ -50,6 +50,29 @@ func TestWindowsDeploymentScriptsKeepAdminChannelManual(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWindowsInstallerRequiresExplicitFreshReset(t *testing.T) {
|
||||
root := filepath.Clean(filepath.Join("..", "..", ".."))
|
||||
installer, err := os.ReadFile(filepath.Join(root, "deploy", "install-windows.ps1"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := string(installer)
|
||||
for _, required := range []string{
|
||||
"[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)
|
||||
}
|
||||
}
|
||||
if strings.Contains(content, "全新部署已停止:$existing 不是空目录") {
|
||||
t.Fatal("Windows installer must not reject existing data without an explicit fresh-reset request")
|
||||
}
|
||||
}
|
||||
|
||||
func hasUTF8BOM(data []byte) bool {
|
||||
return len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF
|
||||
}
|
||||
|
||||
@ -1,13 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
echo "用法:$0 [--fresh] [发布目录]"
|
||||
echo "默认模式保留配置、数据库、证书、日志和插件;--fresh 模式必须输入确认词后才会备份并清理 Smail 专属运行数据。"
|
||||
}
|
||||
|
||||
fresh=false
|
||||
root_argument=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--fresh)
|
||||
fresh=true
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
-* )
|
||||
echo "未知参数:$1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
if [ -n "$root_argument" ]; then
|
||||
echo "发布目录只能指定一次。" >&2
|
||||
exit 1
|
||||
fi
|
||||
root_argument="$1"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "必须使用 root 用户安装 Smail" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
root_dir="$(realpath -e -- "${1:-$(dirname "$0")/..}")"
|
||||
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
|
||||
fi
|
||||
case "$(uname -m)" in
|
||||
x86_64) arch=amd64 ;;
|
||||
aarch64|arm64) arch=arm64 ;;
|
||||
@ -66,31 +106,48 @@ if [ -L /run/smail/admin-control.sock ] || { [ -e /run/smail/admin-control.sock
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 全新安装不继承目标目录中的配置、证书或业务数据,也不自动清理它们。
|
||||
for directory in config data ssl log plugins; do
|
||||
target="$runtime_dir/$directory"
|
||||
if [ -d "$target" ] && [ -n "$(find "$target" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
|
||||
echo "全新部署已停止:$target 非空。请先确认并处理旧数据,再通过初始化页面重新配置。" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
for file in config.json config.dev.json smail.db smail-state.db; do
|
||||
if [ -e "$runtime_dir/$file" ] || [ -L "$runtime_dir/$file" ]; then
|
||||
echo "全新部署已停止:发现历史运行文件 $file,不会自动继承或删除。" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if [ -e /etc/smail/smail.env ]; then
|
||||
echo "全新部署已停止:请先确认并处理 /etc/smail/smail.env 中的旧部署环境配置。" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -d /etc/systemd/system/smail.service.d ] && [ -n "$(find /etc/systemd/system/smail.service.d -mindepth 1 -maxdepth 1 -print -quit)" ]; then
|
||||
echo "全新部署已停止:请先确认并处理 Smail 的旧服务覆盖配置。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 安装备份存放在系统备份目录,不在部署根目录遗留 .bak 文件。
|
||||
backup_dir="$(mktemp -d /var/backups/smail-deploy-XXXXXXXX)"
|
||||
chmod 0700 "$backup_dir"
|
||||
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
|
||||
if [ -d /etc/smail ]; then
|
||||
find /etc/smail -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +
|
||||
fi
|
||||
if [ -d /etc/systemd/system/smail.service.d ]; then
|
||||
rm -rf -- /etc/systemd/system/smail.service.d
|
||||
fi
|
||||
if [ -d /run/smail ]; then
|
||||
find /run/smail -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +
|
||||
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
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
param(
|
||||
[string]$PackageRoot = $null,
|
||||
[string]$InstallRoot = (Join-Path $env:ProgramData "Smail"),
|
||||
[switch]$Fresh,
|
||||
[switch]$StartService
|
||||
)
|
||||
|
||||
@ -99,6 +100,16 @@ function Stop-SmailService {
|
||||
}
|
||||
|
||||
Assert-Administrator
|
||||
if ($Fresh -and $StartService) {
|
||||
throw "全新部署必须先完成初始化配置,不能在安装时启动服务"
|
||||
}
|
||||
if ($Fresh) {
|
||||
$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"),
|
||||
(Join-Path $PackageRoot "backend\smail_windows_amd64.exe"),
|
||||
@ -121,27 +132,38 @@ if (Test-Path -LiteralPath $resolvedInstallRoot) {
|
||||
throw "安装目录不能是文件或链接"
|
||||
}
|
||||
}
|
||||
foreach ($name in @('config', 'data', 'ssl', 'log', 'plugins')) {
|
||||
$existing = Join-Path $resolvedInstallRoot $name
|
||||
if (-not (Test-Path -LiteralPath $existing)) { continue }
|
||||
$info = Get-Item -LiteralPath $existing -Force
|
||||
if (-not $info.PSIsContainer -or $info.Attributes -band [IO.FileAttributes]::ReparsePoint -or
|
||||
(Get-ChildItem -LiteralPath $existing -Force | Select-Object -First 1)) {
|
||||
throw "全新部署已停止:$existing 不是空目录。请先确认并处理旧数据,再通过初始化页面重新配置。"
|
||||
}
|
||||
}
|
||||
foreach ($name in @('config.json', 'config.dev.json', 'smail.db', 'smail-state.db')) {
|
||||
if (Test-Path -LiteralPath (Join-Path $resolvedInstallRoot $name)) {
|
||||
throw "全新部署已停止:发现历史运行文件 $name,不会自动继承或删除。"
|
||||
if ($Fresh -and (Test-Path -LiteralPath $resolvedInstallRoot)) {
|
||||
foreach ($item in Get-ChildItem -LiteralPath $resolvedInstallRoot -Force) {
|
||||
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
||||
throw "全新部署已停止:安装目录顶层存在重解析点,拒绝清理:$($item.FullName)"
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
Write-Output "已完成 Smail 专属运行数据清理;完整备份保存在 $backupRoot。"
|
||||
} else {
|
||||
$backupRoot = Join-Path $resolvedInstallRoot (".backup-" + [Guid]::NewGuid().ToString("N"))
|
||||
}
|
||||
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"))
|
||||
$backupRoot = Join-Path $resolvedInstallRoot (".backup-" + [Guid]::NewGuid().ToString("N"))
|
||||
New-Item -ItemType Directory -Path $transactionRoot -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path $backupRoot -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"
|
||||
@ -214,7 +236,11 @@ try {
|
||||
throw
|
||||
}
|
||||
|
||||
foreach ($temporaryRoot in @($transactionRoot, $backupRoot)) {
|
||||
$temporaryRoots = @($transactionRoot)
|
||||
if (-not $Fresh) {
|
||||
$temporaryRoots += $backupRoot
|
||||
}
|
||||
foreach ($temporaryRoot in $temporaryRoots) {
|
||||
if (Test-Path -LiteralPath $temporaryRoot) {
|
||||
$resolvedTemporaryRoot = (Resolve-Path -LiteralPath $temporaryRoot).Path
|
||||
if ([IO.Path]::GetDirectoryName($resolvedTemporaryRoot) -ne $resolvedInstallRoot.TrimEnd('\')) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user