Search code examples
c++winapirecycle-bin

How to programatically restore a recycle bin item


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`

Solution

  • 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() + [&]()