Accessing content of Recycle Bin (Windows XP, Vista, 7)
Posted: Sun Sep 11, 2016 9:52 pm
				
				Hi. I was playing a bit with a code which can to delete files from Recycle basing on date when they were deleted to it.
There are two ways to do this: use special interface to control recycle, or use direct access. Surely I've choose 2nd ^^
The recycle in XP is organized following way:
- in a root of every drive is folder called "RECYCLER"
- inside of this folder are subfolders named using user SID
- at last, inside of those subfolders files placed to recycle are located
You need to use external file manager (like Total Commander) to access those folders, or do it programatically.
The files and folders placed to recycle are named like "Dd123.ext" - where "Dd" is like a prefix, 123 is file index, and .ext is extension.
And there is another file placed near - it's named "INFO2" and is system file where all metadata is stored about deleted files.
Windows uses such files to not enumerate all recycled files everytime and to allow files with exact names be placed to recycle without conflicts.
To do something with recycle content or just to examine it, you need to read or write this file manually.
It's however simple enough, the following code reads it and can be easily changed to write too.
For me that stuff interesting to make some daemon for my ElDiablo which will automatically delete oldest files from recycle (basing on deletion date) - as I don't like to purge whole recycle, but also don't like when it contains too much files, but still not reached size limit.
For windows Vista and newer the recycle index format is different (for now I didn't played with it, but maybe soon will).
			There are two ways to do this: use special interface to control recycle, or use direct access. Surely I've choose 2nd ^^
The recycle in XP is organized following way:
- in a root of every drive is folder called "RECYCLER"
- inside of this folder are subfolders named using user SID
- at last, inside of those subfolders files placed to recycle are located
You need to use external file manager (like Total Commander) to access those folders, or do it programatically.
The files and folders placed to recycle are named like "Dd123.ext" - where "Dd" is like a prefix, 123 is file index, and .ext is extension.
And there is another file placed near - it's named "INFO2" and is system file where all metadata is stored about deleted files.
Windows uses such files to not enumerate all recycled files everytime and to allow files with exact names be placed to recycle without conflicts.
To do something with recycle content or just to examine it, you need to read or write this file manually.
It's however simple enough, the following code reads it and can be easily changed to write too.
For me that stuff interesting to make some daemon for my ElDiablo which will automatically delete oldest files from recycle (basing on deletion date) - as I don't like to purge whole recycle, but also don't like when it contains too much files, but still not reached size limit.
For windows Vista and newer the recycle index format is different (for now I didn't played with it, but maybe soon will).
Code: Select all
EnableExplicit
;{ Common/declares }
	
	Structure TOKEN_USER
		User.SID_AND_ATTRIBUTES
	EndStructure
	
	; Returns string representation of user SID (security identifier)
	; Use ConvertStringSidToSid() API to convert string back to binary
	; RETURN:		string view of SID on success
	Procedure$ GetUserSID()
		Protected hToken, dwBufferSize, hLib
		Protected SID$, *SID.String
		Protected *User.TOKEN_USER
		
		; get process token
		OpenProcessToken_(GetCurrentProcess_(), #TOKEN_QUERY, @hToken)
		; get user token with SID 
		GetTokenInformation_(hToken, #TokenUser, 0, 0, @dwBufferSize)
		*User = AllocateMemory(dwBufferSize)
		GetTokenInformation_(hToken, #TokenUser, *User, dwBufferSize, @dwBufferSize)
		
		; convert SID to a string
		hLib = OpenLibrary(#PB_Any, "Advapi32.dll")
		If IsLibrary(hLib)
			CallFunction(hLib, "ConvertSidToStringSidW", *User\User\Sid, @*SID)
			SID$ = PeekS(*SID)
			LocalFree_(*SID)
			CloseLibrary(hLib)
		EndIf
		
		
		; cleanup
		CloseHandle_(hToken)
		FreeMemory(*User)
		
		; return
		ProcedureReturn SID$
	EndProcedure
	
	
	; enumerates files within given folder (or it's subfolders too)
	; Filter$			a mask to filter files by extensions, separated using "|". example: "txt|jpg", "bmp". empty string used to find all files
	; IgnoredPaths$ 	a list of folders to skip on scan. full paths expected without trailing "\", separated using "|"
	; ScanDeepth 		"-1" = unlimited, "0" = scan only specified folder, "1+" = custom
	; GetFolders:		if true, includes also found folders to results. folder paths ends with "\", unlike file paths
	; RETURN: number of files found; Files() array contains full paths (starting from element 1)
	Procedure GetFiles (Path$, Array Files$ (1), Filter$ = "", IgnoredPaths$ = "", ScanDeepth = 0, GetFolders = #False)
		Static CStep = 10240           	; step to resize Files () array, has affect on performance
		Static Count, Counter, Deepth	; Files() size and count. Deepth is current recursion deepth
		If Deepth = 0
			Count = 0 :   Counter = 0 ; reset static stuff
			If Filter$ : Filter$ + "|" : EndIf
			If IgnoredPaths$ : IgnoredPaths$ + "|" : EndIf
			ReplaceString (Path$, "/", "\", #PB_String_InPlace) ; windows can handle both \ and / (at least explorer.exe can), but it is better to ensure
			Dim Files$ (0)
		EndIf
		If Not (Right(Path$, 1)) = "\" : Path$ + "\" : EndIf
		Protected HDir = ExamineDirectory(#PB_Any, Path$, "*.*")
		Protected Current$
		If HDir
			Deepth + 1
			While NextDirectoryEntry(HDir)         ; iterate all files within directory
				Current$ = DirectoryEntryName(HDir); store current file/folder name for future
				If DirectoryEntryType(HDir) = #PB_DirectoryEntry_File ; enumerate files only
					If PeekC(@Filter$) = 0 Or FindString(Filter$, GetExtensionPart(Current$) + "|", 1, #PB_String_NoCase)
						Counter + 1
						If Counter >= Count
							Count + CStep
							ReDim Files$ (Count)
						EndIf
						Files$(Counter) = Path$ + Current$ ; store full path
					EndIf
				ElseIf DirectoryEntryType(HDir) = #PB_DirectoryEntry_Directory 
					If ScanDeepth = -1 Or Deepth <= ScanDeepth; check recursion deepth
						If Not Current$ = "." And Not Current$ = ".."
							Current$ = Path$ + Current$
							If PeekC(@IgnoredPaths$) And FindString(IgnoredPaths$, Current$ + "|", 1, #PB_String_NoCase)
								; skip this subfolder   
							Else
								If GetFolders ; add folders to results too
									Counter + 1
									If Counter >= Count
										Count + CStep
										ReDim Files$ (Count)
									EndIf
									Files$(Counter) = Current$ + "\"  ; store full path
								EndIf
								GetFiles (Current$, Files$(), Filter$, IgnoredPaths$, ScanDeepth, GetFolders) ; go on with subfolders
							EndIf
						EndIf
					EndIf
				EndIf
			Wend
			FinishDirectory(HDir)
			Deepth - 1
			If Deepth = 0 And Counter   ; proceed return only from first call and only if is something to return
				ReDim Files$ (Counter)
				ProcedureReturn Counter
			EndIf
		EndIf
	EndProcedure
	
	
	; just a quickly written function to format FileTime
	Procedure$ FileTimeToString (FileTime.q)
		Protected STime.SYSTEMTIME
		FileTimeToSystemTime_(@FileTime, @STime)
		
		ProcedureReturn RSet(Str(STime\wYear), 4, "0") + "." +
			 RSet(Str(STime\wMonth), 2, "0") + "." + 
			 RSet(Str(STime\wDay), 2, "0") + " " + 
			 RSet(Str(STime\wHour), 2, "0") + ":" +
			 RSet(Str(STime\wMinute), 2, "0") + ":" + 
			 RSet(Str(STime\wSecond), 2, "0")
	EndProcedure
	
;}
;{ Recycle index file format: Win XP and 98 }
	
	; Following declarations ported from 
	; 	https://github.com/abelcheung/rifiuti2/releases/tag/0.6.1
	
	; These offsets are relative To file start 
	#RECORD_COUNT_OFFSET     = 4
	#RECORD_MAXINDEX_OFFSET  = 8
	#RECORD_SIZE_OFFSET      = 12
	#RECORD_START_OFFSET     = 20
	
	; Following offsets are relative To start of each record
	#LEGACY_FILENAME_OFFSET  = $0
	#RECORD_INDEX_OFFSET     = #MAX_PATH
	#DRIVE_LETTER_OFFSET     = ((#MAX_PATH) + 4)
	#FILETIME_OFFSET         = ((#MAX_PATH) + 8)
	#FILESIZE_OFFSET         = ((#MAX_PATH) + 16)
	#UNICODE_FILENAME_OFFSET = ((#MAX_PATH) + 20)
	
	#VERSION4_RECORD_SIZE    = ((#MAX_PATH) + 20)        ; /* 280 bytes */
	#VERSION5_RECORD_SIZE    = ((#MAX_PATH) * 3 + 20)	 ; /* 800 bytes */
	
	
	; Index file header
	Structure HEADER
		SIGNATURE.l			; should be $00000005, but maybe that only for Version5 format
		RECORD_COUNT.l		; total number of records inside of file
		RECENT_INDEX.l		; the last index used to enumerate files. windows adds + 1 to this when adding new record, but don't decrement when deleting
		RECORD_SIZE.l		; the size of every record. valid values are 280 or 800 bytes
		UNKNOWN1.l
	EndStructure
	
	; Windows 98 index record format = 280 bytes
	Structure V4RECORD
		LegacyFilename.a [#MAX_PATH]	; old filename (win98), ascii
		Index.l							; record index, corresponding to index of some deleted file (recycle filenames are like "Dd123.ext", where 123 is index)
		DriveLetter.l					; index of drive letter. 1 = "A", 2 = "B" and so on
		FileTime.q						; timestamp showing when file was deleted
		FileSize.l						; the size of deleted file
	EndStructure
	
	; Win2k, XP (unicode filenames) = 800 bytes
	Structure V5RECORD Extends V4RECORD
		FileName.w [#MAX_PATH]			; actual filename, unicode
	EndStructure
	
;}
;{ INFO2 read/write }
	
	; Read INFO2 index file to array of records
	; this function didn't tested with V4 records (Windows 98 format), but should be easy to adapt it
	; Out()			output array
	; FileName$		path to a INFO2 file
	; RETURN:		number of records read, and records placed to Out() array starting from index 1
	Procedure INFO2_ReadRecords (Array Out.V5RECORD(1), FileName$)
		Protected hFile = OpenFile(#PB_Any, FileName$, #PB_File_SharedRead)
		Protected Header.HEADER
		Protected *Buffer.V5RECORD
		Protected RealCount
		
		If IsFile(hFile)
			; check if file header is valid
			If ReadData(hFile, Header, SizeOf(Header)) = SizeOf(HEADER) And HEADER\SIGNATURE = $00000005
				; check if record size is one of valid
				If (Header\RECORD_SIZE = #VERSION4_RECORD_SIZE) Or (Header\RECORD_SIZE = #VERSION5_RECORD_SIZE)
					; 				Debug "Record count is " + Str(Header\RECORD_COUNT)
					; 				Debug "Record max index is " + Str(Header\RECENT_INDEX)
					
					; allocate buffer to read records
					*Buffer = AllocateMemory(Header\RECORD_SIZE)
					
					; jump to records section
					FileSeek(hFile, #RECORD_START_OFFSET, #PB_Absolute)
					
					; read all records until the end of file reached
					While Eof(hFile) = 0
						; try to read next record
						If ReadData(hFile, *Buffer, Header\RECORD_SIZE) = Header\RECORD_SIZE
							RealCount + 1
							If RealCount > ArraySize(Out())
								ReDim Out(RealCount + 2048)
							EndIf
							; add readed record to results
							CopyMemory(*Buffer, @Out(RealCount), Header\RECORD_SIZE)
						EndIf
					Wend
					FreeMemory(*Buffer)
				EndIf
			EndIf
			
			CloseFile(hFile)
		EndIf
		
		; return
		ReDim Out(RealCount)
		ProcedureReturn RealCount
	EndProcedure
	
	; Write records to a INFO2 file
	; this function didn't tested with V4 records (Windows 98 format), but should be easy to adapt it
	; Out()			records to write
	; FileName$		path to an output INFO2 file
	; RETURN:		true on success
	Procedure INFO2_WriteRecords (Array Records.V5RECORD(1), FileName$)
		Protected hFile = CreateFile(#PB_Any, FileName$)
		Protected Header.HEADER
		Protected RealCount
		
		If IsFile(hFile)
			; sort records by index
			SortStructuredArray(Records(), #PB_Sort_Ascending, OffsetOf(V5RECORD\Index), #PB_Long, 1, ArraySize(Records()))
			
			; write file header
			Header\SIGNATURE = $00000005
			Header\RECORD_SIZE = SizeOf(V5RECORD)
			Header\RECORD_COUNT = ArraySize(Records())
			Header\RECENT_INDEX = Records(ArraySize(Records()))\Index
			WriteData(hFile, Header, SizeOf(Header))
			
			; write all records
			For RealCount = 1 To ArraySize(Records())
				WriteData(hFile, Records(RealCount), SizeOf(V5RECORD))
			Next RealCount
			
			CloseFile(hFile)
		EndIf
		
		; return
		ProcedureReturn Bool(hFile)
	EndProcedure
	
;}
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; test
; to examine drives and RECYCLER paths
Define Drive, Path$
Define UserSID$ = GetUserSID()
; to examine files inside of RECYCLER
Dim R$(0): Define RCount
; to store content of loaded INFO2 file
Dim Records.V5RECORD (0): Define RecNum
; count all records in all INFO2 files
Define TotalDel
For Drive = 'A' To 'Z'
	; current user recycled files on current drive
	Path$ = Chr(Drive) + ":\RECYCLER\" + UserSID$
	
	; if path is valid
	If FileSize(Path$) = -2
		; get all files of user recycler
		RCount = GetFiles(Path$, R$(), "", "", -1, #False)
		
		; iterate all collected files
		; ; only INFO2 files are interesting
		While RCount > 0
			If GetFilePart(R$(RCount)) = "INFO2"
				RecNum = INFO2_ReadRecords(Records(), R$(RCount))
				TotalDel + RecNum
			EndIf
			RCount - 1
		Wend
	EndIf
Next Drive
Debug "Records found in INFO2 files: " + Str(TotalDel)