blob: 4784e4d2b01fd3ed23e81c2d8ec680706034cb0c [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/offline_pages/core/model/delete_page_task.h"
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/offline_pages/core/client_policy_controller.h"
#include "components/offline_pages/core/model/offline_page_model_utils.h"
#include "components/offline_pages/core/offline_clock.h"
#include "components/offline_pages/core/offline_page_client_policy.h"
#include "components/offline_pages/core/offline_page_metadata_store.h"
#include "components/offline_pages/core/offline_page_model.h"
#include "components/offline_pages/core/offline_page_types.h"
#include "components/offline_pages/core/offline_store_utils.h"
#include "sql/database.h"
#include "sql/statement.h"
#include "sql/transaction.h"
namespace offline_pages {
using DeletePageTaskResult = DeletePageTask::DeletePageTaskResult;
namespace {
#define OFFLINE_PAGES_TABLE_NAME "offlinepages_v1"
// A wrapper of DeletedPageInfo to include |file_path| in order to be used
// through the deletion process. This is implementation detail and it will be
// used to create OfflinePageModel::DeletedPageInfo that are passed through
// callback.
// Please keep WRAPPER_FIELDS, WRAPPER_FIELD_COUNT, the struct declaration of
// DeletedPageInfoWrapper and the method CreateInfoWrapper in sync.
// The WRAPPER_FIELD_COUNT is used for queries which requires more info than the
// fields of INFO_WRAPPER_FIELD, as the additional field can be added manually
// in the SQL query and the result of it can be simply fetched by calling
// statement.Column*(INFO_WRAPPER_COUNT), as it's the last column. For example,
// please take a look at GetCachedDeletedPageInfoWrappersByUrlPredicateSync.
#define INFO_WRAPPER_FIELDS \
"offline_id, system_download_id, client_namespace, client_id, file_path, " \
"request_origin, access_count, creation_time, online_url"
#define INFO_WRAPPER_FIELD_COUNT 8
struct DeletedPageInfoWrapper {
DeletedPageInfoWrapper();
DeletedPageInfoWrapper(const DeletedPageInfoWrapper& other);
int64_t offline_id;
int64_t system_download_id;
ClientId client_id;
base::FilePath file_path;
std::string request_origin;
// Used by metric collection only:
int access_count;
base::Time creation_time;
GURL url;
};
DeletedPageInfoWrapper CreateInfoWrapper(const sql::Statement& statement) {
DeletedPageInfoWrapper info_wrapper;
info_wrapper.offline_id = statement.ColumnInt64(0);
info_wrapper.system_download_id = statement.ColumnInt64(1);
info_wrapper.client_id.name_space = statement.ColumnString(2);
info_wrapper.client_id.id = statement.ColumnString(3);
info_wrapper.file_path =
store_utils::FromDatabaseFilePath(statement.ColumnString(4));
info_wrapper.request_origin = statement.ColumnString(5);
info_wrapper.access_count = statement.ColumnInt(6);
info_wrapper.creation_time =
store_utils::FromDatabaseTime(statement.ColumnInt64(7));
info_wrapper.url = GURL(statement.ColumnString(8));
return info_wrapper;
}
DeletedPageInfoWrapper::DeletedPageInfoWrapper() = default;
DeletedPageInfoWrapper::DeletedPageInfoWrapper(
const DeletedPageInfoWrapper& other) = default;
void ReportDeletePageHistograms(
const std::vector<DeletedPageInfoWrapper>& info_wrappers) {
const int max_minutes = base::TimeDelta::FromDays(365).InMinutes();
base::Time delete_time = OfflineTimeNow();
for (const auto& info_wrapper : info_wrappers) {
base::UmaHistogramCustomCounts(
model_utils::AddHistogramSuffix(info_wrapper.client_id.name_space,
"OfflinePages.PageLifetime"),
(delete_time - info_wrapper.creation_time).InMinutes(), 1, max_minutes,
100);
base::UmaHistogramCustomCounts(
model_utils::AddHistogramSuffix(info_wrapper.client_id.name_space,
"OfflinePages.AccessCount"),
info_wrapper.access_count, 1, 1000000, 50);
}
}
bool DeleteArchiveSync(const base::FilePath& file_path) {
// Delete the file only, |false| for recursive.
return base::DeleteFile(file_path, false);
}
// Deletes a page from the store by |offline_id|.
bool DeletePageEntryByOfflineIdSync(sql::Database* db, int64_t offline_id) {
static const char kSql[] =
"DELETE FROM " OFFLINE_PAGES_TABLE_NAME " WHERE offline_id = ?";
sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt64(0, offline_id);
return statement.Run();
}
// Deletes pages by DeletedPageInfoWrapper. This will return a
// DeletePageTaskResult which contains the infos of the deleted pages (which are
// successfully deleted from the disk and the store) and a DeletePageResult.
// For each DeletedPageInfoWrapper to be deleted, the deletion will delete the
// archive file first, then database entry, in order to avoid the potential
// issue of leaving archive files behind (and they may be imported later).
// Since the database entry will only be deleted while the associated archive
// file is deleted successfully, there will be no such issue.
DeletePageTaskResult DeletePagesByDeletedPageInfoWrappersSync(
sql::Database* db,
const std::vector<DeletedPageInfoWrapper>& info_wrappers) {
std::vector<OfflinePageModel::DeletedPageInfo> deleted_page_infos;
// If there's no page to delete, return an empty list with SUCCESS.
if (info_wrappers.size() == 0)
return DeletePageTaskResult(DeletePageResult::SUCCESS, deleted_page_infos);
ReportDeletePageHistograms(info_wrappers);
bool any_archive_deleted = false;
for (const auto& info_wrapper : info_wrappers) {
if (DeleteArchiveSync(info_wrapper.file_path)) {
any_archive_deleted = true;
if (DeletePageEntryByOfflineIdSync(db, info_wrapper.offline_id)) {
deleted_page_infos.emplace_back(
info_wrapper.offline_id, info_wrapper.system_download_id,
info_wrapper.client_id, info_wrapper.request_origin,
info_wrapper.url);
}
}
}
// If there're no files deleted, return DEVICE_FAILURE.
if (!any_archive_deleted)
return DeletePageTaskResult(DeletePageResult::DEVICE_FAILURE,
deleted_page_infos);
return DeletePageTaskResult(DeletePageResult::SUCCESS, deleted_page_infos);
}
// Gets the page info for |offline_id|, returning in |info_wrapper|. Returns
// false if there's no record for |offline_id|.
bool GetDeletedPageInfoWrapperByOfflineIdSync(
sql::Database* db,
int64_t offline_id,
DeletedPageInfoWrapper* info_wrapper) {
static const char kSql[] =
"SELECT " INFO_WRAPPER_FIELDS " FROM " OFFLINE_PAGES_TABLE_NAME
" WHERE offline_id = ?";
sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt64(0, offline_id);
if (statement.Step()) {
*info_wrapper = CreateInfoWrapper(statement);
return true;
}
return false;
}
DeletePageTaskResult DeletePagesByOfflineIdsSync(
const std::vector<int64_t>& offline_ids,
sql::Database* db) {
if (offline_ids.empty())
return DeletePageTaskResult(DeletePageResult::SUCCESS, {});
// If you create a transaction but dont Commit() it is automatically
// rolled back by its destructor when it falls out of scope.
sql::Transaction transaction(db);
if (!transaction.Begin())
return DeletePageTaskResult(DeletePageResult::STORE_FAILURE, {});
std::vector<DeletedPageInfoWrapper> infos;
for (int64_t offline_id : offline_ids) {
DeletedPageInfoWrapper info;
if (GetDeletedPageInfoWrapperByOfflineIdSync(db, offline_id, &info))
infos.push_back(info);
}
DeletePageTaskResult result =
DeletePagesByDeletedPageInfoWrappersSync(db, infos);
if (!transaction.Commit())
return DeletePageTaskResult(DeletePageResult::STORE_FAILURE, {});
return result;
}
// Gets page infos for |client_id|, returning a vector of
// DeletedPageInfoWrappers because ClientId can refer to multiple pages.
std::vector<DeletedPageInfoWrapper> GetDeletedPageInfoWrappersByClientIdSync(
sql::Database* db,
ClientId client_id) {
std::vector<DeletedPageInfoWrapper> info_wrappers;
static const char kSql[] =
"SELECT " INFO_WRAPPER_FIELDS " FROM " OFFLINE_PAGES_TABLE_NAME
" WHERE client_namespace = ? AND client_id = ?";
sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindString(0, client_id.name_space);
statement.BindString(1, client_id.id);
while (statement.Step())
info_wrappers.emplace_back(CreateInfoWrapper(statement));
return info_wrappers;
}
DeletePageTaskResult DeletePagesByClientIdsSync(
const std::vector<ClientId> client_ids,
sql::Database* db) {
std::vector<DeletedPageInfoWrapper> infos;
if (client_ids.empty())
return DeletePageTaskResult(DeletePageResult::SUCCESS, {});
// If you create a transaction but dont Commit() it is automatically
// rolled back by its destructor when it falls out of scope.
sql::Transaction transaction(db);
if (!transaction.Begin())
return DeletePageTaskResult(DeletePageResult::STORE_FAILURE, {});
for (ClientId client_id : client_ids) {
std::vector<DeletedPageInfoWrapper> temp_infos =
GetDeletedPageInfoWrappersByClientIdSync(db, client_id);
infos.insert(infos.end(), temp_infos.begin(), temp_infos.end());
}
DeletePageTaskResult result =
DeletePagesByDeletedPageInfoWrappersSync(db, infos);
if (!transaction.Commit())
return DeletePageTaskResult(DeletePageResult::STORE_FAILURE, {});
return result;
}
// Gets page infos for |client_id|, returning a vector of
// DeletedPageInfoWrappers because ClientId can refer to multiple pages.
std::vector<DeletedPageInfoWrapper>
GetDeletedPageInfoWrappersByClientIdAndOriginSync(sql::Database* db,
ClientId client_id,
const std::string& origin) {
std::vector<DeletedPageInfoWrapper> info_wrappers;
static const char kSql[] =
"SELECT " INFO_WRAPPER_FIELDS " FROM " OFFLINE_PAGES_TABLE_NAME
" WHERE client_namespace = ? AND client_id = ? AND request_origin = ?";
sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindString(0, client_id.name_space);
statement.BindString(1, client_id.id);
statement.BindString(2, origin);
while (statement.Step())
info_wrappers.emplace_back(CreateInfoWrapper(statement));
return info_wrappers;
}
DeletePageTaskResult DeletePagesByClientIdsAndOriginSync(
const std::vector<ClientId> client_ids,
const std::string& origin,
sql::Database* db) {
std::vector<DeletedPageInfoWrapper> infos;
if (client_ids.empty())
return DeletePageTaskResult(DeletePageResult::SUCCESS, {});
// If you create a transaction but dont Commit() it is automatically
// rolled back by its destructor when it falls out of scope.
sql::Transaction transaction(db);
if (!transaction.Begin())
return DeletePageTaskResult(DeletePageResult::STORE_FAILURE, {});
for (ClientId client_id : client_ids) {
std::vector<DeletedPageInfoWrapper> temp_infos =
GetDeletedPageInfoWrappersByClientIdAndOriginSync(db, client_id,
origin);
infos.insert(infos.end(), temp_infos.begin(), temp_infos.end());
}
DeletePageTaskResult result =
DeletePagesByDeletedPageInfoWrappersSync(db, infos);
if (!transaction.Commit())
return DeletePageTaskResult(DeletePageResult::STORE_FAILURE, {});
return result;
}
// Gets the page information of pages that are within the provided temporary
// namespaces and satisfy the provided URL predicate.
std::vector<DeletedPageInfoWrapper>
GetCachedDeletedPageInfoWrappersByUrlPredicateSync(
sql::Database* db,
const std::vector<std::string>& temp_namespaces,
const UrlPredicate& url_predicate) {
std::vector<DeletedPageInfoWrapper> info_wrappers;
static const char kSql[] =
"SELECT " INFO_WRAPPER_FIELDS
", online_url"
" FROM " OFFLINE_PAGES_TABLE_NAME " WHERE client_namespace = ?";
for (const auto& temp_namespace : temp_namespaces) {
sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindString(0, temp_namespace);
while (statement.Step()) {
if (!url_predicate.Run(
GURL(statement.ColumnString(INFO_WRAPPER_FIELD_COUNT))))
continue;
DeletedPageInfoWrapper info_wrapper = CreateInfoWrapper(statement);
info_wrappers.push_back(info_wrapper);
}
}
return info_wrappers;
}
DeletePageTaskResult DeleteCachedPagesByUrlPredicateSync(
const std::vector<std::string>& namespaces,
const UrlPredicate& predicate,
sql::Database* db) {
// If you create a transaction but dont Commit() it is automatically
// rolled back by its destructor when it falls out of scope.
sql::Transaction transaction(db);
if (!transaction.Begin())
return DeletePageTaskResult(DeletePageResult::STORE_FAILURE, {});
const std::vector<DeletedPageInfoWrapper>& infos =
GetCachedDeletedPageInfoWrappersByUrlPredicateSync(db, namespaces,
predicate);
DeletePageTaskResult result =
DeletePagesByDeletedPageInfoWrappersSync(db, infos);
if (!transaction.Commit())
return DeletePageTaskResult(DeletePageResult::STORE_FAILURE, {});
return result;
}
// Gets the page information of pages whose url and name_space equal to |url|
// and |name_space|. The pages will be deleted from old to new (by last access
// time) until there are limit - 1 pages left.
// TODO(romax): This might be affected by https://crbug.com/753609 for url
// matching.
std::vector<DeletedPageInfoWrapper>
GetDeletedPageInfoWrappersForPageLimitDeletion(sql::Database* db,
const GURL& url,
std::string name_space,
size_t limit) {
std::vector<DeletedPageInfoWrapper> info_wrappers;
static const char kSql[] =
"SELECT " INFO_WRAPPER_FIELDS " FROM " OFFLINE_PAGES_TABLE_NAME
" WHERE client_namespace = ? AND online_url = ?"
" ORDER BY last_access_time ASC";
sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindString(0, name_space);
statement.BindString(1, url.spec());
while (statement.Step()) {
DeletedPageInfoWrapper info_wrapper = CreateInfoWrapper(statement);
info_wrappers.push_back(info_wrapper);
}
// Since the page information was selected by ascending order of last access
// time, only the first |size - limit| pages needs to be deleted.
int page_to_delete = info_wrappers.size() - limit;
if (page_to_delete < 0)
page_to_delete = 0;
info_wrappers.resize(page_to_delete);
return info_wrappers;
}
DeletePageTaskResult DeletePagesForPageLimit(const GURL& url,
std::string name_space,
size_t limit,
sql::Database* db) {
// If the namespace can have unlimited pages per url, just return success.
if (limit == kUnlimitedPages)
return DeletePageTaskResult(DeletePageResult::SUCCESS, {});
// If you create a transaction but dont Commit() it is automatically
// rolled back by its destructor when it falls out of scope.
sql::Transaction transaction(db);
if (!transaction.Begin())
return DeletePageTaskResult(DeletePageResult::STORE_FAILURE, {});
const std::vector<DeletedPageInfoWrapper>& infos =
GetDeletedPageInfoWrappersForPageLimitDeletion(db, url, name_space,
limit);
DeletePageTaskResult result =
DeletePagesByDeletedPageInfoWrappersSync(db, infos);
if (!transaction.Commit())
return DeletePageTaskResult(DeletePageResult::STORE_FAILURE, {});
return result;
}
} // namespace
// DeletePageTaskResult implementations.
DeletePageTaskResult::DeletePageTaskResult() = default;
DeletePageTaskResult::DeletePageTaskResult(
DeletePageResult result,
const std::vector<OfflinePageModel::DeletedPageInfo>& infos)
: result(result), infos(infos) {}
DeletePageTaskResult::DeletePageTaskResult(const DeletePageTaskResult& other) =
default;
DeletePageTaskResult::~DeletePageTaskResult() = default;
// static
std::unique_ptr<DeletePageTask> DeletePageTask::CreateTaskMatchingOfflineIds(
OfflinePageMetadataStore* store,
DeletePageTask::DeletePageTaskCallback callback,
const std::vector<int64_t>& offline_ids) {
return std::unique_ptr<DeletePageTask>(new DeletePageTask(
store, base::BindOnce(&DeletePagesByOfflineIdsSync, offline_ids),
std::move(callback)));
}
// static
std::unique_ptr<DeletePageTask> DeletePageTask::CreateTaskMatchingClientIds(
OfflinePageMetadataStore* store,
DeletePageTask::DeletePageTaskCallback callback,
const std::vector<ClientId>& client_ids) {
return std::unique_ptr<DeletePageTask>(new DeletePageTask(
store, base::BindOnce(&DeletePagesByClientIdsSync, client_ids),
std::move(callback)));
}
// static
std::unique_ptr<DeletePageTask>
DeletePageTask::CreateTaskMatchingClientIdsAndOrigin(
OfflinePageMetadataStore* store,
DeletePageTask::DeletePageTaskCallback callback,
const std::vector<ClientId>& client_ids,
const std::string& origin) {
return std::unique_ptr<DeletePageTask>(new DeletePageTask(
store,
base::BindOnce(&DeletePagesByClientIdsAndOriginSync, client_ids, origin),
std::move(callback)));
}
// static
std::unique_ptr<DeletePageTask>
DeletePageTask::CreateTaskMatchingUrlPredicateForCachedPages(
OfflinePageMetadataStore* store,
DeletePageTask::DeletePageTaskCallback callback,
ClientPolicyController* policy_controller,
const UrlPredicate& predicate) {
std::vector<std::string> temp_namespaces =
policy_controller->GetNamespacesRemovedOnCacheReset();
return std::unique_ptr<DeletePageTask>(
new DeletePageTask(store,
base::BindOnce(&DeleteCachedPagesByUrlPredicateSync,
temp_namespaces, predicate),
std::move(callback)));
}
// static
std::unique_ptr<DeletePageTask> DeletePageTask::CreateTaskDeletingForPageLimit(
OfflinePageMetadataStore* store,
DeletePageTask::DeletePageTaskCallback callback,
ClientPolicyController* policy_controller,
const OfflinePageItem& page) {
std::string name_space = page.client_id.name_space;
size_t limit = policy_controller->GetPolicy(name_space).pages_allowed_per_url;
return std::unique_ptr<DeletePageTask>(new DeletePageTask(
store,
base::BindOnce(&DeletePagesForPageLimit, page.url, name_space, limit),
std::move(callback)));
}
DeletePageTask::DeletePageTask(OfflinePageMetadataStore* store,
DeleteFunction func,
DeletePageTaskCallback callback)
: store_(store),
func_(std::move(func)),
callback_(std::move(callback)),
weak_ptr_factory_(this) {
DCHECK(store_);
DCHECK(!callback_.is_null());
}
DeletePageTask::~DeletePageTask() {}
void DeletePageTask::Run() {
store_->Execute(std::move(func_),
base::BindOnce(&DeletePageTask::OnDeletePageDone,
weak_ptr_factory_.GetWeakPtr()),
DeletePageTaskResult(DeletePageResult::STORE_FAILURE, {}));
}
void DeletePageTask::OnDeletePageDone(DeletePageTaskResult result) {
std::move(callback_).Run(result.result, result.infos);
TaskComplete();
}
} // namespace offline_pages