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.