| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/download/public/common/base_file.h" |
| |
| #include <objbase.h> |
| |
| #include <shobjidl.h> |
| #include <windows.h> |
| |
| #include <shellapi.h> |
| #include <wrl/client.h> |
| #include <wrl/implements.h> |
| |
| #include "base/threading/scoped_blocking_call.h" |
| #include "base/win/com_init_util.h" |
| #include "components/download/public/common/download_interrupt_reasons_utils.h" |
| #include "components/download/public/common/download_stats.h" |
| |
| namespace download { |
| namespace { |
| |
| // By and large the errors seen here are listed in sherrors.h, included from |
| // shobjidl.h. |
| DownloadInterruptReason HRESULTToDownloadInterruptReason(HRESULT hr) { |
| // S_OK, other success values are aggregated here. |
| if (SUCCEEDED(hr) && HRESULT_FACILITY(hr) != FACILITY_SHELL) |
| return DOWNLOAD_INTERRUPT_REASON_NONE; |
| |
| DownloadInterruptReason reason = DOWNLOAD_INTERRUPT_REASON_NONE; |
| // All of the remaining HRESULTs to be considered are either from the copy |
| // engine, or are unknown; we've got handling for all the copy engine errors, |
| // and otherwise we'll just return the generic error reason. |
| switch (hr) { |
| case COPYENGINE_S_YES: |
| case COPYENGINE_S_NOT_HANDLED: |
| case COPYENGINE_S_USER_RETRY: |
| case COPYENGINE_S_MERGE: |
| case COPYENGINE_S_DONT_PROCESS_CHILDREN: |
| case COPYENGINE_S_ALREADY_DONE: |
| case COPYENGINE_S_PENDING: |
| case COPYENGINE_S_KEEP_BOTH: |
| case COPYENGINE_S_COLLISIONRESOLVED: |
| case COPYENGINE_S_PROGRESS_PAUSE: |
| return DOWNLOAD_INTERRUPT_REASON_NONE; |
| |
| case COPYENGINE_S_CLOSE_PROGRAM: |
| // Like sharing violations, another process is using the file we want to |
| // touch, so wait for it to close. |
| case COPYENGINE_E_SHARING_VIOLATION_SRC: |
| case COPYENGINE_E_SHARING_VIOLATION_DEST: |
| // Sharing violations are encountered when some other process has a file |
| // open; often it's antivirus scanning, and this error can be treated as |
| // transient, as we assume eventually the other process will close its |
| // handle. |
| reason = DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR; |
| break; |
| |
| case COPYENGINE_E_PATH_TOO_DEEP_DEST: |
| case COPYENGINE_E_PATH_TOO_DEEP_SRC: |
| case COPYENGINE_E_NEWFILE_NAME_TOO_LONG: |
| case COPYENGINE_E_NEWFOLDER_NAME_TOO_LONG: |
| // Any of these errors can be encountered if MAXPATH is hit while writing |
| // out a filename. This can happen really just about anywhere. |
| reason = DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG; |
| break; |
| |
| case COPYENGINE_S_USER_IGNORED: |
| // On Windows 7, inability to access a file may return "user ignored" |
| // instead of correctly reporting the failure. |
| case COPYENGINE_E_ACCESS_DENIED_DEST: |
| case COPYENGINE_E_ACCESS_DENIED_SRC: |
| // There's a security problem, or the file is otherwise inaccessible. |
| case COPYENGINE_E_DEST_IS_RO_CD: |
| case COPYENGINE_E_DEST_IS_RW_CD: |
| case COPYENGINE_E_DEST_IS_R_CD: |
| case COPYENGINE_E_DEST_IS_RO_DVD: |
| case COPYENGINE_E_DEST_IS_RW_DVD: |
| case COPYENGINE_E_DEST_IS_R_DVD: |
| case COPYENGINE_E_SRC_IS_RO_CD: |
| case COPYENGINE_E_SRC_IS_RW_CD: |
| case COPYENGINE_E_SRC_IS_R_CD: |
| case COPYENGINE_E_SRC_IS_RO_DVD: |
| case COPYENGINE_E_SRC_IS_RW_DVD: |
| case COPYENGINE_E_SRC_IS_R_DVD: |
| // When the source is actually a disk, and a Move is attempted, it can't |
| // delete the source. This is unlikely to be encountered in our scenario. |
| reason = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED; |
| break; |
| |
| case COPYENGINE_E_FILE_TOO_LARGE: |
| case COPYENGINE_E_DISK_FULL: |
| case COPYENGINE_E_REMOVABLE_FULL: |
| case COPYENGINE_E_DISK_FULL_CLEAN: |
| // No room for the file in the destination location. |
| reason = DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE; |
| break; |
| |
| case COPYENGINE_E_ALREADY_EXISTS_NORMAL: |
| case COPYENGINE_E_ALREADY_EXISTS_READONLY: |
| case COPYENGINE_E_ALREADY_EXISTS_SYSTEM: |
| case COPYENGINE_E_ALREADY_EXISTS_FOLDER: |
| // The destination already exists and can't be replaced. |
| case COPYENGINE_E_INVALID_FILES_SRC: |
| case COPYENGINE_E_INVALID_FILES_DEST: |
| // Either the source or destination file was invalid. |
| case COPYENGINE_E_STREAM_LOSS: |
| case COPYENGINE_E_EA_LOSS: |
| case COPYENGINE_E_PROPERTY_LOSS: |
| case COPYENGINE_E_PROPERTIES_LOSS: |
| case COPYENGINE_E_ENCRYPTION_LOSS: |
| // The destination can't support some functionality that the file needs. |
| // The interesting one here is E_STREAM_LOSS, especially with MOTW. |
| case COPYENGINE_E_FLD_IS_FILE_DEST: |
| case COPYENGINE_E_FILE_IS_FLD_DEST: |
| // There is an existing file with the same name as a new folder, and |
| // vice versa. |
| case COPYENGINE_E_ROOT_DIR_DEST: |
| case COPYENGINE_E_ROOT_DIR_SRC: |
| case COPYENGINE_E_DIFF_DIR: |
| case COPYENGINE_E_SAME_FILE: |
| case COPYENGINE_E_MANY_SRC_1_DEST: |
| case COPYENGINE_E_DEST_SUBTREE: |
| case COPYENGINE_E_DEST_SAME_TREE: |
| case COPYENGINE_E_USER_CANCELLED: |
| case COPYENGINE_E_CANCELLED: |
| case COPYENGINE_E_REQUIRES_ELEVATION: |
| reason = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; |
| break; |
| } |
| |
| if (reason != DOWNLOAD_INTERRUPT_REASON_NONE) { |
| return reason; |
| } |
| |
| // Copy operations may still return Win32 error codes, so handle those here. |
| if (HRESULT_FACILITY(hr) == FACILITY_WIN32) { |
| return ConvertFileErrorToInterruptReason( |
| base::File::OSErrorToFileError(HRESULT_CODE(hr))); |
| } |
| |
| return DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; |
| } |
| |
| class FileOperationProgressSink |
| : public Microsoft::WRL::RuntimeClass< |
| Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, |
| IFileOperationProgressSink> { |
| public: |
| FileOperationProgressSink() = default; |
| |
| FileOperationProgressSink(const FileOperationProgressSink&) = delete; |
| FileOperationProgressSink& operator=(const FileOperationProgressSink&) = |
| delete; |
| |
| HRESULT GetOperationResult() { return result_; } |
| |
| // IFileOperationProgressSink: |
| IFACEMETHODIMP FinishOperations(HRESULT hr) override { |
| // If a failure has already been captured, don't bother overriding it. That |
| // way, the original failure can be propagated; in the event that the new |
| // HRESULT is also a success, overwriting will not harm anything and |
| // captures the final state of the whole operation. |
| if (SUCCEEDED(result_)) |
| result_ = hr; |
| return S_OK; |
| } |
| |
| IFACEMETHODIMP PauseTimer() override { return S_OK; } |
| IFACEMETHODIMP PostCopyItem(DWORD, |
| IShellItem*, |
| IShellItem*, |
| PCWSTR, |
| HRESULT, |
| IShellItem*) override { |
| return E_NOTIMPL; |
| } |
| IFACEMETHODIMP PostDeleteItem(DWORD, |
| IShellItem*, |
| HRESULT, |
| IShellItem*) override { |
| return E_NOTIMPL; |
| } |
| IFACEMETHODIMP PostMoveItem(DWORD, |
| IShellItem*, |
| IShellItem*, |
| PCWSTR, |
| HRESULT hr, |
| IShellItem*) override { |
| // Like in FinishOperations, overwriting with a different success value |
| // does not have a negative impact, but replacing an existing failure will |
| // cause issues. |
| if (SUCCEEDED(result_)) |
| result_ = hr; |
| return S_OK; |
| } |
| IFACEMETHODIMP PostNewItem(DWORD, |
| IShellItem*, |
| PCWSTR, |
| PCWSTR, |
| DWORD, |
| HRESULT, |
| IShellItem*) override { |
| return E_NOTIMPL; |
| } |
| IFACEMETHODIMP |
| PostRenameItem(DWORD, IShellItem*, PCWSTR, HRESULT, IShellItem*) override { |
| return E_NOTIMPL; |
| } |
| IFACEMETHODIMP PreCopyItem(DWORD, IShellItem*, IShellItem*, PCWSTR) override { |
| return E_NOTIMPL; |
| } |
| IFACEMETHODIMP PreDeleteItem(DWORD, IShellItem*) override { |
| return E_NOTIMPL; |
| } |
| IFACEMETHODIMP PreMoveItem(DWORD, IShellItem*, IShellItem*, PCWSTR) override { |
| return S_OK; |
| } |
| IFACEMETHODIMP PreNewItem(DWORD, IShellItem*, PCWSTR) override { |
| return E_NOTIMPL; |
| } |
| IFACEMETHODIMP PreRenameItem(DWORD, IShellItem*, PCWSTR) override { |
| return E_NOTIMPL; |
| } |
| IFACEMETHODIMP ResetTimer() override { return S_OK; } |
| IFACEMETHODIMP ResumeTimer() override { return S_OK; } |
| IFACEMETHODIMP StartOperations() override { return S_OK; } |
| IFACEMETHODIMP UpdateProgress(UINT, UINT) override { return S_OK; } |
| |
| protected: |
| ~FileOperationProgressSink() override = default; |
| |
| private: |
| HRESULT result_ = S_OK; |
| }; |
| |
| } // namespace |
| |
| // Renames a file using IFileOperation::MoveItem() to ensure that the target |
| // file gets the correct default security descriptor in the new path. |
| // Returns a network error, or net::OK for success. |
| DownloadInterruptReason BaseFile::MoveFileAndAdjustPermissions( |
| const base::FilePath& new_path) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| |
| base::win::AssertComInitialized(); |
| Microsoft::WRL::ComPtr<IShellItem> original_path; |
| HRESULT hr = SHCreateItemFromParsingName(full_path_.value().c_str(), nullptr, |
| IID_PPV_ARGS(&original_path)); |
| |
| // |new_path| can be broken down to provide the new folder, as well as the |
| // new filename. We'll start with the folder, which the caller should ensure |
| // exists. |
| Microsoft::WRL::ComPtr<IShellItem> destination_folder; |
| if (SUCCEEDED(hr)) { |
| hr = |
| SHCreateItemFromParsingName(new_path.DirName().value().c_str(), nullptr, |
| IID_PPV_ARGS(&destination_folder)); |
| } |
| |
| Microsoft::WRL::ComPtr<IFileOperation> file_operation; |
| if (SUCCEEDED(hr)) { |
| hr = CoCreateInstance(CLSID_FileOperation, nullptr, CLSCTX_INPROC_SERVER, |
| IID_PPV_ARGS(&file_operation)); |
| } |
| |
| if (SUCCEEDED(hr)) { |
| // Don't show any UI, don't migrate security attributes (use the |
| // destination's attributes), and stop on first error-retaining the original |
| // failure reason. |
| hr = file_operation->SetOperationFlags( |
| FOF_NO_UI | FOF_NOCOPYSECURITYATTRIBS | FOFX_EARLYFAILURE); |
| } |
| |
| Microsoft::WRL::ComPtr<FileOperationProgressSink> sink = |
| Microsoft::WRL::Make<FileOperationProgressSink>(); |
| if (SUCCEEDED(hr)) { |
| hr = file_operation->MoveItem(original_path.Get(), destination_folder.Get(), |
| new_path.BaseName().value().c_str(), |
| sink.Get()); |
| } |
| |
| if (SUCCEEDED(hr)) |
| hr = file_operation->PerformOperations(); |
| |
| if (SUCCEEDED(hr)) |
| hr = sink->GetOperationResult(); |
| |
| // Convert HRESULT to DownloadInterruptReason. |
| DownloadInterruptReason interrupt_reason = |
| HRESULTToDownloadInterruptReason(hr); |
| |
| if (interrupt_reason == DOWNLOAD_INTERRUPT_REASON_NONE) { |
| // The operation could still have been aborted; we can't get a better reason |
| // at this point, but we've got more information to go by. |
| BOOL any_operations_aborted = TRUE; |
| file_operation->GetAnyOperationsAborted(&any_operations_aborted); |
| if (any_operations_aborted) |
| interrupt_reason = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; |
| } else { |
| return LogInterruptReason("IFileOperation::MoveItem", hr, interrupt_reason); |
| } |
| |
| return interrupt_reason; |
| } |
| |
| } // namespace download |