$ErrorActionPreference = "Stop" Set-StrictMode -Version Latest $ProgressPreference = "SilentlyContinue" # Windows PowerShell 5.1 może domyślnie negocjować starszy protokół; serwis # aktualizacji akceptuje wyłącznie HTTPS z TLS 1.2 lub nowszym. [Net.ServicePointManager]::SecurityProtocol = ( [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 ) $TargetVersion = "0.0.104" $ArtifactBaseUrl = "https://updates.putex.org/ristu/core/releases/0.0.104" $UpdateManifestUrl = "https://updates.putex.org/ristu/core/stable.json" $ExpectedComposeSha256 = "0967a44f30ae74ec1cd75c99e312839f752fa10a45093d601e396df2cd433047" $ExpectedEnvSha256 = "f081a9953c0adbded3f0f963dc8e35eb6d7bcad0f313f28f58b6c5df861a0a05" function Write-RistuLog([string]$Message) { Write-Host "Ristu: $Message" } function Invoke-Compose([string[]]$ComposeArgs) { & docker compose @ComposeArgs if ($LASTEXITCODE -ne 0) { throw "Polecenie Docker Compose zakończyło się błędem." } } function New-RistuSecret { [byte[]]$Bytes = New-Object byte[] 32 $Generator = [Security.Cryptography.RandomNumberGenerator]::Create() try { $Generator.GetBytes($Bytes) } finally { $Generator.Dispose() } return (($Bytes | ForEach-Object { $_.ToString("x2") }) -join "") } $TargetDir = (Get-Location).Path $EnvFile = Join-Path $TargetDir ".env" $ComposeFile = Join-Path $TargetDir "docker-compose.yml" $EnvExampleFile = Join-Path $TargetDir ".env.example" $InstallSecretKeys = [System.Collections.Generic.HashSet[string]]::new( [string[]]@( "RISTU_DB_PASSWORD", "ORTHANC_DB_PASSWORD", "ORTHANC_AUTH_SERVICE_PASSWORD", "ORTHANC_AUTH_SECRET", "RISTU_PROXY_SESSION_SECRET", "RISTU_SETTINGS_SECRET" ), [StringComparer]::Ordinal ) if (Test-Path -LiteralPath $EnvFile -PathType Leaf) { $Mode = "update" } elseif (Test-Path -LiteralPath $EnvFile) { throw "Ristu: błąd: plik .env istnieje, ale nie jest zwykłym plikiem." } else { $ExistingEntries = @(Get-ChildItem -LiteralPath $TargetDir -Force) if ($ExistingEntries.Count -ne 0) { throw "Ristu: błąd: nową instalację uruchom w całkowicie pustym katalogu; istniejącą instalację uruchom w katalogu zawierającym plik .env." } $Mode = "install" } if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { throw "Ristu: błąd: brak programu Docker." } Invoke-Compose @("version") $UpHelp = (& docker compose up --help 2>&1 | Out-String) if ($LASTEXITCODE -ne 0 -or $UpHelp -notmatch "--wait") { throw "Ristu: błąd: Docker Compose jest zbyt stary; zaktualizuj go do wersji obsługującej --wait." } if ($Mode -eq "install") { $KnownContainers = [System.Collections.Generic.HashSet[string]]::new( [string[]]@("ristu-db", "orthanc-db", "orthanc-auth-service", "orthanc", "ohif", "ristu-api", "ristu-web"), [StringComparer]::Ordinal ) $ExistingContainers = @(& docker ps -a --format "{{.Names}}") if ($LASTEXITCODE -ne 0) { throw "Ristu: błąd: nie można sprawdzić istniejących kontenerów." } foreach ($Name in $ExistingContainers) { if ($KnownContainers.Contains($Name)) { throw "Ristu: błąd: Docker zawiera już kontenery Ristu; odzyskaj ich plik .env i uruchom tryb aktualizacji." } } $KnownVolumes = [System.Collections.Generic.HashSet[string]]::new( [string[]]@( "ristu-fresh_ristu_db", "ristu-fresh_ristu_api_runtime", "ristu-fresh_ristu_web_tls", "ristu-fresh_orthanc_db", "ristu-fresh_orthanc_dicom" ), [StringComparer]::Ordinal ) $ExistingVolumes = @(& docker volume ls --format "{{.Name}}") if ($LASTEXITCODE -ne 0) { throw "Ristu: błąd: nie można sprawdzić istniejących wolumenów." } foreach ($Name in $ExistingVolumes) { if ($KnownVolumes.Contains($Name)) { throw "Ristu: błąd: Docker zawiera już wolumeny Ristu; nie można bezpiecznie utworzyć nowych sekretów instalacji." } } } $TempDir = Join-Path $TargetDir (".ristu-bootstrap." + [Guid]::NewGuid().ToString("N")) New-Item -ItemType Directory -Path $TempDir | Out-Null try { $CandidateCompose = Join-Path $TempDir "docker-compose.yml" $CandidateEnvExample = Join-Path $TempDir ".env.example" $CandidateEnv = Join-Path $TempDir ".env" if ($Mode -eq "install") { Write-RistuLog "tryb nowej instalacji w pustym katalogu" } else { Write-RistuLog "tryb aktualizacji istniejącej instalacji" } Write-RistuLog "pobieranie plików wydania $TargetVersion" Invoke-WebRequest -UseBasicParsing -Uri "$ArtifactBaseUrl/docker-compose.yml" -OutFile $CandidateCompose Invoke-WebRequest -UseBasicParsing -Uri "$ArtifactBaseUrl/env.example" -OutFile $CandidateEnvExample $ComposeHash = (Get-FileHash -LiteralPath $CandidateCompose -Algorithm SHA256).Hash.ToLowerInvariant() $EnvHash = (Get-FileHash -LiteralPath $CandidateEnvExample -Algorithm SHA256).Hash.ToLowerInvariant() if ($ComposeHash -ne $ExpectedComposeSha256) { throw "Ristu: błąd: pobrany plik docker-compose.yml ma nieprawidłową sumę SHA-256." } if ($EnvHash -ne $ExpectedEnvSha256) { throw "Ristu: błąd: pobrany plik env.example ma nieprawidłową sumę SHA-256." } $MergedLines = [System.Collections.Generic.List[string]]::new() if ($Mode -eq "install") { $GeneratedSecretKeys = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) foreach ($Line in [IO.File]::ReadAllLines($CandidateEnvExample)) { if ($Line -match "^([A-Za-z_][A-Za-z0-9_]*)=") { $Key = $Matches[1] if ($InstallSecretKeys.Contains($Key)) { if (-not $GeneratedSecretKeys.Add($Key)) { throw "Ristu: błąd: env.example zawiera więcej niż jedno pole $Key." } $MergedLines.Add("$Key=$(New-RistuSecret)") continue } if ($Key -eq "RISTU_IMAGE_TAG") { $MergedLines.Add("RISTU_IMAGE_TAG=latest") continue } } $MergedLines.Add($Line) } foreach ($Key in $InstallSecretKeys) { if (-not $GeneratedSecretKeys.Contains($Key)) { throw "Ristu: błąd: env.example nie zawiera pola $Key." } } foreach ($Line in $MergedLines) { if ($Line -match "^[A-Za-z_][A-Za-z0-9_]*=.*HASLO_ZMIEN_MNIE") { throw "Ristu: błąd: nie zastąpiono wszystkich placeholderów sekretów." } } } else { # Plik .env jest czytany wyłącznie jako tekst. Sekrety i własny, # niepusty adres manifestu pozostają bez zmian; pusty adres dostaje # oficjalny kanał, a aktualizator zmienia tag oraz dodaje nowe klucze. $ImageTagWritten = $false $ManifestUrlWritten = $false foreach ($Line in [IO.File]::ReadAllLines($EnvFile)) { if ($Line -match "^\s*RISTU_UPDATE_MANIFEST_URL\s*=(.*)$") { if (-not $ManifestUrlWritten) { if ([string]::IsNullOrWhiteSpace($Matches[1])) { $MergedLines.Add("RISTU_UPDATE_MANIFEST_URL=$UpdateManifestUrl") } else { $MergedLines.Add($Line) } $ManifestUrlWritten = $true } continue } if ($Line -match "^\s*RISTU_IMAGE_TAG\s*=") { if (-not $ImageTagWritten) { $MergedLines.Add("RISTU_IMAGE_TAG=latest") $ImageTagWritten = $true } continue } $MergedLines.Add($Line) } if (-not $ImageTagWritten) { $MergedLines.Add("RISTU_IMAGE_TAG=latest") } $ExistingKeys = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) foreach ($Line in $MergedLines) { if ($Line -match "^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=") { [void]$ExistingKeys.Add($Matches[1]) } } $MissingHeaderWritten = $false foreach ($Line in [IO.File]::ReadAllLines($CandidateEnvExample)) { if ($Line -match "^([A-Za-z_][A-Za-z0-9_]*)=") { $Key = $Matches[1] if (-not $ExistingKeys.Contains($Key)) { if (-not $MissingHeaderWritten) { $MergedLines.Add("") $MergedLines.Add("# Ustawienia dodane automatycznie podczas aktualizacji Ristu.") $MissingHeaderWritten = $true } if ($InstallSecretKeys.Contains($Key)) { $MergedLines.Add("$Key=$(New-RistuSecret)") } else { $MergedLines.Add($Line) } [void]$ExistingKeys.Add($Key) } } } } [IO.File]::WriteAllLines($CandidateEnv, $MergedLines, [Text.UTF8Encoding]::new($false)) $CandidateFiles = @("--env-file", $CandidateEnv, "-f", $CandidateCompose) $FinalFiles = @("--env-file", $EnvFile, "-f", $ComposeFile) $OverrideFile = Join-Path $TargetDir "docker-compose.override.yml" if (Test-Path -LiteralPath $OverrideFile -PathType Leaf) { $CandidateFiles += @("-f", $OverrideFile) $FinalFiles += @("-f", $OverrideFile) } Write-RistuLog "sprawdzanie konfiguracji" Invoke-Compose ($CandidateFiles + @("config", "--quiet")) Write-RistuLog "pobieranie najnowszych obrazów" Invoke-Compose ($CandidateFiles + @("pull")) if ($Mode -eq "update") { $Timestamp = (Get-Date).ToUniversalTime().ToString("yyyyMMddTHHmmssZ") $BackupRoot = Join-Path $TargetDir ".ristu-backups" $BackupDir = Join-Path $BackupRoot "$Timestamp-$PID" New-Item -ItemType Directory -Path $BackupRoot -Force | Out-Null New-Item -ItemType Directory -Path $BackupDir | Out-Null Copy-Item -LiteralPath $EnvFile -Destination (Join-Path $BackupDir ".env") if (Test-Path -LiteralPath $ComposeFile -PathType Leaf) { Copy-Item -LiteralPath $ComposeFile -Destination (Join-Path $BackupDir "docker-compose.yml") } if (Test-Path -LiteralPath $EnvExampleFile -PathType Leaf) { Copy-Item -LiteralPath $EnvExampleFile -Destination (Join-Path $BackupDir ".env.example") } } Move-Item -LiteralPath $CandidateCompose -Destination $ComposeFile -Force Move-Item -LiteralPath $CandidateEnvExample -Destination $EnvExampleFile -Force Move-Item -LiteralPath $CandidateEnv -Destination $EnvFile -Force Write-RistuLog "uruchamianie wydania z obrazów latest" Invoke-Compose ($FinalFiles + @("up", "-d", "--remove-orphans", "--wait")) Invoke-Compose ($FinalFiles + @("ps")) if ($Mode -eq "install") { Write-RistuLog "instalacja zakończona" } else { Write-RistuLog "aktualizacja zakończona; kopia poprzedniej konfiguracji: $BackupDir" } } finally { if (Test-Path -LiteralPath $TempDir) { Remove-Item -LiteralPath $TempDir -Recurse -Force } }