| param( |
| [Parameter(Mandatory = $true)] |
| [string]$OutputDir |
| ) |
|
|
| $ErrorActionPreference = "Stop" |
|
|
| function Relative-Path([string]$Base, [string]$Path) { |
| $basePath = [System.IO.Path]::GetFullPath($Base).TrimEnd([char[]]@('\', '/')) |
| $fullPath = [System.IO.Path]::GetFullPath($Path) |
| $prefix = $basePath + [System.IO.Path]::DirectorySeparatorChar |
| if (-not $fullPath.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { |
| throw "$fullPath is not inside $basePath" |
| } |
| $fullPath.Substring($prefix.Length) |
| } |
|
|
| $adbCandidates = @() |
| if ($env:LOCALAPPDATA) { |
| $adbCandidates += Join-Path $env:LOCALAPPDATA "Android\Sdk\platform-tools\adb.exe" |
| } |
| if ($env:ANDROID_HOME) { |
| $adbCandidates += Join-Path $env:ANDROID_HOME "platform-tools\adb.exe" |
| } |
| $adbCandidates = @($adbCandidates | Where-Object { Test-Path -LiteralPath $_ }) |
|
|
| if ($adbCandidates.Count -eq 0) { |
| throw "adb.exe was not found under LOCALAPPDATA or ANDROID_HOME" |
| } |
| $adb = $adbCandidates[0] |
|
|
| $devices = @(& $adb devices | Select-Object -Skip 1 | Where-Object { $_ -match "\tdevice$" }) |
| if ($devices.Count -ne 1) { |
| throw "Expected exactly one authorized device; found $($devices.Count). Check 'adb devices'." |
| } |
|
|
| $destination = [System.IO.Path]::GetFullPath($OutputDir) |
| if (Test-Path -LiteralPath $destination) { |
| if (@(Get-ChildItem -LiteralPath $destination -Force).Count -ne 0) { |
| throw "Refusing to merge a recovery pull into non-empty directory: $destination" |
| } |
| } else { |
| New-Item -ItemType Directory -Path $destination | Out-Null |
| } |
|
|
| $runtimeDir = Join-Path $destination "runtime" |
| $captureDir = Join-Path $destination "captures" |
| New-Item -ItemType Directory -Path $runtimeDir | Out-Null |
| New-Item -ItemType Directory -Path $captureDir | Out-Null |
|
|
| $runtimeRemote = "/data/local/tmp/dinovision" |
| $capturesRemote = "/sdcard/Android/data/rust.dinovision_xr/files/captures" |
|
|
| |
| |
| $runtimePresent = $false |
| & $adb shell "test -d $runtimeRemote" |
| if ($LASTEXITCODE -eq 0) { |
| $runtimePresent = $true |
| & $adb pull "$runtimeRemote/." $runtimeDir |
| if ($LASTEXITCODE -ne 0) { |
| throw "Failed to pull $runtimeRemote" |
| } |
| } |
|
|
| $capturesPresent = $false |
| & $adb shell "test -d $capturesRemote" |
| if ($LASTEXITCODE -eq 0) { |
| $capturesPresent = $true |
| & $adb pull "$capturesRemote/." $captureDir |
| if ($LASTEXITCODE -ne 0) { |
| throw "Failed to pull $capturesRemote" |
| } |
| } |
|
|
| $metadata = [ordered]@{ |
| schema_version = 1 |
| pulled_utc = [DateTime]::UtcNow.ToString("o") |
| device = (& $adb get-serialno).Trim() |
| product = (& $adb shell getprop ro.product.name).Trim() |
| model = (& $adb shell getprop ro.product.model).Trim() |
| build_fingerprint = (& $adb shell getprop ro.build.fingerprint).Trim() |
| runtime_directory_present = $runtimePresent |
| capture_directory_present = $capturesPresent |
| files = @( |
| Get-ChildItem -LiteralPath $destination -File -Recurse | ForEach-Object { |
| [ordered]@{ |
| path = (Relative-Path $destination $_.FullName).Replace("\", "/") |
| bytes = $_.Length |
| sha256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant() |
| } |
| } |
| ) |
| } |
|
|
| $manifestPath = Join-Path $destination "recovery_manifest.json" |
| $metadata | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $manifestPath -Encoding utf8 |
| Write-Host "Recovered $($metadata.files.Count) files to $destination" |
| Write-Host "Manifest: $manifestPath" |
|
|