A while ago, I posted an Iron Scripter challenge asking you to write some PowerShell code for working with items in the recycle bin. You were asked to calculate how much space the recycle bin is using and then restore a file. If you'd prefer, stop reading this post, check out the challenge and see what you can come up with. If you get stuck, this article might get you back on track. Although, I'm sure there are several ways to meet the challenge. My solution is far from the only solution.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Parsing the Recycle Bin
I decided to use the COM object approach and used a reference to Shell.Application. The Windows Recycle Bin can be accessed as Namespace 10.
$shell = New-Object -com shell.application $rb = $shell.Namespace(10)
This namespace will include the recycle bin on all fixed disks. Even though this object type may not be well documented, you can still use PowerShell to discover what to do. Get-Member is still your friend.
The Items property looks like it will give me deleted items.
And sure enough it does. To make this easier, I'll add the items to an array.
This is where it gets tricky.
Even though it looks like there are only 156 items in my recycle bin, there are many more. I only see the top level 156 items. Items that are folders have their own Items property.
As you can see, there is good information for each deleted file. In exploring objects with Get-Member, I put together a function to turn each deleted item into a usable PowerShell object.
Function ParseItem {
[cmdletbinding()]
Param(
[Parameter(Mandatory, ValueFromPipeline)]
[object]$Item
)
#this function relies variables set in a parent scope
Process {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing $($item.path)"
# uncomment for troubleshooting
# $global:raw += $item
if ($item.IsFolder -AND ($item.type -notmatch "ZIP")) {
Write-Verbose "Enumerating $($item.name)"
Try {
#track the path name through each child object
if ($fldpath) {
$fldpath = Join-Path -Path $fldPath -ChildPath $item.GetFolder.Title
}
else {
$fldPath = $item.GetFolder.Title
}
#recurse through child items
$item.GetFolder().Items() | ParseItem
Remove-Variable -Name fldpath
}
Catch {
# Uncomment for troubleshooting
# $global:rbwarn += $item
Write-Warning ($item | Out-String)
Write-Warning $_.exception.message
}
}
else {
#sometimes the original location is stored in an extended property
$data = $item.ExtendedProperty("infotip").split("`n") | Where-Object { $_ -match "Original location" }
if ($data) {
$origPath = $data.split(":", 2)[1].trim()
$full = Join-Path -path $origPath -ChildPath $item.name -ErrorAction stop
Remove-Variable -Name data
}
else {
#no extended property so use this code to attemp to rebuild the original location
if ($item.parent.title -match "^[C-Zc-z]:\\") {
$origPath = $item.parent.title
}
elseif ($fldpath) {
$origPath = $fldPath
}
else {
$test = $item.parent
Write-Host "searching for parent on $($test.self.path)" -ForegroundColor cyan
do { $test = $test.parentfolder; $save = $test.title } until ($test.title -match "^[C-Zc-z]:\\" -OR $test.title -eq $save)
$origPath = $test.title
}
$full = Join-Path -path $origPath -ChildPath $item.name -ErrorAction stop
}
[pscustomobject]@{
PSTypename = "DeletedItem"
Name = $item.name
Path = $item.Path
Modified = $item.ModifyDate
OriginalPath = $origPath
OriginalFullName = $full
Size = $item.Size
IsFolder = $item.IsFolder
Type = $item.Type
}
}
} #process
}
My function has a non-standard name because I am using it as an internal, helper function inside of a control script. I can use this function to transform each recycle bin item into something easier to work with.
This means I can get all the items like this:
$bin = $rb.items() | ParseItem
Which makes it very easy to calculate how much space my recycle bin is using.
With this code as my foundation, it wouldn't be that much more work to get Recycle Bin information for different fixed drives.
$bin | group-object -Property {$_.path.substring(0,2)} |
Select-Object -Property Name,Count,
@{Name="SizeMB";Expression = {($_.group | measure-object -Property size -sum).sum/1MB}}
Restoring a Recycle Bin Item
Because I did all the work up front to write an object to the pipeline with original location information, it was pretty easy to write a function to restore the deleted item by moving it from the recycle bin location to the original location.
Function Restore-RecycleBinItem {
[cmdletbinding(SupportsShouldProcess)]
Param(
[Parameter(Mandatory, ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[object]$Item
)
Begin {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
} #begin
Process {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] $($Item.Path) "
if (-Not (Test-Path $Item.originalPath)) {
New-Item $Item.originalpath -force -itemtype directory
}
Move-Item -path $Item.Path -Destination $Item.OriginalFullName -PassThru -Force
} #process
End {
Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
} #end
} #close Restore-RecyleBinItem
Now it is very simple to restore a deleted item.
I should probably revise the restore function to let me specify an alternate location. Or add some logic not to overwrite an existing file. But I can leave those scripting challenges to you.
