# encoding: ascii
# api: csharp
# title: Recycle Bin Object
# description: Recycle Bin Object
# version: 00.000
# type: class
# author: skourlatov
# license: CC0
# function: ConvertTo-LitheralSize
# x-poshcode-id: 5362
# x-archived: 2015-01-31T20:32:32
# x-published: 2015-08-11T15:08:00
#
# —————————
# This script creates object which correctly works with ntfs streams and reparse points
#
## ============================================================
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;

public static class RecycleBin
{
	[DllImport("shell32.dll",CharSet=CharSet.Unicode)]
	public static extern void SHUpdateRecycleBinIcon();
}
"@
## ============================================================
Function ConvertTo-LitheralSize
{
	Param
	(
		[Parameter(ValueFromPipeline)]
		[System.UInt64[]]$Size
	)
	Process
	{
		$Size | ForEach-Object -Process {
			switch ($_)
			{
				{ $_ -lt 1KB }  { $litheral = "$_ bytes"; break }
				{ $_ -lt 1MB }  { $litheral = ($_ / 1KB).ToString('.0 KiB'); break }
				{ $_ -lt 1GB }  { $litheral = ($_ / 1MB).ToString('.00 MiB'); break }
				{ $_ -lt 1TB }  { $litheral = ($_ / 1GB).ToString('.00 GiB'); break }
				{ $_ -lt 1PB }  { $litheral = ($_ / 1TB).ToString('.00 TiB'); break }
					 Default	{ $litheral = ($_ / 1TB).ToString('.000 PiB') }
			}
				$litheral | write
		}
	}
}
## ============================================================
[void] [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic')
Function Recycle-Item
{
	[CmdletBinding()]
	Param(
		[Parameter(Position=0,Mandatory,ValueFromPipeline)]
			[System.Object[]]$Path,
			[System.Management.Automation.SwitchParameter]$WipeOut
	)
	Begin
	{
		[Microsoft.VisualBasic.FileIO.UIOption]$dialog_mode = 0x02
		[Microsoft.VisualBasic.FileIO.RecycleOption]$recycle_mode = 0x02 -bor -not $WipeOut
	}
	Process
	{
		$Path | gi -Force -ErrorAction 'SilentlyContinue' | ForEach-Object -Process {
			if (($_.Attributes -band 0x10) -eq 0x10)
			{
				"Removing directory: `"$($_.FullName)`"" | Write-Verbose
				[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory($_.FullName,$dialog_mode,$recycle_mode)
			}
			else
			{
				"Removing file: `"$($_.FullName)`"" | Write-Verbose
				[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile($_.FullName,$dialog_mode,$recycle_mode)
			}
		}
	}
}

# -- Recycle Bin Object --
$method = @{}

$method['Help'] = {
	$help = @"

	`"Recycle Bin Object`"

	Author   : Michael Skourlatov
	Created  : 2014-Jun-27
	Modified : 2014-Aug-12

This script creates object which correctly works with ntfs streams and reparse points

Usage samples:

	`$RecycleBin.List()[`$id].Object -> return the item as file or folder by it's ID

	`$RecycleBin.ShowItems([<mask>]) -> show items list
	`$RecycleBin.Restore([<mask>]) -> restore items (may need confirmation)
	`$RecycleBin.Measure([<mask>]) -> measure true size of all items in Recycle Bin
	`$RecycleBin.Clear([<mask>]) -> this is obvious
	`$RecycleBin.List([<mask>]) -> this is the core method
	`$RecycleBin.Help() -> show this help

More examples:

	`$RecycleBin.List([<mask>]).Measure()
	`$RecycleBin.List([<mask>]).Restore()
	`$RecycleBin.List([<mask>]).Delete()

	`$too_old = [datetime]::Now.AddDays(-20)
	`$selected = `$RecycleBin.List() | where { `$_.DecryptInfo().DeletionDate -le `$too_old }
	`$selected.Delete()

Notes:

	<mask> is the standard file mask like '*.txt'
	[] brackets mean parameter isn't mandatory

"@
	$help | Write-Output
}

$method['List'] = {
	.{ param ([Parameter(Mandatory=$false)][System.String]$Mask)} @args

	$actual_list = $this._ComWrapper.Items() | select
	if ($Mask) { $actual_list = $actual_list | where -Property 'Name' -like -Value $Mask }
	$sked = @()

	$actual_list | foreach {
		$rec = $_ | select 'Name','Type','ModifyDate',
			@{Name = 'Actions';		Expression = { $_.Verbs() | select }},
			@{Name = 'Object';		Expression = { gi -LiteralPath $_.Path -Force }},
			@{Name = 'InfoFile';	Expression = {
				$seppos = $_.Path.LastIndexOf(92)
				$parent = $_.Path.Substring(0, $seppos)
				$child =  $_.Path.Substring($seppos + 1).Replace('$R','$I')
				gi -LiteralPath "$parent\$child" -Force
			}}

		$rec | Add-Member -MemberType 'ScriptMethod' -Name 'DecryptInfo' -Value {
			[byte[]]$bb = cat $this.InfoFile -Encoding 'Byte'

		##	[uint64[]]$length_raw = [char[]][System.Text.Encoding]::Unicode.GetString($bb[8..15])
		##	$length = 0
		##	for ($i = 0; $i -le 3; $i++) { $length += $length_raw[$i] -shl $i*16 }

			$timestamp_raw = [BitConverter]::ToInt64($bb[16..23], 0)
			$ref_date	= [DateTime]::FromBinary(5116597250427387904).ToLocalTime() ## 1601-01-01 00:00:00.000 UTC
			$timespamp	= $ref_date.AddTicks($timestamp_raw)

			$full_name	= [System.Text.Encoding]::Unicode.GetString($bb[24..543])
			$seppos		= $full_name.LastIndexOf(92)
			$paret_path	= $full_name.Substring(0, $seppos + 1)

			New-Object -TypeName 'PSObject' -Property @{
													ParentPath = $paret_path;
													DeletionDate = $timespamp;
												##	SystemSize = $length;
												}
		}

		$rec | Add-Member -MemberType 'ScriptMethod' -Name 'Delete' -Value {
			$this.Object, $this.InfoFile | recycle -WipeOut
		}

		$rec | Add-Member -MemberType 'ScriptMethod' -Name 'Restore' -Value {
			$this.Actions[0].DoIt()
			if (!$this.Object.Exists)
			{
				$this.InfoFile.Delete()
			}
		}

		$rec | Add-Member -MemberType 'ScriptMethod' -Name 'Measure' -Value {

			Set-Variable -Name _sum -Value @{
												Item = $this.Name;
												Links = 0;
												Files = 0;
												Folders = 0;
												Size = 0
							} -Option AllScope

			function calculate ([System.Object]$Obj, [System.IO.DirectoryInfo]$Fol)
			{
				if (-not $Obj)
				{
					$Obj = $Fol | ls -Force
				}
				if ($Obj)
				{
					$Obj | foreach {
						if (($_.Attributes -band 0x0400) -eq 0x0400)
						{
							++$_sum.Links
						}
						elseif (($_.Attributes -band 0x0010) -ne 0x0010)
						{
							try
							{
								[UInt64]$_sum.Size += ($_ | gi -Force -Stream '*' | measure Length -Sum).Sum
							}
							catch
							{
								[UInt64]$_sum.Size += $_.Length
							}
							finally
							{
								++$_sum.Files
							}
						}
						else
						{
							++$_sum.Folders
							calculate -Fol $_
						}
					}
				}
			}
			calculate -Obj $this.Object
			return New-Object -TypeName 'PSObject' -Property $_sum
		}
		$sked += $rec
	}
	return $sked
}

$method['Clear'] = {
	.{ param ([Parameter(Mandatory=$false)][System.String]$Mask)} @args

	$wipe_list = $this.List($Mask)
	if ($wipe_list)
	{
		$wipe_list.Delete()
		$true
	}
	else
	{
		$false
	}

	[RecycleBin]::SHUpdateRecycleBinIcon()
}

$method['Measure'] = {
	.{ param ([Parameter(Mandatory=$false)][System.String]$Mask)} @args

	$res = @{
		Links = 0;
		Files = 0;
		Folders = 0;
		Size = 0
	}

	$this.List($Mask) | ForEach-Object -Process {
		$item = $_.Measure()
		$res.Links   += $item.Links
		$res.Files   += $item.Files
		$res.Folders += $item.Folders
		[UInt64]$res.Size += $item.Size
	}

	return New-Object -TypeName 'PSObject' -Property $res
}
$method['Restore'] = {
	.{ param ([Parameter(Mandatory=$false)][System.String]$Mask)} @args

	$items_list = $this.List($Mask)

	if (-not $items_list)
	{
		return $false
	}

	$confirmation = [bool]$Mask
	if (-not $confirmation)
	{
						[console]::Write('Are you sure to restore ALL items? [Y/N]: ')
		$confirmation = [console]::ReadKey().Key -eq 'Y' 
						[console]::WriteLine()
	}
	if ($confirmation)
	{
		$items_list.Restore()
		$true
	}
	else
	{
		$false
	}
}

$method['ShowItems'] = {
	.{ param ([Parameter(Mandatory=$false)][System.String]$Mask)} @args

	$items_list = $this.List($Mask)
	if (-not $items_list)
	{
		return $false
	}

	$output_list = @()
	$id = 0

	$items_list | ForEach-Object -Process {

		$item_measure = $_.Measure()
		$item_size = $item_measure.Size | ConvertTo-LitheralSize

		$item_children = ""
		if ($item_measure.Folders)
		{
			$files = $links = $folders = ""
			if ($item_measure.Files) { $files = "F/$($item_measure.Files); " }
			if ($item_measure.Links) { $links = "L/$($item_measure.Links); " }
			if ($item_measure.Folders - 1) { $folders = "D/$($item_measure.Folders - 1); " }
			$item_children = $folders + $files + $links
		}
		$desc = $_.DecryptInfo()
		$item = New-Object -TypeName 'PSObject'
		$item | Add-Member -MemberType 'NoteProperty' -Name 'ID'			-Value $id
		$item | Add-Member -MemberType 'NoteProperty' -Name 'Size'			-Value $item_measure.Size
		$item | Add-Member -MemberType 'NoteProperty' -Name 'Literal'		-Value $item_size
		$item | Add-Member -MemberType 'NoteProperty' -Name 'Parent'		-Value $desc.ParentPath
		$item | Add-Member -MemberType 'NoteProperty' -Name 'Child'			-Value $_.Name
		$item | Add-Member -MemberType 'NoteProperty' -Name 'Type'			-Value $_.Type
		$item | Add-Member -MemberType 'NoteProperty' -Name 'Encapsulates'	-Value $item_children
		$item | Add-Member -MemberType 'NoteProperty' -Name 'ModifyDate'	-Value $_.ModifyDate
		$item | Add-Member -MemberType 'NoteProperty' -Name 'Deleted'		-Value $desc.DeletionDate
	##	$item | Add-Member -MemberType 'NoteProperty' -Name 'Object'		-Value $_.Object.FullName
		$output_list += $item
		$id++
	}
	$output_list | ogv -Title "Recycle Bin Items ($Mask)"
	return $true
}

$bin = New-Object -TypeName 'PSObject'

$bin | Add-Member -MemberType 'NoteProperty' -Name '_ComWrapper' -Value (
	New-Object -ComObject 'Shell.Application'
).Namespace(0xA)

$method.Keys | ForEach-Object -Process {
	$bin | Add-Member -MemberType 'ScriptMethod' -Name $_ -Value $method[$_]
}

New-Variable -Name 'RecycleBin' -Value $bin -Option 'Constant'
Remove-Variable -Name 'method', 'bin'
## -- Recycle Bin Object --
