chromium/chrome/browser/ash/crosapi/move_migrator.cc

// 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/crosapi/move_migrator.h"

#include <errno.h>

#include <memory>
#include <string>

#include "base/containers/contains.h"
#include "base/debug/dump_without_crashing.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/timer/elapsed_timer.h"
#include "base/values.h"
#include "chrome/browser/ash/crosapi/browser_data_migrator.h"
#include "chrome/browser/ash/crosapi/browser_data_migrator_util.h"
#include "chrome/browser/extensions/extension_keeplist_chromeos.h"
#include "chrome/common/chrome_constants.h"
#include "chromeos/ash/components/standalone_browser/migration_progress_tracker.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"

namespace ash {

MoveMigrator::MoveMigrator(
    const base::FilePath& original_profile_dir,
    const std::string& user_id_hash,
    std::unique_ptr<standalone_browser::MigrationProgressTracker>
        progress_tracker,
    scoped_refptr<browser_data_migrator_util::CancelFlag> cancel_flag,
    PrefService* local_state,
    MigrationFinishedCallback finished_callback)
    : original_profile_dir_(original_profile_dir),
      user_id_hash_(user_id_hash),
      progress_tracker_(std::move(progress_tracker)),
      cancel_flag_(cancel_flag),
      local_state_(local_state),
      finished_callback_(std::move(finished_callback)) {
  DCHECK(local_state_);
}

MoveMigrator::~MoveMigrator() = default;

void MoveMigrator::Migrate() {
  ResumeStep resume_step = GetResumeStep(local_state_, user_id_hash_);

  if (IsResumeStep(resume_step)) {
    const int resume_count =
        UpdateResumeAttemptCountForUser(local_state_, user_id_hash_);

    if (resume_count > 0) {
      LOG(WARNING) << "Resuming move migration for the " << resume_count
                   << "th time.";
      UMA_HISTOGRAM_COUNTS_100(kMoveMigratorResumeCount, resume_count);
    }

    if (resume_count > kMoveMigrationResumeCountLimit) {
      LOG(ERROR) << "The number of resume attempt limit has reached. Marking "
                    "move migration as completed.";
      base::UmaHistogramBoolean(kMoveMigratorMaxResumeReached, true);
      base::debug::DumpWithoutCrashing();
      SetResumeStep(local_state_, user_id_hash_, ResumeStep::kCompleted);
      resume_step = ResumeStep::kCompleted;
    }
  }

  // Start or resume migration.
  UMA_HISTOGRAM_ENUMERATION(kMoveMigratorResumeStepUMA, resume_step);
  switch (resume_step) {
    case ResumeStep::kStart:
      base::ThreadPool::PostTaskAndReplyWithResult(
          FROM_HERE,
          {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
           base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
          base::BindOnce(&MoveMigrator::PreMigrationCleanUp,
                         original_profile_dir_),
          base::BindOnce(&MoveMigrator::OnPreMigrationCleanUp,
                         weak_factory_.GetWeakPtr()));
      return;
    case ResumeStep::kMoveLacrosItems:
      LOG(ERROR) << "Migration did not complete in the previous attempt. "
                    "Resuming migration from kMoveLacrosItems step.";
      base::ThreadPool::PostTaskAndReplyWithResult(
          FROM_HERE,
          {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
           base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
          base::BindOnce(&MoveMigrator::MoveLacrosItemsToNewDir,
                         original_profile_dir_),
          base::BindOnce(&MoveMigrator::OnMoveLacrosItemsToNewDir,
                         weak_factory_.GetWeakPtr()));
      return;
    case ResumeStep::kMoveSplitItems:
      LOG(ERROR) << "Migration did not complete in the previous attempt. "
                    "Resuming migration from kMoveSplitItems step.";
      base::ThreadPool::PostTaskAndReplyWithResult(
          FROM_HERE,
          {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
           base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
          base::BindOnce(&MoveMigrator::MoveSplitItemsToOriginalDir,
                         original_profile_dir_),
          base::BindOnce(&MoveMigrator::OnMoveSplitItemsToOriginalDir,
                         weak_factory_.GetWeakPtr()));
      return;
    case ResumeStep::kMoveTmpDir:
      LOG(ERROR) << "Migration did not complete in the previous attempt. "
                    "Resuming migration from kMoveTmpDir step.";
      base::ThreadPool::PostTaskAndReplyWithResult(
          FROM_HERE,
          {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
           base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
          base::BindOnce(&MoveMigrator::MoveTmpDirToLacrosDir,
                         original_profile_dir_),
          base::BindOnce(&MoveMigrator::OnMoveTmpDirToLacrosDir,
                         weak_factory_.GetWeakPtr()));
      return;
    case ResumeStep::kCompleted:
      LOG(ERROR)
          << "This state indicates that migration was marked as completed by"
             "`MoveMigrator` but was not by `BrowserDataMigratorImpl`";
      InvokeCallback({TaskStatus::kSucceeded});
      return;
  }
}

// static
bool MoveMigrator::ResumeRequired(PrefService* local_state,
                                  const std::string& user_id_hash) {
  ResumeStep resume_step = GetResumeStep(local_state, user_id_hash);

  return IsResumeStep(resume_step);
}

bool MoveMigrator::IsResumeStep(ResumeStep resume_step) {
  switch (resume_step) {
    case ResumeStep::kStart:
      return false;
    case ResumeStep::kMoveLacrosItems:
      return true;
    case ResumeStep::kMoveSplitItems:
      return true;
    case ResumeStep::kMoveTmpDir:
      return true;
    case ResumeStep::kCompleted:
      return false;
  }
}

// static
void MoveMigrator::RegisterLocalStatePrefs(PrefRegistrySimple* registry) {
  registry->RegisterDictionaryPref(kMoveMigrationResumeStepPref);
  registry->RegisterDictionaryPref(kMoveMigrationResumeCountPref);
}

// static
MoveMigrator::ResumeStep MoveMigrator::GetResumeStep(
    PrefService* local_state,
    const std::string& user_id_hash) {
  return static_cast<ResumeStep>(
      local_state->GetDict(kMoveMigrationResumeStepPref)
          .FindInt(user_id_hash)
          .value_or(0));
}

// static
void MoveMigrator::SetResumeStep(PrefService* local_state,
                                 const std::string& user_id_hash,
                                 const ResumeStep step) {
  ScopedDictPrefUpdate update(local_state, kMoveMigrationResumeStepPref);
  base::Value::Dict& dict = update.Get();
  dict.Set(user_id_hash, static_cast<int>(step));
  local_state->CommitPendingWrite();
}

int MoveMigrator::UpdateResumeAttemptCountForUser(
    PrefService* local_state,
    const std::string& user_id_hash) {
  int count = local_state->GetDict(kMoveMigrationResumeCountPref)
                  .FindIntByDottedPath(user_id_hash)
                  .value_or(0);
  count += 1;
  ScopedDictPrefUpdate update(local_state, kMoveMigrationResumeCountPref);
  base::Value::Dict& dict = update.Get();
  dict.Set(user_id_hash, count);
  return count;
}

void MoveMigrator::ClearResumeAttemptCountForUser(
    PrefService* local_state,
    const std::string& user_id_hash) {
  ScopedDictPrefUpdate update(local_state, kMoveMigrationResumeCountPref);
  base::Value::Dict& dict = update.Get();
  dict.Remove(user_id_hash);
}

// static
void MoveMigrator::ClearResumeStepForUser(PrefService* local_state,
                                          const std::string& user_id_hash) {
  ScopedDictPrefUpdate update(local_state, kMoveMigrationResumeStepPref);
  base::Value::Dict& dict = update.Get();
  dict.Remove(user_id_hash);
}

// static
MoveMigrator::TaskResult MoveMigrator::PreMigrationCleanUp(
    const base::FilePath& original_profile_dir) {
  LOG(WARNING) << "Running PreMigrationCleanUp()";
  base::ElapsedTimer timer;

  const base::FilePath new_user_dir =
      original_profile_dir.Append(browser_data_migrator_util::kLacrosDir);

  if (base::PathExists(new_user_dir)) {
    // Delete an existing lacros profile before the migration.
    if (!base::DeletePathRecursively(new_user_dir)) {
      PLOG(ERROR) << "Deleting " << new_user_dir.value() << " failed: ";
      return {TaskStatus::kPreMigrationCleanUpDeleteLacrosDirFailed, errno};
    }
  }

  const base::FilePath tmp_user_dir =
      original_profile_dir.Append(browser_data_migrator_util::kMoveTmpDir);
  if (base::PathExists(tmp_user_dir)) {
    // Delete tmp_user_dir if any were left from a previous failed move
    // migration attempt. Note that if resuming move migration from later steps
    // such as `MoveLacrosItemsToNewDir()`, this tmp_user_dir will not be
    // deleted. This is an intended behaviour because we do not want to delete
    // tmp_user_dir once we start deleting items from the Ash PDD.
    if (!base::DeletePathRecursively(tmp_user_dir)) {
      PLOG(ERROR) << "Deleting " << tmp_user_dir.value() << " failed: ";
      return {TaskStatus::kPreMigrationCleanUpDeleteTmpDirFailed, errno};
    }
  }

  const base::FilePath tmp_split_dir =
      original_profile_dir.Append(browser_data_migrator_util::kSplitTmpDir);
  if (base::PathExists(tmp_split_dir)) {
    // Delete tmp_split_dir if any were left from a previous failed move
    // migration attempt. Similar considerations to tmp_user_dir apply.
    if (!base::DeletePathRecursively(tmp_split_dir)) {
      PLOG(ERROR) << "Deleting" << tmp_split_dir.value() << " failed: ";
      return {TaskStatus::kPreMigrationCleanUpDeleteTmpSplitDirFailed, errno};
    }
  }

  // Delete deletable items to free up space.
  browser_data_migrator_util::TargetItems deletable_items =
      browser_data_migrator_util::GetTargetItems(
          original_profile_dir,
          browser_data_migrator_util::ItemType::kDeletable);
  for (const auto& item : deletable_items.items) {
    bool result = item.is_directory ? base::DeletePathRecursively(item.path)
                                    : base::DeleteFile(item.path);
    if (!result) {
      // This is not critical to the migration so log the error but do stop the
      // migration.
      PLOG(ERROR) << "Could not delete " << item.path.value();
    }
  }

  const int64_t estimated_extra_bytes_created =
      browser_data_migrator_util::EstimatedExtraBytesCreated(
          original_profile_dir);
  // Now check if there is enough disk space for the migration to be carried
  // out.
  const int64_t extra_bytes_required_to_be_freed =
      browser_data_migrator_util::ExtraBytesRequiredToBeFreed(
          estimated_extra_bytes_created, original_profile_dir);

  UMA_HISTOGRAM_MEDIUM_TIMES(kMoveMigratorPreMigrationCleanUpTimeUMA,
                             timer.Elapsed());
  if (extra_bytes_required_to_be_freed > 0u) {
    UMA_HISTOGRAM_CUSTOM_COUNTS(kMoveMigratorExtraSpaceRequiredMB,
                                extra_bytes_required_to_be_freed / 1024 / 1024,
                                1, 10000, 100);
    LOG(ERROR) << "Not enough disk space available to carry out the migration "
                  "safely. Need to free up "
               << extra_bytes_required_to_be_freed << " bytes from "
               << original_profile_dir;
    return {TaskStatus::kPreMigrationCleanUpNotEnoughSpace, std::nullopt,
            extra_bytes_required_to_be_freed};
  }

  return {TaskStatus::kSucceeded, std::nullopt, std::nullopt,
          estimated_extra_bytes_created};
}

void MoveMigrator::OnPreMigrationCleanUp(MoveMigrator::TaskResult result) {
  if (result.status != TaskStatus::kSucceeded) {
    LOG(ERROR) << "PreMigrationCleanup() failed.";
    InvokeCallback(result);
    return;
  }

  estimated_extra_bytes_created_ = *result.estimated_extra_bytes_created;

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
       base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
      base::BindOnce(&MoveMigrator::SetupLacrosDir, original_profile_dir_,
                     std::move(progress_tracker_), cancel_flag_),
      base::BindOnce(&MoveMigrator::OnSetupLacrosDir,
                     weak_factory_.GetWeakPtr()));
}

// static
MoveMigrator::TaskResult MoveMigrator::SetupLacrosDir(
    const base::FilePath& original_profile_dir,
    std::unique_ptr<standalone_browser::MigrationProgressTracker>
        progress_tracker,
    scoped_refptr<browser_data_migrator_util::CancelFlag> cancel_flag) {
  LOG(WARNING) << "Running SetupLacrosDir()";
  base::ElapsedTimer timer;

  if (cancel_flag->IsSet()) {
    LOG(WARNING) << "Migration is cancelled.";
    return {TaskStatus::kCancelled};
  }
  const base::FilePath tmp_user_dir =
      original_profile_dir.Append(browser_data_migrator_util::kMoveTmpDir);
  const base::FilePath tmp_profile_dir =
      tmp_user_dir.Append(browser_data_migrator_util::kLacrosProfilePath);

  if (!base::CreateDirectory(tmp_user_dir)) {
    PLOG(ERROR) << "CreateDirectory() failed for  " << tmp_user_dir.value();
    return {TaskStatus::kSetupLacrosDirCreateTmpDirFailed, errno};
  }
  if (!base::CreateDirectory(tmp_profile_dir)) {
    PLOG(ERROR) << "CreateDirectory() failed for  " << tmp_profile_dir.value();
    return {TaskStatus::kSetupLacrosDirCreateTmpProfileDirFailed, errno};
  }

  browser_data_migrator_util::TargetItems need_copy_items =
      browser_data_migrator_util::GetTargetItems(
          original_profile_dir,
          browser_data_migrator_util::ItemType::kNeedCopyForMove);

  progress_tracker->SetTotalSizeToCopy(need_copy_items.total_size);

  base::ElapsedTimer timer_for_copy;
  if (!browser_data_migrator_util::CopyTargetItems(
          tmp_profile_dir, need_copy_items, cancel_flag.get(),
          progress_tracker.get())) {
    if (cancel_flag->IsSet()) {
      return {TaskStatus::kCancelled};
    }
    LOG(ERROR) << "CopyTargetItems() failed for need_copy_items.";
    return {TaskStatus::kSetupLacrosDirCopyTargetItemsFailed, errno};
  }
  UMA_HISTOGRAM_MEDIUM_TIMES(kMoveMigratorSetupLacrosDirCopyTargetItemsTimeUMA,
                             timer_for_copy.Elapsed());

  if (!base::WriteFile(tmp_user_dir.Append(chrome::kFirstRunSentinel), "")) {
    PLOG(ERROR) << "WriteFile() failed for " << chrome::kFirstRunSentinel;
    return {TaskStatus::kSetupLacrosDirWriteFirstRunSentinelFileFailed, errno};
  }

  return {TaskStatus::kSucceeded};
}

void MoveMigrator::OnSetupLacrosDir(TaskResult result) {
  if (result.status != TaskStatus::kSucceeded) {
    LOG(ERROR) << "MoveMigrator::SetupLacrosDir() failed.";
    if (cancel_flag_->IsSet()) {
      UMA_HISTOGRAM_MEDIUM_TIMES(kMoveMigratorCancelledMigrationTimeUMA,
                                 timer_.Elapsed());
    }
    InvokeCallback(result);
    return;
  }

  DCHECK(estimated_extra_bytes_created_.has_value());

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
       base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
      base::BindOnce(&MoveMigrator::SetupAshSplitDir, original_profile_dir_,
                     *estimated_extra_bytes_created_),
      base::BindOnce(&MoveMigrator::OnSetupAshSplitDir,
                     weak_factory_.GetWeakPtr()));
}

// static
MoveMigrator::TaskResult MoveMigrator::SetupAshSplitDir(
    const base::FilePath& original_profile_dir,
    const int64_t estimated_extra_bytes_created) {
  LOG(WARNING) << "Running SetupAshSplitDir()";

  const base::FilePath tmp_user_dir =
      original_profile_dir.Append(browser_data_migrator_util::kMoveTmpDir);
  const base::FilePath tmp_profile_dir =
      tmp_user_dir.Append(browser_data_migrator_util::kLacrosProfilePath);
  const base::FilePath tmp_split_dir =
      original_profile_dir.Append(browser_data_migrator_util::kSplitTmpDir);
  if (!base::CreateDirectory(tmp_split_dir)) {
    PLOG(ERROR) << "CreateDirectory() failed for  " << tmp_split_dir.value();
    return {TaskStatus::kSetupAshDirCreateSplitDirFailed, errno};
  }

  // Copy extensions that have to be in both Ash and Lacros.
  TaskResult task_result =
      CopyBothChromesSubdirs(original_profile_dir, tmp_split_dir,
                             browser_data_migrator_util::kExtensionsFilePath,
                             TaskStatus::kSetupAshDirCopyExtensionsFailed);
  if (task_result.status != TaskStatus::kSucceeded)
    return task_result;

  // Copy Storage objects for extensions that have to be both in Ash and Lacros.
  task_result = CopyBothChromesSubdirs(
      original_profile_dir, tmp_split_dir,
      base::FilePath(browser_data_migrator_util::kStorageFilePath)
          .Append(browser_data_migrator_util::kStorageExtFilePath)
          .value(),
      TaskStatus::kSetupAshDirCopyStorageFailed);
  if (task_result.status != TaskStatus::kSucceeded)
    return task_result;

  // Copy IndexedDB objects for extensions that have to be both in
  // Ash and Lacros.
  const base::FilePath original_indexed_db_dir = original_profile_dir.Append(
      browser_data_migrator_util::kIndexedDBFilePath);
  if (base::PathExists(original_indexed_db_dir)) {
    const base::FilePath split_indexed_db_dir =
        tmp_split_dir.Append(browser_data_migrator_util::kIndexedDBFilePath);
    if (!base::CreateDirectory(split_indexed_db_dir)) {
      PLOG(ERROR) << "CreateDirectory() failed for "
                  << split_indexed_db_dir.value();
      return {TaskStatus::kSetupAshDirCreateDirFailed, errno};
    }

    for (const auto& extension_id :
         extensions::GetExtensionsAndAppsRunInOSAndStandaloneBrowser()) {
      if (!browser_data_migrator_util::MigrateAshIndexedDB(
              original_profile_dir, split_indexed_db_dir, extension_id.data(),
              /*copy=*/true)) {
        return {TaskStatus::kSetupAshDirCopyIndexedDBFailed, errno};
      }
    }
  }

  // Create Ash's version of `Local Storage`, holding *only* the keys
  // associated to the extensions that have to stay in Ash.
  if (base::PathExists(
          original_profile_dir
              .Append(browser_data_migrator_util::kLocalStorageFilePath)
              .Append(browser_data_migrator_util::kLocalStorageLeveldbName))) {
    if (!browser_data_migrator_util::MigrateLevelDB(
            original_profile_dir
                .Append(browser_data_migrator_util::kLocalStorageFilePath)
                .Append(browser_data_migrator_util::kLocalStorageLeveldbName),
            tmp_split_dir
                .Append(browser_data_migrator_util::kLocalStorageFilePath)
                .Append(browser_data_migrator_util::kLocalStorageLeveldbName),
            browser_data_migrator_util::LevelDBType::kLocalStorage)) {
      LOG(ERROR) << "MigrateLevelDB() failed for `Local Storage`";
      return {TaskStatus::kSetupAshDirMigrateLevelDBForLocalStateFailed};
    }
  }

  // Create Ash's version of all the state stores (Extension State, etc.).
  for (const char* path : browser_data_migrator_util::kStateStorePaths) {
    if (base::PathExists(original_profile_dir.Append(path))) {
      if (!browser_data_migrator_util::MigrateLevelDB(
              original_profile_dir.Append(path), tmp_split_dir.Append(path),
              browser_data_migrator_util::LevelDBType::kStateStore)) {
        LOG(ERROR) << "MigrateLevelDB() failed for `" << path << "`";
        return {TaskStatus::kSetupAshDirMigrateLevelDBForStateFailed};
      }
    }
  }

  // Split Preferences.
  if (!browser_data_migrator_util::MigratePreferences(
          original_profile_dir.Append(chrome::kPreferencesFilename),
          tmp_split_dir.Append(chrome::kPreferencesFilename),
          tmp_profile_dir.Append(chrome::kPreferencesFilename))) {
    LOG(ERROR) << "MigratePreferences() failed.";
    return {TaskStatus::kSetupAshDirMigratePreferencesFailed};
  }

  // Split Sync Data.
  if (base::PathExists(
          original_profile_dir
              .Append(browser_data_migrator_util::kSyncDataFilePath)
              .Append(browser_data_migrator_util::kSyncDataLeveldbName))) {
    if (!browser_data_migrator_util::MigrateSyncDataLevelDB(
            original_profile_dir
                .Append(browser_data_migrator_util::kSyncDataFilePath)
                .Append(browser_data_migrator_util::kSyncDataLeveldbName),
            tmp_split_dir.Append(browser_data_migrator_util::kSyncDataFilePath)
                .Append(browser_data_migrator_util::kSyncDataLeveldbName),
            tmp_profile_dir
                .Append(browser_data_migrator_util::kSyncDataFilePath)
                .Append(browser_data_migrator_util::kSyncDataLeveldbName))) {
      LOG(ERROR) << "MigrateSyncDataLevelDB() failed";
      return {TaskStatus::kSetupAshDirMigrateSyncDataLevelDBFailed};
    }
  }

  // Collect UMAs on sizes of artifacts created by migration.
  const int64_t tmp_profile_dir_size =
      base::ComputeDirectorySize(tmp_profile_dir);
  const int64_t tmp_split_dir_size = base::ComputeDirectorySize(tmp_split_dir);
  const int64_t extra_bytes_created = tmp_profile_dir_size + tmp_split_dir_size;
  const int64_t extra_bytes_over_estimate =
      extra_bytes_created - estimated_extra_bytes_created;
  base::UmaHistogramCustomCounts(kMoveMigratorTmpProfileDirSize,
                                 tmp_profile_dir_size / 1024 / 1024, 1, 10000,
                                 100);
  base::UmaHistogramCustomCounts(kMoveMigratorTmpSplitDirSize,
                                 tmp_split_dir_size / 1024 / 1024, 1, 10000,
                                 100);
  base::UmaHistogramCustomCounts(kMoveMigratorExtraDiskSpaceOccupied,
                                 extra_bytes_created / 1024 / 1024, 1, 10000,
                                 100);
  base::UmaHistogramCustomCounts(kMoveMigratorExtraDiskSpaceOccupiedDiffWithEst,
                                 extra_bytes_over_estimate / 1024 / 1024,
                                 -10000, 10000, 100);

  return {TaskStatus::kSucceeded};
}

void MoveMigrator::OnSetupAshSplitDir(TaskResult result) {
  if (result.status != TaskStatus::kSucceeded) {
    LOG(ERROR) << "MoveMigrator::SetupAshSplitDir() failed.";
    InvokeCallback(result);
    return;
  }

  // Once `MoveLacrosItemsToNewDir()` is started, it should be completed.
  // Otherwise the profile in ash directory becomes fragmented. We store the
  // resume step as `kMoveLacrosItems` in Local State so that if the migration
  // is interrupted during `MoveLacrosItemsToNewDir()` then the migrator can
  // resume the migration from that point.
  SetResumeStep(local_state_, user_id_hash_, ResumeStep::kMoveLacrosItems);

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
       base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
      base::BindOnce(&MoveMigrator::MoveLacrosItemsToNewDir,
                     original_profile_dir_),
      base::BindOnce(&MoveMigrator::OnMoveLacrosItemsToNewDir,
                     weak_factory_.GetWeakPtr()));
}

// static
MoveMigrator::TaskResult MoveMigrator::MoveLacrosItemsToNewDir(
    const base::FilePath& original_profile_dir) {
  LOG(WARNING) << "Running MoveLacrosItemsToNewDir()";

  base::ElapsedTimer timer;

  browser_data_migrator_util::TargetItems lacros_items =
      browser_data_migrator_util::GetTargetItems(
          original_profile_dir, browser_data_migrator_util::ItemType::kLacros);

  for (const auto& item : lacros_items.items) {
    if (item.is_directory && !base::PathIsWritable(item.path)) {
      PLOG(ERROR) << "The current process does not have write permission to "
                     "the directory "
                  << item.path.value();
      return {TaskStatus::kMoveLacrosItemsToNewDirNoWritePerm, errno};
    }
  }

  const base::FilePath tmp_profile_dir =
      original_profile_dir.Append(browser_data_migrator_util::kMoveTmpDir)
          .Append(browser_data_migrator_util::kLacrosProfilePath);

  // Nigori file needs special handling, because it's not stored directly under
  // |original_profile_dir|.
  const base::FilePath original_nigori_path =
      original_profile_dir.Append(browser_data_migrator_util::kSyncDataFilePath)
          .Append(browser_data_migrator_util::kSyncDataNigoriFileName);
  if (base::PathExists(original_nigori_path)) {
    // In theory, `Sync Data` directory should be already created by
    // SplitSyncDataLevelDB() as long as DB exists. It still needs to be created
    // manually here, to handle the case when DB doesn't exist, but Nigori file
    // does.
    if (!base::CreateDirectory(tmp_profile_dir.Append(
            browser_data_migrator_util::kSyncDataFilePath))) {
      PLOG(ERROR) << "Failure while creating "
                  << tmp_profile_dir.Append(
                         browser_data_migrator_util::kSyncDataFilePath)
                  << " directory.";
      return {TaskStatus::kMoveLacrosItemsCreateDirFailed, errno};
    }

    const base::FilePath target_nigori_path =
        tmp_profile_dir.Append(browser_data_migrator_util::kSyncDataFilePath)
            .Append(browser_data_migrator_util::kSyncDataNigoriFileName);
    if (!base::Move(original_nigori_path, target_nigori_path)) {
      PLOG(ERROR) << "Failed to move item " << original_nigori_path.value()
                  << " to " << target_nigori_path << ": ";
      return {TaskStatus::kMoveLacrosItemsToNewDirMoveFailed, errno};
    }
  }

  for (const auto& item : lacros_items.items) {
    if (!base::Move(item.path, tmp_profile_dir.Append(item.path.BaseName()))) {
      PLOG(ERROR) << "Failed to move item " << item.path.value() << " to "
                  << tmp_profile_dir.Append(item.path.BaseName()) << ": ";
      return {TaskStatus::kMoveLacrosItemsToNewDirMoveFailed, errno};
    }
  }

  UMA_HISTOGRAM_MEDIUM_TIMES(kMoveMigratorMoveLacrosItemsTimeUMA,
                             timer.Elapsed());
  return {TaskStatus::kSucceeded};
}

void MoveMigrator::OnMoveLacrosItemsToNewDir(TaskResult result) {
  if (result.status != TaskStatus::kSucceeded) {
    LOG(ERROR) << "Moving Lacros items to temporary directory failed.";
    InvokeCallback(result);
    return;
  }

  SetResumeStep(local_state_, user_id_hash_, ResumeStep::kMoveSplitItems);

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
       base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
      base::BindOnce(&MoveMigrator::MoveSplitItemsToOriginalDir,
                     original_profile_dir_),
      base::BindOnce(&MoveMigrator::OnMoveSplitItemsToOriginalDir,
                     weak_factory_.GetWeakPtr()));
}

// static
MoveMigrator::TaskResult MoveMigrator::MoveSplitItemsToOriginalDir(
    const base::FilePath& original_profile_dir) {
  LOG(WARNING) << "Running MoveSplitItemsToOriginalDir()";

  const base::FilePath tmp_split_dir =
      original_profile_dir.Append(browser_data_migrator_util::kSplitTmpDir);
  const base::FilePath tmp_profile_dir =
      original_profile_dir.Append(browser_data_migrator_util::kMoveTmpDir)
          .Append(browser_data_migrator_util::kLacrosProfilePath);

  // Move everything inside tmp_split_dir to Ash's profile directory.
  base::FileEnumerator e(
      tmp_split_dir, false,
      base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES);

  for (base::FilePath path = e.Next(); !path.empty(); path = e.Next()) {
    base::FilePath ash_path = original_profile_dir.Append(path.BaseName());
    if (base::DirectoryExists(ash_path) && !DeletePathRecursively(ash_path)) {
      PLOG(ERROR) << "Failed deleting " << ash_path.value();
      return {TaskStatus::kMoveSplitItemsToOriginalDirMoveSplitItemsFailed,
              errno};
    }

    if (!base::Move(path, ash_path)) {
      PLOG(ERROR) << "Failed moving " << path.value() << " to "
                  << ash_path.value();
      return {TaskStatus::kMoveSplitItemsToOriginalDirMoveSplitItemsFailed,
              errno};
    }
  }

  // Delete tmp_split_dir.
  if (!base::DeleteFile(tmp_split_dir)) {
    PLOG(ERROR) << "Failed removing " << tmp_split_dir.value();
  }

  // Move extensions in the keeplist back to Ash's profile directory.
  TaskResult task_result = MoveAshSubdirs(
      tmp_profile_dir, original_profile_dir,
      browser_data_migrator_util::kExtensionsFilePath,
      TaskStatus::kMoveSplitItemsToOriginalDirMoveExtensionsFailed);
  if (task_result.status != TaskStatus::kSucceeded)
    return task_result;

  // Move Storage objects related to extensions in the keeplist back to Ash's
  // profile directory.
  task_result = MoveAshSubdirs(
      tmp_profile_dir, original_profile_dir,
      base::FilePath(browser_data_migrator_util::kStorageFilePath)
          .Append(browser_data_migrator_util::kStorageExtFilePath)
          .value(),
      TaskStatus::kMoveSplitItemsToOriginalDirMoveStorageFailed);
  if (task_result.status != TaskStatus::kSucceeded)
    return task_result;

  // Move IndexedDB objects related to extensions in the keeplist back to Ash's
  // profile directory.
  const base::FilePath lacros_indexed_db_dir =
      tmp_profile_dir.Append(browser_data_migrator_util::kIndexedDBFilePath);
  if (base::PathExists(lacros_indexed_db_dir)) {
    const base::FilePath ash_indexed_db_dir = original_profile_dir.Append(
        browser_data_migrator_util::kIndexedDBFilePath);
    if (!base::CreateDirectory(ash_indexed_db_dir)) {
      PLOG(ERROR) << "CreateDirectory() failed for  "
                  << ash_indexed_db_dir.value();
      return {TaskStatus::kMoveSplitItemsToOriginalDirCreateDirFailed, errno};
    }

    for (const char* extension_id :
         browser_data_migrator_util::kExtensionsAshOnly) {
      if (!browser_data_migrator_util::MigrateAshIndexedDB(
              tmp_profile_dir, ash_indexed_db_dir, extension_id,
              /*copy=*/false)) {
        return {TaskStatus::kMoveSplitItemsToOriginalDirMoveIndexedDBFailed,
                errno};
      }
    }
  }

  return {TaskStatus::kSucceeded};
}

void MoveMigrator::OnMoveSplitItemsToOriginalDir(TaskResult result) {
  if (result.status != TaskStatus::kSucceeded) {
    LOG(ERROR) << "Moving split objects has failed.";
    InvokeCallback(result);
    return;
  }

  SetResumeStep(local_state_, user_id_hash_, ResumeStep::kMoveTmpDir);

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
       base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
      base::BindOnce(&MoveMigrator::MoveTmpDirToLacrosDir,
                     original_profile_dir_),
      base::BindOnce(&MoveMigrator::OnMoveTmpDirToLacrosDir,
                     weak_factory_.GetWeakPtr()));
}

// static
MoveMigrator::TaskResult MoveMigrator::MoveTmpDirToLacrosDir(
    const base::FilePath& original_profile_dir) {
  LOG(WARNING) << "Running MoveTmpDirToLacrosDir()";

  // Move the newly created lacros user data dir into the final location where
  // lacros chrome can access.
  if (!base::Move(
          original_profile_dir.Append(browser_data_migrator_util::kMoveTmpDir),
          original_profile_dir.Append(
              browser_data_migrator_util::kLacrosDir))) {
    PLOG(ERROR) << "Failed moving "
                << original_profile_dir
                       .Append(browser_data_migrator_util::kMoveTmpDir)
                       .value()
                << " to "
                << original_profile_dir
                       .Append(browser_data_migrator_util::kLacrosDir)
                       .value();
    return {TaskStatus::kMoveTmpDirToLacrosDirMoveFailed, errno};
  }

  return {TaskStatus::kSucceeded};
}

void MoveMigrator::OnMoveTmpDirToLacrosDir(TaskResult result) {
  if (result.status != TaskStatus::kSucceeded) {
    LOG(ERROR) << "Moving tmp dir to lacros dir failed.";
    InvokeCallback(result);
    return;
  }

  UMA_HISTOGRAM_MEDIUM_TIMES(kMoveMigratorSuccessfulMigrationTimeUMA,
                             timer_.Elapsed());
  SetResumeStep(local_state_, user_id_hash_, ResumeStep::kCompleted);

  LOG(WARNING) << "Move migration completed successfully.";
  InvokeCallback(result);
}

// static
MoveMigrator::TaskResult MoveMigrator::CopyBothChromesSubdirs(
    const base::FilePath& original_profile_dir,
    const base::FilePath& tmp_split_dir,
    const std::string& target_dir,
    TaskStatus copy_fail_status) {
  const base::FilePath original_target_dir =
      original_profile_dir.Append(target_dir);

  if (base::PathExists(original_target_dir)) {
    const base::FilePath split_target_dir = tmp_split_dir.Append(target_dir);
    if (!base::CreateDirectory(split_target_dir)) {
      PLOG(ERROR) << "CreateDirectory() failed for  "
                  << split_target_dir.value();
      return {TaskStatus::kSetupAshDirCreateDirFailed, errno};
    }

    // Copy objects that belong to both Ash and Lacros.
    for (const auto& extension_id :
         extensions::GetExtensionsAndAppsRunInOSAndStandaloneBrowser()) {
      base::FilePath original_target_path =
          original_target_dir.Append(extension_id);

      if (base::PathExists(original_target_path)) {
        base::FilePath split_target_path =
            split_target_dir.Append(extension_id);

        if (!base::CopyDirectory(original_target_path, split_target_path,
                                 /*recursive=*/true)) {
          PLOG(ERROR) << "Failed copying " << original_target_path.value()
                      << " to " << split_target_path.value();
          return {copy_fail_status, errno};
        }
      }
    }
  }

  return {TaskStatus::kSucceeded};
}

// static
MoveMigrator::TaskResult MoveMigrator::MoveAshSubdirs(
    const base::FilePath& tmp_profile_dir,
    const base::FilePath& original_profile_dir,
    const std::string& target_dir,
    TaskStatus move_fail_status) {
  const base::FilePath lacros_target_dir = tmp_profile_dir.Append(target_dir);

  if (base::PathExists(lacros_target_dir)) {
    const base::FilePath ash_target_dir =
        original_profile_dir.Append(target_dir);
    if (!base::CreateDirectory(ash_target_dir)) {
      PLOG(ERROR) << "CreateDirectory() failed for  " << ash_target_dir.value();
      return {TaskStatus::kMoveSplitItemsToOriginalDirCreateDirFailed, errno};
    }

    // Move Ash-only objects.
    for (const char* extension_id :
         browser_data_migrator_util::kExtensionsAshOnly) {
      base::FilePath lacros_path = lacros_target_dir.Append(extension_id);
      if (base::PathExists(lacros_path)) {
        base::FilePath ash_path = ash_target_dir.Append(extension_id);
        if (!base::Move(lacros_path, ash_path)) {
          PLOG(ERROR) << "Failed moving " << lacros_path.value() << " to "
                      << ash_path.value();
          return {move_fail_status, errno};
        }
      }
    }
  }

  return {TaskStatus::kSucceeded};
}

void MoveMigrator::InvokeCallback(TaskResult result) {
  UMA_HISTOGRAM_ENUMERATION(kMoveMigratorTaskStatusUMA, result.status);
  if (result.status != TaskStatus::kSucceeded &&
      result.posix_errno.has_value()) {
    RecordPosixErrnoUMA(result.status, result.posix_errno.value());
  }

  std::move(finished_callback_)
      .Run(ToBrowserDataMigratorMigrationResult(result));
}

BrowserDataMigratorImpl::MigrationResult
MoveMigrator::ToBrowserDataMigratorMigrationResult(TaskResult result) {
  switch (result.status) {
    case TaskStatus::kSucceeded:
      return {BrowserDataMigratorImpl::DataWipeResult::kSucceeded,
              {BrowserDataMigratorImpl::ResultKind::kSucceeded}};
    case TaskStatus::kCancelled:
      return {BrowserDataMigratorImpl::DataWipeResult::kSucceeded,
              {BrowserDataMigratorImpl::ResultKind::kCancelled}};
    case TaskStatus::kPreMigrationCleanUpDeleteLacrosDirFailed:
    case TaskStatus::kPreMigrationCleanUpDeleteTmpDirFailed:
    case TaskStatus::kPreMigrationCleanUpDeleteTmpSplitDirFailed:
      return {BrowserDataMigratorImpl::DataWipeResult::kFailed,
              {BrowserDataMigratorImpl::ResultKind::kFailed}};
    case TaskStatus::kPreMigrationCleanUpNotEnoughSpace:
      return {BrowserDataMigratorImpl::DataWipeResult::kSucceeded,
              {BrowserDataMigratorImpl::ResultKind::kFailed,
               result.extra_bytes_required_to_be_freed.value()}};
    case TaskStatus::kSetupLacrosDirCreateTmpDirFailed:
    case TaskStatus::kSetupLacrosDirCreateTmpProfileDirFailed:
    case TaskStatus::kSetupLacrosDirCopyTargetItemsFailed:
    case TaskStatus::kSetupLacrosDirWriteFirstRunSentinelFileFailed:
    case TaskStatus::kSetupAshDirCreateSplitDirFailed:
    case TaskStatus::kSetupAshDirMigrateLevelDBForLocalStateFailed:
    case TaskStatus::kSetupAshDirMigrateLevelDBForStateFailed:
    case TaskStatus::kSetupAshDirMigratePreferencesFailed:
    case TaskStatus::kMoveLacrosItemsToNewDirNoWritePerm:
    case TaskStatus::kMoveLacrosItemsToNewDirMoveFailed:
    case TaskStatus::kMoveSplitItemsToOriginalDirMoveSplitItemsFailed:
    case TaskStatus::kMoveSplitItemsToOriginalDirCreateDirFailed:
    case TaskStatus::kMoveSplitItemsToOriginalDirMoveExtensionsFailed:
    case TaskStatus::kMoveSplitItemsToOriginalDirMoveIndexedDBFailed:
    case TaskStatus::kMoveTmpDirToLacrosDirMoveFailed:
    case TaskStatus::kSetupAshDirCreateDirFailed:
    case TaskStatus::kSetupAshDirCopyExtensionsFailed:
    case TaskStatus::kSetupAshDirCopyIndexedDBFailed:
    case TaskStatus::kSetupAshDirMigrateSyncDataLevelDBFailed:
    case TaskStatus::kSetupAshDirCopyStorageFailed:
    case TaskStatus::kMoveSplitItemsToOriginalDirMoveStorageFailed:
    case TaskStatus::kMoveLacrosItemsCreateDirFailed:
      return {BrowserDataMigratorImpl::DataWipeResult::kSucceeded,
              {BrowserDataMigratorImpl::ResultKind::kFailed}};
  }
}

// static
void MoveMigrator::RecordPosixErrnoUMA(TaskStatus task_status,
                                       const int posix_errno) {
  if (posix_errno == 0)
    return;

  std::string uma_name =
      kMoveMigratorPosixErrnoUMA + TaskStatusToString(task_status);
  base::UmaHistogramSparse(uma_name, posix_errno);
}

// static
std::string MoveMigrator::TaskStatusToString(TaskStatus task_status) {
  switch (task_status) {
#define MAPPING(name)       \
  case TaskStatus::k##name: \
    return #name
    MAPPING(Succeeded);
    MAPPING(Cancelled);
    MAPPING(PreMigrationCleanUpDeleteLacrosDirFailed);
    MAPPING(PreMigrationCleanUpDeleteTmpDirFailed);
    MAPPING(PreMigrationCleanUpDeleteTmpSplitDirFailed);
    MAPPING(PreMigrationCleanUpNotEnoughSpace);
    MAPPING(SetupLacrosDirCreateTmpDirFailed);
    MAPPING(SetupLacrosDirCreateTmpProfileDirFailed);
    MAPPING(SetupLacrosDirCopyTargetItemsFailed);
    MAPPING(SetupLacrosDirWriteFirstRunSentinelFileFailed);
    MAPPING(SetupAshDirCreateSplitDirFailed);
    MAPPING(SetupAshDirMigrateLevelDBForLocalStateFailed);
    MAPPING(SetupAshDirMigrateLevelDBForStateFailed);
    MAPPING(SetupAshDirMigratePreferencesFailed);
    MAPPING(MoveLacrosItemsToNewDirNoWritePerm);
    MAPPING(MoveLacrosItemsToNewDirMoveFailed);
    MAPPING(MoveSplitItemsToOriginalDirMoveSplitItemsFailed);
    MAPPING(MoveSplitItemsToOriginalDirCreateDirFailed);
    MAPPING(MoveSplitItemsToOriginalDirMoveExtensionsFailed);
    MAPPING(MoveSplitItemsToOriginalDirMoveIndexedDBFailed);
    MAPPING(MoveTmpDirToLacrosDirMoveFailed);
    MAPPING(SetupAshDirCreateDirFailed);
    MAPPING(SetupAshDirCopyExtensionsFailed);
    MAPPING(SetupAshDirCopyIndexedDBFailed);
    MAPPING(SetupAshDirMigrateSyncDataLevelDBFailed);
    MAPPING(SetupAshDirCopyStorageFailed);
    MAPPING(MoveSplitItemsToOriginalDirMoveStorageFailed);
    MAPPING(MoveLacrosItemsCreateDirFailed);
#undef MAPPING
  }
}

}  // namespace ash