Trying to replicate TreeSize functionality with PowerShell

Vincent Sprague 5 Reputation points
2024-02-14T18:16:47.1266667+00:00

I have a need to find the size of user's home directories. However the user's sub directories are restricted to the SYSTEM and specific user. So any PowerShell script I've tried to run has so far failed to gain permission. Even when run as admin. I was directed to this script which supposedly enables some backup privileges that may allow my script access enough to find the size of the directory. However incorporating this function with the primary script appears to have failed.

The program TreeSize has no issue pulling this information and providing me a report. And at this point I'm very close to just buying a license and using it instead of trying to recreate the functionality in PowerShell. But before I throw in the towel I figured I'd post my as-is script here and see what the community thought of it. Currently the script will error out and say access is denied when trying to access the user's home directories. Perhaps someone here can make this work?

# Define the Privileges enum
enum Privileges {
    SeBackupPrivilege
    # Add other privileges as needed
}

# Define the structures required for privilege enabling
Add-Type @"
    using System;
    using System.Runtime.InteropServices;

    public struct TokPriv1Luid {
        public int Count;
        public long Luid;
        public int Attr;
    }

    public class PoshPrivilege {
        [DllImport("kernel32.dll", ExactSpelling = true)]
        public static extern IntPtr GetCurrentProcess();

        [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
        public static extern bool OpenProcessToken(
            IntPtr h,
            int acc,
            ref IntPtr phtok
        );

        [DllImport("advapi32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool AdjustTokenPrivileges(
            IntPtr htok,
            bool disall,
            ref TokPriv1Luid newst,
            int len,
            IntPtr prev,
            IntPtr relen
        );

        [DllImport("advapi32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool LookupPrivilegeValue(
            string host,
            string name,
            ref long pluid
        );
    }
"@

Function Enable-Privilege {
    [cmdletbinding(
        SupportsShouldProcess = $True
    )]
    Param (
        [parameter(Mandatory = $True)]
        [Privileges[]]$Privilege
    )    
    If ($PSCmdlet.ShouldProcess("Process ID: $PID", "Enable Privilege(s): $($Privilege -join ', ')")) {
        #region Constants
        $SE_PRIVILEGE_ENABLED = 0x00000002
        $SE_PRIVILEGE_DISABLED = 0x00000000
        $TOKEN_QUERY = 0x00000008
        $TOKEN_ADJUST_PRIVILEGES = 0x00000020
        #endregion Constants

        $TokenPriv = New-Object TokPriv1Luid
        $HandleToken = [intptr]::Zero
        $TokenPriv.Count = 1
        $TokenPriv.Attr = $SE_PRIVILEGE_ENABLED
    
        #Open the process token
        $Return = [PoshPrivilege]::OpenProcessToken(
            [PoshPrivilege]::GetCurrentProcess(),
            ($TOKEN_QUERY -BOR $TOKEN_ADJUST_PRIVILEGES), 
            [ref]$HandleToken
        )    
        If (-NOT $Return) {
            Write-Warning "Unable to open process token! Aborting!"
            Break
        }
        ForEach ($Priv in $Privilege) {
            $PrivValue = $Null
            $TokenPriv.Luid = 0
            #Lookup privilege value
            $Return = [PoshPrivilege]::LookupPrivilegeValue($Null, $Priv, [ref]$PrivValue)             
            If ($Return) {
                $TokenPriv.Luid = $PrivValue
                #Adjust the process privilege value
                $return = [PoshPrivilege]::AdjustTokenPrivileges(
                    $HandleToken, 
                    $False, 
                    [ref]$TokenPriv, 
                    [System.Runtime.InteropServices.Marshal]::SizeOf($TokenPriv), 
                    [IntPtr]::Zero, 
                    [IntPtr]::Zero
                )
                If (-NOT $Return) {
                    Write-Warning "Unable to enable privilege <$priv>! "
                }
            }
        }
    }
}

# Check if the Active Directory module is loaded
if (-not (Get-Module -Name ActiveDirectory -ErrorAction SilentlyContinue)) {
    # Try to load the Active Directory module
    try {
        Import-Module ActiveDirectory -ErrorAction Stop
    } catch {
        Write-Host "Failed to load the Active Directory module. Please ensure the Active Directory RSAT tools are installed."
        exit 1
    }
}

$recipients = "Admin@domain.local"
$sender = "Users-Group@domain.local"
$subject = "Group User Backup Report"

# Get all members of the "users-Group" group recursively
$members = Get-ADGroupMember -Identity "users-Group" -Recursive | Get-ADUser -Properties DisplayName, Mail, HomeDirectory, Department

# Create an array to store the results
$results = @()

foreach ($member in $members) {
    $displayName = $member.DisplayName
    $mail = $member.Mail
    $homeDirectory = $member.HomeDirectory
    $department = $member.Department

    # Use a local path for calculating the size
    #$localHomeDirectory = $homeDirectory -replace '\\domain.local\dfs\users\', 'E:\folder\Users\'
    $localHomeDirectory = $homeDirectory -replace [regex]::Escape('\\domain.local\dfs\users\'), 'E:\folder\Users\'

    # Check if $localHomeDirectory is not null before testing its path
    if ($localHomeDirectory -ne $null -and (Test-Path $localHomeDirectory)) {
        # Get the size of the local home directory
        $homeDirectorySize = (Get-ChildItem -Path $localHomeDirectory -Recurse | Measure-Object -Property Length).Sum
    } else {
        # Set $homeDirectorySize to 0 if $localHomeDirectory is null or the path doesn't exist
        $homeDirectorySize = 0
    }

    # Convert the size to a human-readable format
    $homeDirectorySizeFormatted = "{0:N2} MB" -f ($homeDirectorySize / 1MB

)

    # Create a custom object with the user details and home directory size
    $userDetails = [PSCustomObject]@{
        DisplayName        = $displayName
        Mail               = $mail
        HomeDirectory      = $homeDirectory  # Display UNC path in the email
        Department         = $department
        HomeDirectorySize  = $homeDirectorySizeFormatted
    }

    # Add the user details to the results array
    $results += $userDetails
}

# Sort the results by display name
$results = $results | Sort-Object DisplayName

# Convert the results to HTML
$htmlBody = $results | ConvertTo-Html | Out-String

# Send the email
Send-MailMessage -To $recipients -From $sender -Subject $subject -Body $htmlBody -BodyAsHtml -SmtpServer "smtpgw.domain.local"

Windows for business | Windows Server | User experience | PowerShell
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. Aung Zaw Min Thwin 306 Reputation points
    2024-02-15T01:38:02.84+00:00

    Hi, Pls try to use "-Force" switch in Get-ChildItem cmdlet. https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-childitem

    -Force> Allows the cmdlet to get items that otherwise can't be accessed by the user, such as hidden or system files. The Force parameter doesn't override security restrictions. Implementation varies among providers. For more information, see about_Providers.

    Alternatively, try using robocopy. I think it is a little faster. Pls see the example below.

    $RoboCopyRpt = robocopy 'c:\users\UserA\' NULLDir /E /L /NFL /NDL /NJH /BYTES /R:0 /W:0 | out-string
    $FolderSize = [math]::Round( ([uint64]([regex]::Match($RoboCopyRpt, '(?<=Bytes\s*:\s*)\d+').value)) / 1MB , 2)
    Write-Host "Folder Size (MB) = $FolderSize"
    
    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.