#Requires -Version 5.1
<#
.SYNOPSIS
Filer Manager - analyse des dossiers Windows et rapporte les tailles des
dossiers, les chemins trop longs et les permissions NTFS, avec un export HTML.
.DESCRIPTION
S'exécute avec une interface graphique WinForms par défaut. Peut aussi
s'exécuter sans interface (mode headless) pour la planification :
.\filer-manager.ps1 -Path "D:\Shares\Public" -Output report.html -NoGui
Fonctionne sur Windows PowerShell 5.1 et PowerShell 7+.
.PARAMETER Path
Un ou plusieurs dossiers racines à analyser. S'il est fourni avec -Output (ou
avec -NoGui), le script s'exécute sans interface et écrit un rapport HTML.
.PARAMETER Output
Chemin du rapport HTML à écrire en mode headless.
.PARAMETER MaxPathLength
Longueur de chemin (caractères) à partir de laquelle un élément est signalé
comme « trop long ». Par défaut 260 (Windows MAX_PATH).
.PARAMETER PermissionDepth
Nombre de niveaux de dossiers sous chaque racine pour lesquels collecter les
permissions NTFS. 0 = racines uniquement, 1 = racines + enfants immédiats
(par défaut), etc.
.PARAMETER IncludeFilesInTree
Inclure les fichiers individuels (pas seulement les dossiers) dans
l'arborescence des tailles.
.PARAMETER GrantAccess
Lorsqu'un dossier/fichier ne peut pas être lu (accès refusé), s'approprie
automatiquement la propriété pour Administrators et accorde le FullControl à
BUILTIN\Administrators, puis réessaye. Nécessite une exécution en mode élevé.
Chaque modification est consignée dans le rapport.
.PARAMETER KeepGrants
Conserver l'accès accordé par -GrantAccess après l'analyse. Par défaut, l'outil
annule chaque modification une fois l'analyse terminée (en restaurant le
propriétaire et l'ACL d'origine là où ils ont pu être lus au préalable).
.PARAMETER NoGui
Forcer le mode sans interface (nécessite -Path et -Output).
.PARAMETER Category
Catégories de rapport à inclure dans le HTML. Par défaut 'All'. Passez toute
combinaison de : Tree (tailles des dossiers), LongPaths, Permissions, Grants
(journal des accès accordés), Errors. À utiliser pour garder de gros rapports
petits, p. ex. -Category Permissions ou -Category Tree,LongPaths.
.EXAMPLE
.\filer-manager.ps1
Lance l'interface graphique.
.EXAMPLE
.\filer-manager.ps1 -Path "\\FILER01\Data","D:\Profiles" -Output C:\Reports\filer.html -NoGui
.EXAMPLE
.\filer-manager.ps1 -Path "D:\Shares" -Output filer.html -NoGui -GrantAccess
Analyse D:\Shares en accordant l'accès Administrators aux éléments refusés afin
que l'analyse puisse se terminer, puis annule ces modifications ensuite.
#>
[CmdletBinding()]
param(
[string[]]$Path,
[string]$Output,
[int]$MaxPathLength = 260,
[int]$PermissionDepth = 1,
[switch]$IncludeFilesInTree,
[switch]$GrantAccess,
[switch]$KeepGrants,
[switch]$NoGui,
[ValidateSet('All', 'Tree', 'LongPaths', 'Permissions', 'Grants', 'Errors')]
[string[]]$Category = @('All')
)
# ============================================================================
# CORE (GUI-independent). Kept as a string so it can be dot-sourced both here
# and inside a background runspace used by the GUI to stay responsive.
# ============================================================================
$CoreFunctions = @'
function Format-Bytes {
param([long]$Bytes)
if ($Bytes -ge 1TB) { '{0:N2} TB' -f ($Bytes / 1TB) }
elseif ($Bytes -ge 1GB) { '{0:N2} GB' -f ($Bytes / 1GB) }
elseif ($Bytes -ge 1MB) { '{0:N2} MB' -f ($Bytes / 1MB) }
elseif ($Bytes -ge 1KB) { '{0:N2} KB' -f ($Bytes / 1KB) }
else { "$Bytes B" }
}
function Test-IsElevated {
# True when the current process is running with Administrator rights.
try {
$id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$pr = New-Object System.Security.Principal.WindowsPrincipal($id)
return $pr.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
} catch { return $false }
}
function Test-IsAccessDenied {
# Recognise an "access denied" failure across locales and PowerShell hosts.
param($ErrorRecord)
$ex = $ErrorRecord.Exception
if ($ex -is [System.UnauthorizedAccessException]) { return $true }
if ($ErrorRecord.CategoryInfo -and $ErrorRecord.CategoryInfo.Category -eq 'PermissionDenied') { return $true }
return $false
}
function Grant-AdminAccess {
<# Seizes ownership for Administrators and grants BUILTIN\Administrators
FullControl on a single item (no recursion), so a denied path can be
scanned. Captures the original owner/ACL first (when readable) so the
change can be reverted later. Records every attempt in $Grants. Returns
$true when something was changed (so the caller can retry). #>
param(
[string]$Path,
[string]$Reason,
[System.Collections.ArrayList]$Grants,
[System.Collections.ArrayList]$ScanErrors
)
# BUILTIN\Administrators well-known SID - locale-independent (the group is
# named differently on non-English systems).
$adminSid = 'S-1-5-32-544'
# Don't act twice on the same path.
foreach ($g in $Grants) { if ($g.Path -eq $Path) { return $g.Success } }
# Capture the original state for a possible revert (may be unreadable).
$origSddl = $null; $origOwner = $null
try {
$a = Get-Acl -LiteralPath $Path -ErrorAction Stop
$origSddl = $a.Sddl; $origOwner = [string]$a.Owner
} catch { }
$isDir = $true
try { $isDir = [bool]((Get-Item -LiteralPath $Path -Force -ErrorAction Stop).PSIsContainer) } catch { }
$changes = New-Object System.Collections.ArrayList
$errText = $null
# 1) Seize ownership for Administrators (icacls enables the needed privilege
# itself and, unlike takeown, has no locale-specific confirmation prompt).
$null = & icacls.exe $Path /setowner "*$adminSid" /C /Q 2>&1
if ($LASTEXITCODE -eq 0) { [void]$changes.Add('Propriétaire défini -> Administrators') }
else { $errText = "setowner a échoué (code $LASTEXITCODE)" }
# 2) Grant Administrators FullControl (inheritable on directories).
$perm = if ($isDir) { "*${adminSid}:(OI)(CI)F" } else { "*${adminSid}:F" }
$null = & icacls.exe $Path /grant $perm /C /Q 2>&1
if ($LASTEXITCODE -eq 0) { [void]$changes.Add('Administrators -> FullControl accordé') }
else { if ($errText) { $errText += '; ' }; $errText += "grant a échoué (code $LASTEXITCODE)" }
$success = ($changes.Count -gt 0)
[void]$Grants.Add([pscustomobject]@{
Path = $Path
IsDir = $isDir
Reason = $Reason
Changes = ($changes -join '; ')
OriginalOwner = $origOwner
OriginalSddl = $origSddl
Success = $success
Error = $errText
Reverted = $false
RevertError = $null
})
if (-not $success -and $errText) {
[void]$ScanErrors.Add([pscustomobject]@{ Path = $Path; Error = "Octroi automatique échoué : $errText" })
}
return $success
}
function Restore-Grants {
<# Reverts the changes made by Grant-AdminAccess, children before parents.
Restores the full original security descriptor (owner + ACL) when it was
captured; otherwise removes only the Administrators ACE we added. #>
param(
[System.Collections.ArrayList]$Grants,
[System.Collections.ArrayList]$ScanErrors,
[hashtable]$Progress
)
$adminSid = 'S-1-5-32-544'
for ($i = $Grants.Count - 1; $i -ge 0; $i--) {
$g = $Grants[$i]
if (-not $g.Success) { continue }
if ($Progress) { $Progress.Status = "Annulation de l'accès : $($g.Path)" }
$done = $false; $err = $null
if ($g.OriginalSddl) {
try {
if ($g.IsDir) { $sec = New-Object System.Security.AccessControl.DirectorySecurity }
else { $sec = New-Object System.Security.AccessControl.FileSecurity }
$sec.SetSecurityDescriptorSddlForm($g.OriginalSddl)
Set-Acl -LiteralPath $g.Path -AclObject $sec -ErrorAction Stop
$done = $true
} catch { $err = "Échec de la restauration Set-Acl : $($_.Exception.Message)" }
}
if (-not $done) {
# Best-effort fallback: drop the ACE we added and put the owner back.
try {
$null = & icacls.exe $g.Path /remove:g "*$adminSid" /C /Q 2>&1
if ($g.OriginalOwner) { $null = & icacls.exe $g.Path /setowner $g.OriginalOwner /C /Q 2>&1 }
if (-not $g.OriginalSddl) {
$done = $true
$err = "l'ACL d'origine était illisible ; seul l'ACE Administrators ajouté a été supprimé"
}
} catch { if ($err) { $err += '; ' }; $err += "Repli icacls échoué : $($_.Exception.Message)" }
}
$g.Reverted = $done
$g.RevertError = $err
if ($err) { [void]$ScanErrors.Add([pscustomobject]@{ Path = $g.Path; Error = "Annulation : $err" }) }
}
}
function Get-FolderNode {
<# Recursively builds a size tree. Accumulates long paths and errors via
synchronized collections passed by the caller. #>
param(
[string]$Path,
[int]$MaxLen,
[System.Collections.ArrayList]$LongPaths,
[System.Collections.ArrayList]$ScanErrors,
[hashtable]$Progress,
[int]$Depth = 0
)
$name = Split-Path -Path $Path -Leaf
if ([string]::IsNullOrEmpty($name)) { $name = $Path } # e.g. a drive root
$node = [ordered]@{
Name = $name
FullPath = $Path
Size = [long]0
FileCount = 0
FolderCount = 0
Depth = $Depth
Children = New-Object System.Collections.ArrayList
}
if ($Progress) { $Progress.Status = "Analyse : $Path" }
if ($Path.Length -ge $MaxLen) {
[void]$LongPaths.Add([pscustomobject]@{ Type = 'Dossier'; Length = $Path.Length; Path = $Path })
}
$entries = $null
$granted = $false
while ($true) {
try {
$entries = Get-ChildItem -LiteralPath $Path -Force -ErrorAction Stop
break
}
catch {
if ($GrantAccessFlag -and -not $granted -and (Test-IsAccessDenied $_)) {
$granted = $true
if ($Progress) { $Progress.Status = "Octroi de l'accès : $Path" }
if (Grant-AdminAccess -Path $Path -Reason 'énumérer le dossier' -Grants $GrantList -ScanErrors $ScanErrors) {
continue # retry once, now that access has been granted
}
}
[void]$ScanErrors.Add([pscustomobject]@{ Path = $Path; Error = $_.Exception.Message })
return [pscustomobject]$node
}
}
foreach ($entry in $entries) {
if ($entry.PSIsContainer) {
$child = Get-FolderNode -Path $entry.FullName -MaxLen $MaxLen `
-LongPaths $LongPaths -ScanErrors $ScanErrors `
-Progress $Progress -Depth ($Depth + 1)
$node.Size += $child.Size
$node.FileCount += $child.FileCount
$node.FolderCount += 1 + $child.FolderCount
[void]$node.Children.Add($child)
}
else {
$len = 0
try { $len = [long]$entry.Length } catch { $len = 0 }
$node.Size += $len
$node.FileCount += 1
if ($entry.FullName.Length -ge $MaxLen) {
[void]$LongPaths.Add([pscustomobject]@{ Type = 'Fichier'; Length = $entry.FullName.Length; Path = $entry.FullName })
}
if ($IncludeFilesInTreeFlag) {
[void]$node.Children.Add([pscustomobject]@{
Name = $entry.Name; FullPath = $entry.FullName; Size = $len
FileCount = 0; FolderCount = 0; Depth = $Depth + 1; IsFile = $true
Children = (New-Object System.Collections.ArrayList)
})
}
}
}
return [pscustomobject]$node
}
function Get-FolderPermissions {
param(
[string]$Path,
[int]$Depth,
[System.Collections.ArrayList]$ScanErrors,
[int]$Current = 0
)
$results = New-Object System.Collections.ArrayList
$granted = $false
while ($true) {
try {
$acl = Get-Acl -LiteralPath $Path -ErrorAction Stop
foreach ($ace in $acl.Access) {
[void]$results.Add([pscustomobject]@{
Folder = $Path
Owner = $acl.Owner
Identity = [string]$ace.IdentityReference
Rights = [string]$ace.FileSystemRights
Type = [string]$ace.AccessControlType
Inherited = $ace.IsInherited
})
}
break
}
catch {
if ($GrantAccessFlag -and -not $granted -and (Test-IsAccessDenied $_)) {
$granted = $true
if (Grant-AdminAccess -Path $Path -Reason 'lire les permissions' -Grants $GrantList -ScanErrors $ScanErrors) {
continue # retry once, now that the ACL is readable
}
}
[void]$ScanErrors.Add([pscustomobject]@{ Path = $Path; Error = "ACL : $($_.Exception.Message)" })
break
}
}
if ($Current -lt $Depth) {
$subDirs = $null
try { $subDirs = Get-ChildItem -LiteralPath $Path -Directory -Force -ErrorAction Stop } catch { $subDirs = @() }
foreach ($d in $subDirs) {
$child = Get-FolderPermissions -Path $d.FullName -Depth $Depth -ScanErrors $ScanErrors -Current ($Current + 1)
foreach ($r in $child) { [void]$results.Add($r) }
}
}
return $results
}
function Invoke-FilerScan {
param(
[string[]]$Paths,
[int]$MaxPathLength = 260,
[int]$PermissionDepth = 1,
[bool]$IncludeFilesInTree = $false,
[bool]$GrantAccess = $false,
[bool]$RevertGrants = $true,
[hashtable]$Progress
)
$script:IncludeFilesInTreeFlag = $IncludeFilesInTree
$script:GrantAccessFlag = $GrantAccess
$script:GrantList = New-Object System.Collections.ArrayList
$longPaths = New-Object System.Collections.ArrayList
$scanErrors = New-Object System.Collections.ArrayList
$permissions = New-Object System.Collections.ArrayList
$roots = New-Object System.Collections.ArrayList
foreach ($p in $Paths) {
if (-not (Test-Path -LiteralPath $p)) {
[void]$scanErrors.Add([pscustomobject]@{ Path = $p; Error = 'Chemin introuvable' })
continue
}
$fullPath = (Resolve-Path -LiteralPath $p).Path
$node = Get-FolderNode -Path $fullPath -MaxLen $MaxPathLength `
-LongPaths $longPaths -ScanErrors $scanErrors -Progress $Progress
[void]$roots.Add($node)
if ($Progress) { $Progress.Status = "Lecture des permissions : $fullPath" }
$perms = Get-FolderPermissions -Path $fullPath -Depth $PermissionDepth -ScanErrors $scanErrors
foreach ($r in $perms) { [void]$permissions.Add($r) }
}
# Revert the access we granted, once the scan (and ACL reads) are done.
if ($GrantAccess -and $RevertGrants -and $script:GrantList.Count -gt 0) {
if ($Progress) { $Progress.Status = 'Annulation des accès accordés...' }
Restore-Grants -Grants $script:GrantList -ScanErrors $scanErrors -Progress $Progress
}
$totalSize = ($roots | Measure-Object -Property Size -Sum).Sum
if (-not $totalSize) { $totalSize = 0 }
$grantsMade = @($script:GrantList | Where-Object { $_.Success })
return [pscustomobject]@{
Roots = $roots
LongPaths = ($longPaths | Sort-Object Length -Descending)
Permissions = $permissions
Grants = $script:GrantList
Errors = $scanErrors
Settings = [pscustomobject]@{
MaxPathLength = $MaxPathLength
PermissionDepth = $PermissionDepth
IncludeFilesInTree = $IncludeFilesInTree
GrantAccess = $GrantAccess
RevertGrants = $RevertGrants
}
Stats = [pscustomobject]@{
TotalSize = [long]$totalSize
TotalFiles = ($roots | Measure-Object -Property FileCount -Sum).Sum
TotalFolders = ($roots | Measure-Object -Property FolderCount -Sum).Sum
LongPaths = $longPaths.Count
Errors = $scanErrors.Count
Grants = $grantsMade.Count
}
Computer = $env:COMPUTERNAME
GeneratedAt = (Get-Date)
}
}
function ConvertTo-FilerHtmlReport {
param(
[Parameter(Mandatory)] $Scan,
[Parameter(Mandatory)] [string]$Path,
# Which report categories to include. 'All' (default) emits everything;
# otherwise pass any combination of Tree, LongPaths, Permissions, Grants, Errors.
[ValidateSet('All', 'Tree', 'LongPaths', 'Permissions', 'Grants', 'Errors')]
[string[]]$Categories = @('All')
)
function _enc([string]$s) { [System.Net.WebUtility]::HtmlEncode($s) }
# Resolve the requested categories into per-section switches.
$all = ($Categories -contains 'All') -or ($Categories.Count -eq 0)
$wantTree = $all -or ($Categories -contains 'Tree')
$wantLong = $all -or ($Categories -contains 'LongPaths')
$wantPerm = $all -or ($Categories -contains 'Permissions')
$wantGrants = $all -or ($Categories -contains 'Grants')
$wantErrors = $all -or ($Categories -contains 'Errors')
$sb = New-Object System.Text.StringBuilder
# ---- recursive tree renderer (folders sorted largest-first) ----
function _renderNode {
param($Node, [long]$ParentSize, [System.Text.StringBuilder]$Out)
$pct = if ($ParentSize -gt 0) { [math]::Round(($Node.Size / $ParentSize) * 100, 1) } else { 100 }
$sizeStr = Format-Bytes $Node.Size
$isFile = ($Node.PSObject.Properties.Name -contains 'IsFile' -and $Node.IsFile)
$icon = if ($isFile) { '📄' } else { '📁' }
$meta = if ($isFile) { '' } else { " $($Node.FolderCount) dossiers, $($Node.FileCount) fichiers " }
$kids = @($Node.Children | Where-Object { $_ } )
$summary = "$icon $(_enc $Node.Name) $sizeStr " +
"$pct% $meta"
if ($kids.Count -gt 0) {
$open = if ($Node.Depth -eq 0) { ' open' } else { '' }
[void]$Out.Append("$summary ")
foreach ($c in ($kids | Sort-Object @{E={$_.Size}} -Descending)) {
_renderNode -Node $c -ParentSize $Node.Size -Out $Out
}
[void]$Out.Append("
")
}
else {
[void]$Out.Append("
$summary
")
}
}
$treeSb = New-Object System.Text.StringBuilder
if ($wantTree) {
foreach ($root in $Scan.Roots) { _renderNode -Node $root -ParentSize $root.Size -Out $treeSb }
}
# ---- long paths ----
$longRows = New-Object System.Text.StringBuilder
if ($wantLong) {
foreach ($lp in $Scan.LongPaths) {
[void]$longRows.Append("$($lp.Length) $(_enc $lp.Type) $(_enc $lp.Path) ")
}
if ($Scan.LongPaths.Count -eq 0) { [void]$longRows.Append("Aucun chemin égal ou supérieur à $($Scan.Settings.MaxPathLength) caractères. ✅ ") }
}
# ---- permissions (grouped by folder) ----
$permSb = New-Object System.Text.StringBuilder
if ($wantPerm) {
$byFolder = $Scan.Permissions | Group-Object Folder
foreach ($grp in $byFolder) {
$owner = ($grp.Group | Select-Object -First 1).Owner
[void]$permSb.Append("$(_enc $grp.Name) Propriétaire : $(_enc $owner) ")
[void]$permSb.Append("Identité Droits Type Hérité ")
foreach ($ace in $grp.Group) {
$cls = if ($ace.Type -eq 'Deny') { ' class=deny' } else { '' }
[void]$permSb.Append("$(_enc $ace.Identity) $(_enc $ace.Rights) $(_enc $ace.Type) $($ace.Inherited) ")
}
[void]$permSb.Append("
")
}
if ($byFolder.Count -eq 0) { [void]$permSb.Append("Aucune permission collectée.
") }
}
# ---- granted access (auto-remediation log) ----
$grantSb = New-Object System.Text.StringBuilder
$grantRows = @($Scan.Grants | Where-Object { $_.Success })
if ($wantGrants) {
foreach ($g in $grantRows) {
if ($g.Reverted) { $state = "annulé " }
elseif ($Scan.Settings.RevertGrants) { $state = "NON annulé$(if($g.RevertError){' - ' + (_enc $g.RevertError)}) " }
else { $state = "conservé " }
[void]$grantSb.Append("$(_enc $g.Path) $(_enc $g.Reason) $(_enc $g.Changes) $(_enc $g.OriginalOwner) $state ")
}
}
# ---- errors ----
$errSb = New-Object System.Text.StringBuilder
if ($wantErrors) {
foreach ($e in $Scan.Errors) {
[void]$errSb.Append("$(_enc $e.Path) $(_enc $e.Error) ")
}
}
$rootsList = ($Scan.Roots | ForEach-Object { _enc $_.FullPath }) -join ' '
$gen = $Scan.GeneratedAt.ToString('yyyy-MM-dd HH:mm:ss')
# When only some categories are exported, name them in the header.
$catLabels = [ordered]@{
Tree = 'Tailles des dossiers'; LongPaths = 'Chemins trop longs'; Permissions = 'Permissions'
Grants = 'Accès accordé'; Errors = 'Erreurs'
}
$included = @()
if ($wantTree) { $included += $catLabels.Tree }
if ($wantLong) { $included += $catLabels.LongPaths }
if ($wantPerm) { $included += $catLabels.Permissions }
if ($wantGrants -and $Scan.Settings.GrantAccess) { $included += $catLabels.Grants }
if ($wantErrors -and $Scan.Errors.Count -gt 0) { $included += $catLabels.Errors }
$catNote = if ($all) { '' } else { "Catégories : $(_enc ($included -join ', '))
" }
$html = @"
Rapport Filer Manager - $gen
$(Format-Bytes $Scan.Stats.TotalSize)
Taille totale
$('{0:N0}' -f $Scan.Stats.TotalFolders)
Dossiers
$('{0:N0}' -f $Scan.Stats.TotalFiles)
Fichiers
$($Scan.Stats.LongPaths)
Chemins trop longs
$($Scan.Stats.Errors)
Erreurs / refusés
$(if ($Scan.Settings.GrantAccess) { "
$($Scan.Stats.Grants)
Accès accordé
" })
$(if ($wantTree) { @"
📊 Tailles des dossiers
$($treeSb.ToString())
"@ })
$(if ($wantLong) { @"
"@ })
$(if ($wantPerm) { @"
🔐 Permissions
$($permSb.ToString())
"@ })
$(if ($wantGrants -and $Scan.Settings.GrantAccess) { @"
🔨 Accès accordé pour terminer l'analyse
Éléments qui étaient refusés, sur lesquels la propriété/le FullControl Administrators a été appliqué afin de pouvoir les analyser.
Mode : $(if ($Scan.Settings.RevertGrants) { "annuler après l'analyse" } else { 'conserver les modifications' }).
$(if ($grantRows.Count -gt 0) { @"
"@ } else { "Aucune modification d'accès n'était nécessaire. ✅
" })
"@ })
$(if ($wantErrors -and $Scan.Errors.Count -gt 0) { @"
❌ Erreurs et accès refusé
"@ })
Filer Manager · rapport autonome · $gen
"@
Set-Content -LiteralPath $Path -Value $html -Encoding UTF8
return $Path
}
'@
# Dot-source core functions into the current scope (for headless + GUI export).
. ([scriptblock]::Create($CoreFunctions))
# ============================================================================
# HEADLESS MODE
# ============================================================================
$runHeadless = $NoGui -or ($Path -and $Output)
if ($runHeadless) {
if (-not $Path) { throw "Le mode sans interface requiert -Path." }
if (-not $Output) { throw "Le mode sans interface requiert -Output." }
if ($GrantAccess -and -not (Test-IsElevated)) {
Write-Warning "-GrantAccess nécessite des droits Administrateur ; ce processus n'est pas élevé. Les éléments refusés peuvent ne pas être corrigés."
}
Write-Host "Analyse de $($Path -join ', ') en cours ..." -ForegroundColor Cyan
$progress = [hashtable]::Synchronized(@{ Status = '' })
$scan = Invoke-FilerScan -Paths $Path -MaxPathLength $MaxPathLength `
-PermissionDepth $PermissionDepth `
-IncludeFilesInTree:$IncludeFilesInTree `
-GrantAccess:$GrantAccess -RevertGrants:(-not $KeepGrants) -Progress $progress
$out = ConvertTo-FilerHtmlReport -Scan $scan -Path $Output -Categories $Category
Write-Host "Rapport généré : $out" -ForegroundColor Green
Write-Host (" {0} au total, {1} dossiers, {2} fichiers, {3} chemins trop longs, {4} erreurs" -f `
(Format-Bytes $scan.Stats.TotalSize), $scan.Stats.TotalFolders, $scan.Stats.TotalFiles,
$scan.Stats.LongPaths, $scan.Stats.Errors)
if ($GrantAccess) {
Write-Host (" {0} élément(s) ont reçu un accès ({1})" -f `
$scan.Stats.Grants, $(if ($KeepGrants) { 'conservé' } else { 'annulé' })) -ForegroundColor Yellow
}
return
}
# ============================================================================
# GUI MODE
# ============================================================================
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()
$form = New-Object System.Windows.Forms.Form
$form.Text = 'Filer Manager'
$form.Size = New-Object System.Drawing.Size(960, 720)
$form.StartPosition = 'CenterScreen'
$form.MinimumSize = New-Object System.Drawing.Size(760, 560)
# ---- top: folder list + add/remove ----
$lblFolders = New-Object System.Windows.Forms.Label
$lblFolders.Text = 'Dossiers à analyser :'
$lblFolders.Location = '12,12'; $lblFolders.AutoSize = $true
$form.Controls.Add($lblFolders)
$lstFolders = New-Object System.Windows.Forms.ListBox
$lstFolders.Location = '12,32'; $lstFolders.Size = '700,84'
$lstFolders.Anchor = 'Top,Left,Right'
$lstFolders.HorizontalScrollbar = $true
$form.Controls.Add($lstFolders)
$btnAdd = New-Object System.Windows.Forms.Button
$btnAdd.Text = 'Ajouter...'; $btnAdd.Location = '724,32'; $btnAdd.Size = '110,28'
$btnAdd.Anchor = 'Top,Right'
$form.Controls.Add($btnAdd)
$btnRemove = New-Object System.Windows.Forms.Button
$btnRemove.Text = 'Supprimer'; $btnRemove.Location = '724,66'; $btnRemove.Size = '110,28'
$btnRemove.Anchor = 'Top,Right'
$form.Controls.Add($btnRemove)
# ---- settings row ----
$lblMax = New-Object System.Windows.Forms.Label
$lblMax.Text = 'Longueur max :'; $lblMax.Location = '12,128'; $lblMax.AutoSize = $true
$form.Controls.Add($lblMax)
$numMax = New-Object System.Windows.Forms.NumericUpDown
$numMax.Location = '118,126'; $numMax.Size = '70,24'
$numMax.Minimum = 1; $numMax.Maximum = 32767; $numMax.Value = $MaxPathLength
$form.Controls.Add($numMax)
$lblDepth = New-Object System.Windows.Forms.Label
$lblDepth.Text = 'Profondeur :'; $lblDepth.Location = '202,128'; $lblDepth.AutoSize = $true
$form.Controls.Add($lblDepth)
$numDepth = New-Object System.Windows.Forms.NumericUpDown
$numDepth.Location = '288,126'; $numDepth.Size = '60,24'
$numDepth.Minimum = 0; $numDepth.Maximum = 20; $numDepth.Value = $PermissionDepth
$form.Controls.Add($numDepth)
$chkFiles = New-Object System.Windows.Forms.CheckBox
$chkFiles.Text = 'Inclure les fichiers'; $chkFiles.Location = '362,127'; $chkFiles.AutoSize = $true
$chkFiles.Checked = [bool]$IncludeFilesInTree
$form.Controls.Add($chkFiles)
# ---- auto-grant row ----
$chkGrant = New-Object System.Windows.Forms.CheckBox
$chkGrant.Text = "Accorder l'accès Administrators aux éléments refusés"
$chkGrant.Location = '12,156'; $chkGrant.AutoSize = $true
$chkGrant.Checked = [bool]$GrantAccess
$form.Controls.Add($chkGrant)
$chkRevert = New-Object System.Windows.Forms.CheckBox
$chkRevert.Text = "Annuler les modifications après l'analyse"
$chkRevert.Location = '380,156'; $chkRevert.AutoSize = $true
$chkRevert.Checked = (-not $KeepGrants)
$chkRevert.Enabled = $chkGrant.Checked
$form.Controls.Add($chkRevert)
$chkGrant.Add_CheckedChanged({ $chkRevert.Enabled = $chkGrant.Checked })
$btnScan = New-Object System.Windows.Forms.Button
$btnScan.Text = 'Analyser'; $btnScan.Location = '600,124'; $btnScan.Size = '110,30'
$btnScan.Anchor = 'Top,Right'
$btnScan.BackColor = [System.Drawing.Color]::FromArgb(79,140,255)
$btnScan.ForeColor = [System.Drawing.Color]::White
$btnScan.FlatStyle = 'Flat'
$form.Controls.Add($btnScan)
$btnExport = New-Object System.Windows.Forms.Button
$btnExport.Text = 'Exporter HTML...'; $btnExport.Location = '724,124'; $btnExport.Size = '110,30'
$btnExport.Anchor = 'Top,Right'; $btnExport.Enabled = $false
$form.Controls.Add($btnExport)
# ---- tabs ----
$tabs = New-Object System.Windows.Forms.TabControl
$tabs.Location = '12,188'; $tabs.Size = '922,456'
$tabs.Anchor = 'Top,Bottom,Left,Right'
$form.Controls.Add($tabs)
$tabTree = New-Object System.Windows.Forms.TabPage; $tabTree.Text = 'Tailles des dossiers'
$tabLong = New-Object System.Windows.Forms.TabPage; $tabLong.Text = 'Chemins trop longs'
$tabPerm = New-Object System.Windows.Forms.TabPage; $tabPerm.Text = 'Permissions'
$tabGrant = New-Object System.Windows.Forms.TabPage; $tabGrant.Text = 'Accès accordé'
$tabs.TabPages.AddRange(@($tabTree, $tabLong, $tabPerm, $tabGrant))
$tree = New-Object System.Windows.Forms.TreeView
$tree.Dock = 'Fill'; $tree.HideSelection = $false
$tabTree.Controls.Add($tree)
$lvLong = New-Object System.Windows.Forms.ListView
$lvLong.Dock = 'Fill'; $lvLong.View = 'Details'; $lvLong.FullRowSelect = $true; $lvLong.GridLines = $true
[void]$lvLong.Columns.Add('Longueur', 70); [void]$lvLong.Columns.Add('Type', 70); [void]$lvLong.Columns.Add('Chemin', 760)
$tabLong.Controls.Add($lvLong)
$lvPerm = New-Object System.Windows.Forms.ListView
$lvPerm.Dock = 'Fill'; $lvPerm.View = 'Details'; $lvPerm.FullRowSelect = $true; $lvPerm.GridLines = $true
[void]$lvPerm.Columns.Add('Dossier', 320); [void]$lvPerm.Columns.Add('Identité', 200)
[void]$lvPerm.Columns.Add('Droits', 200); [void]$lvPerm.Columns.Add('Type', 60); [void]$lvPerm.Columns.Add('Hérité', 70)
$tabPerm.Controls.Add($lvPerm)
$lvGrant = New-Object System.Windows.Forms.ListView
$lvGrant.Dock = 'Fill'; $lvGrant.View = 'Details'; $lvGrant.FullRowSelect = $true; $lvGrant.GridLines = $true
[void]$lvGrant.Columns.Add('Chemin', 360); [void]$lvGrant.Columns.Add('Raison', 110)
[void]$lvGrant.Columns.Add('Modification', 240); [void]$lvGrant.Columns.Add("Propriétaire d'origine", 150)
[void]$lvGrant.Columns.Add('État', 90)
$tabGrant.Controls.Add($lvGrant)
# Filter toolbar for the permissions tab (docked above the list).
$permBar = New-Object System.Windows.Forms.Panel
$permBar.Dock = 'Top'; $permBar.Height = 30
$lblPermHint = New-Object System.Windows.Forms.Label
$lblPermHint.Text = 'Cliquez sur un en-tête de colonne pour filtrer ou trier.'
$lblPermHint.AutoSize = $true; $lblPermHint.Location = '6,7'
$lblPermHint.ForeColor = [System.Drawing.Color]::DimGray
$btnPermClear = New-Object System.Windows.Forms.Button
$btnPermClear.Text = 'Effacer les filtres'; $btnPermClear.Size = '130,24'
$btnPermClear.Dock = 'Right'; $btnPermClear.Enabled = $false
$permBar.Controls.Add($lblPermHint)
$permBar.Controls.Add($btnPermClear)
$tabPerm.Controls.Add($permBar)
$lvPerm.Add_ColumnClick({ param($s, $e) Show-PermColumnMenu -ColumnIndex $e.Column })
$btnPermClear.Add_Click({ $script:PermFilters = @{}; Update-PermView })
# ---- status bar ----
$status = New-Object System.Windows.Forms.StatusStrip
$statusLbl = New-Object System.Windows.Forms.ToolStripStatusLabel
$statusLbl.Text = 'Prêt. Ajoutez un ou plusieurs dossiers, puis Analyser.'
$progressBar = New-Object System.Windows.Forms.ToolStripProgressBar
$progressBar.Style = 'Marquee'; $progressBar.Visible = $false; $progressBar.Width = 140
[void]$status.Items.Add($statusLbl)
[void]$status.Items.Add($progressBar)
$form.Controls.Add($status)
# ---- state ----
$script:LastScan = $null
$script:Runspace = $null
$script:PowerShell = $null
$script:Handle = $null
$script:Shared = $null
# Permissions view state: source rows, active per-column filters, and sort.
$script:AllPerms = @()
$script:PermCols = @('Folder', 'Identity', 'Rights', 'Type', 'Inherited') # column index -> property
# Display labels for the permission columns (property name -> French header).
$script:PermColLabels = @{ Folder = 'Dossier'; Identity = 'Identité'; Rights = 'Droits'; Type = 'Type'; Inherited = 'Hérité' }
$script:PermFilters = @{} # property -> System.Collections.Generic.HashSet[string] of allowed values
$script:PermSort = @{ Prop = 'Folder'; Asc = $true }
# ---- helpers ----
function Add-TreeNodes {
param($Parent, $Node)
$sizeStr = Format-Bytes $Node.Size
$isFile = ($Node.PSObject.Properties.Name -contains 'IsFile' -and $Node.IsFile)
if ($isFile) {
$text = "$($Node.Name) - $sizeStr"
} else {
$text = "$($Node.Name) - $sizeStr ($($Node.FolderCount) dossiers, $($Node.FileCount) fichiers)"
}
$tn = New-Object System.Windows.Forms.TreeNode($text)
$tn.Tag = $Node.FullPath
[void]$Parent.Add($tn)
$kids = @($Node.Children | Where-Object { $_ } | Sort-Object @{E={$_.Size}} -Descending)
foreach ($c in $kids) { Add-TreeNodes -Parent $tn.Nodes -Node $c }
}
function Show-Results {
param($Scan)
$script:LastScan = $Scan
$tree.BeginUpdate(); $tree.Nodes.Clear()
foreach ($root in $Scan.Roots) { Add-TreeNodes -Parent $tree.Nodes -Node $root }
if ($tree.Nodes.Count -gt 0) { $tree.Nodes[0].Expand() }
$tree.EndUpdate()
$lvLong.BeginUpdate(); $lvLong.Items.Clear()
foreach ($lp in $Scan.LongPaths) {
$it = New-Object System.Windows.Forms.ListViewItem([string]$lp.Length)
[void]$it.SubItems.Add($lp.Type); [void]$it.SubItems.Add($lp.Path)
[void]$lvLong.Items.Add($it)
}
$lvLong.EndUpdate()
$script:AllPerms = @($Scan.Permissions)
$script:PermFilters = @{}
$script:PermSort = @{ Prop = 'Folder'; Asc = $true }
Update-PermView
$lvGrant.BeginUpdate(); $lvGrant.Items.Clear()
foreach ($g in @($Scan.Grants | Where-Object { $_.Success })) {
$it = New-Object System.Windows.Forms.ListViewItem([string]$g.Path)
[void]$it.SubItems.Add([string]$g.Reason)
[void]$it.SubItems.Add([string]$g.Changes)
[void]$it.SubItems.Add([string]$g.OriginalOwner)
if ($g.Reverted) { $state = 'annulé' }
elseif ($Scan.Settings.RevertGrants) { $state = 'NON annulé'; $it.ForeColor = [System.Drawing.Color]::Firebrick }
else { $state = 'conservé'; $it.ForeColor = [System.Drawing.Color]::DarkGoldenrod }
[void]$it.SubItems.Add($state)
[void]$lvGrant.Items.Add($it)
}
$lvGrant.EndUpdate()
$tabGrant.Text = if ($lvGrant.Items.Count -gt 0) { "Accès accordé ($($lvGrant.Items.Count))" } else { 'Accès accordé' }
$statusLbl.Text = ("Terminé. {0} au total, {1} dossiers, {2} fichiers, {3} chemins trop longs, {4} erreurs." -f `
(Format-Bytes $Scan.Stats.TotalSize), $Scan.Stats.TotalFolders, $Scan.Stats.TotalFiles,
$Scan.Stats.LongPaths, $Scan.Stats.Errors)
$btnExport.Enabled = $true
}
function Get-PermCellValue {
param($Ace, [string]$Prop)
if ($Prop -eq 'Inherited') { return [string]$Ace.Inherited }
return [string]$Ace.$Prop
}
function Update-PermView {
# Re-renders the permissions ListView from $script:AllPerms applying the
# active per-column filters and the current sort.
$rows = $script:AllPerms
foreach ($prop in $script:PermFilters.Keys) {
$allowed = $script:PermFilters[$prop]
$rows = @($rows | Where-Object { $allowed.Contains((Get-PermCellValue $_ $prop)) })
}
$sortProp = $script:PermSort.Prop
$rows = @($rows | Sort-Object @{ E = { Get-PermCellValue $_ $sortProp } })
if (-not $script:PermSort.Asc) { [array]::Reverse($rows) }
$lvPerm.BeginUpdate(); $lvPerm.Items.Clear()
foreach ($ace in $rows) {
$it = New-Object System.Windows.Forms.ListViewItem($ace.Folder)
[void]$it.SubItems.Add($ace.Identity); [void]$it.SubItems.Add($ace.Rights)
[void]$it.SubItems.Add($ace.Type); [void]$it.SubItems.Add([string]$ace.Inherited)
if ($ace.Type -eq 'Deny') { $it.ForeColor = [System.Drawing.Color]::Firebrick }
[void]$lvPerm.Items.Add($it)
}
$lvPerm.EndUpdate()
# Update column headers to show sort arrow and filter funnel.
for ($i = 0; $i -lt $script:PermCols.Count; $i++) {
$prop = $script:PermCols[$i]
$label = $script:PermColLabels[$prop]
if ($script:PermFilters.ContainsKey($prop)) { $label += ' (v)' }
if ($prop -eq $sortProp) { $label += $(if ($script:PermSort.Asc) { ' ^' } else { ' v' }) }
$lvPerm.Columns[$i].Text = $label
}
$shown = $lvPerm.Items.Count; $total = $script:AllPerms.Count
$nFilt = $script:PermFilters.Count
$btnPermClear.Enabled = ($nFilt -gt 0)
if ($script:AllPerms.Count -gt 0) {
$msg = "Permissions : $shown sur $total affichées"
if ($nFilt -gt 0) { $msg += " ($nFilt filtre$(if($nFilt -gt 1){'s'}) de colonne) - cliquez sur un en-tête pour filtrer/trier" }
else { $msg += " - cliquez sur un en-tête de colonne pour filtrer ou trier" }
$statusLbl.Text = $msg
}
}
function Show-PermColumnMenu {
param([int]$ColumnIndex)
if ($script:AllPerms.Count -eq 0) { return }
$prop = $script:PermCols[$ColumnIndex]
$menu = New-Object System.Windows.Forms.ContextMenuStrip
$menu.ShowImageMargin = $false
$asc = New-Object System.Windows.Forms.ToolStripMenuItem("Trier A -> Z")
$asc.Add_Click({ $script:PermSort = @{ Prop = $prop; Asc = $true }; Update-PermView }.GetNewClosure())
$desc = New-Object System.Windows.Forms.ToolStripMenuItem("Trier Z -> A")
$desc.Add_Click({ $script:PermSort = @{ Prop = $prop; Asc = $false }; Update-PermView }.GetNewClosure())
[void]$menu.Items.Add($asc); [void]$menu.Items.Add($desc)
[void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
# Select all / Clear toggles for the value checklist.
$selAll = New-Object System.Windows.Forms.ToolStripMenuItem("(Tout sélectionner)")
$clrAll = New-Object System.Windows.Forms.ToolStripMenuItem("(Tout effacer)")
[void]$menu.Items.Add($selAll); [void]$menu.Items.Add($clrAll)
[void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
# Distinct values for this column (from the unfiltered source).
$distinct = @($script:AllPerms | ForEach-Object { Get-PermCellValue $_ $prop } | Sort-Object -Unique)
$current = $script:PermFilters[$prop] # $null => everything allowed
$valueItems = New-Object System.Collections.ArrayList
foreach ($v in $distinct) {
$label = if ([string]::IsNullOrEmpty($v)) { '(vide)' } else { $v }
$mi = New-Object System.Windows.Forms.ToolStripMenuItem($label)
$mi.CheckOnClick = $true
$mi.Checked = ($null -eq $current) -or $current.Contains($v)
$mi.Tag = $v
[void]$menu.Items.Add($mi)
[void]$valueItems.Add($mi)
}
$selAll.Add_Click({ foreach ($m in $valueItems) { $m.Checked = $true } }.GetNewClosure())
$clrAll.Add_Click({ foreach ($m in $valueItems) { $m.Checked = $false } }.GetNewClosure())
[void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
$apply = New-Object System.Windows.Forms.ToolStripMenuItem("Appliquer le filtre")
$apply.Add_Click({
$checked = New-Object 'System.Collections.Generic.HashSet[string]'
foreach ($m in $valueItems) { if ($m.Checked) { [void]$checked.Add([string]$m.Tag) } }
if ($checked.Count -eq $valueItems.Count) { $script:PermFilters.Remove($prop) } # all selected = no filter
else { $script:PermFilters[$prop] = $checked }
Update-PermView
}.GetNewClosure())
$clear = New-Object System.Windows.Forms.ToolStripMenuItem("Supprimer le filtre de cette colonne")
$clear.Add_Click({ [void]$script:PermFilters.Remove($prop); Update-PermView }.GetNewClosure())
[void]$menu.Items.Add($apply); [void]$menu.Items.Add($clear)
# Keep the menu open while ticking value checkboxes or using Select/Clear all;
# close only on Sort / Apply / Remove filter.
$menu.Add_Closing({
param($s, $e)
if ($e.CloseReason -eq [System.Windows.Forms.ToolStripDropDownCloseReason]::ItemClicked) {
$clicked = $s.GetItemAt($s.PointToClient([System.Windows.Forms.Cursor]::Position))
if ($clicked -and (($valueItems -contains $clicked) -or ($clicked -eq $selAll) -or ($clicked -eq $clrAll))) {
$e.Cancel = $true
}
}
}.GetNewClosure())
# Show just under the clicked column header.
$x = 0
for ($i = 0; $i -lt $ColumnIndex; $i++) { $x += $lvPerm.Columns[$i].Width }
$menu.Show($lvPerm, $x, 4)
}
function Show-ExportDialog {
<# Modal picker for which report categories to export. Returns an array of
canonical category names (Tree/LongPaths/Permissions/Grants/Errors) or
$null if the user cancelled. Categories with no data are disabled. #>
param($Scan)
$hasGrants = @($Scan.Grants | Where-Object { $_.Success }).Count -gt 0
$hasErrors = $Scan.Errors.Count -gt 0
# label, canonical name, enabled?
$cats = @(
@{ Text = 'Tailles des dossiers'; Name = 'Tree'; Enabled = $true },
@{ Text = 'Chemins trop longs'; Name = 'LongPaths'; Enabled = $true },
@{ Text = 'Permissions'; Name = 'Permissions'; Enabled = $true },
@{ Text = 'Accès accordé'; Name = 'Grants'; Enabled = $hasGrants },
@{ Text = 'Erreurs / accès refusé'; Name = 'Errors'; Enabled = $hasErrors }
)
$dlg = New-Object System.Windows.Forms.Form
$dlg.Text = 'Exporter HTML - choisir les catégories'
$dlg.FormBorderStyle = 'FixedDialog'
$dlg.StartPosition = 'CenterParent'
$dlg.MaximizeBox = $false; $dlg.MinimizeBox = $false
$dlg.ClientSize = New-Object System.Drawing.Size(300, 250)
$lbl = New-Object System.Windows.Forms.Label
$lbl.Text = 'Inclure ces catégories dans le rapport :'
$lbl.Location = '14,12'; $lbl.AutoSize = $true
$dlg.Controls.Add($lbl)
$boxes = @()
$y = 40
foreach ($c in $cats) {
$cb = New-Object System.Windows.Forms.CheckBox
$cb.Text = $c.Text
$cb.Tag = $c.Name
$cb.Location = "20,$y"; $cb.AutoSize = $true
$cb.Checked = $c.Enabled
$cb.Enabled = $c.Enabled
if (-not $c.Enabled) { $cb.Text += ' (aucune donnée)' }
$dlg.Controls.Add($cb)
$boxes += $cb
$y += 26
}
$btnOk = New-Object System.Windows.Forms.Button
$btnOk.Text = 'Exporter...'; $btnOk.Size = '90,28'; $btnOk.Location = '104,210'
$btnOk.DialogResult = [System.Windows.Forms.DialogResult]::OK
$dlg.Controls.Add($btnOk); $dlg.AcceptButton = $btnOk
$btnCancel = New-Object System.Windows.Forms.Button
$btnCancel.Text = 'Annuler'; $btnCancel.Size = '90,28'; $btnCancel.Location = '200,210'
$btnCancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$dlg.Controls.Add($btnCancel); $dlg.CancelButton = $btnCancel
# Disable Export when nothing is ticked.
$sync = {
$btnOk.Enabled = @($boxes | Where-Object { $_.Checked }).Count -gt 0
}.GetNewClosure()
foreach ($cb in $boxes) { $cb.Add_CheckedChanged($sync) }
$result = $dlg.ShowDialog($form)
$picked = @($boxes | Where-Object { $_.Checked } | ForEach-Object { [string]$_.Tag })
$dlg.Dispose()
if ($result -ne [System.Windows.Forms.DialogResult]::OK -or $picked.Count -eq 0) { return $null }
# All categories ticked -> 'All' so the header shows the full report.
if ($picked.Count -eq $boxes.Count) { return @('All') }
return $picked
}
# ---- poll timer (reads background runspace) ----
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 200
$timer.Add_Tick({
if ($script:Shared) {
if ($script:Shared.Status) { $statusLbl.Text = $script:Shared.Status }
if ($script:Handle -and $script:Handle.IsCompleted) {
$timer.Stop()
try {
$result = $script:PowerShell.EndInvoke($script:Handle)
$scan = $script:Shared.Result
if ($scan) { Show-Results -Scan $scan }
else { $statusLbl.Text = 'Analyse terminée mais aucune donnée renvoyée.' }
}
catch {
[System.Windows.Forms.MessageBox]::Show("Échec de l'analyse :`n$($_.Exception.Message)", 'Filer Manager',
'OK', 'Error') | Out-Null
$statusLbl.Text = "Échec de l'analyse."
}
finally {
if ($script:PowerShell) { $script:PowerShell.Dispose() }
if ($script:Runspace) { $script:Runspace.Close(); $script:Runspace.Dispose() }
$script:PowerShell = $null; $script:Runspace = $null; $script:Handle = $null
$progressBar.Visible = $false
$btnScan.Enabled = $true; $btnAdd.Enabled = $true; $btnRemove.Enabled = $true
}
}
}
})
# ---- events ----
$btnAdd.Add_Click({
$dlg = New-Object System.Windows.Forms.FolderBrowserDialog
$dlg.Description = 'Sélectionnez un dossier à analyser'
if ($dlg.ShowDialog() -eq 'OK') {
if (-not $lstFolders.Items.Contains($dlg.SelectedPath)) { [void]$lstFolders.Items.Add($dlg.SelectedPath) }
}
})
$btnRemove.Add_Click({
if ($lstFolders.SelectedIndex -ge 0) { $lstFolders.Items.RemoveAt($lstFolders.SelectedIndex) }
})
$btnScan.Add_Click({
if ($lstFolders.Items.Count -eq 0) {
[System.Windows.Forms.MessageBox]::Show('Ajoutez au moins un dossier à analyser.', 'Filer Manager', 'OK', 'Information') | Out-Null
return
}
$paths = @($lstFolders.Items)
$maxLen = [int]$numMax.Value
$depth = [int]$numDepth.Value
$incFiles = [bool]$chkFiles.Checked
$grant = [bool]$chkGrant.Checked
$revert = [bool]$chkRevert.Checked
if ($grant -and -not (Test-IsElevated)) {
$msg = "L'octroi automatique nécessite des droits Administrateur, mais Filer Manager n'est pas exécuté en mode élevé.`n`n" +
"Les éléments refusés peuvent ne pas être corrigés. Continuer quand même ?"
if ([System.Windows.Forms.MessageBox]::Show($msg, 'Filer Manager', 'YesNo', 'Warning') -ne 'Yes') { return }
}
$btnScan.Enabled = $false; $btnAdd.Enabled = $false; $btnRemove.Enabled = $false; $btnExport.Enabled = $false
$progressBar.Visible = $true
$statusLbl.Text = 'Analyse en cours...'
$script:Shared = [hashtable]::Synchronized(@{ Status = 'Démarrage...'; Result = $null })
$script:Runspace = [runspacefactory]::CreateRunspace()
$script:Runspace.ApartmentState = 'STA'
$script:Runspace.Open()
$script:Runspace.SessionStateProxy.SetVariable('Shared', $script:Shared)
$script:PowerShell = [powershell]::Create()
$script:PowerShell.Runspace = $script:Runspace
[void]$script:PowerShell.AddScript($CoreFunctions)
[void]$script:PowerShell.AddScript(@'
param($Paths, $MaxLen, $Depth, $IncFiles, $Grant, $Revert, $Shared)
$Shared.Result = Invoke-FilerScan -Paths $Paths -MaxPathLength $MaxLen -PermissionDepth $Depth -IncludeFilesInTree $IncFiles -GrantAccess $Grant -RevertGrants $Revert -Progress $Shared
'@).AddArgument($paths).AddArgument($maxLen).AddArgument($depth).AddArgument($incFiles).AddArgument($grant).AddArgument($revert).AddArgument($script:Shared)
$script:Handle = $script:PowerShell.BeginInvoke()
$timer.Start()
})
$btnExport.Add_Click({
if (-not $script:LastScan) { return }
$categories = Show-ExportDialog -Scan $script:LastScan
if (-not $categories) { return } # cancelled or nothing selected
# Build a filename hint reflecting the chosen scope.
$scope = if ($categories -contains 'All') { 'all' } else { ($categories -join '-').ToLower() }
$dlg = New-Object System.Windows.Forms.SaveFileDialog
$dlg.Filter = 'Rapport HTML (*.html)|*.html'
$dlg.FileName = "filer-report-$scope-$(Get-Date -Format 'yyyyMMdd-HHmmss').html"
if ($dlg.ShowDialog() -eq 'OK') {
try {
$out = ConvertTo-FilerHtmlReport -Scan $script:LastScan -Path $dlg.FileName -Categories $categories
$statusLbl.Text = "Rapport enregistré : $out"
if ([System.Windows.Forms.MessageBox]::Show("Rapport enregistré dans :`n$out`n`nL'ouvrir maintenant ?", 'Filer Manager',
'YesNo', 'Question') -eq 'Yes') {
Start-Process $out
}
}
catch {
[System.Windows.Forms.MessageBox]::Show("Échec de l'export :`n$($_.Exception.Message)", 'Filer Manager', 'OK', 'Error') | Out-Null
}
}
})
# Pre-fill folders if -Path was supplied without -Output
if ($Path) { foreach ($p in $Path) { [void]$lstFolders.Items.Add($p) } }
[void]$form.ShowDialog()
$form.Dispose()