diff options
Diffstat (limited to 'zen/IFileOperation/file_op.cpp')
-rw-r--r-- | zen/IFileOperation/file_op.cpp | 241 |
1 files changed, 209 insertions, 32 deletions
diff --git a/zen/IFileOperation/file_op.cpp b/zen/IFileOperation/file_op.cpp index 8acc3d17..3591de12 100644 --- a/zen/IFileOperation/file_op.cpp +++ b/zen/IFileOperation/file_op.cpp @@ -13,6 +13,7 @@ #include <zen/com_ptr.h> #include <zen/com_error.h> #include <zen/scope_guard.h> +#include <zen/stl_tools.h> #include <boost/thread/tss.hpp> @@ -29,8 +30,140 @@ using namespace zen; namespace { -void moveToRecycleBin(const wchar_t* fileNames[], //throw ComError - size_t fileNo) //size of fileNames array +struct Win32Error +{ + Win32Error(DWORD errorCode) : errorCode_(errorCode) {} + DWORD errorCode_; +}; + +std::vector<std::wstring> getLockingProcesses(const wchar_t* filename); //throw Win32Error + + +class RecyclerProgressCallback : public IFileOperationProgressSink +{ + //Sample implementation: %ProgramFiles%\Microsoft SDKs\Windows\v7.1\Samples\winui\shell\appplatform\FileOperationProgressSink + + ~RecyclerProgressCallback() {} //private: do not allow stack usage "thanks" to IUnknown lifetime management! + +public: + RecyclerProgressCallback(fileop::RecyclerCallback callback, void* sink) : + cancellationRequested(false), + callback_(callback), + sink_(sink), + refCount(1) {} + + //IUnknown: reference implementation according to: http://msdn.microsoft.com/en-us/library/office/cc839627.aspx + virtual ULONG STDMETHODCALLTYPE AddRef() + { + return ::InterlockedIncrement(&refCount); + } + + virtual ULONG STDMETHODCALLTYPE Release() + { + ULONG newRefCount = ::InterlockedDecrement(&refCount); + if (newRefCount == 0) //race condition caveat: do NOT check refCount, which might have changed already! + delete this; + return newRefCount; + } + + virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void __RPC_FAR* __RPC_FAR* ppvObject) + { + if (!ppvObject) + return E_INVALIDARG; + + if (riid == IID_IUnknown || riid == IID_IFileOperationProgressSink) + { + *ppvObject = this; + AddRef(); + return S_OK; + } + *ppvObject = NULL; + return E_NOINTERFACE; + } + + //IFileOperationProgressSink + virtual HRESULT STDMETHODCALLTYPE StartOperations() { return S_OK; } + virtual HRESULT STDMETHODCALLTYPE FinishOperations(HRESULT hrResult) { return S_OK; } + virtual HRESULT STDMETHODCALLTYPE PreRenameItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiItem, __RPC__in_opt_string LPCWSTR pszNewName) { return S_OK; } + virtual HRESULT STDMETHODCALLTYPE PostRenameItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiItem, __RPC__in_string LPCWSTR pszNewName, HRESULT hrRename, __RPC__in_opt IShellItem* psiNewlyCreated) { return S_OK; } + virtual HRESULT STDMETHODCALLTYPE PreMoveItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiItem, __RPC__in_opt IShellItem* psiDestinationFolder, __RPC__in_opt_string LPCWSTR pszNewName) { return S_OK; } + virtual HRESULT STDMETHODCALLTYPE PostMoveItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiItem, __RPC__in_opt IShellItem* psiDestinationFolder, __RPC__in_opt_string LPCWSTR pszNewName, HRESULT hrMove, __RPC__in_opt IShellItem* psiNewlyCreated) { return S_OK; } + virtual HRESULT STDMETHODCALLTYPE PreCopyItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiItem, __RPC__in_opt IShellItem* psiDestinationFolder, __RPC__in_opt_string LPCWSTR pszNewName) { return S_OK; } + virtual HRESULT STDMETHODCALLTYPE PostCopyItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiItem, __RPC__in_opt IShellItem* psiDestinationFolder, __RPC__in_opt_string LPCWSTR pszNewName, HRESULT hrCopy, __RPC__in_opt IShellItem* psiNewlyCreated) { return S_OK; } + virtual HRESULT STDMETHODCALLTYPE PreNewItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiDestinationFolder, __RPC__in_opt_string LPCWSTR pszNewName) { return S_OK; } + virtual HRESULT STDMETHODCALLTYPE PostNewItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiDestinationFolder, __RPC__in_opt_string LPCWSTR pszNewName, __RPC__in_opt_string LPCWSTR pszTemplateName, DWORD dwFileAttributes, HRESULT hrNew, __RPC__in_opt IShellItem* psiNewItem) { return S_OK; } + + virtual HRESULT STDMETHODCALLTYPE PreDeleteItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiItem) + { + if (psiItem) + { + LPWSTR itemPath = nullptr; + HRESULT hr = psiItem->GetDisplayName(SIGDN_FILESYSPATH, &itemPath); + if (FAILED(hr)) + return hr; + ZEN_ON_SCOPE_EXIT(::CoTaskMemFree(itemPath)); + + currentItem = itemPath; + } + //"Returns S_OK if successful, or an error value otherwise. In the case of an error value, the delete operation + //and all subsequent operations pending from the call to IFileOperation are canceled." + return cancellationRequested ? HRESULT_FROM_WIN32(ERROR_CANCELLED) : S_OK; + } + + virtual HRESULT STDMETHODCALLTYPE PostDeleteItem(DWORD dwFlags, + __RPC__in_opt IShellItem* psiItem, + HRESULT hrDelete, + __RPC__in_opt IShellItem* psiNewlyCreated) + { + if (FAILED(hrDelete)) + lastError = make_unique<std::pair<std::wstring, HRESULT>>(currentItem, hrDelete); + + currentItem.clear(); + //"Returns S_OK if successful, or an error value otherwise. In the case of an error value, + //all subsequent operations pending from the call to IFileOperation are canceled." + return cancellationRequested ? HRESULT_FROM_WIN32(ERROR_CANCELLED) : S_OK; + } + + virtual HRESULT STDMETHODCALLTYPE UpdateProgress(UINT iWorkTotal, UINT iWorkSoFar) + { + if (callback_) + try + { + if (!callback_(currentItem.c_str(), sink_)) //should not throw! + cancellationRequested = true; + } + catch (...) { return E_UNEXPECTED; } + //"If this method succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code." + //-> this probably means, we cannot rely on returning a custom error code here and have IFileOperation::PerformOperations() fail with same + //=> defer cancellation to PreDeleteItem()/PostDeleteItem() + return S_OK; + } + virtual HRESULT STDMETHODCALLTYPE ResetTimer() { return S_OK; } + virtual HRESULT STDMETHODCALLTYPE PauseTimer() { return S_OK; } + virtual HRESULT STDMETHODCALLTYPE ResumeTimer() { return S_OK; } + + //call after IFileOperation::PerformOperations() + const std::pair<std::wstring, HRESULT>* getLastError() const { return lastError.get(); } //(file path, error code) + +private: + std::wstring currentItem; + bool cancellationRequested; + + std::unique_ptr<std::pair<std::wstring, HRESULT>> lastError; + + //file_op user callback + fileop::RecyclerCallback callback_; + void* sink_; + + //support IUnknown + LONG refCount; +}; + + +void moveToRecycleBin(const wchar_t* fileNames[], //throw ComError + size_t fileCount, + fileop::RecyclerCallback callback, + void* sink) { ComPtr<IFileOperation> fileOp; ZEN_CHECK_COM(::CoCreateInstance(CLSID_FileOperation, //throw ComError @@ -38,21 +171,48 @@ void moveToRecycleBin(const wchar_t* fileNames[], //throw ComError CLSCTX_ALL, IID_PPV_ARGS(fileOp.init()))); - // Set the operation flags. Turn off all UI - // from being shown to the user during the - // operation. This includes error, confirmation - // and progress dialogs. - ZEN_CHECK_COM(fileOp->SetOperationFlags(FOF_ALLOWUNDO | //throw ComError + // Set the operation flags. Turn off all UI from being shown to the user during the + // operation. This includes error, confirmation and progress dialogs. + ZEN_CHECK_COM(fileOp->SetOperationFlags(FOF_ALLOWUNDO | FOF_NOCONFIRMATION | - FOF_SILENT | + FOF_SILENT | //no progress dialog box FOF_NOERRORUI | - FOFX_EARLYFAILURE | + FOFX_EARLYFAILURE | + //without FOFX_EARLYFAILURE, IFileOperationProgressSink::PostDeleteItem() will always report success, even if deletion failed!!? WTF!? + //PerformOperations() will still succeed but set the uselessly generic GetAnyOperationsAborted() instead :((( + //=> always set FOFX_EARLYFAILURE since we prefer good error messages over "doing as much as possible" + //luckily for FreeFileSync we don't expect failures on individual files anyway: FreeFileSync moves files to be + //deleted to a temporary folder first, so there is no reason why a second move (the recycling itself) should fail FOF_NO_CONNECTED_ELEMENTS)); + //use FOFX_RECYCLEONDELETE when Windows 8 is available!? + + ComPtr<RecyclerProgressCallback> opProgress; + *opProgress.init() = new (std::nothrow) RecyclerProgressCallback(callback, sink); + if (!opProgress) + throw ComError(L"Error creating RecyclerProgressCallback.", E_OUTOFMEMORY); + + DWORD callbackID = 0; + ZEN_CHECK_COM(fileOp->Advise(opProgress.get(), &callbackID)); + ZEN_ON_SCOPE_EXIT(fileOp->Unadvise(callbackID)); //RecyclerProgressCallback might outlive current scope, so cut access to "callback, sink" int operationCount = 0; - for (size_t i = 0; i < fileNo; ++i) + for (size_t i = 0; i < fileCount; ++i) { + //SHCreateItemFromParsingName() physically checks file existence => callback + if (callback) + { + bool continueExecution = false; + try + { + continueExecution = callback(fileNames[i], sink); //should not throw! + } + catch (...) { throw ComError(L"Unexpected exception in callback.", E_UNEXPECTED); } + + if (!continueExecution) + throw ComError(L"Operation cancelled.", HRESULT_FROM_WIN32(ERROR_CANCELLED)); + } + //create file/folder item object ComPtr<IShellItem> psiFile; HRESULT hr = ::SHCreateItemFromParsingName(fileNames[i], @@ -63,7 +223,7 @@ void moveToRecycleBin(const wchar_t* fileNames[], //throw ComError if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || //file not existing anymore hr == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)) continue; - throw ComError(std::wstring(L"Error calling \"SHCreateItemFromParsingName\" for file:\n") + L"\"" + fileNames[i] + L"\".", hr); + throw ComError(std::wstring(L"Error calling \"SHCreateItemFromParsingName\" for file:\n") + L"\'" + fileNames[i] + L"\'.", hr); } ZEN_CHECK_COM(fileOp->DeleteItem(psiFile.get(), nullptr)); @@ -71,13 +231,37 @@ void moveToRecycleBin(const wchar_t* fileNames[], //throw ComError ++operationCount; } - if (operationCount == 0) //calling PerformOperations() without anything to do would result in E_UNEXPECTED + if (operationCount == 0) //calling PerformOperations() without anything to do would yielt E_UNEXPECTED return; - //perform actual operations - ZEN_CHECK_COM(fileOp->PerformOperations()); + //perform planned operations + try + { + ZEN_CHECK_COM(fileOp->PerformOperations()); + } + catch (const ComError&) + { + //first let's check if we have more detailed error information available + if (const std::pair<std::wstring, HRESULT>* lastError = opProgress->getLastError()) + { + try //create an even better error message if we detect a locking issue: + { + std::vector<std::wstring> processes = getLockingProcesses(lastError->first.c_str()); //throw Win32Error + if (!processes.empty()) + { + std::wstring msg = L"The file \'" + lastError->first + L"\' is locked by another process:"; + std::for_each(processes.begin(), processes.end(), [&](const std::wstring& proc) { msg += L'\n'; msg += proc; }); + throw ComError(msg); //message is already descriptive enough, no need to add the HRESULT code + } + } + catch (const Win32Error&) {} - //check if errors occured: if FOFX_EARLYFAILURE is not used, PerformOperations() can return with success despite errors! + throw ComError(std::wstring(L"Error during \"PerformOperations\" for file:\n") + L"\'" + lastError->first + L"\'.", lastError->second); + } + throw; + } + + //if FOF_NOERRORUI without FOFX_EARLYFAILURE is set, PerformOperations() can return with success despite errors, but sets the following "aborted" flag instead BOOL pfAnyOperationsAborted = FALSE; ZEN_CHECK_COM(fileOp->GetAnyOperationsAborted(&pfAnyOperationsAborted)); @@ -110,7 +294,7 @@ void copyFile(const wchar_t* sourceFile, //throw ComError nullptr, IID_PPV_ARGS(psiSourceFile.init())); if (FAILED(hr)) - throw ComError(std::wstring(L"Error calling \"SHCreateItemFromParsingName\" for file:\n") + L"\"" + sourceFile + L"\".", hr); + throw ComError(std::wstring(L"Error calling \"SHCreateItemFromParsingName\" for file:\n") + L"\'" + sourceFile + L"\'.", hr); } const size_t pos = std::wstring(targetFile).find_last_of(L'\\'); @@ -127,7 +311,7 @@ void copyFile(const wchar_t* sourceFile, //throw ComError nullptr, IID_PPV_ARGS(psiTargetFolder.init())); if (FAILED(hr)) - throw ComError(std::wstring(L"Error calling \"SHCreateItemFromParsingName\" for folder:\n") + L"\"" + targetFolder + L"\".", hr); + throw ComError(std::wstring(L"Error calling \"SHCreateItemFromParsingName\" for folder:\n") + L"\'" + targetFolder + L"\'.", hr); } //schedule file copy operation @@ -168,12 +352,6 @@ void getFolderClsid(const wchar_t* dirname, CLSID& pathCLSID) //throw ComError } -struct Win32Error -{ - Win32Error(DWORD errorCode) : errorCode_(errorCode) {} - DWORD errorCode_; -}; - std::vector<std::wstring> getLockingProcesses(const wchar_t* filename) //throw Win32Error { wchar_t sessionKey[CCH_RM_SESSION_KEY + 1] = {}; //fixes two bugs: http://blogs.msdn.com/b/oldnewthing/archive/2012/02/17/10268840.aspx @@ -260,11 +438,14 @@ boost::thread_specific_ptr<std::wstring> lastErrorMessage; //use "thread_local" } -bool fileop::moveToRecycleBin(const wchar_t* fileNames[], size_t fileNo) //size of fileNames array +bool fileop::moveToRecycleBin(const wchar_t* fileNames[], + size_t fileCount, + RecyclerCallback callback, + void* sink) { try { - ::moveToRecycleBin(fileNames, fileNo); //throw ComError + ::moveToRecycleBin(fileNames, fileCount, callback, sink); //throw ComError return true; } catch (const ComError& e) @@ -318,15 +499,11 @@ bool fileop::getLockingProcesses(const wchar_t* filename, const wchar_t*& procLi { try { - std::vector<std::wstring> result = ::getLockingProcesses(filename); //throw Win32Error + std::vector<std::wstring> processes = ::getLockingProcesses(filename); //throw Win32Error std::wstring buffer; - for (auto iter = result.begin(); iter != result.end(); ++iter) - { - buffer += *iter; - buffer += L'\n'; - } - if (!buffer.empty()) + std::for_each(processes.begin(), processes.end(), [&](const std::wstring& proc) { buffer += proc; buffer += L'\n'; }); + if (!processes.empty()) buffer.resize(buffer.size() - 1); //remove last line break auto tmp = new wchar_t [buffer.size() + 1]; //bad_alloc ? |