blob: 9a8cb55a85ea726a063676a7e499e39b7e051c8c [file] [log] [blame]
// Copyright 2013 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/services/storage/dom_storage/legacy_dom_storage_database.h"
#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "components/services/storage/public/cpp/filesystem/filesystem_proxy.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "third_party/sqlite/sqlite3.h"
namespace storage {
LegacyDomStorageDatabase::LegacyDomStorageDatabase(
const base::FilePath& file_path,
std::unique_ptr<FilesystemProxy> filesystem_proxy)
: file_path_(file_path), filesystem_proxy_(std::move(filesystem_proxy)) {
DCHECK(!file_path_.empty());
Init();
}
LegacyDomStorageDatabase::LegacyDomStorageDatabase(
std::unique_ptr<FilesystemProxy> filesystem_proxy)
: filesystem_proxy_(std::move(filesystem_proxy)) {
Init();
}
void LegacyDomStorageDatabase::Init() {
failed_to_open_ = false;
tried_to_recreate_ = false;
known_to_be_empty_ = false;
}
LegacyDomStorageDatabase::~LegacyDomStorageDatabase() {
if (known_to_be_empty_ && !file_path_.empty()) {
// Delete the db and any lingering journal file from disk.
Close();
sql::Database::Delete(file_path_);
}
}
void LegacyDomStorageDatabase::ReadAllValues(
LegacyDomStorageValuesMap* result) {
if (!LazyOpen(false))
return;
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE, "SELECT * from ItemTable"));
DCHECK(statement.is_valid());
while (statement.Step()) {
base::string16 key = statement.ColumnString16(0);
base::string16 value;
statement.ColumnBlobAsString16(1, &value);
(*result)[key] = base::NullableString16(value, false);
}
known_to_be_empty_ = result->empty();
// Drop SQLite's caches.
db_->TrimMemory();
}
bool LegacyDomStorageDatabase::CommitChanges(
bool clear_all_first,
const LegacyDomStorageValuesMap& changes) {
if (!LazyOpen(!changes.empty())) {
// If we're being asked to commit changes that will result in an
// empty database, we return true if the database file doesn't exist.
return clear_all_first && changes.empty() &&
!filesystem_proxy_->PathExists(file_path_);
}
bool old_known_to_be_empty = known_to_be_empty_;
sql::Transaction transaction(db_.get());
if (!transaction.Begin())
return false;
if (clear_all_first) {
if (!db_->Execute("DELETE FROM ItemTable"))
return false;
known_to_be_empty_ = true;
}
bool did_delete = false;
bool did_insert = false;
auto it = changes.begin();
for (; it != changes.end(); ++it) {
sql::Statement statement;
base::string16 key = it->first;
base::NullableString16 value = it->second;
if (value.is_null()) {
statement.Assign(db_->GetCachedStatement(
SQL_FROM_HERE, "DELETE FROM ItemTable WHERE key=?"));
statement.BindString16(0, key);
did_delete = true;
} else {
statement.Assign(db_->GetCachedStatement(
SQL_FROM_HERE, "INSERT INTO ItemTable VALUES (?,?)"));
statement.BindString16(0, key);
statement.BindBlob(1, value.string().data(),
value.string().length() * sizeof(base::char16));
known_to_be_empty_ = false;
did_insert = true;
}
DCHECK(statement.is_valid());
statement.Run();
}
if (!known_to_be_empty_ && did_delete && !did_insert) {
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE, "SELECT count(key) from ItemTable"));
if (statement.Step())
known_to_be_empty_ = statement.ColumnInt(0) == 0;
}
bool success = transaction.Commit();
if (!success)
known_to_be_empty_ = old_known_to_be_empty;
// Drop SQLite's caches.
db_->TrimMemory();
return success;
}
void LegacyDomStorageDatabase::ReportMemoryUsage(
base::trace_event::ProcessMemoryDump* pmd,
const std::string& name) {
if (IsOpen())
db_->ReportMemoryUsage(pmd, name);
}
bool LegacyDomStorageDatabase::LazyOpen(bool create_if_needed) {
if (failed_to_open_) {
// Don't try to open a database that we know has failed
// already.
return false;
}
if (IsOpen())
return true;
bool database_exists = filesystem_proxy_->PathExists(file_path_);
if (!database_exists && !create_if_needed) {
// If the file doesn't exist already and we haven't been asked to create
// a file on disk, then we don't bother opening the database. This means
// we wait until we absolutely need to put something onto disk before we
// do so.
return false;
}
db_ = std::make_unique<sql::Database>();
db_->set_histogram_tag("DOMStorageDatabase");
// This database should only be accessed from the process hosting the storage
// service, so exclusive locking is appropriate.
db_->set_exclusive_locking();
// This database is only opened to migrate DOMStorage data to a new backend.
// Given the use case, mmap()'s performance improvements are not worth the
// (tiny amount of) problems that mmap() may cause.
db_->set_mmap_disabled();
if (!db_->Open(file_path_)) {
LOG(ERROR) << "Unable to open DOM storage database at "
<< file_path_.value() << " error: " << db_->GetErrorMessage();
if (database_exists && !tried_to_recreate_)
return DeleteFileAndRecreate();
failed_to_open_ = true;
return false;
}
// sql::Database uses UTF-8 encoding, but WebCore style databases use
// UTF-16, so ensure we match.
ignore_result(db_->Execute("PRAGMA encoding=\"UTF-16\""));
if (!database_exists) {
// This is a new database, create the table and we're done!
if (CreateTableV2())
return true;
} else {
// The database exists already - check if we need to upgrade
// and whether it's usable (i.e. not corrupted).
SchemaVersion current_version = DetectSchemaVersion();
if (current_version == V2)
return true;
}
// This is the exceptional case - to try and recover we'll attempt
// to delete the file and start again.
Close();
return DeleteFileAndRecreate();
}
LegacyDomStorageDatabase::SchemaVersion
LegacyDomStorageDatabase::DetectSchemaVersion() {
DCHECK(IsOpen());
// Connection::Open() may succeed even if the file we try and open is not a
// database, however in the case that the database is corrupted to the point
// that SQLite doesn't actually think it's a database,
// sql::Database::GetCachedStatement will DCHECK when we later try and
// run statements. So we run a query here that will not DCHECK but fail
// on an invalid database to verify that what we've opened is usable.
if (db_->ExecuteAndReturnErrorCode("PRAGMA auto_vacuum") != SQLITE_OK)
return INVALID;
// Look at the current schema - if it doesn't look right, assume corrupt.
if (!db_->DoesTableExist("ItemTable") ||
!db_->DoesColumnExist("ItemTable", "key") ||
!db_->DoesColumnExist("ItemTable", "value"))
return INVALID;
return V2;
}
bool LegacyDomStorageDatabase::CreateTableV2() {
DCHECK(IsOpen());
return db_->Execute(
"CREATE TABLE ItemTable ("
"key TEXT UNIQUE ON CONFLICT REPLACE, "
"value BLOB NOT NULL ON CONFLICT FAIL)");
}
bool LegacyDomStorageDatabase::DeleteFileAndRecreate() {
DCHECK(!IsOpen());
DCHECK(filesystem_proxy_->PathExists(file_path_));
// We should only try and do this once.
if (tried_to_recreate_)
return false;
tried_to_recreate_ = true;
base::Optional<base::File::Info> info =
filesystem_proxy_->GetFileInfo(file_path_);
// If it's not a directory and we can delete the file, try and open it again.
if (info && !info->is_directory && sql::Database::Delete(file_path_)) {
return LazyOpen(true);
}
failed_to_open_ = true;
return false;
}
void LegacyDomStorageDatabase::Close() {
db_.reset(nullptr);
}
} // namespace storage