319 lines
13 KiB
PowerShell
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
|