From ec2953001f48f65141ebae6f57f57d23a2b7fb8b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20QUEROL?= <2+kawa@not.obvious>
Date: Wed, 10 Jun 2026 14:37:39 +0200
Subject: [PATCH] Delete filer-manager.ps1
---
filer-manager.ps1 | 1762 ---------------------------------------------
1 file changed, 1762 deletions(-)
delete mode 100644 filer-manager.ps1
diff --git a/filer-manager.ps1 b/filer-manager.ps1
deleted file mode 100644
index dbb5f8b..0000000
--- a/filer-manager.ps1
+++ /dev/null
@@ -1,1762 +0,0 @@
-#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 export HTML et CSV.
-
-.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 CsvOutput
- Chemin de base du/des rapport(s) CSV à écrire en mode headless. Un fichier CSV
- distinct est produit par catégorie ayant des données (p. ex. base.csv ->
- base-tree.csv, base-permissions.csv, ...). Peut être combiné avec -Output pour
- écrire HTML et CSV en même temps, ou utilisé seul pour un export CSV uniquement.
-
-.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 HideInheritedChildPerms
- Omettre des rapports les permissions héritées portées par les dossiers
- enfants (les entrées héritées qui ne font que recopier celles du parent). Les
- permissions héritées des dossiers racines sont conservées, car le parent d'une
- racine est hors analyse et ces entrées sont la seule trace des droits en vigueur.
-
-.PARAMETER HideSystemPrincipals
- Omettre des rapports les permissions portées par des comptes/groupes système
- et intégrés bien connus (p. ex. NT AUTHORITY\SYSTEM, BUILTIN\Administrators,
- CREATOR OWNER, NT SERVICE\*). La détection se fait par SID (indépendante de la
- langue), avec un repli sur le nom. Utile pour ne garder que les identités
- métier dans l'audit des permissions.
-
-.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,
- [string]$CsvOutput,
- [int]$MaxPathLength = 260,
- [int]$PermissionDepth = 1,
- [switch]$IncludeFilesInTree,
- [switch]$HideInheritedChildPerms,
- [switch]$HideSystemPrincipals,
- [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 Resolve-PrincipalSid {
- # Best-effort translation of an ACE IdentityReference (an NTAccount or a
- # SecurityIdentifier) to its SID string. Returns $null when unresolvable
- # (e.g. an orphaned account from a deleted domain user).
- param($IdentityReference)
- try {
- if ($IdentityReference -is [System.Security.Principal.SecurityIdentifier]) { return $IdentityReference.Value }
- return $IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value
- } catch {
- # The reference may already be a raw SID string (unresolved account).
- try { return (New-Object System.Security.Principal.SecurityIdentifier ([string]$IdentityReference)).Value } catch { return $null }
- }
-}
-
-function Test-IsSystemPrincipal {
- # True for well-known built-in / system accounts and groups, so the audit can
- # focus on real business identities. Detection is by SID prefix (locale-
- # independent), with a name-based fallback for the few cases where the SID
- # could not be resolved. Examples: NT AUTHORITY\SYSTEM (S-1-5-18),
- # BUILTIN\Administrators (S-1-5-32-544), CREATOR OWNER (S-1-3-*),
- # NT SERVICE\TrustedInstaller (S-1-5-80-*).
- param([string]$Identity, [string]$Sid)
-
- if ($Sid) {
- switch -Regex ($Sid) {
- '^S-1-5-(18|19|20)$' { return $true } # SYSTEM / LOCAL SERVICE / NETWORK SERVICE
- '^S-1-5-(6|9|17)$' { return $true } # SERVICE / Enterprise DCs / IUSR
- '^S-1-5-32-' { return $true } # BUILTIN\* (Administrators, Users, ...)
- '^S-1-5-(80|83|90|96)-' { return $true } # NT SERVICE / VM / Window Manager / Font driver
- '^S-1-3-' { return $true } # CREATOR OWNER / CREATOR GROUP
- }
- }
-
- # Name fallback: only when the SID could not be resolved (offline domain,
- # broken trust). When a SID is known it is authoritative, so groups like
- # Authenticated Users (S-1-5-11) are intentionally not treated as system even
- # though their name carries the NT AUTHORITY prefix. Covers common
- # English/French/German forms of the built-in domains and standalone principals.
- if (-not $Sid -and $Identity) {
- $id = $Identity.Trim()
- foreach ($p in @('NT AUTHORITY\', 'AUTORITE NT\', "AUTORIT$([char]0xC9) NT\", 'NT-AUTORIT', 'BUILTIN\', 'NT SERVICE\', 'AUTORITE DE SECURITE')) {
- if ($id.StartsWith($p, [System.StringComparison]::OrdinalIgnoreCase)) { return $true }
- }
- foreach ($e in @('SYSTEM', 'CREATOR OWNER', 'CREATOR GROUP', 'TrustedInstaller')) {
- if ($id.Equals($e, [System.StringComparison]::OrdinalIgnoreCase)) { 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
- Sid = Resolve-PrincipalSid $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 Get-FilerRootPathSet {
- # Case-insensitive set of the scan-root full paths. Used to decide which
- # permission rows sit on a scan root (vs. a child folder).
- param($Scan)
- $set = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
- foreach ($r in $Scan.Roots) { [void]$set.Add([string]$r.FullPath) }
- return $set
-}
-
-function Select-FilerPerms {
- # Returns the scan's permission ACEs, optionally dropping inherited ACEs that
- # sit on a child folder and/or ACEs held by well-known system principals.
- # Inherited ACEs on the scan roots are always kept: a root's parent is outside
- # the scan, so those entries are the only record of the permissions in effect there.
- param($Scan, [bool]$HideInheritedChildPerms, [bool]$HideSystemPrincipals)
- $rows = @($Scan.Permissions)
- if ($HideInheritedChildPerms) {
- $roots = Get-FilerRootPathSet -Scan $Scan
- $rows = @($rows | Where-Object { (-not $_.Inherited) -or $roots.Contains([string]$_.Folder) })
- }
- if ($HideSystemPrincipals) {
- $rows = @($rows | Where-Object { -not (Test-IsSystemPrincipal -Identity $_.Identity -Sid $_.Sid) })
- }
- return $rows
-}
-
-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'),
- # When set, inherited permissions on child folders are omitted (roots keep theirs).
- [bool]$HideInheritedChildPerms = $false,
- # When set, ACEs held by well-known system/built-in principals are omitted.
- [bool]$HideSystemPrincipals = $false
- )
-
- 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 (nested folder tree; every level is collapsible) ----
- $permSb = New-Object System.Text.StringBuilder
- if ($wantPerm) {
- # Canonicalise so 8.3 / trailing-slash / casing differences between a folder
- # and its parent (from Get-ChildItem) still line up when building the tree.
- function _canon([string]$p) { try { ([System.IO.Path]::GetFullPath($p)).TrimEnd('\') } catch { $p.TrimEnd('\') } }
- $byFolder = (Select-FilerPerms -Scan $Scan -HideInheritedChildPerms $HideInheritedChildPerms -HideSystemPrincipals $HideSystemPrincipals) | Group-Object Folder
-
- # One node per folder, keyed by canonical path.
- $nodes = @{}
- foreach ($grp in $byFolder) {
- $canon = _canon ([string]$grp.Name)
- $nodes[$canon] = [pscustomobject]@{
- Canon = $canon
- Display = [string]$grp.Name
- Owner = ($grp.Group | Select-Object -First 1).Owner
- Aces = $grp.Group
- Children = New-Object System.Collections.ArrayList
- }
- }
- # Attach each folder to its nearest ancestor that also has permissions; the
- # ones with no such ancestor are the tree's top-level (scan-root) nodes.
- $tops = New-Object System.Collections.ArrayList
- foreach ($node in $nodes.Values) {
- $parent = [System.IO.Path]::GetDirectoryName($node.Canon)
- $attached = $false
- while ($parent) {
- if ($nodes.ContainsKey($parent)) { [void]$nodes[$parent].Children.Add($node); $attached = $true; break }
- $up = [System.IO.Path]::GetDirectoryName($parent)
- if ($up -eq $parent) { break }
- $parent = $up
- }
- if (-not $attached) { [void]$tops.Add($node) }
- }
-
- function _renderPermNode {
- param($Node, [bool]$IsTop, [System.Text.StringBuilder]$Out)
- # Top-level nodes show their full path; nested ones just the leaf name.
- $label = if ($IsTop) { $Node.Display } else { Split-Path -Leaf $Node.Display }
- $topCls = if ($IsTop) { ' top' } else { '' }
- [void]$Out.Append("$(_enc $label) Propriétaire : $(_enc $Node.Owner) ")
- [void]$Out.Append("Identité Droits Type Hérité ")
- foreach ($ace in $Node.Aces) {
- $cls = if ($ace.Type -eq 'Deny') { ' class=deny' } else { '' }
- $sys = Test-IsSystemPrincipal -Identity $ace.Identity -Sid $ace.Sid
- [void]$Out.Append("$(_enc $ace.Identity) $(_enc $ace.Rights) $(_enc $ace.Type) $($ace.Inherited) ")
- }
- [void]$Out.Append("
")
- if ($Node.Children.Count -gt 0) {
- [void]$Out.Append("")
- foreach ($c in ($Node.Children | Sort-Object Display)) { _renderPermNode -Node $c -IsTop $false -Out $Out }
- [void]$Out.Append("
")
- }
- [void]$Out.Append(" ")
- }
-
- foreach ($t in ($tops | Sort-Object Display)) { _renderPermNode -Node $t -IsTop $true -Out $permSb }
- 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) { @"
-
-"@ })
-
- $(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
-}
-
-function ConvertTo-FilerCsvReport {
- <# Writes the scan results to CSV. Because the report categories have very
- different shapes, each selected category with data is written to its own
- file, derived from the base $Path (e.g. report.csv -> report-tree.csv,
- report-permissions.csv, ...). Returns the list of files written. The list
- separator follows the current culture so the files open cleanly in the
- local Excel (';' on French systems, ',' elsewhere). #>
- param(
- [Parameter(Mandatory)] $Scan,
- [Parameter(Mandatory)] [string]$Path,
- [ValidateSet('All', 'Tree', 'LongPaths', 'Permissions', 'Grants', 'Errors')]
- [string[]]$Categories = @('All'),
- # When set, inherited permissions on child folders are omitted (roots keep theirs).
- [bool]$HideInheritedChildPerms = $false,
- # When set, ACEs held by well-known system/built-in principals are omitted.
- [bool]$HideSystemPrincipals = $false
- )
-
- $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')
-
- $delim = (Get-Culture).TextInfo.ListSeparator
- if ([string]::IsNullOrEmpty($delim)) { $delim = ',' }
-
- # Derive a per-category file path from the base path.
- $dir = Split-Path -Path $Path -Parent
- $base = [System.IO.Path]::GetFileNameWithoutExtension($Path)
- if ([string]::IsNullOrEmpty($base)) { $base = 'filer-report' }
-
- $written = New-Object System.Collections.ArrayList
- function _writeCsv {
- param($Rows, [string]$Suffix)
- $rows = @($Rows | Where-Object { $null -ne $_ })
- if ($rows.Count -eq 0) { return } # don't emit empty files
- $name = "$base-$Suffix.csv"
- $p = if ([string]::IsNullOrEmpty($dir)) { $name } else { Join-Path $dir $name }
- $rows | Export-Csv -LiteralPath $p -NoTypeInformation -Encoding UTF8 -Delimiter $delim
- [void]$written.Add($p)
- }
-
- # ---- tree (flattened depth-first, folders largest-first like the HTML) ----
- if ($wantTree) {
- $treeRows = New-Object System.Collections.ArrayList
- function _flattenNode {
- param($Node, [string]$Root, [System.Collections.ArrayList]$Acc)
- $isFile = ($Node.PSObject.Properties.Name -contains 'IsFile' -and $Node.IsFile)
- [void]$Acc.Add([pscustomobject]@{
- Racine = $Root
- Chemin = $Node.FullPath
- Nom = $Node.Name
- Type = if ($isFile) { 'Fichier' } else { 'Dossier' }
- Profondeur = $Node.Depth
- TailleOctets = [long]$Node.Size
- Taille = Format-Bytes $Node.Size
- Fichiers = $Node.FileCount
- Dossiers = $Node.FolderCount
- })
- foreach ($c in @($Node.Children | Where-Object { $_ } | Sort-Object @{E={$_.Size}} -Descending)) {
- _flattenNode -Node $c -Root $Root -Acc $Acc
- }
- }
- foreach ($root in $Scan.Roots) { _flattenNode -Node $root -Root $root.FullPath -Acc $treeRows }
- _writeCsv -Rows $treeRows -Suffix 'tree'
- }
-
- # ---- long paths ----
- if ($wantLong) {
- $longRows = foreach ($lp in $Scan.LongPaths) {
- [pscustomobject]@{ Longueur = $lp.Length; Type = $lp.Type; Chemin = $lp.Path }
- }
- _writeCsv -Rows $longRows -Suffix 'longpaths'
- }
-
- # ---- permissions ----
- if ($wantPerm) {
- $permRows = foreach ($ace in (Select-FilerPerms -Scan $Scan -HideInheritedChildPerms $HideInheritedChildPerms -HideSystemPrincipals $HideSystemPrincipals)) {
- [pscustomobject]@{
- Dossier = $ace.Folder
- Proprietaire = $ace.Owner
- Identite = $ace.Identity
- Droits = $ace.Rights
- Type = $ace.Type
- Herite = $ace.Inherited
- }
- }
- _writeCsv -Rows $permRows -Suffix 'permissions'
- }
-
- # ---- granted access (successful remediations only, like the HTML) ----
- if ($wantGrants -and $Scan.Settings.GrantAccess) {
- $grantRows = foreach ($g in @($Scan.Grants | Where-Object { $_.Success })) {
- if ($g.Reverted) { $state = 'annulé' }
- elseif ($Scan.Settings.RevertGrants) { $state = 'NON annulé' }
- else { $state = 'conservé' }
- [pscustomobject]@{
- Chemin = $g.Path
- Raison = $g.Reason
- Modification = $g.Changes
- ProprietaireOrigine = $g.OriginalOwner
- Etat = $state
- ErreurAnnulation = $g.RevertError
- }
- }
- _writeCsv -Rows $grantRows -Suffix 'grants'
- }
-
- # ---- errors ----
- if ($wantErrors) {
- $errRows = foreach ($e in $Scan.Errors) {
- [pscustomobject]@{ Chemin = $e.Path; Erreur = $e.Error }
- }
- _writeCsv -Rows $errRows -Suffix 'errors'
- }
-
- return $written.ToArray()
-}
-'@
-
-# Dot-source core functions into the current scope (for headless + GUI export).
-. ([scriptblock]::Create($CoreFunctions))
-
-# ============================================================================
-# HEADLESS MODE
-# ============================================================================
-$runHeadless = $NoGui -or ($Path -and ($Output -or $CsvOutput))
-if ($runHeadless) {
- if (-not $Path) { throw "Le mode sans interface requiert -Path." }
- if (-not $Output -and -not $CsvOutput) { throw "Le mode sans interface requiert -Output et/ou -CsvOutput." }
-
- 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
-
- if ($Output) {
- $out = ConvertTo-FilerHtmlReport -Scan $scan -Path $Output -Categories $Category -HideInheritedChildPerms:$HideInheritedChildPerms -HideSystemPrincipals:$HideSystemPrincipals
- Write-Host "Rapport HTML généré : $out" -ForegroundColor Green
- }
- if ($CsvOutput) {
- $csvFiles = @(ConvertTo-FilerCsvReport -Scan $scan -Path $CsvOutput -Categories $Category -HideInheritedChildPerms:$HideInheritedChildPerms -HideSystemPrincipals:$HideSystemPrincipals)
- if ($csvFiles.Count -gt 0) {
- Write-Host "Rapport(s) CSV généré(s) :" -ForegroundColor Green
- foreach ($f in $csvFiles) { Write-Host " $f" -ForegroundColor Green }
- } else {
- Write-Host "Aucun fichier CSV généré (catégories sans données)." -ForegroundColor Yellow
- }
- }
- 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...'; $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
-# Owner-draw the labels so we can paint a proportional "space taken" bar after
-# each node, mirroring the bars in the HTML report. The control still draws the
-# +/- expanders and connecting lines itself.
-$tree.DrawMode = 'OwnerDrawText'
-$tabTree.Controls.Add($tree)
-# Fill a node's real children the first time it is expanded (lazy loading keeps
-# the GUI responsive no matter how large the scanned tree is).
-$tree.Add_BeforeExpand({ param($s, $e) Expand-FilerTreeNode -TreeNode $e.Node })
-
-# Render each tree node: the label, then a bar showing the folder's share of its
-# parent's size (roots = 100%), then the percentage - matching the HTML report.
-$tree.Add_DrawNode({
- param($s, $e)
- $nodeData = $e.Node.Tag
- # Placeholder ("Chargement...") and not-yet-measured nodes: draw normally.
- if ($null -eq $nodeData -or $e.Bounds.Width -le 0 -or
- -not ($nodeData.PSObject.Properties.Name -contains 'Size')) {
- $e.DrawDefault = $true; return
- }
-
- $selected = ($e.State -band [System.Windows.Forms.TreeNodeStates]::Selected) -ne 0
- $foreColor = if ($selected) { [System.Drawing.SystemColors]::HighlightText } else { $tree.ForeColor }
- if ($selected) {
- $hl = New-Object System.Drawing.SolidBrush ([System.Drawing.SystemColors]::Highlight)
- $e.Graphics.FillRectangle($hl, $e.Bounds); $hl.Dispose()
- }
-
- $flags = [System.Windows.Forms.TextFormatFlags]::VerticalCenter -bor `
- [System.Windows.Forms.TextFormatFlags]::Left -bor `
- [System.Windows.Forms.TextFormatFlags]::NoPrefix
- [System.Windows.Forms.TextRenderer]::DrawText($e.Graphics, $e.Node.Text, $tree.Font, $e.Bounds, $foreColor, $flags)
-
- # Share of the parent folder's size (top-level roots have no parent = 100%).
- $pct = 100.0
- $parent = $e.Node.Parent
- if ($parent -and $parent.Tag -and
- ($parent.Tag.PSObject.Properties.Name -contains 'Size') -and $parent.Tag.Size -gt 0) {
- $pct = [math]::Round(($nodeData.Size / $parent.Tag.Size) * 100, 1)
- }
- if ($pct -lt 0) { $pct = 0 } elseif ($pct -gt 100) { $pct = 100 }
-
- # Bar just to the right of the label.
- $barW = 120; $barH = 9
- $barX = $e.Bounds.Right + 10
- $barY = $e.Bounds.Top + [int](($e.Bounds.Height - $barH) / 2)
-
- $track = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(228, 230, 235))
- $e.Graphics.FillRectangle($track, $barX, $barY, $barW, $barH); $track.Dispose()
-
- $fillW = [int][math]::Round($barW * $pct / 100)
- if ($fillW -gt 0) {
- $fill = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(59, 111, 212))
- $e.Graphics.FillRectangle($fill, $barX, $barY, $fillW, $barH); $fill.Dispose()
- }
- $pen = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(170, 175, 185))
- $e.Graphics.DrawRectangle($pen, $barX, $barY, $barW, $barH); $pen.Dispose()
-
- $pctRect = New-Object System.Drawing.Rectangle (($barX + $barW + 6), $e.Bounds.Top, 52, $e.Bounds.Height)
- [System.Windows.Forms.TextRenderer]::DrawText($e.Graphics, ('{0:N1}%' -f $pct), $tree.Font, $pctRect, $tree.ForeColor, $flags)
-})
-
-$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
-$chkPermHideInh = New-Object System.Windows.Forms.CheckBox
-$chkPermHideInh.Text = 'Masquer les permissions héritées (enfants)'
-$chkPermHideInh.AutoSize = $true; $chkPermHideInh.Dock = 'Right'
-$chkPermHideInh.Padding = '0,4,8,0'
-$chkPermHideSys = New-Object System.Windows.Forms.CheckBox
-$chkPermHideSys.Text = 'Masquer les comptes/groupes système'
-$chkPermHideSys.AutoSize = $true; $chkPermHideSys.Dock = 'Right'
-$chkPermHideSys.Padding = '0,4,8,0'
-$permBar.Controls.Add($lblPermHint)
-$permBar.Controls.Add($chkPermHideInh)
-$permBar.Controls.Add($chkPermHideSys)
-$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 })
-$chkPermHideInh.Add_CheckedChanged({ $script:PermHideInherited = $chkPermHideInh.Checked; Update-PermView })
-$chkPermHideSys.Add_CheckedChanged({ $script:PermHideSystem = $chkPermHideSys.Checked; 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
-
-# Background export state (report generation runs off the UI thread too).
-$script:ExportRunspace = $null
-$script:ExportPowerShell = $null
-$script:ExportHandle = $null
-$script:ExportShared = $null
-$script:ExportHtmlPath = $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 }
-# When $true, inherited ACEs on child folders are hidden (scan roots keep theirs).
-$script:PermHideInherited = $false
-# When $true, ACEs held by well-known system/built-in principals are hidden.
-$script:PermHideSystem = $false
-$script:PermRootPaths = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
-
-# ---- helpers ----
-function New-FilerTreeNode {
- # Creates a TreeNode for a single scan node WITHOUT recursing into its
- # children. A placeholder child is added so the [+] expander shows; the real
- # children are filled on demand by Expand-FilerTreeNode (see the tree's
- # BeforeExpand handler). This makes even very large trees appear instantly.
- param($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 # keep the scan node so its children can be filled lazily
- $kids = @($Node.Children | Where-Object { $_ })
- if ($kids.Count -gt 0) {
- # Placeholder (Tag stays $null, which marks the node as "not yet loaded").
- [void]$tn.Nodes.Add((New-Object System.Windows.Forms.TreeNode('Chargement...')))
- }
- return $tn
-}
-
-function Expand-FilerTreeNode {
- # Replaces the placeholder with the node's real children the first time it is
- # expanded. A node is "not yet loaded" while its single child has a $null Tag.
- param($TreeNode)
- if ($TreeNode.Nodes.Count -ne 1 -or $null -ne $TreeNode.Nodes[0].Tag) { return }
- $node = $TreeNode.Tag
- $TreeNode.TreeView.BeginUpdate()
- $TreeNode.Nodes.Clear()
- $kids = @($node.Children | Where-Object { $_ } | Sort-Object @{E={$_.Size}} -Descending)
- foreach ($c in $kids) { [void]$TreeNode.Nodes.Add((New-FilerTreeNode -Node $c)) }
- $TreeNode.TreeView.EndUpdate()
-}
-
-function Show-Results {
- param($Scan)
- $script:LastScan = $Scan
-
- $tree.BeginUpdate(); $tree.Nodes.Clear()
- foreach ($root in $Scan.Roots) { [void]$tree.Nodes.Add((New-FilerTreeNode -Node $root)) }
- if ($tree.Nodes.Count -gt 0) { $tree.Nodes[0].Expand() } # loads first level lazily
- $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:PermRootPaths = Get-FilerRootPathSet -Scan $Scan
- $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
- if ($script:PermHideInherited) {
- $rows = @($rows | Where-Object { (-not $_.Inherited) -or $script:PermRootPaths.Contains([string]$_.Folder) })
- }
- if ($script:PermHideSystem) {
- $rows = @($rows | Where-Object { -not (Test-IsSystemPrincipal -Identity $_.Identity -Sid $_.Sid) })
- }
- 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 the export: which report categories and which formats
- (HTML and/or CSV). Returns $null if the user cancelled, otherwise an object
- with .Categories (array of Tree/LongPaths/Permissions/Grants/Errors or
- @('All')), .Html and .Csv booleans. 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 - choisir les catégories et formats'
- $dlg.FormBorderStyle = 'FixedDialog'
- $dlg.StartPosition = 'CenterParent'
- $dlg.MaximizeBox = $false; $dlg.MinimizeBox = $false
- $dlg.ClientSize = New-Object System.Drawing.Size(320, 330)
-
- $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
- }
-
- # ---- format selection (HTML and/or CSV) ----
- $y += 8
- $lblFmt = New-Object System.Windows.Forms.Label
- $lblFmt.Text = 'Formats à générer :'
- $lblFmt.Location = "14,$y"; $lblFmt.AutoSize = $true
- $dlg.Controls.Add($lblFmt)
- $y += 26
-
- $chkHtml = New-Object System.Windows.Forms.CheckBox
- $chkHtml.Text = 'HTML (rapport unique)'; $chkHtml.Tag = 'Html'
- $chkHtml.Location = "20,$y"; $chkHtml.AutoSize = $true; $chkHtml.Checked = $true
- $dlg.Controls.Add($chkHtml)
- $y += 26
-
- $chkCsv = New-Object System.Windows.Forms.CheckBox
- $chkCsv.Text = 'CSV (un fichier par catégorie)'; $chkCsv.Tag = 'Csv'
- $chkCsv.Location = "20,$y"; $chkCsv.AutoSize = $true; $chkCsv.Checked = $false
- $dlg.Controls.Add($chkCsv)
- $y += 36
-
- $btnOk = New-Object System.Windows.Forms.Button
- $btnOk.Text = 'Exporter...'; $btnOk.Size = '90,28'; $btnOk.Location = "124,$y"
- $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 = "220,$y"
- $btnCancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
- $dlg.Controls.Add($btnCancel); $dlg.CancelButton = $btnCancel
-
- # Enable Export only when at least one category AND at least one format is ticked.
- $sync = {
- $anyCat = @($boxes | Where-Object { $_.Checked }).Count -gt 0
- $anyFmt = $chkHtml.Checked -or $chkCsv.Checked
- $btnOk.Enabled = $anyCat -and $anyFmt
- }.GetNewClosure()
- foreach ($cb in $boxes) { $cb.Add_CheckedChanged($sync) }
- $chkHtml.Add_CheckedChanged($sync)
- $chkCsv.Add_CheckedChanged($sync)
-
- $result = $dlg.ShowDialog($form)
- $picked = @($boxes | Where-Object { $_.Checked } | ForEach-Object { [string]$_.Tag })
- $wantHtml = $chkHtml.Checked
- $wantCsv = $chkCsv.Checked
- $dlg.Dispose()
-
- if ($result -ne [System.Windows.Forms.DialogResult]::OK -or $picked.Count -eq 0) { return $null }
- if (-not ($wantHtml -or $wantCsv)) { return $null }
- # All categories ticked -> 'All' so the header shows the full report.
- $categories = if ($picked.Count -eq $boxes.Count) { @('All') } else { $picked }
- return [pscustomobject]@{ Categories = $categories; Html = $wantHtml; Csv = $wantCsv }
-}
-
-# ---- 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
- }
- }
- }
-})
-
-# ---- poll timer (reads background export runspace) ----
-$exportTimer = New-Object System.Windows.Forms.Timer
-$exportTimer.Interval = 200
-$exportTimer.Add_Tick({
- if (-not $script:ExportHandle) { $exportTimer.Stop(); return }
- if (-not $script:ExportHandle.IsCompleted) { return }
- $exportTimer.Stop()
-
- $written = $null; $err = $null
- try {
- $script:ExportPowerShell.EndInvoke($script:ExportHandle) | Out-Null
- if ($script:ExportShared) { $written = $script:ExportShared.Written; $err = $script:ExportShared.Error }
- }
- catch { $err = $_.Exception.Message }
- finally {
- if ($script:ExportPowerShell) { $script:ExportPowerShell.Dispose() }
- if ($script:ExportRunspace) { $script:ExportRunspace.Close(); $script:ExportRunspace.Dispose() }
- $script:ExportPowerShell = $null; $script:ExportRunspace = $null; $script:ExportHandle = $null
- $progressBar.Visible = $false
- $btnScan.Enabled = $true; $btnAdd.Enabled = $true; $btnRemove.Enabled = $true
- $btnExport.Enabled = ($null -ne $script:LastScan)
- }
-
- $htmlPath = $script:ExportHtmlPath
- if ($err) {
- $statusLbl.Text = "Échec de l'export."
- [System.Windows.Forms.MessageBox]::Show("Échec de l'export :`n$err", 'Filer Manager', 'OK', 'Error') | Out-Null
- return
- }
-
- $written = @($written)
- if ($written.Count -eq 0) {
- $statusLbl.Text = 'Aucun fichier généré.'
- [System.Windows.Forms.MessageBox]::Show('Aucun fichier généré (catégories CSV sans données).',
- 'Filer Manager', 'OK', 'Information') | Out-Null
- return
- }
-
- $statusLbl.Text = "Export terminé : $($written.Count) fichier(s)."
- $list = ($written -join "`n")
- $prompt = if ($htmlPath) { "`n`nOuvrir le rapport HTML maintenant ?" } else { "`n`nOuvrir le dossier maintenant ?" }
- if ([System.Windows.Forms.MessageBox]::Show("Fichier(s) enregistré(s) :`n$list$prompt", 'Filer Manager',
- 'YesNo', 'Question') -eq 'Yes') {
- if ($htmlPath) { Start-Process $htmlPath }
- else { Start-Process (Split-Path -Path $written[0] -Parent) }
- }
-})
-
-# ---- 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 }
-
- $choice = Show-ExportDialog -Scan $script:LastScan
- if (-not $choice) { return } # cancelled or nothing selected
- $categories = $choice.Categories
-
- # Build a filename hint reflecting the chosen scope.
- $scope = if ($categories -contains 'All') { 'all' } else { ($categories -join '-').ToLower() }
- $stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
- $baseName = "filer-report-$scope-$stamp"
-
- $dlg = New-Object System.Windows.Forms.SaveFileDialog
- if ($choice.Html -and $choice.Csv) {
- $dlg.Title = 'Enregistrer les rapports (nom de base) - HTML + CSV'
- $dlg.Filter = 'Rapports HTML + CSV|*.html'
- $dlg.FileName = "$baseName.html"
- }
- elseif ($choice.Html) {
- $dlg.Title = 'Enregistrer le rapport HTML'
- $dlg.Filter = 'Rapport HTML (*.html)|*.html'
- $dlg.FileName = "$baseName.html"
- }
- else {
- $dlg.Title = 'Enregistrer le(s) rapport(s) CSV (nom de base)'
- $dlg.Filter = 'Rapport CSV (*.csv)|*.csv'
- $dlg.FileName = "$baseName.csv"
- }
-
- if ($dlg.ShowDialog() -ne 'OK') { return }
-
- # Generate the report(s) on a background runspace so the GUI stays responsive;
- # rendering a large scan to HTML/CSV can take several seconds.
- $htmlPath = if ($choice.Html) { [System.IO.Path]::ChangeExtension($dlg.FileName, '.html') } else { $null }
- $csvBase = $dlg.FileName
- $script:ExportHtmlPath = $htmlPath
-
- $btnScan.Enabled = $false; $btnAdd.Enabled = $false; $btnRemove.Enabled = $false; $btnExport.Enabled = $false
- $progressBar.Visible = $true
- $statusLbl.Text = 'Export en cours...'
-
- $script:ExportShared = [hashtable]::Synchronized(@{ Written = $null; Error = $null })
- $script:ExportRunspace = [runspacefactory]::CreateRunspace()
- $script:ExportRunspace.ApartmentState = 'STA'
- $script:ExportRunspace.Open()
-
- $script:ExportPowerShell = [powershell]::Create()
- $script:ExportPowerShell.Runspace = $script:ExportRunspace
- [void]$script:ExportPowerShell.AddScript($CoreFunctions)
- [void]$script:ExportPowerShell.AddScript(@'
-param($Scan, $HtmlPath, $WantHtml, $WantCsv, $CsvBase, $Categories, $HidePerm, $HideSys, $Shared)
-try {
- $written = New-Object System.Collections.ArrayList
- if ($WantHtml) {
- $out = ConvertTo-FilerHtmlReport -Scan $Scan -Path $HtmlPath -Categories $Categories -HideInheritedChildPerms $HidePerm -HideSystemPrincipals $HideSys
- [void]$written.Add($out)
- }
- if ($WantCsv) {
- $csvFiles = @(ConvertTo-FilerCsvReport -Scan $Scan -Path $CsvBase -Categories $Categories -HideInheritedChildPerms $HidePerm -HideSystemPrincipals $HideSys)
- foreach ($f in $csvFiles) { [void]$written.Add($f) }
- }
- $Shared.Written = $written.ToArray()
-}
-catch { $Shared.Error = $_.Exception.Message }
-'@).AddArgument($script:LastScan).AddArgument($htmlPath).AddArgument([bool]$choice.Html).AddArgument([bool]$choice.Csv).AddArgument($csvBase).AddArgument($categories).AddArgument([bool]$script:PermHideInherited).AddArgument([bool]$script:PermHideSystem).AddArgument($script:ExportShared)
-
- $script:ExportHandle = $script:ExportPowerShell.BeginInvoke()
- $exportTimer.Start()
-})
-
-# 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()
\ No newline at end of file