<# Romexis Implant Library Online Bulk Installer GUI Funktion: - Laedt die Planmeca Implant Library Webseite - Parst die verfuegbaren Downloadlinks zu content.planmeca.com - Zeigt die Bibliotheken als auswählbare Liste - Laedt nur die ausgewaehlten ZIPs herunter - Entpackt jede ZIP-Datei - Prueft die mitgelieferten Installationsskripte per SHA256 gegen bekannte kompatible Versionen - Ruft die originalen Planmeca/Hersteller-Batchdateien auf, statt deren SQL-Logik nachzubauen Wichtig: - Am Romexis-Server ausfuehren. - Romexis 3D Implant Lizenz wird von Planmeca vorausgesetzt. - Erstellt optional vor der Installation ein SQL-Backup der Romexis-Datenbank. - Dieses Skript ist ein Orchestrator; die eigentliche Installation bleibt bei den Originalskripten. #> # WinForms reicht hier aus, WPF waere etwas zu gross dafuer Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing Add-Type -AssemblyName System.Web Add-Type -AssemblyName System.IO.Compression.FileSystem # bei Fehler erstmal hart abbrechen $ErrorActionPreference = 'Stop' try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 if ([enum]::GetNames([Net.SecurityProtocolType]) -contains 'Tls13') { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls13 } } catch { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } # Standardwerte, koennen in der GUI noch angepasst werden $DefaultLibraryUrl = 'https://www.planmeca.com/dental-software/planmeca-romexis/3d-implantology-software/implant-library/' $DefaultCacheDir = Join-Path $env:ProgramData 'RomexisImplantLibraryCache' # Hashes aus Bekannten install script, bei Änderungen muss wieder geprüft werden $KnownHashes = @{ 'Install_implant.bat' = @( '8409D680D78A51C194CBCA7EC4B69F8EAC49E63B4BCF515C55D0A170BC385599' ) 'Install_script.bat' = @( '1A7FD946D555620260D6D58FDFF0E3F623684AE836FA496F6E8E7482286D41E4' ) } # kleines Logfenster unten in der Maske function Write-UiLog { param([string]$Text) $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $script:txtLog.AppendText("[$timestamp] $Text`r`n") $script:txtLog.SelectionStart = $script:txtLog.Text.Length $script:txtLog.ScrollToCaret() [System.Windows.Forms.Application]::DoEvents() } function Select-Folder { param([string]$Description) $dlg = New-Object System.Windows.Forms.FolderBrowserDialog $dlg.Description = $Description $dlg.ShowNewFolderButton = $true if ($dlg.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { return $dlg.SelectedPath } return $null } function Clean-HtmlText { param([string]$Html) if ([string]::IsNullOrWhiteSpace($Html)) { return '' } $text = [regex]::Replace($Html, '<[^>]+>', ' ') $text = [System.Web.HttpUtility]::HtmlDecode($text) $text = [regex]::Replace($text, '\s+', ' ').Trim() return $text } function Resolve-Url { param([string]$BaseUrl, [string]$Href) if ($Href -match '^https?://') { return $Href } $base = [Uri]$BaseUrl return ([Uri]::new($base, $Href)).AbsoluteUri } # Seite auslesen und die Downloadlinks rausfischen # nicht schoen, aber aktuell stabil genug function Get-PlanmecaLibraries { param( [string]$Url ) $response = Invoke-WebRequest -Uri $Url -UseBasicParsing $html = $response.Content # Planmeca baut die Liste aktuell mit h3 + Download link auf $pattern = '(?s)

\s*(?.*?)\s*

.*?]+href="(?https://content\.planmeca\.com/implantlibrary/[^"]+?\.zip)"[^>]*>\s*Download library\s*' $items = @() foreach ($m in [regex]::Matches($html, $pattern)) { $name = [System.Net.WebUtility]::HtmlDecode($m.Groups['name'].Value).Trim() $downloadUrl = [System.Net.WebUtility]::HtmlDecode($m.Groups['url'].Value).Trim() if (-not [string]::IsNullOrWhiteSpace($name) -and -not [string]::IsNullOrWhiteSpace($downloadUrl)) { Write-UiLog "Gefunden: $name" Write-UiLog "URL: $downloadUrl" $items += [pscustomobject]@{ Name = $name Url = $downloadUrl } } } $items | Sort-Object Name -Unique } function Get-SafeFileName { param([string]$Name) $safe = $Name -replace '[\\/:*?"<>|]', '_' $safe = $safe -replace '\s+', '_' return $safe.Trim('_') } # fuer Cache/Update pruefung, wenn Planmeca die ZIP ersetzt function Get-RemoteFileInfo { param([string]$Url) $request = [System.Net.HttpWebRequest]::Create($Url) $request.Method = 'HEAD' $request.UserAgent = 'Mozilla/5.0' $response = $request.GetResponse() try { return [pscustomobject]@{ Url = $Url ETag = $response.Headers['ETag'] LastModified = $response.LastModified.ToString('o') ContentLength = [int64]$response.ContentLength } } finally { $response.Close() } } function Get-CacheMetaPath { param([string]$ZipPath) return "$ZipPath.meta.json" } function Read-CacheMeta { param([string]$ZipPath) $metaPath = Get-CacheMetaPath -ZipPath $ZipPath if (-not (Test-Path -LiteralPath $metaPath)) { return $null } return Get-Content -LiteralPath $metaPath -Raw | ConvertFrom-Json } function Write-CacheMeta { param( [string]$ZipPath, [object]$RemoteInfo ) $metaPath = Get-CacheMetaPath -ZipPath $ZipPath $meta = [pscustomobject]@{ Url = $RemoteInfo.Url ETag = $RemoteInfo.ETag LastModified = $RemoteInfo.LastModified ContentLength = $RemoteInfo.ContentLength DownloadedAt = (Get-Date).ToString('o') Sha256 = (Get-FileHash -LiteralPath $ZipPath -Algorithm SHA256).Hash } $meta | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $metaPath -Encoding UTF8 } # Cache nur verwenden wenn Groesse/Datum/ETag noch passen function Test-CacheIsCurrent { param( [string]$ZipPath, [object]$RemoteInfo ) if (-not (Test-Path -LiteralPath $ZipPath)) { return $false } $localFile = Get-Item -LiteralPath $ZipPath if ($localFile.Length -le 0) { return $false } $meta = Read-CacheMeta -ZipPath $ZipPath if (-not $meta) { return $false } if ($RemoteInfo.ContentLength -gt 0 -and [int64]$meta.ContentLength -ne [int64]$RemoteInfo.ContentLength) { return $false } if ($RemoteInfo.ETag -and $meta.ETag -and $RemoteInfo.ETag -ne $meta.ETag) { return $false } if ($RemoteInfo.LastModified -and $meta.LastModified -and $RemoteInfo.LastModified -ne $meta.LastModified) { return $false } return $true } # eigener Download, damit der Balken in der GUI was tut function Download-FileWithProgress { param( [string]$Url, [string]$TargetFile, [string]$DisplayName ) $request = [System.Net.HttpWebRequest]::Create($Url) $request.UserAgent = 'Mozilla/5.0' $response = $request.GetResponse() try { $totalBytes = $response.ContentLength $inputStream = $response.GetResponseStream() $outputStream = [System.IO.File]::Create($TargetFile) try { $buffer = New-Object byte[] 1048576 $totalRead = 0 while (($read = $inputStream.Read($buffer, 0, $buffer.Length)) -gt 0) { $outputStream.Write($buffer, 0, $read) $totalRead += $read if ($totalBytes -gt 0) { $percent = [int](($totalRead / $totalBytes) * 100) $script:progressBar.Value = [Math]::Min(100, $percent) $script:lblProgress.Text = "Download $DisplayName - $percent% ($([math]::Round($totalRead / 1MB, 1)) / $([math]::Round($totalBytes / 1MB, 1)) MB)" } else { $script:progressBar.Style = 'Marquee' $script:lblProgress.Text = "Download $DisplayName - $([math]::Round($totalRead / 1MB, 1)) MB" } [System.Windows.Forms.Application]::DoEvents() } } finally { $outputStream.Close() $inputStream.Close() } } finally { $response.Close() $script:progressBar.Style = 'Blocks' } } # Expand-Archive hat keine vernuenftige Anzeige, daher hier per ZIP API function Expand-ZipWithProgress { param( [string]$ZipPath, [string]$DestinationPath, [string]$DisplayName ) $zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath) try { $entries = @($zip.Entries) $total = $entries.Count $current = 0 foreach ($entry in $entries) { $current++ $percent = [int](($current / $total) * 100) $script:progressBar.Value = [Math]::Min(100, $percent) $script:lblProgress.Text = "Entpacke $DisplayName - $percent% ($current / $total Dateien)" [System.Windows.Forms.Application]::DoEvents() $targetPath = Join-Path $DestinationPath $entry.FullName $targetDir = Split-Path $targetPath -Parent if (-not (Test-Path -LiteralPath $targetDir)) { New-Item -ItemType Directory -Path $targetDir -Force | Out-Null } if ($entry.FullName.EndsWith('/')) { continue } [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $targetPath, $true) } } finally { $zip.Dispose() } } # Download mit lokalem Cache # alte ZIPs werden ersetzt sobald die Online Datei anders aussieht function Download-LibraryZip { param( [pscustomobject]$Library, [string]$CacheDir ) if ([string]::IsNullOrWhiteSpace($Library.Url)) { throw "Keine Download-URL fuer Bibliothek '$($Library.Name)' vorhanden." } New-Item -ItemType Directory -Path $CacheDir -Force | Out-Null $uri = [Uri]$Library.Url $leaf = [System.IO.Path]::GetFileName($uri.AbsolutePath) if ([string]::IsNullOrWhiteSpace($leaf) -or $leaf -notmatch '\.zip$') { $leaf = (Get-SafeFileName $Library.Name) + '.zip' } $target = Join-Path $CacheDir $leaf $tmpFile = "$target.download" Write-UiLog "Download: $($Library.Name)" Write-UiLog "URL: $($Library.Url)" Write-UiLog "Ziel: $target" Write-UiLog "Prüfe Online-Version..." $remoteInfo = Get-RemoteFileInfo -Url $Library.Url Write-UiLog "Remote Größe: $($remoteInfo.ContentLength) Bytes" Write-UiLog "Remote Last-Modified: $($remoteInfo.LastModified)" if ($remoteInfo.ETag) { Write-UiLog "Remote ETag: $($remoteInfo.ETag)" } if (Test-CacheIsCurrent -ZipPath $target -RemoteInfo $remoteInfo) { $existingSize = (Get-Item -LiteralPath $target).Length Write-UiLog "ZIP im Cache ist aktuell: $leaf ($existingSize Bytes)" return $target } if (Test-Path -LiteralPath $target) { Write-UiLog "Cache veraltet oder ohne Metadaten, lade neu: $leaf" Remove-Item -LiteralPath $target -Force } $oldMeta = Get-CacheMetaPath -ZipPath $target if (Test-Path -LiteralPath $oldMeta) { Remove-Item -LiteralPath $oldMeta -Force } if (Test-Path -LiteralPath $tmpFile) { Remove-Item -LiteralPath $tmpFile -Force } try { Write-UiLog "Starte Download..." $script:progressBar.Value = 0 $script:lblProgress.Text = "Download $($Library.Name) wird gestartet..." Download-FileWithProgress ` -Url $Library.Url ` -TargetFile $tmpFile ` -DisplayName $Library.Name Write-UiLog "Download abgeschlossen." $script:progressBar.Value = 100 $script:lblProgress.Text = "Download $($Library.Name) abgeschlossen." } catch { $script:progressBar.Value = 0 $script:lblProgress.Text = "Download fehlgeschlagen." throw "Download fehlgeschlagen fuer '$($Library.Name)': $($_.Exception.Message)" } if (-not (Test-Path -LiteralPath $tmpFile)) { throw "Download fehlgeschlagen. Temporäre Datei wurde nicht erzeugt: $tmpFile" } $fileSize = (Get-Item -LiteralPath $tmpFile).Length if ($fileSize -le 0) { Remove-Item -LiteralPath $tmpFile -Force -ErrorAction SilentlyContinue throw "Download fehlgeschlagen. ZIP-Datei ist leer: $tmpFile" } Write-UiLog "Download abgeschlossen, verschiebe Datei in Cache..." Move-Item -LiteralPath $tmpFile -Destination $target -Force Write-UiLog "Datei im Cache gespeichert." if (-not (Test-Path -LiteralPath $target)) { throw "Download wurde nicht korrekt abgeschlossen. Datei fehlt: $target" } Write-CacheMeta -ZipPath $target -RemoteInfo $remoteInfo $zipHash = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash Write-UiLog "Download erfolgreich: $leaf ($fileSize Bytes)" Write-UiLog "ZIP SHA256: $zipHash" Write-UiLog "Cache-Metadaten gespeichert: $(Get-CacheMetaPath -ZipPath $target)" return $target } function Get-Sha256 { param([string]$Path) if (-not (Test-Path -LiteralPath $Path)) { return $null } return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToUpperInvariant() } function Convert-ManufacturerNameFromFolder { param([string]$FolderName, [string]$FallbackName) if (-not [string]::IsNullOrWhiteSpace($FallbackName)) { return $FallbackName } $name = $FolderName -replace '_', ' ' return (Get-Culture).TextInfo.ToTitleCase($name.ToLowerInvariant()) } function Find-ImplantRoot { param([string]$WorkDir) return Get-ChildItem -LiteralPath $WorkDir -Directory -Recurse -Filter 'Implant_library_files' | Select-Object -First 1 } function Get-SqlServerFiles { param([string]$ManufacturerDir) $sqlFiles = @(Get-ChildItem -LiteralPath $ManufacturerDir -Recurse -File -Filter '*sqlsrv.sql' | Select-Object -ExpandProperty FullName) if ($sqlFiles.Count -eq 0) { $sqlFiles = @(Get-ChildItem -LiteralPath $ManufacturerDir -Recurse -File -Filter '*.sql' | Where-Object { $_.Name -notmatch 'fb\.sql$' } | Select-Object -ExpandProperty FullName) } return @($sqlFiles) } # Romexis Installationspfad aus Registry holen # die zweite GUID ist fuer alte Installationen drin geblieben function Get-RomexisInstallDir { $guids = @( '{B77EAE13-6B8D-477C-93F1-9C9A9ABA4355}', '{A9256EA8-FAD2-4B23-90A9-B78CD122C0BF}' ) foreach ($guid in $guids) { $key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$guid" $keyWow = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$guid" foreach ($path in @($key, $keyWow)) { if (Test-Path $path) { $installDir = (Get-ItemProperty $path).InstallLocation if ($installDir -and (Test-Path $installDir)) { return $installDir.TrimEnd('\') } } } } throw 'Romexis Installationsverzeichnis wurde nicht gefunden.' } # DB Daten aus Romexis Config lesen # moeglichst nah an den original Batchscripten function Get-RomexisSqlConfig { param( [string]$RomexisInstallDir ) $propsFile = Join-Path $RomexisInstallDir 'sconfig\romexis_server.properties' if (-not (Test-Path -LiteralPath $propsFile)) { throw "Romexis Server-Konfiguration nicht gefunden: $propsFile" } $props = @{} Get-Content -LiteralPath $propsFile | ForEach-Object { $line = $_.Trim() if ($line -eq '' -or $line.StartsWith('#')) { return } $idx = $line.IndexOf('=') if ($idx -lt 1) { return } $key = $line.Substring(0, $idx).Trim() $value = $line.Substring($idx + 1).Trim() $props[$key] = $value } $dbType = $props['SERVER_DB'] $dbUrl = $props['SERVER_DB_URL'] $uid = $props['SERVER_DB_UID'] $pwd = $props['SERVER_DB_PWD'] if ([string]::IsNullOrWhiteSpace($pwd)) { $pwd = 'romexis' } $instance = $null $database = $null if ($dbType -eq '3') { # jTDS: # jdbc:jtds:sqlserver://SERVER/DB;instance=ROMEXIS if ($dbUrl -match 'sqlserver://([^/;]+)/([^;]+)') { $server = $Matches[1] $database = $Matches[2] } if ($dbUrl -match 'instance=([^;]+)') { $instance = "$server\$($Matches[1])" } else { $instance = $server } } elseif ($dbType -eq '5') { # Microsoft JDBC: # jdbc:sqlserver://SERVER\INSTANCE;databaseName=romexis_db if ($dbUrl -match 'sqlserver://([^;]+)') { $instance = $Matches[1] } if ($dbUrl -match 'databaseName=([^;]+)') { $database = $Matches[1] } } else { throw "Unbekannter Romexis SQL-Verbindungstyp SERVER_DB=$dbType" } if ([string]::IsNullOrWhiteSpace($instance)) { throw "SQL Server/Instanz konnte aus SERVER_DB_URL nicht gelesen werden: $dbUrl" } if ([string]::IsNullOrWhiteSpace($database)) { throw "SQL Datenbank konnte aus SERVER_DB_URL nicht gelesen werden: $dbUrl" } if ([string]::IsNullOrWhiteSpace($uid)) { throw "SQL Benutzer konnte nicht gelesen werden." } [pscustomobject]@{ Instance = $instance Database = $database User = $uid Password = $pwd DbType = $dbType DbUrl = $dbUrl } } # Java aus der Romexis Installation suchen function Get-RomexisJavaPath { param([string]$RomexisInstallDir) $candidates = @() if ($env:PROCESSOR_ARCHITECTURE -eq 'x86') { $candidates += Join-Path $RomexisInstallDir 'tools\jre_x86\bin\java.exe' } else { $candidates += Join-Path $RomexisInstallDir 'tools\jre_x64\bin\java.exe' } $candidates += Join-Path $RomexisInstallDir 'tools\jre\bin\java.exe' foreach ($candidate in $candidates) { if (Test-Path -LiteralPath $candidate) { return $candidate } } return $null } function Get-RomexisVersion { param([string]$RomexisInstallDir) $java = Get-RomexisJavaPath -RomexisInstallDir $RomexisInstallDir $jar = Join-Path $RomexisInstallDir 'server\RomexisServer.jar' if (-not $java -or -not (Test-Path -LiteralPath $jar)) { return 'unknown' } try { $old = Get-Location Set-Location -LiteralPath (Split-Path $jar -Parent) $out = & $java -jar 'RomexisServer.jar' -version 2>&1 | Out-String Set-Location $old if ($out -match '([0-9]+)\.([0-9]+)') { return "$($Matches[1])$($Matches[2])" } } catch { try { Set-Location $old } catch {} } return 'unknown' } function Request-PasswordDialog { param([string]$Title, [string]$Prompt) $dlg = New-Object System.Windows.Forms.Form $dlg.Text = $Title $dlg.Size = New-Object System.Drawing.Size(430, 160) $dlg.StartPosition = 'CenterParent' $dlg.FormBorderStyle = 'FixedDialog' $dlg.MaximizeBox = $false $dlg.MinimizeBox = $false $lbl = New-Object System.Windows.Forms.Label $lbl.Text = $Prompt $lbl.Location = New-Object System.Drawing.Point(12, 15) $lbl.Size = New-Object System.Drawing.Size(390, 22) $dlg.Controls.Add($lbl) $txt = New-Object System.Windows.Forms.TextBox $txt.Location = New-Object System.Drawing.Point(15, 43) $txt.Size = New-Object System.Drawing.Size(385, 24) $txt.UseSystemPasswordChar = $true $dlg.Controls.Add($txt) $ok = New-Object System.Windows.Forms.Button $ok.Text = 'OK' $ok.Location = New-Object System.Drawing.Point(244, 80) $ok.Size = New-Object System.Drawing.Size(75, 28) $ok.DialogResult = [System.Windows.Forms.DialogResult]::OK $dlg.AcceptButton = $ok $dlg.Controls.Add($ok) $cancel = New-Object System.Windows.Forms.Button $cancel.Text = 'Abbrechen' $cancel.Location = New-Object System.Drawing.Point(325, 80) $cancel.Size = New-Object System.Drawing.Size(75, 28) $cancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel $dlg.CancelButton = $cancel $dlg.Controls.Add($cancel) if ($dlg.ShowDialog($form) -eq [System.Windows.Forms.DialogResult]::OK) { return $txt.Text } return $null } function Invoke-SqlcmdWithCredential { param( [object]$SqlConfig, [object]$Credential, [string]$Database, [string]$Query ) $args = @('-S', $SqlConfig.Instance) $args += $Credential.Args if (-not [string]::IsNullOrWhiteSpace($Database)) { $args += @('-d', $Database) } # WICHTIG: # Kein "-r 1", weil SQL Server auch Erfolgsmeldungen wie # "Processed xxxx pages..." ausgibt. Mit "-r 1" landen diese im Fehlerstrom. $args += @('-b', '-Q', $Query) $oldErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { $output = & sqlcmd @args 2>&1 $exitCode = $LASTEXITCODE } finally { $ErrorActionPreference = $oldErrorActionPreference } [pscustomobject]@{ ExitCode = $exitCode Output = ($output | Out-String) } } function Get-SqlCredentialCandidates { param([object]$SqlConfig, [string]$CustomSaPassword) $candidates = @() $candidates += [pscustomobject]@{ Name = 'Windows Authentifizierung' Args = @('-E') } if (-not [string]::IsNullOrWhiteSpace($SqlConfig.User) -and -not [string]::IsNullOrWhiteSpace($SqlConfig.Password)) { $candidates += [pscustomobject]@{ Name = 'Romexis DB Benutzer' Args = @('-U', $SqlConfig.User, '-P', $SqlConfig.Password) } } if (-not [string]::IsNullOrWhiteSpace($CustomSaPassword)) { $candidates += [pscustomobject]@{ Name = 'SQL sa Benutzer (eingegeben)' Args = @('-U', 'sa', '-P', $CustomSaPassword) } } $candidates += [pscustomobject]@{ Name = 'SQL sa Standard 1' Args = @('-U', 'sa', '-P', 'pwr0mex!s') } $candidates += [pscustomobject]@{ Name = 'SQL sa Standard 2' Args = @('-U', 'sa', '-P', 'Pwr0mex!s!!!') } return @($candidates) } function Get-WorkingSqlCredential { param([object]$SqlConfig) $testQuery = "SET NOCOUNT ON;SELECT 'Database connection OK'" foreach ($cred in (Get-SqlCredentialCandidates -SqlConfig $SqlConfig)) { $res = Invoke-SqlcmdWithCredential -SqlConfig $SqlConfig -Credential $cred -Database $SqlConfig.Database -Query $testQuery if ($res.ExitCode -eq 0) { Write-UiLog "SQL-Zugriff OK mit: $($cred.Name)" return $cred } } $custom = Request-PasswordDialog -Title 'SQL sa Passwort' -Prompt 'SQL sa Passwort eingeben, falls Standardzugänge nicht funktionieren:' if (-not [string]::IsNullOrWhiteSpace($custom)) { foreach ($cred in (Get-SqlCredentialCandidates -SqlConfig $SqlConfig -CustomSaPassword $custom)) { if ($cred.Name -ne 'SQL sa Benutzer (eingegeben)') { continue } $res = Invoke-SqlcmdWithCredential -SqlConfig $SqlConfig -Credential $cred -Database $SqlConfig.Database -Query $testQuery if ($res.ExitCode -eq 0) { Write-UiLog "SQL-Zugriff OK mit: $($cred.Name)" return $cred } } } throw 'Kein funktionierender SQL-Zugang gefunden. Backup wurde nicht erstellt.' } function Invoke-SqlScalar { param( [object]$SqlConfig, [object]$Credential, [string]$Database, [string]$Query ) $args = @('-S', $SqlConfig.Instance) $args += $Credential.Args if (-not [string]::IsNullOrWhiteSpace($Database)) { $args += @('-d', $Database) } $args += @('-h', '-1', '-W', '-Q', $Query) $output = & sqlcmd @args 2>&1 if ($LASTEXITCODE -ne 0) { throw "SQL-Abfrage fehlgeschlagen: $($output | Out-String)" } foreach ($line in $output) { $value = ($line.ToString()).Trim() if ($value -and $value -notmatch '^\([0-9]+ rows? affected\)$') { return $value } } return $null } function Escape-SqlLiteral { param([string]$Value) return $Value -replace "'", "''" } function Escape-SqlName { param([string]$Value) return $Value -replace ']', ']]' } # Sicherheitsbackup vor DB Aenderungen # kein Ersatz fuer das normale Backupscript function New-RomexisDatabaseBackup { param( [string]$RomexisInstallDir, [object]$SqlConfig ) if (-not (Get-Command sqlcmd -ErrorAction SilentlyContinue)) { throw 'sqlcmd wurde nicht gefunden. Bitte SQL Server Command Line Tools installieren oder PATH prüfen.' } Write-UiLog 'Prüfe SQL-Zugriff für Backup...' $cred = Get-WorkingSqlCredential -SqlConfig $SqlConfig $romexisVersion = Get-RomexisVersion -RomexisInstallDir $RomexisInstallDir Write-UiLog "Romexis Version fuer Backup-Dateiname: $romexisVersion" $imageDir = Invoke-SqlScalar -SqlConfig $SqlConfig -Credential $cred -Database $SqlConfig.Database -Query 'SET NOCOUNT ON;SELECT param_value FROM RBA_Server_Param_S WHERE param_number=1' if ([string]::IsNullOrWhiteSpace($imageDir)) { throw 'Romexis Image-Verzeichnis konnte nicht aus der Datenbank gelesen werden.' } $imageDir = $imageDir -replace '/', '\' Write-UiLog "Romexis Image Dir: $imageDir" $backupDir = Join-Path $imageDir 'Backup' if (-not (Test-Path -LiteralPath $backupDir)) { New-Item -ItemType Directory -Path $backupDir -Force | Out-Null } $dateTime = Get-Date -Format 'yyyyMMddHHmmss' $backupFile = Join-Path $backupDir ("$dateTime#$($SqlConfig.Database)#$romexisVersion#.BAK") Write-UiLog "Erstelle Datenbankbackup: $backupFile" $dbName = Escape-SqlName $SqlConfig.Database $backupPath = Escape-SqlLiteral $backupFile $query = "BACKUP DATABASE [$dbName] TO DISK=N'$backupPath';" $res = Invoke-SqlcmdWithCredential -SqlConfig $SqlConfig -Credential $cred -Database '' -Query $query if (-not [string]::IsNullOrWhiteSpace($res.Output)) { Write-UiLog ($res.Output.Trim()) } if ($res.ExitCode -ne 0) { throw "Backup fehlgeschlagen. sqlcmd ExitCode: $($res.ExitCode)" } if (-not (Test-Path -LiteralPath $backupFile)) { throw "Backup wurde laut sqlcmd ausgeführt, aber die Datei wurde nicht gefunden: $backupFile" } Write-UiLog "Backup erstellt: $backupFile" return $backupFile } # Geometrie Dateien kopieren # manche Hersteller haben implants und sleeves, andere nur eins davon function Copy-LibraryFiles { param( [string]$ManufacturerDir, [string]$RomexisInstallDir ) $manufacturerFolder = Split-Path $ManufacturerDir -Leaf $copyJobs = @( [pscustomobject]@{ Name = 'Implantatdateien' Source = Join-Path $ManufacturerDir 'implants\files' Target = Join-Path $RomexisInstallDir "geometries\implants\$manufacturerFolder" }, [pscustomobject]@{ Name = 'Sleeve-Dateien' Source = Join-Path $ManufacturerDir 'sleeves\files' Target = Join-Path $RomexisInstallDir "geometries\sleeves\$manufacturerFolder" } ) foreach ($job in $copyJobs) { if (-not (Test-Path -LiteralPath $job.Source)) { Write-UiLog "$($job.Name) nicht vorhanden, überspringe: $($job.Source)" continue } Write-UiLog "Kopiere $($job.Name):" Write-UiLog "Quelle: $($job.Source)" Write-UiLog "Ziel: $($job.Target)" New-Item -ItemType Directory -Path $job.Target -Force | Out-Null Copy-Item -LiteralPath (Join-Path $job.Source '*') -Destination $job.Target -Recurse -Force } } function Quote-CmdArg { param([string]$Value) if ($null -eq $Value) { return '""' } return '"' + ($Value -replace '"', '\"') + '"' } # Wichtiger Teil: # SQL Import nicht selbst nachbauen, sondern Hersteller Batch verwenden function Invoke-OriginalInstaller { param( [string]$ImplantRoot, [string]$ManufacturerDir, [string]$ManufacturerName, [string[]]$SqlFiles, [string]$DbUser, [string]$DbPassword, [bool]$AllowUnknownScripts ) $installImplant = Join-Path $ImplantRoot 'Install_implant.bat' $installScript = Join-Path $ImplantRoot 'Install_script.bat' if (-not (Test-Path -LiteralPath $installImplant)) { throw 'Install_implant.bat fehlt.' } $hash1 = Get-Sha256 $installImplant $hash2 = Get-Sha256 $installScript $hashOk = ( ($hash1 -and $KnownHashes['Install_implant.bat'] -contains $hash1) -or ($hash2 -and $KnownHashes['Install_script.bat'] -contains $hash2) ) Write-UiLog "Install_implant.bat: $installImplant" Write-UiLog "Install_implant SHA256: $hash1" if ($hash2) { Write-UiLog "Install_script.bat SHA256: $hash2" } if (-not $hashOk -and -not $AllowUnknownScripts) { throw "Unbekannte Installer-Skriptversion. Hashes: Install_implant=$hash1 Install_script=$hash2" } if (-not $hashOk) { Write-UiLog "WARNUNG: unbekannte Skript-Hashes erlaubt: $ManufacturerName" } if (-not $SqlFiles -or $SqlFiles.Count -eq 0) { throw "Keine SQL-Server-SQL-Datei gefunden für $ManufacturerName." } Write-UiLog "Starte Originalskript fuer: $ManufacturerName" Write-UiLog "SQL-Dateien fuer $ManufacturerName :" foreach ($sqlFile in $SqlFiles) { Write-UiLog " SQL: $sqlFile" if (-not (Test-Path -LiteralPath $sqlFile)) { throw "SQL-Datei existiert nicht: $sqlFile" } } $cmdParts = @() $cmdParts += 'call' $cmdParts += (Quote-CmdArg $installImplant) $cmdParts += (Quote-CmdArg $DbUser) $cmdParts += (Quote-CmdArg $DbPassword) $cmdParts += (Quote-CmdArg $ManufacturerName) foreach ($sqlFile in $SqlFiles) { $cmdParts += (Quote-CmdArg $sqlFile) } $cmdLine = $cmdParts -join ' ' Write-UiLog "Arbeitsverzeichnis: $ImplantRoot" Write-UiLog "CMD: $cmdLine" Push-Location -LiteralPath $ImplantRoot try { $output = & cmd.exe /d /s /c $cmdLine 2>&1 $exitCode = $LASTEXITCODE } finally { Pop-Location } if ($output) { Write-UiLog "Ausgabe Originalskript:" foreach ($line in $output) { $text = $line.ToString().TrimEnd() if (-not [string]::IsNullOrWhiteSpace($text)) { Write-UiLog " $text" } } } Write-UiLog "Originalskript ExitCode: $exitCode" if ($exitCode -ne 0) { throw "Originalskript beendet mit ExitCode $exitCode." } } # Ablauf fuer eine Bibliothek: holen, entpacken, kopieren, SQL import function Install-Library { param( [pscustomobject]$Library, [string]$CacheDir, [object]$SqlConfig, [bool]$AllowUnknownScripts, [bool]$CopyFiles ) $zip = Download-LibraryZip -Library $Library -CacheDir $CacheDir Write-UiLog "Verwende ZIP: $zip" if (-not (Test-Path -LiteralPath $zip)) { throw "ZIP-Datei wurde nicht gefunden: $zip" } $work = Join-Path $env:TEMP ('RomexisOnlineInstall_' + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $work | Out-Null try { Write-UiLog "Entpacke: $($Library.Name)" $script:progressBar.Value = 0 $script:lblProgress.Text = "Entpacke $($Library.Name) ..." Expand-ZipWithProgress ` -ZipPath $zip ` -DestinationPath $work ` -DisplayName $Library.Name $script:progressBar.Value = 100 $script:lblProgress.Text = "Entpacken $($Library.Name) abgeschlossen." $root = Find-ImplantRoot $work if (-not $root) { throw 'Implant_library_files nicht gefunden.' } $manufacturerDirs = @(Get-ChildItem -LiteralPath $root.FullName -Directory | Where-Object { $_.Name -notin @('scripts') }) if ($manufacturerDirs.Count -eq 0) { throw 'Kein Herstellerordner unter Implant_library_files gefunden.' } foreach ($manufacturerDir in $manufacturerDirs) { $manufacturerName = Convert-ManufacturerNameFromFolder -FolderName $manufacturerDir.Name -FallbackName $Library.Name $sqlFiles = @(Get-SqlServerFiles -ManufacturerDir $manufacturerDir.FullName) Write-UiLog "Herstellerordner: $($manufacturerDir.FullName)" Write-UiLog "Gefundene SQL-Dateien: $($sqlFiles.Count)" foreach ($sqlFile in $sqlFiles) { Write-UiLog " gefunden: $sqlFile" } $romexisInstallDir = Get-RomexisInstallDir if ($CopyFiles) { Copy-LibraryFiles -ManufacturerDir $manufacturerDir.FullName -RomexisInstallDir $romexisInstallDir } Invoke-OriginalInstaller -ImplantRoot $root.FullName -ManufacturerDir $manufacturerDir.FullName -ManufacturerName $manufacturerName -SqlFiles $sqlFiles -DbUser $SqlConfig.User -DbPassword $SqlConfig.Password -AllowUnknownScripts $AllowUnknownScripts } Write-UiLog "OK: $($Library.Name)" } finally { Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue } } # ---------------- GUI ---------------- # gewachsene WinForms Maske, nicht huebsch aber ok $form = New-Object System.Windows.Forms.Form $form.Text = 'Romexis Implant Library Online Installer' $form.Size = New-Object System.Drawing.Size(1080, 760) $form.StartPosition = 'CenterScreen' $lblUrl = New-Object System.Windows.Forms.Label $lblUrl.Text = 'Planmeca URL:' $lblUrl.Location = New-Object System.Drawing.Point(12, 15) $lblUrl.Size = New-Object System.Drawing.Size(100, 22) $form.Controls.Add($lblUrl) $txtUrl = New-Object System.Windows.Forms.TextBox $txtUrl.Text = $DefaultLibraryUrl $txtUrl.Location = New-Object System.Drawing.Point(115, 12) $txtUrl.Size = New-Object System.Drawing.Size(770, 24) $form.Controls.Add($txtUrl) $btnLoad = New-Object System.Windows.Forms.Button $btnLoad.Text = 'Liste laden' $btnLoad.Location = New-Object System.Drawing.Point(895, 10) $btnLoad.Size = New-Object System.Drawing.Size(150, 28) $form.Controls.Add($btnLoad) $lblCache = New-Object System.Windows.Forms.Label $lblCache.Text = 'Download-Cache:' $lblCache.Location = New-Object System.Drawing.Point(12, 50) $lblCache.Size = New-Object System.Drawing.Size(100, 22) $form.Controls.Add($lblCache) $txtCache = New-Object System.Windows.Forms.TextBox $txtCache.Text = $DefaultCacheDir $txtCache.Location = New-Object System.Drawing.Point(115, 47) $txtCache.Size = New-Object System.Drawing.Size(770, 24) $form.Controls.Add($txtCache) $btnCache = New-Object System.Windows.Forms.Button $btnCache.Text = 'Auswählen' $btnCache.Location = New-Object System.Drawing.Point(895, 45) $btnCache.Size = New-Object System.Drawing.Size(150, 28) $form.Controls.Add($btnCache) $list = New-Object System.Windows.Forms.CheckedListBox $list.Location = New-Object System.Drawing.Point(12, 85) $list.Size = New-Object System.Drawing.Size(1033, 300) $list.CheckOnClick = $true $form.Controls.Add($list) $btnAll = New-Object System.Windows.Forms.Button $btnAll.Text = 'Alle auswählen' $btnAll.Location = New-Object System.Drawing.Point(12, 392) $btnAll.Size = New-Object System.Drawing.Size(130, 28) $form.Controls.Add($btnAll) $btnNone = New-Object System.Windows.Forms.Button $btnNone.Text = 'Alle abwählen' $btnNone.Location = New-Object System.Drawing.Point(150, 392) $btnNone.Size = New-Object System.Drawing.Size(130, 28) $form.Controls.Add($btnNone) $chkUnknown = New-Object System.Windows.Forms.CheckBox $chkUnknown.Text = 'Unbekannte Skript-Hashes erlauben' $chkUnknown.Location = New-Object System.Drawing.Point(12, 433) $chkUnknown.Size = New-Object System.Drawing.Size(320, 22) $form.Controls.Add($chkUnknown) $chkCopy = New-Object System.Windows.Forms.CheckBox $chkCopy.Text = 'Implantatdateien automatisch nach Romexis kopieren' $chkCopy.Location = New-Object System.Drawing.Point(12, 470) $chkCopy.Size = New-Object System.Drawing.Size(420, 22) $chkCopy.Checked = $true $form.Controls.Add($chkCopy) $chkBackup = New-Object System.Windows.Forms.CheckBox $chkBackup.Text = 'Vor Installation automatisch Romexis-Datenbankbackup erstellen' $chkBackup.Location = New-Object System.Drawing.Point(455, 470) $chkBackup.Size = New-Object System.Drawing.Size(420, 22) $chkBackup.Checked = $true $form.Controls.Add($chkBackup) $btnInstall = New-Object System.Windows.Forms.Button $btnInstall.Text = 'Ausgewählte downloaden und installieren' $btnInstall.Location = New-Object System.Drawing.Point(12, 505) $btnInstall.Size = New-Object System.Drawing.Size(250, 34) $form.Controls.Add($btnInstall) $btnDownloadOnly = New-Object System.Windows.Forms.Button $btnDownloadOnly.Text = 'Ausgewählte nur downloaden' $btnDownloadOnly.Location = New-Object System.Drawing.Point(270, 505) $btnDownloadOnly.Size = New-Object System.Drawing.Size(130, 34) $form.Controls.Add($btnDownloadOnly) $progressBar = New-Object System.Windows.Forms.ProgressBar $progressBar.Location = New-Object System.Drawing.Point(420, 510) $progressBar.Size = New-Object System.Drawing.Size(575, 22) $progressBar.Minimum = 0 $progressBar.Maximum = 100 $progressBar.Value = 0 $form.Controls.Add($progressBar) $script:progressBar = $progressBar $lblProgress = New-Object System.Windows.Forms.Label $lblProgress.Text = 'Bereit.' $lblProgress.Location = New-Object System.Drawing.Point(420, 535) $lblProgress.Size = New-Object System.Drawing.Size(575, 18) $form.Controls.Add($lblProgress) $script:lblProgress = $lblProgress $txtLog = New-Object System.Windows.Forms.TextBox $txtLog.Location = New-Object System.Drawing.Point(12, 560) $txtLog.Size = New-Object System.Drawing.Size(1033, 150) $txtLog.Multiline = $true $txtLog.ScrollBars = 'Vertical' $txtLog.ReadOnly = $true $form.Controls.Add($txtLog) $script:txtLog = $txtLog $script:Libraries = @() $btnCache.Add_Click({ $p = Select-Folder 'Download-Cache auswählen' if ($p) { $txtCache.Text = $p } }) # Nur downloaden, z.B. fuer anderen Romexis Server mitnehmen $btnDownloadOnly.Add_Click({ try { $btnDownloadOnly.Enabled = $false $btnInstall.Enabled = $false $form.Cursor = [System.Windows.Forms.Cursors]::WaitCursor $script:progressBar.Value = 0 $script:lblProgress.Text = 'Download wird vorbereitet...' if ($script:Libraries.Count -eq 0) { throw 'Bitte zuerst die Liste laden.' } $selected = @() foreach ($idx in $list.CheckedIndices) { $selected += $script:Libraries[[int]$idx] } if ($selected.Count -eq 0) { throw 'Keine Bibliothek ausgewählt.' } Write-UiLog "Nur-Download gestartet: $($selected.Count) Bibliotheken" foreach ($lib in $selected) { $zip = Download-LibraryZip -Library $lib -CacheDir $txtCache.Text Write-UiLog "Gespeichert: $zip" } $script:progressBar.Value = 100 $script:lblProgress.Text = 'Download abgeschlossen.' Write-UiLog 'Nur-Download fertig.' [System.Windows.Forms.MessageBox]::Show( 'Download abgeschlossen.', 'Fertig', 'OK', 'Information' ) | Out-Null } catch { $script:progressBar.Value = 0 $script:lblProgress.Text = 'Fehler beim Download.' Write-UiLog "FEHLER Nur-Download: $($_.Exception.Message)" [System.Windows.Forms.MessageBox]::Show( $_.Exception.Message, 'Fehler', 'OK', 'Error' ) | Out-Null } finally { $btnDownloadOnly.Enabled = $true $btnInstall.Enabled = $true $form.Cursor = [System.Windows.Forms.Cursors]::Default } }) $btnLoad.Add_Click({ try { $list.Items.Clear() $script:Libraries = @(Get-PlanmecaLibraries -Url $txtUrl.Text) foreach ($lib in $script:Libraries) { $label = $lib.Name if (-not [string]::IsNullOrWhiteSpace($lib.Contains)) { $label += " [$($lib.Contains)]" } [void]$list.Items.Add($label, $false) } Write-UiLog "$($script:Libraries.Count) Bibliotheken gefunden." foreach ($lib in $script:Libraries) { Write-UiLog "$($lib.Name) => $($lib.Url)" } } catch { Write-UiLog "FEHLER beim Laden: $($_.Exception.Message)" [System.Windows.Forms.MessageBox]::Show($_.Exception.Message, 'Fehler beim Laden', 'OK', 'Error') | Out-Null } }) $btnAll.Add_Click({ for ($i = 0; $i -lt $list.Items.Count; $i++) { $list.SetItemChecked($i, $true) } }) $btnNone.Add_Click({ for ($i = 0; $i -lt $list.Items.Count; $i++) { $list.SetItemChecked($i, $false) } }) # Hauptbutton: optional Backup, dann alle gewaehlten Bibliotheken installieren $btnInstall.Add_Click({ try { $btnInstall.Enabled = $false $form.Cursor = [System.Windows.Forms.Cursors]::WaitCursor $script:progressBar.Value = 0 $script:lblProgress.Text = 'Installation wird vorbereitet...' if ($script:Libraries.Count -eq 0) { throw 'Bitte zuerst die Liste laden.' } $romexisInstallDir = Get-RomexisInstallDir $sqlConfig = Get-RomexisSqlConfig -RomexisInstallDir $romexisInstallDir Write-UiLog "Romexis Install Dir: $romexisInstallDir" Write-UiLog "Romexis SQL Instance: $($sqlConfig.Instance)" Write-UiLog "Romexis SQL Database: $($sqlConfig.Database)" Write-UiLog "Romexis SQL User: $($sqlConfig.User)" $selected = @() foreach ($idx in $list.CheckedIndices) { $selected += $script:Libraries[[int]$idx] } if ($selected.Count -eq 0) { throw 'Keine Bibliothek ausgewählt.' } $backupText = if ($chkBackup.Checked) { "Vorher wird automatisch ein Romexis-Datenbankbackup erstellt." } else { "ACHTUNG: Automatisches Backup ist deaktiviert." } $confirm = [System.Windows.Forms.MessageBox]::Show( "Es werden $($selected.Count) Bibliotheken heruntergeladen und am Romexis-Server installiert.`r`n`r`n$backupText`r`n`r`nFortfahren?", 'Installation starten', 'YesNo', 'Warning') if ($confirm -ne [System.Windows.Forms.DialogResult]::Yes) { return } if ($chkBackup.Checked) { $backupFile = New-RomexisDatabaseBackup -RomexisInstallDir $romexisInstallDir -SqlConfig $sqlConfig Write-UiLog "Backup vor Installation erfolgreich: $backupFile" } else { Write-UiLog 'WARNUNG: Installation ohne automatisches Backup gestartet.' } foreach ($lib in $selected) { Install-Library -Library $lib -CacheDir $txtCache.Text -SqlConfig $sqlConfig -AllowUnknownScripts:$chkUnknown.Checked -CopyFiles:$chkCopy.Checked } $script:progressBar.Value = 100 $script:lblProgress.Text = 'Installation abgeschlossen.' Write-UiLog 'Fertig.' [System.Windows.Forms.MessageBox]::Show('Installation abgeschlossen.', 'Fertig', 'OK', 'Information') | Out-Null } catch { $script:progressBar.Value = 0 $script:lblProgress.Text = 'Fehler.' Write-UiLog "FEHLER: $($_.Exception.Message)" [System.Windows.Forms.MessageBox]::Show($_.Exception.Message, 'Fehler', 'OK', 'Error') | Out-Null } finally { $btnInstall.Enabled = $true $form.Cursor = [System.Windows.Forms.Cursors]::Default } }) # beim Start gleich versuchen die Romexis Daten und die Onlineliste zu laden $form.Add_Shown({ try { $romexisInstallDir = Get-RomexisInstallDir $sqlConfig = Get-RomexisSqlConfig -RomexisInstallDir $romexisInstallDir Write-UiLog "Romexis Install Dir: $romexisInstallDir" Write-UiLog "Romexis SQL Instance: $($sqlConfig.Instance)" Write-UiLog "Romexis SQL Database: $($sqlConfig.Database)" Write-UiLog "Romexis SQL User: $($sqlConfig.User)" } catch { Write-UiLog "WARNUNG: Romexis SQL-Konfiguration konnte nicht automatisch gelesen werden: $($_.Exception.Message)" } Write-UiLog 'Lade Bibliotheksliste automatisch...' $btnLoad.PerformClick() }) [void]$form.ShowDialog()