blob: 890ff988336b78c86a518906ea3b9e19d4a321d2 [file] [log] [blame]
// Copyright 2016 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/offline_page_metadata_store.h"
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/sequenced_task_runner.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/clock.h"
#include "components/offline_pages/core/client_namespace_constants.h"
#include "components/offline_pages/core/offline_clock.h"
#include "components/offline_pages/core/offline_page_item.h"
#include "components/offline_pages/core/offline_store_types.h"
#include "components/offline_pages/core/offline_store_utils.h"
#include "sql/database.h"
#include "sql/meta_table.h"
#include "sql/statement.h"
#include "sql/transaction.h"
namespace offline_pages {
const int OfflinePageMetadataStore::kFirstPostLegacyVersion;
const int OfflinePageMetadataStore::kCurrentVersion;
const int OfflinePageMetadataStore::kCompatibleVersion;
namespace {
// This is a macro instead of a const so that
// it can be used inline in other SQL statements below.
#define OFFLINE_PAGES_TABLE_NAME "offlinepages_v1"
void ReportStoreEvent(OfflinePagesStoreEvent event) {
UMA_HISTOGRAM_ENUMERATION("OfflinePages.SQLStorage.StoreEvent", event);
}
bool CreateOfflinePagesTable(sql::Database* db) {
static const char kSql[] =
"CREATE TABLE IF NOT EXISTS " OFFLINE_PAGES_TABLE_NAME
"(offline_id INTEGER PRIMARY KEY NOT NULL,"
" creation_time INTEGER NOT NULL,"
" file_size INTEGER NOT NULL,"
" last_access_time INTEGER NOT NULL,"
" access_count INTEGER NOT NULL,"
" system_download_id INTEGER NOT NULL DEFAULT 0,"
" file_missing_time INTEGER NOT NULL DEFAULT 0,"
" upgrade_attempt INTEGER NOT NULL DEFAULT 0,"
" client_namespace VARCHAR NOT NULL,"
" client_id VARCHAR NOT NULL,"
" online_url VARCHAR NOT NULL,"
" file_path VARCHAR NOT NULL,"
" title VARCHAR NOT NULL DEFAULT '',"
" original_url VARCHAR NOT NULL DEFAULT '',"
" request_origin VARCHAR NOT NULL DEFAULT '',"
" digest VARCHAR NOT NULL DEFAULT ''"
")";
return db->Execute(kSql);
}
bool UpgradeWithQuery(sql::Database* db, const char* upgrade_sql) {
if (!db->Execute("ALTER TABLE " OFFLINE_PAGES_TABLE_NAME
" RENAME TO temp_" OFFLINE_PAGES_TABLE_NAME)) {
return false;
}
if (!CreateOfflinePagesTable(db))
return false;
if (!db->Execute(upgrade_sql))
return false;
if (!db->Execute("DROP TABLE IF EXISTS temp_" OFFLINE_PAGES_TABLE_NAME))
return false;
return true;
}
bool UpgradeFrom52(sql::Database* db) {
static const char kSql[] =
"INSERT INTO " OFFLINE_PAGES_TABLE_NAME
" (offline_id, creation_time, file_size, last_access_time, "
"access_count, client_namespace, client_id, "
"online_url, file_path) "
"SELECT "
"offline_id, creation_time, file_size, last_access_time, "
"access_count, client_namespace, client_id, "
"online_url, file_path "
"FROM temp_" OFFLINE_PAGES_TABLE_NAME;
return UpgradeWithQuery(db, kSql);
}
bool UpgradeFrom53(sql::Database* db) {
static const char kSql[] =
"INSERT INTO " OFFLINE_PAGES_TABLE_NAME
" (offline_id, creation_time, file_size, last_access_time, "
"access_count, client_namespace, client_id, online_url, "
"file_path) "
"SELECT "
"offline_id, creation_time, file_size, last_access_time, "
"access_count, client_namespace, client_id, online_url, "
"file_path "
"FROM temp_" OFFLINE_PAGES_TABLE_NAME;
return UpgradeWithQuery(db, kSql);
}
bool UpgradeFrom54(sql::Database* db) {
static const char kSql[] =
"INSERT INTO " OFFLINE_PAGES_TABLE_NAME
" (offline_id, creation_time, file_size, last_access_time, "
"access_count, client_namespace, client_id, online_url, "
"file_path, title) "
"SELECT "
"offline_id, creation_time, file_size, last_access_time, "
"access_count, client_namespace, client_id, online_url, "
"file_path, title "
"FROM temp_" OFFLINE_PAGES_TABLE_NAME;
return UpgradeWithQuery(db, kSql);
}
bool UpgradeFrom55(sql::Database* db) {
static const char kSql[] =
"INSERT INTO " OFFLINE_PAGES_TABLE_NAME
" (offline_id, creation_time, file_size, last_access_time, "
"access_count, client_namespace, client_id, online_url, "
"file_path, title) "
"SELECT "
"offline_id, creation_time, file_size, last_access_time, "
"access_count, client_namespace, client_id, online_url, "
"file_path, title "
"FROM temp_" OFFLINE_PAGES_TABLE_NAME;
return UpgradeWithQuery(db, kSql);
}
bool UpgradeFrom56(sql::Database* db) {
static const char kSql[] =
"INSERT INTO " OFFLINE_PAGES_TABLE_NAME
" (offline_id, creation_time, file_size, last_access_time, "
"access_count, client_namespace, client_id, online_url, "
"file_path, title, original_url) "
"SELECT "
"offline_id, creation_time, file_size, last_access_time, "
"access_count, client_namespace, client_id, online_url, "
"file_path, title, original_url "
"FROM temp_" OFFLINE_PAGES_TABLE_NAME;
return UpgradeWithQuery(db, kSql);
}
bool UpgradeFrom57(sql::Database* db) {
static const char kSql[] =
"INSERT INTO " OFFLINE_PAGES_TABLE_NAME
" (offline_id, creation_time, file_size, last_access_time, "
"access_count, client_namespace, client_id, online_url, "
"file_path, title, original_url) "
"SELECT "
"offline_id, creation_time, file_size, last_access_time, "
"access_count, client_namespace, client_id, online_url, "
"file_path, title, original_url "
"FROM temp_" OFFLINE_PAGES_TABLE_NAME;
return UpgradeWithQuery(db, kSql);
}
bool UpgradeFrom61(sql::Database* db) {
static const char kSql[] =
"INSERT INTO " OFFLINE_PAGES_TABLE_NAME
" (offline_id, creation_time, file_size, last_access_time, "
"access_count, client_namespace, client_id, online_url, "
"file_path, title, original_url, request_origin) "
"SELECT "
"offline_id, creation_time, file_size, last_access_time, "
"access_count, client_namespace, client_id, online_url, "
"file_path, title, original_url, request_origin "
"FROM temp_" OFFLINE_PAGES_TABLE_NAME;
return UpgradeWithQuery(db, kSql);
}
bool CreatePageThumbnailsTable(sql::Database* db) {
static const char kSql[] =
"CREATE TABLE IF NOT EXISTS page_thumbnails"
" (offline_id INTEGER PRIMARY KEY NOT NULL,"
" expiration INTEGER NOT NULL,"
" thumbnail BLOB NOT NULL"
")";
return db->Execute(kSql);
}
bool CreateLatestSchema(sql::Database* db) {
sql::Transaction transaction(db);
if (!transaction.Begin())
return false;
// First time database initialization.
if (!CreateOfflinePagesTable(db))
return false;
if (!CreatePageThumbnailsTable(db))
return false;
sql::MetaTable meta_table;
if (!meta_table.Init(db, OfflinePageMetadataStore::kCurrentVersion,
OfflinePageMetadataStore::kCompatibleVersion))
return false;
return transaction.Commit();
}
// Upgrades the database from before the database version was stored in the
// MetaTable. This function should never need to be modified.
bool UpgradeFromLegacyVersion(sql::Database* db) {
sql::Transaction transaction(db);
if (!transaction.Begin())
return false;
// Legacy upgrade section. Details are described in the header file.
if (!db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "expiration_time") &&
!db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "title")) {
if (!UpgradeFrom52(db))
return false;
} else if (!db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "title")) {
if (!UpgradeFrom53(db))
return false;
} else if (db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "offline_url")) {
if (!UpgradeFrom54(db))
return false;
} else if (!db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "original_url")) {
if (!UpgradeFrom55(db))
return false;
} else if (db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "expiration_time")) {
if (!UpgradeFrom56(db))
return false;
} else if (!db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "request_origin")) {
if (!UpgradeFrom57(db))
return false;
} else if (!db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "digest")) {
if (!UpgradeFrom61(db))
return false;
}
sql::MetaTable meta_table;
if (!meta_table.Init(db, OfflinePageMetadataStore::kFirstPostLegacyVersion,
OfflinePageMetadataStore::kCompatibleVersion))
return false;
return transaction.Commit();
}
bool UpgradeFromVersion1ToVersion2(sql::Database* db,
sql::MetaTable* meta_table) {
sql::Transaction transaction(db);
if (!transaction.Begin())
return false;
static const char kSql[] = "UPDATE " OFFLINE_PAGES_TABLE_NAME
" SET upgrade_attempt = 5 "
" WHERE client_namespace = 'async_loading'"
" OR client_namespace = 'download'"
" OR client_namespace = 'ntp_suggestions'"
" OR client_namespace = 'browser_actions'";
sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
if (!statement.Run())
return false;
meta_table->SetVersionNumber(2);
return transaction.Commit();
}
bool UpgradeFromVersion2ToVersion3(sql::Database* db,
sql::MetaTable* meta_table) {
sql::Transaction transaction(db);
if (!transaction.Begin())
return false;
if (!CreatePageThumbnailsTable(db)) {
return false;
}
meta_table->SetVersionNumber(3);
return transaction.Commit();
}
bool CreateSchema(sql::Database* db) {
if (!sql::MetaTable::DoesTableExist(db)) {
// If this looks like a completely empty DB, simply start from scratch.
if (!db->DoesTableExist(OFFLINE_PAGES_TABLE_NAME))
return CreateLatestSchema(db);
// Otherwise we need to run a legacy upgrade.
if (!UpgradeFromLegacyVersion(db))
return false;
}
sql::MetaTable meta_table;
if (!meta_table.Init(db, OfflinePageMetadataStore::kCurrentVersion,
OfflinePageMetadataStore::kCompatibleVersion))
return false;
for (;;) {
switch (meta_table.GetVersionNumber()) {
case 1:
if (!UpgradeFromVersion1ToVersion2(db, &meta_table))
return false;
break;
case 2:
if (!UpgradeFromVersion2ToVersion3(db, &meta_table))
return false;
break;
case OfflinePageMetadataStore::kCurrentVersion:
return true;
default:
return false;
}
}
}
bool PrepareDirectory(const base::FilePath& path) {
base::File::Error error = base::File::FILE_OK;
if (!base::DirectoryExists(path.DirName())) {
if (!base::CreateDirectoryAndGetError(path.DirName(), &error)) {
LOG(ERROR) << "Failed to create offline pages db directory: "
<< base::File::ErrorToString(error);
return false;
}
}
UMA_HISTOGRAM_ENUMERATION("OfflinePages.SQLStorage.CreateDirectoryResult",
-error, -base::File::FILE_ERROR_MAX);
return true;
}
bool InitDatabase(sql::Database* db,
const base::FilePath& path,
bool in_memory) {
db->set_page_size(4096);
db->set_cache_size(500);
db->set_histogram_tag("OfflinePageMetadata");
db->set_exclusive_locking();
if (!in_memory && !PrepareDirectory(path))
return false;
bool open_db_result = false;
if (in_memory)
open_db_result = db->OpenInMemory();
else
open_db_result = db->Open(path);
if (!open_db_result) {
LOG(ERROR) << "Failed to open database, in memory: " << in_memory;
return false;
}
db->Preload();
return CreateSchema(db);
}
void CloseDatabaseSync(
sql::Database* db,
scoped_refptr<base::SingleThreadTaskRunner> callback_runner,
base::OnceClosure callback) {
if (db)
db->Close();
callback_runner->PostTask(FROM_HERE, std::move(callback));
}
} // anonymous namespace
// static
constexpr base::TimeDelta OfflinePageMetadataStore::kClosingDelay;
OfflinePageMetadataStore::OfflinePageMetadataStore(
scoped_refptr<base::SequencedTaskRunner> background_task_runner)
: background_task_runner_(std::move(background_task_runner)),
in_memory_(true),
state_(StoreState::NOT_LOADED),
weak_ptr_factory_(this),
closing_weak_ptr_factory_(this) {}
OfflinePageMetadataStore::OfflinePageMetadataStore(
scoped_refptr<base::SequencedTaskRunner> background_task_runner,
const base::FilePath& path)
: background_task_runner_(std::move(background_task_runner)),
in_memory_(false),
db_file_path_(path.AppendASCII("OfflinePages.db")),
state_(StoreState::NOT_LOADED),
weak_ptr_factory_(this),
closing_weak_ptr_factory_(this) {}
OfflinePageMetadataStore::~OfflinePageMetadataStore() {
if (db_.get() &&
!background_task_runner_->DeleteSoon(FROM_HERE, db_.release())) {
DLOG(WARNING) << "SQL database will not be deleted.";
}
}
StoreState OfflinePageMetadataStore::GetStateForTesting() const {
return state_;
}
void OfflinePageMetadataStore::SetStateForTesting(StoreState state,
bool reset_db) {
state_ = state;
if (reset_db)
db_.reset(nullptr);
}
void OfflinePageMetadataStore::InitializeInternal(
base::OnceClosure pending_command) {
TRACE_EVENT_ASYNC_BEGIN1("offline_pages", "Metadata Store", this, "is reopen",
!last_closing_time_.is_null());
DCHECK_EQ(state_, StoreState::NOT_LOADED);
if (!last_closing_time_.is_null()) {
ReportStoreEvent(OfflinePagesStoreEvent::kReopened);
UMA_HISTOGRAM_CUSTOM_TIMES("OfflinePages.SQLStorage.TimeFromCloseToOpen",
OfflineClock()->Now() - last_closing_time_,
base::TimeDelta::FromMilliseconds(10),
base::TimeDelta::FromMinutes(10),
50 /* buckets */);
} else {
ReportStoreEvent(OfflinePagesStoreEvent::kOpenedFirstTime);
}
state_ = StoreState::INITIALIZING;
db_.reset(new sql::Database());
base::PostTaskAndReplyWithResult(
background_task_runner_.get(), FROM_HERE,
base::BindOnce(&InitDatabase, db_.get(), db_file_path_, in_memory_),
base::BindOnce(&OfflinePageMetadataStore::OnInitializeInternalDone,
weak_ptr_factory_.GetWeakPtr(),
std::move(pending_command)));
}
void OfflinePageMetadataStore::OnInitializeInternalDone(
base::OnceClosure pending_command,
bool success) {
TRACE_EVENT_ASYNC_STEP_PAST1("offline_pages", "Metadata Store", this,
"Initializing", "succeeded", success);
// TODO(fgorski): DCHECK initialization is in progress, once we have a
// relevant value for the store state.
if (success) {
state_ = StoreState::LOADED;
} else {
state_ = StoreState::FAILED_LOADING;
db_.reset();
TRACE_EVENT_ASYNC_END0("offline_pages", "Metadata Store", this);
}
CHECK(!pending_command.is_null());
std::move(pending_command).Run();
// Execute other pending commands.
for (auto command_iter = pending_commands_.begin();
command_iter != pending_commands_.end();) {
std::move(*command_iter++).Run();
}
pending_commands_.clear();
if (state_ == StoreState::FAILED_LOADING)
state_ = StoreState::NOT_LOADED;
}
void OfflinePageMetadataStore::CloseInternal() {
if (state_ != StoreState::LOADED) {
ReportStoreEvent(OfflinePagesStoreEvent::kCloseSkipped);
return;
}
TRACE_EVENT_ASYNC_STEP_PAST0("offline_pages", "Metadata Store", this, "Open");
last_closing_time_ = OfflineClock()->Now();
ReportStoreEvent(OfflinePagesStoreEvent::kClosed);
state_ = StoreState::NOT_LOADED;
background_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
&CloseDatabaseSync, db_.get(), base::ThreadTaskRunnerHandle::Get(),
base::BindOnce(&OfflinePageMetadataStore::CloseInternalDone,
weak_ptr_factory_.GetWeakPtr(), std::move(db_))));
}
void OfflinePageMetadataStore::CloseInternalDone(
std::unique_ptr<sql::Database> db) {
db.reset();
TRACE_EVENT_ASYNC_STEP_PAST0("offline_pages", "Metadata Store", this,
"Closing");
TRACE_EVENT_ASYNC_END0("offline_pages", "Metadata Store", this);
}
} // namespace offline_pages