Compare commits
3
Commits
29e79a2529
...
v1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
242307cee2 | ||
|
|
08ee699e44 | ||
|
|
8a6657f496 |
+530
-29
@@ -83,6 +83,13 @@
|
|||||||
.PARAMETER NoGui
|
.PARAMETER NoGui
|
||||||
Forcer le mode sans interface (nécessite -Path et -Output).
|
Forcer le mode sans interface (nécessite -Path et -Output).
|
||||||
|
|
||||||
|
.PARAMETER RevertPendingGrants
|
||||||
|
Annuler les modifications d'accès laissées par une exécution précédente qui
|
||||||
|
s'est arrêtée avant de les restaurer (interface fermée pendant l'analyse,
|
||||||
|
plantage, fermeture de session). Chaque octroi est journalisé sous
|
||||||
|
%LOCALAPPDATA%\FilerManager avant d'être appliqué ; ce commutateur rejoue les
|
||||||
|
journaux orphelins puis se termine. Ne demande aucun autre paramètre.
|
||||||
|
|
||||||
.PARAMETER Category
|
.PARAMETER Category
|
||||||
Catégories de rapport à inclure dans le HTML. Par défaut 'All'. Passez toute
|
Catégories de rapport à inclure dans le HTML. Par défaut 'All'. Passez toute
|
||||||
combinaison de : Tree (tailles des dossiers), LongPaths, Permissions, Grants
|
combinaison de : Tree (tailles des dossiers), LongPaths, Permissions, Grants
|
||||||
@@ -121,6 +128,7 @@ param(
|
|||||||
[switch]$GrantAccess,
|
[switch]$GrantAccess,
|
||||||
[switch]$KeepGrants,
|
[switch]$KeepGrants,
|
||||||
[switch]$NoGui,
|
[switch]$NoGui,
|
||||||
|
[switch]$RevertPendingGrants,
|
||||||
[ValidateSet('All', 'Tree', 'LongPaths', 'Permissions', 'Grants', 'Errors')]
|
[ValidateSet('All', 'Tree', 'LongPaths', 'Permissions', 'Grants', 'Errors')]
|
||||||
[string[]]$Category = @('All')
|
[string[]]$Category = @('All')
|
||||||
)
|
)
|
||||||
@@ -154,6 +162,14 @@ function Test-IsAccessDenied {
|
|||||||
$ex = $ErrorRecord.Exception
|
$ex = $ErrorRecord.Exception
|
||||||
if ($ex -is [System.UnauthorizedAccessException]) { return $true }
|
if ($ex -is [System.UnauthorizedAccessException]) { return $true }
|
||||||
if ($ErrorRecord.CategoryInfo -and $ErrorRecord.CategoryInfo.Category -eq 'PermissionDenied') { return $true }
|
if ($ErrorRecord.CategoryInfo -and $ErrorRecord.CategoryInfo.Category -eq 'PermissionDenied') { return $true }
|
||||||
|
# A failure raised inside a .NET call reaches us wrapped in a
|
||||||
|
# MethodInvocationException, so the real cause is only visible on the base
|
||||||
|
# exception. Windows PowerShell 5.1 also reports some denials as a bare
|
||||||
|
# Win32Exception carrying ERROR_ACCESS_DENIED.
|
||||||
|
$base = $null
|
||||||
|
try { $base = $ex.GetBaseException() } catch { }
|
||||||
|
if ($base -is [System.UnauthorizedAccessException]) { return $true }
|
||||||
|
if ($base -is [System.ComponentModel.Win32Exception] -and $base.NativeErrorCode -eq 5) { return $true }
|
||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +187,29 @@ function Resolve-PrincipalSid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Resolve-SidName {
|
||||||
|
# Friendly account name for a SID, falling back to the raw SID string when it
|
||||||
|
# cannot be translated (deleted account, broken trust, offline domain).
|
||||||
|
# Results are cached: a large scan meets the same handful of principals
|
||||||
|
# thousands of times and every miss costs an LSA / directory round trip.
|
||||||
|
param($Sid)
|
||||||
|
if (-not $Sid) { return '' }
|
||||||
|
if ($null -eq $script:FilerSidNameCache) { $script:FilerSidNameCache = @{} }
|
||||||
|
$key = [string]$Sid
|
||||||
|
if ($script:FilerSidNameCache.ContainsKey($key)) { return $script:FilerSidNameCache[$key] }
|
||||||
|
$name = $key
|
||||||
|
try {
|
||||||
|
if ($Sid -is [System.Security.Principal.SecurityIdentifier]) {
|
||||||
|
$name = $Sid.Translate([System.Security.Principal.NTAccount]).Value
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$name = (New-Object System.Security.Principal.SecurityIdentifier $key).Translate([System.Security.Principal.NTAccount]).Value
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
$script:FilerSidNameCache[$key] = $name
|
||||||
|
return $name
|
||||||
|
}
|
||||||
|
|
||||||
function Test-IsSystemPrincipal {
|
function Test-IsSystemPrincipal {
|
||||||
# True for well-known built-in / system accounts and groups, so the audit can
|
# 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-
|
# focus on real business identities. Detection is by SID prefix (locale-
|
||||||
@@ -267,6 +306,10 @@ function Grant-AdminAccess {
|
|||||||
RevertError = $null
|
RevertError = $null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Persist before returning: from here on the change is live on disk, so it
|
||||||
|
# has to be recoverable even if this process never reaches Restore-Grants.
|
||||||
|
if ($success) { Save-FilerGrantJournal -Grants $Grants }
|
||||||
|
|
||||||
if (-not $success -and $errText) {
|
if (-not $success -and $errText) {
|
||||||
[void]$ScanErrors.Add([pscustomobject]@{ Path = $Path; Error = "Octroi automatique échoué : $errText" })
|
[void]$ScanErrors.Add([pscustomobject]@{ Path = $Path; Error = "Octroi automatique échoué : $errText" })
|
||||||
}
|
}
|
||||||
@@ -314,6 +357,113 @@ function Restore-Grants {
|
|||||||
$g.RevertError = $err
|
$g.RevertError = $err
|
||||||
if ($err) { [void]$ScanErrors.Add([pscustomobject]@{ Path = $g.Path; Error = "Annulation : $err" }) }
|
if ($err) { [void]$ScanErrors.Add([pscustomobject]@{ Path = $g.Path; Error = "Annulation : $err" }) }
|
||||||
}
|
}
|
||||||
|
Save-FilerGrantJournal -Grants $Grants
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-FilerGrantJournalDir {
|
||||||
|
# Where the pending-grant journals live: one file per process id.
|
||||||
|
$dir = Join-Path ([string]$env:LOCALAPPDATA) 'FilerManager'
|
||||||
|
if (-not (Test-Path -LiteralPath $dir)) {
|
||||||
|
try { $null = New-Item -ItemType Directory -Path $dir -Force -ErrorAction Stop } catch { return $null }
|
||||||
|
}
|
||||||
|
return $dir
|
||||||
|
}
|
||||||
|
|
||||||
|
function Save-FilerGrantJournal {
|
||||||
|
<# Mirrors the not-yet-reverted grants to disk. Without it, losing the process
|
||||||
|
between a grant and Restore-Grants - the GUI closed mid-scan, a crash, a
|
||||||
|
logoff - would leave Administrators as owner with FullControl on the
|
||||||
|
scanned folders and no record that Filer Manager put it there. Best effort
|
||||||
|
by design: a journal failure must never abort a scan. The file is removed
|
||||||
|
as soon as nothing is pending. #>
|
||||||
|
param([System.Collections.ArrayList]$Grants)
|
||||||
|
if (-not $script:FilerGrantJournalPath) { return }
|
||||||
|
try {
|
||||||
|
$pending = @($Grants | Where-Object { $_.Success -and -not $_.Reverted } | ForEach-Object {
|
||||||
|
[pscustomobject]@{
|
||||||
|
Path = [string]$_.Path
|
||||||
|
IsDir = [bool]$_.IsDir
|
||||||
|
OriginalOwner = [string]$_.OriginalOwner
|
||||||
|
OriginalSddl = [string]$_.OriginalSddl
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if ($pending.Count -eq 0) {
|
||||||
|
Remove-Item -LiteralPath $script:FilerGrantJournalPath -Force -ErrorAction SilentlyContinue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
# Wrapped in an object: ConvertTo-Json collapses a one-element array.
|
||||||
|
$doc = [pscustomobject]@{
|
||||||
|
Computer = [string]$env:COMPUTERNAME
|
||||||
|
Pid = $PID
|
||||||
|
Written = (Get-Date).ToString('o')
|
||||||
|
Grants = $pending
|
||||||
|
}
|
||||||
|
(ConvertTo-Json -InputObject $doc -Depth 4) |
|
||||||
|
Set-Content -LiteralPath $script:FilerGrantJournalPath -Encoding UTF8 -ErrorAction Stop
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-FilerPendingGrantJournal {
|
||||||
|
<# Journals left behind by a run that never reverted. A journal whose owning
|
||||||
|
process is still alive is skipped - that scan is running and will clean up
|
||||||
|
after itself. A recycled process id can therefore delay a recovery, never
|
||||||
|
break one: the file simply stays until a later run finds the id free. #>
|
||||||
|
$dir = Get-FilerGrantJournalDir
|
||||||
|
if (-not $dir) { return @() }
|
||||||
|
$out = New-Object System.Collections.ArrayList
|
||||||
|
foreach ($f in @(Get-ChildItem -LiteralPath $dir -Filter 'grants-*.json' -File -ErrorAction SilentlyContinue)) {
|
||||||
|
$doc = $null
|
||||||
|
try { $doc = (Get-Content -LiteralPath $f.FullName -Raw -ErrorAction Stop) | ConvertFrom-Json } catch { continue }
|
||||||
|
$rows = @($doc.Grants | Where-Object { $_ -and $_.Path })
|
||||||
|
if ($rows.Count -eq 0) { Remove-Item -LiteralPath $f.FullName -Force -ErrorAction SilentlyContinue; continue }
|
||||||
|
$owner = 0
|
||||||
|
try { $owner = [int]$doc.Pid } catch { }
|
||||||
|
if ($owner -eq $PID) { continue }
|
||||||
|
if ($owner -gt 0) {
|
||||||
|
$alive = $null
|
||||||
|
try { $alive = Get-Process -Id $owner -ErrorAction Stop } catch { }
|
||||||
|
if ($alive) { continue }
|
||||||
|
}
|
||||||
|
[void]$out.Add([pscustomobject]@{ File = $f.FullName; Grants = $rows; Written = [string]$doc.Written })
|
||||||
|
}
|
||||||
|
return $out.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Restore-FilerPendingGrants {
|
||||||
|
<# Replays one orphaned journal through Restore-Grants, deleting it only once
|
||||||
|
every entry came back. Returns a small report. #>
|
||||||
|
param([Parameter(Mandatory)] $Journal)
|
||||||
|
$grants = New-Object System.Collections.ArrayList
|
||||||
|
foreach ($g in @($Journal.Grants)) {
|
||||||
|
[void]$grants.Add([pscustomobject]@{
|
||||||
|
Path = [string]$g.Path
|
||||||
|
IsDir = [bool]$g.IsDir
|
||||||
|
Reason = 'journal'
|
||||||
|
Changes = ''
|
||||||
|
OriginalOwner = [string]$g.OriginalOwner
|
||||||
|
OriginalSddl = [string]$g.OriginalSddl
|
||||||
|
Success = $true
|
||||||
|
Error = $null
|
||||||
|
Reverted = $false
|
||||||
|
RevertError = $null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
$errs = New-Object System.Collections.ArrayList
|
||||||
|
# Blank the journal path while replaying: Restore-Grants must not rewrite the
|
||||||
|
# very file we are consuming.
|
||||||
|
$saved = $script:FilerGrantJournalPath
|
||||||
|
$script:FilerGrantJournalPath = $null
|
||||||
|
try { Restore-Grants -Grants $grants -ScanErrors $errs -Progress $null }
|
||||||
|
finally { $script:FilerGrantJournalPath = $saved }
|
||||||
|
|
||||||
|
$failed = @($grants | Where-Object { -not $_.Reverted })
|
||||||
|
if ($failed.Count -eq 0) { Remove-Item -LiteralPath $Journal.File -Force -ErrorAction SilentlyContinue }
|
||||||
|
return [pscustomobject]@{
|
||||||
|
Total = $grants.Count
|
||||||
|
Reverted = @($grants | Where-Object { $_.Reverted }).Count
|
||||||
|
Failed = $failed.Count
|
||||||
|
Errors = @($errs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Test-IsExcludedFolder {
|
function Test-IsExcludedFolder {
|
||||||
@@ -337,6 +487,71 @@ function Test-IsExcludedFolder {
|
|||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Test-IsReparsePoint {
|
||||||
|
# True for a junction, a directory symlink or any other reparse point. Those
|
||||||
|
# must never be descended into: a link that points at one of its own
|
||||||
|
# ancestors makes the walk recurse until the stack (or the path length) gives
|
||||||
|
# out, and a lateral one counts the very same bytes twice. They are still
|
||||||
|
# reported as an entry, just never followed.
|
||||||
|
param($Entry)
|
||||||
|
try { return (($Entry.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) } catch { return $false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Expand-FilerShortPath {
|
||||||
|
<# Expands 8.3 short components of a path to their long form. Resolve-Path
|
||||||
|
keeps them as-is and [IO.Path]::GetFullPath expands them, so neither is a
|
||||||
|
reliable answer on its own; GetLongPathNameW is authoritative and also
|
||||||
|
accepts an extended-length path. #>
|
||||||
|
param([string]$Path)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Path)) { return $Path }
|
||||||
|
if (-not ('FilerManager.LongPath' -as [type])) { try { Initialize-FilerNetApi } catch { } }
|
||||||
|
if ('FilerManager.LongPath' -as [type]) {
|
||||||
|
try { return [FilerManager.LongPath]::ExpandShort($Path) } catch { }
|
||||||
|
}
|
||||||
|
return $Path
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-FilerChildEntry {
|
||||||
|
<# Directory listing that is not bounded by MAX_PATH, used by every part of
|
||||||
|
the scan that walks a tree. Returns Entries / ErrorCode / ErrorMessage
|
||||||
|
instead of throwing, so the caller can tell "access denied" (code 5) from
|
||||||
|
"not there" without unwrapping a MethodInvocationException.
|
||||||
|
|
||||||
|
Entries carry Name / FullPath / Attributes / IsDirectory / IsReparsePoint /
|
||||||
|
Length, which is the subset of FileSystemInfo the scan actually used, so
|
||||||
|
Test-IsExcludedFolder and Test-IsReparsePoint work on them unchanged.
|
||||||
|
Get-ChildItem remains as a fallback in case the P/Invoke shim cannot be
|
||||||
|
compiled. #>
|
||||||
|
param([string]$Path)
|
||||||
|
|
||||||
|
if (-not ('FilerManager.LongPath' -as [type])) { try { Initialize-FilerNetApi } catch { } }
|
||||||
|
if ('FilerManager.LongPath' -as [type]) {
|
||||||
|
try { return [FilerManager.LongPath]::GetEntries($Path) } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$out = New-Object System.Collections.ArrayList
|
||||||
|
foreach ($i in @(Get-ChildItem -LiteralPath $Path -Force -ErrorAction Stop)) {
|
||||||
|
$isDir = [bool]$i.PSIsContainer
|
||||||
|
$len = [long]0
|
||||||
|
if (-not $isDir) { try { $len = [long]$i.Length } catch { $len = [long]0 } }
|
||||||
|
[void]$out.Add([pscustomobject]@{
|
||||||
|
Name = [string]$i.Name
|
||||||
|
FullPath = [string]$i.FullName
|
||||||
|
Attributes = $i.Attributes
|
||||||
|
IsDirectory = $isDir
|
||||||
|
IsReparsePoint = (($i.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0)
|
||||||
|
Length = $len
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return [pscustomobject]@{ Entries = $out.ToArray(); ErrorCode = 0; ErrorMessage = $null }
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$code = if (Test-IsAccessDenied $_) { 5 } else { -1 }
|
||||||
|
return [pscustomobject]@{ Entries = @(); ErrorCode = $code; ErrorMessage = $_.Exception.GetBaseException().Message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Get-FolderNode {
|
function Get-FolderNode {
|
||||||
<# Recursively builds a size tree. Accumulates long paths and errors via
|
<# Recursively builds a size tree. Accumulates long paths and errors via
|
||||||
synchronized collections passed by the caller. #>
|
synchronized collections passed by the caller. #>
|
||||||
@@ -371,27 +586,37 @@ function Get-FolderNode {
|
|||||||
$entries = $null
|
$entries = $null
|
||||||
$granted = $false
|
$granted = $false
|
||||||
while ($true) {
|
while ($true) {
|
||||||
try {
|
$listing = Get-FilerChildEntry -Path $Path
|
||||||
$entries = Get-ChildItem -LiteralPath $Path -Force -ErrorAction Stop
|
if ($listing.ErrorCode -eq 0) { $entries = $listing.Entries; break }
|
||||||
break
|
if ($GrantAccessFlag -and -not $granted -and $listing.ErrorCode -eq 5) {
|
||||||
}
|
|
||||||
catch {
|
|
||||||
if ($GrantAccessFlag -and -not $granted -and (Test-IsAccessDenied $_)) {
|
|
||||||
$granted = $true
|
$granted = $true
|
||||||
if ($Progress) { $Progress.Status = "Octroi de l'accès : $Path" }
|
if ($Progress) { $Progress.Status = "Octroi de l'accès : $Path" }
|
||||||
if (Grant-AdminAccess -Path $Path -Reason 'énumérer le dossier' -Grants $GrantList -ScanErrors $ScanErrors) {
|
if (Grant-AdminAccess -Path $Path -Reason 'énumérer le dossier' -Grants $GrantList -ScanErrors $ScanErrors) {
|
||||||
continue # retry once, now that access has been granted
|
continue # retry once, now that access has been granted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
[void]$ScanErrors.Add([pscustomobject]@{ Path = $Path; Error = $_.Exception.Message })
|
[void]$ScanErrors.Add([pscustomobject]@{ Path = $Path; Error = [string]$listing.ErrorMessage })
|
||||||
return [pscustomobject]$node
|
return [pscustomobject]$node
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($entry in $entries) {
|
foreach ($entry in $entries) {
|
||||||
if ($entry.PSIsContainer) {
|
if ($entry.IsDirectory) {
|
||||||
if (Test-IsExcludedFolder -Entry $entry -Patterns $ExcludeFolderFlag -ExcludeHidden $ExcludeHiddenFlag) { continue }
|
if (Test-IsExcludedFolder -Entry $entry -Patterns $ExcludeFolderFlag -ExcludeHidden $ExcludeHiddenFlag) { continue }
|
||||||
$child = Get-FolderNode -Path $entry.FullName -MaxLen $MaxLen `
|
if (Test-IsReparsePoint -Entry $entry) {
|
||||||
|
# Junction / directory symlink: recorded as a leaf, never walked,
|
||||||
|
# and contributing no size (the bytes belong to the link target).
|
||||||
|
$node.FolderCount += 1
|
||||||
|
[void]$node.Children.Add([pscustomobject]@{
|
||||||
|
Name = $entry.Name; FullPath = $entry.FullPath; Size = [long]0
|
||||||
|
FileCount = 0; FolderCount = 0; Depth = $Depth + 1; IsLink = $true
|
||||||
|
Children = (New-Object System.Collections.ArrayList)
|
||||||
|
})
|
||||||
|
if ($entry.FullPath.Length -ge $MaxLen) {
|
||||||
|
[void]$LongPaths.Add([pscustomobject]@{ Type = 'Lien'; Length = $entry.FullPath.Length; Path = $entry.FullPath })
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$child = Get-FolderNode -Path $entry.FullPath -MaxLen $MaxLen `
|
||||||
-LongPaths $LongPaths -ScanErrors $ScanErrors `
|
-LongPaths $LongPaths -ScanErrors $ScanErrors `
|
||||||
-Progress $Progress -Depth ($Depth + 1)
|
-Progress $Progress -Depth ($Depth + 1)
|
||||||
$node.Size += $child.Size
|
$node.Size += $child.Size
|
||||||
@@ -400,16 +625,16 @@ function Get-FolderNode {
|
|||||||
[void]$node.Children.Add($child)
|
[void]$node.Children.Add($child)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$len = 0
|
$len = [long]0
|
||||||
try { $len = [long]$entry.Length } catch { $len = 0 }
|
try { $len = [long]$entry.Length } catch { $len = [long]0 }
|
||||||
$node.Size += $len
|
$node.Size += $len
|
||||||
$node.FileCount += 1
|
$node.FileCount += 1
|
||||||
if ($entry.FullName.Length -ge $MaxLen) {
|
if ($entry.FullPath.Length -ge $MaxLen) {
|
||||||
[void]$LongPaths.Add([pscustomobject]@{ Type = 'Fichier'; Length = $entry.FullName.Length; Path = $entry.FullName })
|
[void]$LongPaths.Add([pscustomobject]@{ Type = 'Fichier'; Length = $entry.FullPath.Length; Path = $entry.FullPath })
|
||||||
}
|
}
|
||||||
if ($IncludeFilesInTreeFlag) {
|
if ($IncludeFilesInTreeFlag) {
|
||||||
[void]$node.Children.Add([pscustomobject]@{
|
[void]$node.Children.Add([pscustomobject]@{
|
||||||
Name = $entry.Name; FullPath = $entry.FullName; Size = $len
|
Name = $entry.Name; FullPath = $entry.FullPath; Size = $len
|
||||||
FileCount = 0; FolderCount = 0; Depth = $Depth + 1; IsFile = $true
|
FileCount = 0; FolderCount = 0; Depth = $Depth + 1; IsFile = $true
|
||||||
Children = (New-Object System.Collections.ArrayList)
|
Children = (New-Object System.Collections.ArrayList)
|
||||||
})
|
})
|
||||||
@@ -432,12 +657,19 @@ function Get-FolderPermissions {
|
|||||||
while ($true) {
|
while ($true) {
|
||||||
try {
|
try {
|
||||||
$acl = Get-Acl -LiteralPath $Path -ErrorAction Stop
|
$acl = Get-Acl -LiteralPath $Path -ErrorAction Stop
|
||||||
foreach ($ace in $acl.Access) {
|
# Read the owner and the ACEs as raw SIDs, then translate each name on its
|
||||||
|
# own. The NTAccount form of these APIs ($acl.Owner / $acl.Access) throws
|
||||||
|
# IdentityNotMappedException as soon as ONE principal is unresolvable - a
|
||||||
|
# single orphaned SID would otherwise cost us the whole folder's ACL.
|
||||||
|
$ownerSid = $null
|
||||||
|
try { $ownerSid = $acl.GetOwner([System.Security.Principal.SecurityIdentifier]) } catch { }
|
||||||
|
$owner = Resolve-SidName $ownerSid
|
||||||
|
foreach ($ace in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) {
|
||||||
[void]$results.Add([pscustomobject]@{
|
[void]$results.Add([pscustomobject]@{
|
||||||
Folder = $Path
|
Folder = $Path
|
||||||
Owner = $acl.Owner
|
Owner = $owner
|
||||||
Identity = [string]$ace.IdentityReference
|
Identity = (Resolve-SidName $ace.IdentityReference)
|
||||||
Sid = Resolve-PrincipalSid $ace.IdentityReference
|
Sid = [string]$ace.IdentityReference
|
||||||
Rights = [string]$ace.FileSystemRights
|
Rights = [string]$ace.FileSystemRights
|
||||||
Type = [string]$ace.AccessControlType
|
Type = [string]$ace.AccessControlType
|
||||||
Inherited = $ace.IsInherited
|
Inherited = $ace.IsInherited
|
||||||
@@ -458,11 +690,13 @@ function Get-FolderPermissions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($Current -lt $Depth) {
|
if ($Current -lt $Depth) {
|
||||||
$subDirs = $null
|
$subDirs = @()
|
||||||
try { $subDirs = Get-ChildItem -LiteralPath $Path -Directory -Force -ErrorAction Stop } catch { $subDirs = @() }
|
$listing = Get-FilerChildEntry -Path $Path
|
||||||
|
if ($listing.ErrorCode -eq 0) { $subDirs = @($listing.Entries | Where-Object { $_.IsDirectory }) }
|
||||||
foreach ($d in $subDirs) {
|
foreach ($d in $subDirs) {
|
||||||
if (Test-IsExcludedFolder -Entry $d -Patterns $ExcludeFolderFlag -ExcludeHidden $ExcludeHiddenFlag) { continue }
|
if (Test-IsExcludedFolder -Entry $d -Patterns $ExcludeFolderFlag -ExcludeHidden $ExcludeHiddenFlag) { continue }
|
||||||
$child = Get-FolderPermissions -Path $d.FullName -Depth $Depth -ScanErrors $ScanErrors -Current ($Current + 1)
|
if (Test-IsReparsePoint -Entry $d) { continue } # never follow a junction / symlink
|
||||||
|
$child = Get-FolderPermissions -Path $d.FullPath -Depth $Depth -ScanErrors $ScanErrors -Current ($Current + 1)
|
||||||
foreach ($r in $child) { [void]$results.Add($r) }
|
foreach ($r in $child) { [void]$results.Add($r) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -488,6 +722,19 @@ function Invoke-FilerScan {
|
|||||||
$script:GrantAccessFlag = $GrantAccess
|
$script:GrantAccessFlag = $GrantAccess
|
||||||
$script:GrantList = New-Object System.Collections.ArrayList
|
$script:GrantList = New-Object System.Collections.ArrayList
|
||||||
|
|
||||||
|
# Arm the on-disk journal only when we actually intend to revert: with
|
||||||
|
# -KeepGrants the changes are meant to stay, so there is nothing to recover.
|
||||||
|
$script:FilerGrantJournalPath = $null
|
||||||
|
if ($GrantAccess -and $RevertGrants) {
|
||||||
|
$jdir = Get-FilerGrantJournalDir
|
||||||
|
if ($jdir) { $script:FilerGrantJournalPath = Join-Path $jdir ("grants-{0}.json" -f $PID) }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compile the P/Invoke shim up front: Get-FilerChildEntry needs
|
||||||
|
# FilerManager.LongPath to get past MAX_PATH, and doing it once here keeps the
|
||||||
|
# cost off the recursion.
|
||||||
|
try { Initialize-FilerNetApi } catch { }
|
||||||
|
|
||||||
$longPaths = New-Object System.Collections.ArrayList
|
$longPaths = New-Object System.Collections.ArrayList
|
||||||
$scanErrors = New-Object System.Collections.ArrayList
|
$scanErrors = New-Object System.Collections.ArrayList
|
||||||
$permissions = New-Object System.Collections.ArrayList
|
$permissions = New-Object System.Collections.ArrayList
|
||||||
@@ -532,8 +779,20 @@ function Invoke-FilerScan {
|
|||||||
[void]$scanErrors.Add([pscustomobject]@{ Path = $rawPath; Error = $msg })
|
[void]$scanErrors.Add([pscustomobject]@{ Path = $rawPath; Error = $msg })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
# Canonicalise the root once. Every child path is derived from this
|
||||||
|
# string, and every length compared against -MaxPathLength is measured on
|
||||||
|
# it, so an 8.3 alias here would understate how long the real paths are.
|
||||||
|
# ProviderPath, not Path: on a UNC root the latter comes back in
|
||||||
|
# provider-qualified form ("Microsoft.PowerShell.Core\FileSystem::\\srv\
|
||||||
|
# share"), which no Win32 call can open - the scan then found nothing.
|
||||||
$fullPath = $p
|
$fullPath = $p
|
||||||
try { $fullPath = (Resolve-Path -LiteralPath $p).Path } catch { }
|
try {
|
||||||
|
$rp = Resolve-Path -LiteralPath $p -ErrorAction Stop
|
||||||
|
$cand = [string]$rp.ProviderPath
|
||||||
|
if ([string]::IsNullOrWhiteSpace($cand)) { $cand = [string]$rp.Path }
|
||||||
|
if ($cand) { $fullPath = $cand }
|
||||||
|
} catch { }
|
||||||
|
$fullPath = Expand-FilerShortPath -Path $fullPath
|
||||||
$node = Get-FolderNode -Path $fullPath -MaxLen $MaxPathLength `
|
$node = Get-FolderNode -Path $fullPath -MaxLen $MaxPathLength `
|
||||||
-LongPaths $longPaths -ScanErrors $scanErrors -Progress $Progress
|
-LongPaths $longPaths -ScanErrors $scanErrors -Progress $Progress
|
||||||
[void]$roots.Add($node)
|
[void]$roots.Add($node)
|
||||||
@@ -640,8 +899,11 @@ function ConvertTo-FilerHtmlReport {
|
|||||||
$pct = if ($ParentSize -gt 0) { [math]::Round(($Node.Size / $ParentSize) * 100, 1) } else { 100 }
|
$pct = if ($ParentSize -gt 0) { [math]::Round(($Node.Size / $ParentSize) * 100, 1) } else { 100 }
|
||||||
$sizeStr = Format-Bytes $Node.Size
|
$sizeStr = Format-Bytes $Node.Size
|
||||||
$isFile = ($Node.PSObject.Properties.Name -contains 'IsFile' -and $Node.IsFile)
|
$isFile = ($Node.PSObject.Properties.Name -contains 'IsFile' -and $Node.IsFile)
|
||||||
$icon = if ($isFile) { '📄' } else { '📁' }
|
$isLink = ($Node.PSObject.Properties.Name -contains 'IsLink' -and $Node.IsLink)
|
||||||
$meta = if ($isFile) { '' } else { " <span class='meta'>$($Node.FolderCount) dossiers, $($Node.FileCount) fichiers</span>" }
|
$icon = if ($isLink) { '🔗' } elseif ($isFile) { '📄' } else { '📁' }
|
||||||
|
$meta = if ($isLink) { " <span class='meta'>lien (non parcouru)</span>" }
|
||||||
|
elseif ($isFile) { '' }
|
||||||
|
else { " <span class='meta'>$($Node.FolderCount) dossiers, $($Node.FileCount) fichiers</span>" }
|
||||||
$kids = @($Node.Children | Where-Object { $_ } )
|
$kids = @($Node.Children | Where-Object { $_ } )
|
||||||
|
|
||||||
$summary = "$icon <span class='nm'>$(_enc $Node.Name)</span> <span class='sz'>$sizeStr</span>" +
|
$summary = "$icon <span class='nm'>$(_enc $Node.Name)</span> <span class='sz'>$sizeStr</span>" +
|
||||||
@@ -967,6 +1229,12 @@ function ConvertTo-FilerCsvReport {
|
|||||||
$base = [System.IO.Path]::GetFileNameWithoutExtension($Path)
|
$base = [System.IO.Path]::GetFileNameWithoutExtension($Path)
|
||||||
if ([string]::IsNullOrEmpty($base)) { $base = 'filer-report' }
|
if ([string]::IsNullOrEmpty($base)) { $base = 'filer-report' }
|
||||||
|
|
||||||
|
# 'UTF8' means "with BOM" on Windows PowerShell 5.1 but "without BOM" on
|
||||||
|
# PowerShell 7. Excel reads a BOM-less file as the ANSI code page, which turns
|
||||||
|
# every accent into mojibake (Proprietaire -> "Propriétaire"), so ask for the
|
||||||
|
# BOM explicitly on the hosts that know the name.
|
||||||
|
$csvEncoding = if ($PSVersionTable.PSVersion.Major -ge 6) { 'utf8BOM' } else { 'UTF8' }
|
||||||
|
|
||||||
$written = New-Object System.Collections.ArrayList
|
$written = New-Object System.Collections.ArrayList
|
||||||
function _writeCsv {
|
function _writeCsv {
|
||||||
param($Rows, [string]$Suffix)
|
param($Rows, [string]$Suffix)
|
||||||
@@ -974,7 +1242,7 @@ function ConvertTo-FilerCsvReport {
|
|||||||
if ($rows.Count -eq 0) { return } # don't emit empty files
|
if ($rows.Count -eq 0) { return } # don't emit empty files
|
||||||
$name = "$base-$Suffix.csv"
|
$name = "$base-$Suffix.csv"
|
||||||
$p = if ([string]::IsNullOrEmpty($dir)) { $name } else { Join-Path $dir $name }
|
$p = if ([string]::IsNullOrEmpty($dir)) { $name } else { Join-Path $dir $name }
|
||||||
$rows | Export-Csv -LiteralPath $p -NoTypeInformation -Encoding UTF8 -Delimiter $delim
|
$rows | Export-Csv -LiteralPath $p -NoTypeInformation -Encoding $csvEncoding -Delimiter $delim
|
||||||
[void]$written.Add($p)
|
[void]$written.Add($p)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -984,11 +1252,12 @@ function ConvertTo-FilerCsvReport {
|
|||||||
function _flattenNode {
|
function _flattenNode {
|
||||||
param($Node, [string]$Root, [System.Collections.ArrayList]$Acc)
|
param($Node, [string]$Root, [System.Collections.ArrayList]$Acc)
|
||||||
$isFile = ($Node.PSObject.Properties.Name -contains 'IsFile' -and $Node.IsFile)
|
$isFile = ($Node.PSObject.Properties.Name -contains 'IsFile' -and $Node.IsFile)
|
||||||
|
$isLink = ($Node.PSObject.Properties.Name -contains 'IsLink' -and $Node.IsLink)
|
||||||
[void]$Acc.Add([pscustomobject]@{
|
[void]$Acc.Add([pscustomobject]@{
|
||||||
Racine = $Root
|
Racine = $Root
|
||||||
Chemin = $Node.FullPath
|
Chemin = $Node.FullPath
|
||||||
Nom = $Node.Name
|
Nom = $Node.Name
|
||||||
Type = if ($isFile) { 'Fichier' } else { 'Dossier' }
|
Type = if ($isLink) { 'Lien' } elseif ($isFile) { 'Fichier' } else { 'Dossier' }
|
||||||
Profondeur = $Node.Depth
|
Profondeur = $Node.Depth
|
||||||
TailleOctets = [long]$Node.Size
|
TailleOctets = [long]$Node.Size
|
||||||
Taille = Format-Bytes $Node.Size
|
Taille = Format-Bytes $Node.Size
|
||||||
@@ -1087,6 +1356,157 @@ namespace FilerManager {
|
|||||||
public bool IsDisk;
|
public bool IsDisk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One directory entry, shaped so the PowerShell side can treat it like the
|
||||||
|
// FileSystemInfo it replaces (same Name / Attributes contract).
|
||||||
|
public class DirEntry {
|
||||||
|
public string Name;
|
||||||
|
public string FullPath;
|
||||||
|
public System.IO.FileAttributes Attributes;
|
||||||
|
public bool IsDirectory;
|
||||||
|
public bool IsReparsePoint;
|
||||||
|
public long Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result of one listing. Errors are returned, not thrown: the caller has to
|
||||||
|
// tell "access denied" from "gone" and PowerShell wraps every exception
|
||||||
|
// raised by a .NET call in a MethodInvocationException.
|
||||||
|
public class DirListing {
|
||||||
|
public DirEntry[] Entries;
|
||||||
|
public int ErrorCode;
|
||||||
|
public string ErrorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directory enumeration that is not bounded by MAX_PATH.
|
||||||
|
//
|
||||||
|
// This has to go straight to the Win32 API. Windows PowerShell 5.1 runs .NET
|
||||||
|
// with legacy path handling, which rejects the \\?\ prefix outright, and
|
||||||
|
// Get-ChildItem there stops at 260 characters - exactly the paths this tool
|
||||||
|
// exists to report. FindFirstFileW has no such limit.
|
||||||
|
public static class LongPath {
|
||||||
|
|
||||||
|
// Must match WIN32_FIND_DATAW byte for byte (592 bytes). Each FILETIME is
|
||||||
|
// two DWORDs and therefore 4-byte aligned: declaring one as a managed
|
||||||
|
// long would make the runtime align it on 8 bytes, pad after
|
||||||
|
// dwFileAttributes and shift every field that follows.
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
|
private struct FindData {
|
||||||
|
public uint dwFileAttributes;
|
||||||
|
public uint ftCreationTimeLow;
|
||||||
|
public uint ftCreationTimeHigh;
|
||||||
|
public uint ftLastAccessTimeLow;
|
||||||
|
public uint ftLastAccessTimeHigh;
|
||||||
|
public uint ftLastWriteTimeLow;
|
||||||
|
public uint ftLastWriteTimeHigh;
|
||||||
|
public uint nFileSizeHigh;
|
||||||
|
public uint nFileSizeLow;
|
||||||
|
public uint dwReserved0;
|
||||||
|
public uint dwReserved1;
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string cFileName;
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] public string cAlternateFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guards the layout above against a silent regression.
|
||||||
|
public static int StructSize() { return Marshal.SizeOf(typeof(FindData)); }
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||||
|
private static extern IntPtr FindFirstFileW(string lpFileName, out FindData lpFindFileData);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||||
|
private static extern bool FindNextFileW(IntPtr hFindFile, out FindData lpFindFileData);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern bool FindClose(IntPtr hFindFile);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||||
|
private static extern uint GetLongPathNameW(string lpszShortPath, StringBuilder lpszLongPath, uint cchBuffer);
|
||||||
|
|
||||||
|
private const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
|
||||||
|
private const uint FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400;
|
||||||
|
private const int ERROR_NO_MORE_FILES = 18;
|
||||||
|
private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
|
||||||
|
|
||||||
|
// Extended-length form of a path, which lifts MAX_PATH on the Win32 call.
|
||||||
|
public static string Extended(string path) {
|
||||||
|
if (string.IsNullOrEmpty(path)) return path;
|
||||||
|
if (path.StartsWith(@"\\?\")) return path;
|
||||||
|
if (path.StartsWith(@"\\")) return @"\\?\UNC\" + path.Substring(2);
|
||||||
|
return @"\\?\" + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inverse of Extended, so reports show the path the user typed.
|
||||||
|
public static string Trim(string path) {
|
||||||
|
if (string.IsNullOrEmpty(path)) return path;
|
||||||
|
if (path.StartsWith(@"\\?\UNC\")) return @"\\" + path.Substring(8);
|
||||||
|
if (path.StartsWith(@"\\?\")) return path.Substring(4);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expands 8.3 short components ("SEBAST~1") to their real names, keeping
|
||||||
|
// the caller's choice about the \\?\ prefix. Returns the input
|
||||||
|
// unchanged when the path cannot be resolved.
|
||||||
|
public static string ExpandShort(string path) {
|
||||||
|
if (string.IsNullOrEmpty(path)) return path;
|
||||||
|
bool wasExtended = path.StartsWith(@"\\?\");
|
||||||
|
StringBuilder sb = new StringBuilder(32768);
|
||||||
|
uint n = GetLongPathNameW(Extended(path), sb, (uint)sb.Capacity);
|
||||||
|
if (n == 0 || n > sb.Capacity) return path;
|
||||||
|
string expanded = sb.ToString();
|
||||||
|
return wasExtended ? expanded : Trim(expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DirListing GetEntries(string directory) {
|
||||||
|
DirListing result = new DirListing();
|
||||||
|
result.Entries = new DirEntry[0];
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(directory)) {
|
||||||
|
result.ErrorCode = 3; // ERROR_PATH_NOT_FOUND
|
||||||
|
result.ErrorMessage = "Chemin vide.";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Children are reported under the caller's own spelling of the
|
||||||
|
// directory, never under the \\?\ form, so the prefix stays an
|
||||||
|
// implementation detail.
|
||||||
|
string basePath = Trim(directory).TrimEnd('\\');
|
||||||
|
string pattern = Extended(directory).TrimEnd('\\') + @"\*";
|
||||||
|
|
||||||
|
FindData fd;
|
||||||
|
IntPtr h = FindFirstFileW(pattern, out fd);
|
||||||
|
if (h == INVALID_HANDLE_VALUE) {
|
||||||
|
int rc = Marshal.GetLastWin32Error();
|
||||||
|
if (rc == ERROR_NO_MORE_FILES) return result; // empty directory
|
||||||
|
result.ErrorCode = rc;
|
||||||
|
result.ErrorMessage = new System.ComponentModel.Win32Exception(rc).Message;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DirEntry> list = new List<DirEntry>();
|
||||||
|
try {
|
||||||
|
do {
|
||||||
|
if (fd.cFileName == "." || fd.cFileName == "..") continue;
|
||||||
|
DirEntry e = new DirEntry();
|
||||||
|
e.Name = fd.cFileName;
|
||||||
|
e.FullPath = basePath + @"\" + fd.cFileName;
|
||||||
|
e.Attributes = (System.IO.FileAttributes)fd.dwFileAttributes;
|
||||||
|
e.IsDirectory = (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||||
|
e.IsReparsePoint = (fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
|
||||||
|
e.Length = e.IsDirectory ? 0L : (((long)fd.nFileSizeHigh) << 32) | (long)fd.nFileSizeLow;
|
||||||
|
list.Add(e);
|
||||||
|
} while (FindNextFileW(h, out fd));
|
||||||
|
|
||||||
|
int last = Marshal.GetLastWin32Error();
|
||||||
|
if (last != 0 && last != ERROR_NO_MORE_FILES) {
|
||||||
|
result.ErrorCode = last;
|
||||||
|
result.ErrorMessage = new System.ComponentModel.Win32Exception(last).Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally { FindClose(h); }
|
||||||
|
|
||||||
|
result.Entries = list.ToArray();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
internal struct ShareInfo1 {
|
internal struct ShareInfo1 {
|
||||||
[MarshalAs(UnmanagedType.LPWStr)] public string NetName;
|
[MarshalAs(UnmanagedType.LPWStr)] public string NetName;
|
||||||
@@ -1553,6 +1973,25 @@ function Connect-FilerShare {
|
|||||||
# Dot-source core functions into the current scope (for headless + GUI export).
|
# Dot-source core functions into the current scope (for headless + GUI export).
|
||||||
. ([scriptblock]::Create($CoreFunctions))
|
. ([scriptblock]::Create($CoreFunctions))
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# RECOVERY MODE (-RevertPendingGrants)
|
||||||
|
# ============================================================================
|
||||||
|
if ($RevertPendingGrants) {
|
||||||
|
$journals = @(Get-FilerPendingGrantJournal)
|
||||||
|
if ($journals.Count -eq 0) {
|
||||||
|
Write-Host "Aucune modification d'accès en attente." -ForegroundColor Green
|
||||||
|
return
|
||||||
|
}
|
||||||
|
foreach ($j in $journals) {
|
||||||
|
Write-Host ("Journal {0} - {1} élément(s) ..." -f $j.File, @($j.Grants).Count) -ForegroundColor Cyan
|
||||||
|
$r = Restore-FilerPendingGrants -Journal $j
|
||||||
|
$col = if ($r.Failed -gt 0) { 'Yellow' } else { 'Green' }
|
||||||
|
Write-Host (" {0} restauré(s), {1} échec(s)" -f $r.Reverted, $r.Failed) -ForegroundColor $col
|
||||||
|
foreach ($e in $r.Errors) { Write-Host " ! $($e.Path) : $($e.Error)" -ForegroundColor Yellow }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# HEADLESS MODE
|
# HEADLESS MODE
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -1565,6 +2004,12 @@ if ($runHeadless) {
|
|||||||
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-Warning "-GrantAccess nécessite des droits Administrateur ; ce processus n'est pas élevé. Les éléments refusés peuvent ne pas être corrigés."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stale = @(Get-FilerPendingGrantJournal)
|
||||||
|
if ($stale.Count -gt 0) {
|
||||||
|
$nStale = (@($stale | ForEach-Object { @($_.Grants).Count }) | Measure-Object -Sum).Sum
|
||||||
|
Write-Warning ("Une exécution précédente n'a pas annulé ses modifications d'accès ({0} élément(s)). Lancez : .\filer-manager.ps1 -RevertPendingGrants" -f $nStale)
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host "Analyse de $($Path -join ', ') en cours ..." -ForegroundColor Cyan
|
Write-Host "Analyse de $($Path -join ', ') en cours ..." -ForegroundColor Cyan
|
||||||
$progress = [hashtable]::Synchronized(@{ Status = '' })
|
$progress = [hashtable]::Synchronized(@{ Status = '' })
|
||||||
$scan = Invoke-FilerScan -Paths $Path -MaxPathLength $MaxPathLength `
|
$scan = Invoke-FilerScan -Paths $Path -MaxPathLength $MaxPathLength `
|
||||||
@@ -1983,6 +2428,9 @@ $script:Runspace = $null
|
|||||||
$script:PowerShell = $null
|
$script:PowerShell = $null
|
||||||
$script:Handle = $null
|
$script:Handle = $null
|
||||||
$script:Shared = $null
|
$script:Shared = $null
|
||||||
|
# True while a running scan may already have granted access that still needs
|
||||||
|
# reverting - checked by the FormClosing guard below.
|
||||||
|
$script:ScanGrantsPending = $false
|
||||||
|
|
||||||
# Background export state (report generation runs off the UI thread too).
|
# Background export state (report generation runs off the UI thread too).
|
||||||
$script:ExportRunspace = $null
|
$script:ExportRunspace = $null
|
||||||
@@ -2018,7 +2466,10 @@ function New-FilerTreeNode {
|
|||||||
param($Node)
|
param($Node)
|
||||||
$sizeStr = Format-Bytes $Node.Size
|
$sizeStr = Format-Bytes $Node.Size
|
||||||
$isFile = ($Node.PSObject.Properties.Name -contains 'IsFile' -and $Node.IsFile)
|
$isFile = ($Node.PSObject.Properties.Name -contains 'IsFile' -and $Node.IsFile)
|
||||||
if ($isFile) {
|
$isLink = ($Node.PSObject.Properties.Name -contains 'IsLink' -and $Node.IsLink)
|
||||||
|
if ($isLink) {
|
||||||
|
$text = "$($Node.Name) - lien (non parcouru)"
|
||||||
|
} elseif ($isFile) {
|
||||||
$text = "$($Node.Name) - $sizeStr"
|
$text = "$($Node.Name) - $sizeStr"
|
||||||
} else {
|
} else {
|
||||||
$text = "$($Node.Name) - $sizeStr ($($Node.FolderCount) dossiers, $($Node.FileCount) fichiers)"
|
$text = "$($Node.Name) - $sizeStr ($($Node.FolderCount) dossiers, $($Node.FileCount) fichiers)"
|
||||||
@@ -2413,7 +2864,11 @@ function Invoke-RenameListing {
|
|||||||
Dir = (Split-Path -LiteralPath $c.FullName -Parent); Depth = ($cur.Depth + 1)
|
Dir = (Split-Path -LiteralPath $c.FullName -Parent); Depth = ($cur.Depth + 1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if ($recurse) { $stack.Push([pscustomobject]@{ Path = $c.FullName; Depth = ($cur.Depth + 1) }) }
|
# A junction / symlink can be renamed, but descending into one
|
||||||
|
# risks an endless walk when it points back up the tree.
|
||||||
|
if ($recurse -and -not (Test-IsReparsePoint -Entry $c)) {
|
||||||
|
$stack.Push([pscustomobject]@{ Path = $c.FullName; Depth = ($cur.Depth + 1) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
elseif ($wantFile) {
|
elseif ($wantFile) {
|
||||||
[void]$items.Add([pscustomobject]@{
|
[void]$items.Add([pscustomobject]@{
|
||||||
@@ -2585,6 +3040,7 @@ $timer.Add_Tick({
|
|||||||
if ($script:PowerShell) { $script:PowerShell.Dispose() }
|
if ($script:PowerShell) { $script:PowerShell.Dispose() }
|
||||||
if ($script:Runspace) { $script:Runspace.Close(); $script:Runspace.Dispose() }
|
if ($script:Runspace) { $script:Runspace.Close(); $script:Runspace.Dispose() }
|
||||||
$script:PowerShell = $null; $script:Runspace = $null; $script:Handle = $null
|
$script:PowerShell = $null; $script:Runspace = $null; $script:Handle = $null
|
||||||
|
$script:ScanGrantsPending = $false
|
||||||
$progressBar.Visible = $false
|
$progressBar.Visible = $false
|
||||||
$btnScan.Enabled = $true; $btnAdd.Enabled = $true; $btnRemove.Enabled = $true
|
$btnScan.Enabled = $true; $btnAdd.Enabled = $true; $btnRemove.Enabled = $true
|
||||||
}
|
}
|
||||||
@@ -3204,6 +3660,7 @@ $btnScan.Add_Click({
|
|||||||
$progressBar.Visible = $true
|
$progressBar.Visible = $true
|
||||||
$statusLbl.Text = 'Analyse en cours...'
|
$statusLbl.Text = 'Analyse en cours...'
|
||||||
|
|
||||||
|
$script:ScanGrantsPending = ($grant -and $revert)
|
||||||
$script:Shared = [hashtable]::Synchronized(@{ Status = 'Démarrage...'; Result = $null })
|
$script:Shared = [hashtable]::Synchronized(@{ Status = 'Démarrage...'; Result = $null })
|
||||||
$script:Runspace = [runspacefactory]::CreateRunspace()
|
$script:Runspace = [runspacefactory]::CreateRunspace()
|
||||||
$script:Runspace.ApartmentState = 'STA'
|
$script:Runspace.ApartmentState = 'STA'
|
||||||
@@ -3292,6 +3749,50 @@ catch { $Shared.Error = $_.Exception.Message }
|
|||||||
$exportTimer.Start()
|
$exportTimer.Start()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Closing the window kills the background runspace, so a scan that granted
|
||||||
|
# access would never reach Restore-Grants. The journal written by
|
||||||
|
# Save-FilerGrantJournal makes that recoverable, but warning first is cheaper
|
||||||
|
# than recovering afterwards.
|
||||||
|
$form.Add_FormClosing({
|
||||||
|
param($s, $e)
|
||||||
|
if (-not $script:Handle -or $script:Handle.IsCompleted) { return }
|
||||||
|
$msg = if ($script:ScanGrantsPending) {
|
||||||
|
"Une analyse est en cours et elle a peut-être déjà accordé l'accès Administrators à des éléments refusés.`n`n" +
|
||||||
|
"Fermer maintenant l'interrompt AVANT l'annulation de ces modifications : les droits resteraient en place sur le disque. " +
|
||||||
|
"Ils sont journalisés et pourront être restaurés au prochain démarrage, ou avec -RevertPendingGrants.`n`n" +
|
||||||
|
"Fermer quand même ?"
|
||||||
|
} else {
|
||||||
|
"Une analyse est en cours. Fermer maintenant l'interrompt.`n`nFermer quand même ?"
|
||||||
|
}
|
||||||
|
if ([System.Windows.Forms.MessageBox]::Show($msg, 'Filer Manager', 'YesNo', 'Warning') -ne 'Yes') { $e.Cancel = $true }
|
||||||
|
})
|
||||||
|
|
||||||
|
# Offer to undo grants left behind by a run that never got to revert them.
|
||||||
|
$pendingJournals = @(Get-FilerPendingGrantJournal)
|
||||||
|
if ($pendingJournals.Count -gt 0) {
|
||||||
|
$nPending = (@($pendingJournals | ForEach-Object { @($_.Grants).Count }) | Measure-Object -Sum).Sum
|
||||||
|
$msg = "Une exécution précédente s'est arrêtée sans annuler ses modifications d'accès.`n`n" +
|
||||||
|
"$nPending élément(s) portent encore le propriétaire et/ou le FullControl Administrators accordés par Filer Manager.`n`n" +
|
||||||
|
'Les restaurer maintenant ?'
|
||||||
|
if ([System.Windows.Forms.MessageBox]::Show($msg, 'Filer Manager', 'YesNo', 'Warning') -eq 'Yes') {
|
||||||
|
$form.Cursor = [System.Windows.Forms.Cursors]::WaitCursor
|
||||||
|
$done = 0; $failed = 0
|
||||||
|
try {
|
||||||
|
foreach ($j in $pendingJournals) {
|
||||||
|
$r = Restore-FilerPendingGrants -Journal $j
|
||||||
|
$done += $r.Reverted; $failed += $r.Failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally { $form.Cursor = [System.Windows.Forms.Cursors]::Default }
|
||||||
|
$statusLbl.Text = "Restauration : $done annulé(s), $failed échec(s)."
|
||||||
|
if ($failed -gt 0) {
|
||||||
|
[System.Windows.Forms.MessageBox]::Show(
|
||||||
|
"$done élément(s) restauré(s), $failed échec(s). Les échecs restent journalisés et seront réessayés au prochain démarrage.",
|
||||||
|
'Filer Manager', 'OK', 'Warning') | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Pre-fill folders if -Path was supplied without -Output
|
# Pre-fill folders if -Path was supplied without -Output
|
||||||
if ($Path) { foreach ($p in $Path) { [void]$lstFolders.Items.Add($p) } }
|
if ($Path) { foreach ($p in $Path) { [void]$lstFolders.Items.Add($p) } }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user