This commit is contained in:
Laurent Chedanne committed 2026-09-17 09:32:15 +02:00
commit ab17ec1e48
4 files changed
+520

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
# Installation de wsl-manage
## 1. Placer les fichiers
Conservez les fichiers suivants dans un dossier permanent, par exemple :
```text
E:\Drive\Apps\WSL\
|-- wsl-manage.cmd
|-- wsl-manage-core.ps1
|-- README.md
`-- Install.md
```
Ne placez pas ce dossier dans un emplacement temporaire.
## 2. Ajouter le dossier au PATH
Ouvrez PowerShell et executez :
```powershell
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$installPath = 'E:\Drive\Apps\WSL'
if (($userPath -split ';') -notcontains $installPath) {
[Environment]::SetEnvironmentVariable(
'Path',
(($userPath.TrimEnd(';') + ';' + $installPath).Trim(';')),
'User'
)
}
```
Fermez puis ouvrez un nouveau terminal PowerShell pour que le changement soit
pris en compte.
## 3. Verifier l'installation
Executez :
```powershell
Get-Command wsl-manage
```
La commande doit pointer vers :
```text
E:\Drive\Apps\WSL\wsl-manage.cmd
```
Affichez ensuite l'aide :
```powershell
wsl-manage -?
```
## 4. Utiliser le script
Sauvegarde par defaut :
```powershell
wsl-manage
```
Restauration :
```powershell
wsl-manage restore
```
## Depannage
Si PowerShell tente de charger directement un fichier `.ps1`, verifiez que le
fichier public s'appelle bien `wsl-manage.cmd` et que seul le dossier
d'installation est present dans le `PATH`.
Le lanceur utilise `-ExecutionPolicy Bypass` uniquement pour cette execution.
Il n'est normalement pas necessaire de modifier la politique d'execution
PowerShell du systeme.
L'execution en administrateur n'est pas requise pour une sauvegarde standard.
Elle peut etre necessaire pour acceder a certains fichiers VHDX ou les
compacter.
+116
View File
@@ -0,0 +1,116 @@
# wsl-manage
Outil PowerShell interactif de sauvegarde et de restauration des distributions WSL.
Il gere aussi les donnees Docker Desktop lorsque la distribution `docker` ou
`docker-desktop` est selectionnee.
## Fichiers
- `wsl-manage.cmd` : commande a lancer.
- `wsl-manage-core.ps1` : implementation PowerShell.
Le dossier contenant ces fichiers doit etre dans le `PATH` Windows pour pouvoir
lancer `wsl-manage` depuis n'importe quel terminal.
## Prerequis
- Windows avec WSL installe et accessible par `wsl.exe`.
- PowerShell.
- Docker Desktop doit etre completement arrete lorsqu'une sauvegarde ou une
restauration Docker est concernee.
- Le lecteur de destination doit etre accessible et disposer de suffisamment
d'espace libre.
L'execution en administrateur n'est normalement pas necessaire. Elle peut etre
necessaire pour acceder a certains fichiers VHDX ou effectuer un compactage.
## Sauvegarde
Lancer sans argument :
```powershell
wsl-manage
```
Le script :
1. Liste les distributions WSL disponibles.
2. Demande de sauvegarder toutes les distributions ou une seule.
3. Demande le lecteur de destination et affiche son espace libre.
4. Demande confirmation.
5. Arrete WSL avec `wsl --shutdown`.
6. Exporte les distributions selectionnees au format `.tar`.
7. Affiche une progression estimee et le temps ecoule.
Les sauvegardes sont rangees dans un dossier date sous `WSL-Backup` :
```text
D:\WSL-Backup\2026-09-16_20-05-42\
|-- Ubuntu.tar
|-- Debian.tar
`-- ...
```
Le pourcentage affiche est une estimation basee sur l'espace utilise par la
distribution. `wsl.exe --export` ne fournit pas de progression native.
## Donnees Docker Desktop
Si `docker` ou `docker-desktop` est selectionne, le script :
1. Verifie que Docker Desktop est arrete.
2. Demande le chemin des donnees Docker.
3. Utilise par defaut :
```text
E:\WSL\Docker\desktop-data
```
4. Verifie la presence des dossiers `disk` et `main`.
5. Copie ces dossiers dans :
```text
D:\WSL-Backup\2026-09-16_20-05-42\Docker\
|-- disk\
`-- main\
```
La distribution `docker-desktop` exportee en `.tar` contient principalement
les fichiers systeme. Les images, conteneurs et volumes Docker sont dans les
donnees `disk` et `main`.
## Restauration
Lancer avec l'argument `restore` :
```powershell
wsl-manage restore
```
Le script :
1. Demande le lecteur contenant `WSL-Backup`.
2. Demande la date de sauvegarde.
3. Demande une distribution ou `Toutes les machines`.
4. Demande confirmation apres recapitulatif.
5. Arrete WSL.
6. Importe les distributions sous `WSL-Install\DATE`.
7. Restaure les donnees Docker uniquement si `docker` ou `docker-desktop` est
selectionne.
Une distribution restauree porte le nom de son fichier sans extension :
```text
Ubuntu.tar -> Ubuntu
```
Le dossier d'installation d'une distribution ne doit pas deja exister.
## Conseils
- Quitter Docker Desktop depuis son icone de notification avant une operation
Docker.
- Ne pas interrompre la copie des fichiers VHDX.
- Conserver une copie de secours des donnees Docker avant restauration.
- Les anciennes sauvegardes ne contenant pas de dossier `Docker` ne permettent
pas de restaurer les donnees Docker.
+318
View File
@@ -0,0 +1,318 @@
# 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
+3
View File
@@ -0,0 +1,3 @@
@echo off
chcp 65001 >nul
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0wsl-manage-core.ps1" %*