Working with user profiles

  • The context

I’ve got a pool of Windows 7 virtual machines dedicated to people who are out of office.
These VMs aren’t configured with differentiacing because we want them to be maintained by System Center Configuration Manager and have them applying security updates for the operating system as well as applications. In other words, they aren’t based on a VDI template, don’t get reset and patched through an offline imaging process. So, the major problem with such a scenario is that after a certain amount of time and many users login on, the VMs’ system drives are getting full. The solution is easy and consists in launching a Powershell script on a regular basis through the tasks scheduler that does the following:

    • Check the freespace left on the systemdrive
    • based on a threshold specified as parameter (whether there’s less than 20% of freespace left on the drive for example) take remediation actions
    • Get additional information on user profiles size
    • Delete profiles older that x days specified as parameter
    • Get the freespace left on the systemdrive after user profiles removal
    • Send a nice HTML based report to the admin
  • Step 1: Getting the list of user profiles

There are actually two ways to get the list of user profiles.
We can either query the registry under ‘HKLM\software\microsoft\windows nt\currentversion\profilelist’ or use the win32_userprofile WMI class
Ed Wilson, the Hey scripting guy shows these 2 techniques in the following article entitled “Use PowerShell to Find User Profiles on a Computer”
Well, there’s also a more straight forward technique that consists in querying the last write time of the ntuser.dat (the user hive) file in each folder (representing a username) located under C:\users (the default location of user profiles).
Paradoxically though it may seem but I prefer the WMI approach as it’s the easiest. WMI instances have already all the interesting properties we are looking for. With WMI, you don’t need to convert paths or usernames to SIDs (security identifier), have a exclusion list of SIDs or folder names of special accounts (‘Default User’,’All Users’,’Default’,”Public”,…), you don’t need to understand ‘State’ value in the registry (by the way, will there be an official documentation provided one day ?). All we may have to do is to convert dates.

Function Get-UserProfile             
{            
    Param(            
    [CmdletBinding()]            
        [parameter(ValueFromPipeline = $True,ValueFromPipeLineByPropertyName = $True)]            
        [Alias('CN','__Server','IPAddress','Server')]            
        [string[]]$Computername = $Env:Computername,            
                    
        [parameter()]            
        [Alias('RunAs')]            
        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty                   
    )            
    Begin            
    {            
        # Make sure we run as admin            
        $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()            
        $IsAdmin = $usercontext.IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")                               
        if (-not($IsAdmin))            
        {            
            Write-Warning "Must run powerShell as Administrator to perform these actions"            
            return            
        }             
        # Prepare HT            
        $wmiHT = @{            
            ErrorAction = "Stop"            
            Query = "Select * FROM Win32_UserProfile WHERE Loaded = $false AND Special = $false"            
        }            
        #Supplied Alternate Credentials?                        
        If ($PSBoundParameters['Credential'])            
        {                        
            $wmiHT.credential = $Credential                        
        }            
    }             
    Process             
    {            
        $ComputerName | ForEach-Object -Process {            
            $UserProfiles = $null            
            $Computer = $_            
            If ($Computer -eq $Env:Computername)            
            {            
                $wmiHT.remove('Credential')            
            } Else {            
                $wmiHT += @{Computername = $Computer}            
            }            
            try {            
                $UserProfiles = Get-WmiObject @wmiHT            
            } catch {            
                Write-Warning -Message "Failed to query Win32_UserProfile on computer $Computer"            
            }            
            if ($UserProfiles)            
            {            
                $UserProfiles | ForEach-Object -Process {            
                    $LastUseDate = $Status = $Type = $UserName = $LastDownloadTime = $LastUploadTime = $null            
                    Switch($_.Status)            
                    {            
                        # 0 = 'Temporary' according to :            
                        # http://msdn.microsoft.com/en-us/library/windows/desktop/ee886409%28v=vs.85%29.aspx            
                        # Comment from Thomas Lee also says:            
                        # The actual values for the uint field are (0, 1, 2, 4, 8) where a value of zero denotes unset or default            
                        # and 1 = Temporary, 2 = Roaming, 4 = Mandatory and 8 = Corrupted.            
                        0       { $Status = 'Local'}            
                        1       { $Status = 'Roaming'}            
                        2       { $Status = 'Mandatory'}            
                        3       { $Status = 'Corrupted'}            
                        6       { $Status = 'Roaming'}            
                        8       { $Status = 'Temporary and loaded'}            
                        10      { $Status = 'Temporary'}            
                        default { $Status = $_}            
                    }            
                    if ($_.LastUseTime)            
                    {            
                        $LastUseDate = $_.ConvertToDateTime($_.LastUseTime)            
                    }            
                    if ($_.LastDownloadTime)            
                    {            
                       $LastDownloadTime = $_.ConvertToDateTime($_.LastDownloadTime)            
                    }            
                    if ($_.LastUploadTime)            
                    {            
                        $LastUploadTime = $_.ConvertToDateTime($_.LastUploadTime)            
                    }            
                    if ($_.RoamingConfigured)            
                    {            
                        $Status = "Roaming"            
                    } else {            
            
                        $Status = $Status            
                    }            
                    if ($_.RoamingPreference)            
                    {            
                        $Type = "Roaming"            
                    } else {            
                        $Type = "Local"            
                    }                                   
                                
                    if ($_.SID)            
                    {            
                        $UserName = ConvertTo-NtAccount -Sid $($_.SID)            
                    }            
                    New-Object -TypeName PSObject -Property @{            
                        UserName = $UserName            
                        SID = $_.SID            
                        Type = $Type            
                        Status = $Status            
                        Path = $_.LocalPath            
                        LastUsedDate = $LastUseDate            
                        LastUploadTime = $LastUploadTime            
                        LastDownloadTime = $LastDownloadTime            
                        BinaryState = $_.Status            
                        CentralProfile = $_.RoamingPath            
                        WMIObject = $_            
                    }            
                }            
            }            
        }            
    }            
                
    End {}            
            
}            
  • Step 2: Delete/Remove user profiles

As you can see in the above Get-UserProfile function, I’ve chosen to return the full WMI instance as property of the final object. We actually need it to delete the user profile as I’ll use the Delete hidden method on the WMI instance that is fully supported on windows Vista and onwards.
Deleting a user profile consists in the following actions:

    • Remove the key representing the SID of the user under HKLM\software\microsoft\windows nt\currentversion\profilelist
    • Remove the GUID of the user profile under the key HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileGuid’
    • Remove the folder of the user under C:\users

Note that whenever you’ve incoherencies between folders under C:\users and the information under HKLM\software\microsoft\windows nt\currentversion\profilelist, you may have users being logged on a temporary profile and have their registry key backed up…

Function Delete-UserProfile             
{            
    [CmdletBinding()]            
    param            
    (            
    [parameter(ValueFromPipeline=$true, Mandatory=$true, Position=0)]            
    [ValidateNotNullOrEmpty()]            
    # [PSTypeName('System.Management.ManagementObject#root\cimv2\Win32_UserProfile')]            
    [System.Management.ManagementObject[]]${WMIObject}            
    )            
    Begin{}            
    Process            
    {            
        $WMIObject | ForEach-Object -Process {            
            $profile = (ConvertTo-NTAccount -SID $_.SID)            
            try            
            {            
                # Invoke-WmiMethod -Name Delete reports that 'This method is not implemented'            
                # ([wmi]$_.__Path).Delete()            
                $_.Delete()            
            } catch {            
                Write-Warning -Message "Failed to delete profile $profile"            
            }            
        }            
    }             
    End {}            
} # end of function
  • Step 3: Assemble all the pieces and build the final report
    • Use the above Get-UserProfile and Delete-UserProfile functions
    • Write a new Remove-Profile function that accepts the threshold and days parameters and sends an HTML report

Here’s the result 🙂

#Requires -Version 2.0

function Get-FolderSize {
<# 
.SYNOPSIS 
Gets the size of a folder by getting the size of all files contained within.
 
.DESCRIPTION 
This function uses recursion to get the size of a folder and its subfolders. The output is
a collection of objects that each have a folder name, a size in bytes, and a size in 
formatted bytes (converted to kilo, Mega, Giga, etc).

.PARAMETER Path
Path to the folder whose info is needed.

.PARAMETER RecurseLevel
The level under the parent folder to return objects. Negative values mean infinite depth,
so all subfolders will be returned. Zero means that you only want to have an object returned
for the parent folder. See examples for more information.

Default is -1 (Infinite)

.PARAMETER Descending
Because of the nature of the function, the data cannot be sorted in ascending order unless
no objects are returned until after all of the folder information has been obtained. All of 
the folder names can be returned in descending order in real time, though.

Default is false, so objects returned have folder information in a semi-ascending order.

.PARAMETER IncludeHidden
Include hidden objects in folder information.

Default is false.

.PARAMETER IncludeReparsePoints
Include reparse points in folder information. This means that sizes may be counted more than
once.

Default is false.

.EXAMPLE
PS C:\> Get-FolderSize -LiteralPath C:\Temp

  SizeInBytes Size of Folder                                               Folder                                                      
  ----------- --------------                                               ------                                                      
        34288 33.00 kiloBytes                                              C:\temp\R196853\Vi32\Data\Cur                               
        34288 33.00 kiloBytes                                              C:\temp\R196853\Vi32\Data                                   
        60393 59.00 kiloBytes                                              C:\temp\R196853\Vi32\Eula                                   
     14906422 14.00 MegaBytes                                              C:\temp\R196853\Vi32                                        
        34288 33.00 kiloBytes                                              C:\temp\R196853\Vi64\Data\Cur                               
        34288 33.00 kiloBytes                                              C:\temp\R196853\Vi64\Data                                   
        60393 59.00 kiloBytes                                              C:\temp\R196853\Vi64\Eula                                   
     15772346 15.00 MegaBytes                                              C:\temp\R196853\Vi64                                        
     30797949 29.00 MegaBytes                                              C:\temp\R196853                                             
        15944 16.00 kiloBytes                                              C:\temp\R252187\Vi32\Eula                                   
     16542513 16.00 MegaBytes                                              C:\temp\R252187\Vi32                                        
     16626114 16.00 MegaBytes                                              C:\temp\R252187                                             
    137289106 131.00 MegaBytes                                             C:\temp                                                     

.EXAMPLE
PS C:\> Get-FolderSize -LiteralPath C:\Temp -RecurseLevel 1

  SizeInBytes Size of Folder                                               Folder                                                      
  ----------- --------------                                               ------                                                      
     30797949 29.00 MegaBytes                                              C:\temp\R196853                                             
     34209551 33.00 MegaBytes                                              C:\temp\R252187                                             
     65007500 62.00 MegaBytes                                              C:\temp                                                     



.EXAMPLE
PS C:\> Get-FolderSize -LiteralPath C:\Temp -RecurseLevel 0


  SizeInBytes Size of Folder                                               Folder                                                      
  ----------- --------------                                               ------                                                      
     65007500 62.00 MegaBytes                                              C:\temp                                                     


.EXAMPLE
PS C:\> Get-FolderSize -LiteralPath C:\Temp -Descending

  SizeInBytes Size of Folder                                               Folder                                                      
  ----------- --------------                                               ------                                                      
        73775 72.00 kiloBytes                                              C:\temp\R252187\Vi64\Eula                                   
     17525606 17.00 MegaBytes                                              C:\temp\R252187\Vi64                                        
        73775 72.00 kiloBytes                                              C:\temp\R252187\Vi32\Eula                                   
     16600344 16.00 MegaBytes                                              C:\temp\R252187\Vi32                                        
     34209551 33.00 MegaBytes                                              C:\temp\R252187                                             
        60393 59.00 kiloBytes                                              C:\temp\R196853\Vi64\Eula                                   
        34288 33.00 kiloBytes                                              C:\temp\R196853\Vi64\Data\Cur                               
        34288 33.00 kiloBytes                                              C:\temp\R196853\Vi64\Data                                   
     15772346 15.00 MegaBytes                                              C:\temp\R196853\Vi64                                        
        60393 59.00 kiloBytes                                              C:\temp\R196853\Vi32\Eula                                   
        34288 33.00 kiloBytes                                              C:\temp\R196853\Vi32\Data\Cur                               
        34288 33.00 kiloBytes                                              C:\temp\R196853\Vi32\Data                                   
     14906422 14.00 MegaBytes                                              C:\temp\R196853\Vi32                                        
     30797949 29.00 MegaBytes                                              C:\temp\R196853                                             
     65007500 62.00 MegaBytes                                              C:\temp                                                     


.EXAMPLE
PS C:\> Get-FolderSize -LiteralPath C:\Temp -IncludeHidden -IncludeReparsePoints

If there were hidden files/folders or reparse points in C:\Temp, they would be counted in the size
totals.

#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, Position=0)]
        [Alias("Path")]
        [System.IO.DirectoryInfo] $LiteralPath,
        [Parameter(Mandatory=$false, Position=1)]
        [int] $RecurseLevel = -1,
        [Parameter(Mandatory=$false)]
        [switch] $Descending = $false,
        [Parameter(Mandatory=$false)]
        [switch] $IncludeHidden = $false,
        [Parameter(Mandatory=$false)]
        [switch] $IncludeReparsePoints = $false
    )
    
    Write-Debug "In $LiteralPath"
    Write-Verbose "In $LiteralPath"
    
    # Initialize variable that keeps up with folder size
    $Size = 0
    
    # $RecurseLevel and $WriteToPipeline control whether or not an object
    # is written to the pipeline. See below when recursive call is made
    # for more info.
    $WriteToPipeline = $true

    if ($RecurseLevel -eq 0) {  
        # Do not write any more objects to pipeline. Recursive calls still
        # need to be made in order to get size of subfolders, though
        $WriteToPipeline = $false
    }
    elseif ($RecurseLevel -gt 0) {
        # Keep $WriteToPipeline equal to $true, but decrement the RecurseLevel
        # for future calls
        $RecurseLevel--
    }
    
    # List all child items in current path. Notice that $IncludeHidden controls whether
    # -Force parameter is used.
    Get-ChildItem -LiteralPath $LiteralPath -Force:$IncludeHidden -ErrorAction SilentlyContinue | 
      Sort-Object -Property FullName -Descending:$Descending | # Sort either ascending or descending
      ForEach-Object {

        # Enter if block if we are either including reparse points OR the current file/folder isn't 
        # a reparse point:
        if ($IncludeReparsePoints -or !($_.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {

            if ($_.PsIsContainer) {
                # We've found a sub folder!
                
                # Prepare parameters to pass:                
                $Parameters = @{    LiteralPath = $_.FullName;
                                    Descending = $Descending;
                                    IncludeHidden = $IncludeHidden;
                                    IncludeReparsePoints = $IncludeReparsePoints;
                                    RecurseLevel = $RecurseLevel
                               }
                               
                # Recursively call on folder size function; tee-object saves the returned object so we 
                # can get information from it, and forwards it along the pipeline so it can (potentially)
                # be written to output
                Get-FolderSize @Parameters | Tee-Object -Variable SubFolder | Where-Object { 
                        # This acts as a block if we're not supposed to return an object for this folder.
                        # Script execution must continue, though, so that we can get the true folder size
                        # for the original parent folder(s)

                        $WriteToPipeline 
                    }

                # As we get deeper into folder structure, $SubFolder object will contain more than one of
                # the custom objects since $SubFolder contains what has been output by all previous subfolder 
                # calls.
                if ($SubFolder.Count) {
        
                    # To get around this, simply throw away all objects except the last one (remember,
                    # all subfolder objects have already been written; this is just the tee'd variable
                    # we're working with here)
                    $SubFolder = $SubFolder[-1]

                }
                
                # Get the size of the subfolders from the returned objects
                $Size += $SubFolder.SizeInBytes

            }
            else {
                # File, so add this to the folder's size
                $Size += $_.Length
            }
        } # End of Reparse point if block
    } # End of ForEach-Object

    # Create PSObject with folder information:
    $ReturnObject = New-Object PSObject -Property @{ 
            Folder = $LiteralPath;
            SizeInBytes = $Size;
#            "Size of Folder (SI)" = Get-FormattedByte -Bytes $Size -Standard "si";
#            "Size of Folder (IEC)" = Get-FormattedByte -Bytes $Size -Standard "iec";
            "Size of Folder" = Get-FormattedByte -Bytes $Size -Standard "legacy"
    }
    
    # Return object
    Write-Output $ReturnObject
}

function Get-FormattedByte {
<# 
.SYNOPSIS 
Changes raw number of Bytes into a more readable string.
 
.DESCRIPTION 
This function takes a raw number of bytes and outputs a readable string. Example is 2048 Bytes
would be changed to 2 kiloBytes.

.PARAMETER Bytes
Number of bytes that need to be formatted.

.PARAMETER Precision
Number of decimal places to take conversion to. Default is 2.

.PARAMETER Standard
Determines the unit and prefixes used when converting the 'Bytes'. Choose between SI, IEC and 
Legacy standards. NOTE: Legacy standard is not an official standard. Legacy is what Windows 
operating systems use. 

Default is legacy.

.PARAMETER Suffix
A string that will be added to the end of the prefix used in formatted text. Default is 'Bytes'

.EXAMPLE 
PS C:\> Get-FormattedByte 2000000000
1.86 GigaBytes

.EXAMPLE 
PS C:\> Get-FormattedByte 2000000000 -Standard si
2.00 GigaBytes

PS C:\> Get-FormattedByte 2000000000 -Standard iec
1.86 GibiBytes

PS C:\> Get-FormattedByte 2000000000 -Precision 4
1.8626 GigaBytes

PS C:\> Get-FormattedByte 2000000000 -Precision 4 -Suffix "B"
1.8626 GigaB

#>
    
    param(
        [Parameter(Mandatory=$true, Position=0)]
        [double] $Bytes,
        [Parameter(Mandatory=$false, Position=1)]
        [int] $Precision = 2,
        [Parameter(Mandatory=$false)]
        [ValidateSet("si","iec", "legacy")]
        $Standard = "legacy",
        [Parameter(Mandatory=$false)]
        [string] $Suffix = "Bytes"
    )
    

    # International System of Units Standard:
    $si =     @{ Unit = 1000;
                 Prefixes = "", "kilo","Mega", "Giga", "Tera", "Peta", "Exa" }

    # International Electrotechnial Commision Standard:
    $iec =    @{ Unit = 1024;
                 Prefixes = "", "Kibi", "Mebi", "Gibi", "Tebi", "Pebi", "Exbi" }
    
    # Legacy unit/prefix where binary unit uses SI prefixes:
    $legacy = @{ Unit = 1024;
                 Prefixes = "", "kilo","Mega", "Giga", "Tera", "Peta", "Exa" }
            
    # Get the hashtable that contains the prefixes and unit:    
    $StandardHT = Invoke-Expression "`$$Standard"

    foreach ($Prefix in $StandardHT.Prefixes) {
        if (($Prefix -eq $StandardHT.Prefixes[-1]) -or ($Bytes -lt $StandardHT.Unit)) { 
            # Either out of prefixes, in which case we should just say how
            # many "whatever" bytes we have, or we have a small enough number
            # of "whatever" bytes to display the formatted bytes
            return "$($Bytes.ToString(`"F$Precision`")) $($Prefix)$Suffix" 
        }
        else { 
            # Divide by unit and use a new prefix on next loop
            $Bytes /= $StandardHT.Unit 
        }
    }
}


function ConvertTo-NtAccount
{
<#
    
.SYNOPSIS    
    Translate a SID to its displayname

.DESCRIPTION  
    Translate a SID to its displayname

.PARAMETER Sid
    Provide a SID

.NOTES    
    Name: ConvertTo-NtAccount
    Author: thepowershellguy

.LINK    
    http://thepowershellguy.com/blogs/posh/archive/2007/01/23/powershell-converting-accountname-to-sid-and-vice-versa.aspx
     
.EXAMPLE
    ConvertTo-NtAccount S-1-1-0
    Convert a well-known SID to its displayname
    
#>

param(
[parameter(Mandatory=$true,Position=0)][system.string]$Sid = $null
)
 begin
 {
    $obj = new-object system.security.principal.securityidentifier($sid)
 }
 process
 {
    try
    {
        $obj.translate([system.security.principal.ntaccount])
    }
    catch
    {
        # To remove the silent fail, uncomment next line
        # $_
    }
 }
 End {}
}

Function Remove-Profile {
    [cmdletbinding()]
    Param (
        [parameter(
            HelpMessage="Threshold of freespace left expressed in percent")]
        [ValidateRange(0,100)]
        [int]$FreeSpaceThreshold = 20,
        
        [parameter()]
        [int]$Days = 5
    )
    Begin {
        # Define the Html parts of the report
        $head = @"
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>HTML TABLE</title>
</head><body>
<table>
"@    
    $end = "</table></body></html>"

    # Not an array as Send-mailmessage doesn't accept a system.object and tries a conversion to system.string that fails
    $body = @"
"@    
}
Process {
    
    # Define the  HKLM Constant and the Key we are looking for
    $HKLM = 2147483650
    $Key = "software\microsoft\windows nt\currentversion\profilelist"
    $result = $null
    
    # Get the location of user profiles from the registry
    try {
        $result = Invoke-WmiMethod -Path "ROOT\DEFAULT:StdRegProv" -Name GetExpandedStringValue -ArgumentList $HKLM,$Key,"ProfilesDirectory" -ErrorAction Stop
    } catch  {
        Write-Warning -Message "WMI query failed"
    }
    if ($result.sValue) {
        $drive = Split-Path $result.sValue -Qualifier
        try {
            $wmi_worked = $true
            # Get the freespace of the drive where user profiles are located
            $disk = Get-WmiObject -Class Win32_LogicalDisk -filter "DeviceID='$drive'" -ErrorAction Stop
        } catch  {
            Write-Warning -Message "Failed to query WMI"
            $wmi_worked = $false
        }
        if ($wmi_worked) {
            
            # Get a nicely formatted of the freespace initially left on the drive
            $FreeDividedSize=$disk.Freespace/$disk.Size
            [string]$PerFree="{0:P}" -f $FreeDividedSize
            
            # Add the header to the Html report
            $body += $head
            $body += "<p></p><br>"
            
            # Get the freespace 
            $body += New-Object -TypeName PSObject -Property @{
                'Initial Freespace' = (Get-FormattedByte -Bytes $disk.Freespace -Standard "legacy" -Precision 1)
                'Percent of total drive' = $PerFree 
            } | ConvertTo-Html -Property 'Initial Freespace','Percent of total drive' -Fragment -As LIST
            
            # Calculate  the total size of C:\users
            $body += New-Object -TypeName PSObject -Property @{
                'Location of profiles' = $result.sValue
                'Initial size of profiles' = (Get-FolderSize -LiteralPath $result.sValue -RecurseLevel 0 -IncludeHidden:$true ).'Size of Folder'
            } | ConvertTo-Html -Property 'Location of profiles','Initial size of profiles' -Fragment -As LIST
            $body += "<p></p><br>"
            
            # Define a black list of profiles not to delete
            $ExclusionList = 'Default User','All Users','Default',"Public"
            
            # Calculate the size of each profile
            $body += Get-ChildItem -Path $result.sValue  -Exclude $ExclusionList | Where {$_  -is [system.IO.directoryInfo]} |ForEach-Object -Process {
                New-Object -TypeName PSObject -Property @{
                    'Profile' = $_.FullName
                    'Size' = (Get-FolderSize -LiteralPath $_.FullName -RecurseLevel -1 -IncludeHidden:$true )[-1].'Size of Folder'
                } 
            } | ConvertTo-Html -Property 'Profile','Size' -Fragment -As TABLE 
            
            # Evaluate the threshold
            if ($FreeDividedSize -gt $($FreeSpaceThreshold/100)) {
                "There's more freespace than the {0:P0} threshold specified" -f ($FreeSpaceThreshold/100)
                # We do nothing...
            } else {
            
                # We start deleting folders older that the Days specified as parameter
                $body += "<p></p><br>"
                $body += Get-UserProfile | Where-Object -FilterScript { $_.LastUsedDate -le (Get-Date).AddDays(-$Days) } |
                    ForEach-Object -Process {
                        # Remove profiles
                        $status = 'ok'
                        Write-Verbose -Message "Attempt to delete profile $($_.UserName) located in $($_.Path)" -Verbose
                        $_ | Select-Object -ExpandProperty WMIObject | Delete-UserProfile
                        # If deleting profiles with WMI failed we may still have a folder left
                        if (Test-Path $_.Path) {
                            $status = 'failed'
                            # Attempt to delete this folder, but not with Remove-Item that throws an Access Denied
                            $cmdcommand =  "$env:systemroot\system32\cmd.exe  /C `"rd /S/Q " + $_.Path + "`""
                            $cmdcommandresult = Invoke-Expression $cmdcommand
                            # Test whether our good old rd command succeeded or failed
                            if (Test-Path $_.Path) {
                                $status = 'failed'
                                Write-Warning -Message "Failed to remove folder $($_.Path)"
                            } else {
                                $status = 'ok'
                                Write-Warning -Message "2nd attempt to remove folder $($_.Path) was successful"
                            }
                        }
                        New-Object -TypeName PSObject -Property @{
                            'Deleted Profile' = $_.UserName
                            'Result' = $status
                        }
                } | ConvertTo-Html -Property 'Deleted Profile','Result' -Fragment -As TABLE

                # Get the freespace left on the disk after user profiles removals
                $disk = Get-WmiObject -Class Win32_LogicalDisk -filter "DeviceID='$drive'"
                $FreeDividedSize=$disk.Freespace/$disk.Size
                [string]$PerFree="{0:P}" -f $FreeDividedSize
                $body += "<p></p><br>"
                $body += New-Object -TypeName PSObject -Property @{
                    'Final Freespace' = (Get-FormattedByte -Bytes $disk.Freespace -Standard "legacy" -Precision 1)
                    'Percent of total drive' = $PerFree 
                } | ConvertTo-Html -Property 'Final Freespace','Percent of total drive' -Fragment -As LIST
                
                # Get the total size of profiles after removal
                $body += New-Object -TypeName PSObject -Property @{
                    'Final size of profiles' = (Get-FolderSize -LiteralPath $result.sValue -RecurseLevel 0 -IncludeHidden:$true ).'Size of Folder'
                } | ConvertTo-Html -Property 'Final size of profiles' -Fragment -As LIST 
                $body += "<p></p><br>"
                
                # Get profiles left after the removal
                $body += Get-ChildItem -Path $result.sValue  | Where {$_  -is [system.IO.directoryInfo]} |ForEach-Object -Process {
                    New-Object -TypeName PSObject -Property @{
                        'Profile' = $_.FullName
                        'Size' = (Get-FolderSize -LiteralPath $_.FullName -RecurseLevel -1 -IncludeHidden:$true )[-1].'Size of Folder'
                    } 
                } | ConvertTo-Html -Property 'Profile','Size' -Fragment -As TABLE 
            }
            $body += $end
            # Send the final report
            $smtpserver = "the.smtp.server.address"
            $from = "$env:computername@$(($env:USERDNSDOMAIN).ToLower())"
            $to = "the.admin.email.address"
            $Subject = "Freespace operation"
            try {
                Send-MailMessage -Body $body -From $from -Subject $Subject -To $to -SmtpServer $smtpserver -BodyAsHtml -ErrorAction Stop
            } catch {
                Write-Warning -Message "Failed to send the final report because $($_.Exception.Message)"
            }

        }        
    }
}
End {}

} # end of function

# Main
Remove-Profile

The only issue I’ve encountered so far with the above script is that the Tee-Object cmdlet that Rohn Edwards uses in his advanced functions fails with the following error message – Tee-Object : The pipeline failed due to call depth overflow. The call depth reached 51 and the maximum is 50. – when used in powershell remote session

Advertisements

2 thoughts on “Working with user profiles

  1.             
     Get-UserProfile -Computername RemotePC-Name  |             
     Select-Object -Property UserName,Type,Status,LastUsedDate  |             
     Where-Object -FilterScript { $_.LastUsedDate -le (Get-Date).AddDays(-5) } |             
     Sort-Object -Descending -Property LastUsedDate

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s