Files
wsl-manage/wsl-manage-core.ps1
T
2026-09-17 09:32:15 +02:00

319 lines
13 KiB
PowerShell

# Gestion interactive des distributions WSL.
[CmdletBinding()]
param(
[ValidateSet('backup', 'restore')]
[string]$Mode = 'backup'
)
$ErrorActionPreference = 'Stop'
function Stop-WithMessage {
param([string]$Message)
Write-Error $Message
exit 1
}
if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) {
Stop-WithMessage 'wsl.exe est introuvable. Lancez ce script depuis Windows avec WSL installe.'
}
function Invoke-Wsl {
param([string[]]$Arguments)
$output = & wsl.exe @Arguments 2>&1
if ($LASTEXITCODE -ne 0) {
$details = (($output | ForEach-Object { $_.ToString() }) -join ' ').Trim()
if (-not $details) { $details = 'aucun detail fourni par wsl.exe' }
throw ('wsl.exe a retourne le code {0} : {1}' -f $LASTEXITCODE, $details)
}
return $output
}
function Get-Distros {
@(Invoke-Wsl @('--list', '--quiet') | ForEach-Object {
($_.ToString() -replace '\x00', '').Trim()
} | Where-Object { $_ })
}
function Select-Drive {
$drives = @(Get-PSDrive -PSProvider FileSystem | Where-Object {
$_.Root -and (Test-Path -LiteralPath $_.Root)
} | Sort-Object Name)
if ($drives.Count -eq 0) {
Stop-WithMessage 'Aucun disque accessible n''a ete trouve.'
}
Write-Host ''
Write-Host 'Disques disponibles :' -ForegroundColor Cyan
for ($i = 0; $i -lt $drives.Count; $i++) {
$drive = New-Object -TypeName System.IO.DriveInfo -ArgumentList $drives[$i].Root
$free = '{0:N2} Go libres' -f ($drive.AvailableFreeSpace / 1GB)
Write-Host ('[{0}] {1} ({2}) - {3}' -f ($i + 1), $drives[$i].Name, $drives[$i].Root, $free)
}
$selection = Read-Host ('Choisissez le disque (1-{0})' -f $drives.Count)
$driveIndex = 0
if (-not [int]::TryParse($selection, [ref]$driveIndex) -or
$driveIndex -lt 1 -or $driveIndex -gt $drives.Count) {
Stop-WithMessage 'Selection de disque invalide.'
}
return $drives[$driveIndex - 1]
}
function Confirm-Action {
param([string]$Message)
$answer = Read-Host "$Message (o/n)"
return $answer -match '^(o|oui|y|yes)$'
}
function Get-DockerDataPath {
param([bool]$RequireData)
$defaultPath = 'E:\WSL\Docker\desktop-data'
$path = Read-Host ('Chemin des donnees Docker Desktop [{0}]' -f $defaultPath)
if (-not $path) { $path = $defaultPath }
$path = [IO.Path]::GetFullPath($path)
if ($RequireData -and (-not (Test-Path -LiteralPath (Join-Path $path 'disk')) -or
-not (Test-Path -LiteralPath (Join-Path $path 'main')))) {
Stop-WithMessage ('Le chemin Docker doit contenir les dossiers disk et main : {0}' -f $path)
}
if (-not $RequireData -and -not (Test-Path -LiteralPath $path)) {
New-Item -ItemType Directory -Path $path -Force | Out-Null
}
return $path
}
function Assert-DockerStopped {
$dockerProcesses = @(Get-Process -ErrorAction SilentlyContinue | Where-Object {
$_.ProcessName -in @('Docker Desktop', 'com.docker.backend', 'com.docker.proxy', 'com.docker.build')
})
if ($dockerProcesses.Count -gt 0) {
Stop-WithMessage 'Docker Desktop est encore en cours d''execution. Quittez-le completement puis relancez la commande.'
}
}
function Copy-DockerData {
param([string]$SourceRoot, [string]$DestinationRoot)
New-Item -ItemType Directory -Path $DestinationRoot -Force | Out-Null
foreach ($folder in @('disk', 'main')) {
$source = Join-Path $SourceRoot $folder
$destination = Join-Path $DestinationRoot $folder
New-Item -ItemType Directory -Path $destination -Force | Out-Null
& robocopy.exe $source $destination /E /COPY:DAT /DCOPY:DAT /R:2 /W:2 /XJ
if ($LASTEXITCODE -gt 7) {
throw ('La copie Docker a echoue pour {0} (code robocopy {1}).' -f $folder, $LASTEXITCODE)
}
}
}
function Get-DistroSize {
param([string]$Distro)
try {
$sizeOutput = & wsl.exe --distribution $Distro -- df -B1 --output=used / 2>$null
$value = @($sizeOutput | ForEach-Object { $_.ToString() -replace '\x00', '' } |
Where-Object { $_ -match '^\s*\d+\s*$' } | Select-Object -Last 1)
if ($value) { return [int64]$value.Trim() }
} catch { }
return 0
}
function Export-Distro {
param(
[string]$Distro,
[string]$Archive,
[int64]$EstimatedBytes
)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = 'wsl.exe'
$psi.Arguments = '--export "' + $Distro.Replace('"', '\"') + '" "' + $Archive.Replace('"', '\"') + '"'
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $psi
[void]$process.Start()
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
$stderrTask = $process.StandardError.ReadToEndAsync()
$started = Get-Date
do {
Start-Sleep -Milliseconds 500
$percent = 0
if ($EstimatedBytes -gt 0 -and (Test-Path -LiteralPath $Archive)) {
$percent = [math]::Min(99, [int](100 * ((Get-Item -LiteralPath $Archive).Length / $EstimatedBytes)))
}
$elapsed = ((Get-Date) - $started).ToString('hh\:mm\:ss')
Write-Progress -Activity ('Export de {0}' -f $Distro) -Status ('{0}% - {1} ecoule' -f $percent, $elapsed) -PercentComplete $percent
} while (-not $process.HasExited)
$process.WaitForExit()
Write-Progress -Activity ('Export de {0}' -f $Distro) -Completed
if ($process.ExitCode -ne 0) {
$details = $stderrTask.Result.Trim()
if (-not $details) { $details = 'aucun detail fourni par wsl.exe' }
throw ('wsl.exe a retourne le code {0} : {1}' -f $process.ExitCode, $details)
}
}
if ($Mode -eq 'restore') {
$drive = Select-Drive
$backupRoot = Join-Path $drive.Root 'WSL-Backup'
$dates = @(Get-ChildItem -LiteralPath $backupRoot -Directory -ErrorAction SilentlyContinue |
Sort-Object Name -Descending)
if ($dates.Count -eq 0) {
Stop-WithMessage ('Aucune sauvegarde trouvee dans {0}.' -f $backupRoot)
}
Write-Host ''
Write-Host 'Sauvegardes disponibles :' -ForegroundColor Cyan
for ($i = 0; $i -lt $dates.Count; $i++) {
Write-Host ('[{0}] {1}' -f ($i + 1), $dates[$i].Name)
}
$dateSelection = Read-Host ('Choisissez une date (1-{0})' -f $dates.Count)
$dateIndex = 0
if (-not [int]::TryParse($dateSelection, [ref]$dateIndex) -or
$dateIndex -lt 1 -or $dateIndex -gt $dates.Count) {
Stop-WithMessage 'Selection de date invalide.'
}
$dateFolder = $dates[$dateIndex - 1]
$dockerBackupPath = Join-Path $dateFolder.FullName 'Docker'
$archives = @(Get-ChildItem -LiteralPath $dateFolder.FullName -File |
Where-Object { $_.Name -match '\.tar(\.gz|\.xz)?$' } | Sort-Object Name)
if ($archives.Count -eq 0) {
Stop-WithMessage 'Cette sauvegarde ne contient aucune archive exploitable.'
}
Write-Host ''
Write-Host 'Machines disponibles :' -ForegroundColor Cyan
Write-Host '[1] Toutes les machines'
for ($i = 0; $i -lt $archives.Count; $i++) {
Write-Host ('[{0}] {1}' -f ($i + 2), $archives[$i].Name)
}
$machineSelection = Read-Host ('Choisissez une machine (1-{0})' -f ($archives.Count + 1))
$machineIndex = 0
if (-not [int]::TryParse($machineSelection, [ref]$machineIndex) -or
$machineIndex -lt 1 -or $machineIndex -gt ($archives.Count + 1)) {
Stop-WithMessage 'Selection de machine invalide.'
}
if ($machineIndex -eq 1) { $selectedArchives = $archives } else { $selectedArchives = @($archives[$machineIndex - 2]) }
$restoreDocker = @($selectedArchives | Where-Object {
(($_.Name -replace '\.tar(\.gz|\.xz)?$', '') -ieq 'docker' -or
($_.Name -replace '\.tar(\.gz|\.xz)?$', '') -ieq 'docker-desktop')
}).Count -gt 0
if ($restoreDocker) {
if (-not (Test-Path -LiteralPath (Join-Path $dockerBackupPath 'disk')) -or
-not (Test-Path -LiteralPath (Join-Path $dockerBackupPath 'main'))) {
Stop-WithMessage 'Cette sauvegarde ne contient pas les donnees Docker (disk et main).'
}
Assert-DockerStopped
$dockerDataPath = Get-DockerDataPath $false
}
Write-Host ''
Write-Host 'Recapitulatif :' -ForegroundColor Cyan
Write-Host (' Date : {0}' -f $dateFolder.Name)
Write-Host (' Machine(s) : {0}' -f (($selectedArchives | ForEach-Object { $_.BaseName -replace '\.tar$' }) -join ', '))
if ($restoreDocker) { Write-Host (' Donnees Docker : {0}' -f $dockerDataPath) }
Write-Host (' Lecteur : {0}' -f $drive.Root)
if (-not (Confirm-Action 'Lancer la restauration')) { Write-Host 'Operation annulee.'; exit 0 }
try { Invoke-Wsl @('--shutdown') | Out-Null } catch { Stop-WithMessage ('Impossible d''arreter WSL : {0}' -f $_.Exception.Message) }
if ($restoreDocker) { try {
Copy-DockerData -SourceRoot $dockerBackupPath -DestinationRoot $dockerDataPath
Write-Host 'Donnees Docker restaurees.' -ForegroundColor Green
} catch { Stop-WithMessage ('Restauration Docker impossible : {0}' -f $_.Exception.Message) } }
$failed = @()
$installRoot = Join-Path $drive.Root ('WSL-Install\' + $dateFolder.Name)
New-Item -ItemType Directory -Path $installRoot -Force | Out-Null
foreach ($archive in $selectedArchives) {
$name = $archive.Name -replace '\.tar(\.gz|\.xz)?$', ''
$installPath = Join-Path $installRoot $name
try {
if (Test-Path -LiteralPath $installPath) {
throw ('Le dossier d''installation existe deja : {0}' -f $installPath)
}
Invoke-Wsl @('--import', $name, $installPath, $archive.FullName) | Out-Null
Write-Host ('Restauration OK : {0}' -f $name) -ForegroundColor Green
} catch { $failed += $name; Write-Warning ('Echec pour {0} : {1}' -f $name, $_.Exception.Message) }
}
if ($failed.Count -gt 0) { Stop-WithMessage ('Restauration terminee avec erreurs : {0}' -f ($failed -join ', ')) }
Write-Host 'Restauration terminee avec succes.' -ForegroundColor Green
exit 0
}
$distros = Get-Distros
if ($distros.Count -eq 0) {
Stop-WithMessage 'Aucune distribution WSL n''a ete trouvee.'
}
Write-Host ''
Write-Host 'Distributions a sauvegarder :' -ForegroundColor Cyan
Write-Host '[1] Toutes les distributions'
for ($i = 0; $i -lt $distros.Count; $i++) {
Write-Host ('[{0}] {1}' -f ($i + 2), $distros[$i])
}
$distroSelection = Read-Host ('Choisissez une option (1-{0})' -f ($distros.Count + 1))
$distroIndex = 0
if (-not [int]::TryParse($distroSelection, [ref]$distroIndex) -or
$distroIndex -lt 1 -or $distroIndex -gt ($distros.Count + 1)) {
Stop-WithMessage 'Selection de distribution invalide.'
}
if ($distroIndex -eq 1) { $selectedDistros = $distros } else { $selectedDistros = @($distros[$distroIndex - 2]) }
$includeDockerData = @($selectedDistros | Where-Object {
$_ -ieq 'docker' -or $_ -ieq 'docker-desktop'
}).Count -gt 0
if ($includeDockerData) {
Assert-DockerStopped
$dockerDataPath = Get-DockerDataPath $true
}
$drive = Select-Drive
$backupRoot = Join-Path -Path $drive.Root -ChildPath 'WSL-Backup'
$backupFolder = Join-Path -Path $backupRoot -ChildPath (Get-Date -Format 'yyyy-MM-dd_HH-mm-ss')
New-Item -ItemType Directory -Path $backupFolder -Force | Out-Null
Write-Host ('Sauvegarde dans : {0}' -f $backupFolder) -ForegroundColor Green
Write-Host ('Distributions : {0}' -f ($selectedDistros -join ', '))
$freeSpace = New-Object -TypeName System.IO.DriveInfo -ArgumentList $drive.Root
Write-Host ('Espace libre : {0:N2} Go' -f ($freeSpace.AvailableFreeSpace / 1GB)) -ForegroundColor Cyan
$estimatedSizes = @{}
foreach ($distro in $selectedDistros) { $estimatedSizes[$distro] = Get-DistroSize $distro }
if (-not (Confirm-Action 'Lancer la sauvegarde')) { Remove-Item -LiteralPath $backupFolder -Force; Write-Host 'Operation annulee.'; exit 0 }
try { Invoke-Wsl @('--shutdown') | Out-Null } catch { Stop-WithMessage ('Impossible d''arreter WSL : {0}' -f $_.Exception.Message) }
if ($includeDockerData) {
$dockerBackupPath = Join-Path $backupFolder 'Docker'
try {
Copy-DockerData -SourceRoot $dockerDataPath -DestinationRoot $dockerBackupPath
Write-Host ('Donnees Docker sauvegardees dans : {0}' -f $dockerBackupPath) -ForegroundColor Green
} catch {
Stop-WithMessage ('Sauvegarde Docker impossible : {0}' -f $_.Exception.Message)
}
}
$failed = @()
foreach ($distro in $selectedDistros) {
$archive = Join-Path -Path $backupFolder -ChildPath ($distro + '.tar')
Write-Host ('Export de {0}...' -f $distro) -ForegroundColor Yellow
try {
Export-Distro -Distro $distro -Archive $archive -EstimatedBytes $estimatedSizes[$distro]
Write-Host (' OK : {0}' -f $archive) -ForegroundColor Green
}
catch {
$failed += $distro
Write-Warning ('Echec pour {0} : {1}' -f $distro, $_.Exception.Message)
}
}
if ($failed.Count -gt 0) {
Write-Warning ('Sauvegarde terminee avec des erreurs : {0}' -f ($failed -join ', '))
exit 1
}
Write-Host ''
Write-Host 'Sauvegarde terminee avec succes.' -ForegroundColor Green