blob: f06300be24c9b50c8911bef49edee29d98a8a402 [file] [log] [blame]
// 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