#requires -version 3 <# .Synopsis Deletes selected user profiles from a local or remote computer .DESCRIPTION Based on Delete-InactiveProfilesGUI.ps1, written to replace DelProf, this interactive script deletes user profiles from a local or remote computer. This gets folder size using Robocopy, which is very fast .NOTES Alan Kaplan www.akaplan.com 12/24/21 #> [CmdletBinding()] param ( [Parameter(mandatory = $true)] [string] $ComputerName ) Add-Type -assemblyname Microsoft.visualBasic Function Minimize-Self { #http://stackoverflow.com/questions/16552801/how-do-i-conditionally-add-a-class-with-add-type-typedefinition-if-it-isnt-add if (-not ([System.Management.Automation.PSTypeName]'ShoWinAsync').Type) { #Based on Add-Type Help example $Sig = '[DllImport("user32.dll")]public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);' $shoWinAsync = Add-Type -MemberDefinition $Sig -Name "Win32ShowWindowAsync" -Namespace Win32Functions -PassThru } $shoWinAsync::ShowWindowAsync((Get-Process -Id $pid).MainWindowHandle, 2) | out-null } Function Restore-Self { #http://stackoverflow.com/questions/16552801/how-do-i-conditionally-add-a-class-with-add-type-typedefinition-if-it-isnt-add if (-not ([System.Management.Automation.PSTypeName]'ShoWinAsync').Type) { #Based on Add-Type Help example $Sig = '[DllImport("user32.dll")]public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);' $shoWinAsync = Add-Type -MemberDefinition $Sig -Name "Win32ShowWindowAsync" -Namespace Win32Functions -PassThru } $shoWinAsync::ShowWindowAsync((Get-Process -Id $pid).MainWindowHandle, 1) | out-null } Function Get-OldestDate($d1, $d2) { $startdate = (Get-Date $d1) $enddate = (Get-Date $d2) if ($startdate -gt $enddate) { $enddate } Else { $Startdate } } Function Get-ComputerFQDN { $owmi = get-wmiobject -class Win32_computerSystem [string]"$($owmi.name).$($owmi.domain)" } Function Convert-SIDtoNTName($sidVal) { $objSID = New-Object System.Security.Principal.SecurityIdentifier ($sidVAl) Try { $Identity = $objSID.Translate([System.Security.Principal.NTAccount]) $Identity.Value } catch { $LDAPpath = "LDAP://" if ([adsi]::Exists($LDAPpath)) { $IDRef = ([adsi]$LDAPpath) $SamName = [string]$IDRef.SamAccountName $DN = [string]$IDRef.distinguishedName $Dom = $DN.Substring($DN.IndexOf(",DC")).Replace(",DC=", ".").Substring(1) "$Dom\$SamName" } Else { Write-warning "$sidVAl not found in AD" $sidVal } } } function IsAdministrator { $Identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() $Principal = New-Object System.Security.Principal.WindowsPrincipal($Identity) [bool]$Principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } Function ConvertFrom-CIMDateTime { [OutputType([datetime])] Param ( # strDMTF help description [Parameter( Mandatory = $true, Position = 0)] $strDMTF ) [management.managementdatetimeconverter]::todatetime($strDMTF) } <# Based on code here: http://www.powershelladmin.com/wiki/Get_Folder_Size_with_PowerShell,_Blazingly_Fast I left in original data return for later use #> Function Get-FolderSizeMB ($FolderPath) { $Precision = 2 $RoboThreadCount = 16 Write-Progress "Getting size of '$FolderPath' with Robocopy beginning $(([datetime]::Now).ToShortTimeString())." $RoboCopyArgs = "/L /S /NJH /BYTES /FP /NC /NDL /TS /XJ /R:0 /W:0 /MT:$RoboThreadCount".split(' ') [datetime] $StartedTime = [datetime]::Now [string] $Summary = robocopy $FolderPath NULL $RoboCopyArgs | Select-Object -Last 8 [datetime] $EndedTime = [datetime]::Now [regex] $HeaderRegex = '\s+Total\s*Copied\s+Skipped\s+Mismatch\s+FAILED\s+Extras' [regex] $DirLineRegex = 'Dirs\s*:\s*(?\d+)(?:\s+\d+){3}\s+(?\d+)\s+\d+' [regex] $FileLineRegex = 'Files\s*:\s*(?\d+)(?:\s+\d+){3}\s+(?\d+)\s+\d+' [regex] $BytesLineRegex = 'Bytes\s*:\s*(?\d+)(?:\s+\d+){3}\s+(?\d+)\s+\d+' [regex] $TimeLineRegex = 'Times\s*:\s*(?\d+).*' [regex] $EndedLineRegex = 'Ended\s*:\s*(?.+)' if (($lastexitcode -eq 1) -and ($Summary -match "$HeaderRegex\s+$DirLineRegex\s+$FileLineRegex\s+$BytesLineRegex\s+$TimeLineRegex\s+$EndedLineRegex") ) { $byteCount = [decimal] $Matches['ByteCount'] [PSCustomObject]@{ Path = $p TotalBytes = $byteCount TotalMBytes = [math]::Round($byteCount / 1MB, $Precision) TotalGBytes = [math]::Round($byteCount, $Precision) BytesFailed = [decimal] $Matches['BytesFailed'] DirCount = [decimal] $Matches['DirCount'] FileCount = [decimal] $Matches['FileCount'] DirFailed = [decimal] $Matches['DirFailed'] FileFailed = [decimal] $Matches['FileFailed'] TimeElapsed = [math]::Round([decimal] ($EndedTime - $StartedTime).TotalSeconds, $Precision) StartedTime = $StartedTime EndedTime = $EndedTime } | Select-Object -ExpandProperty TotalMBytes } else { $msg = switch ($lastexitcode) { 16 { 'access denied' } Default { ([ComponentModel.Win32Exception]$lastexitcode).Message } } "Robocopy attempt to get folder size failed with exitcode $lastExitCode`: $msg" } Write-Progress 'Done' -Completed } ### Script Begins Minimize-Self $ProfileList = $InactiveList = $deleteList = $null if ($ComputerName -match $env:COMPUTERNAME) { $bLocal = $true }Else { $bLocal = $false } if (((IsAdministrator) -eq $false) -and ($blocal)) { Restore-Self $title = "Requires Admin Permissions" $message = "This script requires administrative rights. $Env:username is not an admin of $computername, or you must re-run as elevated. Continue?" $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Continue Running." $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Exit Script." $cancel = New-Object System.Management.Automation.Host.ChoiceDescription "&Cancel", "Exit Script." $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no, $cancel) $result = $host.ui.PromptForChoice($title, $message, $options, 0) switch ($result) { 0 { break } default { Exit } } } ##Log file. Use GetFolderPath to reliably locate regular and OneDrive desktop $desktop = [environment]::GetFolderPath('Desktop') $Logfile = "$desktop\$computername profiles.csv" $Logfile = [Microsoft.VisualBasic.Interaction]::InputBox("Path to CSV log file:", "Log File", $logfile) if ($Logfile -eq '') { Exit } Restore-Self Write-Progress "Getting user profiles from WMI on $ComputerName" #Use this to exclude local administrator and builtin service accounts #$filter = "not SID like '%-500' and Special = False" #Exclude only builtin service accounts $filter = "Special = False" $Params = @{ Class = 'Win32_UserProfile' filter = $filter ComputerName = $ComputerName ErrorAction = 'Stop' } Try { $ProfileList = Get-WmiObject @params if ($blocal) { $bGetSize = $true } Else { $msg = "$computername has $(($ProfileList).count) user profiles. It can take a while to get the profile size on remote computers. Do you want to get profile size?" $retval = [Microsoft.VisualBasic.Interaction]::MsgBox($msg, 'YesNoCancel,Question', "Get Profile Size?") switch ($retval) { 'Yes' { $bGetSize = $true } 'No' { $bGetSize = $false } Default { Exit } } } $InactiveList = $ProfileList | ForEach-Object { $LastUsed = $localPath = $lastUsedDays = $Remarks = '' $ProfileSize = $LastUsedDays = $LastUsed = 'n/a' $bPathFound = $false $SID = $_.SID $user = Convert-SIDtoNTName $SID Write-Progress "Getting information about profile for `"$user`" with SID $sid" if ($_.localpath) { $LocalPath = $_.localpath if ($blocal) { $ProfilePath = $localpath } ELSE { $Profilepath = "\\" + $computerName + "\" + $localPath.Replace(":", "$") } $bPathFound = (test-Path $ProfilePath) } ELSE { $bPathFound = $false } #Get last used from WMI if available if ($_.LastUseTime) { $LastUsedWMI = ConvertFrom-cimDateTime $_.LastUseTime } Else { $LastUsedWMI = [datetime]'1/1/1600' } #Exists, get last written for user profile if ($bPathFound) { #your milage may vary based on file permissions $LastUsedFile = (Get-Item -force "$profilePath").LastWriteTime #lastAccessTime } Else { $LastUsedFile = [datetime]'1/1/1600' } $LastUsed = Get-OldestDate $LastUsedFile $LastUsedWMI if ($LastUsed -ne '1/1/1600') { $LastUsedDays = ((get-date) - ($LastUsed)).days $LastUsedDays = [math]::abs($LastUsedDays) $Remarks = 'Last Used from folder last write' } ELSE { $LastUsed = 'n/a' } if (($bPathFound) -and ($bGetSize)) { Write-Progress "Getting size of $localPath on $ComputerName" $profileSize = Get-folderSizeMB $profilePath } if (-not $bGetSize) { $ProfileSize = "Not collected" } [PSCustomObject]@{ Computer = $computername SID = $SID User = $User LastUsed = $LastUsed ElapsedDays = $LastUsedDays ProfilePath = $LocalPath ProfileSizeMB = $ProfileSize Object = $_ Remarks = $remarks } } } Catch { Write-Warning $Error[0].Exception.Message } Write-Progress "Profile information collected." -Completed $deleteList = $InactiveList | Out-GridView -Title 'Select Profile(s) to delete' -PassThru #make foreach an array to support piped export to CSV @(ForEach ($profile in $deleteList) { #WMI to delete the profile $msg = "Confirm deletion of profile for `"$(($profile).user)`" with SID $(($Profile).sid)" $retval = [Microsoft.VisualBasic.Interaction]::MsgBox($msg, 'YesNo,defaultbutton1,Question', "Confirm") switch ($retval) { 'Yes' { Try { $Profile.object | remove-wmiobject -ErrorAction Stop -ErrorVariable $eMsg $result = 'Success' } Catch { $result = $eMsg } } Default { $result = 'Skipped' } } [PSCustomObject]@{ Computer = $profile.computer SID = $profile.SID User = $profile.User LastUsed = $profile.LastUsed ElapsedDays = $profile.elapsedDays ProfilePath = $profile.ProfilePath ProfileSizeMB = $profile.ProfileSizeMB Remarks = $profile.Remarks Result = $result } }) | export-csv $logfile -NoTypeInformation Write-Host "Done. Log is $logfile" Pause