blob: 444d255a5a2cbdc02a419529231c476c3b71baa6 [file] [log] [blame]
// Copyright 2020 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/downgrade/snapshot_manager.h"
#include <utility>
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/trace_event/trace_event.h"
#include "chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h"
#include "chrome/browser/downgrade/downgrade_utils.h"
#include "chrome/browser/downgrade/snapshot_file_collector.h"
#include "chrome/browser/downgrade/user_data_downgrade.h"
#include "chrome/common/chrome_constants.h"
namespace downgrade {
namespace {
constexpr base::FilePath::StringViewType kSQLiteJournalSuffix(
FILE_PATH_LITERAL("-journal"));
constexpr base::FilePath::StringViewType kSQLiteWalSuffix(
FILE_PATH_LITERAL("-wal"));
constexpr base::FilePath::StringViewType kSQLiteShmSuffix(
FILE_PATH_LITERAL("-shm"));
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class SnapshotOperationResult {
kSuccess = 0,
kPartialSuccess = 1,
kFailure = 2,
kFailedToCreateSnapshotDirectory = 3,
kMaxValue = kFailedToCreateSnapshotDirectory
};
// Copies the item at |user_data_dir|/|relative_path| to
// |snapshot_dir|/|relative_path| if the item exists. This also copies all files
// related to items that are SQLite databases. Returns |true| if the item was
// found at the source and successfully copied. Returns |false| if the item was
// found at the source but not successfully copied. Returns no value if the file
// was not at the source.
std::optional<bool> CopyItemToSnapshotDirectory(
const base::FilePath& relative_path,
const base::FilePath& user_data_dir,
const base::FilePath& snapshot_dir,
bool is_directory) {
const auto source = user_data_dir.Append(relative_path);
const auto destination = snapshot_dir.Append(relative_path);
// If nothing exists to be moved, do not consider it a success or a failure.
if (!base::PathExists(source))
return std::nullopt;
bool copy_success = is_directory ? base::CopyDirectory(source, destination,
/*recursive=*/true)
: base::CopyFile(source, destination);
if (is_directory)
return copy_success;
// Copy SQLite journal, WAL and SHM files associated with the files that are
// snapshotted if they exist.
for (const auto& suffix :
{kSQLiteJournalSuffix, kSQLiteWalSuffix, kSQLiteShmSuffix}) {
const auto sqlite_file_path =
base::FilePath(source.value() + base::FilePath::StringType(suffix));
if (!base::PathExists(sqlite_file_path))
continue;
const auto destination_journal = base::FilePath(
destination.value() + base::FilePath::StringType(suffix));
copy_success &= base::CopyFile(sqlite_file_path, destination_journal);
}
return copy_success;
}
// Returns true if |base_name| matches a user profile directory's format. This
// function will ignore the "System Profile" directory.
bool IsProfileDir(const base::FilePath& base_name) {
// The initial profile ("Default") is an easy one.
if (base_name == base::FilePath().AppendASCII(chrome::kInitialProfile))
return true;
// Other profile dirs begin with "Profile " and end with a number.
const base::FilePath prefix(
base::FilePath().AppendASCII(chrome::kMultiProfileDirPrefix));
int number;
return base::StartsWith(base_name.value(), prefix.value(),
base::CompareCase::SENSITIVE) &&
base::StringToInt(base::FilePath::StringViewType(base_name.value())
.substr(prefix.value().length()),
&number);
}
// Returns a list of profile directory base names under |user_data_dir|.
std::vector<base::FilePath> GetUserProfileDirectories(
const base::FilePath& user_data_dir) {
std::vector<base::FilePath> profile_dirs;
base::FileEnumerator enumerator(user_data_dir, /*recursive=*/false,
base::FileEnumerator::DIRECTORIES);
for (base::FilePath path = enumerator.Next(); !path.empty();
path = enumerator.Next()) {
const auto base_name = path.BaseName();
if (IsProfileDir(base_name))
profile_dirs.push_back(std::move(base_name));
}
return profile_dirs;
}
// Moves the |source| directory to |target| to be deleted later. If the move
// initially fails, move the contents of the directory.
void MoveFolderForLaterDeletion(const base::FilePath& source,
const base::FilePath& target) {
if (MoveWithoutFallback(source, target))
return;
// If some files failed to be moved, try and delete them immediately.
if (!MoveContents(source, base::GetUniquePath(target),
ExclusionPredicate())) {
base::DeletePathRecursively(source);
}
}
} // namespace
SnapshotManager::SnapshotManager(const base::FilePath& user_data_dir)
: user_data_dir_(user_data_dir) {}
SnapshotManager::~SnapshotManager() = default;
void SnapshotManager::TakeSnapshot(const base::Version& version) {
TRACE_EVENT0("browser", "SnapshotManager::TakeSnapshot");
DCHECK(version.IsValid());
base::FilePath snapshot_dir =
user_data_dir_.Append(kSnapshotsDir).AppendASCII(version.GetString());
// If the target snapshot directory already exists, try marking it for
// deletion. In case of failure, try moving the contents then keep going.
if (base::PathExists(snapshot_dir)) {
auto move_target_dir = user_data_dir_.Append(kSnapshotsDir)
.AddExtension(kDowngradeDeleteSuffix);
base::CreateDirectory(move_target_dir);
// This succeeds more than 80% of the time.
MoveFolderForLaterDeletion(
snapshot_dir, move_target_dir.AppendASCII(version.GetString()));
}
auto record_item_failure = [](std::optional<bool> success,
SnapshotItemId id) {
if (!success.value_or(true))
base::UmaHistogramEnumeration("Downgrade.TakeSnapshot.ItemFailure", id);
};
// Abort the snapshot if the snapshot directory could not be created.
if (!base::CreateDirectory(snapshot_dir))
return;
// Copy items to be preserved at the top-level of User Data.
for (const auto& file : GetUserSnapshotItemDetails()) {
record_item_failure(
CopyItemToSnapshotDirectory(base::FilePath(file.path), user_data_dir_,
snapshot_dir, file.is_directory),
file.id);
}
const auto profile_snapshot_item_details = GetProfileSnapshotItemDetails();
// Copy items to be preserved in each Profile directory.
for (const auto& profile_dir : GetUserProfileDirectories(user_data_dir_)) {
// Abort the current profile snapshot if the profile directory could not be
// created. This succeeds almost all the time.
if (!base::CreateDirectory(snapshot_dir.Append(profile_dir)))
continue;
for (const auto& file : profile_snapshot_item_details) {
record_item_failure(CopyItemToSnapshotDirectory(
profile_dir.Append(file.path), user_data_dir_,
snapshot_dir, file.is_directory),
file.id);
}
}
// Copy the "Last Version" file to the snapshot directory last since it is the
// file that determines, by its presence in the snapshot directory, if the
// snapshot is complete.
record_item_failure(
CopyItemToSnapshotDirectory(base::FilePath(kDowngradeLastVersionFile),
user_data_dir_, snapshot_dir,
/*is_directory=*/false),
SnapshotItemId::kLastVersion);
}
void SnapshotManager::RestoreSnapshot(const base::Version& version) {
TRACE_EVENT0("browser", "SnapshotManager::RestoreSnapshot");
DCHECK(version.IsValid());
auto snapshot_version = GetSnapshotToRestore(version, user_data_dir_);
if (!snapshot_version)
return;
// The snapshot folder needs to be moved if it matches the current chrome
// milestone. However, if it comes from an earlier version, it should be
// copied so that a future downgrade to that version stays possible.
const bool move_snapshot = snapshot_version == version;
auto snapshot_dir = user_data_dir_.Append(kSnapshotsDir)
.AppendASCII(snapshot_version->GetString());
bool has_success = false;
bool has_error = false;
auto record_success_error = [&has_success, &has_error](bool success) {
has_success |= success;
has_error |= !success;
};
base::FileEnumerator enumerator(
snapshot_dir, /*recursive=*/false,
base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES);
// Move or copy the contents of the selected snapshot directory into User
// Data.
for (base::FilePath path = enumerator.Next(); !path.empty();
path = enumerator.Next()) {
const auto item_info = enumerator.GetInfo();
const auto target_path = user_data_dir_.Append(path.BaseName());
if (move_snapshot)
record_success_error(base::Move(path, target_path));
else if (enumerator.GetInfo().IsDirectory())
record_success_error(
base::CopyDirectory(path, target_path, /*recursive=*/true));
else
record_success_error(base::CopyFile(path, target_path));
}
// When there is a partial success, according to
// "Downgrade.RestoreSnapshot.FailureCount", the average number of items that
// fail to be recovered is between 2 and 3.
base::UmaHistogramEnumeration(
"Downgrade.RestoreSnapshot.Result",
!has_error ? SnapshotOperationResult::kSuccess
: has_success ? SnapshotOperationResult::kPartialSuccess
: SnapshotOperationResult::kFailure);
// Mark the snapshot directory for later deletion if its contents were moved
// into User Data. If the snapshot directory cannot be renamed, fallback to
// moving its contents.
if (move_snapshot) {
auto move_target =
GetTempDirNameForDelete(user_data_dir_, base::FilePath(kSnapshotsDir))
.Append(snapshot_dir.BaseName());
// Cleans up the remnants of the moved snapshot directory, this is
// successful 99% of the time. If moving the directory fails, delete the
// "Last Version" file so that this snapshot is considered incomplete and
// deleted later. In case of failure to move the directory, if the Last
// Version file is deleted, this snapshot will now be considered invalid,
// and will be deleted, otherwise it will be overwritten at the next
// upgrade.
MoveFolderForLaterDeletion(snapshot_dir, move_target);
auto last_version_file_path =
snapshot_dir.Append(kDowngradeLastVersionFile);
base::DeleteFile(last_version_file_path);
}
}
void SnapshotManager::PurgeInvalidAndOldSnapshots(
int max_number_of_snapshots,
std::optional<uint32_t> milestone) const {
const auto snapshot_dir = user_data_dir_.Append(kSnapshotsDir);
// Move the invalid snapshots within from Snapshots/NN to Snapshots.DELETE/NN.
const base::FilePath target =
snapshot_dir.AddExtension(kDowngradeDeleteSuffix);
base::CreateDirectory(target);
// Moves all the invalid snapshots for later deletion.
auto invalid_snapshots = GetInvalidSnapshots(snapshot_dir);
for (const auto& path : invalid_snapshots) {
// This succeeds 97% of the time according to
// Downgrade.InvalidSnapshotMove.Result, with most of the failures having
// under 4 files failing to be copied.
MoveFolderForLaterDeletion(path, target.Append(path.BaseName()));
}
base::flat_set<base::Version> available_snapshots =
GetAvailableSnapshots(snapshot_dir);
if (milestone.has_value()) {
// Only consider versions for the specified milestone.
available_snapshots.erase(available_snapshots.upper_bound(
base::Version({*milestone + 1, 0, 0, 0})),
available_snapshots.end());
available_snapshots.erase(
available_snapshots.begin(),
available_snapshots.lower_bound(base::Version({*milestone, 0, 0, 0})));
}
if (available_snapshots.size() <=
base::checked_cast<size_t>(max_number_of_snapshots)) {
return;
}
size_t number_of_snapshots_to_delete =
available_snapshots.size() - max_number_of_snapshots;
// Moves all the older snapshots for later deletion.
for (const auto& snapshot : available_snapshots) {
auto snapshot_path = snapshot_dir.AppendASCII(snapshot.GetString());
// This succeeds 97% of the time according to
// Downgrade.InvalidSnapshotMove.Result, with most of the failures having
// under 4 files failing to be copied.
MoveFolderForLaterDeletion(snapshot_path,
target.Append(snapshot_path.BaseName()));
if (--number_of_snapshots_to_delete == 0)
break;
}
}
void SnapshotManager::DeleteSnapshotDataForProfile(
base::Time delete_begin,
const base::FilePath& profile_base_name,
uint64_t remove_mask) {
using chrome_browsing_data_remover::ALL_DATA_TYPES;
using chrome_browsing_data_remover::WIPE_PROFILE;
bool delete_all = (((remove_mask & WIPE_PROFILE) == WIPE_PROFILE) ||
((remove_mask & ALL_DATA_TYPES) == ALL_DATA_TYPES)) &&
delete_begin.is_null();
std::vector<base::FilePath> files_to_delete;
if (!delete_all) {
for (const auto& item : CollectProfileItems()) {
if (item.data_types & remove_mask)
files_to_delete.push_back(item.path);
}
}
if (!delete_all && files_to_delete.empty())
return;
const auto snapshot_dir = user_data_dir_.Append(kSnapshotsDir);
auto available_snapshots = GetAvailableSnapshots(snapshot_dir);
base::File::Info file_info;
for (const auto& snapshot : available_snapshots) {
auto snapshot_path = snapshot_dir.AppendASCII(snapshot.GetString());
// If we are not able to get the file info, it probably has been deleted.
if (!base::GetFileInfo(snapshot_path, &file_info))
continue;
auto profile_absolute_path = snapshot_path.Append(profile_base_name);
// Deletes the whole profile from the snapshots if it is being wiped
// regardless of |delete_begin|, otherwise deletes the required files from
// the snapshot if it was created after |delete_begin|.
if (delete_all) {
base::DeletePathRecursively(profile_absolute_path);
} else if (delete_begin <= file_info.creation_time &&
base::PathExists(profile_absolute_path)) {
for (const auto& filename : files_to_delete) {
base::DeletePathRecursively(profile_absolute_path.Append(filename));
}
// Non recursive deletion will fail if the directory is not empty. In this
// case we only want to delete the directory if it is empty.
base::DeleteFile(profile_absolute_path);
}
}
}
std::vector<SnapshotItemDetails>
SnapshotManager::GetProfileSnapshotItemDetails() const {
return CollectProfileItems();
}
std::vector<SnapshotItemDetails> SnapshotManager::GetUserSnapshotItemDetails()
const {
return CollectUserDataItems();
}
} // namespace downgrade