I would like to restore a deleted file from the Windows recycle bin. I have access to the original file path before deletion and the path of the recycle bin entry. How can I restore the deleted file programmatically using the Windows C++ API? Bonus points if I can restore it to a different path as well.
// The path of the file before it was deleted
wchar_t const *original_path = L"C:\\code\\swan\\ignore\\dir_2\\file1";
// The path of the generated recycle bin entry upon deletion
wchar_t const *recycle_bin_path = L"C:\\$Recycle.Bin\\S-1-5-21-738277947-893724712-3765747123-1001\\$RJB4I8F";
// TODO: restore `recycle_bin_path` to `original_path`
I decided to implement the restore algorithm from this answer. It's a substantial amount of code but the simplest of the options I considered. It also gives you maximum control.
There is one function for undeleting a file and one for undeleting a directory because the steps differ.
The functions assume you know the full path of the file/directory, in the recycle bin, which you are trying to restore, and optionally its path prior to deletion so you can restore it to the same place. I believe the metadata file ($IXXXXXX) contains the original path, maybe you can use that.
If you are using shlwapi to perform deletions, you can capture the paths like so:
struct MyFileOperationProgressSink : IFileOperationProgressSink {
// Implement PostDeleteItem, example below
};
struct swan_path : std::array<char, 1037> {};
void my_dummy_delete_routine()
{
// NOTE: Error checking and cleanup omitted for brevity.
HRESULT result = {};
result = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
IFileOperation *file_op = nullptr;
result = CoCreateInstance(CLSID_FileOperation, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&file_op));
result = file_op->SetOperationFlags(/*FOF_NOCONFIRMATION | */FOF_ALLOWUNDO);
while (/*...*/) {
IShellItem *to_delete = nullptr;
result = SHCreateItemFromParsingName(full_path_to_delete_utf16.c_str(), nullptr, IID_PPV_ARGS(&to_delete));
result = file_op->DeleteItem(to_delete, nullptr); // Each one of these will trigger MyFileOperationProgressSink::PostDeleteItem
}
MyFileOperationProgressSink prog_sink;
DWORD cookie = {};
result = file_op->Advise(&prog_sink, &cookie);
result = file_op->PerformOperations();
file_op->Unadvise(cookie);
}
HRESULT MyFileOperationProgressSink::PostDeleteItem(DWORD, IShellItem *item, HRESULT result, IShellItem *item_newly_created) noexcept
{
if (FAILED(result)) {
return S_OK;
}
// Extract deleted item path, UTF16
wchar_t *deleted_item_path_utf16 = nullptr;
if (FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, &deleted_item_path_utf16))) {
return S_OK;
}
SCOPE_EXIT { CoTaskMemFree(deleted_item_path_utf16); };
// Convert deleted item path to UTF8
swan_path deleted_item_path_utf8;
if (!utf16_to_utf8(deleted_item_path_utf16, deleted_item_path_utf8.data(), deleted_item_path_utf8.max_size())) {
return S_OK;
}
// Extract recycle bin item path
wchar_t *recycle_bin_hardlink_path_utf16 = nullptr;
bool free_recycle_bin_hardlink_path_utf16 = false;
swan_path recycle_bin_item_path_utf8 = path_create("");
if (item_newly_created != nullptr) {
if (FAILED(item_newly_created->GetDisplayName(SIGDN_FILESYSPATH, &recycle_bin_hardlink_path_utf16))) {
return S_OK;
}
free_recycle_bin_hardlink_path_utf16 = true;
if (!utf16_to_utf8(recycle_bin_hardlink_path_utf16, recycle_bin_item_path_utf8.data(), recycle_bin_item_path_utf8.max_size())) {
return S_OK;
}
}
SCOPE_EXIT { if (free_recycle_bin_hardlink_path_utf16) CoTaskMemFree(recycle_bin_hardlink_path_utf16); };
SFGAOF attributes = {};
if (FAILED(item->GetAttributes(SFGAO_FOLDER|SFGAO_LINK, &attributes))) {
print_debug_msg("FAILED IShellItem::GetAttributes(SFGAO_FOLDER|SFGAO_LINK)");
return S_OK;
}
// (https://learn.microsoft.com/en-us/windows/win32/shell/manage#connected-files)
//
// IFileOperation implicitly deletes "connected files".
// If you are creating/storing custom deletion events from this function,
// you will need to account for this to avoid double counting.
//
// Non issue if only ONE of the files in the connected pair is being deleted in the IFileOperation.
// Issue if BOTH files in the connected pair are being deleted in the IFileOperation, because PostDeleteItem is called twice
// for the same item if connected file/directory pair are queued for deletion in the same IFileOperation.
// I recommend recording any potential files/directories that could be affected by this behaviour into a std::set and
// only record an event if you have not seen the file/directory previously in the set.
print_debug_msg("PostDeleteItem [%s] [%s]", deleted_item_path_utf8.data(), recycle_bin_item_path_utf8.data());
return S_OK;
}
Here's the code to restore a single file:
typedef int8_t s8;
typedef int16_t s16;
typedef int32_t s32;
typedef int64_t s64;
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;
struct undelete_file_result
{
bool step0_convert_hardlink_path_to_utf16;
bool step1_metadata_file_opened;
bool step2_metadata_file_read;
bool step3_new_hardlink_created;
bool step4_old_hardlink_deleted;
bool step5_metadata_file_deleted;
bool success() noexcept
{
return step0_convert_hardlink_path_to_utf16
&& step1_metadata_file_opened
&& step2_metadata_file_read
&& step3_new_hardlink_created
&& step4_old_hardlink_deleted
&& step5_metadata_file_deleted;
}
};
/// @brief Restores a "deleted" file from the recycle bin. Implementation closely follows https://superuser.com/a/1736690.
/// @param recycle_bin_hardlink_path_utf8 The full UTF-8 path to the hardlink in the recycling bin, format: $Recycle.Bin/.../RXXXXXX[.ext]
/// @return A structure of booleans indicating which steps were completed successfully. If all values are true, the undelete was successful.
undelete_file_result undelete_file(char const *recycle_bin_hardlink_path_utf8) noexcept
{
undelete_file_result retval = {};
wchar_t recycle_bin_hardlink_path_utf16[MAX_PATH];
if (!utf8_to_utf16(recycle_bin_hardlink_path_utf8, recycle_bin_hardlink_path_utf16, lengthof(recycle_bin_hardlink_path_utf16))) {
return retval;
}
retval.step0_convert_hardlink_path_to_utf16 = true;
wchar_t recycle_bin_metadata_path_utf16[MAX_PATH];
(void) StrCpyNW(recycle_bin_metadata_path_utf16, recycle_bin_hardlink_path_utf16, lengthof(recycle_bin_metadata_path_utf16));
{
wchar_t *metadata_file_name = path_find_filename(recycle_bin_metadata_path_utf16);
metadata_file_name[1] = L'I'; // $RXXXXXX[.ext] -> $IXXXXXX[.ext]
}
HANDLE metadata_file_handle = CreateFileW(recycle_bin_metadata_path_utf16,
GENERIC_READ,
0,
NULL,
OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN,
NULL);
if (metadata_file_handle == INVALID_HANDLE_VALUE) {
return retval;
}
retval.step1_metadata_file_opened = true;
SCOPE_EXIT { if (metadata_file_handle != INVALID_HANDLE_VALUE) CloseHandle(metadata_file_handle); };
constexpr u64 num_bytes_header = sizeof(s64);
constexpr u64 num_bytes_file_size = sizeof(s64);
constexpr u64 num_bytes_file_deletion_date = sizeof(s64);
constexpr u64 num_bytes_file_path_length = sizeof(s32);
char metadata_buffer[num_bytes_header + 2 + // 2 potential junk bytes preceeding header, is my interpretation
num_bytes_file_size +
num_bytes_file_deletion_date +
num_bytes_file_path_length +
MAX_PATH] = {};
DWORD metadata_file_size = GetFileSize(metadata_file_handle, nullptr);
assert(metadata_file_size <= lengthof(metadata_buffer));
DWORD bytes_read = 0;
if (!ReadFile(metadata_file_handle, metadata_buffer, metadata_file_size, &bytes_read, NULL)) {
return retval;
}
assert(bytes_read == metadata_file_size);
retval.step2_metadata_file_read = true;
u64 num_bytes_junk = 0;
num_bytes_junk += u64(metadata_buffer[0] == 0xFF);
num_bytes_junk += u64(metadata_buffer[1] == 0xFE);
[[maybe_unused]] s64 *header = reinterpret_cast<s64 * >(metadata_buffer + num_bytes_junk);
[[maybe_unused]] s64 *file_size = reinterpret_cast<s64 * >(metadata_buffer + num_bytes_junk + num_bytes_header);
[[maybe_unused]] FILETIME *file_deletion_date = reinterpret_cast<FILETIME *>(metadata_buffer + num_bytes_junk + num_bytes_header + num_bytes_file_size);
[[maybe_unused]] s32 *file_path_len = reinterpret_cast<s32 * >(metadata_buffer + num_bytes_junk + num_bytes_header + num_bytes_file_size + num_bytes_file_deletion_date);
[[maybe_unused]] wchar_t *file_path_utf16 = reinterpret_cast<wchar_t * >(metadata_buffer + num_bytes_junk + num_bytes_header + num_bytes_file_size + num_bytes_file_deletion_date + num_bytes_file_path_length);
if (!CreateHardLinkW(file_path_utf16, recycle_bin_hardlink_path_utf16, NULL)) {
return retval;
}
retval.step3_new_hardlink_created = true;
retval.step4_old_hardlink_deleted = DeleteFileW(recycle_bin_hardlink_path_utf16);
if (CloseHandle(metadata_file_handle)) {
metadata_file_handle = INVALID_HANDLE_VALUE;
}
retval.step5_metadata_file_deleted = DeleteFileW(recycle_bin_metadata_path_utf16);
return retval;
}
Here's the code to restore a directory:
(Ignore the init_done_mutex
, init_done_cond
, init_done
variables in perform_undelete_directory
as I am simply copy pasting the code from my application.)
// OPTIONAL
struct undelete_directory_progress_sink : public IFileOperationProgressSink {
// do whatever you want here.
};
void perform_undelete_directory(
swan_path directory_path_in_recycle_bin_utf8,
swan_path destination_dir_path_utf8,
swan_path destination_full_path_utf8,
std::mutex *init_done_mutex,
std::condition_variable *init_done_cond,
bool *init_done,
std::string *init_error) noexcept
{
auto set_init_error_and_notify = [&](std::string const &err) noexcept {
std::unique_lock lock(*init_done_mutex);
*init_done = true;
*init_error = err;
init_done_cond->notify_one();
};
wchar_t directory_path_in_recycle_bin_utf16[MAX_PATH];
if (!utf8_to_utf16(directory_path_in_recycle_bin_utf8.data(), directory_path_in_recycle_bin_utf16, lengthof(directory_path_in_recycle_bin_utf16))) {
return set_init_error_and_notify("Conversion of directory path (in recycle bin) from UTF-8 to UTF-16.");
}
{
auto begin = directory_path_in_recycle_bin_utf16;
auto end = directory_path_in_recycle_bin_utf16 + wcslen(directory_path_in_recycle_bin_utf16);
std::replace(begin, end, L'/', L'\\');
}
wchar_t destination_dir_path_utf16[MAX_PATH];
if (!utf8_to_utf16(destination_dir_path_utf8.data(), destination_dir_path_utf16, lengthof(destination_dir_path_utf16))) {
return set_init_error_and_notify("Conversion of destination path from UTF-8 to UTF-16.");
}
{
auto begin = destination_dir_path_utf16;
auto end = destination_dir_path_utf16 + wcslen(destination_dir_path_utf16);
std::replace(begin, end, L'/', L'\\');
}
HRESULT result = {};
result = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(result)) {
return set_init_error_and_notify(make_str("CoInitializeEx(COINIT_APARTMENTTHREADED), %s", _com_error(result).ErrorMessage()));
}
SCOPE_EXIT { CoUninitialize(); };
IFileOperation *file_op = nullptr;
result = CoCreateInstance(CLSID_FileOperation, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&file_op));
if (FAILED(result)) {
return set_init_error_and_notify(make_str("CoCreateInstance(CLSID_FileOperation), %s", _com_error(result).ErrorMessage()));
}
SCOPE_EXIT { file_op->Release(); };
#if 0
result = file_op->SetOperationFlags(FOF_NOCONFIRMATION);
if (FAILED(result)) {
return set_init_error_and_notify(make_str("IFileOperation::SetOperationFlags, %s", _com_error(result).ErrorMessage()));
}
#endif
IShellItem *item_to_restore = nullptr;
result = SHCreateItemFromParsingName(directory_path_in_recycle_bin_utf16, nullptr, IID_PPV_ARGS(&item_to_restore));
if (FAILED(result)) {
return set_init_error_and_notify(make_str("SHCreateItemFromParsingName, %s", _com_error(result).ErrorMessage()));
}
SCOPE_EXIT { item_to_restore->Release(); };
IShellItem *destination = nullptr;
result = SHCreateItemFromParsingName(destination_dir_path_utf16, nullptr, IID_PPV_ARGS(&destination));
if (FAILED(result)) {
return set_init_error_and_notify(make_str("SHCreateItemFromParsingName, %s", _com_error(result).ErrorMessage()));
}
SCOPE_EXIT { destination->Release(); };
undelete_directory_progress_sink prog_sink; // OPTIONAL
DWORD cookie = {};
wchar_t const *restored_name = path_cfind_filename(destination_dir_path_utf16);
result = file_op->MoveItem(item_to_restore, destination, restored_name, &prog_sink);
if (FAILED(result)) {
return set_init_error_and_notify(make_str("FAILED IFileOperation::MoveItem, %s", _com_error(result).ErrorMessage()));
}
result = file_op->Advise(&prog_sink, &cookie); // OPTIONAL
if (FAILED(result)) {
return set_init_error_and_notify(make_str("FAILED IFileOperation::Advise, %s", _com_error(result).ErrorMessage()));
}
set_init_error_and_notify(""); // init succeeded, no error
result = file_op->PerformOperations();
if (FAILED(result)) {
_com_error err(result);
print_debug_msg("FAILED IFileOperation::PerformOperations, %s", err.ErrorMessage());
}
file_op->Unadvise(cookie); // OPTIONAL
if (FAILED(result)) {
print_debug_msg("FAILED IFileOperation::Unadvise(%d), %s", cookie, _com_error(result).ErrorMessage());
}
}
Definition for SCOPE_EXIT
macro, from CppCon 2015: Andrei Alexandrescu “Declarative Control Flow":
template <typename Fun>
class ScopeGuard {
public:
Fun m_fn;
ScopeGuard(Fun&& fn) : m_fn(fn) {}
~ScopeGuard() { m_fn(); }
};
namespace detail {
enum class ScopeGuardOnExit {};
template <typename Fun>
ScopeGuard<Fun> operator+(ScopeGuardOnExit, Fun&& fn) {
return ScopeGuard<Fun>(std::forward<Fun>(fn));
}
}
#define CONCATENATE_IMPL(s1, s2) s1##s2
#define CONCATENATE(s1, s2) CONCATENATE_IMPL(s1, s2)
#ifdef __COUNTER__
#define ANONYMOUS_VARIABLE(str) CONCATENATE(str, __COUNTER__)
#define ANON_VAR(str) CONCATENATE(str, __COUNTER__)
#else
#define ANONYMOUS_VARIABLE(str) CONCATENATE(str, __LINE__)
#define ANON_VAR(str) CONCATENATE(str, __LINE__)
#endif
#define SCOPE_EXIT \
auto ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE) = ::detail::ScopeGuardOnExit() + [&]()