| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012 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. | |
| 4 | |
| 5 #include "webkit/browser/dom_storage/dom_storage_database.h" | |
| 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 | |
| 18 } // anon namespace | |
| 19 | |
| 20 namespace dom_storage { | |
| 21 | |
| 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); | |
| 28 } | |
| 29 | |
| 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(); | |
| 36 } | |
| 37 | |
| 38 DomStorageDatabase::DomStorageDatabase() { | |
| 39 Init(); | |
| 40 } | |
| 41 | |
| 42 void DomStorageDatabase::Init() { | |
| 43 failed_to_open_ = false; | |
| 44 tried_to_recreate_ = false; | |
| 45 known_to_be_empty_ = false; | |
| 46 } | |
| 47 | |
| 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_); | |
| 53 } | |
| 54 } | |
| 55 | |
| 56 void DomStorageDatabase::ReadAllValues(ValuesMap* result) { | |
| 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); | |
| 68 (*result)[key] = base::NullableString16(value, false); | |
| 69 } | |
| 70 known_to_be_empty_ = result->empty(); | |
| 71 } | |
| 72 | |
| 73 bool DomStorageDatabase::CommitChanges(bool clear_all_first, | |
| 74 const ValuesMap& 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_); | |
| 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; | |
| 95 ValuesMap::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(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 | |
| 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; | |
| 136 } | |
| 137 | |
| 138 if (IsOpen()) | |
| 139 return true; | |
| 140 | |
| 141 bool database_exists = base::PathExists(file_path_); | |
| 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()); | |
| 152 db_->set_histogram_tag("DomStorageDatabase"); | |
| 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 | |
| 200 DomStorageDatabase::SchemaVersion DomStorageDatabase::DetectSchemaVersion() { | |
| 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 | |
| 236 bool DomStorageDatabase::CreateTableV2() { | |
| 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 | |
| 245 bool DomStorageDatabase::DeleteFileAndRecreate() { | |
| 246 DCHECK(!IsOpen()); | |
| 247 DCHECK(base::PathExists(file_path_)); | |
| 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. | |
| 256 if (!base::DirectoryExists(file_path_) && | |
| 257 sql::Connection::Delete(file_path_)) { | |
| 258 return LazyOpen(true); | |
| 259 } | |
| 260 | |
| 261 failed_to_open_ = true; | |
| 262 return false; | |
| 263 } | |
| 264 | |
| 265 bool DomStorageDatabase::UpgradeVersion1To2() { | |
| 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. | |
| 276 ValuesMap values; | |
| 277 while (statement.Step()) { | |
| 278 base::string16 key = statement.ColumnString16(0); | |
| 279 base::NullableString16 value(statement.ColumnString16(1), false); | |
| 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 | |
| 291 void DomStorageDatabase::Close() { | |
| 292 db_.reset(NULL); | |
| 293 } | |
| 294 | |
| 295 } // namespace dom_storage | |
| OLD | NEW |