# 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