Install-XiaoshuLegacySyncBridge.ps1 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. [CmdletBinding(SupportsShouldProcess = $true)]
  2. param(
  3. [Parameter(Mandatory = $true)]
  4. [string]$PublishPath,
  5. [Parameter(Mandatory = $true)]
  6. [string]$ConfigurationPath,
  7. [string]$SiteName = 'Default Web Site',
  8. [string]$ApplicationName = 'xiaoshu-sync',
  9. [string]$ApplicationPoolName = 'XiaoshuLegacySync',
  10. [string]$PhysicalPath = 'C:\inetpub\xiaoshu-sync',
  11. [string]$HealthUrl = 'http://127.0.0.1/xiaoshu-sync/v1/health',
  12. [switch]$PreflightOnly
  13. )
  14. $ErrorActionPreference = 'Stop'
  15. Set-StrictMode -Version Latest
  16. function Assert-Administrator {
  17. $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
  18. $principal = [Security.Principal.WindowsPrincipal]::new($identity)
  19. if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
  20. throw '请使用“以管理员身份运行”的 PowerShell 执行部署。'
  21. }
  22. }
  23. function Assert-Configuration([string]$Path) {
  24. if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "配置文件不存在:$Path" }
  25. $raw = Get-Content -LiteralPath $Path -Raw
  26. $config = $raw | ConvertFrom-Json
  27. $connection = [string]$config.ConnectionStrings.LegacySqlServer
  28. $keyId = [string]$config.XiaoshuSync.KeyId
  29. $secret = [string]$config.XiaoshuSync.Secret
  30. $allowedIps = @($config.XiaoshuSync.AllowedIps)
  31. if ([string]::IsNullOrWhiteSpace($connection) -or $connection -like 'REPLACE_*') { throw 'LegacySqlServer 连接串尚未配置。' }
  32. if ([string]::IsNullOrWhiteSpace($keyId) -or $keyId -like 'REPLACE_*') { throw '同步桥 KeyId 尚未配置。' }
  33. if ($secret.Length -lt 32 -or $secret -like 'REPLACE_*') { throw '同步桥 Secret 必须是至少 32 位的随机字符串。' }
  34. if ($allowedIps.Count -eq 0 -or $allowedIps | Where-Object { [string]::IsNullOrWhiteSpace([string]$_) -or [string]$_ -like 'REPLACE_*' }) {
  35. throw '必须配置 Parse/云函数服务器的出口 IP 白名单。'
  36. }
  37. return $raw
  38. }
  39. Assert-Administrator
  40. Import-Module WebAdministration
  41. $resolvedPublishPath = (Resolve-Path -LiteralPath $PublishPath).Path
  42. if (-not (Test-Path -LiteralPath (Join-Path $resolvedPublishPath 'Xiaoshu.LegacySyncBridge.dll'))) { throw '发布目录缺少 Xiaoshu.LegacySyncBridge.dll。' }
  43. if (-not (Test-Path -LiteralPath (Join-Path $resolvedPublishPath 'web.config'))) { throw '发布目录缺少 web.config。' }
  44. $productionConfiguration = Assert-Configuration $ConfigurationPath
  45. if (-not (Get-WebGlobalModule -Name 'AspNetCoreModuleV2' -ErrorAction SilentlyContinue)) {
  46. throw '服务器尚未安装 IIS AspNetCoreModuleV2。同步桥为 Windows x64 自包含发布,不要求服务器安装 .NET 8 Runtime,但仍需要该 IIS 模块。'
  47. }
  48. if (-not (Test-Path "IIS:\Sites\$SiteName")) { throw "IIS 站点不存在:$SiteName" }
  49. Write-Host '部署前检查通过:'
  50. Write-Host " IIS 站点:$SiteName"
  51. Write-Host " 应用路径:/$ApplicationName"
  52. Write-Host " 应用池:$ApplicationPoolName"
  53. Write-Host " 物理目录:$PhysicalPath"
  54. Write-Host " 健康地址:$HealthUrl"
  55. Write-Host ' 配置:连接串、HMAC KeyId/Secret 与 IP 白名单均已通过格式检查(未输出敏感值)'
  56. if ($PreflightOnly) {
  57. Write-Host 'PreflightOnly 已启用,未创建目录、应用池或 IIS 应用,也未复制任何文件。'
  58. return
  59. }
  60. $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
  61. $backupPath = "$PhysicalPath.backup-$timestamp"
  62. $applicationPath = "/$ApplicationName"
  63. if ($PSCmdlet.ShouldProcess("$SiteName$applicationPath", '部署小树旧系统同步桥')) {
  64. if (Test-Path "IIS:\AppPools\$ApplicationPoolName") {
  65. Stop-WebAppPool -Name $ApplicationPoolName -ErrorAction SilentlyContinue
  66. }
  67. if (Test-Path -LiteralPath $PhysicalPath) {
  68. Copy-Item -LiteralPath $PhysicalPath -Destination $backupPath -Recurse -Force
  69. Write-Host "已备份现有版本:$backupPath"
  70. } else {
  71. New-Item -ItemType Directory -Path $PhysicalPath -Force | Out-Null
  72. }
  73. Copy-Item -Path (Join-Path $resolvedPublishPath '*') -Destination $PhysicalPath -Recurse -Force
  74. $productionConfiguration | Set-Content -LiteralPath (Join-Path $PhysicalPath 'appsettings.Production.json') -Encoding UTF8
  75. if (-not (Test-Path "IIS:\AppPools\$ApplicationPoolName")) {
  76. New-WebAppPool -Name $ApplicationPoolName | Out-Null
  77. }
  78. Set-ItemProperty "IIS:\AppPools\$ApplicationPoolName" -Name managedRuntimeVersion -Value ''
  79. Set-ItemProperty "IIS:\AppPools\$ApplicationPoolName" -Name processModel.identityType -Value ApplicationPoolIdentity
  80. Set-ItemProperty "IIS:\AppPools\$ApplicationPoolName" -Name startMode -Value AlwaysRunning
  81. $existingApplication = Get-WebApplication -Site $SiteName -Name $ApplicationName -ErrorAction SilentlyContinue
  82. if ($null -eq $existingApplication) {
  83. New-WebApplication -Site $SiteName -Name $ApplicationName -PhysicalPath $PhysicalPath -ApplicationPool $ApplicationPoolName | Out-Null
  84. } else {
  85. Set-ItemProperty "IIS:\Sites\$SiteName\$ApplicationName" -Name physicalPath -Value $PhysicalPath
  86. Set-ItemProperty "IIS:\Sites\$SiteName\$ApplicationName" -Name applicationPool -Value $ApplicationPoolName
  87. }
  88. & 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
  89. Start-WebAppPool -Name $ApplicationPoolName
  90. $healthy = $false
  91. foreach ($attempt in 1..12) {
  92. try {
  93. $health = Invoke-RestMethod -Uri $HealthUrl -Method Get -TimeoutSec 10
  94. if ($health.status -eq 'ok') { $healthy = $true; break }
  95. } catch {
  96. Start-Sleep -Seconds 2
  97. }
  98. }
  99. if (-not $healthy) {
  100. throw "IIS 已部署,但健康检查没有通过:$HealthUrl。请检查事件日志与 IIS stdout 日志。"
  101. }
  102. Write-Host '同步桥部署完成,未输出任何连接串或签名密钥。'
  103. Write-Host "健康地址:$HealthUrl"
  104. Write-Host "回滚备份:$(if (Test-Path -LiteralPath $backupPath) { $backupPath } else { '首次部署,无旧版本' })"
  105. }