param( [Parameter(Mandatory = $true)][string]$Archive, [string]$ComposeFile = "compose.yaml", [string]$DataPath = "./data", [switch]$Force ) $ErrorActionPreference = "Stop" $resolvedArchive = (Resolve-Path -LiteralPath $Archive).Path $target = [System.IO.Path]::GetFullPath($DataPath) $targetRoot = [System.IO.Path]::GetPathRoot($target).TrimEnd('\', '/') if ($target.TrimEnd('\', '/') -eq $targetRoot) { throw "Refusing to restore into a filesystem root." } $checksumFile = "$resolvedArchive.sha256" if (Test-Path -LiteralPath $checksumFile) { $expected = ((Get-Content -LiteralPath $checksumFile -TotalCount 1) -split '\s+')[0] $actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $resolvedArchive).Hash.ToLowerInvariant() if ($actual -ne $expected.ToLowerInvariant()) { throw "Backup checksum verification failed." } } New-Item -ItemType Directory -Force -Path $target | Out-Null if ((Get-ChildItem -Force -LiteralPath $target | Select-Object -First 1) -and -not $Force) { throw "Target data directory is not empty. Re-run with -Force after verifying the archive." } docker compose -f $ComposeFile stop | Out-Host if ($LASTEXITCODE -ne 0) { throw "Unable to stop MemRelay services." } try { if ($Force) { Get-ChildItem -Force -LiteralPath $target | Remove-Item -Recurse -Force } $archiveDirectory = [System.IO.Path]::GetDirectoryName($resolvedArchive) $archiveName = [System.IO.Path]::GetFileName($resolvedArchive) & docker run --rm --user "1000:1000" ` --mount "type=bind,source=$archiveDirectory,target=/backup,readonly" ` --mount "type=bind,source=$target,target=/target" ` alpine:3.22 tar -xzf "/backup/$archiveName" -C /target if ($LASTEXITCODE -ne 0) { throw "Unable to extract the backup archive." } Write-Host "Restore completed: $target" } finally { docker compose -f $ComposeFile up -d | Out-Host if ($LASTEXITCODE -ne 0) { Write-Error "Restore finished, but MemRelay services could not be started." } }