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)