Re-subimission of https://codereview.chromium.org/1041213003/
[chromium-blink-merge.git] / content / browser / dom_storage / dom_storage_database.cc
blob1e0166f68f849f157e55b25e199f917416a70b92
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "content/browser/dom_storage/dom_storage_database.h"
7 #include "base/bind.h"
8 #include "base/files/file_util.h"
9 #include "base/logging.h"
10 #include "sql/statement.h"
11 #include "sql/transaction.h"
12 #include "third_party/sqlite/sqlite3.h"
14 namespace {
16 const base::FilePath::CharType kJournal[] = FILE_PATH_LITERAL("-journal");
18 } // anon namespace
20 namespace content {
22 // static
23 base::FilePath DOMStorageDatabase::GetJournalFilePath(
24 const base::FilePath& database_path) {
25 base::FilePath::StringType journal_file_name =
26 database_path.BaseName().value() + kJournal;
27 return database_path.DirName().Append(journal_file_name);
30 DOMStorageDatabase::DOMStorageDatabase(const base::FilePath& file_path)
31 : file_path_(file_path) {
32 // Note: in normal use we should never get an empty backing path here.
33 // However, the unit test for this class can contruct an instance
34 // with an empty path.
35 Init();
38 DOMStorageDatabase::DOMStorageDatabase() {
39 Init();
42 void DOMStorageDatabase::Init() {
43 failed_to_open_ = false;
44 tried_to_recreate_ = false;
45 known_to_be_empty_ = false;
48 DOMStorageDatabase::~DOMStorageDatabase() {
49 if (known_to_be_empty_ && !file_path_.empty()) {
50 // Delete the db and any lingering journal file from disk.
51 Close();
52 sql::Connection::Delete(file_path_);
56 void DOMStorageDatabase::ReadAllValues(DOMStorageValuesMap* result) {
57 if (!LazyOpen(false))
58 return;
60 sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE,
61 "SELECT * from ItemTable"));
62 DCHECK(statement.is_valid());
64 while (statement.Step()) {
65 base::string16 key = statement.ColumnString16(0);
66 base::string16 value;
67 statement.ColumnBlobAsString16(1, &value);
68 (*result)[key] = base::NullableString16(value, false);
70 known_to_be_empty_ = result->empty();
73 bool DOMStorageDatabase::CommitChanges(bool clear_all_first,
74 const DOMStorageValuesMap& changes) {
75 if (!LazyOpen(!changes.empty())) {
76 // If we're being asked to commit changes that will result in an
77 // empty database, we return true if the database file doesn't exist.
78 return clear_all_first && changes.empty() &&
79 !base::PathExists(file_path_);
82 bool old_known_to_be_empty = known_to_be_empty_;
83 sql::Transaction transaction(db_.get());
84 if (!transaction.Begin())
85 return false;
87 if (clear_all_first) {
88 if (!db_->Execute("DELETE FROM ItemTable"))
89 return false;
90 known_to_be_empty_ = true;
93 bool did_delete = false;
94 bool did_insert = false;
95 DOMStorageValuesMap::const_iterator it = changes.begin();
96 for(; it != changes.end(); ++it) {
97 sql::Statement statement;
98 base::string16 key = it->first;
99 base::NullableString16 value = it->second;
100 if (value.is_null()) {
101 statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE,
102 "DELETE FROM ItemTable WHERE key=?"));
103 statement.BindString16(0, key);
104 did_delete = true;
105 } else {
106 statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE,
107 "INSERT INTO ItemTable VALUES (?,?)"));
108 statement.BindString16(0, key);
109 statement.BindBlob(1, value.string().data(),
110 value.string().length() * sizeof(base::char16));
111 known_to_be_empty_ = false;
112 did_insert = true;
114 DCHECK(statement.is_valid());
115 statement.Run();
118 if (!known_to_be_empty_ && did_delete && !did_insert) {
119 sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE,
120 "SELECT count(key) from ItemTable"));
121 if (statement.Step())
122 known_to_be_empty_ = statement.ColumnInt(0) == 0;
125 bool success = transaction.Commit();
126 if (!success)
127 known_to_be_empty_ = old_known_to_be_empty;
128 return success;
131 bool DOMStorageDatabase::LazyOpen(bool create_if_needed) {
132 if (failed_to_open_) {
133 // Don't try to open a database that we know has failed
134 // already.
135 return false;
138 if (IsOpen())
139 return true;
141 bool database_exists = base::PathExists(file_path_);
143 if (!database_exists && !create_if_needed) {
144 // If the file doesn't exist already and we haven't been asked to create
145 // a file on disk, then we don't bother opening the database. This means
146 // we wait until we absolutely need to put something onto disk before we
147 // do so.
148 return false;
151 db_.reset(new sql::Connection());
152 db_->set_histogram_tag("DOMStorageDatabase");
154 if (file_path_.empty()) {
155 // This code path should only be triggered by unit tests.
156 if (!db_->OpenInMemory()) {
157 NOTREACHED() << "Unable to open DOM storage database in memory.";
158 failed_to_open_ = true;
159 return false;
161 } else {
162 if (!db_->Open(file_path_)) {
163 LOG(ERROR) << "Unable to open DOM storage database at "
164 << file_path_.value()
165 << " error: " << db_->GetErrorMessage();
166 if (database_exists && !tried_to_recreate_)
167 return DeleteFileAndRecreate();
168 failed_to_open_ = true;
169 return false;
173 // sql::Connection uses UTF-8 encoding, but WebCore style databases use
174 // UTF-16, so ensure we match.
175 ignore_result(db_->Execute("PRAGMA encoding=\"UTF-16\""));
177 if (!database_exists) {
178 // This is a new database, create the table and we're done!
179 if (CreateTableV2())
180 return true;
181 } else {
182 // The database exists already - check if we need to upgrade
183 // and whether it's usable (i.e. not corrupted).
184 SchemaVersion current_version = DetectSchemaVersion();
186 if (current_version == V2) {
187 return true;
188 } else if (current_version == V1) {
189 if (UpgradeVersion1To2())
190 return true;
194 // This is the exceptional case - to try and recover we'll attempt
195 // to delete the file and start again.
196 Close();
197 return DeleteFileAndRecreate();
200 DOMStorageDatabase::SchemaVersion DOMStorageDatabase::DetectSchemaVersion() {
201 DCHECK(IsOpen());
203 // Connection::Open() may succeed even if the file we try and open is not a
204 // database, however in the case that the database is corrupted to the point
205 // that SQLite doesn't actually think it's a database,
206 // sql::Connection::GetCachedStatement will DCHECK when we later try and
207 // run statements. So we run a query here that will not DCHECK but fail
208 // on an invalid database to verify that what we've opened is usable.
209 if (db_->ExecuteAndReturnErrorCode("PRAGMA auto_vacuum") != SQLITE_OK)
210 return INVALID;
212 // Look at the current schema - if it doesn't look right, assume corrupt.
213 if (!db_->DoesTableExist("ItemTable") ||
214 !db_->DoesColumnExist("ItemTable", "key") ||
215 !db_->DoesColumnExist("ItemTable", "value"))
216 return INVALID;
218 // We must use a unique statement here as we aren't going to step it.
219 sql::Statement statement(
220 db_->GetUniqueStatement("SELECT key,value from ItemTable LIMIT 1"));
221 if (statement.DeclaredColumnType(0) != sql::COLUMN_TYPE_TEXT)
222 return INVALID;
224 switch (statement.DeclaredColumnType(1)) {
225 case sql::COLUMN_TYPE_BLOB:
226 return V2;
227 case sql::COLUMN_TYPE_TEXT:
228 return V1;
229 default:
230 return INVALID;
234 bool DOMStorageDatabase::CreateTableV2() {
235 DCHECK(IsOpen());
237 return db_->Execute(
238 "CREATE TABLE ItemTable ("
239 "key TEXT UNIQUE ON CONFLICT REPLACE, "
240 "value BLOB NOT NULL ON CONFLICT FAIL)");
243 bool DOMStorageDatabase::DeleteFileAndRecreate() {
244 DCHECK(!IsOpen());
245 DCHECK(base::PathExists(file_path_));
247 // We should only try and do this once.
248 if (tried_to_recreate_)
249 return false;
251 tried_to_recreate_ = true;
253 // If it's not a directory and we can delete the file, try and open it again.
254 if (!base::DirectoryExists(file_path_) &&
255 sql::Connection::Delete(file_path_)) {
256 return LazyOpen(true);
259 failed_to_open_ = true;
260 return false;
263 bool DOMStorageDatabase::UpgradeVersion1To2() {
264 DCHECK(IsOpen());
265 DCHECK(DetectSchemaVersion() == V1);
267 sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE,
268 "SELECT * FROM ItemTable"));
269 DCHECK(statement.is_valid());
271 // Need to migrate from TEXT value column to BLOB.
272 // Store the current database content so we can re-insert
273 // the data into the new V2 table.
274 DOMStorageValuesMap values;
275 while (statement.Step()) {
276 base::string16 key = statement.ColumnString16(0);
277 base::NullableString16 value(statement.ColumnString16(1), false);
278 values[key] = value;
281 sql::Transaction migration(db_.get());
282 return migration.Begin() &&
283 db_->Execute("DROP TABLE ItemTable") &&
284 CreateTableV2() &&
285 CommitChanges(false, values) &&
286 migration.Commit();
289 void DOMStorageDatabase::Close() {
290 db_.reset(NULL);
293 } // namespace content