Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 1 | // Copyright 2013 The Chromium Authors. All rights reserved. |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 5 | #include "content/browser/dom_storage/dom_storage_database.h" |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 6 | |
| 7 | #include "base/bind.h" |
| 8 | #include "base/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" |
| 13 | |
| 14 | namespace { |
| 15 | |
| 16 | const base::FilePath::CharType kJournal[] = FILE_PATH_LITERAL("-journal"); |
| 17 | |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 18 | } // anon namespace |
| 19 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 20 | namespace content { |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 21 | |
| 22 | // static |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 23 | base::FilePath DOMStorageDatabase::GetJournalFilePath( |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 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); |
| 28 | } |
| 29 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 30 | DOMStorageDatabase::DOMStorageDatabase(const base::FilePath& file_path) |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 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(); |
| 36 | } |
| 37 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 38 | DOMStorageDatabase::DOMStorageDatabase() { |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 39 | Init(); |
| 40 | } |
| 41 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 42 | void DOMStorageDatabase::Init() { |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 43 | failed_to_open_ = false; |
| 44 | tried_to_recreate_ = false; |
| 45 | known_to_be_empty_ = false; |
| 46 | } |
| 47 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 48 | DOMStorageDatabase::~DOMStorageDatabase() { |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 49 | if (known_to_be_empty_ && !file_path_.empty()) { |
| 50 | // Delete the db and any lingering journal file from disk. |
| 51 | Close(); |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 52 | sql::Connection::Delete(file_path_); |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 53 | } |
| 54 | } |
| 55 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 56 | void DOMStorageDatabase::ReadAllValues(DOMStorageValuesMap* result) { |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 57 | if (!LazyOpen(false)) |
| 58 | return; |
| 59 | |
| 60 | sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, |
| 61 | "SELECT * from ItemTable")); |
| 62 | DCHECK(statement.is_valid()); |
| 63 | |
| 64 | while (statement.Step()) { |
| 65 | base::string16 key = statement.ColumnString16(0); |
| 66 | base::string16 value; |
| 67 | statement.ColumnBlobAsString16(1, &value); |
Torne (Richard Coles) | 7d4cd47 | 2013-06-19 11:58:07 +0100 | [diff] [blame] | 68 | (*result)[key] = base::NullableString16(value, false); |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 69 | } |
| 70 | known_to_be_empty_ = result->empty(); |
| 71 | } |
| 72 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 73 | bool DOMStorageDatabase::CommitChanges(bool clear_all_first, |
| 74 | const DOMStorageValuesMap& changes) { |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 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() && |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 79 | !base::PathExists(file_path_); |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 80 | } |
| 81 | |
| 82 | bool old_known_to_be_empty = known_to_be_empty_; |
| 83 | sql::Transaction transaction(db_.get()); |
| 84 | if (!transaction.Begin()) |
| 85 | return false; |
| 86 | |
| 87 | if (clear_all_first) { |
| 88 | if (!db_->Execute("DELETE FROM ItemTable")) |
| 89 | return false; |
| 90 | known_to_be_empty_ = true; |
| 91 | } |
| 92 | |
| 93 | bool did_delete = false; |
| 94 | bool did_insert = false; |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 95 | DOMStorageValuesMap::const_iterator it = changes.begin(); |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 96 | for(; it != changes.end(); ++it) { |
| 97 | sql::Statement statement; |
| 98 | base::string16 key = it->first; |
Torne (Richard Coles) | 7d4cd47 | 2013-06-19 11:58:07 +0100 | [diff] [blame] | 99 | base::NullableString16 value = it->second; |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 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(char16)); |
| 111 | known_to_be_empty_ = false; |
| 112 | did_insert = true; |
| 113 | } |
| 114 | DCHECK(statement.is_valid()); |
| 115 | statement.Run(); |
| 116 | } |
| 117 | |
| 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; |
| 123 | } |
| 124 | |
| 125 | bool success = transaction.Commit(); |
| 126 | if (!success) |
| 127 | known_to_be_empty_ = old_known_to_be_empty; |
| 128 | return success; |
| 129 | } |
| 130 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 131 | bool DOMStorageDatabase::LazyOpen(bool create_if_needed) { |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 132 | if (failed_to_open_) { |
| 133 | // Don't try to open a database that we know has failed |
| 134 | // already. |
| 135 | return false; |
| 136 | } |
| 137 | |
| 138 | if (IsOpen()) |
| 139 | return true; |
| 140 | |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 141 | bool database_exists = base::PathExists(file_path_); |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 142 | |
| 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; |
| 149 | } |
| 150 | |
| 151 | db_.reset(new sql::Connection()); |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 152 | db_->set_histogram_tag("DOMStorageDatabase"); |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 153 | |
| 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; |
| 160 | } |
| 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; |
| 170 | } |
| 171 | } |
| 172 | |
| 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\"")); |
| 176 | |
| 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(); |
| 185 | |
| 186 | if (current_version == V2) { |
| 187 | return true; |
| 188 | } else if (current_version == V1) { |
| 189 | if (UpgradeVersion1To2()) |
| 190 | return true; |
| 191 | } |
| 192 | } |
| 193 | |
| 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(); |
| 198 | } |
| 199 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 200 | DOMStorageDatabase::SchemaVersion DOMStorageDatabase::DetectSchemaVersion() { |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 201 | DCHECK(IsOpen()); |
| 202 | |
| 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; |
| 211 | |
| 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; |
| 217 | |
| 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; |
| 223 | |
| 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; |
| 231 | } |
| 232 | NOTREACHED(); |
| 233 | return INVALID; |
| 234 | } |
| 235 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 236 | bool DOMStorageDatabase::CreateTableV2() { |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 237 | DCHECK(IsOpen()); |
| 238 | |
| 239 | return db_->Execute( |
| 240 | "CREATE TABLE ItemTable (" |
| 241 | "key TEXT UNIQUE ON CONFLICT REPLACE, " |
| 242 | "value BLOB NOT NULL ON CONFLICT FAIL)"); |
| 243 | } |
| 244 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 245 | bool DOMStorageDatabase::DeleteFileAndRecreate() { |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 246 | DCHECK(!IsOpen()); |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 247 | DCHECK(base::PathExists(file_path_)); |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 248 | |
| 249 | // We should only try and do this once. |
| 250 | if (tried_to_recreate_) |
| 251 | return false; |
| 252 | |
| 253 | tried_to_recreate_ = true; |
| 254 | |
| 255 | // If it's not a directory and we can delete the file, try and open it again. |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 256 | if (!base::DirectoryExists(file_path_) && |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 257 | sql::Connection::Delete(file_path_)) { |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 258 | return LazyOpen(true); |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 259 | } |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 260 | |
| 261 | failed_to_open_ = true; |
| 262 | return false; |
| 263 | } |
| 264 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 265 | bool DOMStorageDatabase::UpgradeVersion1To2() { |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 266 | DCHECK(IsOpen()); |
| 267 | DCHECK(DetectSchemaVersion() == V1); |
| 268 | |
| 269 | sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, |
| 270 | "SELECT * FROM ItemTable")); |
| 271 | DCHECK(statement.is_valid()); |
| 272 | |
| 273 | // Need to migrate from TEXT value column to BLOB. |
| 274 | // Store the current database content so we can re-insert |
| 275 | // the data into the new V2 table. |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 276 | DOMStorageValuesMap values; |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 277 | while (statement.Step()) { |
| 278 | base::string16 key = statement.ColumnString16(0); |
Torne (Richard Coles) | 7d4cd47 | 2013-06-19 11:58:07 +0100 | [diff] [blame] | 279 | base::NullableString16 value(statement.ColumnString16(1), false); |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 280 | values[key] = value; |
| 281 | } |
| 282 | |
| 283 | sql::Transaction migration(db_.get()); |
| 284 | return migration.Begin() && |
| 285 | db_->Execute("DROP TABLE ItemTable") && |
| 286 | CreateTableV2() && |
| 287 | CommitChanges(false, values) && |
| 288 | migration.Commit(); |
| 289 | } |
| 290 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 291 | void DOMStorageDatabase::Close() { |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 292 | db_.reset(NULL); |
| 293 | } |
| 294 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 295 | } // namespace content |