Check Profile Sizes

Check Profile Sizes examines user profiles for all or selected user accounts on the target machine, grouping the results by file type, using the extension.

For each group of files, if the size of the group exceeds a threshold (default 15% of the total profile size) the individual files are listed, sorted by path or by size (descending) and showing the actual file size in bytes.

To keep the output reasonably short, a threshold is set on the number of files shown individually per are listed by path (default 6) - beyond this, files are summarized by folder (order by count of files, descending).

Arguments:
ThresholdPercentToExpand (default: 15) - the threshold percent of the total profile size at which a file-extension group is listed.
SamAccountNameList (default: All) - the list of account names to be reported (comma-separated, any leading or trailing spaces will be trimmed). If set to All, the script will include local user and Active Directory user accounts.
SortBy (default: Size) - must be set to Size (individual files are listed by size, descending) or Path (individual files are listed by full path, ascending).
PreSummarySize (default: 6) - the number of files that will be listed individually (by group, according to the configured sort order) before the script switches to reporting files grouped by folder.
Version 1.2.17
Created on 2023-03-08
Modified on 2023-03-23
Created by Bill Powell
Downloads: 224

The Script Copy Script Copied to clipboard
#Requires -version 3

<#
  .SYNOPSIS
  Check User Profile sizes on a machine.

  .DESCRIPTION
  This function Check Profile Sizes examines user profiles for all or selected users
  on the target machine, grouping the results by file type, using the extension. 
  For each group of files, if the size of the group exceeds a threshold 
  (default 15% of the total profile size) the individual files are listed
  Options provide for sorting and summarization preferences.

  .PARAMETER ThresholdPercentToExpand
  Specifies the threshold percentage for any group of files.

  .PARAMETER SamAccountNameList
  Specifies the profiles to be checked, using the sAMAccountName. 
  Specifying 'All' will check all profiles found on the machine, including those
  for local accounts.

  .PARAMETER SortBy
  When set to 'Size', items in a file-extension group will be ordered in descending order of size.
  When set to 'Path', items in a file-extension group will be ordered in ascending order by path (FullName).

  .PARAMETER PreSummarySize
  An integer, specifying the maximum number of items in a file-extension group that will be displayed in full.
  Any remaining items will be displayed in summary form.

  .NOTES
  The script uses the WMI class Win32_UserProfile to report the profiles on
  the machine, so will detect profiles regardless of where they are stored.
  The script will enumerate every file in the profile, which may cause files
  to be fetched from network locations for some profile types (e.g. Citrix
  Profiles - "UPM") and generate peaks in network traffic.
  The script outputs exact file sizes, in bytes.
  The script will differentiate between active AD accounts (using ADSI)
  and local accounts by querying WinNT accounts. Any accounts not detected by
  these techniques may correspond to deleted accounts in AD and will be identified
  as such in the output.

   
  Modification History:
  2023-03-10   Bill Powell       Initial public release

  .LINK
  For more information refer to:
    https://www.controlup.com

  .LINK
  Stay in touch:
  https://twitter.com/asoftman

  .EXAMPLE
  PS> .\Check-ProfileSize.ps1

  .EXAMPLE
  PS> .\Check-ProfileSize.ps1 -ThresholdPercentToExpand 20 -SamAccountNameList "tom,dick,harry"
#>

[CmdletBinding()]
param (
    [Parameter(Mandatory=$false,
               HelpMessage = "Threshold of total size above which files are analyzed")]
               [int]$ThresholdPercentToExpand = 15,
    [Parameter(Mandatory=$false,
               HelpMessage = "List of account names to analyze")]
               [string]$SamAccountNameList,
    [Parameter(Mandatory=$false,
               HelpMessage = "sort order to present results")]
               [ValidateSet('Path','Size')]
               [string]$SortBy = 'Size',
    [Parameter(Mandatory=$false,
               HelpMessage = "where there are many files, the first ones are itemised, the remainder shown by folder")]
               [int]$PreSummarySize = 6
)

$ErrorActionPreference = 'Stop'
$VerbosePreference = 'SilentlyContinue'
$DebugPreference = 'SilentlyContinue'

function Get-FilesizeString {
    [CmdletBinding()]
    param (
        [parameter(Mandatory=$true)][long]$Filesize
    )
    $Units = $null
    if ($Filesize -ge 1pb) {
        $Units = "PB"
        $DecimalSize = [decimal]($Filesize / 1PB)
    }
    elseif ($Filesize -ge 1tb) {
        $Units = "TB"
        $DecimalSize = [decimal]($Filesize / 1TB)
    }
    elseif ($Filesize -ge 1gb) {
        $Units = "GB"
        $DecimalSize = [decimal]($Filesize / 1GB)
    }
    elseif ($Filesize -ge 1mb) {
        $Units = "MB"
        $DecimalSize = [decimal]($Filesize / 1MB)
    }
    elseif ($Filesize -ge 1kb) {
        $Units = "KB"
        $DecimalSize = [decimal]($Filesize / 1KB)
    }
    if ($Units -ne $null) {
        if ($DecimalSize -ge 100) {
            [string]$result = "{0} $Units" -f [math]::Round($DecimalSize)
        }
        elseif ($DecimalSize -ge 10) {
            [string]$result = "{0:n1} $Units" -f [math]::Round($DecimalSize,1)
        }
        else {
            [string]$result = "{0:n2} $Units" -f [math]::Round($DecimalSize,2)
        }
    }
    else {
        [string]$result = $Filesize.ToString('d')
    }
    $result.PadLeft(7)
}

<#
#
# test data
$TestNumbers = @(0,1,10,100,1000,1024,1025,123456,12345678,123456789,1234567890,12345678901)

$TestNumbers | foreach {
    $Test = $_
    $message = "Input {0} -> '{1}'" -f $Test, (Get-FilesizeString $Test)
    Write-Host $message
}
#>

[int]$outputWidth = 400
try
{
    if( ( $PSWindow = (Get-Host).UI.RawUI ) -and ( $WideDimensions = $PSWindow.BufferSize ) )
    {
        Write-Verbose -Message "Setting output width to $outputWidth"
        $WideDimensions.Width = $outputWidth
        $PSWindow.BufferSize = $WideDimensions
        Write-Verbose -Message "Set output width to $($WideDimensions.width)"
    }
}
catch
{
    ## not much we can do but will hide the error since it is not fundamental to script functionality, just output
    Write-Warning -Message "Failed to set output width to $($WideDimensions.width) : $_"
}

Write-Output "**********************************************************************************************************"
Write-Output "System:             $($env:COMPUTERNAME)"
Write-Output "Date/Time:          $((Get-Date).ToString("yyyy-MM-dd HH:mm:ss"))"
Write-Output "ThresholdPercent:   $ThresholdPercentToExpand"
Write-Output "SamAccountNameList: $SamAccountNameList"
Write-Output "SortBy:             $SortBy"
Write-Output "PreSummarySize:     $PreSummarySize"
Write-Output ""

if ($SamAccountNameList -eq "All") {
    $SamAccountNameList = $null
}
$SamAccountNameArray = @()

if (-not [string]::IsNullOrWhiteSpace($SamAccountNameList)) {
    $SamAccountNameList -split ',' -replace "^\s+",'' -replace "\s+$",'' | foreach {
        $SamAccountNameArray += $_
    }
}

$ProfileFieldList = "LocalPath,SID,SamAccountName,userPrincipalName,accountExpires" -split ','
$AllUserProfiles = @()
Get-CimInstance win32_userprofile | 
  where {$_.SID -like 'S-1-5-21-*'} | 
  Select-Object $ProfileFieldList | 
  foreach {
    $Profile = $_
    $strSID=$Profile.SID
    # see https://serverfault.com/questions/120411/retrieve-user-details-from-active-directory-using-sid
    try {
        $uSid = [ADSI]"LDAP://<SID=$strSID>"
        $Profile.SamAccountName = [string]($uSid.sAMAccountName)
        $Profile.userPrincipalName = [string]($uSid.userPrincipalName)
        $Profile.accountExpires = [string]($uSid.accountExpires)
    }
    catch {
        $e = $_
    }
    if ([string]::IsNullOrWhiteSpace($Profile.SamAccountName)) {
        $Name = Split-Path $Profile.LocalPath -Leaf
        try {
            $LocalUser = [adsi]"WinNT://./$Name,user"
            if (-not [string]::IsNullOrWhiteSpace($LocalUser.Name)) {
                $Profile.SamAccountName = $Name + " (Local account - SID $($Profile.SID) not found in AD)"
            }
            else {
                $Profile.SamAccountName = $Name + " (Deleted/anomalous account - SID $($Profile.SID) not found in AD or local accounts)"
            }
        }
        catch {
            $e = $_
            $Profile.SamAccountName = $Name + " (Deleted/anomalous account - SID $($Profile.SID) not found in AD or local accounts)"
        }
    }
    if ($SamAccountNameArray.Count -eq 0) {
        # empty list -> all profiles checked
        $AllUserProfiles += $Profile
    }
    elseif ($Profile.SamAccountName -in $SamAccountNameArray) {
        # non-empty list -> only check profiles passed as parameter
        $AllUserProfiles += $Profile
    }
}

if ($AllUserProfiles.Count -eq 0) {
    throw "No user profiles found matching $SamAccountNameList"
    exit 0
}

$script:ProfileFolderLookup = @{}
$script:AppDataPrefix = $null
#
# we want to classify files by Hidden/AppData/Normal
function Get-FileClass {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]$FileObject
    )
    if ($FileObject.Mode -match "[hs]" ) {
        return 'hidden'
    }
    elseif (([string]$FileObject.FullName).StartsWith($script:AppDataPrefix)) {
        return 'appdata'
    }
    return 'standard'
}

filter Partition-LargeFilesets ([int]$PreSummarySize) {
    begin {
        $IndividualItems = @()
        $SummaryItems = @()
        $SummaryItemSize = 0
        $ItemCount = 0
        $FolderList = @{}
    }
    process {
        if ($null -eq $PreSummarySize) {
            $IndividualItems += $_
        }
        elseif ($IndividualItems.Count -lt $PreSummarySize) {
            $IndividualItems += $_
        }
        else {
            $SummaryItems += $_
            $SummaryItemSize += $_.Length
            $StatsForDirectory = $FolderList[$_.DirectoryName]
            if ($null -eq $StatsForDirectory) {
                $StatsForDirectory = New-Object PSObject | Select-Object -Property FileCount,FileSize,Folder
                $StatsForDirectory.FileCount = 0
                $StatsForDirectory.FileSize = 0
                $StatsForDirectory.Folder = $_.DirectoryName
            }
            $StatsForDirectory.FileCount += 1
            $StatsForDirectory.FileSize += $_.Length
            $FolderList[$_.DirectoryName] = $StatsForDirectory
        }
    }
    end {
        New-Object PSObject |
            Add-Member NoteProperty IndividualItems     $IndividualItems       -PassThru |
            Add-Member NoteProperty SummaryItems        $SummaryItems          -PassThru |
            Add-Member NoteProperty SummaryItemSize     $SummaryItemSize       -PassThru |
            Add-Member NoteProperty SummaryItemCount    $SummaryItems.Count    -PassThru |
            Add-Member NoteProperty SummaryItemFolders  $FolderList            -PassThru
    }
}

switch ($SortBy) {
    "Path" {
            #
            # sort by path, ascending
            $SortExpression = @{Expression={($_.FullName)};Descending=$false}
        }
    "Size" {
            #
            # sort by file size, descending
            $SortExpression = @{Expression={($_.Length)};Descending=$true}
        }
    default {
            #
            # sort by file size, descending
            $SortExpression = @{Expression={($_.FullName)};Descending=$false}
        }
}

$AllUserProfiles | foreach {
    $FilesByExtensionHash = @{}
    $TotalFileSize = 0
    $TotalFileCount = 0
    $SystemHiddenFileSize = 0
    $SystemHiddenFileCount = 0
    $AppDataFileSize = 0
    $AppDataFileCount = 0
    $StandardFileSize = 0
    $StandardFileCount = 0

    $Profile = $_
    #
    # we need to classify the folders - at least to identify AppData 
    $script:ProfileFolderLookup = @{}
    (Get-Item $profile.LocalPath).GetDirectories() | foreach {
        $TopLevelFolder = $_
        $ProfileFolderLookup[$TopLevelFolder.Name] = $TopLevelFolder
        if ($TopLevelFolder.Mode -match "l") {
            #
            # link - can ignore
        }
        elseif ($TopLevelFolder.Mode -match "h") {
            #
            # hidden, but not link - this will be AppData
            $script:AppDataPrefix = $TopLevelFolder.FullName
        }
    }
    #
    # now we classify each of the files
    Get-ChildItem $Profile.LocalPath -Recurse -File -Force -ErrorAction SilentlyContinue | foreach {
        $FileObj = $_
        $FileClass = Get-FileClass $FileObj
        $TotalFileSize += $FileObj.Length
        $TotalFileCount++
        switch ($FileClass) {
            'hidden' {
                    $SystemHiddenFileSize += $FileObj.Length
                    $SystemHiddenFileCount++
                }
            'appdata' {
                    $AppDataFileSize += $FileObj.Length
                    $AppDataFileCount++
                }
            'standard' {
                    $StandardFileSize += $FileObj.Length
                    $StandardFileCount++
                    #
                    # now we record specifics by extension
                    $previous = $FilesByExtensionHash[$FileObj.Extension]
                    if ($previous -eq $null) {
                        $previous = New-Object psobject | select-object Extension,TotalSize,Items
                        $previous.Extension = $FileObj.Extension
                        $previous.TotalSize = $FileObj.Length
                        $previous.Items = @($FileObj)
                    }
                    else {
                        $previous.TotalSize += $FileObj.Length
                        $previous.Items += $FileObj
                    }
                    $FilesByExtensionHash[$FileObj.Extension] = $previous
                }
        }
    }

    Write-Output "**********************************************************************************************************"
    Write-Output "User: $($Profile.SamAccountName)"
                ("    Total files:         {0,12}     Total File size:         {1,15} (bytes) / {2} , comprising:" -f $TotalFileCount,$TotalFileSize,(Get-FilesizeString $TotalFileSize)) | Out-Default
                ("    Standard files:      {0,12}     Standard File size:      {1,15} (bytes) / {2}"              -f $StandardFileCount,$StandardFileSize,(Get-FilesizeString $StandardFileSize)) | Out-Default
                ("    AppData files:       {0,12}     AppData File size:       {1,15} (bytes) / {2}"              -f $AppDataFileCount,$AppDataFileSize,(Get-FilesizeString $AppDataFileSize)) | Out-Default
                ("    Hidden/System files: {0,12}     Hidden/System File size: {1,15} (bytes) / {2}"              -f $SystemHiddenFileCount,$SystemHiddenFileSize,(Get-FilesizeString $SystemHiddenFileSize)) | Out-Default
    Write-Output "**********************************************************************************************************"

    $ThresholdLength = $StandardFileSize * $ThresholdPercentToExpand / 100

    $ExtensionStats = $FilesByExtensionHash.GetEnumerator() | % {$_.Value} | Sort-Object -Property TotalSize -Descending

    $ExtensionStats | Format-Table -Property Extension,@{Label = "Total (Bytes)";Expression={$_.TotalSize}},@{Label = "Total";Expression={(Get-FilesizeString $_.TotalSize)}}

    $ExtensionStats | where {$_.TotalSize -gt $ThresholdLength} | foreach {
        Write-Output "==============================================="
        Write-Output "Expanding extension $($_.Extension)"
        $_.Items | Sort-Object -Property $SortExpression |
          #  Select-Object -Property Mode,FullName,Length | 
            Partition-LargeFilesets -PreSummarySize $PreSummarySize | 
            ForEach-Object {
                $_.IndividualItems |
                    Format-Table -Property @{Label = "Attributes";Expression={$_.Mode}},FullName,@{Label = "FileSize (bytes)";Expression={$_.Length}},@{Label = "FileSize";Expression={(Get-FilesizeString $_.Length)}}
                if ($_.SummaryItems.Count -gt 0) {
                    Write-Output "plus $($_.SummaryItemCount) items totalling $($_.SummaryItemSize) bytes ($(Get-FilesizeString $_.SummaryItemSize)) in the folders below:"
                    $_.SummaryItemFolders.GetEnumerator() | ForEach-Object {$_.Value} | Sort-Object -Property FileSize -Descending | Format-Table -Property FileCount,@{Label = "FileSize (bytes)";Expression={$_.FileSize}},@{Label = "FileSize";Expression={(Get-FilesizeString $_.FileSize)}},Folder
                }
            }
    }
}

Write-Output "Complete"