| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ash/file_manager/copy_or_move_io_task_impl.h" |
| |
| #include <cmath> |
| #include <cstdint> |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "ash/constants/ash_features.h" |
| #include "base/check_op.h" |
| #include "base/files/file.h" |
| #include "base/files/file_error_or.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/notreached.h" |
| #include "base/system/sys_info.h" |
| #include "base/task/bind_post_task.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/thread_pool.h" |
| #include "chrome/browser/ash/drive/drive_integration_service.h" |
| #include "chrome/browser/ash/drive/file_system_util.h" |
| #include "chrome/browser/ash/file_manager/file_manager_copy_or_move_hook_delegate.h" |
| #include "chrome/browser/ash/file_manager/file_tasks.h" |
| #include "chrome/browser/ash/file_manager/fileapi_util.h" |
| #include "chrome/browser/ash/file_manager/filesystem_api_util.h" |
| #include "chrome/browser/ash/file_manager/io_task.h" |
| #include "chrome/browser/ash/file_manager/io_task_util.h" |
| #include "chrome/browser/ash/file_manager/path_util.h" |
| #include "chrome/browser/ash/file_manager/volume_manager.h" |
| #include "chrome/browser/policy/profile_policy_connector.h" |
| #include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.h" |
| #include "chromeos/ash/components/browser_context_helper/browser_context_helper.h" |
| #include "components/user_manager/user_manager.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "google_apis/common/task_util.h" |
| #include "storage/browser/file_system/file_system_context.h" |
| #include "storage/browser/file_system/file_system_operation.h" |
| #include "storage/browser/file_system/file_system_operation_runner.h" |
| #include "storage/browser/file_system/file_system_url.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "third_party/cros_system_api/constants/cryptohome.h" |
| |
| namespace file_manager::io_task { |
| |
| namespace { |
| |
| // Starts the copy operation via FileSystemOperationRunner. |
| storage::FileSystemOperationRunner::OperationID StartCopyOnIOThread( |
| scoped_refptr<storage::FileSystemContext> file_system_context, |
| const storage::FileSystemURL& source_url, |
| const storage::FileSystemURL& destination_url, |
| storage::FileSystemOperation::CopyOrMoveOptionSet options, |
| storage::FileSystemOperation::ErrorBehavior error_behavior, |
| std::unique_ptr<storage::CopyOrMoveHookDelegate> copy_or_move_hook_delegate, |
| storage::FileSystemOperation::StatusCallback complete_callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| return file_system_context->operation_runner()->Copy( |
| source_url, destination_url, options, error_behavior, |
| std::move(copy_or_move_hook_delegate), std::move(complete_callback)); |
| } |
| |
| // Starts the move operation via FileSystemOperationRunner. |
| storage::FileSystemOperationRunner::OperationID StartMoveOnIOThread( |
| scoped_refptr<storage::FileSystemContext> file_system_context, |
| const storage::FileSystemURL& source_url, |
| const storage::FileSystemURL& destination_url, |
| storage::FileSystemOperation::CopyOrMoveOptionSet options, |
| storage::FileSystemOperation::ErrorBehavior error_behavior, |
| std::unique_ptr<storage::CopyOrMoveHookDelegate> copy_or_move_hook_delegate, |
| storage::FileSystemOperation::StatusCallback complete_callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| return file_system_context->operation_runner()->Move( |
| source_url, destination_url, options, error_behavior, |
| std::move(copy_or_move_hook_delegate), std::move(complete_callback)); |
| } |
| |
| // Helper function for copy or move tasks that determines whether or not |
| // entries identified by their URLs should be considered as being on the |
| // different file systems or not. The entries are seen as being on different |
| // filesystems if either: |
| // - the entries are not on the same volume OR |
| // - one entry is in My files, and the other one in Downloads. |
| // crbug.com/1200251 |
| bool IsCrossFileSystem(Profile* profile, |
| const storage::FileSystemURL& source_url, |
| const storage::FileSystemURL& destination_url) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| file_manager::VolumeManager* const volume_manager = |
| file_manager::VolumeManager::Get(profile); |
| |
| base::WeakPtr<file_manager::Volume> source_volume = |
| volume_manager->FindVolumeFromPath(source_url.path()); |
| base::WeakPtr<file_manager::Volume> destination_volume = |
| volume_manager->FindVolumeFromPath(destination_url.path()); |
| |
| if (!(source_volume && destination_volume)) { |
| // When either volume is unavailable, fallback to only checking the |
| // filesystem_id, which uniquely maps a URL to its ExternalMountPoints |
| // instance. NOTE: different volumes (e.g. for removables), might share the |
| // same ExternalMountPoints. NOTE 2: if either volume is unavailable, the |
| // operation itself is likely to fail. |
| return source_url.filesystem_id() != destination_url.filesystem_id(); |
| } |
| |
| if (source_volume->volume_id() != destination_volume->volume_id()) { |
| return true; |
| } |
| |
| // On volumes other than DOWNLOADS, I/O operations within volumes that have |
| // the same ID are considered same-filesystem. |
| if (source_volume->type() != file_manager::VOLUME_TYPE_DOWNLOADS_DIRECTORY) { |
| return false; |
| } |
| |
| // The Downloads folder being bind mounted in My files, I/O operations within |
| // My files may need to be considered cross-filesystem (if one path is in |
| // Downloads and the other is not). |
| base::FilePath my_files_path = |
| file_manager::util::GetMyFilesFolderForProfile(profile); |
| base::FilePath downloads_path = my_files_path.Append("Downloads"); |
| |
| bool source_in_downloads = downloads_path.IsParent(source_url.path()); |
| // The destination_url can be the destination folder, so Downloads is a valid |
| // destination. |
| bool destination_in_downloads = |
| downloads_path == destination_url.path() || |
| downloads_path.IsParent(destination_url.path()); |
| return source_in_downloads != destination_in_downloads; |
| } |
| |
| } // namespace |
| |
| ItemProgress::ItemProgress() = default; |
| ItemProgress::~ItemProgress() = default; |
| |
| CopyOrMoveIOTaskImpl::CopyOrMoveIOTaskImpl( |
| OperationType type, |
| ProgressStatus& progress, |
| std::vector<base::FilePath> destination_file_names, |
| storage::FileSystemURL destination_folder, |
| Profile* profile, |
| scoped_refptr<storage::FileSystemContext> file_system_context, |
| bool show_notification) |
| : progress_(progress), |
| profile_(profile), |
| file_system_context_(file_system_context), |
| source_sizes_(progress_->sources.size()), |
| item_progresses(progress_->sources.size()) { |
| DCHECK(type == OperationType::kCopy || type == OperationType::kMove); |
| if (!destination_file_names.empty()) { |
| DCHECK_EQ(progress_->sources.size(), destination_file_names.size()); |
| } |
| destination_file_names_ = std::move(destination_file_names); |
| } |
| |
| CopyOrMoveIOTaskImpl::~CopyOrMoveIOTaskImpl() { |
| if (operation_id_) { |
| content::GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| [](scoped_refptr<storage::FileSystemContext> file_system_context, |
| storage::FileSystemOperationRunner::OperationID operation_id) { |
| file_system_context->operation_runner()->Cancel( |
| operation_id, base::DoNothing()); |
| }, |
| file_system_context_, *operation_id_)); |
| } |
| } |
| |
| // static |
| bool CopyOrMoveIOTaskImpl::IsCrossFileSystemForTesting( |
| Profile* profile, |
| const storage::FileSystemURL& source_url, |
| const storage::FileSystemURL& destination_url) { |
| return IsCrossFileSystem(profile, source_url, destination_url); |
| } |
| |
| void CopyOrMoveIOTaskImpl::Execute(IOTask::ProgressCallback progress_callback, |
| IOTask::CompleteCallback complete_callback) { |
| progress_callback_ = std::move(progress_callback); |
| complete_callback_ = std::move(complete_callback); |
| |
| if (progress_->sources.size() == 0) { |
| Complete(State::kSuccess); |
| return; |
| } |
| |
| VerifyTransfer(); |
| } |
| |
| void CopyOrMoveIOTaskImpl::VerifyTransfer() { |
| // TODO(b/280947989) remove this code once Multi-user sign-in is deprecated. |
| // Prevent files being copied or moved to ODFS if there is a managed user |
| // present amongst other logged in users. Ensures managed user's files can't |
| // be leaked to a non-managed user's ODFS b/278644796. |
| if (ash::cloud_upload::UrlIsOnODFS(profile_, |
| progress_->GetDestinationFolder())) { |
| // Check none of the logged in users are managed. |
| for (auto* user : user_manager::UserManager::Get()->GetLoggedInUsers()) { |
| Profile* user_profile = Profile::FromBrowserContext( |
| ash::BrowserContextHelper::Get()->GetBrowserContextByUser(user)); |
| if (user_profile->GetProfilePolicyConnector()->IsManaged()) { |
| Complete(State::kError); |
| return; |
| } |
| } |
| } |
| |
| StartTransfer(); |
| } |
| |
| void CopyOrMoveIOTaskImpl::StartTransfer() { |
| progress_->state = State::kInProgress; |
| |
| // Start the transfer by getting the file size. |
| for (size_t i = 0; i < progress_->sources.size(); i++) { |
| GetFileSize(i); |
| } |
| } |
| |
| void CopyOrMoveIOTaskImpl::Cancel() { |
| progress_->state = State::kCancelled; |
| // Any in-flight operation will be cancelled when the task is destroyed. |
| } |
| |
| // Calls the completion callback for the task. |progress_| should not be |
| // accessed after calling this. |
| void CopyOrMoveIOTaskImpl::Complete(State state) { |
| completed_ = true; |
| progress_->state = state; |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(complete_callback_), std::move(*progress_))); |
| } |
| |
| // Computes the total size of all source files and stores it in |
| // |progress_.total_bytes|. |
| void CopyOrMoveIOTaskImpl::GetFileSize(size_t idx) { |
| DCHECK(idx < progress_->sources.size()); |
| |
| const base::FilePath& source = progress_->sources[idx].url.path(); |
| const base::FilePath& destination = progress_->GetDestinationFolder().path(); |
| |
| constexpr auto metadata_fields = |
| storage::FileSystemOperation::GET_METADATA_FIELD_IS_DIRECTORY | |
| storage::FileSystemOperation::GET_METADATA_FIELD_SIZE | |
| storage::FileSystemOperation::GET_METADATA_FIELD_TOTAL_SIZE; |
| |
| auto get_metadata_callback = |
| base::BindOnce(&GetFileMetadataOnIOThread, file_system_context_, |
| progress_->sources[idx].url, metadata_fields, |
| google_apis::CreateRelayCallback( |
| base::BindOnce(&CopyOrMoveIOTaskImpl::GotFileSize, |
| weak_ptr_factory_.GetWeakPtr(), idx))); |
| |
| if (file_manager::util::IsDriveLocalPath(profile_, source) && |
| file_manager::file_tasks::IsOfficeFile(source) && |
| !file_manager::util::IsDriveLocalPath(profile_, destination)) { |
| if (progress_->type == OperationType::kCopy) { |
| UMA_HISTOGRAM_ENUMERATION( |
| file_manager::file_tasks::kUseOutsideDriveMetricName, |
| file_manager::file_tasks::OfficeFilesUseOutsideDriveHook::COPY); |
| } else { |
| UMA_HISTOGRAM_ENUMERATION( |
| file_manager::file_tasks::kUseOutsideDriveMetricName, |
| file_manager::file_tasks::OfficeFilesUseOutsideDriveHook::MOVE); |
| } |
| auto* drive_service = drive::util::GetIntegrationServiceByProfile(profile_); |
| if (drive_service) { |
| drive_service->ForceReSyncFile( |
| source, |
| base::BindPostTask(content::GetIOThreadTaskRunner({}), |
| std::move(get_metadata_callback), FROM_HERE)); |
| return; |
| } |
| // If there is no Drive connection, we should continue as normal. |
| } |
| |
| content::GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, std::move(get_metadata_callback)); |
| } |
| |
| // Helper function to GetFileSize() that is called when the metadata for a file |
| // is retrieved. |
| void CopyOrMoveIOTaskImpl::GotFileSize(size_t idx, |
| base::File::Error error, |
| const base::File::Info& file_info) { |
| if (completed_) { |
| // If Complete() has been called (e.g. due to an error), |progress_| is no |
| // longer valid, so return immediately. |
| return; |
| } |
| |
| DCHECK(idx < progress_->sources.size()); |
| if (error != base::File::FILE_OK) { |
| progress_->sources[idx].error = error; |
| LOG(ERROR) << "Could not get size of source file: error " << error << " " |
| << base::File::ErrorToString(error); |
| Complete(State::kError); |
| return; |
| } |
| |
| progress_->total_bytes += file_info.size; |
| source_sizes_[idx] = file_info.size; |
| progress_->sources[idx].is_directory = file_info.is_directory; |
| |
| // Return early if we didn't yet get the file size for all files. |
| DCHECK_LT(files_preprocessed_, progress_->sources.size()); |
| if (++files_preprocessed_ < progress_->sources.size()) { |
| return; |
| } |
| |
| // Got file size for all files at this point! |
| speedometer_.SetTotalBytes(progress_->total_bytes); |
| |
| if (util::IsNonNativeFileSystemType( |
| progress_->GetDestinationFolder().type())) { |
| // Destination is a virtual filesystem, so skip checking free space. |
| GenerateDestinationURL(0); |
| } else { |
| // For Drive, check we have enough local disk first, then check quota. |
| base::FilePath path = progress_->GetDestinationFolder().path(); |
| auto* drive_integration_service = |
| drive::util::GetIntegrationServiceByProfile(profile_); |
| if (drive_integration_service && drive_integration_service->IsMounted() && |
| drive_integration_service->GetMountPointPath().IsParent( |
| progress_->GetDestinationFolder().path())) { |
| path = drive_integration_service->GetDriveFsHost()->GetDataPath(); |
| } |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::MayBlock()}, |
| base::BindOnce(&base::SysInfo::AmountOfFreeDiskSpace, path), |
| base::BindOnce(&CopyOrMoveIOTaskImpl::GotFreeDiskSpace, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| |
| // Ensures that there is enough free space on the destination volume. |
| void CopyOrMoveIOTaskImpl::GotFreeDiskSpace(int64_t free_space) { |
| auto* drive_integration_service = |
| drive::util::GetIntegrationServiceByProfile(profile_); |
| bool is_drive = drive_integration_service && |
| drive_integration_service->IsMounted() && |
| drive_integration_service->GetMountPointPath().IsParent( |
| progress_->GetDestinationFolder().path()); |
| if (progress_->GetDestinationFolder().filesystem_id() == |
| util::GetDownloadsMountPointName(profile_) || |
| is_drive) { |
| free_space -= cryptohome::kMinFreeSpaceInBytes; |
| } |
| |
| int64_t required_bytes = progress_->total_bytes; |
| |
| // Move operations that are same-filesystem do not require disk space. |
| if (progress_->type == OperationType::kMove) { |
| for (size_t i = 0; i < source_sizes_.size(); i++) { |
| if (!IsCrossFileSystem(profile_, progress_->sources[i].url, |
| progress_->GetDestinationFolder())) { |
| required_bytes -= source_sizes_[i]; |
| } |
| } |
| } |
| |
| if (required_bytes > free_space) { |
| progress_->outputs.emplace_back(progress_->GetDestinationFolder(), |
| base::File::FILE_ERROR_NO_SPACE); |
| LOG(ERROR) << "Insufficient free space in destination"; |
| Complete(State::kError); |
| return; |
| } |
| |
| if (is_drive) { |
| bool is_shared_drive = drive_integration_service->IsSharedDrive( |
| progress_->GetDestinationFolder().path()); |
| drive_integration_service->GetPooledQuotaUsage( |
| base::BindOnce(base::BindOnce( |
| &CopyOrMoveIOTaskImpl::GotDrivePooledQuota, |
| weak_ptr_factory_.GetWeakPtr(), required_bytes, is_shared_drive))); |
| return; |
| } |
| |
| GenerateDestinationURL(0); |
| } |
| |
| void CopyOrMoveIOTaskImpl::GotDrivePooledQuota( |
| int64_t required_bytes, |
| bool is_shared_drive, |
| drive::FileError error, |
| drivefs::mojom::PooledQuotaUsagePtr usage) { |
| if (error != drive::FileError::FILE_ERROR_OK) { |
| // Log the error if we couldn't fetch the quota (probably because we are |
| // offline), but continue the operation and we will show an error later |
| // when we come back online and try to sync. |
| LOG(ERROR) << "Error fetching drive quota: " |
| << drive::FileErrorToString(error); |
| } else { |
| bool org_exceeded = |
| usage->user_type == drivefs::mojom::UserType::kOrganization && |
| usage->organization_limit_exceeded; |
| // User quota does not apply to shared drives. |
| bool user_exceeded = |
| !is_shared_drive && usage->total_user_bytes != -1 && |
| (usage->total_user_bytes - usage->used_user_bytes) < required_bytes; |
| if (org_exceeded || user_exceeded) { |
| progress_->outputs.emplace_back(progress_->GetDestinationFolder(), |
| base::File::FILE_ERROR_NO_SPACE); |
| LOG(ERROR) << "Insufficient drive quota"; |
| Complete(State::kError); |
| return; |
| } |
| } |
| |
| // Check shared drive quota if applicable. |
| auto* drive_integration_service = |
| drive::util::GetIntegrationServiceByProfile(profile_); |
| if (is_shared_drive && drive_integration_service && |
| drive_integration_service->IsMounted()) { |
| drive_integration_service->GetMetadata( |
| progress_->GetDestinationFolder().path(), |
| base::BindOnce(&CopyOrMoveIOTaskImpl::GotSharedDriveMetadata, |
| weak_ptr_factory_.GetWeakPtr(), required_bytes)); |
| return; |
| } |
| |
| GenerateDestinationURL(0); |
| } |
| |
| void CopyOrMoveIOTaskImpl::GotSharedDriveMetadata( |
| int64_t required_bytes, |
| drive::FileError error, |
| drivefs::mojom::FileMetadataPtr metadata) { |
| if (error != drive::FileError::FILE_ERROR_OK) { |
| // Log the error if we couldn't fetch the metadata (probably because we are |
| // offline), but continue the operation and we will show an error later |
| // when we come back online and try to sync. |
| LOG(ERROR) << "Error fetching shared drive metadata: " |
| << drive::FileErrorToString(error); |
| } else if (metadata->shared_drive_quota) { |
| const auto& quota = metadata->shared_drive_quota; |
| if ((quota->individual_quota_bytes_total - |
| quota->quota_bytes_used_in_drive) < required_bytes) { |
| progress_->outputs.emplace_back(progress_->GetDestinationFolder(), |
| base::File::FILE_ERROR_NO_SPACE); |
| LOG(ERROR) << "Insufficient shared drive quota"; |
| Complete(State::kError); |
| return; |
| } |
| } |
| |
| GenerateDestinationURL(0); |
| } |
| |
| // Tries to find an unused filename in the destination folder for a specific |
| // entry being transferred. |
| void CopyOrMoveIOTaskImpl::GenerateDestinationURL(size_t idx) { |
| DCHECK(idx < progress_->sources.size()); |
| |
| // In the event no `destination_file_names_` exist, fall back to the |
| // `BaseName` from the source URL. |
| const auto destination_file_name = |
| (destination_file_names_.size() == progress_->sources.size()) |
| ? destination_file_names_[idx] |
| : progress_->sources[idx].url.path().BaseName(); |
| |
| util::GenerateUnusedFilename( |
| progress_->GetDestinationFolder(), destination_file_name, |
| file_system_context_, |
| base::BindOnce(&CopyOrMoveIOTaskImpl::CopyOrMoveFile, |
| weak_ptr_factory_.GetWeakPtr(), idx)); |
| } |
| |
| // Starts the underlying copy or move operation. |
| void CopyOrMoveIOTaskImpl::CopyOrMoveFile( |
| size_t idx, |
| base::FileErrorOr<storage::FileSystemURL> destination_result) { |
| DCHECK(idx < progress_->sources.size()); |
| |
| if (!destination_result.has_value()) { |
| progress_->outputs.emplace_back(progress_->GetDestinationFolder(), |
| absl::nullopt); |
| OnCopyOrMoveComplete(idx, destination_result.error()); |
| return; |
| } |
| |
| progress_->outputs.emplace_back(destination_result.value(), absl::nullopt); |
| DCHECK_EQ(idx + 1, progress_->outputs.size()); |
| |
| const storage::FileSystemURL& source_url = progress_->sources[idx].url; |
| const storage::FileSystemURL& destination_url = destination_result.value(); |
| |
| // If the conflict dialog feature is disabled, use the destination url to |
| // implement default files app behavior: 'keepboth'. |
| if (!ash::features::IsFilesConflictDialogEnabled()) { |
| ContinueCopyOrMoveFile(idx, std::move(destination_url)); |
| return; |
| } |
| |
| // Create a replace url using the source base name and destination folder |
| // as the parent directory. |
| auto basename = source_url.path().BaseName(); |
| auto replace_url = file_system_context_->CreateCrackedFileSystemURL( |
| progress_->GetDestinationFolder().storage_key(), |
| progress_->GetDestinationFolder().mount_type(), |
| progress_->GetDestinationFolder().virtual_path().Append( |
| base::FilePath::FromUTF8Unsafe(basename.AsUTF8Unsafe()))); |
| |
| // If the source url and replace url are the same, the copy/move operation |
| // must use the destination url: default files app behavior 'keepboth'. |
| if (source_url == replace_url) { |
| ContinueCopyOrMoveFile(idx, std::move(destination_url)); |
| return; |
| } |
| |
| // Otherwise, if the base names are the same, there is no conflict and the |
| // copy/move operation can use the destination url. |
| if (basename == destination_url.path().BaseName()) { |
| ContinueCopyOrMoveFile(idx, std::move(destination_url)); |
| return; |
| } |
| |
| // If the base names are not the same, then the destination url exists and |
| // we must resolve the file name conflict. If the user's previous resolve |
| // was 'ApplyToAll', |conflict_resolve_| contains 'keepboth' or 'replace'. |
| // Use it to automatically resolve the conflict (no need to ask the UI). |
| if (!conflict_resolve_.empty()) { |
| ResumeParams params; |
| params.conflict_resolve = conflict_resolve_; |
| params.conflict_apply_to_all = true; |
| ResumeCopyOrMoveFile(idx, std::move(replace_url), |
| std::move(destination_url), std::move(params)); |
| return; |
| } |
| |
| // Setup the resume callback prior to entering state::PAUSED. ResumeIOTask |
| // will invoke this callback, once the user has resolved the conflict. See |
| // CopyOrMoveIOTaskImpl::Resume() below. |
| DCHECK(!resume_callback_); |
| resume_callback_ = google_apis::CreateRelayCallback( |
| base::BindOnce(&CopyOrMoveIOTaskImpl::ResumeCopyOrMoveFile, |
| weak_ptr_factory_.GetWeakPtr(), idx, |
| std::move(replace_url), std::move(destination_url))); |
| |
| // Enter state PAUSED: send pause params to the UI, to ask the user how to |
| // resolve the file name conflict. |
| progress_->state = State::kPaused; |
| progress_->pause_params.conflict_name = basename.AsUTF8Unsafe(); |
| progress_->pause_params.conflict_multiple = |
| (idx < progress_->sources.size() - 1) ? true : false; |
| progress_->pause_params.conflict_is_directory = |
| progress_->sources[idx].is_directory; |
| auto destination_folder = file_system_context_->CreateCrackedFileSystemURL( |
| progress_->GetDestinationFolder().storage_key(), |
| progress_->GetDestinationFolder().mount_type(), |
| progress_->GetDestinationFolder().virtual_path()); |
| progress_->pause_params.conflict_target_url = |
| destination_folder.ToGURL().spec(); |
| progress_callback_.Run(*progress_); |
| } |
| |
| void CopyOrMoveIOTaskImpl::Resume(ResumeParams params) { |
| LOG_IF(ERROR, !resume_callback_) << "Resume but no resume_callback_"; |
| |
| if (resume_callback_) { |
| std::move(resume_callback_).Run(std::move(params)); |
| } |
| } |
| |
| void CopyOrMoveIOTaskImpl::ResumeCopyOrMoveFile( |
| size_t idx, |
| storage::FileSystemURL replace_url, |
| storage::FileSystemURL destination_url, |
| ResumeParams params) { |
| DCHECK(idx < progress_->sources.size()); |
| DCHECK(idx < progress_->outputs.size()); |
| |
| // Re-enter state progress if needed. |
| if (progress_->state != State::kInProgress) { |
| progress_->state = State::kInProgress; |
| progress_callback_.Run(*progress_); |
| } |
| |
| // Get the user's conflict resolve choice. |
| const std::string& conflict_resolve = params.conflict_resolve; |
| const bool resolve_keepboth = conflict_resolve == "keepboth"; |
| const bool resolve_replace = conflict_resolve == "replace"; |
| |
| // The Files app UI always returns valid conflict resolve values. |
| if (!resolve_keepboth && !resolve_replace) { |
| LOG(ERROR) << "Invalid conflict resolve: " << conflict_resolve; |
| OnCopyOrMoveComplete(idx, base::File::FILE_ERROR_INVALID_OPERATION); |
| return; |
| } |
| |
| // Remember the 'ApplyToAll' choice for future conflict handling. |
| if (conflict_resolve_.empty() && params.conflict_apply_to_all) { |
| conflict_resolve_ = conflict_resolve; |
| } |
| |
| // For 'keepboth' resolve, use the destination url as the target. |
| if (resolve_keepboth) { |
| ContinueCopyOrMoveFile(idx, destination_url); |
| return; |
| } |
| |
| // For 'replace': delete replace_url so it can become the target. |
| auto did_delete_callback = google_apis::CreateRelayCallback( |
| base::BindOnce(&CopyOrMoveIOTaskImpl::DidDeleteDestinationURL, |
| weak_ptr_factory_.GetWeakPtr(), idx, replace_url)); |
| |
| content::GetIOThreadTaskRunner({})->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&StartDeleteOnIOThread, file_system_context_, replace_url, |
| std::move(did_delete_callback)), |
| base::BindOnce(&CopyOrMoveIOTaskImpl::SetCurrentOperationID, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CopyOrMoveIOTaskImpl::DidDeleteDestinationURL( |
| size_t idx, |
| storage::FileSystemURL replace_url, |
| base::File::Error error) { |
| DCHECK(idx < progress_->sources.size()); |
| DCHECK(idx < progress_->outputs.size()); |
| |
| operation_id_.reset(); |
| |
| // If replace_url delete failed, a copy/move of the source to that url will |
| // also fail. Report the error to OnCopyOrMoveComplete. Otherwise, call the |
| // ContinueCopyOrMoveFile() flow with the replace url as the target. |
| if (error) { |
| OnCopyOrMoveComplete(idx, error); |
| } else { |
| ContinueCopyOrMoveFile(idx, replace_url); |
| } |
| } |
| |
| void CopyOrMoveIOTaskImpl::ContinueCopyOrMoveFile( |
| size_t idx, |
| storage::FileSystemURL destination_url) { |
| DCHECK(idx < progress_->sources.size()); |
| DCHECK(idx < progress_->outputs.size()); |
| |
| const storage::FileSystemURL& source_url = progress_->sources[idx].url; |
| |
| // For a source entry name 'test', the destination url base name will be: |
| // `test` if that entry name did not exist at the destination. |
| // `test` if that entry name existed at the destination and was deleted |
| // because the user choice was to 'replace' that entry. |
| // `test (2)` if the entry name exists at the destination and 'keepboth' |
| // is active, either by default behavior or by user choice. |
| progress_->outputs[idx].url = destination_url; |
| |
| // File browsers generally default to preserving mtimes on copy/move so we |
| // should do the same. |
| storage::FileSystemOperation::CopyOrMoveOptionSet options = |
| storage::FileSystemOperation::CopyOrMoveOptionSet( |
| storage::FileSystemOperation::CopyOrMoveOption::kPreserveLastModified, |
| storage::FileSystemOperation::CopyOrMoveOption:: |
| kRemovePartiallyCopiedFilesOnError); |
| |
| // To ensure progress updates, force cross-filesystem I/O operations when the |
| // source and the destination are on different volumes, or between My files |
| // and Downloads. |
| if (IsCrossFileSystem(profile_, source_url, destination_url)) { |
| options.Put( |
| storage::FileSystemOperation::CopyOrMoveOption::kForceCrossFilesystem); |
| } |
| |
| auto* transfer_function = progress_->type == OperationType::kCopy |
| ? &StartCopyOnIOThread |
| : &StartMoveOnIOThread; |
| |
| // Using CreateRelayCallback to ensure that the callbacks are executed on the |
| // current thread. |
| auto complete_callback = google_apis::CreateRelayCallback( |
| base::BindOnce(&CopyOrMoveIOTaskImpl::OnCopyOrMoveComplete, |
| weak_ptr_factory_.GetWeakPtr(), idx)); |
| |
| content::GetIOThreadTaskRunner({})->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(transfer_function, file_system_context_, source_url, |
| destination_url, options, GetErrorBehavior(), |
| GetHookDelegate(idx), std::move(complete_callback)), |
| base::BindOnce(&CopyOrMoveIOTaskImpl::SetCurrentOperationID, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| storage::FileSystemOperation::ErrorBehavior |
| CopyOrMoveIOTaskImpl::GetErrorBehavior() { |
| return storage::FileSystemOperation::ERROR_BEHAVIOR_ABORT; |
| } |
| |
| std::unique_ptr<storage::CopyOrMoveHookDelegate> |
| CopyOrMoveIOTaskImpl::GetHookDelegate(size_t idx) { |
| // Using CreateRelayCallback to ensure that the callbacks are executed on the |
| // current thread. |
| auto progress_callback = google_apis::CreateRelayCallback( |
| base::BindRepeating(&CopyOrMoveIOTaskImpl::OnCopyOrMoveProgress, |
| weak_ptr_factory_.GetWeakPtr(), idx)); |
| return std::make_unique<FileManagerCopyOrMoveHookDelegate>(progress_callback); |
| } |
| |
| void CopyOrMoveIOTaskImpl::OnCopyOrMoveProgress( |
| size_t idx, |
| FileManagerCopyOrMoveHookDelegate::ProgressType type, |
| const storage::FileSystemURL& source_url, |
| const storage::FileSystemURL& destination_url, |
| int64_t size) { |
| const std::string destination_path = destination_url.path().AsUTF8Unsafe(); |
| auto& [individual_progress, aggregate_progress] = item_progresses[idx]; |
| |
| const auto log_progress = [&]() { |
| VLOG(1) << type << "\ncopy_move_src" << source_url.path() |
| << "\ncopy_move_des " << destination_url.path(); |
| }; |
| |
| using ProgressType = FileManagerCopyOrMoveHookDelegate::ProgressType; |
| if (type != ProgressType::kProgress) { |
| switch (type) { |
| case ProgressType::kBegin: |
| log_progress(); |
| individual_progress[destination_path] = 0; |
| return; |
| case ProgressType::kEndCopy: |
| log_progress(); |
| individual_progress.erase(destination_path); |
| return; |
| case ProgressType::kEndMove: |
| log_progress(); |
| individual_progress.erase(destination_path); |
| return; |
| case ProgressType::kEndRemoveSource: |
| log_progress(); |
| return; |
| case ProgressType::kError: |
| log_progress(); |
| return; |
| default: |
| NOTREACHED() << "Unknown ProgressType: " << int(type); |
| return; |
| } |
| } |
| |
| // The |size| is only valid for ProgressType::kProgress. |
| DCHECK_EQ(ProgressType::kProgress, type); |
| int64_t& last_size = individual_progress.at(destination_path); |
| int64_t delta = size - last_size; |
| last_size = size; |
| |
| aggregate_progress += delta; |
| progress_->bytes_transferred += delta; |
| speedometer_.Update(progress_->bytes_transferred); |
| |
| // Speedometer can produce infinite result which can't be serialized to JSON |
| // when sending the status via private API. |
| double remaining_seconds = speedometer_.GetRemainingSeconds(); |
| if (std::isfinite(remaining_seconds)) { |
| progress_->remaining_seconds = remaining_seconds; |
| } |
| |
| progress_callback_.Run(*progress_); |
| } |
| |
| void CopyOrMoveIOTaskImpl::OnCopyOrMoveComplete(size_t idx, |
| base::File::Error error) { |
| DCHECK(idx < progress_->sources.size()); |
| DCHECK(idx < progress_->outputs.size()); |
| |
| operation_id_.reset(); |
| |
| progress_->sources[idx].error = error; |
| progress_->outputs[idx].error = error; |
| |
| auto& [individual_progress, aggregate_progress] = item_progresses[idx]; |
| individual_progress.clear(); |
| |
| // Some copy and move operations (depending on the source and destination |
| // filesystems) don't support progress reporting yet, so we rely on setting |
| // bytes_transferred only when each item completes. By also deducting |
| // `aggregate_progress` from bytes_transferred, we ensure that both operations |
| // that report progress and those that don't are supported. |
| progress_->bytes_transferred += source_sizes_[idx] - aggregate_progress; |
| |
| if (idx < progress_->sources.size() - 1) { |
| progress_callback_.Run(*progress_); |
| GenerateDestinationURL(idx + 1); |
| return; |
| } |
| |
| // Complete: assume State::kSuccess. |
| file_manager::io_task::State complete_state = State::kSuccess; |
| |
| // Look for source errors and set the complete state to State::Error if any |
| // source errors are found. |
| for (const auto& source : progress_->sources) { |
| DCHECK(source.error.has_value()); |
| if (source.error != base::File::FILE_OK) { |
| LOG(ERROR) << "Error on complete: error " << source.error.value() << " " |
| << base::File::ErrorToString(source.error.value()); |
| complete_state = State::kError; |
| break; |
| } |
| } |
| |
| Complete(complete_state); |
| } |
| |
| void CopyOrMoveIOTaskImpl::SetCurrentOperationID( |
| storage::FileSystemOperationRunner::OperationID id) { |
| operation_id_.emplace(id); |
| } |
| |
| } // namespace file_manager::io_task |