10

I'm looking for a way to put a folder (with subfolders) into a Recycle Bin with these conditions:

  1. It must be done silently -- without any Windows UI.

  2. The folder must never be permanently deleted. If it can't be put into Recycle Bin, I'd expect the API to fail.

  3. Get a callback routine for the process like CopyFileEx does.

So far I was able to come up with this:

SHFILEOPSTRUCT sfo = {0};
sfo.wFunc = FO_DELETE;
sfo.pFrom = L"K:\\test del from USB\0";     //Folder on a USB stick
sfo.fFlags = FOF_ALLOWUNDO |
     FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NOCONFIRMMKDIR |
     FOF_WANTNUKEWARNING;

int res = SHFileOperation(&sfo);
BOOL bFullSuccess = res == 0 && !sfo.fAnyOperationsAborted;

Which horribly fails on a folder located on a USB flash drive, i.e. it is permanently deleted in despite of the FOF_ALLOWUNDO flag.

So whether I'm not doing something right, or SHFileOperation API is very wrong!

Any idea how to do what I outlined above?

EDIT: I implemented the IRecycleBinManager::WillRecycle method as was suggested by @Denis Anisimov, but there's evidently more to it. Here's my C++ version. First interface definition for the method I need:

#if defined(__cplusplus) && !defined(CINTERFACE)

    MIDL_INTERFACE("5869092D-8AF9-4A6C-AE84-1F03BE2246CC")
    IRecycleBinManager : public IUnknown
    {
    public:

    //function WillRecycle(const pszPath: LPCWSTR): HRESULT; stdcall;
        virtual HRESULT STDMETHODCALLTYPE WillRecycle( 
            /* [string][in] */ __RPC__in LPCWSTR pszFile) = 0;
    };

#endif

and then the call itself:

HRESULT hr;

CoInitializeEx(NULL, COINIT_DISABLE_OLE1DDE | COINIT_APARTMENTTHREADED);

// {4A04656D-52AA-49DE-8A09-CB178760E748}
const CLSID CLSID_RecycleBinManager = {0x4A04656D, 0x52AA, 0x49DE, {0x8A, 0x09, 0xCB, 0x17, 0x87, 0x60, 0xE7, 0x48}};

// {5869092D-8AF9-4A6C-AE84-1F03BE2246CC}
const IID IID_IRecycleBinManager = {0x5869092D, 0x8AF9, 0x4A6C, {0xAE, 0x84, 0x1F, 0x03, 0xBE, 0x22, 0x46, 0xCC}};

IRecycleBinManager* pIRBM = NULL;

hr = CoCreateInstance(CLSID_RecycleBinManager, NULL, CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER,
                      IID_IRecycleBinManager, (void**) &pIRBM);
//  hr = SHCoCreateInstance(NULL, &CLSID_RecycleBinManager, NULL, IID_IRecycleBinManager, (void **)&pIRBM);
if (SUCCEEDED(hr))
{

    hr = pIRBM->WillRecycle(L"C:\\test del");   //Crashes

    pIRBM->Release();
}

Unfortunately I'm getting this error on the line where I'm supposed to call WillRecycle method:

Run-Time Check Failure #0 - The value of ESP was not properly saved across a function call. This is usually a result of calling a function declared with one calling convention with a function pointer declared with a different calling convention.

enter image description here

c00000fd
  • 20,994
  • 29
  • 177
  • 400
  • Condition #2 (without further qualification) appears to exclude the use of the Recycle Bin. Since items in the recycle **are** eventually removed (silently). Is your desire that the folder not be nuked by _your_ program, or that it not be nuked by _any_ program (read: explorer.exe which maintains the recycle bin) – enhzflep May 18 '14 at 09:42
  • Casual Googling suggests the Recycle Bin is *not* created on removable and network media. (Couldn't locate a definitive MSDN page in the first 3 Google pages, sorry.) – Jongware May 18 '14 at 10:40
  • Looks like Windows feature: http://answers.microsoft.com/en-us/windows/forum/windows_7-performance/has-windows-7-cancelled-the-recycle-feature-of-my/51810824-e331-4e1a-a3d8-8592916c6b75 – Alex F May 18 '14 at 10:43
  • 3
    Probably SHQueryRecycleBin function can help to detect whether recycle bin is available for given drive. – Alex F May 18 '14 at 10:58
  • It appears as though `SHFileOperation` will not do to implement your goal. You may have to roll your own solution. Helpful information is available under [Handling Shell Data Transfer Scenarios - Dropping Files on the Recycle Bin](http://msdn.microsoft.com/en-us/library/windows/desktop/bb776904.aspx#recycle). – IInspectable May 18 '14 at 11:10
  • 1
    @IInspectable It does not work. Try to drag and drop file from USB without Recycle Bin on Recycle Bin icon on desktop and Windows will ask you: "Are you sure you want to permanently delete this file?" – Denis Anisimov May 18 '14 at 11:30
  • @AlexFarber: Thanks. So Alex, is there a way to know if a folder being deleted via the `SHFileOperation` API is placed into the Recycle Bin vs. permanently deleted? – c00000fd May 18 '14 at 17:45
  • @IInspectable: I was trying to read the link you provided and it mostly talks about drag-and-dropping files into the Recycle Bin. Is there something that I missed -- relative to my question here? – c00000fd May 18 '14 at 17:48
  • **FOF_ALLOWUNDO**: Preserve undo information, if possible. Operations can be undone only from the same process that performed the original operation. If, despite earlier warnings against doing so, pFrom does not contain fully-qualified path and file names, this flag is ignored. – xenophōn Dec 21 '18 at 09:12

2 Answers2

10

Every drive has its own Recycle Bin. And when you delete file from drive С: it should be moved to Recycle Bin on drive С:. When you delete file from USB drive it should be moved to Recycle Bin on USB drive. But when USB drive has no Recycle Bin then file is permanently deleted. This is default Windows behavior.

FOF_ALLOWUNDO flag is RECOMMENDATION only. MSDN says about FOF_ALLOWUNDO flag:

Preserve undo information, if possible.

So there is no any error when Windows permanently deleted files even when you use FOF_ALLOWUNDO flag.

The only way I see is to check presence of Recycle Bin on drive with SHQueryRecycleBin function (as pointed by Alex Farber in comment) before delete operation. But even if Recycle Bin presents it is not full guaranty that file will be deleted to Recycle Bin. Recycle Bin has maximal limit of size and it can be already full.

UPDATE

You can use hack. You can emulate removing of file into Recycle Bin with you own code which will create all necessary system records in C:\$Recycle.Bin\UserSID folder. I tested this method on Windows 7 and it works correctly. It allows to ignore limitation of max size of Recycle Bin. Also it allows to move files from USB into Recycle Bin on any drive.

UPDATE 2

For Vista+ you can use undocumented interface IRecycleBinManager (Russian description can be found on webpage http://rcrrad.com/2010/10/14/bitbucket-interfaces/):

const
  IID_IEnumRecycleItems: TGUID = '{6E325F88-D12F-49E5-895B-8EC98630C021}';
  IID_IRecycle: TGUID = '{0125E62F-8349-443A-854B-A55FB84CFA35}';
  IID_IRecycleBin: TGUID = '{F964AD97-96F4-48AB-B444-E8588BC7C7B3}';
  IID_IRecycleBinManager: TGUID = '{5869092D-8AF9-4A6C-AE84-1F03BE2246CC}';
  CLSID_RecycleBinManager: TGUID = '{4A04656D-52AA-49DE-8A09-CB178760E748}';

type
  { Тип Корзины }
  tagRECYCLEBIN_TYPE = (RBTYPE_VOLUME, RBTYPE_KNOWNFOLDER);
  TRecycleBinType = tagRECYCLEBIN_TYPE;

  { Данные об удаленном элементе }
  PDeletedItem = ^TDeletedItem;
  tagDELETEDITEM = packed record
    dwFileSizeLow: DWORD;
    dwFileSizeHigh: DWORD;
    ftDeletionTime: TFileTime;
    szOriginalPath: array[0..Pred(MAX_PATH)] of WideChar;
    szDisplacedPath: array[0..Pred(MAX_PATH)] of WideChar;
  end;
  TDeletedItem = tagDELETEDITEM;

  { Перечислитель элементов Корзины }
  IEnumRecycleItems = interface(IUnknown)
    ['{6E325F88-D12F-49E5-895B-8EC98630C021}']
    { celt может быть равен только единице }
    function Next(celt: ULONG; out rgelt: TDeletedItem;
      var pceltFetched: ULONG): HRESULT; stdcall;
    { Not Implemented }
    function Skip(celt: ULONG): HRESULT; stdcall;
    function Reset: HRESULT; stdcall;
    { Not Implemented }
    function Clone(out ppenum: IEnumRecycleItems): HRESULT; stdcall;
  end;

  { "Интерфейс-переходник" между IRecycleBin и IRecycleBinManager }
  IRecycle = interface(IUnknown)
    ['{0125E62F-8349-443A-854B-A55FB84CFA35}']
    function Compact(): HRESULT; stdcall;
    function GetFileData(const pszPath: LPCWSTR;
      out lpData: TDeletedItem): HRESULT; stdcall;
    function GetItemCount(out lpCount: TLargeInteger): HRESULT; stdcall;
    function GetUsedSpace(out lpUsedSpace: TLargeInteger): HRESULT; stdcall;
    function IsEmpty(): HRESULT; stdcall;
    function PurgeAll(pfo: IFileOperation): HRESULT; stdcall;
    function PurgeItems(const lpstrItems: LPCWSTR;
      pfo: IFileOperation): HRESULT; stdcall;
    function SuspendUpdating(fSuspend: BOOL): HRESULT; stdcall;
    function RecycleItem(const lpstrItem: LPCWSTR; const dwAttrs: DWORD;
      const iFileSize: TLargeInteger; out psi: IShellItem): HRESULT; stdcall;
    function RestoreItems(const lpstrItems: LPCWSTR;
      pfo: IFileOperation): HRESULT; stdcall;
    function IsRecycled(const pszPath: LPCWSTR;
      lpRecycled: PBOOL): HRESULT; stdcall;
    function EnumItems(dwFlags: DWORD;
      out EnumRecycleItems: IEnumRecycleItems): HRESULT; stdcall;
    function WillRecycle(const pszPath: LPCWSTR): HRESULT; stdcall;
  end;

  { Представляет определенную Корзину на конкретном диске }
  IRecycleBin = interface(IUnknown)
    ['{F964AD97-96F4-48AB-B444-E8588BC7C7B3}']
    function Compact(): HRESULT; stdcall;
    function GetFileData(const pszPath: LPCWSTR;
      out lpData: TDeletedItem): HRESULT; stdcall;
    function GetItemCount(out lpCount: TLargeInteger): HRESULT; stdcall;
    function GetUsedSpace(out lpUsedSpace: TLargeInteger): HRESULT; stdcall;
    function IsEmpty(): HRESULT; stdcall;
    function PurgeAll(pfo: IFileOperation): HRESULT; stdcall;
    function PurgeItems(const lpstrItems: LPCWSTR;
      pfo: IFileOperation): HRESULT; stdcall;
    function SuspendUpdating(fSuspend: BOOL): HRESULT; stdcall;
    function RecycleItem(const lpstrItem: LPCWSTR; const dwAttrs: DWORD;
      const iFileSize: TLargeInteger; out psi: IShellItem): HRESULT; stdcall;
    function RestoreItems(const lpstrItems: LPCWSTR;
      pfo: IFileOperation): HRESULT; stdcall;
    function IsRecycled(const pszPath: LPCWSTR;
      lpRecycled: PBOOL): HRESULT; stdcall;
    function EnumItems(dwFlags: DWORD;
      out EnumRecycleItems: IEnumRecycleItems): HRESULT; stdcall;
    function WillRecycle(const pszPath: LPCWSTR): HRESULT; stdcall;
    function Initialize(const rbType: TRecycleBinType;
      const pszID: LPCWSTR): HRESULT; stdcall;
    function GetTypeID(out rbType: TRecycleBinType;
      var pszID: LPWSTR): HRESULT; stdcall;
    function GetIDList(out ppidl: PItemIDList): HRESULT; stdcall;
    function GetLocation(pszPathBuffer: LPWSTR;
      cchMax: DWORD): HRESULT; stdcall;
    function GetMaxCapacityRange(out lpMin: TLargeInteger;
      out lpMax: TLargeInteger): HRESULT; stdcall;
    function GetMaxCapacity(out lpCapacity: TLargeInteger): HRESULT; stdcall;
    function SetMaxCapacity(const lpCapacity: TLargeInteger): HRESULT; stdcall;
    function GetPurgeOnDelete(out fNukeOnDelete: BOOL): HRESULT; stdcall;
    function SetPurgeOnDelete(const fNukeOnDelete: BOOL): HRESULT; stdcall;
  end;

  { Менеджер всех Корзин данной ОС }
  IRecycleBinManager = interface(IUnknown)
    ['{5869092D-8AF9-4A6C-AE84-1F03BE2246CC}']
    function Compact(): HRESULT; stdcall;
    function GetFileData(const pszPath: LPCWSTR;
      out lpData: TDeletedItem): HRESULT; stdcall;
    function GetItemCount(out lpCount: TLargeInteger): HRESULT; stdcall;
    function GetUsedSpace(out lpUsedSpace: TLargeInteger): HRESULT; stdcall;
    function IsEmpty(): HRESULT; stdcall;
    function PurgeAll(pfo: IFileOperation): HRESULT; stdcall;
    function PurgeItems(const lpstrItems: LPCWSTR;
      pfo: IFileOperation): HRESULT; stdcall;
    function SuspendUpdating(fSuspend: BOOL): HRESULT; stdcall;
    { Not Implemented }
    function RecycleItem(const lpstrItem: LPCWSTR; const dwAttrs: DWORD;
      const iFileSize: TLargeInteger; out psi: IShellItem): HRESULT; stdcall;
    function RestoreItems(const lpstrItems: LPCWSTR;
      pfo: IFileOperation): HRESULT; stdcall;
    function IsRecycled(const pszPath: LPCWSTR;
      lpRecycled: PBOOL): HRESULT; stdcall;
    function EnumItems(dwFlags: DWORD;
      out EnumRecycleItems: IEnumRecycleItems): HRESULT; stdcall;
    function WillRecycle(const pszPath: LPCWSTR): HRESULT; stdcall;
    function DelayCompaction(const fDelay: BOOL): HRESULT; stdcall;
    function GetRecycleBinCount(out iCount: Integer): HRESULT; stdcall;
    function GetRecycleBinAt(const index: Integer; const iid: TGUID;
      out ppv): HRESULT; stdcall;
    function GetRecycleBin(const pszPath: LPCWSTR; const iid: TGUID;
      out ppv): HRESULT; stdcall;
    function Refresh(): HRESULT; stdcall;
  end;

You can check the possibility of deletion of the file into Recycle Bin with the following code:

function CanFileBeDeletedToRecycleBin(const AFileName: UnicodeString): Boolean;
var
  RecycleBinManager: IRecycleBinManager;
begin
  OleCheck(CoCreateInstance(CLSID_RecycleBinManager, nil, CLSCTX_INPROC_SERVER or CLSCTX_LOCAL_SERVER, IRecycleBinManager, RecycleBinManager));
  try
    Result := RecycleBinManager.WillRecycle(PWideChar(AFileName)) = S_OK;
  finally
    RecycleBinManager := nil;
  end;
end;

UPDATE 3

Also you can try the following code for delete oject into Recycle Bin:

function GetObjectSize(const AFileName: UnicodeString): Int64;
var
  FindHandle: THandle;
  FindData: TWin32FindDataW;
  S: Int64;
begin
  Result := 0;
  FindHandle := FindFirstFileW(PWideChar(AFileName), FindData);
  if FindHandle = INVALID_HANDLE_VALUE then
    RaiseLastOSError;
  try
    repeat
      if (FindData.cFileName <> UnicodeString('.')) and (FindData.cFileName <> '..') then
        begin
          Int64Rec(S).Lo := FindData.nFileSizeLow;
          Int64Rec(S).Hi := FindData.nFileSizeHigh;
          Result := Result + S;
          if FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY <> 0 then
            Result := Result + GetObjectSize(AFileName + '\*.*');
        end;
    until not FindNextFileW(FindHandle, FindData);
  finally
    FindClose(FindHandle);
  end;
end;

procedure DeleteToRecycleBin(const AFileName: UnicodeString);
var
  Attr: DWORD;
  Size: Int64;
  RecycleBinManager: IRecycleBinManager;
  RecycleBin: IRecycleBin;
  ShellItem: IShellItem;
begin
  OleCheck(CoCreateInstance(CLSID_RecycleBinManager, nil, CLSCTX_INPROC_SERVER or CLSCTX_LOCAL_SERVER, IRecycleBinManager, RecycleBinManager));
  try
    OleCheck(RecycleBinManager.GetRecycleBin(PWideChar(AFileName), IRecycleBin, RecycleBin));
    try
      Attr := GetFileAttributes(PWideChar(AFileName));
      Size := GetObjectSize(AFileName);
      OleCheck(RecycleBin.RecycleItem(PWideChar(AFileName), Attr, Size, ShellItem));
      ShellItem := nil;
    finally
      RecycleBin := nil;
    end;
  finally
    RecycleBinManager := nil;
  end;
end;
Denis Anisimov
  • 3,297
  • 1
  • 10
  • 18
  • Thanks. Yes, I know that about Recycle Bin. What I don't understand is how unpredictable that API is. So far, true to say, I can call `SHQueryRecycleBin` before calling `SHFileOperation` but in case of a USB drive, all I get is `E_FAIL` which is not very descriptive and I'm sure can be returned in a host of other situations. So there's seriously no way of knowing whether a file or a folder will be permanently deleted or placed in the Recycle Bin, is there? – c00000fd May 18 '14 at 17:43
  • And another issue with your suggested approach is the situation when a folder/file being deleted is too large for the Recycle Bin. In that case it is also permanently deleted. So how will I know that that will be done by the `SHFileOperation` API? It sounds like I need a new question... – c00000fd May 18 '14 at 17:56
  • 1
    Why do you want to delete folders into Recycle Bin? Maybe it will be better if you create your own storage for deleted files? – Denis Anisimov May 18 '14 at 19:57
  • I don't. It's a client's request. – c00000fd May 18 '14 at 20:10
  • Which the target OS is? – Denis Anisimov May 18 '14 at 20:24
  • I tried your suggested `WillRecycle` method but it crashes the app. I posted my C++ code in the original question. Can you take a look? I'm clearly missing something. – c00000fd May 29 '14 at 18:05
  • Sorry but I am not C++ guru. I cannot convert Delphi code I use to C++. It is better if you ask somebody why knows both languages. – Denis Anisimov May 29 '14 at 18:33
  • I understand. Can you try to compile your Delphi code and see if `RecycleBinManager.WillRecycle` works on your end -- just like you showed above? Because when I call it like you did, it crashes. – c00000fd May 29 '14 at 19:18
  • Hmm. Interesting. Why is mine crashing then... Just curious, when you define the `IRecycleBinManager` interface, do you need to provide all methods or just the ones I used -- `WillRecycle` in my case? – c00000fd May 29 '14 at 19:27
  • Yes, you must provide all methods. – Denis Anisimov May 29 '14 at 19:56
  • Thanks. Let me try to redo it. Hopefully I'll convert it to C. Also, what is `PWideChar`? Is it a pointer to a null-terminated string consisting of 16-bit chars? Or is it some special COM string? – c00000fd May 29 '14 at 20:05
  • Yes, it is a pointer to null-terminated string consisting of 16-bit chars (PWideChar = LPCWSTR) – Denis Anisimov May 29 '14 at 20:19
  • Sorry for asking too many questions, I'm just trying to wade through this Delphi thicket. How can I tell a pointer from a plain variable? For instance, `const iid: TGUID;` in the `GetRecycleBinAt` function definition, is it a pointer to GUID or just a GUID variable. – c00000fd May 29 '14 at 20:49
  • Delphi "const iid: TGUID" is equivalent to C++ "[in] TGUID riid" – Denis Anisimov May 29 '14 at 21:09
  • @DenisAnisimov: Not quite. In Delphi, passing a record by `const` passes it by reference, not by value. So the equivalent of `const iid: TGUID` in C++ is actually `const TGUID& iid`, or `const REFIID iid` in COM syntax. – Remy Lebeau May 29 '14 at 22:36
  • @DenisAnisimov: I just happened to build a project with your code and no matter what path I pass into `RecycleBinManager.WillRecycle` it returns `S_OK` or a plain 0. Am I missing something? – c00000fd May 31 '14 at 09:34
  • @c00000fd Cannot confirm. What are the filenames do you pass in WillRecycle? – Denis Anisimov May 31 '14 at 11:50
  • @DenisAnisimov: One nice person converted your project to C++ for me and I'm testing it there: http://1drv.ms/SZxHCJ True to say, that's the first time I'm dealing with C++ Builder. So I put in a path that is definitely too big for recycle bin into `CanFileBeDeletedToRecycleBin` and it still returned S_OK. I then put an invalid path and it still gave me S_OK. Can you take a look? – c00000fd May 31 '14 at 19:32
  • @c00000fd: You are right. It looks WillRecycle check only drive letter of filename. Did you try code from Update3 of my answer? Also there is still a possibility to you hack (first update of my answer). I described structure of RB files in the answer on you other question. – Denis Anisimov May 31 '14 at 20:58
  • Thanks. Without your `IRecycleBinManager.WillRecycle` method I'm back to basics. Which means trying to **guess** if files can be placed into the recycle bin, which is kinda dangerous. I gave details over here: http://forums.codeguru.com/showthread.php?545395-How-can-I-get-path-to-the-recycle-bin-folder-for-a-specific-drive&p=2155927#post2155927 – c00000fd May 31 '14 at 22:45
  • Btw, on an unrelated subject. One word of advice about your `GetObjectSize` method in update 3. Make sure to skip symbolic links, `FILE_ATTRIBUTE_REPARSE_POINT` attribute. Otherwise you may end up with an infinite recursive loop. – c00000fd May 31 '14 at 22:47
  • The above link to IRecycleBinManager has gone. I, too am very interested in [deciphering the file handling](http://www.theassimilationlab.com/forums/topic/15480-testing-alpha-pre-release-code-anyone/page-2#entry342878) in shell32, please add anything that comes to light. :) – Laurie Stearn Mar 30 '16 at 11:11
1

I was able to come up with a solution to all 3 of my original points/requests.

In a nutshell, one needs to use the IFileOperation interface and implement IFileOperationProgressSink in it.

Here's full code sample and explanation for that.

EDIT: Ok, there's more to it. The method I posted above doesn't cover all bases :(

c00000fd
  • 20,994
  • 29
  • 177
  • 400