| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- [CmdletBinding(SupportsShouldProcess = $true)]
- param(
- [Parameter(Mandatory = $true)]
- [string]$PublishPath,
- [Parameter(Mandatory = $true)]
- [string]$ConfigurationPath,
- [string]$SiteName = 'Default Web Site',
- [string]$ApplicationName = 'xiaoshu-sync',
- [string]$ApplicationPoolName = 'XiaoshuLegacySync',
- [string]$PhysicalPath = 'C:\inetpub\xiaoshu-sync',
- [string]$HealthUrl = 'http://127.0.0.1/xiaoshu-sync/v1/health',
- [switch]$PreflightOnly
- )
- $ErrorActionPreference = 'Stop'
- Set-StrictMode -Version Latest
- function Assert-Administrator {
- $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
- $principal = [Security.Principal.WindowsPrincipal]::new($identity)
- if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
- throw '请使用“以管理员身份运行”的 PowerShell 执行部署。'
- }
- }
- function Assert-Configuration([string]$Path) {
- if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "配置文件不存在:$Path" }
- $raw = Get-Content -LiteralPath $Path -Raw
- $config = $raw | ConvertFrom-Json
- $connection = [string]$config.ConnectionStrings.LegacySqlServer
- $keyId = [string]$config.XiaoshuSync.KeyId
- $secret = [string]$config.XiaoshuSync.Secret
- $allowedIps = @($config.XiaoshuSync.AllowedIps)
- if ([string]::IsNullOrWhiteSpace($connection) -or $connection -like 'REPLACE_*') { throw 'LegacySqlServer 连接串尚未配置。' }
- if ([string]::IsNullOrWhiteSpace($keyId) -or $keyId -like 'REPLACE_*') { throw '同步桥 KeyId 尚未配置。' }
- if ($secret.Length -lt 32 -or $secret -like 'REPLACE_*') { throw '同步桥 Secret 必须是至少 32 位的随机字符串。' }
- if ($allowedIps.Count -eq 0 -or $allowedIps | Where-Object { [string]::IsNullOrWhiteSpace([string]$_) -or [string]$_ -like 'REPLACE_*' }) {
- throw '必须配置 Parse/云函数服务器的出口 IP 白名单。'
- }
- return $raw
- }
- Assert-Administrator
- Import-Module WebAdministration
- $resolvedPublishPath = (Resolve-Path -LiteralPath $PublishPath).Path
- if (-not (Test-Path -LiteralPath (Join-Path $resolvedPublishPath 'Xiaoshu.LegacySyncBridge.dll'))) { throw '发布目录缺少 Xiaoshu.LegacySyncBridge.dll。' }
- if (-not (Test-Path -LiteralPath (Join-Path $resolvedPublishPath 'web.config'))) { throw '发布目录缺少 web.config。' }
- $productionConfiguration = Assert-Configuration $ConfigurationPath
- if (-not (Get-WebGlobalModule -Name 'AspNetCoreModuleV2' -ErrorAction SilentlyContinue)) {
- throw '服务器尚未安装 IIS AspNetCoreModuleV2。同步桥为 Windows x64 自包含发布,不要求服务器安装 .NET 8 Runtime,但仍需要该 IIS 模块。'
- }
- if (-not (Test-Path "IIS:\Sites\$SiteName")) { throw "IIS 站点不存在:$SiteName" }
- Write-Host '部署前检查通过:'
- Write-Host " IIS 站点:$SiteName"
- Write-Host " 应用路径:/$ApplicationName"
- Write-Host " 应用池:$ApplicationPoolName"
- Write-Host " 物理目录:$PhysicalPath"
- Write-Host " 健康地址:$HealthUrl"
- Write-Host ' 配置:连接串、HMAC KeyId/Secret 与 IP 白名单均已通过格式检查(未输出敏感值)'
- if ($PreflightOnly) {
- Write-Host 'PreflightOnly 已启用,未创建目录、应用池或 IIS 应用,也未复制任何文件。'
- return
- }
- $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
- $backupPath = "$PhysicalPath.backup-$timestamp"
- $applicationPath = "/$ApplicationName"
- if ($PSCmdlet.ShouldProcess("$SiteName$applicationPath", '部署小树旧系统同步桥')) {
- if (Test-Path "IIS:\AppPools\$ApplicationPoolName") {
- Stop-WebAppPool -Name $ApplicationPoolName -ErrorAction SilentlyContinue
- }
- if (Test-Path -LiteralPath $PhysicalPath) {
- Copy-Item -LiteralPath $PhysicalPath -Destination $backupPath -Recurse -Force
- Write-Host "已备份现有版本:$backupPath"
- } else {
- New-Item -ItemType Directory -Path $PhysicalPath -Force | Out-Null
- }
- Copy-Item -Path (Join-Path $resolvedPublishPath '*') -Destination $PhysicalPath -Recurse -Force
- $productionConfiguration | Set-Content -LiteralPath (Join-Path $PhysicalPath 'appsettings.Production.json') -Encoding UTF8
- if (-not (Test-Path "IIS:\AppPools\$ApplicationPoolName")) {
- New-WebAppPool -Name $ApplicationPoolName | Out-Null
- }
- Set-ItemProperty "IIS:\AppPools\$ApplicationPoolName" -Name managedRuntimeVersion -Value ''
- Set-ItemProperty "IIS:\AppPools\$ApplicationPoolName" -Name processModel.identityType -Value ApplicationPoolIdentity
- Set-ItemProperty "IIS:\AppPools\$ApplicationPoolName" -Name startMode -Value AlwaysRunning
- $existingApplication = Get-WebApplication -Site $SiteName -Name $ApplicationName -ErrorAction SilentlyContinue
- if ($null -eq $existingApplication) {
- New-WebApplication -Site $SiteName -Name $ApplicationName -PhysicalPath $PhysicalPath -ApplicationPool $ApplicationPoolName | Out-Null
- } else {
- Set-ItemProperty "IIS:\Sites\$SiteName\$ApplicationName" -Name physicalPath -Value $PhysicalPath
- Set-ItemProperty "IIS:\Sites\$SiteName\$ApplicationName" -Name applicationPool -Value $ApplicationPoolName
- }
- & icacls.exe $PhysicalPath /inheritance:r /grant:r "SYSTEM:(OI)(CI)F" "Administrators:(OI)(CI)F" "IIS AppPool\${ApplicationPoolName}:(OI)(CI)RX" /T /C | Out-Null
- Start-WebAppPool -Name $ApplicationPoolName
- $healthy = $false
- foreach ($attempt in 1..12) {
- try {
- $health = Invoke-RestMethod -Uri $HealthUrl -Method Get -TimeoutSec 10
- if ($health.status -eq 'ok') { $healthy = $true; break }
- } catch {
- Start-Sleep -Seconds 2
- }
- }
- if (-not $healthy) {
- throw "IIS 已部署,但健康检查没有通过:$HealthUrl。请检查事件日志与 IIS stdout 日志。"
- }
- Write-Host '同步桥部署完成,未输出任何连接串或签名密钥。'
- Write-Host "健康地址:$HealthUrl"
- Write-Host "回滚备份:$(if (Test-Path -LiteralPath $backupPath) { $backupPath } else { '首次部署,无旧版本' })"
- }
|