| // 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 |