// 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/web_applications/chrome_pwa_launcher/launcher_update.h"
#include "base/check.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "chrome/browser/web_applications/chrome_pwa_launcher/chrome_pwa_launcher_util.h"
#include "chrome/installer/util/callback_work_item.h"
#include "chrome/installer/util/delete_tree_work_item.h"
#include "chrome/installer/util/work_item_list.h"
namespace {
constexpr base::FilePath::StringPieceType kOldLauncherSuffix =
FILE_PATH_LITERAL("_old");
// A callback invoked by |work_item| that tries to create a hardlink to
// |latest_version_path| at |launcher_path|. If it fails, tries to create a copy
// of |latest_version_path| at |launcher_path| instead. Returns true if either a
// hardlink or copy were created, or false otherwise.
bool CreateHardLinkOrCopyCallback(const base::FilePath& launcher_path,
const base::FilePath& latest_version_path,
const CallbackWorkItem& work_item) {
return base::CreateWinHardLink(launcher_path, latest_version_path) ||
base::CopyFile(latest_version_path, launcher_path);
}
// A callback invoked by |work_item| that deletes the file at |launcher_path|.
void DeleteHardLinkOrCopyCallback(const base::FilePath& launcher_path,
const CallbackWorkItem& work_item) {
base::DeleteFile(launcher_path);
}
// Replaces |launcher_path| with the one at |latest_version_path|. This is done
// by atomically renaming |launcher_path| to |old_path| and creating a hardlink
// to or copy of |latest_version_path| at |launcher_path|. Makes a best-effort
// attempt to delete |old_path|. Aside from the best-effort deletion, all
// changes are rolled back if any step fails.
void ReplaceLauncherWithLatestVersion(const base::FilePath& launcher_path,
const base::FilePath& latest_version_path,
const base::FilePath& old_path) {
if (!base::PathExists(latest_version_path))
return;
// Create a temporary backup directory for use while moving in-use files.
base::ScopedTempDir temp_dir;
if (!temp_dir.CreateUniqueTempDirUnderPath(launcher_path.DirName()))
return;
// Move |launcher_path| to |old_path|.
std::unique_ptr<WorkItemList> change_list(WorkItem::CreateWorkItemList());
change_list->AddMoveTreeWorkItem(launcher_path, old_path, temp_dir.GetPath(),
WorkItem::ALWAYS_MOVE);
// Create a hardlink or copy of |latest_version_path| at |launcher_path|.
change_list->AddCallbackWorkItem(
base::BindOnce(&CreateHardLinkOrCopyCallback, launcher_path,
latest_version_path),
base::BindOnce(&DeleteHardLinkOrCopyCallback, launcher_path));
// Make a best-effort, no-rollback attempt to delete |old_path|; deletion
// will fail when |old_path| is still in use.
std::unique_ptr<DeleteTreeWorkItem> delete_old_version_work_item(
WorkItem::CreateDeleteTreeWorkItem(old_path, temp_dir.GetPath()));
delete_old_version_work_item->set_best_effort(true);
delete_old_version_work_item->set_rollback_enabled(false);
change_list->AddWorkItem(delete_old_version_work_item.release());
if (!change_list->Do())
change_list->Rollback();
}
// Deletes |old_path| and any variations on it (e.g., |old_path| (1), |old_path|
// (2), etc.) if they exist. |old_path| is renamed to a unique name before
// deletion to ensure its filename is available for use immediately (as
// Windows' file deletion returns success before the deleted file's name
// actually becomes available).
void CleanUpOldLauncherVersions(const base::FilePath& old_path) {
// If |old_path| exists, rename it to |unique_path| and delete it.
const base::FilePath unique_path = base::GetUniquePath(old_path);
if (!unique_path.empty() && unique_path != old_path) {
base::Move(old_path, unique_path);
base::DeleteFile(unique_path);
}
// Delete any old versions of |unique_path| that may exist from failed delete
// attempts (e.g., if antivirus software prevented deletion of |unique_path|).
base::FileEnumerator files(
old_path.DirName(), /*recursive=*/false, base::FileEnumerator::FILES,
old_path.BaseName()
.InsertBeforeExtension(FILE_PATH_LITERAL(" (*)"))
.value());
for (base::FilePath file = files.Next(); !file.empty(); file = files.Next()) {
base::DeleteFile(file);
}
}
} // namespace
namespace web_app {
void UpdatePwaLaunchers(std::vector<base::FilePath> launcher_paths) {
const base::FilePath latest_version_path = GetChromePwaLauncherPath();
for (const auto& path : launcher_paths) {
DCHECK(!path.empty());
const base::FilePath old_path =
path.InsertBeforeExtension(kOldLauncherSuffix);
CleanUpOldLauncherVersions(old_path);
// Make a hardlink or copy of |latest_version_path|, and replace the current
// launcher with it.
if (base::PathExists(path))
ReplaceLauncherWithLatestVersion(path, latest_version_path, old_path);
}
}
} // namespace web_app