| // Copyright 2024 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/policy/skyvault/drive_skyvault_uploader.h" |
| |
| #include <optional> |
| |
| #include "base/check_is_test.h" |
| #include "base/check_op.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/location.h" |
| #include "base/notreached.h" |
| #include "base/task/thread_pool.h" |
| #include "base/timer/timer.h" |
| #include "chrome/browser/ash/drive/drive_integration_service.h" |
| #include "chrome/browser/ash/drive/drive_integration_service_factory.h" |
| #include "chrome/browser/ash/drive/file_system_util.h" |
| #include "chrome/browser/ash/file_manager/copy_or_move_io_task.h" |
| #include "chrome/browser/ash/file_manager/delete_io_task.h" |
| #include "chrome/browser/ash/file_manager/fileapi_util.h" |
| #include "chrome/browser/ash/file_manager/io_task.h" |
| #include "chrome/browser/ash/file_manager/office_file_tasks.h" |
| #include "chrome/browser/ash/file_manager/volume_manager.h" |
| #include "chrome/browser/ash/policy/skyvault/histogram_helper.h" |
| #include "chrome/browser/ash/policy/skyvault/local_files_migration_constants.h" |
| #include "chrome/browser/ash/policy/skyvault/policy_utils.h" |
| #include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chromeos/ash/components/drivefs/drivefs_host.h" |
| #include "components/drive/file_errors.h" |
| |
| using storage::FileSystemURL; |
| |
| namespace policy::local_user_files { |
| namespace { |
| // Creates a directory at `dir_path`, if it doesn't already exist. Returns true |
| // if directory exists or is created successfully. |
| bool CreateDirectoryIfNeeded(const base::FilePath& dir_path) { |
| base::File::Error error = base::File::FILE_OK; |
| if (base::DirectoryExists(dir_path)) { |
| return true; |
| } |
| if (!base::CreateDirectoryAndGetError(dir_path, &error)) { |
| PLOG(ERROR) << "Failed to create directory: " |
| << base::File::ErrorToString(error); |
| return false; |
| } |
| return true; |
| } |
| } // namespace |
| |
| DriveSkyvaultUploader::DriveSkyvaultUploader( |
| Profile* profile, |
| const base::FilePath& file_path, |
| const base::FilePath& relative_source_path, |
| const std::string& upload_root, |
| UploadCallback callback) |
| : profile_(profile), |
| file_system_context_( |
| file_manager::util::GetFileManagerFileSystemContext(profile)), |
| drive_integration_service_( |
| drive::DriveIntegrationServiceFactory::FindForProfile(profile)), |
| source_url_(file_system_context_->CreateCrackedFileSystemURL( |
| blink::StorageKey(), |
| storage::kFileSystemTypeLocal, |
| file_path)), |
| relative_source_path_(relative_source_path), |
| upload_root_(upload_root), |
| callback_(std::move(callback)) {} |
| |
| DriveSkyvaultUploader::~DriveSkyvaultUploader() = default; |
| |
| void DriveSkyvaultUploader::Run() { |
| DCHECK(callback_); |
| |
| if (cancelled_) { |
| OnEndCopy(MigrationUploadError::kCancelled); |
| return; |
| } |
| |
| if (!profile_) { |
| LOG(ERROR) << "No profile"; |
| OnEndCopy(MigrationUploadError::kUnexpectedError); |
| return; |
| } |
| |
| file_manager::VolumeManager* volume_manager = |
| file_manager::VolumeManager::Get(profile_); |
| if (!volume_manager) { |
| LOG(ERROR) << "No volume manager"; |
| OnEndCopy(MigrationUploadError::kUnexpectedError); |
| return; |
| } |
| io_task_controller_ = volume_manager->io_task_controller(); |
| if (!io_task_controller_) { |
| LOG(ERROR) << "No task_controller"; |
| OnEndCopy(MigrationUploadError::kUnexpectedError); |
| return; |
| } |
| |
| if (!drive_integration_service_) { |
| LOG(ERROR) << "No Drive integration service"; |
| OnEndCopy(MigrationUploadError::kServiceUnavailable); |
| return; |
| } |
| |
| // Observe Drive updates. |
| drive::DriveIntegrationService::Observer::Observe(drive_integration_service_); |
| |
| if (!drive_integration_service_->is_enabled()) { |
| // Drive is completely disabled for this profile. |
| LOG(ERROR) << "Drive integration service isn't available"; |
| OnEndCopy(MigrationUploadError::kServiceUnavailable); |
| return; |
| } |
| |
| auto drive_status = drive::util::GetDriveConnectionStatus(profile_); |
| waiting_for_connection_ = |
| drive_status != drive::util::ConnectionStatus::kConnected; |
| SkyVaultMigrationWaitForConnectionHistogram( |
| MigrationDestination::kGoogleDrive, waiting_for_connection_); |
| if (waiting_for_connection_) { |
| LOG(ERROR) << "Waiting for connection to Drive"; |
| connection_wait_start_time_ = base::Time::Now(); |
| |
| reconnection_timer_.Start( |
| FROM_HERE, kReconnectionTimeout, |
| base::BindOnce(&DriveSkyvaultUploader::OnReconnectionTimeout, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| // Observe IO tasks updates. |
| io_task_controller_observer_.Observe(io_task_controller_); |
| |
| drivefs::DriveFsHost::Observer::Observe( |
| drive_integration_service_->GetDriveFsHost()); |
| |
| if (!drive_integration_service_->IsMounted()) { |
| LOG(ERROR) << "Google Drive is not mounted"; |
| OnEndCopy(MigrationUploadError::kServiceUnavailable); |
| return; |
| } |
| |
| upload_root_path_ = drive_integration_service_->GetMountPointPath() |
| .AppendASCII("root") |
| .AppendASCII(upload_root_); |
| auto destination_folder_path = |
| upload_root_path_.Append(relative_source_path_); |
| // Copy will fail if the full path doesn't already exist in drive, so first |
| // create the destination folder if needed. |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::MayBlock()}, |
| base::BindOnce(&CreateDirectoryIfNeeded, destination_folder_path), |
| base::BindOnce(&DriveSkyvaultUploader::CreateCopyIOTask, |
| weak_ptr_factory_.GetWeakPtr(), destination_folder_path)); |
| } |
| |
| void DriveSkyvaultUploader::Cancel() { |
| cancelled_ = true; |
| // If delete task has started, copy must have finished. |
| if (observed_delete_task_id_.has_value()) { |
| io_task_controller_->Cancel(observed_delete_task_id_.value()); |
| } else if (observed_copy_task_id_.has_value()) { |
| io_task_controller_->Cancel(observed_copy_task_id_.value()); |
| } |
| } |
| |
| void DriveSkyvaultUploader::CreateCopyIOTask( |
| const base::FilePath& destination_folder_path, |
| bool created) { |
| if (observed_copy_task_id_) { |
| NOTREACHED() |
| << "The Copy IOTask was already triggered. Case should not be reached."; |
| } |
| |
| if (cancelled_) { |
| OnEndCopy(MigrationUploadError::kCancelled); |
| return; |
| } |
| |
| if (!created) { |
| OnEndCopy(MigrationUploadError::kCreateFolderFailed); |
| return; |
| } |
| |
| // Destination url. |
| FileSystemURL destination_folder_url = |
| ash::cloud_upload::FilePathToFileSystemURL(profile_, file_system_context_, |
| destination_folder_path); |
| if (!destination_folder_url.is_valid()) { |
| LOG(ERROR) << "Unable to generate destination folder Drive URL"; |
| OnEndCopy(MigrationUploadError::kServiceUnavailable); |
| return; |
| } |
| |
| std::vector<FileSystemURL> source_urls{source_url_}; |
| // Always use a copy task. Will convert to a move upon success. |
| std::unique_ptr<file_manager::io_task::IOTask> copy_task = |
| std::make_unique<file_manager::io_task::CopyOrMoveIOTask>( |
| file_manager::io_task::OperationType::kCopy, std::move(source_urls), |
| std::move(destination_folder_url), profile_, file_system_context_, |
| /*show_notification=*/false); |
| |
| observed_copy_task_id_ = io_task_controller_->Add(std::move(copy_task)); |
| } |
| |
| void DriveSkyvaultUploader::SetFailDeleteForTesting(bool fail) { |
| CHECK_IS_TEST(); |
| fail_delete_for_testing_ = fail; |
| } |
| |
| void DriveSkyvaultUploader::OnEndCopy( |
| std::optional<MigrationUploadError> error) { |
| if (copy_ended_) { |
| // Prevent loops in case Copy IO task and Drive sync fail separately. |
| return; |
| } |
| copy_ended_ = true; |
| CHECK(!error_); |
| error_ = error; |
| |
| // If destination file doesn't exist, no delete is required. |
| base::FilePath rel_path; |
| bool destination_file_exists = |
| !observed_absolute_dest_path_.empty() && |
| drive_integration_service_->GetRelativeDrivePath( |
| observed_absolute_dest_path_, &rel_path); |
| if (!destination_file_exists) { |
| OnEndUpload(); |
| return; |
| } |
| |
| if (cancelled_) { |
| error_ = error_.value_or(MigrationUploadError::kCancelled); |
| OnEndUpload(); |
| return; |
| } |
| |
| if (observed_delete_task_id_) { |
| NOTREACHED() << "The delete IOTask was already triggered. Case should not " |
| "be reached."; |
| } |
| |
| std::vector<FileSystemURL> file_urls; |
| if (!error.has_value()) { |
| // If copy to Drive was successful, delete source file to convert the upload |
| // to a move to Drive. |
| file_urls.push_back(source_url_); |
| } else { |
| // If copy to Drive was unsuccessful, delete destination file to undo the |
| // copy to Drive. |
| FileSystemURL dest_url = ash::cloud_upload::FilePathToFileSystemURL( |
| profile_, file_system_context_, observed_absolute_dest_path_); |
| file_urls.push_back(dest_url); |
| } |
| |
| std::unique_ptr<file_manager::io_task::IOTask> task = |
| std::make_unique<file_manager::io_task::DeleteIOTask>( |
| std::move(file_urls), file_system_context_, |
| /*show_notification=*/false); |
| observed_delete_task_id_ = io_task_controller_->Add(std::move(task)); |
| } |
| |
| void DriveSkyvaultUploader::OnEndUpload() { |
| observed_relative_drive_path_.clear(); |
| SkyVaultDeleteErrorHistogram(UploadTrigger::kMigration, |
| MigrationDestination::kGoogleDrive, |
| error_ == MigrationUploadError::kDeleteFailed); |
| std::move(callback_).Run(error_, upload_root_path_); |
| } |
| |
| void DriveSkyvaultUploader::OnIOTaskStatus( |
| const file_manager::io_task::ProgressStatus& status) { |
| if (status.task_id == observed_copy_task_id_) { |
| OnCopyStatus(status); |
| return; |
| } |
| if (status.task_id == observed_delete_task_id_) { |
| OnDeleteStatus(status); |
| return; |
| } |
| } |
| |
| void DriveSkyvaultUploader::OnCopyStatus( |
| const file_manager::io_task::ProgressStatus& status) { |
| switch (status.state) { |
| case file_manager::io_task::State::kScanning: |
| case file_manager::io_task::State::kQueued: |
| case file_manager::io_task::State::kPaused: |
| return; |
| case file_manager::io_task::State::kInProgress: |
| if (observed_relative_drive_path_.empty() && !status.outputs.empty()) { |
| // It's always one file. |
| DCHECK_EQ(status.sources.size(), 1u); |
| DCHECK_EQ(status.outputs.size(), 1u); |
| |
| if (!drive_integration_service_) { |
| LOG(ERROR) << "No Drive integration service"; |
| OnEndCopy(MigrationUploadError::kServiceUnavailable); |
| return; |
| } |
| |
| // Get the output path from the IOTaskController's ProgressStatus. The |
| // destination file name is not known in advance, given that it's |
| // generated from the IOTaskController which resolves potential name |
| // clashes. |
| observed_absolute_dest_path_ = status.outputs[0].url.path(); |
| drive_integration_service_->GetRelativeDrivePath( |
| observed_absolute_dest_path_, &observed_relative_drive_path_); |
| } |
| return; |
| case file_manager::io_task::State::kSuccess: |
| DCHECK_EQ(status.outputs.size(), 1u); |
| return; |
| case file_manager::io_task::State::kCancelled: |
| OnEndCopy(MigrationUploadError::kCancelled); |
| return; |
| case file_manager::io_task::State::kError: |
| ProcessCopyError(status); |
| return; |
| case file_manager::io_task::State::kNeedPassword: |
| NOTREACHED() << "Encrypted file should not need password to be copied or " |
| "moved. Case should not be reached."; |
| } |
| } |
| |
| void DriveSkyvaultUploader::ProcessCopyError( |
| const file_manager::io_task::ProgressStatus& status) { |
| // It's always one file. |
| DCHECK_EQ(status.sources.size(), 1u); |
| DCHECK_EQ(status.outputs.size(), 1u); |
| DCHECK_EQ(status.state, file_manager::io_task::State::kError); |
| |
| base::File::Error error = |
| status.outputs.front().error.value_or(base::File::FILE_ERROR_FAILED); |
| MigrationUploadError upload_error = MigrationUploadError::kCopyFailed; |
| |
| switch (error) { |
| case base::File::FILE_ERROR_NOT_FOUND: |
| upload_error = MigrationUploadError::kFileNotFound; |
| break; |
| case base::File::FILE_ERROR_NO_SPACE: |
| upload_error = MigrationUploadError::kCloudQuotaFull; |
| break; |
| default: |
| break; |
| } |
| |
| OnEndCopy(upload_error); |
| } |
| |
| void DriveSkyvaultUploader::OnDeleteStatus( |
| const file_manager::io_task::ProgressStatus& status) { |
| switch (status.state) { |
| case file_manager::io_task::State::kCancelled: |
| // Don't override errors occurred during the copy. |
| error_ = error_.value_or(MigrationUploadError::kCancelled); |
| break; |
| case file_manager::io_task::State::kError: |
| // Don't override errors occurred during the copy. |
| error_ = error_.value_or(MigrationUploadError::kDeleteFailed); |
| break; |
| case file_manager::io_task::State::kSuccess: |
| break; |
| default: |
| return; |
| } |
| |
| if (fail_delete_for_testing_) { |
| CHECK_IS_TEST(); |
| if (!error_) { |
| error_ = MigrationUploadError::kDeleteFailed; |
| } |
| } |
| |
| OnEndUpload(); |
| } |
| |
| void DriveSkyvaultUploader::OnUnmounted() {} |
| |
| void DriveSkyvaultUploader::ImmediatelyUploadDone(drive::FileError error) { |
| LOG_IF(ERROR, error != drive::FileError::FILE_ERROR_OK) |
| << "ImmediatelyUpload failed with status: " << error; |
| } |
| |
| void DriveSkyvaultUploader::OnSyncingStatusUpdate( |
| const drivefs::mojom::SyncingStatus& syncing_status) { |
| for (const auto& item : syncing_status.item_events) { |
| if (base::FilePath(item->path) != observed_relative_drive_path_) { |
| continue; |
| } |
| if (item->state == drivefs::mojom::ItemEvent::State::kCancelledAndDeleted) { |
| continue; |
| } |
| switch (item->state) { |
| case drivefs::mojom::ItemEvent::State::kQueued: { |
| // Tell Drive to upload the file now. If successful, we will receive a |
| // kInProgress or kCompleted event sooner. If this fails, we ignore it. |
| // The file will get uploaded eventually. |
| drive_integration_service_->ImmediatelyUpload( |
| observed_relative_drive_path_, |
| base::BindOnce(&DriveSkyvaultUploader::ImmediatelyUploadDone, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| case drivefs::mojom::ItemEvent::State::kInProgress: |
| return; |
| case drivefs::mojom::ItemEvent::State::kCompleted: |
| // The file has fully synced. |
| OnEndCopy(); |
| return; |
| case drivefs::mojom::ItemEvent::State::kFailed: |
| LOG(ERROR) << "Drive sync error: failed"; |
| OnEndCopy(MigrationUploadError::kSyncFailed); |
| return; |
| case drivefs::mojom::ItemEvent::State::kCancelledAndDeleted: |
| NOTREACHED(); |
| case drivefs::mojom::ItemEvent::State::kCancelledAndTrashed: |
| LOG(ERROR) << "Drive sync error: cancelled and trashed"; |
| OnEndCopy(MigrationUploadError::kSyncFailed); |
| return; |
| } |
| } |
| } |
| |
| void DriveSkyvaultUploader::OnError(const drivefs::mojom::DriveError& error) { |
| if (base::FilePath(error.path) != observed_relative_drive_path_) { |
| return; |
| } |
| OnEndCopy(MigrationUploadError::kCloudQuotaFull); |
| } |
| |
| void DriveSkyvaultUploader::OnDriveConnectionStatusChanged( |
| drive::util::ConnectionStatus status) { |
| if (waiting_for_connection_) { |
| if (status == drive::util::ConnectionStatus::kConnected) { |
| LOG(ERROR) << "Reconnected to Drive"; |
| waiting_for_connection_ = false; |
| CHECK(connection_wait_start_time_.has_value()); |
| SkyVaultMigrationReconnectionDurationHistogram( |
| MigrationDestination::kGoogleDrive, |
| base::Time::Now() - connection_wait_start_time_.value()); |
| connection_wait_start_time_.reset(); |
| reconnection_timer_.Stop(); |
| drive::DriveIntegrationService::Observer::Reset(); |
| Run(); |
| } |
| return; |
| } |
| if (status == drive::util::ConnectionStatus::kNoService) { |
| LOG(ERROR) << "Drive service became unavailable during upload"; |
| OnEndCopy(MigrationUploadError::kServiceUnavailable); |
| return; |
| } |
| if (status != drive::util::ConnectionStatus::kConnected) { |
| LOG(ERROR) << "Lost connection to Drive during upload"; |
| OnEndCopy(MigrationUploadError::kNetworkError); |
| } |
| } |
| |
| void DriveSkyvaultUploader::OnReconnectionTimeout() { |
| if (!waiting_for_connection_) { |
| LOG(ERROR) << "Reconnection timer fired, but currently not waiting for " |
| "connection; ignoring"; |
| return; |
| } |
| LOG(ERROR) |
| << "Reconnection not established within the timeout, failing the upload"; |
| OnEndCopy(MigrationUploadError::kReconnectTimeout); |
| } |
| |
| } // namespace policy::local_user_files |