Public/vss/Restore-ShadowCopyFile.ps1
|
#Requires -Version 5.1 function Restore-ShadowCopyFile { <# .SYNOPSIS Restore a file from a VSS shadow copy snapshot to a destination path .DESCRIPTION Restores a specific file from a Volume Shadow Copy snapshot identified by its ShadowCopyId. The SourcePath is relative to the drive root of the shadow copy volume. The function locates the shadow copy device object, constructs the full shadow path, and copies the file to the specified destination. Supports -Force to overwrite existing files. .PARAMETER ShadowCopyId The unique identifier (GUID) of the shadow copy to restore from. .PARAMETER SourcePath The path to the file relative to the drive root. Example: 'Data\report.xlsx' for C:\Data\report.xlsx. .PARAMETER DestinationPath The full destination path where the file will be restored to. .PARAMETER Force Overwrite the destination file if it already exists. .PARAMETER ComputerName One or more computer names to target. Defaults to the local computer. .PARAMETER Credential Optional credential for remote execution. .EXAMPLE Restore-ShadowCopyFile -ShadowCopyId '{AB12CD34-EF56-7890-AB12-CD34EF567890}' -SourcePath 'Data\report.xlsx' -DestinationPath 'C:\Restore\report.xlsx' Restores report.xlsx from the specified shadow copy to C:\Restore on the local machine. .EXAMPLE Restore-ShadowCopyFile -ShadowCopyId '{AB12CD34-EF56-7890-AB12-CD34EF567890}' -SourcePath 'Logs\app.log' -DestinationPath 'D:\Recovery\app.log' -Force -ComputerName 'SRV01' Restores app.log from a shadow copy on SRV01, overwriting any existing file. .EXAMPLE Get-ShadowCopy -DriveLetter 'C' | Select-Object -First 1 | Restore-ShadowCopyFile -SourcePath 'Config\settings.json' -DestinationPath 'C:\Backup\settings.json' Restores settings.json using shadow copy information piped from Get-ShadowCopy. .OUTPUTS PSWinOps.ShadowCopyRestoreResult Returns an object with ComputerName, ShadowCopyId, SourcePath, DestinationPath, Restored, SizeBytes, SizeMB, ErrorMessage, 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 (access to shadow copy device objects) .LINK https://github.com/k9fr4n/PSWinOps .LINK https://learn.microsoft.com/en-us/windows/win32/vss/volume-shadow-copy-service-overview #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [OutputType('PSWinOps.ShadowCopyRestoreResult')] param( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [string]$ShadowCopyId, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$SourcePath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$DestinationPath, [Parameter(Mandatory = $false)] [switch]$Force, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('CN', 'Name', 'DNSHostName')] [string[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] [ValidateNotNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential ) begin { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting" $forceFlag = $Force.IsPresent $scriptBlock = { param( [string]$shadowId, [string]$sourcePath, [string]$destPath, [bool]$forceOverwrite ) $resultHash = @{ ShadowCopyId = $shadowId SourcePath = $sourcePath DestinationPath = $destPath Restored = $false SizeBytes = [long]0 ErrorMessage = '' } try { $shadow = Get-CimInstance -ClassName 'Win32_ShadowCopy' -ErrorAction Stop | Where-Object -Property 'ID' -EQ -Value $shadowId if ($null -eq $shadow) { $resultHash.ErrorMessage = "Shadow copy with ID '$shadowId' not found" return $resultHash } $shadowRoot = $shadow.DeviceObject + '\' $fullSource = Join-Path -Path $shadowRoot -ChildPath $sourcePath if (-not (Test-Path -LiteralPath $fullSource)) { $resultHash.ErrorMessage = "Source file '$sourcePath' not found in shadow copy at '$fullSource'" return $resultHash } if ((Test-Path -LiteralPath $destPath) -and (-not $forceOverwrite)) { $resultHash.ErrorMessage = "Destination '$destPath' already exists. Use -Force to overwrite." return $resultHash } $destDir = Split-Path -Path $destPath -Parent if (-not [string]::IsNullOrWhiteSpace($destDir)) { $null = New-Item -ItemType 'Directory' -Path $destDir -Force -ErrorAction SilentlyContinue } Copy-Item -LiteralPath $fullSource -Destination $destPath -Force:$forceOverwrite -ErrorAction Stop $copiedFile = Get-Item -LiteralPath $destPath -ErrorAction Stop $resultHash.SizeBytes = $copiedFile.Length $resultHash.Restored = $true } catch { $resultHash.ErrorMessage = $_.Exception.Message $resultHash.Restored = $false } return $resultHash } } process { foreach ($machine in $ComputerName) { $shouldProcessTarget = "Restore '$SourcePath' from shadow copy $ShadowCopyId to '$DestinationPath' on $machine" if (-not $PSCmdlet.ShouldProcess($shouldProcessTarget, 'Restore-ShadowCopyFile')) { continue } Write-Verbose -Message "[$($MyInvocation.MyCommand)] Processing '$machine' - Restoring '$SourcePath' from $ShadowCopyId" try { $invokeParams = @{ ComputerName = $machine ScriptBlock = $scriptBlock ArgumentList = @($ShadowCopyId, $SourcePath, $DestinationPath, $forceFlag) } if ($PSBoundParameters.ContainsKey('Credential')) { $invokeParams['Credential'] = $Credential } $raw = Invoke-RemoteOrLocal @invokeParams $sizeBytes = [long]$raw.SizeBytes $sizeMB = [math]::Round($sizeBytes / 1MB, 2) [PSCustomObject]@{ PSTypeName = 'PSWinOps.ShadowCopyRestoreResult' ComputerName = $machine ShadowCopyId = $raw.ShadowCopyId SourcePath = $raw.SourcePath DestinationPath = $raw.DestinationPath Restored = $raw.Restored SizeBytes = $sizeBytes SizeMB = $sizeMB ErrorMessage = $raw.ErrorMessage Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' } } catch { Write-Error -Message "[$($MyInvocation.MyCommand)] Failed on '${machine}': $_" [PSCustomObject]@{ PSTypeName = 'PSWinOps.ShadowCopyRestoreResult' ComputerName = $machine ShadowCopyId = $ShadowCopyId SourcePath = $SourcePath DestinationPath = $DestinationPath Restored = $false SizeBytes = [long]0 SizeMB = 0 ErrorMessage = "$_" Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' } } } } end { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed" } } |