| 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/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 void DatabaseErrorCallback(int error, sql::Statement* stmt) { | |
| 19 // Without a callback to ignore errors, | |
| 20 // DomStorageDatabaseTest.TestCanOpenFileThatIsNotADatabase fails with: | |
| 21 // ERROR:connection.cc(735)] sqlite error 522, errno 0: disk I/O error | |
| 22 // FATAL:connection.cc(750)] disk I/O error | |
| 23 // <backtrace> | |
| 24 // <crash> | |
| 25 // | |
| 26 // TODO(shess): If/when infrastructure lands which can allow tests | |
| 27 // to handle SQLite errors appropriately, remove this. | |
| 28 } | |
| 29 | |
| 30 } // anon namespace | |
| 31 | |
| 32 namespace dom_storage { | |
| 33 | |
| 34 // static | |
| 35 base::FilePath DomStorageDatabase::GetJournalFilePath( | |
| 36 const base::FilePath& database_path) { | |
| 37 base::FilePath::StringType journal_file_name = | |
| 38 database_path.BaseName().value() + kJournal; | |
| 39 return database_path.DirName().Append(journal_file_name); | |
| 40 } | |
| 41 | |
| 42 DomStorageDatabase::DomStorageDatabase(const base::FilePath& file_path) | |
| 43 : file_path_(file_path) { | |
| 44 // Note: in normal use we should never get an empty backing path here. | |
| 45 // However, the unit test for this class can contruct an instance | |
| 46 // with an empty path. | |
| 47 Init(); | |
| 48 } | |
| 49 | |
| 50 DomStorageDatabase::DomStorageDatabase() { | |
| 51 Init(); | |
| 52 } | |
| 53 | |
| 54 void DomStorageDatabase::Init() { | |
| 55 failed_to_open_ = false; | |
| 56 tried_to_recreate_ = false; | |
| 57 known_to_be_empty_ = false; | |
| 58 } | |
| 59 | |
| 60 DomStorageDatabase::~DomStorageDatabase() { | |
| 61 if (known_to_be_empty_ && !file_path_.empty()) { | |
| 62 // Delete the db and any lingering journal file from disk. | |
| 63 Close(); | |
| 64 file_util::Delete(file_path_, false); | |
| 65 file_util::Delete(GetJournalFilePath(file_path_), false); | |
| 66 } | |
| 67 } | |
| 68 | |
| 69 void DomStorageDatabase::ReadAllValues(ValuesMap* result) { | |
| 70 if (!LazyOpen(false)) | |
| 71 return; | |
| 72 | |
| 73 sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, | |
| 74 "SELECT * from ItemTable")); | |
| 75 DCHECK(statement.is_valid()); | |
| 76 | |
| 77 while (statement.Step()) { | |
| 78 base::string16 key = statement.ColumnString16(0); | |
| 79 base::string16 value; | |
| 80 statement.ColumnBlobAsString16(1, &value); | |
| 81 (*result)[key] = NullableString16(value, false); | |
| 82 } | |
| 83 known_to_be_empty_ = result->empty(); | |
| 84 } | |
| 85 | |
| 86 bool DomStorageDatabase::CommitChanges(bool clear_all_first, | |
| 87 const ValuesMap& changes) { | |
| 88 if (!LazyOpen(!changes.empty())) { | |
| 89 // If we're being asked to commit changes that will result in an | |
| 90 // empty database, we return true if the database file doesn't exist. | |
| 91 return clear_all_first && changes.empty() && | |
| 92 !file_util::PathExists(file_path_); | |
| 93 } | |
| 94 | |
| 95 bool old_known_to_be_empty = known_to_be_empty_; | |
| 96 sql::Transaction transaction(db_.get()); | |
| 97 if (!transaction.Begin()) | |
| 98 return false; | |
| 99 | |
| 100 if (clear_all_first) { | |
| 101 if (!db_->Execute("DELETE FROM ItemTable")) | |
| 102 return false; | |
| 103 known_to_be_empty_ = true; | |
| 104 } | |
| 105 | |
| 106 bool did_delete = false; | |
| 107 bool did_insert = false; | |
| 108 ValuesMap::const_iterator it = changes.begin(); | |
| 109 for(; it != changes.end(); ++it) { | |
| 110 sql::Statement statement; | |
| 111 base::string16 key = it->first; | |
| 112 NullableString16 value = it->second; | |
| 113 if (value.is_null()) { | |
| 114 statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE, | |
| 115 "DELETE FROM ItemTable WHERE key=?")); | |
| 116 statement.BindString16(0, key); | |
| 117 did_delete = true; | |
| 118 } else { | |
| 119 statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE, | |
| 120 "INSERT INTO ItemTable VALUES (?,?)")); | |
| 121 statement.BindString16(0, key); | |
| 122 statement.BindBlob(1, value.string().data(), | |
| 123 value.string().length() * sizeof(char16)); | |
| 124 known_to_be_empty_ = false; | |
| 125 did_insert = true; | |
| 126 } | |
| 127 DCHECK(statement.is_valid()); | |
| 128 statement.Run(); | |
| 129 } | |
| 130 | |
| 131 if (!known_to_be_empty_ && did_delete && !did_insert) { | |
| 132 sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, | |
| 133 "SELECT count(key) from ItemTable")); | |
| 134 if (statement.Step()) | |
| 135 known_to_be_empty_ = statement.ColumnInt(0) == 0; | |
| 136 } | |
| 137 | |
| 138 bool success = transaction.Commit(); | |
| 139 if (!success) | |
| 140 known_to_be_empty_ = old_known_to_be_empty; | |
| 141 return success; | |
| 142 } | |
| 143 | |
| 144 bool DomStorageDatabase::LazyOpen(bool create_if_needed) { | |
| 145 if (failed_to_open_) { | |
| 146 // Don't try to open a database that we know has failed | |
| 147 // already. | |
| 148 return false; | |
| 149 } | |
| 150 | |
| 151 if (IsOpen()) | |
| 152 return true; | |
| 153 | |
| 154 bool database_exists = file_util::PathExists(file_path_); | |
| 155 | |
| 156 if (!database_exists && !create_if_needed) { | |
| 157 // If the file doesn't exist already and we haven't been asked to create | |
| 158 // a file on disk, then we don't bother opening the database. This means | |
| 159 // we wait until we absolutely need to put something onto disk before we | |
| 160 // do so. | |
| 161 return false; | |
| 162 } | |
| 163 | |
| 164 db_.reset(new sql::Connection()); | |
| 165 db_->set_histogram_tag("DomStorageDatabase"); | |
| 166 db_->set_error_callback(base::Bind(&DatabaseErrorCallback)); | |
| 167 | |
| 168 if (file_path_.empty()) { | |
| 169 // This code path should only be triggered by unit tests. | |
| 170 if (!db_->OpenInMemory()) { | |
| 171 NOTREACHED() << "Unable to open DOM storage database in memory."; | |
| 172 failed_to_open_ = true; | |
| 173 return false; | |
| 174 } | |
| 175 } else { | |
| 176 if (!db_->Open(file_path_)) { | |
| 177 LOG(ERROR) << "Unable to open DOM storage database at " | |
| 178 << file_path_.value() | |
| 179 << " error: " << db_->GetErrorMessage(); | |
| 180 if (database_exists && !tried_to_recreate_) | |
| 181 return DeleteFileAndRecreate(); | |
| 182 failed_to_open_ = true; | |
| 183 return false; | |
| 184 } | |
| 185 } | |
| 186 | |
| 187 // sql::Connection uses UTF-8 encoding, but WebCore style databases use | |
| 188 // UTF-16, so ensure we match. | |
| 189 ignore_result(db_->Execute("PRAGMA encoding=\"UTF-16\"")); | |
| 190 | |
| 191 if (!database_exists) { | |
| 192 // This is a new database, create the table and we're done! | |
| 193 if (CreateTableV2()) | |
| 194 return true; | |
| 195 } else { | |
| 196 // The database exists already - check if we need to upgrade | |
| 197 // and whether it's usable (i.e. not corrupted). | |
| 198 SchemaVersion current_version = DetectSchemaVersion(); | |
| 199 | |
| 200 if (current_version == V2) { | |
| 201 return true; | |
| 202 } else if (current_version == V1) { | |
| 203 if (UpgradeVersion1To2()) | |
| 204 return true; | |
| 205 } | |
| 206 } | |
| 207 | |
| 208 // This is the exceptional case - to try and recover we'll attempt | |
| 209 // to delete the file and start again. | |
| 210 Close(); | |
| 211 return DeleteFileAndRecreate(); | |
| 212 } | |
| 213 | |
| 214 DomStorageDatabase::SchemaVersion DomStorageDatabase::DetectSchemaVersion() { | |
| 215 DCHECK(IsOpen()); | |
| 216 | |
| 217 // Connection::Open() may succeed even if the file we try and open is not a | |
| 218 // database, however in the case that the database is corrupted to the point | |
| 219 // that SQLite doesn't actually think it's a database, | |
| 220 // sql::Connection::GetCachedStatement will DCHECK when we later try and | |
| 221 // run statements. So we run a query here that will not DCHECK but fail | |
| 222 // on an invalid database to verify that what we've opened is usable. | |
| 223 if (db_->ExecuteAndReturnErrorCode("PRAGMA auto_vacuum") != SQLITE_OK) | |
| 224 return INVALID; | |
| 225 | |
| 226 // Look at the current schema - if it doesn't look right, assume corrupt. | |
| 227 if (!db_->DoesTableExist("ItemTable") || | |
| 228 !db_->DoesColumnExist("ItemTable", "key") || | |
| 229 !db_->DoesColumnExist("ItemTable", "value")) | |
| 230 return INVALID; | |
| 231 | |
| 232 // We must use a unique statement here as we aren't going to step it. | |
| 233 sql::Statement statement( | |
| 234 db_->GetUniqueStatement("SELECT key,value from ItemTable LIMIT 1")); | |
| 235 if (statement.DeclaredColumnType(0) != sql::COLUMN_TYPE_TEXT) | |
| 236 return INVALID; | |
| 237 | |
| 238 switch (statement.DeclaredColumnType(1)) { | |
| 239 case sql::COLUMN_TYPE_BLOB: | |
| 240 return V2; | |
| 241 case sql::COLUMN_TYPE_TEXT: | |
| 242 return V1; | |
| 243 default: | |
| 244 return INVALID; | |
| 245 } | |
| 246 NOTREACHED(); | |
| 247 return INVALID; | |
| 248 } | |
| 249 | |
| 250 bool DomStorageDatabase::CreateTableV2() { | |
| 251 DCHECK(IsOpen()); | |
| 252 | |
| 253 return db_->Execute( | |
| 254 "CREATE TABLE ItemTable (" | |
| 255 "key TEXT UNIQUE ON CONFLICT REPLACE, " | |
| 256 "value BLOB NOT NULL ON CONFLICT FAIL)"); | |
| 257 } | |
| 258 | |
| 259 bool DomStorageDatabase::DeleteFileAndRecreate() { | |
| 260 DCHECK(!IsOpen()); | |
| 261 DCHECK(file_util::PathExists(file_path_)); | |
| 262 | |
| 263 // We should only try and do this once. | |
| 264 if (tried_to_recreate_) | |
| 265 return false; | |
| 266 | |
| 267 tried_to_recreate_ = true; | |
| 268 | |
| 269 // If it's not a directory and we can delete the file, try and open it again. | |
| 270 if (!file_util::DirectoryExists(file_path_) && | |
| 271 file_util::Delete(file_path_, false)) | |
| 272 return LazyOpen(true); | |
| 273 | |
| 274 failed_to_open_ = true; | |
| 275 return false; | |
| 276 } | |
| 277 | |
| 278 bool DomStorageDatabase::UpgradeVersion1To2() { | |
| 279 DCHECK(IsOpen()); | |
| 280 DCHECK(DetectSchemaVersion() == V1); | |
| 281 | |
| 282 sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, | |
| 283 "SELECT * FROM ItemTable")); | |
| 284 DCHECK(statement.is_valid()); | |
| 285 | |
| 286 // Need to migrate from TEXT value column to BLOB. | |
| 287 // Store the current database content so we can re-insert | |
| 288 // the data into the new V2 table. | |
| 289 ValuesMap values; | |
| 290 while (statement.Step()) { | |
| 291 base::string16 key = statement.ColumnString16(0); | |
| 292 NullableString16 value(statement.ColumnString16(1), false); | |
| 293 values[key] = value; | |
| 294 } | |
| 295 | |
| 296 sql::Transaction migration(db_.get()); | |
| 297 return migration.Begin() && | |
| 298 db_->Execute("DROP TABLE ItemTable") && | |
| 299 CreateTableV2() && | |
| 300 CommitChanges(false, values) && | |
| 301 migration.Commit(); | |
| 302 } | |
| 303 | |
| 304 void DomStorageDatabase::Close() { | |
| 305 db_.reset(NULL); | |
| 306 } | |
| 307 | |
| 308 } // namespace dom_storage | |
| OLD | NEW |