Citrix User Profile Management (or UPM) is used in many environments to ensure that users' data is uniform across the entire Citrix infrastructure. This way, a user can access his work anywhere.

Citrix UPM handles this by synchronising users' profiles using the UPM share, which can be hosted on a file server. However, there is a drawback to this: data like browser cache are synchronised across the environment as well, along with user profile data.

Chrome is particularly notorious when it comes to browser data. Chrome user profiles can range from a few hundred MBs to a few GBs. If this data is synchronised across the entire Citrix infrastructure, (all of) your Citrix vda servers can quickly run out of space on the drive that hosts user profiles.

There are policies that you can set to exclude Chrome data from UPM sync, though.

The background

One of our clients had published Chrome through Citrix (they had their requirements). They were probably unaware of Chrome’s behaviour. In no time, SCOM went crazy and sent out tens of disk space alerts.

On analysis, it was found that Chrome data was consuming most of the disk space. They were quick to enable the exclusion of Chrome data from UPM, but it was still necessary to clean up the data that had already been synced.

The solution

PowerShell to the rescue. We wrote a PowerShell function to perform the deletion. Here is the function.

function Clear-Repository {
    param(
        # Path to the UPM repository
        [Parameter(Mandatory=$true, Position=1)]
        [string]
        $Path,

        # Name of the folder to be removed
        [Parameter(Mandatory=$false)]
        [string]
        $SubDirectory
    )
    begin {
        try {
            Write-Verbose "Testing connection to the path, $Path."
            $null = Test-Path -Path $Path -ErrorAction Stop
        }
        catch {
            Write-Error "Unable to reach the path, $Path."
            break
        }
        $DeletionTable = @()
    }
    process {
        $ChildItems = (Get-ChildItem $Path | Where-Object PsIsContainer | Select-Object FullName).FullName

        foreach ($Item in $ChildItems) {
            $DeletionStatus = $null
            $FolderSize     = $null

            if ($SubDirectory) {
                Write-Verbose "Joining the subdirectory to the path."
                $Item = Join-Path -Path $Item -ChildPath $SubDirectory
                Write-Verbose "The full path is $Item."
            }

            Write-Verbose "Testing if $Item exists."
            if (Test-Path -Path $Item) {
                $PathExists = $true
                try {
                    try {
                        Write-Verbose "Calculating folder size."
                        $FolderSize = [math]::Round(((Get-ChildItem -Path $Item -Recurse | Measure-Object -Property Length -Sum -ErrorAction Stop).Sum/1MB), 2)
                    }
                    catch {
                        Write-Verbose 'Folder size could not be determined.'
                    }
                    Write-Verbose "Attempting to delete the folder, $Item and its contents."
                    Remove-Item -LiteralPath $Item -Recurse -Force -WhatIf -ErrorAction Stop
                    $DeletionStatus = 'Deleted'
                }
                catch {
                    $DeletionStatus = 'Error'
                    Write-Error "Unable to delete $Item."
                }
            }
            else {
                Write-Verbose "$Item doesn't exist."
                $PathExists     = $false
            }

            $DeletionTable += New-Object -TypeName PsObject -Property @{
                Location        = $Item
                PathExists      = $PathExists
                DeletionStatus  = $DeletionStatus
                FolderSizeMB    = $FolderSize
            }
        }
        $DeletionTable | Select-Object Location, PathExists, DeletionStatus, FolderSizeMB
    }
}

What the function does

We define the function with two parameters: the path that contains the profiles, and the subdirectory that needs to be deleted from each profile. For instance, the Path would be C:\Users, and the SubDirectory would be AppData\Local\Google\Chrome. If a certain user’s SAM account name is U3234, the location to be cleared would be C:\Users\U3234\AppData\Google\Chrome.

How the function works

The begin block tests if the profile repository exists or not. It does not make sense to proceed if the profile repository itself cannot be reached. Therefore, an error in connecting to the path causes the flow to break out of the function.

Next, all the user profiles within the specified Path are listed out. The full name is selected. Next, the items are looped through. The subdirectory name is joined with the path (now user profiles). Then, the existence of the path is checked for. The path is recursively queried for length, and the total size of the path to be deleted, is gotten.

Next, the function attempts to delete the path recursively and forcefully. The function is designed to perform a WhatIf on the location first. This is so that you can perform a dry run and find out what will be deleted, and what amount of space will be cleared. An error is returned if the deletion fails. The existence of the path as well as the errors in deletion are duly noted.

The function returns a table with the location, whether the path exists or not, whether the path was deleted, and what amount of space was cleared.

Note: When running the function to actually perform the deletion, remove -WhatIf from the Remove-Item statement.

Extending the function

Now, the function above is good enough if you are cleaning the data from only one server. What if you have, like, forty servers from where the data needs to be removed?

This can be achieved using a wrapper function, which takes input from a CSV file, and calls the aforementioned function on loop. Here is the function:

function Clear-ProfileData {
    param(
        # List of servers
        [Parameter(Mandatory=$true, Position=1)]
        [string[]]
        $FilePath
    )

    begin {
        try {
            Test-Path $FilePath -ErrorAction Stop
        }
        catch {
            Write-Error "Unable to reach $FilePath"
            break
        }
    }

    process {
        $InputObject = Import-Csv $FilePath
        foreach ($Object in $InputObject) {
            Clear-Repository -Path $Object.ProfilePath -SubDirectory $Object.ChildPath
        }
    }
}

All it does is, accepts the path to the CSV file as input, loops through the list using a foreach loop, and calls the Clear-Repository function for each of the paths.

The corresponding CSV file is as follows:

ProfilePath,ChildPath
\\CTXSVR001\C$\Users,AppData\Local\Google\Chrome
\\CTXSVR002\C$\Users,AppData\Local\Google\Chrome
\\CTXSVR003\C$\Users,AppData\Local\Google\Chrome
\\CTXSVR004\C$\Users,AppData\Local\Google\Chrome
\\UPMSVR001\UpmProfile$,UPM_Profile\AppData\Local\Google\Chrome

When performing a bulk cleanup, you would do the following:

Clear-ProfileData -Filepath .\AllProfileLocations.csv

Wrapping up

In today’s post, we saw how to perform a cleanup of a certain part of multiple profiles from multiple servers. We used two functions to do the job: one to perform the deletion, and the other, to use an input file to pass multiple server locations to the profile cleanup function. The functions have been wrapped into a single script file, available in my GitHub repository.

While this was actually designed to perform a cleanup of a subdirectory within Citrix UPM profiles, this can be used in any other situation that involves cleanup of a subdirectory within multiple profiles/directories.