[CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$PsqlPath, [Parameter(Mandatory = $true)] [string]$AdminUser, [Parameter(Mandatory = $true)] [string]$AdminPassword, [Parameter(Mandatory = $true)] [ValidateRange(1, 65535)] [int]$PostgreSqlPort, [Parameter(Mandatory = $true)] [string]$DatabaseName, [Parameter(Mandatory = $true)] [string]$DatabaseUser, [Parameter(Mandatory = $true)] [string]$DatabasePassword, [Parameter(Mandatory = $true)] [ValidateRange(1, 65535)] [int]$ApplicationPort, [Parameter(Mandatory = $false)] [ValidateSet('full', 'cartridge')] [string]$Edition = 'full', [Parameter(Mandatory = $true)] [string]$ConfigPath ) $ErrorActionPreference = 'Stop' trap { $logDirectory = Split-Path -Parent $ConfigPath New-Item -ItemType Directory -Path $logDirectory -Force -ErrorAction SilentlyContinue | Out-Null $_ | Out-String | Set-Content -LiteralPath (Join-Path $logDirectory 'install-error.log') -Encoding UTF8 $env:PGPASSWORD = $null exit 1 } function Assert-SafeIdentifier { param([string]$Value, [string]$Label) if ($Value -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') { throw "$Label содержит недопустимые символы." } } function Quote-Identifier { param([string]$Value) return '"' + $Value.Replace('"', '""') + '"' } function Quote-Literal { param([string]$Value) return "'" + $Value.Replace("'", "''") + "'" } function Convert-PsqlOutput { param([string]$Text) if ([string]::IsNullOrEmpty($Text)) { return $Text } # Russian PostgreSQL messages can be emitted in the Windows ANSI code page # while Windows PowerShell 5.1 decodes them as the OEM code page. if ($Text -match '[\u2500-\u259F]') { try { $culture = [System.Globalization.CultureInfo]::CurrentCulture $oemEncoding = [System.Text.Encoding]::GetEncoding($culture.TextInfo.OEMCodePage) $ansiEncoding = [System.Text.Encoding]::GetEncoding($culture.TextInfo.ANSICodePage) return $ansiEncoding.GetString($oemEncoding.GetBytes($Text)) } catch { return $Text } } return $Text } function Invoke-Psql { param( [string]$Database, [string]$Sql, [string]$Username = $AdminUser, [switch]$Capture ) $sqlFilePath = Join-Path ` ([System.IO.Path]::GetTempPath()) ` ("inventera-{0}.sql" -f ([Guid]::NewGuid().ToString('N'))) [System.IO.File]::WriteAllText( $sqlFilePath, $Sql + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false) ) try { $arguments = @( '--host', '127.0.0.1', '--port', [string]$PostgreSqlPort, '--username', $Username, '--dbname', $Database, '--no-password', '--set', 'ON_ERROR_STOP=1', '--tuples-only', '--no-align', '--file', $sqlFilePath ) $previousErrorActionPreference = $ErrorActionPreference try { $ErrorActionPreference = 'Continue' $output = & $PsqlPath @arguments 2>&1 $exitCode = $LASTEXITCODE } finally { $ErrorActionPreference = $previousErrorActionPreference } $outputText = Convert-PsqlOutput -Text ( ($output -join [Environment]::NewLine).Trim() ) if ($exitCode -ne 0) { if ($outputText -eq '') { $outputText = "psql завершился с кодом $exitCode." } throw $outputText } if ($Capture) { return $outputText } if ($outputText -ne '') { Write-Host $outputText } } finally { Remove-Item -LiteralPath $sqlFilePath -Force -ErrorAction SilentlyContinue } } if (-not (Test-Path -LiteralPath $PsqlPath -PathType Leaf)) { throw "Не найден psql.exe: $PsqlPath" } Assert-SafeIdentifier -Value $AdminUser -Label 'Логин администратора PostgreSQL' Assert-SafeIdentifier -Value $DatabaseName -Label 'Имя базы данных' Assert-SafeIdentifier -Value $DatabaseUser -Label 'Логин пользователя базы данных' $env:PGPASSWORD = $AdminPassword $connected = $false $lastConnectionError = $null for ($attempt = 1; $attempt -le 45; $attempt++) { try { Invoke-Psql -Database 'postgres' -Sql 'SELECT 1;' | Out-Null $connected = $true break } catch { $lastConnectionError = $_ Start-Sleep -Seconds 2 } } if (-not $connected) { throw "Не удалось подключиться к PostgreSQL на порту $PostgreSqlPort. $lastConnectionError" } $databaseIdentifier = Quote-Identifier $DatabaseName $databaseLiteral = Quote-Literal $DatabaseName $userIdentifier = Quote-Identifier $DatabaseUser $userLiteral = Quote-Literal $DatabaseUser $passwordLiteral = Quote-Literal $DatabasePassword $roleExists = Invoke-Psql -Database 'postgres' -Sql "SELECT 1 FROM pg_roles WHERE rolname = $userLiteral;" -Capture if ($roleExists -eq '1') { Invoke-Psql -Database 'postgres' -Sql "ALTER ROLE $userIdentifier WITH LOGIN PASSWORD $passwordLiteral;" } else { Invoke-Psql -Database 'postgres' -Sql "CREATE ROLE $userIdentifier WITH LOGIN PASSWORD $passwordLiteral;" } $databaseExists = Invoke-Psql -Database 'postgres' -Sql "SELECT 1 FROM pg_database WHERE datname = $databaseLiteral;" -Capture if ($databaseExists -ne '1') { Invoke-Psql -Database 'postgres' -Sql "CREATE DATABASE $databaseIdentifier OWNER $userIdentifier ENCODING 'UTF8';" } else { Invoke-Psql -Database 'postgres' -Sql "ALTER DATABASE $databaseIdentifier OWNER TO $userIdentifier;" } Invoke-Psql -Database 'postgres' -Sql "GRANT ALL PRIVILEGES ON DATABASE $databaseIdentifier TO $userIdentifier;" $env:PGPASSWORD = $DatabasePassword try { Invoke-Psql ` -Database $DatabaseName ` -Username $DatabaseUser ` -Sql 'SELECT 1;' | Out-Null } catch { throw "Не удалось проверить подключение с учётными данными приложения. $_" } $configDirectory = Split-Path -Parent $ConfigPath New-Item -ItemType Directory -Path $configDirectory -Force | Out-Null $config = [ordered]@{ edition = $Edition database = [ordered]@{ host = '127.0.0.1' port = $PostgreSqlPort name = $DatabaseName user = $DatabaseUser password = $DatabasePassword } application = [ordered]@{ host = '0.0.0.0' port = $ApplicationPort } } $config | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $ConfigPath -Encoding UTF8 $env:PGPASSWORD = $null Write-Host "База данных $DatabaseName и конфигурация Inventera подготовлены."