#requires -Version 5.1 <# .SYNOPSIS Stellt bestehende Scoop-Installationen von elexis/streichtech auf medelexis um. .DESCRIPTION Ohne -Apply nur ein Probelauf. Keine Sicherungen oder Git-Pushes. Bearbeitet alle Versionsverzeichnisse der benutzerspezifischen Scoop-Installation. Ein passender bestehender Bucket wird umbenannt. Danach werden elexis und streichtech entfernt. Andere Buckets wie main und extras bleiben bestehen. Alte Paketnamen mit Unterstrich werden auf passende Manifestnamen mit Bindestrich umgestellt. ee-iso/ee_iso und ee-cli werden deinstalliert. MyElexis CLI wird neu installiert. Mit -Apply wird abschliessend gefragt, ob taegliche Auto-Updates um 03:00 Uhr eingerichtet werden sollen. Das separate Skript wird aus dem Repository geladen. .EXAMPLE .\scripts\update-bucket.ps1 .EXAMPLE .\scripts\update-bucket.ps1 -Apply #> [CmdletBinding()] param( [switch] $Apply, [string] $ScoopRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' }) ) $ErrorActionPreference = 'Stop' $repository = 'https://gitea.medelexis.ch/public/scoop' $targetName = 'medelexis' $target = Join-Path $ScoopRoot "buckets\$targetName" $apps = Join-Path $ScoopRoot 'apps' if (-not (Test-Path -LiteralPath $apps -PathType Container)) { throw "Kein Scoop-Apps-Verzeichnis gefunden: $apps" } function Test-MedelexisRepository([string] $Path) { if (-not (Test-Path -LiteralPath $Path -PathType Container)) { return $false } $remote = & git -C $Path remote get-url origin 2>$null if ($LASTEXITCODE -ne 0) { return $false } try { $uri = [Uri] ([string] $remote).Trim() } catch { return $false } return ($uri.Scheme -eq 'https' -and $uri.Host -eq 'gitea.medelexis.ch' -and $uri.AbsolutePath.TrimEnd('/').Replace('.git', '') -eq '/public/scoop') } function Get-PackageRenames([string] $BucketPath) { if (-not $BucketPath -or -not (Test-Path -LiteralPath $BucketPath)) { return } foreach ($app in Get-ChildItem -LiteralPath $apps -Directory) { if ($app.Name -in @('ee-iso', 'ee_iso')) { continue } if (-not $app.Name.Contains('_')) { continue } $newName = $app.Name.Replace('_', '-') $newManifest = Join-Path $BucketPath "bucket\$newName.json" if (-not (Test-Path -LiteralPath $newManifest)) { $newManifest = Join-Path $BucketPath "$newName.json" } if (-not (Test-Path -LiteralPath $newManifest)) { continue } $current = Join-Path $app.FullName 'current' $installFile = Join-Path $current 'install.json' if (-not (Test-Path -LiteralPath $installFile)) { continue } $install = [IO.File]::ReadAllText($installFile) | ConvertFrom-Json if ($install.bucket -notin @('elexis', 'streichtech', 'medelexis')) { continue } $destination = Join-Path $apps $newName if (Test-Path -LiteralPath $destination) { throw "'$($app.Name)' und '$newName' sind beide installiert. Keine automatische Zusammenführung." } $link = Get-Item -LiteralPath $current -Force if ($link.LinkType -ne 'Junction') { throw "Für '$($app.Name)' wird eine Scoop-current-Junction erwartet. Keine automatische Umbenennung." } $versionPath = [IO.Path]::GetFullPath([string] @($link.Target)[0]) if ((Split-Path $versionPath -Parent) -ne $app.FullName) { throw "Die current-Verknüpfung von '$($app.Name)' zeigt ausserhalb des Paketordners." } $persistSource = Join-Path $ScoopRoot "persist\$($app.Name)" $persistTarget = Join-Path $ScoopRoot "persist\$newName" if ((Test-Path -LiteralPath $persistSource) -and (Test-Path -LiteralPath $persistTarget)) { throw "Beide Persistenzordner für '$($app.Name)' und '$newName' existieren. Keine automatische Zusammenführung." } Write-Host "Paket umbenennen: $($app.Name) -> $newName" [pscustomobject]@{ OldName = $app.Name; NewName = $newName; Source = $app.FullName Destination = $destination; Version = (Split-Path $versionPath -Leaf) PersistSource = $persistSource; PersistTarget = $persistTarget } } } $installCli = (Test-Path -LiteralPath (Join-Path $apps 'ee-cli')) -or -not (Test-Path -LiteralPath (Join-Path $apps 'myelexis-cli')) $obsoleteApps = @(@('ee-iso', 'ee_iso', 'ee-cli') | Where-Object { Test-Path -LiteralPath (Join-Path $apps $_) }) if ($installCli -and (Test-Path -LiteralPath (Join-Path $apps 'myelexis-cli'))) { $obsoleteApps += 'myelexis-cli' } foreach ($obsolete in $obsoleteApps) { Write-Host "Veraltetes Paket deinstallieren: $obsolete" } if ($installCli) { Write-Host 'MyElexis CLI wird aus medelexis neu installiert.' } $renameSource = $null if (Test-Path -LiteralPath $target) { if (-not (Test-MedelexisRepository $target)) { throw 'Der vorhandene Bucket medelexis zeigt auf ein anderes Repository. Keine Änderung.' } Write-Host 'Bucket medelexis ist bereits korrekt eingerichtet.' } else { foreach ($oldName in @('elexis', 'streichtech')) { $candidate = Join-Path $ScoopRoot "buckets\$oldName" if (Test-MedelexisRepository $candidate) { $renameSource = $candidate Write-Host "Bucket umbenennen: $oldName -> $targetName" break } } if (-not $renameSource) { Write-Host "Bucket hinzufügen: $targetName -> $repository" } } $changes = @( foreach ($app in Get-ChildItem -LiteralPath $apps -Directory) { if ($app.Name -in $obsoleteApps) { continue } foreach ($version in Get-ChildItem -LiteralPath $app.FullName -Directory) { # current und andere Verknüpfungen nicht nochmals bearbeiten. if ($version.Attributes -band [IO.FileAttributes]::ReparsePoint) { continue } $file = Join-Path $version.FullName 'install.json' if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { continue } $original = [IO.File]::ReadAllText($file) $data = $original | ConvertFrom-Json if ($data.bucket -notin @('elexis', 'streichtech')) { continue } $pattern = '(?i)("bucket"\s*:\s*)"(?:elexis|streichtech)"' $updated = [regex]::Replace($original, $pattern, '${1}"medelexis"') if (($updated | ConvertFrom-Json).bucket -ne $targetName) { throw "Bucket-Zuordnung konnte nicht angepasst werden: $file" } Write-Host "$($app.Name) / $($version.Name): $($data.bucket) -> $targetName" [pscustomobject]@{ File = $file; Original = $original; Updated = $updated; App = $app.Name } } } ) $manifestRoot = $(if (Test-Path -LiteralPath $target) { $target } else { $renameSource }) $renames = @(Get-PackageRenames $manifestRoot) if (-not $Apply) { foreach ($oldName in @('elexis', 'streichtech')) { if (Test-Path -LiteralPath (Join-Path $ScoopRoot "buckets\$oldName")) { Write-Host "Alter Bucket wird nach der Umstellung entfernt oder umbenannt: $oldName" } } Write-Host "Probelauf: $($changes.Count) install.json-Dateien würden geändert." Write-Host "Probelauf: $($renames.Count) Paketnamen würden umgestellt." Write-Host "Probelauf: $(@($obsoleteApps).Count) alte Pakete würden deinstalliert." Write-Host 'Mit -Apply wird optional ein Auto-Update um 03:00 Uhr mit Nachholen eingerichtet (y/n).' Write-Host 'Mit -Apply anwenden. Es werden keine Sicherungen erstellt.' return } $activeRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' }) if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne [IO.Path]::GetFullPath($activeRoot).TrimEnd('\')) { throw 'ScoopRoot muss der aktiven Scoop-Installation entsprechen.' } if (-not (Test-Path -LiteralPath $target)) { if ($renameSource) { $bucketRoot = (Resolve-Path -LiteralPath (Join-Path $ScoopRoot 'buckets')).Path $sourcePath = (Resolve-Path -LiteralPath $renameSource).Path $targetPath = [IO.Path]::GetFullPath($target) if ((Split-Path $sourcePath -Parent) -ne $bucketRoot -or (Split-Path $targetPath -Parent) -ne $bucketRoot) { throw 'Bucket-Pfade liegen ausserhalb des erwarteten Verzeichnisses. Keine Umbenennung.' } [IO.Directory]::Move($sourcePath, $targetPath) } else { # scoop verwendet dieselbe Root-Variable für benutzerdefinierte Installationen. if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne [IO.Path]::GetFullPath($(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' })).TrimEnd('\')) { throw 'ScoopRoot weicht von der aktiven Scoop-Installation ab; Bucket bitte dort zuerst einrichten.' } & scoop bucket add $targetName $repository if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $target)) { throw 'Bucket konnte nicht hinzugefügt werden. Installationsmetadaten bleiben unverändert.' } } } # Falls der Ziel-Bucket erst hinzugefügt wurde, stehen seine Manifestnamen jetzt bereit. if (-not $manifestRoot) { $renames = @(Get-PackageRenames $target) } # Vor dem ersten Schreiben prüfen, ob während des Probelaufs Dateien geändert wurden. foreach ($change in $changes) { if ([IO.File]::ReadAllText($change.File) -cne $change.Original) { throw 'Installationsmetadaten wurden zwischenzeitlich geändert. Skript bitte erneut starten.' } } foreach ($change in $changes) { [IO.File]::WriteAllText($change.File, $change.Updated, [Text.UTF8Encoding]::new($false)) } if ($installCli -and -not (Test-Path -LiteralPath (Join-Path $target 'bucket\myelexis-cli.json')) -and -not (Test-Path -LiteralPath (Join-Path $target 'myelexis-cli.json'))) { throw 'Manifest myelexis-cli fehlt im Ziel-Bucket. Keine Deinstallation.' } foreach ($obsolete in $obsoleteApps) { $activeRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' }) if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne [IO.Path]::GetFullPath($activeRoot).TrimEnd('\')) { throw 'Für Deinstallationen muss ScoopRoot der aktiven Scoop-Installation entsprechen.' } & scoop uninstall $obsolete if ($LASTEXITCODE -ne 0 -or (Test-Path -LiteralPath (Join-Path $apps $obsolete))) { throw "'$obsolete' konnte nicht deinstalliert werden." } } if ($installCli) { Write-Host 'MyElexis CLI aus medelexis neu installieren ...' & scoop install 'medelexis/myelexis-cli' if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath (Join-Path $apps 'myelexis-cli\current\myelexis-cli.exe'))) { throw 'MyElexis CLI konnte nicht vollständig installiert werden.' } } foreach ($rename in $renames) { $activeRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' }) if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne [IO.Path]::GetFullPath($activeRoot).TrimEnd('\')) { throw 'Für Paketumbenennungen muss ScoopRoot der aktiven Scoop-Installation entsprechen.' } $appsRoot = (Resolve-Path -LiteralPath $apps).Path if ((Split-Path $rename.Source -Parent) -ne $appsRoot -or (Split-Path $rename.Destination -Parent) -ne $appsRoot) { throw 'Paketpfade liegen ausserhalb des Apps-Verzeichnisses.' } if (Test-Path -LiteralPath $rename.PersistSource) { $persistRoot = (Resolve-Path -LiteralPath (Join-Path $ScoopRoot 'persist')).Path if ((Split-Path $rename.PersistSource -Parent) -ne $persistRoot -or (Split-Path $rename.PersistTarget -Parent) -ne $persistRoot) { throw 'Persistenzpfade liegen ausserhalb des Persist-Verzeichnisses.' } [IO.Directory]::Move($rename.PersistSource, $rename.PersistTarget) } $oldCurrent = Join-Path $rename.Source 'current' & attrib -R /L $oldCurrent if ($LASTEXITCODE -ne 0) { throw 'current-Verknüpfung konnte nicht freigegeben werden.' } # Nur die Junction entfernen, ohne rekursiv ihren Inhalt zu löschen. [IO.Directory]::Delete($oldCurrent) [IO.Directory]::Move($rename.Source, $rename.Destination) $newCurrent = Join-Path $rename.Destination 'current' $newVersion = Join-Path $rename.Destination $rename.Version New-Item -ItemType Junction -Path $newCurrent -Target $newVersion | Out-Null Write-Host "Startverknüpfungen für $($rename.NewName) erneuern ..." & scoop reset "$($rename.NewName)@$($rename.Version)" if ($LASTEXITCODE -ne 0) { throw "Scoop reset für '$($rename.NewName)' fehlgeschlagen." } } foreach ($oldName in @('elexis', 'streichtech')) { $oldPath = Join-Path $ScoopRoot "buckets\$oldName" if (-not (Test-Path -LiteralPath $oldPath)) { continue } $activeRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' }) if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne [IO.Path]::GetFullPath($activeRoot).TrimEnd('\')) { throw 'Zum Entfernen alter Buckets muss ScoopRoot der aktiven Scoop-Installation entsprechen.' } Write-Host "Alten Bucket entfernen: $oldName" & scoop bucket rm $oldName if ($LASTEXITCODE -ne 0 -or (Test-Path -LiteralPath $oldPath)) { throw "Alter Bucket '$oldName' konnte nicht entfernt werden. Die Umstellung kann erneut ausgeführt werden." } } foreach ($name in @((Get-ChildItem -LiteralPath $apps -Directory).Name)) { $installFile = Join-Path $apps "$name\current\install.json" if (-not (Test-Path -LiteralPath $installFile)) { continue } if (([IO.File]::ReadAllText($installFile) | ConvertFrom-Json).bucket -ne $targetName) { continue } if (-not (Test-Path -LiteralPath (Join-Path $target "bucket\$name.json")) -and -not (Test-Path -LiteralPath (Join-Path $target "$name.json"))) { Write-Warning "Für '$name' fehlt weiterhin ein gleichnamiges Manifest." } } Write-Host "Fertig: $($changes.Count) install.json-Dateien auf medelexis umgestellt." Write-Host "Paketnamen umgestellt: $($renames.Count)." Write-Host 'Von elexis/streichtech/medelexis bleibt nur medelexis bestehen. Prüfung: scoop status --local' do { $autoUpdateAnswer = (Read-Host 'Scoop Auto-Update einschalten? [y/n]').Trim().ToLowerInvariant() } while ($autoUpdateAnswer -notin @('y', 'n')) if ($autoUpdateAnswer -eq 'y') { $autoUpdateDirectory = Join-Path $ScoopRoot 'scripts\medelexis' New-Item -ItemType Directory -Path $autoUpdateDirectory -Force | Out-Null $autoUpdateScript = Join-Path $autoUpdateDirectory 'auto-update.ps1' $downloadPath = Join-Path $autoUpdateDirectory ('auto-update-{0}.download' -f [guid]::NewGuid()) try { $autoUpdateUrl = "$repository/raw/branch/master/scripts/auto-update.ps1" Invoke-WebRequest -Uri $autoUpdateUrl -OutFile $downloadPath -UseBasicParsing $parseTokens = $null $parseErrors = $null $downloadedScript = [Management.Automation.Language.Parser]::ParseFile($downloadPath, [ref] $parseTokens, [ref] $parseErrors) if ($parseErrors.Count -gt 0 -or -not $downloadedScript.ParamBlock -or 'ScoopRoot' -notin @($downloadedScript.ParamBlock.Parameters.Name.VariablePath.UserPath)) { throw 'Das heruntergeladene Auto-Update-Skript ist kein gueltiges Updateskript.' } Move-Item -LiteralPath $downloadPath -Destination $autoUpdateScript -Force # Nur die Aufgabe einrichten; das Updateskript wird durch die Aufgabe gestartet. $userId = [Security.Principal.WindowsIdentity]::GetCurrent().Name $userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value $taskName = "Medelexis-Scoop-AutoUpdate-$userSid" $powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' $arguments = '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "{0}" -ScoopRoot "{1}"' -f $autoUpdateScript, $ScoopRoot $action = New-ScheduledTaskAction -Execute $powershell -Argument $arguments $trigger = New-ScheduledTaskTrigger -Daily -At '03:00' $principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable ` -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew ` -ExecutionTimeLimit (New-TimeSpan -Hours 4) Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger ` -Principal $principal -Settings $settings -Force ` -Description 'Scoop taeglich um 03:00 aktualisieren; verpasste Termine nachholen; elexis* auf hold setzen.' | Out-Null Write-Host "Scoop Auto-Update eingeschaltet: $taskName (taeglich 03:00, verpasste Termine nachholen)." Write-Host "Ausfuehrung verborgen unter $userId, sobald der Benutzer angemeldet ist; kein Aufwecken des PCs." } finally { if (Test-Path -LiteralPath $downloadPath) { Remove-Item -LiteralPath $downloadPath } } } else { Write-Host 'Auto-Update nicht eingerichtet. Eine eventuell bestehende Aufgabe bleibt unveraendert.' }