From 42e37e8de7ec997c89b0edb35ef9418a71b78945 Mon Sep 17 00:00:00 2001 From: Patrick Gniza Date: Sun, 31 May 2026 17:35:36 +0200 Subject: [PATCH] Initiale Version des Romexis Implant Library Online Installers - GUI zur Verwaltung von Implantbibliotheken - Automatischer Download von Planmeca - Integration der Original-Herstellerinstaller - Automatisches Romexis-Backup - Download-Cache und Updateerkennung - Fortschrittsanzeige und Protokollierung --- .gitignore | 46 ++ CHANGELOG.md | 40 ++ LICENSE | 21 + README.md | 93 +++ RomexisImplantInstaller.ps1 | 1338 +++++++++++++++++++++++++++++++++++ 5 files changed, 1538 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 RomexisImplantInstaller.ps1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bfa84b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# PowerShell / editor +*.ps1xml +*.psm1 +*.psd1 +*.ps1.user +.vscode/ +.idea/ + +# Lokale Logs / Testausgaben +*.log +log.txt +logs/ +*.tmp +*.temp + +# Downloads / Cache / Herstellerpakete +*.zip +*.download +*.meta.json +RomexisImplantLibraryCache/ +cache/ +downloads/ + +# Entpackte Installationsdateien +temp/ +tmp/ +extracted/ +Implant_library_files/ + +# Backups / Datenbank Dumps +*.bak +*.BAK +*.sql.bak +backup/ +backups/ + +# Windows / Explorer +Thumbs.db +Desktop.ini +$RECYCLE.BIN/ + +# macOS falls das Repo mal dort geöffnet wird +.DS_Store + +# PowerShell Transcripts +PowerShell_transcript*.txt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6c94267 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,40 @@ +# Changelog + +Alle relevanten Änderungen an diesem Projekt werden hier dokumentiert. + +## [0.1.0] - 2026-05-31 + +### Hinzugefügt + +- Erste Repo-Struktur vorbereitet +- README.md erstellt +- LICENSE ergänzt +- `.gitignore` ergänzt +- CHANGELOG.md erstellt +- Beschreibung für PowerShell/WinForms Installer ergänzt +- Hinweise zu Cache, Backup und Romexis-Installation aufgenommen + +### Enthaltene Funktionen im Installer + +- Planmeca Implant Library Webseite parsen +- Bibliotheken in GUI anzeigen +- Ausgewählte Bibliotheken herunterladen +- Nur-Download-Modus +- Cache mit Metadatenprüfung +- Fortschrittsbalken für Download und Entpacken +- Automatische Romexis-Pfaderkennung +- SQL-Konfiguration aus Romexis lesen +- Optionales SQL-Backup vor Installation +- Kopieren von Implantat- und Sleeve-Dateien +- Aufruf der originalen Hersteller-/Planmeca-Batchdateien +- Hashprüfung bekannter Installerskripte + +## [Unreleased] + +### Ideen / offen + +- Hersteller-Suche oder Filter in der GUI +- Optionaler Export der Downloadliste +- Bessere Zusammenfassung nach Installation +- Optionaler Silent-Modus +- Ausführlicher Prüfbericht nach SQL-Import diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..69c6f72 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Patrick Gniza + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..30a594e --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# Romexis Implant Library Online Installer + +Kleines PowerShell/WinForms-Tool zum Herunterladen und Installieren von Planmeca-Romexis-Implantatbibliotheken. + +Das Tool liest die öffentliche Planmeca Implant Library Webseite aus, zeigt die gefundenen Bibliotheken in einer GUI an und kann ausgewählte ZIP-Pakete herunterladen, entpacken und über die originalen mitgelieferten Planmeca-/Hersteller-Skripte installieren. + +## Funktionen + +- Online-Liste der verfügbaren Implantatbibliotheken laden +- Bibliotheken per Checkbox auswählen +- ZIP-Dateien herunterladen und lokal cachen +- Cache per HTTP-Metadaten prüfen (`ETag`, `Last-Modified`, Dateigröße) +- Fortschrittsanzeige für Download und Entpacken +- Nur-Download-Modus für Offline-/Mitnahme-Szenarien +- Automatisches Ermitteln des Romexis-Installationspfads +- Automatisches Lesen der SQL-Konfiguration aus `romexis_server.properties` +- Optionales Datenbankbackup vor Installation +- Kopieren von Implantat- und Sleeve-Geometrien +- Aufruf der originalen `Install_implant.bat` / `Install_script.bat` +- Hashprüfung der bekannten Installerskripte + +## Voraussetzungen + +- Windows +- PowerShell 5.1 oder neuer +- Installiertes Planmeca Romexis +- Ausführung am Romexis-Server dringend empfohlen +- SQL Server Command Line Tools (`sqlcmd`) für Backup und SQL-Zugriff +- Schreibrechte auf: + - `C:\ProgramData\RomexisImplantLibraryCache` + - `C:\Program Files\Planmeca\Romexis\geometries` +- Administrative Rechte werden je nach Umgebung benötigt + +## Verwendung + +PowerShell als Administrator starten und das Skript ausführen: + +```powershell +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass +.\RomexisImplantInstaller.ps1 +``` + +Danach: + +1. Bibliotheksliste wird automatisch geladen. +2. Gewünschte Bibliotheken auswählen. +3. Optional Backup aktiviert lassen. +4. Entweder nur herunterladen oder herunterladen und installieren. + +## Cache + +Die heruntergeladenen ZIP-Dateien werden standardmäßig unter folgendem Pfad abgelegt: + +```text +C:\ProgramData\RomexisImplantLibraryCache +``` + +Neben jeder ZIP-Datei wird eine `.meta.json` gespeichert. Darin stehen unter anderem: + +- URL +- ETag +- Last-Modified +- Content-Length +- Downloadzeitpunkt +- SHA256 + +Wenn sich eine Datei online geändert hat, wird sie erneut heruntergeladen. + +## Backup + +Vor der Installation kann automatisch ein SQL-Backup der Romexis-Datenbank erstellt werden. Das Backup ist nur als Sicherheitsnetz vor Bibliotheksänderungen gedacht und ersetzt kein reguläres Backupkonzept. +Die Logik ist an das Format vom Backup & Restore SCript von Tobias Bauer angeleht und kann damit ggf. wieder eingelesen werden + +## Hinweise + +Dieses Projekt ist kein offizielles Planmeca-Tool. Es ruft die von Planmeca bzw. den Herstellern mitgelieferten Installationsskripte auf, statt die SQL-Logik vollständig nachzubauen. + +Die Nutzung erfolgt auf eigene Verantwortung. Vor produktivem Einsatz sollte ein vollständiges Backup vorhanden sein. + +## Projektstruktur + +```text +. +├── RomexisImplantInstaller.ps1 +├── README.md +├── CHANGELOG.md +├── LICENSE +└── .gitignore +``` + +## Lizenz + +Siehe [LICENSE](LICENSE). diff --git a/RomexisImplantInstaller.ps1 b/RomexisImplantInstaller.ps1 new file mode 100644 index 0000000..c02d678 --- /dev/null +++ b/RomexisImplantInstaller.ps1 @@ -0,0 +1,1338 @@ +<# +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() \ No newline at end of file