# DeployCenter - Recherche et lecture des sources Windows # Compatible Windows PowerShell 5.1 function Find-WindowsSources { param( [Parameter(Mandatory=$true)][string]$RootPath, [AllowNull()][scriptblock]$LogAction = $null ) $results = New-Object System.Collections.Generic.List[object] $roots = @($RootPath -split ';' | ForEach-Object { $candidate = $_.Trim().Trim('"') if ($candidate -notmatch '^[A-Za-z]:\\$') { $candidate = $candidate.TrimEnd('\') } $candidate } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique) if ($roots.Count -eq 0) { throw 'Indique au moins un chemin local et/ou un partage SMB, separes par un point-virgule.' } foreach ($root in $roots) { if (-not (Test-Path -LiteralPath $root -PathType Container)) { if ($LogAction) { & $LogAction "Source inaccessible, ignoree : $root" 'WARNING' } continue } if ($LogAction) { & $LogAction "Recherche des sources Windows dans : $root" 'INFO' } # Parcours en lecture seule. Les erreurs d'acces aux sous-dossiers sont ignorees. # Afficher les ISO et les fichiers WIM/ESD autonomes. # Exception : masquer boot.wim uniquement lorsqu'il fait partie d'un # support Windows extrait dont le même dossier Sources contient install.wim/esd. $allFiles = @(Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue) $files = New-Object System.Collections.Generic.List[object] foreach ($file in $allFiles) { if ($file.Extension -ieq '.iso') { [void]$files.Add($file) continue } if ($file.Extension -ine '.wim' -and $file.Extension -ine '.esd') { continue } $isBootWimInExtractedMedia = $false if ($file.Name -ieq 'boot.wim' -and $file.Directory.Name -ieq 'Sources') { $installWim = Join-Path $file.Directory.FullName 'install.wim' $installEsd = Join-Path $file.Directory.FullName 'install.esd' if ((Test-Path -LiteralPath $installWim -PathType Leaf) -or (Test-Path -LiteralPath $installEsd -PathType Leaf)) { $isBootWimInExtractedMedia = $true } } if (-not $isBootWimInExtractedMedia) { [void]$files.Add($file) } } foreach ($file in $files) { $kind = switch ($file.Extension.ToLowerInvariant()) { '.iso' { 'Image ISO' } '.wim' { 'Image WIM Windows' } '.esd' { 'Image ESD Windows' } } $detail = if ($file.Name -ieq 'install.wim' -or $file.Name -ieq 'install.esd') { "Image d’installation dans un support extrait" } elseif ($file.Extension -ieq '.iso') { "Support d’installation ISO" } else { "Image WIM/ESD autonome" } $results.Add([pscustomobject]@{ Type = $kind; Nom = $file.Name; Chemin = $file.FullName Taille = ('{0:N2} Go' -f ($file.Length / 1GB)); Detail = $detail }) } } if (-not ($roots | Where-Object { Test-Path -LiteralPath $_ -PathType Container })) { throw "Aucun des chemins n'est accessible : $($roots -join '; ')`nVerifie que le lecteur local et le partage SMB sont accessibles." } if ($LogAction) { & $LogAction ("Detection terminee : {0} element(s) trouve(s)." -f $results.Count) 'SUCCESS' } return @($results | Sort-Object Type, Nom, Chemin -Unique) } function Get-WindowsImageEditions { param( [Parameter(Mandatory=$true)][string]$ImagePath, [AllowNull()][scriptblock]$LogAction = $null ) if (-not (Test-Path -LiteralPath $ImagePath -PathType Leaf)) { throw "Fichier image introuvable : $ImagePath" } $normalized = @() $getWindowsImage = Get-Command Get-WindowsImage -ErrorAction SilentlyContinue if ($getWindowsImage) { try { $images = @(Get-WindowsImage -ImagePath $ImagePath -ErrorAction Stop) foreach ($image in $images) { $indexValue = $null foreach ($propertyName in @('ImageIndex', 'Index')) { if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) { $indexValue = [int]$image.$propertyName break } } if ($null -eq $indexValue) { continue } $nameValue = $null foreach ($propertyName in @('ImageName', 'Name', 'ImageDescription')) { if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) { $nameValue = [string]$image.$propertyName break } } $archValue = $null foreach ($propertyName in @('Architecture', 'ImageArchitecture')) { if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) { $archValue = $image.$propertyName break } } $versionValue = $null foreach ($propertyName in @('Version', 'ImageVersion')) { if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) { $versionValue = [string]$image.$propertyName break } } $normalized += [pscustomobject]@{ Index = $indexValue Name = $nameValue Description = if ($image.PSObject.Properties['ImageDescription']) { $image.ImageDescription } else { $null } Architecture = $archValue Version = $versionValue Edition = if ($image.PSObject.Properties['EditionId']) { $image.EditionId } else { $null } } } } catch { if ($LogAction) { & $LogAction "Get-WindowsImage indisponible pour $ImagePath : $($_.Exception.Message)" 'WARNING' } } } $dism = Join-Path $env:WINDIR 'System32\dism.exe' if (-not (Test-Path -LiteralPath $dism)) { $dism = 'dism.exe' } # Si le module PowerShell n'a pas renvoyé les index/noms, les récupérer avec DISM. if ($normalized.Count -eq 0) { $output = & $dism /English /Get-WimInfo "/WimFile:$ImagePath" 2>&1 if ($LASTEXITCODE -ne 0) { throw "DISM n'a pas pu lire l'image : $ImagePath`n$($output -join "`n")" } $current = $null foreach ($line in $output) { $lineText = [string]$line if ($lineText -match '^\s*Index\s*:\s*(\d+)\s*$') { if ($null -ne $current) { $normalized += [pscustomobject]$current } $current = @{ Index = [int]$Matches[1]; Name = ''; Description = ''; Architecture = $null; Version = $null; Edition = $null } } elseif ($null -ne $current -and $lineText -match '^\s*Name\s*:\s*(.*?)\s*$') { $current.Name = $Matches[1] } elseif ($null -ne $current -and $lineText -match '^\s*Description\s*:\s*(.*?)\s*$') { $current.Description = $Matches[1] } } if ($null -ne $current) { $normalized += [pscustomobject]$current } } if ($normalized.Count -eq 0) { throw "DISM n’a retourné aucune édition exploitable pour : $ImagePath" } # Interroger DISM pour chaque index même si Get-WindowsImage a fourni les noms : # selon la version du module, les propriétés Architecture/Version peuvent être absentes. foreach ($edition in $normalized) { if ($edition.Architecture -and $edition.Version) { continue } $detailOutput = & $dism /English /Get-WimInfo "/WimFile:$ImagePath" "/Index:$($edition.Index)" 2>&1 if ($LASTEXITCODE -ne 0) { if ($LogAction) { & $LogAction "DISM n'a pas pu lire les metadonnees de l'index $($edition.Index) : $($detailOutput -join ' ')" 'WARNING' } continue } $insideImageDetails = $false $imageVersion = $null $servicePackBuild = $null foreach ($detailLine in $detailOutput) { $detailText = [string]$detailLine if ($detailText -match '^\s*Details for image\s*:') { $insideImageDetails = $true continue } # Ignorer la version de DISM affichée au début de sa sortie. if (-not $insideImageDetails) { continue } if (-not $edition.Architecture -and $detailText -match '^\s*Architecture\s*:\s*(.*?)\s*$') { $edition.Architecture = $Matches[1] } elseif ($detailText -match '^\s*Version\s*:\s*(.*?)\s*$') { $imageVersion = $Matches[1] } elseif ($detailText -match '^\s*ServicePack Build\s*:\s*(.*?)\s*$') { $servicePackBuild = $Matches[1] } elseif (-not $edition.Name -and $detailText -match '^\s*Name\s*:\s*(.*?)\s*$') { $edition.Name = $Matches[1] } elseif (-not $edition.Edition -and $detailText -match '^\s*Edition\s*:\s*(.*?)\s*$') { $edition.Edition = $Matches[1] } } if ($imageVersion) { $edition.Version = if ($servicePackBuild -and $servicePackBuild -ne '0') { "$imageVersion.$servicePackBuild" } else { $imageVersion } } } # Version globale de l'image : ne l'afficher que si les métadonnées des index # donnent une seule version cohérente. Ne pas répéter cette valeur sur chaque ligne. $versionValues = @($normalized | ForEach-Object { if (-not [string]::IsNullOrWhiteSpace([string]$_.Version)) { [string]$_.Version } } | Select-Object -Unique) $globalVersion = $null if ($versionValues.Count -eq 1) { $globalVersion = $versionValues[0] } foreach ($edition in $normalized) { if ($edition.Architecture -is [int] -or [string]$edition.Architecture -match '^\d+$') { switch ([string]$edition.Architecture) { '0' { $edition.Architecture = 'x86' } '5' { $edition.Architecture = 'ARM' } '9' { $edition.Architecture = 'x64' } '12' { $edition.Architecture = 'ARM64' } } } } return [pscustomobject]@{ Editions = @($normalized) GlobalVersion = $globalVersion } } function Resolve-InstallImagePath { param( [Parameter(Mandatory=$true)][string]$SelectedPath, [AllowNull()][object]$ExistingImage = $null ) if ([IO.Path]::GetExtension($SelectedPath) -in @('.wim', '.esd')) { return [pscustomobject]@{ SourcePath = $SelectedPath; ImagePath = $SelectedPath; MountedImage = $null } } if (Test-Path -LiteralPath $SelectedPath -PathType Container) { foreach ($relative in @('sources\install.wim', 'sources\install.esd', 'Sources\install.wim', 'Sources\install.esd')) { $candidate = Join-Path $SelectedPath $relative if (Test-Path -LiteralPath $candidate -PathType Leaf) { return [pscustomobject]@{ SourcePath = $SelectedPath; ImagePath = $candidate; MountedImage = $null } } } throw "Aucun fichier Sources\install.wim ou Sources\install.esd dans : $SelectedPath" } if ([IO.Path]::GetExtension($SelectedPath) -ieq '.iso') { if ($ExistingImage -and $ExistingImage.SourcePath -and [string]::Equals([IO.Path]::GetFullPath([string]$ExistingImage.SourcePath), [IO.Path]::GetFullPath($SelectedPath), [StringComparison]::OrdinalIgnoreCase) -and $ExistingImage.MountedImage -and $ExistingImage.ImagePath) { return $ExistingImage } if (-not (Get-Command Mount-DiskImage -ErrorAction SilentlyContinue)) { throw "Le montage ISO n'est pas disponible dans cet environnement (commande Mount-DiskImage absente). Pour analyser cette source, utilise le dossier extrait ou un fichier install.wim/esd." } # Ne pas laisser Windows attribuer une lettre arbitraire (souvent D:) à l'ISO. $diskImage = Mount-DiskImage -ImagePath $SelectedPath -Access ReadOnly -NoDriveLetter -PassThru -ErrorAction Stop $mountInfo = [pscustomobject]@{ SourcePath = $SelectedPath ImagePath = $null MountedImage = $diskImage MountedPartition = $null DriveLetter = $null } try { # Partir des volumes explicitement associés à cette image évite de # confondre le lecteur CDROM virtuel et un disque physique dans WinPE. $volumes = @(Get-Volume -DiskImage $diskImage -ErrorAction Stop) if ($volumes.Count -eq 0) { throw 'Aucun volume ISO expose par Windows.' } $usedLetters = @( Get-Volume -ErrorAction SilentlyContinue | Where-Object { $_.DriveLetter } | ForEach-Object { ([string]$_.DriveLetter).ToUpperInvariant() } Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue | ForEach-Object { ([string]$_.Name).ToUpperInvariant() } ) $availableLetter = $null foreach ($candidateLetter in @('Z','Y','X','W','V','U','T','S','R','Q','P','O','N','M','L','K','J','I','H','G','F','E')) { if ($candidateLetter -notin @('C','D') -and $candidateLetter -notin $usedLetters) { $availableLetter = $candidateLetter break } } if (-not $availableLetter) { throw 'Aucune lettre de lecteur temporaire disponible pour le montage ISO.' } $checkedPaths = @() foreach ($volume in $volumes) { $volumeRoot = [string]$volume.Path if (-not $volumeRoot.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase) -and $volume.DriveLetter) { $mountVolCommand = Join-Path $env:WINDIR 'System32\mountvol.exe' if (-not (Test-Path -LiteralPath $mountVolCommand -PathType Leaf)) { $mountVolCommand = 'mountvol.exe' } $volumeMountPath = '{0}:\' -f [string]$volume.DriveLetter $volumeNameOutput = & $mountVolCommand $volumeMountPath '/L' 2>&1 $volumeGuidPath = @($volumeNameOutput | ForEach-Object { [string]$_ } | Where-Object { $_.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase) }) | Select-Object -First 1 if ($volumeGuidPath) { $volumeRoot = [string]$volumeGuidPath } } if ($volumeRoot -and $volumeRoot.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase)) { if (-not $volumeRoot.EndsWith('\')) { $volumeRoot += '\' } foreach ($relative in @('sources\install.wim', 'sources\install.esd')) { $candidate = $volumeRoot + $relative $checkedPaths += $candidate if (Test-Path -LiteralPath $candidate -PathType Leaf) { if ([string]$volume.DriveLetter -eq 'D') { $mountVolCommand = Join-Path $env:WINDIR 'System32\mountvol.exe' if (-not (Test-Path -LiteralPath $mountVolCommand -PathType Leaf)) { $mountVolCommand = 'mountvol.exe' } $removeOutput = & $mountVolCommand 'D:\' '/D' 2>&1 if ($LASTEXITCODE -ne 0) { throw "Impossible de libérer D: pour le volume ISO : $($removeOutput -join ' ')" } } $mountInfo.ImagePath = $candidate return $mountInfo } } } $partition = Get-Partition -Volume $volume -ErrorAction Stop if ($null -eq $partition) { continue } $existingDrivePaths = @($partition.AccessPaths | Where-Object { [string]$_ -match '^[A-Za-z]:\\$' }) $reusableDrivePath = $existingDrivePaths | Where-Object { [string]$_ -notmatch '^[CDcd]:\\$' } | Select-Object -First 1 if ($reusableDrivePath) { $driveAccessPath = [string]$reusableDrivePath $driveLetter = $driveAccessPath.Substring(0, 1).ToUpperInvariant() } else { foreach ($existingDrivePath in $existingDrivePaths) { Remove-PartitionAccessPath -InputObject $partition -AccessPath ([string]$existingDrivePath) -ErrorAction Stop | Out-Null } $driveLetter = $availableLetter $driveAccessPath = '{0}:\' -f $driveLetter Add-PartitionAccessPath -InputObject $partition -AccessPath $driveAccessPath -ErrorAction Stop } $mountInfo.MountedPartition = $partition $mountInfo.DriveLetter = $driveLetter foreach ($relative in @('sources\install.wim', 'sources\install.esd')) { $candidate = "${driveLetter}:\$relative" $checkedPaths += $candidate if (Test-Path -LiteralPath $candidate -PathType Leaf) { $mountInfo.ImagePath = $candidate return $mountInfo } } if ($driveLetter -eq $availableLetter) { Remove-PartitionAccessPath -InputObject $partition -AccessPath $driveAccessPath -ErrorAction SilentlyContinue | Out-Null } $mountInfo.MountedPartition = $null $mountInfo.DriveLetter = $null } throw "Aucun fichier Sources\install.wim ou Sources\install.esd trouvé dans l'ISO. Chemins vérifiés : $($checkedPaths -join ', ')." } catch { try { [void](Dismount-DeployCenterInstallImage -Image $mountInfo -ErrorAction SilentlyContinue) } catch { } throw } } throw "Type de source non pris en charge pour l'analyse : $SelectedPath" } function Dismount-DeployCenterInstallImage { param([AllowNull()][object]$Image) if ($null -eq $Image -or $null -eq $Image.MountedImage) { return } try { if ($Image.MountedPartition -and $Image.DriveLetter) { $driveAccessPath = '{0}:\' -f [string]$Image.DriveLetter Remove-PartitionAccessPath -InputObject $Image.MountedPartition -AccessPath $driveAccessPath -ErrorAction Stop | Out-Null } } finally { Dismount-DiskImage -InputObject $Image.MountedImage -ErrorAction Stop | Out-Null } } Export-ModuleMember -Function Find-WindowsSources, Get-WindowsImageEditions, Resolve-InstallImagePath, Dismount-DeployCenterInstallImage