Public/vss/Get-ShadowCopyStorage.ps1

#Requires -Version 5.1
function Get-ShadowCopyStorage {
    <#
        .SYNOPSIS
            Show VSS storage allocation per volume on local or remote computers
 
        .DESCRIPTION
            Retrieves Volume Shadow Copy storage allocation details using Win32_ShadowStorage via CIM.
            Reports used space, allocated space, maximum space, and snapshot count per volume.
            Supports filtering by drive letter and remote execution via Invoke-RemoteOrLocal.
 
        .PARAMETER ComputerName
            One or more computer names to query. Defaults to the local machine.
 
        .PARAMETER DriveLetter
            Single drive letter (A-Z) to filter storage information for a specific volume.
 
        .PARAMETER Credential
            PSCredential object for remote authentication.
 
        .EXAMPLE
            Get-ShadowCopyStorage
 
            Shows VSS storage allocation for all volumes on the local computer.
 
        .EXAMPLE
            Get-ShadowCopyStorage -ComputerName 'SRV01' -DriveLetter 'C'
 
            Shows VSS storage allocation for the C: drive on remote server SRV01.
 
        .EXAMPLE
            'SRV01', 'SRV02' | Get-ShadowCopyStorage -Credential (Get-Credential)
 
            Shows VSS storage allocation on SRV01 and SRV02 using alternate credentials.
 
        .OUTPUTS
            PSWinOps.ShadowCopyStorage
            Returns objects with ComputerName, DriveLetter, UsedSpaceBytes, UsedSpaceMB,
            AllocatedSpaceMB, MaxSpaceMB, UsedPercent, SnapshotCount, and Timestamp properties.
 
        .NOTES
            Author: Franck SALLET
            Version: 1.0.0
            Last Modified: 2026-04-10
            Requires: PowerShell 5.1+ / Windows only
            Requires: Administrator privileges for VSS queries
 
        .LINK
            https://github.com/k9fr4n/PSWinOps
 
        .LINK
            https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-shadowstorage
    #>

    [CmdletBinding()]
    [OutputType('PSWinOps.ShadowCopyStorage')]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('CN', 'Name', 'DNSHostName')]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $false)]
        [ValidatePattern('^[A-Za-z]$')]
        [string]$DriveLetter,

        [Parameter(Mandatory = $false)]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential
    )

    begin {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting"

        $unboundedThreshold = 1PB

        $driveLetterArg = if ($PSBoundParameters.ContainsKey('DriveLetter')) {
            $DriveLetter 
        } else {
            '' 
        }

        $scriptBlock = {
            param([string]$FilterDriveLetter)

            $volumeIndex = @{}
            foreach ($vol in (Get-CimInstance -ClassName Win32_Volume -ErrorAction SilentlyContinue)) {
                if ($vol.DeviceID) {
                    $normalizedId = $vol.DeviceID.TrimEnd('\').ToLower()
                    if ($vol.DriveLetter) {
                        $volumeIndex[$normalizedId] = $vol.DriveLetter.TrimEnd(':')
                    } elseif ($vol.Label) {
                        $shortLabel = if ($vol.Label.Length -gt 8) {
                            $vol.Label.Substring(0, 8) 
                        } else {
                            $vol.Label 
                        }
                        $volumeIndex[$normalizedId] = "[$shortLabel]"
                    } elseif ($vol.DeviceID -match '\{([^}]+)\}') {
                        $volumeIndex[$normalizedId] = $Matches[1].Substring(0, 8)
                    }
                }
            }

            $shadowCountIndex = @{}
            foreach ($shadow in (Get-CimInstance -ClassName Win32_ShadowCopy -ErrorAction SilentlyContinue)) {
                $normalizedVol = $shadow.VolumeName.TrimEnd('\').ToLower()
                if ($shadowCountIndex.ContainsKey($normalizedVol)) {
                    $shadowCountIndex[$normalizedVol] += 1
                } else {
                    $shadowCountIndex[$normalizedVol] = 1
                }
            }

            $storageEntries = Get-CimInstance -ClassName Win32_ShadowStorage -ErrorAction Stop

            foreach ($storage in $storageEntries) {
                $deviceId = ''
                $volumeObj = $storage.Volume
                if ($volumeObj -is [Microsoft.Management.Infrastructure.CimInstance] -and $volumeObj.DeviceID) {
                    $deviceId = $volumeObj.DeviceID.TrimEnd('\').ToLower()
                } else {
                    $volumeRef = $volumeObj.ToString()
                    # CIM reference format varies: DeviceID="..." or DeviceID = "..."
                    if ($volumeRef -match 'DeviceID\s*=\s*"([^"]+)"') {
                        $deviceId = ($Matches[1] -replace '\\\\', '\').TrimEnd('\').ToLower()
                    } elseif ($volumeRef -match '\{([0-9a-fA-F-]+)\}') {
                        # Fallback: extract GUID from reference string
                        $deviceId = "\\?\volume{$($Matches[1])}"
                    }
                }

                $resolvedDrive = if ($deviceId -ne '' -and $volumeIndex.ContainsKey($deviceId)) {
                    $volumeIndex[$deviceId]
                } else {
                    '?'
                }

                if ($FilterDriveLetter -ne '' -and $resolvedDrive -ne $FilterDriveLetter) {
                    continue
                }

                $snapshotCount = if ($deviceId -ne '' -and $shadowCountIndex.ContainsKey($deviceId)) {
                    $shadowCountIndex[$deviceId]
                } else {
                    0
                }

                # MaxSpace can be UInt64.MaxValue (18446744073709551615) when unbounded
                # which overflows [long]/Int64. Detect and normalise to -1.
                $maxSpaceRaw = $storage.MaxSpace
                $maxSpaceLong = if ($maxSpaceRaw -is [UInt64] -and $maxSpaceRaw -gt [long]::MaxValue) {
                    [long]-1
                } else {
                    [long]$maxSpaceRaw
                }

                [PSCustomObject]@{
                    DriveLetter    = $resolvedDrive
                    DeviceID       = $deviceId
                    UsedSpace      = [long]$storage.UsedSpace
                    AllocatedSpace = [long]$storage.AllocatedSpace
                    MaxSpace       = $maxSpaceLong
                    SnapshotCount  = [int]$snapshotCount
                }
            }
        }
    }

    process {
        foreach ($machine in $ComputerName) {
            Write-Verbose -Message "[$($MyInvocation.MyCommand)] Querying VSS storage on '$machine'"

            try {
                $invokeParams = @{
                    ComputerName = $machine
                    ScriptBlock  = $scriptBlock
                    ArgumentList = @($driveLetterArg)
                }
                if ($PSBoundParameters.ContainsKey('Credential')) {
                    $invokeParams['Credential'] = $Credential
                }

                $raw = Invoke-RemoteOrLocal @invokeParams

                foreach ($item in $raw) {
                    $isUnbounded = ($item.MaxSpace -eq -1) -or ($item.MaxSpace -lt 0) -or ($item.MaxSpace -gt $unboundedThreshold)

                    $maxSpaceMB = if ($isUnbounded) {
                        'Unbounded' 
                    } else {
                        [math]::Round($item.MaxSpace / 1MB, 2) 
                    }
                    $usedPercent = if ($isUnbounded -or $item.MaxSpace -eq 0) {
                        0
                    } else {
                        [math]::Round(($item.UsedSpace / $item.MaxSpace) * 100, 1)
                    }

                    [PSCustomObject]@{
                        PSTypeName       = 'PSWinOps.ShadowCopyStorage'
                        ComputerName     = $machine
                        DriveLetter      = $item.DriveLetter
                        UsedSpaceBytes   = [long]$item.UsedSpace
                        UsedSpaceMB      = [math]::Round($item.UsedSpace / 1MB, 2)
                        AllocatedSpaceMB = [math]::Round($item.AllocatedSpace / 1MB, 2)
                        MaxSpaceMB       = $maxSpaceMB
                        UsedPercent      = $usedPercent
                        SnapshotCount    = [int]$item.SnapshotCount
                        Timestamp        = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
                    }
                }
            } catch {
                Write-Error -Message "[$($MyInvocation.MyCommand)] Failed on '${machine}': $_"
                continue
            }
        }
    }

    end {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed"
    }
}