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