From ea2a93121a7b3651cec5d549f7b6b3590d56d029 Mon Sep 17 00:00:00 2001 From: pomodori92 <45762339+pomodori92@users.noreply.github.com> Date: Tue, 26 May 2026 23:30:16 +0200 Subject: [PATCH 1/4] Remove user profiles in Windows 11 --- tool/WinDeleteUserProfiles.ps1 | 427 +++++++++++++++++++++++++++++++++ 1 file changed, 427 insertions(+) create mode 100644 tool/WinDeleteUserProfiles.ps1 diff --git a/tool/WinDeleteUserProfiles.ps1 b/tool/WinDeleteUserProfiles.ps1 new file mode 100644 index 0000000..054f957 --- /dev/null +++ b/tool/WinDeleteUserProfiles.ps1 @@ -0,0 +1,427 @@ +#requires -version 5.1 +#requires -RunAsAdministrator + +function DeleteUserProfilesInWindows11 { + <# + .SYNOPSIS + Rimuove in modo sicuro i profili utente locali non caricati da Windows 11. + + .DESCRIPTION + Esegue una pulizia controllata dei profili locali presenti in C:\Users utilizzando Win32_UserProfile. + Esclude profili speciali, profili caricati, account di sistema, profilo utente corrente e nomi protetti. + La rimozione usa prima Remove-CimInstance, poi una pulizia filesystem di fallback solo se la cartella rimane presente. + + .PARAMETER MaxThreads + Numero massimo di runspace paralleli. Per Win32_UserProfile viene limitato automaticamente a 4. + + .PARAMETER UsersRoot + Percorso radice dei profili utente locali. + + .PARAMETER LogFolder + Cartella in cui salvare il file di log. + + .PARAMETER MinimumProfileAgeDays + Età minima, in giorni, dell'ultima data di utilizzo del profilo. Default 0 mantiene il comportamento originale. + + .PARAMETER Force + Salta la richiesta di conferma interattiva. + + .PARAMETER SuppressToolkitSession + Non richiama Start-ToolkitSession anche se disponibile. + + .EXAMPLE + DeleteUserProfilesInWindows11 + + .EXAMPLE + DeleteUserProfilesInWindows11 -MinimumProfileAgeDays 30 -Force + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param( + [ValidateRange(1, 16)] + [int]$MaxThreads = [Math]::Min(4, [Environment]::ProcessorCount), + + [ValidateNotNullOrEmpty()] + [string]$UsersRoot = 'C:\Users', + + [ValidateNotNullOrEmpty()] + [string]$LogFolder = 'C:\Temp', + + [ValidateRange(0, 3650)] + [int]$MinimumProfileAgeDays = 0, + + [switch]$Force, + + [switch]$SuppressToolkitSession + ) + + begin { + $script:ToolName = 'DeleteUserProfilesInWindows11' + $script:ToolVersion = '3.0' + $script:SessionStart = Get-Date + $script:UsersRoot = [System.IO.Path]::GetFullPath($UsersRoot.TrimEnd('\') + '\') + $script:LogFolder = [System.IO.Path]::GetFullPath($LogFolder) + $script:LogFile = Join-Path $script:LogFolder ("{0}_{1}.log" -f $script:ToolName, (Get-Date -Format 'yyyyMMdd_HHmmss')) + $script:CurrentUser = $env:USERNAME + $script:ComputerName = $env:COMPUTERNAME + $script:MinimumLastUseDate = if ($MinimumProfileAgeDays -gt 0) { (Get-Date).AddDays(-$MinimumProfileAgeDays) } else { $null } + $script:ProtectedProfileNames = @( + 'Public', + 'Pubblica', + 'Default', + 'Default User', + 'All Users', + 'defaultuser0', + 'WDAGUtilityAccount', + 'Administrator', + 'Guest', + $script:CurrentUser + ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + + $savedErrorActionPreference = $ErrorActionPreference + $savedProgressPreference = $ProgressPreference + $ErrorActionPreference = 'Stop' + $ProgressPreference = 'Continue' + + $script:LogQueue = [System.Collections.Concurrent.ConcurrentQueue[string]]::new() + } + + process { + function Write-ToolkitMessage { + param( + [ValidateSet('Info', 'Success', 'Warning', 'Error')] + [string]$Type = 'Info', + + [Parameter(Mandatory = $true)] + [string]$Text + ) + + if (Get-Command -Name Write-StyledMessage -ErrorAction SilentlyContinue) { + Write-StyledMessage -Type $Type -Text $Text + return + } + + $color = switch ($Type) { + 'Success' { 'Green' } + 'Warning' { 'Yellow' } + 'Error' { 'Red' } + default { 'Cyan' } + } + + Write-Host $Text -ForegroundColor $color + } + + function Add-ProfileCleanupLog { + param( + [Parameter(Mandatory = $true)] + [string]$Text, + + [ValidateSet('INFO', 'SUCCESS', 'WARNING', 'ERROR')] + [string]$Level = 'INFO' + ) + + $script:LogQueue.Enqueue( + ('{0} [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Text) + ) + } + + function Test-IsAdministrator { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]::new($identity) + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + } + + function Initialize-ProfileCleanupSession { + [System.IO.Directory]::CreateDirectory($script:LogFolder) | Out-Null + + if (-not (Test-IsAdministrator)) { + throw 'Lo script deve essere eseguito da una console PowerShell avviata come amministratore.' + } + + if (-not (Test-Path -LiteralPath $script:UsersRoot -PathType Container)) { + throw "Il percorso profili non esiste: $script:UsersRoot" + } + + $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue + if ($os -and $os.Caption -notmatch 'Windows 11') { + Write-ToolkitMessage -Type 'Warning' -Text "⚠️ Sistema rilevato: $($os.Caption). Lo script è pensato per Windows 11." + } + + if (-not $SuppressToolkitSession -and (Get-Command -Name Start-ToolkitSession -ErrorAction SilentlyContinue)) { + Start-ToolkitSession -ToolName $script:ToolName -SubTitle 'Profile Cleanup Toolkit' + } + else { + Write-Host '' + Write-Host '====================================================' -ForegroundColor Cyan + Write-Host (" {0} v{1}" -f $script:ToolName, $script:ToolVersion) + Write-Host '====================================================' -ForegroundColor Cyan + Write-Host '' + } + + Write-ToolkitMessage -Type 'Info' -Text ("🖥️ Computer: {0}" -f $script:ComputerName) + Write-ToolkitMessage -Type 'Info' -Text ("👤 Utente corrente protetto: {0}" -f $script:CurrentUser) + Write-ToolkitMessage -Type 'Info' -Text ("📁 Percorso profili: {0}" -f $script:UsersRoot) + Write-ToolkitMessage -Type 'Info' -Text ("🧵 Thread configurati: {0}" -f $MaxThreads) + if ($script:MinimumLastUseDate) { + Write-ToolkitMessage -Type 'Info' -Text ("📅 Soglia ultima attività: profili non usati da almeno {0} giorni." -f $MinimumProfileAgeDays) + } + + Add-ProfileCleanupLog -Text "Sessione avviata su $script:ComputerName." + } + + function Get-RemovableUserProfiles { + $excluded = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + $script:ProtectedProfileNames | ForEach-Object { [void]$excluded.Add($_) } + + Write-ToolkitMessage -Type 'Info' -Text '🔍 Scansione profili locali in corso.' + + $profiles = Get-CimInstance -ClassName Win32_UserProfile | Where-Object { + -not $_.Special -and + -not $_.Loaded -and + $_.LocalPath -and + $_.LocalPath.StartsWith($script:UsersRoot, [System.StringComparison]::OrdinalIgnoreCase) + } + + foreach ($profile in $profiles) { + $profileName = [System.IO.Path]::GetFileName($profile.LocalPath) + + if ($excluded.Contains($profileName)) { + Add-ProfileCleanupLog -Text "Profilo escluso: $profileName ($($profile.LocalPath))." + continue + } + + if ($script:MinimumLastUseDate -and $profile.LastUseTime) { + $lastUse = $profile.LastUseTime + if ($lastUse -gt $script:MinimumLastUseDate) { + Add-ProfileCleanupLog -Text "Profilo escluso per soglia temporale: $profileName, ultimo uso $lastUse." + continue + } + } + + $profile + } + } + + function Show-ProfileCleanupPreview { + param( + [Parameter(Mandatory = $true)] + [array]$Profiles + ) + + Write-Host '' + Write-ToolkitMessage -Type 'Warning' -Text 'Profili selezionati per la rimozione:' + Write-Host '' + + $Profiles | + Select-Object @{Name='User'; Expression={ [System.IO.Path]::GetFileName($_.LocalPath) }}, + @{Name='Loaded'; Expression={ $_.Loaded }}, + @{Name='LastUseTime'; Expression={ $_.LastUseTime }}, + @{Name='Path'; Expression={ $_.LocalPath }} | + Format-Table -AutoSize + + Write-Host '' + } + + function Invoke-ProfileRemovalBatch { + param( + [Parameter(Mandatory = $true)] + [array]$Profiles + ) + + $pool = [RunspaceFactory]::CreateRunspacePool(1, $MaxThreads) + $pool.Open() + + $jobs = [System.Collections.Generic.List[object]]::new() + + $scriptBlock = { + param($Profile, $LogQueue) + + $userPath = $Profile.LocalPath + $userName = [System.IO.Path]::GetFileName($userPath) + $start = Get-Date + + $LogQueue.Enqueue(('{0} [INFO] START - {1} - {2}' -f $start.ToString('yyyy-MM-dd HH:mm:ss'), $userName, $userPath)) + + try { + Remove-CimInstance -InputObject $Profile -ErrorAction Stop + $LogQueue.Enqueue(('{0} [SUCCESS] CIM profile removed - {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName)) + } + catch { + $LogQueue.Enqueue(('{0} [WARNING] CIM remove failed - {1} - {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName, $_.Exception.Message)) + } + + if ([System.IO.Directory]::Exists($userPath)) { + try { + Remove-Item -LiteralPath $userPath -Force -Recurse -ErrorAction Stop + $LogQueue.Enqueue(('{0} [SUCCESS] Folder removed - {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName)) + } + catch { + $LogQueue.Enqueue(('{0} [WARNING] Standard folder cleanup failed - {1} - {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName, $_.Exception.Message)) + + try { + & takeown.exe /F $userPath /R /D Y | Out-Null + & icacls.exe $userPath /grant Administrators:F /T /C | Out-Null + Remove-Item -LiteralPath $userPath -Force -Recurse -ErrorAction Stop + $LogQueue.Enqueue(('{0} [SUCCESS] Folder removed after ACL reset - {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName)) + } + catch { + $LogQueue.Enqueue(('{0} [ERROR] Cleanup failed - {1} - {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName, $_.Exception.Message)) + } + } + } + + $success = -not [System.IO.Directory]::Exists($userPath) + $duration = New-TimeSpan -Start $start -End (Get-Date) + + if ($success) { + $LogQueue.Enqueue(('{0} [SUCCESS] COMPLETED - {1} - {2:hh\:mm\:ss}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName, $duration)) + } + else { + $LogQueue.Enqueue(('{0} [ERROR] FAILED - {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName)) + } + + return [PSCustomObject]@{ + UserName = $userName + Path = $userPath + Success = $success + Duration = $duration + } + } + + try { + foreach ($profile in $Profiles) { + $ps = [PowerShell]::Create() + $ps.RunspacePool = $pool + + [void]$ps.AddScript($scriptBlock, $true). + AddArgument($profile). + AddArgument($script:LogQueue) + + $handle = $ps.BeginInvoke() + + $jobs.Add([PSCustomObject]@{ + PowerShell = $ps + Handle = $handle + }) + } + + $total = $jobs.Count + $lastPercent = -1 + + do { + $completed = ($jobs | Where-Object { $_.Handle.IsCompleted }).Count + $percent = if ($total -gt 0) { [math]::Floor(($completed / $total) * 100) } else { 100 } + + if ($percent -ne $lastPercent) { + $lastPercent = $percent + Write-Progress -Activity 'Rimozione profili utente' -Status "$completed / $total completati" -PercentComplete $percent + } + + Start-Sleep -Milliseconds 500 + } while ($completed -lt $total) + + Write-Progress -Activity 'Rimozione profili utente' -Completed + + $results = foreach ($job in $jobs) { + try { + $job.PowerShell.EndInvoke($job.Handle) + } + catch { + Add-ProfileCleanupLog -Level 'ERROR' -Text "Errore runspace: $($_.Exception.Message)" + } + finally { + $job.PowerShell.Commands.Clear() + $job.PowerShell.Dispose() + } + } + + return $results + } + finally { + if ($pool) { + $pool.Close() + $pool.Dispose() + } + } + } + + function Save-ProfileCleanupLog { + $logLines = [System.Collections.Generic.List[string]]::new() + $line = $null + + while ($script:LogQueue.TryDequeue([ref]$line)) { + $logLines.Add($line) + } + + $logLines | Set-Content -LiteralPath $script:LogFile -Encoding UTF8 + } + + try { + Initialize-ProfileCleanupSession + + $targets = @(Get-RemovableUserProfiles) + + if (-not $targets -or $targets.Count -eq 0) { + Write-Host '' + Write-ToolkitMessage -Type 'Success' -Text '✅ Nessun profilo rimovibile trovato.' + Add-ProfileCleanupLog -Level 'SUCCESS' -Text 'Nessun profilo rimovibile trovato.' + return + } + + Show-ProfileCleanupPreview -Profiles $targets + + $shouldContinue = $Force -or $PSCmdlet.ShouldContinue( + ("Saranno rimossi {0} profili locali. Continuare?" -f $targets.Count), + 'Conferma rimozione profili' + ) + + if (-not $shouldContinue) { + Write-ToolkitMessage -Type 'Warning' -Text 'Operazione annullata dall’utente.' + Add-ProfileCleanupLog -Level 'WARNING' -Text 'Operazione annullata dall’utente.' + return + } + + Write-Host '' + Write-ToolkitMessage -Type 'Info' -Text '🚀 Avvio rimozione profili.' + Write-Host '' + + $results = if ($PSCmdlet.ShouldProcess($script:ComputerName, "Rimozione di $($targets.Count) profili utente locali")) { + @(Invoke-ProfileRemovalBatch -Profiles $targets) + } + else { + @() + } + + $successCount = @($results | Where-Object { $_.Success }).Count + $failedCount = @($results | Where-Object { -not $_.Success }).Count + $script:SessionEnd = Get-Date + $totalDuration = New-TimeSpan -Start $script:SessionStart -End $script:SessionEnd + + Add-ProfileCleanupLog -Level 'INFO' -Text "Sessione completata. Successi: $successCount. Errori: $failedCount. Durata: $totalDuration." + Save-ProfileCleanupLog + + Write-Host '' + Write-Host '====================================================' -ForegroundColor Green + Write-Host ' COMPLETATO' + Write-Host '====================================================' -ForegroundColor Green + Write-Host '' + Write-ToolkitMessage -Type 'Success' -Text ("✅ Profili rimossi: {0}" -f $successCount) + if ($failedCount -gt 0) { + Write-ToolkitMessage -Type 'Warning' -Text ("⚠️ Profili non rimossi: {0}" -f $failedCount) + } + Write-ToolkitMessage -Type 'Info' -Text ("⏱️ Durata: {0:hh\:mm\:ss}" -f $totalDuration) + Write-ToolkitMessage -Type 'Info' -Text ("📄 Log: {0}" -f $script:LogFile) + } + catch { + Add-ProfileCleanupLog -Level 'ERROR' -Text $_.Exception.Message + try { Save-ProfileCleanupLog } catch { } + Write-ToolkitMessage -Type 'Error' -Text ("❌ Errore: {0}" -f $_.Exception.Message) + throw + } + finally { + $ErrorActionPreference = $savedErrorActionPreference + $ProgressPreference = $savedProgressPreference + } + } +} + +DeleteUserProfilesInWindows11 @args From bd8b51488e4924beeecbbd759dd2b297e72b4e1a Mon Sep 17 00:00:00 2001 From: pomodori92 <45762339+pomodori92@users.noreply.github.com> Date: Thu, 28 May 2026 13:22:02 +0200 Subject: [PATCH 2/4] Enhance user profile cleanup script functionality Updated the script to improve user profile cleanup functionality, including changes to parameters and logging. Added a new option to skip residual folder cleanup. --- tool/WinDeleteUserProfiles.ps1 | 282 +++++++++++++++++++++++++++------ 1 file changed, 235 insertions(+), 47 deletions(-) diff --git a/tool/WinDeleteUserProfiles.ps1 b/tool/WinDeleteUserProfiles.ps1 index 054f957..c87e72e 100644 --- a/tool/WinDeleteUserProfiles.ps1 +++ b/tool/WinDeleteUserProfiles.ps1 @@ -4,12 +4,15 @@ function DeleteUserProfilesInWindows11 { <# .SYNOPSIS - Rimuove in modo sicuro i profili utente locali non caricati da Windows 11. + Rimuove in modo sicuro i profili utente locali non caricati e le cartelle residue in C:\Users. .DESCRIPTION Esegue una pulizia controllata dei profili locali presenti in C:\Users utilizzando Win32_UserProfile. Esclude profili speciali, profili caricati, account di sistema, profilo utente corrente e nomi protetti. - La rimozione usa prima Remove-CimInstance, poi una pulizia filesystem di fallback solo se la cartella rimane presente. + Dopo la cancellazione dei profili registrati, controlla la cartella utenti e rimuove eventuali directory residue + non più associate a profili presenti nel registro/CIM, sempre rispettando le esclusioni protette. + + Lo script non chiede conferme interattive prima delle cancellazioni. .PARAMETER MaxThreads Numero massimo di runspace paralleli. Per Win32_UserProfile viene limitato automaticamente a 4. @@ -23,8 +26,8 @@ function DeleteUserProfilesInWindows11 { .PARAMETER MinimumProfileAgeDays Età minima, in giorni, dell'ultima data di utilizzo del profilo. Default 0 mantiene il comportamento originale. - .PARAMETER Force - Salta la richiesta di conferma interattiva. + .PARAMETER SkipResidualFolderCleanup + Salta la pulizia finale delle cartelle residue in C:\Users. .PARAMETER SuppressToolkitSession Non richiama Start-ToolkitSession anche se disponibile. @@ -33,12 +36,12 @@ function DeleteUserProfilesInWindows11 { DeleteUserProfilesInWindows11 .EXAMPLE - DeleteUserProfilesInWindows11 -MinimumProfileAgeDays 30 -Force + DeleteUserProfilesInWindows11 -MinimumProfileAgeDays 30 #> - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + [CmdletBinding()] param( [ValidateRange(1, 16)] - [int]$MaxThreads = [Math]::Min(4, [Environment]::ProcessorCount), + [int]$MaxThreads = [Math]::Min(2, [Environment]::ProcessorCount), [ValidateNotNullOrEmpty()] [string]$UsersRoot = 'C:\Users', @@ -49,14 +52,14 @@ function DeleteUserProfilesInWindows11 { [ValidateRange(0, 3650)] [int]$MinimumProfileAgeDays = 0, - [switch]$Force, + [switch]$SkipResidualFolderCleanup, [switch]$SuppressToolkitSession ) begin { $script:ToolName = 'DeleteUserProfilesInWindows11' - $script:ToolVersion = '3.0' + $script:ToolVersion = '3.1' $script:SessionStart = Get-Date $script:UsersRoot = [System.IO.Path]::GetFullPath($UsersRoot.TrimEnd('\') + '\') $script:LogFolder = [System.IO.Path]::GetFullPath($LogFolder) @@ -64,6 +67,7 @@ function DeleteUserProfilesInWindows11 { $script:CurrentUser = $env:USERNAME $script:ComputerName = $env:COMPUTERNAME $script:MinimumLastUseDate = if ($MinimumProfileAgeDays -gt 0) { (Get-Date).AddDays(-$MinimumProfileAgeDays) } else { $null } + $script:ProtectedProfileNames = @( 'Public', 'Pubblica', @@ -79,8 +83,11 @@ function DeleteUserProfilesInWindows11 { $savedErrorActionPreference = $ErrorActionPreference $savedProgressPreference = $ProgressPreference + $savedConfirmPreference = $ConfirmPreference + $ErrorActionPreference = 'Stop' $ProgressPreference = 'Continue' + $ConfirmPreference = 'None' $script:LogQueue = [System.Collections.Concurrent.ConcurrentQueue[string]]::new() } @@ -110,6 +117,7 @@ function DeleteUserProfilesInWindows11 { Write-Host $Text -ForegroundColor $color } + function Add-ProfileCleanupLog { param( [Parameter(Mandatory = $true)] @@ -124,12 +132,14 @@ function DeleteUserProfilesInWindows11 { ) } + function Test-IsAdministrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } + function Initialize-ProfileCleanupSession { [System.IO.Directory]::CreateDirectory($script:LogFolder) | Out-Null @@ -161,6 +171,8 @@ function DeleteUserProfilesInWindows11 { Write-ToolkitMessage -Type 'Info' -Text ("👤 Utente corrente protetto: {0}" -f $script:CurrentUser) Write-ToolkitMessage -Type 'Info' -Text ("📁 Percorso profili: {0}" -f $script:UsersRoot) Write-ToolkitMessage -Type 'Info' -Text ("🧵 Thread configurati: {0}" -f $MaxThreads) + Write-ToolkitMessage -Type 'Warning' -Text '⚠️ Modalità non interattiva: nessuna conferma verrà richiesta prima delle cancellazioni.' + if ($script:MinimumLastUseDate) { Write-ToolkitMessage -Type 'Info' -Text ("📅 Soglia ultima attività: profili non usati da almeno {0} giorni." -f $MinimumProfileAgeDays) } @@ -168,11 +180,39 @@ function DeleteUserProfilesInWindows11 { Add-ProfileCleanupLog -Text "Sessione avviata su $script:ComputerName." } - function Get-RemovableUserProfiles { + + function New-ProtectedNameSet { $excluded = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $script:ProtectedProfileNames | ForEach-Object { [void]$excluded.Add($_) } + return ,$excluded + } + + + function Get-RegisteredProfilePathSet { + $pathSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + + Get-CimInstance -ClassName Win32_UserProfile -ErrorAction SilentlyContinue | + Where-Object { + $_.LocalPath -and + $_.LocalPath.StartsWith($script:UsersRoot, [System.StringComparison]::OrdinalIgnoreCase) + } | + ForEach-Object { + try { + [void]$pathSet.Add([System.IO.Path]::GetFullPath($_.LocalPath).TrimEnd('\')) + } + catch { + Add-ProfileCleanupLog -Level 'WARNING' -Text "Impossibile normalizzare LocalPath profilo registrato: $($_.LocalPath)" + } + } + + return ,$pathSet + } - Write-ToolkitMessage -Type 'Info' -Text '🔍 Scansione profili locali in corso.' + + function Get-RemovableUserProfiles { + $excluded = New-ProtectedNameSet + + Write-ToolkitMessage -Type 'Info' -Text '🔍 Scansione profili locali registrati in corso.' $profiles = Get-CimInstance -ClassName Win32_UserProfile | Where-Object { -not $_.Special -and @@ -201,6 +241,7 @@ function DeleteUserProfilesInWindows11 { } } + function Show-ProfileCleanupPreview { param( [Parameter(Mandatory = $true)] @@ -208,7 +249,7 @@ function DeleteUserProfilesInWindows11 { ) Write-Host '' - Write-ToolkitMessage -Type 'Warning' -Text 'Profili selezionati per la rimozione:' + Write-ToolkitMessage -Type 'Warning' -Text 'Profili registrati selezionati per la rimozione automatica:' Write-Host '' $Profiles | @@ -221,6 +262,7 @@ function DeleteUserProfilesInWindows11 { Write-Host '' } + function Invoke-ProfileRemovalBatch { param( [Parameter(Mandatory = $true)] @@ -235,14 +277,17 @@ function DeleteUserProfilesInWindows11 { $scriptBlock = { param($Profile, $LogQueue) + $ConfirmPreference = 'None' + $ErrorActionPreference = 'Stop' + $userPath = $Profile.LocalPath $userName = [System.IO.Path]::GetFileName($userPath) $start = Get-Date - $LogQueue.Enqueue(('{0} [INFO] START - {1} - {2}' -f $start.ToString('yyyy-MM-dd HH:mm:ss'), $userName, $userPath)) + $LogQueue.Enqueue(('{0} [INFO] START PROFILE - {1} - {2}' -f $start.ToString('yyyy-MM-dd HH:mm:ss'), $userName, $userPath)) try { - Remove-CimInstance -InputObject $Profile -ErrorAction Stop + Remove-CimInstance -InputObject $Profile -ErrorAction Stop -Confirm:$false $LogQueue.Enqueue(('{0} [SUCCESS] CIM profile removed - {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName)) } catch { @@ -251,16 +296,33 @@ function DeleteUserProfilesInWindows11 { if ([System.IO.Directory]::Exists($userPath)) { try { - Remove-Item -LiteralPath $userPath -Force -Recurse -ErrorAction Stop + $tempEmpty = Join-Path $env:TEMP "EmptyFolder" + + if (-not (Test-Path $tempEmpty)) { + New-Item -ItemType Directory -Path $tempEmpty | Out-Null + } + robocopy $tempEmpty $userPath /MIR /R:1 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Null + Remove-Item -LiteralPath $userPath -Force -Recurse -ErrorAction SilentlyContinue + $LogQueue.Enqueue(('{0} [SUCCESS] Folder removed - {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName)) } catch { $LogQueue.Enqueue(('{0} [WARNING] Standard folder cleanup failed - {1} - {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName, $_.Exception.Message)) try { - & takeown.exe /F $userPath /R /D Y | Out-Null + try { + & takeown.exe /F $userPath /R /D S | Out-Null + } + catch { + try { + & takeown.exe /F $userPath /R /D Y | Out-Null + } + catch { + $LogQueue.Enqueue(('{0} [ERROR] takeown failed - {1} - {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName, $_.Exception.Message)) + } + } & icacls.exe $userPath /grant Administrators:F /T /C | Out-Null - Remove-Item -LiteralPath $userPath -Force -Recurse -ErrorAction Stop + Remove-Item -LiteralPath $userPath -Force -Recurse -ErrorAction Stop -Confirm:$false $LogQueue.Enqueue(('{0} [SUCCESS] Folder removed after ACL reset - {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName)) } catch { @@ -273,13 +335,14 @@ function DeleteUserProfilesInWindows11 { $duration = New-TimeSpan -Start $start -End (Get-Date) if ($success) { - $LogQueue.Enqueue(('{0} [SUCCESS] COMPLETED - {1} - {2:hh\:mm\:ss}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName, $duration)) + $LogQueue.Enqueue(('{0} [SUCCESS] COMPLETED PROFILE - {1} - {2:hh\:mm\:ss}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName, $duration)) } else { - $LogQueue.Enqueue(('{0} [ERROR] FAILED - {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName)) + $LogQueue.Enqueue(('{0} [ERROR] FAILED PROFILE - {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $userName)) } return [PSCustomObject]@{ + Type = 'Profile' UserName = $userName Path = $userPath Success = $success @@ -313,13 +376,13 @@ function DeleteUserProfilesInWindows11 { if ($percent -ne $lastPercent) { $lastPercent = $percent - Write-Progress -Activity 'Rimozione profili utente' -Status "$completed / $total completati" -PercentComplete $percent + Write-Progress -Activity 'Rimozione profili registrati' -Status "$completed / $total completati" -PercentComplete $percent } Start-Sleep -Milliseconds 500 } while ($completed -lt $total) - Write-Progress -Activity 'Rimozione profili utente' -Completed + Write-Progress -Activity 'Rimozione profili registrati' -Completed $results = foreach ($job in $jobs) { try { @@ -344,6 +407,114 @@ function DeleteUserProfilesInWindows11 { } } + + function Get-ResidualUserFolders { + $excluded = New-ProtectedNameSet + $registeredProfilePaths = Get-RegisteredProfilePathSet + + Write-ToolkitMessage -Type 'Info' -Text '🔎 Controllo cartelle residue nella directory utenti.' + + $folders = Get-ChildItem -Path $UsersRoot -Directory -Force | + Where-Object { + -not ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) + } + + foreach ($folder in $folders) { + $folderName = $folder.Name + $folderPath = [System.IO.Path]::GetFullPath($folder.FullName).TrimEnd('\') + + if ($excluded.Contains($folderName)) { + Add-ProfileCleanupLog -Text "Cartella residua esclusa per nome protetto: $folderName ($folderPath)." + continue + } + + if ($registeredProfilePaths.Contains($folderPath)) { + Add-ProfileCleanupLog -Text "Cartella residua esclusa perché ancora associata a Win32_UserProfile: $folderName ($folderPath)." + continue + } + + if ($folder.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { + Add-ProfileCleanupLog -Level 'WARNING' -Text "Cartella residua esclusa perché reparse point/symlink: $folderName ($folderPath)." + continue + } + + [PSCustomObject]@{ + Name = $folderName + Path = $folderPath + } + } + } + + + function Remove-ResidualUserFolders { + param( + [Parameter(Mandatory = $true)] + [array]$Folders + ) + + $results = [System.Collections.Generic.List[object]]::new() + $total = $Folders.Count + $index = 0 + + foreach ($folder in $Folders) { + $index++ + $percent = if ($total -gt 0) { [math]::Floor(($index / $total) * 100) } else { 100 } + + Write-Progress ` + -Activity 'Rimozione cartelle residue in C:\Users' ` + -Status ("{0} / {1} - {2}" -f $index, $total, $folder.Name) ` + -PercentComplete $percent + + $start = Get-Date + $success = $false + + Add-ProfileCleanupLog -Text "START RESIDUAL FOLDER - $($folder.Name) - $($folder.Path)" + + $folderPath = $folder.Path + + try { + Remove-Item -LiteralPath $folderPath -Force -Recurse -ErrorAction Stop -Confirm:$false + $success = -not [System.IO.Directory]::Exists($folderPath) + } + catch { + Add-ProfileCleanupLog -Level 'WARNING' -Text "Rimozione standard cartella residua fallita: $folderPath - $($_.Exception.Message)" + + try { + & takeown.exe /F $folderPath /R /D Y | Out-Null + & icacls.exe $folderPath /grant Administrators:F /T /C | Out-Null + Remove-Item -LiteralPath $folderPath -Force -Recurse -ErrorAction Stop -Confirm:$false + $success = -not [System.IO.Directory]::Exists($folderPath) + } + catch { + Add-ProfileCleanupLog -Level 'ERROR' -Text "Rimozione cartella residua fallita: $folderPath - $($_.Exception.Message)" + $success = $false + } + } + + $duration = New-TimeSpan -Start $start -End (Get-Date) + + if ($success) { + Add-ProfileCleanupLog -Level 'SUCCESS' -Text "COMPLETED RESIDUAL FOLDER - $($folder.Name) - $($duration.ToString())" + } + else { + Add-ProfileCleanupLog -Level 'ERROR' -Text "FAILED RESIDUAL FOLDER - $($folder.Name)" + } + + $results.Add([PSCustomObject]@{ + Type = 'ResidualFolder' + UserName = $folder.Name + Path = $folder.Path + Success = $success + Duration = $duration + }) | Out-Null + } + + Write-Progress -Activity 'Rimozione cartelle residue in C:\Users' -Completed + + return $results + } + + function Save-ProfileCleanupLog { $logLines = [System.Collections.Generic.List[string]]::new() $line = $null @@ -358,45 +529,60 @@ function DeleteUserProfilesInWindows11 { try { Initialize-ProfileCleanupSession + $profileResults = @() + $residualResults = @() + $targets = @(Get-RemovableUserProfiles) - if (-not $targets -or $targets.Count -eq 0) { + if ($targets -and $targets.Count -gt 0) { + Show-ProfileCleanupPreview -Profiles $targets + + Write-Host '' + Write-ToolkitMessage -Type 'Info' -Text ("🚀 Avvio rimozione automatica di {0} profili registrati." -f $targets.Count) Write-Host '' - Write-ToolkitMessage -Type 'Success' -Text '✅ Nessun profilo rimovibile trovato.' - Add-ProfileCleanupLog -Level 'SUCCESS' -Text 'Nessun profilo rimovibile trovato.' - return - } - Show-ProfileCleanupPreview -Profiles $targets + $profileResults = @(Invoke-ProfileRemovalBatch -Profiles $targets) + } + else { + Write-Host '' + Write-ToolkitMessage -Type 'Success' -Text '✅ Nessun profilo registrato rimovibile trovato.' + Add-ProfileCleanupLog -Level 'SUCCESS' -Text 'Nessun profilo registrato rimovibile trovato.' + } - $shouldContinue = $Force -or $PSCmdlet.ShouldContinue( - ("Saranno rimossi {0} profili locali. Continuare?" -f $targets.Count), - 'Conferma rimozione profili' - ) + if (-not $SkipResidualFolderCleanup) { + $residualFolders = @(Get-ResidualUserFolders) - if (-not $shouldContinue) { - Write-ToolkitMessage -Type 'Warning' -Text 'Operazione annullata dall’utente.' - Add-ProfileCleanupLog -Level 'WARNING' -Text 'Operazione annullata dall’utente.' - return - } + if ($residualFolders -and $residualFolders.Count -gt 0) { + Write-Host '' + Write-ToolkitMessage -Type 'Warning' -Text ("Cartelle residue selezionate per la rimozione automatica: {0}" -f $residualFolders.Count) + $residualFolders | Select-Object Name, Path | Format-Table -AutoSize - Write-Host '' - Write-ToolkitMessage -Type 'Info' -Text '🚀 Avvio rimozione profili.' - Write-Host '' + Write-Host '' + Write-ToolkitMessage -Type 'Info' -Text '🧹 Avvio rimozione cartelle residue.' + Write-Host '' - $results = if ($PSCmdlet.ShouldProcess($script:ComputerName, "Rimozione di $($targets.Count) profili utente locali")) { - @(Invoke-ProfileRemovalBatch -Profiles $targets) + $residualResults = @(Remove-ResidualUserFolders -Folders $residualFolders) + } + else { + Write-ToolkitMessage -Type 'Success' -Text '✅ Nessuna cartella residua rimovibile trovata in C:\Users.' + Add-ProfileCleanupLog -Level 'SUCCESS' -Text 'Nessuna cartella residua rimovibile trovata.' + } } else { - @() + Write-ToolkitMessage -Type 'Warning' -Text 'Pulizia cartelle residue saltata per parametro SkipResidualFolderCleanup.' + Add-ProfileCleanupLog -Level 'WARNING' -Text 'Pulizia cartelle residue saltata.' } - $successCount = @($results | Where-Object { $_.Success }).Count - $failedCount = @($results | Where-Object { -not $_.Success }).Count + $allResults = @($profileResults) + @($residualResults) + $successCount = @($allResults | Where-Object { $_.Success }).Count + $failedCount = @($allResults | Where-Object { -not $_.Success }).Count + $profileSuccessCount = @($profileResults | Where-Object { $_.Success }).Count + $residualSuccessCount = @($residualResults | Where-Object { $_.Success }).Count + $script:SessionEnd = Get-Date $totalDuration = New-TimeSpan -Start $script:SessionStart -End $script:SessionEnd - Add-ProfileCleanupLog -Level 'INFO' -Text "Sessione completata. Successi: $successCount. Errori: $failedCount. Durata: $totalDuration." + Add-ProfileCleanupLog -Level 'INFO' -Text "Sessione completata. Profili rimossi: $profileSuccessCount. Cartelle residue rimosse: $residualSuccessCount. Errori: $failedCount. Durata: $totalDuration." Save-ProfileCleanupLog Write-Host '' @@ -404,9 +590,10 @@ function DeleteUserProfilesInWindows11 { Write-Host ' COMPLETATO' Write-Host '====================================================' -ForegroundColor Green Write-Host '' - Write-ToolkitMessage -Type 'Success' -Text ("✅ Profili rimossi: {0}" -f $successCount) + Write-ToolkitMessage -Type 'Success' -Text ("✅ Profili registrati rimossi: {0}" -f $profileSuccessCount) + Write-ToolkitMessage -Type 'Success' -Text ("✅ Cartelle residue rimosse: {0}" -f $residualSuccessCount) if ($failedCount -gt 0) { - Write-ToolkitMessage -Type 'Warning' -Text ("⚠️ Profili non rimossi: {0}" -f $failedCount) + Write-ToolkitMessage -Type 'Warning' -Text ("⚠️ Elementi non rimossi: {0}" -f $failedCount) } Write-ToolkitMessage -Type 'Info' -Text ("⏱️ Durata: {0:hh\:mm\:ss}" -f $totalDuration) Write-ToolkitMessage -Type 'Info' -Text ("📄 Log: {0}" -f $script:LogFile) @@ -420,6 +607,7 @@ function DeleteUserProfilesInWindows11 { finally { $ErrorActionPreference = $savedErrorActionPreference $ProgressPreference = $savedProgressPreference + $ConfirmPreference = $savedConfirmPreference } } } From 97c5197fdf8db85c80697ede94a952d42958c36c Mon Sep 17 00:00:00 2001 From: pomodori92 Date: Sat, 20 Jun 2026 12:36:37 +0200 Subject: [PATCH 3/4] refactor Variabili globali, stampa dei messaggi e altro uniformati agli altri script --- tool/WinDeleteUserProfiles.ps1 | 152 ++++++++++++++++++++++----------- 1 file changed, 101 insertions(+), 51 deletions(-) diff --git a/tool/WinDeleteUserProfiles.ps1 b/tool/WinDeleteUserProfiles.ps1 index c87e72e..726f352 100644 --- a/tool/WinDeleteUserProfiles.ps1 +++ b/tool/WinDeleteUserProfiles.ps1 @@ -1,7 +1,4 @@ -#requires -version 5.1 -#requires -RunAsAdministrator - -function DeleteUserProfilesInWindows11 { +function WinDeleteUserProfiles { <# .SYNOPSIS Rimuove in modo sicuro i profili utente locali non caricati e le cartelle residue in C:\Users. @@ -17,6 +14,12 @@ function DeleteUserProfilesInWindows11 { .PARAMETER MaxThreads Numero massimo di runspace paralleli. Per Win32_UserProfile viene limitato automaticamente a 4. + .PARAMETER CountdownSeconds + Secondi del countdown prima di un eventuale riavvio consigliato. + + .PARAMETER SuppressIndividualReboot + Sopprime il riavvio individuale e delega il riavvio finale al toolkit. + .PARAMETER UsersRoot Percorso radice dei profili utente locali. @@ -33,16 +36,21 @@ function DeleteUserProfilesInWindows11 { Non richiama Start-ToolkitSession anche se disponibile. .EXAMPLE - DeleteUserProfilesInWindows11 + WinDeleteUserProfiles .EXAMPLE - DeleteUserProfilesInWindows11 -MinimumProfileAgeDays 30 + WinDeleteUserProfiles -MinimumProfileAgeDays 30 #> [CmdletBinding()] param( [ValidateRange(1, 16)] [int]$MaxThreads = [Math]::Min(2, [Environment]::ProcessorCount), + [ValidateRange(0, 3600)] + [int]$CountdownSeconds = 30, + + [switch]$SuppressIndividualReboot, + [ValidateNotNullOrEmpty()] [string]$UsersRoot = 'C:\Users', @@ -58,7 +66,7 @@ function DeleteUserProfilesInWindows11 { ) begin { - $script:ToolName = 'DeleteUserProfilesInWindows11' + $script:ToolName = 'WinDeleteUserProfiles' $script:ToolVersion = '3.1' $script:SessionStart = Get-Date $script:UsersRoot = [System.IO.Path]::GetFullPath($UsersRoot.TrimEnd('\') + '\') @@ -66,6 +74,9 @@ function DeleteUserProfilesInWindows11 { $script:LogFile = Join-Path $script:LogFolder ("{0}_{1}.log" -f $script:ToolName, (Get-Date -Format 'yyyyMMdd_HHmmss')) $script:CurrentUser = $env:USERNAME $script:ComputerName = $env:COMPUTERNAME + $script:CountdownSeconds = $CountdownSeconds + $script:SuppressIndividualReboot = $SuppressIndividualReboot + $script:RebootRecommended = $false $script:MinimumLastUseDate = if ($MinimumProfileAgeDays -gt 0) { (Get-Date).AddDays(-$MinimumProfileAgeDays) } else { $null } $script:ProtectedProfileNames = @( @@ -93,28 +104,25 @@ function DeleteUserProfilesInWindows11 { } process { - function Write-ToolkitMessage { - param( - [ValidateSet('Info', 'Success', 'Warning', 'Error')] - [string]$Type = 'Info', - - [Parameter(Mandatory = $true)] - [string]$Text - ) - - if (Get-Command -Name Write-StyledMessage -ErrorAction SilentlyContinue) { - Write-StyledMessage -Type $Type -Text $Text - return - } + if (-not (Get-Command -Name Write-StyledMessage -ErrorAction SilentlyContinue)) { + function Write-StyledMessage { + param( + [ValidateSet('Info', 'Success', 'Warning', 'Error', 'Progress')] + [string]$Type = 'Info', + + [Parameter(Mandatory = $true)] + [string]$Text + ) + + $color = switch ($Type) { + 'Success' { 'Green' } + 'Warning' { 'Yellow' } + 'Error' { 'Red' } + default { 'Cyan' } + } - $color = switch ($Type) { - 'Success' { 'Green' } - 'Warning' { 'Yellow' } - 'Error' { 'Red' } - default { 'Cyan' } + Write-Host $Text -ForegroundColor $color } - - Write-Host $Text -ForegroundColor $color } @@ -133,6 +141,44 @@ function DeleteUserProfilesInWindows11 { } + function Set-ProfileCleanupRebootRecommended { + param( + [Parameter(Mandatory = $true)] + [string]$Reason + ) + + $script:RebootRecommended = $true + Add-ProfileCleanupLog -Level 'WARNING' -Text "Riavvio consigliato: $Reason" + } + + + function Invoke-ProfileCleanupReboot { + if (-not $script:RebootRecommended) { + return + } + + if (Get-Command -Name Invoke-ToolkitReboot -ErrorAction SilentlyContinue) { + Invoke-ToolkitReboot -Message 'Riavvio consigliato dopo pulizia profili' -Seconds $script:CountdownSeconds -SuppressIndividualReboot:$script:SuppressIndividualReboot + return + } + + if ($script:SuppressIndividualReboot) { + $Global:NeedsFinalReboot = $true + Write-StyledMessage -Type 'Info' -Text '🚫 Riavvio individuale soppresso. Verrà gestito un riavvio finale.' + return + } + + if (Get-Command -Name Start-InterruptibleCountdown -ErrorAction SilentlyContinue) { + if (Start-InterruptibleCountdown -Seconds $script:CountdownSeconds -Message 'Riavvio consigliato dopo pulizia profili') { + Restart-Computer -Force + } + return + } + + Write-StyledMessage -Type 'Warning' -Text 'Riavvio consigliato per completare la pulizia dei profili non rimossi.' + } + + function Test-IsAdministrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) @@ -153,7 +199,7 @@ function DeleteUserProfilesInWindows11 { $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue if ($os -and $os.Caption -notmatch 'Windows 11') { - Write-ToolkitMessage -Type 'Warning' -Text "⚠️ Sistema rilevato: $($os.Caption). Lo script è pensato per Windows 11." + Write-StyledMessage -Type 'Warning' -Text "⚠️ Sistema rilevato: $($os.Caption). Lo script è pensato per Windows 11." } if (-not $SuppressToolkitSession -and (Get-Command -Name Start-ToolkitSession -ErrorAction SilentlyContinue)) { @@ -167,14 +213,14 @@ function DeleteUserProfilesInWindows11 { Write-Host '' } - Write-ToolkitMessage -Type 'Info' -Text ("🖥️ Computer: {0}" -f $script:ComputerName) - Write-ToolkitMessage -Type 'Info' -Text ("👤 Utente corrente protetto: {0}" -f $script:CurrentUser) - Write-ToolkitMessage -Type 'Info' -Text ("📁 Percorso profili: {0}" -f $script:UsersRoot) - Write-ToolkitMessage -Type 'Info' -Text ("🧵 Thread configurati: {0}" -f $MaxThreads) - Write-ToolkitMessage -Type 'Warning' -Text '⚠️ Modalità non interattiva: nessuna conferma verrà richiesta prima delle cancellazioni.' + Write-StyledMessage -Type 'Info' -Text ("🖥️ Computer: {0}" -f $script:ComputerName) + Write-StyledMessage -Type 'Info' -Text ("👤 Utente corrente protetto: {0}" -f $script:CurrentUser) + Write-StyledMessage -Type 'Info' -Text ("📁 Percorso profili: {0}" -f $script:UsersRoot) + Write-StyledMessage -Type 'Info' -Text ("🧵 Thread configurati: {0}" -f $MaxThreads) + Write-StyledMessage -Type 'Warning' -Text '⚠️ Modalità non interattiva: nessuna conferma verrà richiesta prima delle cancellazioni.' if ($script:MinimumLastUseDate) { - Write-ToolkitMessage -Type 'Info' -Text ("📅 Soglia ultima attività: profili non usati da almeno {0} giorni." -f $MinimumProfileAgeDays) + Write-StyledMessage -Type 'Info' -Text ("📅 Soglia ultima attività: profili non usati da almeno {0} giorni." -f $MinimumProfileAgeDays) } Add-ProfileCleanupLog -Text "Sessione avviata su $script:ComputerName." @@ -212,7 +258,7 @@ function DeleteUserProfilesInWindows11 { function Get-RemovableUserProfiles { $excluded = New-ProtectedNameSet - Write-ToolkitMessage -Type 'Info' -Text '🔍 Scansione profili locali registrati in corso.' + Write-StyledMessage -Type 'Info' -Text '🔍 Scansione profili locali registrati in corso.' $profiles = Get-CimInstance -ClassName Win32_UserProfile | Where-Object { -not $_.Special -and @@ -249,7 +295,7 @@ function DeleteUserProfilesInWindows11 { ) Write-Host '' - Write-ToolkitMessage -Type 'Warning' -Text 'Profili registrati selezionati per la rimozione automatica:' + Write-StyledMessage -Type 'Warning' -Text 'Profili registrati selezionati per la rimozione automatica:' Write-Host '' $Profiles | @@ -412,7 +458,7 @@ function DeleteUserProfilesInWindows11 { $excluded = New-ProtectedNameSet $registeredProfilePaths = Get-RegisteredProfilePathSet - Write-ToolkitMessage -Type 'Info' -Text '🔎 Controllo cartelle residue nella directory utenti.' + Write-StyledMessage -Type 'Info' -Text '🔎 Controllo cartelle residue nella directory utenti.' $folders = Get-ChildItem -Path $UsersRoot -Directory -Force | Where-Object { @@ -538,14 +584,14 @@ function DeleteUserProfilesInWindows11 { Show-ProfileCleanupPreview -Profiles $targets Write-Host '' - Write-ToolkitMessage -Type 'Info' -Text ("🚀 Avvio rimozione automatica di {0} profili registrati." -f $targets.Count) + Write-StyledMessage -Type 'Info' -Text ("🚀 Avvio rimozione automatica di {0} profili registrati." -f $targets.Count) Write-Host '' $profileResults = @(Invoke-ProfileRemovalBatch -Profiles $targets) } else { Write-Host '' - Write-ToolkitMessage -Type 'Success' -Text '✅ Nessun profilo registrato rimovibile trovato.' + Write-StyledMessage -Type 'Success' -Text '✅ Nessun profilo registrato rimovibile trovato.' Add-ProfileCleanupLog -Level 'SUCCESS' -Text 'Nessun profilo registrato rimovibile trovato.' } @@ -554,22 +600,22 @@ function DeleteUserProfilesInWindows11 { if ($residualFolders -and $residualFolders.Count -gt 0) { Write-Host '' - Write-ToolkitMessage -Type 'Warning' -Text ("Cartelle residue selezionate per la rimozione automatica: {0}" -f $residualFolders.Count) + Write-StyledMessage -Type 'Warning' -Text ("Cartelle residue selezionate per la rimozione automatica: {0}" -f $residualFolders.Count) $residualFolders | Select-Object Name, Path | Format-Table -AutoSize Write-Host '' - Write-ToolkitMessage -Type 'Info' -Text '🧹 Avvio rimozione cartelle residue.' + Write-StyledMessage -Type 'Info' -Text '🧹 Avvio rimozione cartelle residue.' Write-Host '' $residualResults = @(Remove-ResidualUserFolders -Folders $residualFolders) } else { - Write-ToolkitMessage -Type 'Success' -Text '✅ Nessuna cartella residua rimovibile trovata in C:\Users.' + Write-StyledMessage -Type 'Success' -Text '✅ Nessuna cartella residua rimovibile trovata in C:\Users.' Add-ProfileCleanupLog -Level 'SUCCESS' -Text 'Nessuna cartella residua rimovibile trovata.' } } else { - Write-ToolkitMessage -Type 'Warning' -Text 'Pulizia cartelle residue saltata per parametro SkipResidualFolderCleanup.' + Write-StyledMessage -Type 'Warning' -Text 'Pulizia cartelle residue saltata per parametro SkipResidualFolderCleanup.' Add-ProfileCleanupLog -Level 'WARNING' -Text 'Pulizia cartelle residue saltata.' } @@ -579,6 +625,10 @@ function DeleteUserProfilesInWindows11 { $profileSuccessCount = @($profileResults | Where-Object { $_.Success }).Count $residualSuccessCount = @($residualResults | Where-Object { $_.Success }).Count + if ($failedCount -gt 0) { + Set-ProfileCleanupRebootRecommended -Reason "$failedCount elementi non rimossi potrebbero essere bloccati da sessioni o handle aperti." + } + $script:SessionEnd = Get-Date $totalDuration = New-TimeSpan -Start $script:SessionStart -End $script:SessionEnd @@ -590,18 +640,20 @@ function DeleteUserProfilesInWindows11 { Write-Host ' COMPLETATO' Write-Host '====================================================' -ForegroundColor Green Write-Host '' - Write-ToolkitMessage -Type 'Success' -Text ("✅ Profili registrati rimossi: {0}" -f $profileSuccessCount) - Write-ToolkitMessage -Type 'Success' -Text ("✅ Cartelle residue rimosse: {0}" -f $residualSuccessCount) + Write-StyledMessage -Type 'Success' -Text ("✅ Profili registrati rimossi: {0}" -f $profileSuccessCount) + Write-StyledMessage -Type 'Success' -Text ("✅ Cartelle residue rimosse: {0}" -f $residualSuccessCount) if ($failedCount -gt 0) { - Write-ToolkitMessage -Type 'Warning' -Text ("⚠️ Elementi non rimossi: {0}" -f $failedCount) + Write-StyledMessage -Type 'Warning' -Text ("⚠️ Elementi non rimossi: {0}" -f $failedCount) } - Write-ToolkitMessage -Type 'Info' -Text ("⏱️ Durata: {0:hh\:mm\:ss}" -f $totalDuration) - Write-ToolkitMessage -Type 'Info' -Text ("📄 Log: {0}" -f $script:LogFile) + Write-StyledMessage -Type 'Info' -Text ("⏱️ Durata: {0:hh\:mm\:ss}" -f $totalDuration) + Write-StyledMessage -Type 'Info' -Text ("📄 Log: {0}" -f $script:LogFile) + + Invoke-ProfileCleanupReboot } catch { Add-ProfileCleanupLog -Level 'ERROR' -Text $_.Exception.Message try { Save-ProfileCleanupLog } catch { } - Write-ToolkitMessage -Type 'Error' -Text ("❌ Errore: {0}" -f $_.Exception.Message) + Write-StyledMessage -Type 'Error' -Text ("❌ Errore: {0}" -f $_.Exception.Message) throw } finally { @@ -611,5 +663,3 @@ function DeleteUserProfilesInWindows11 { } } } - -DeleteUserProfilesInWindows11 @args From 7eea844c14099fce4fc105a0742b5179df7c79d0 Mon Sep 17 00:00:00 2001 From: pomodori92 Date: Sat, 20 Jun 2026 12:37:50 +0200 Subject: [PATCH 4/4] feature Aggiunta l'opzione di scelta per lo script di cancellazione profili e la conferma prima di avviarlo --- WinToolkit-template.ps1 | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/WinToolkit-template.ps1 b/WinToolkit-template.ps1 index 3381d76..875ac35 100644 --- a/WinToolkit-template.ps1 +++ b/WinToolkit-template.ps1 @@ -1912,6 +1912,7 @@ function WinDriverInstall {} function WinDebloat {} function WinCleaner {} function DisableBitlocker {} +function WinDeleteUserProfiles {} # Office function Install-Office {} @@ -1939,7 +1940,8 @@ $menuStructure = @( [pscustomobject]@{Name = 'WinReinstallStore'; Description = 'Winget/WinStore Reset'; Action = 'RunFunction' }, [pscustomobject]@{Name = 'WinBackupDriver'; Description = 'Backup Driver PC'; Action = 'RunFunction' }, [pscustomobject]@{Name = 'WinCleaner'; Description = 'Pulizia File Temporanei'; Action = 'RunFunction' }, - [pscustomobject]@{Name = 'DisableBitlocker'; Description = 'Disabilita Bitlocker'; Action = 'RunFunction' } + [pscustomobject]@{Name = 'DisableBitlocker'; Description = 'Disabilita Bitlocker'; Action = 'RunFunction' }, + [pscustomobject]@{Name = 'WinDeleteUserProfiles'; Description = 'Cancella profili utenti di Windows'; Action = 'RunFunction' } ) }, @{ 'Name' = 'Office'; 'Icon' = '🏢'; 'Scripts' = @( @@ -1985,7 +1987,30 @@ if (-not $ImportOnly -and -not $Global:GuiSessionActive) { Write-StyledMessage -Type 'Info' -Text '💎 WinToolkit avviato in modalità interattiva' Write-Host "" - while ($true) { + function Confirm-UserProfileDeletion { + Write-Host '' + Write-StyledMessage -Type 'Error' -Text 'ATTENZIONE: eseguendo questa opzione verranno cancellati tutti i profili utenti di Windows, escluso l''utente attuale.' + Write-StyledMessage -Type 'Error' -Text 'I dati contenuti nei profili eliminati saranno irrecuperabili.' + Write-Host '' + Write-Host '💎 [1] Sì, cancella i profili utenti' -ForegroundColor White + Write-Host '[INVIO] Torna al menu principale' -ForegroundColor Gray + $firstConfirm = Microsoft.PowerShell.Utility\Read-Host 'Selezione' + + if ($firstConfirm -ne '1') { + return $false + } + + Write-Host '' + Write-StyledMessage -Type 'Error' -Text 'Sei proprio sicuro?' + Write-Host '' + Write-Host '💎 [1] Sì, sono sicuro di ciò che sto facendo e me ne assumo la responsabilità in caso di cancellazione di file' -ForegroundColor White + Write-Host '[INVIO] Torna al menu principale' -ForegroundColor Gray + $secondConfirm = Microsoft.PowerShell.Utility\Read-Host 'Selezione' + + return ($secondConfirm -eq '1') + } + + :MainMenu while ($true) { Show-Header -SubTitle "Menu Principale" # ── Informazioni di sistema ─────────────────────────────────────────── @@ -2076,6 +2101,12 @@ if (-not $ImportOnly -and -not $Global:GuiSessionActive) { foreach ($sel in $selections) { $scriptToRun = $allScripts[$sel - 1] + if ($scriptToRun.Name -eq 'WinDeleteUserProfiles' -and -not (Confirm-UserProfileDeletion)) { + Write-StyledMessage -Type 'Warning' -Text 'Operazione annullata. Ritorno al menu principale.' + Start-Sleep -Seconds 2 + continue MainMenu + } + Write-StyledMessage -Type 'Progress' -Text "▶️ Avvio: $($scriptToRun.Description)" Write-Host '' try {