blob: 690d02acf2f1b1d079144b1ec72d952f2511a245 [file] [log] [blame]
Ben Murdochbb1529c2013-08-08 10:24:53 +01001// Copyright 2013 The Chromium Authors. All rights reserved.
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Ben Murdochbb1529c2013-08-08 10:24:53 +01005#include "content/browser/dom_storage/dom_storage_database.h"
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01006
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
14namespace {
15
16const base::FilePath::CharType kJournal[] = FILE_PATH_LITERAL("-journal");
17
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010018} // anon namespace
19
Ben Murdochbb1529c2013-08-08 10:24:53 +010020namespace content {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010021
22// static
Ben Murdochbb1529c2013-08-08 10:24:53 +010023base::FilePath DOMStorageDatabase::GetJournalFilePath(
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010024 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 Murdochbb1529c2013-08-08 10:24:53 +010030DOMStorageDatabase::DOMStorageDatabase(const base::FilePath& file_path)
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010031 : 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 Murdochbb1529c2013-08-08 10:24:53 +010038DOMStorageDatabase::DOMStorageDatabase() {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010039 Init();
40}
41
Ben Murdochbb1529c2013-08-08 10:24:53 +010042void DOMStorageDatabase::Init() {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010043 failed_to_open_ = false;
44 tried_to_recreate_ = false;
45 known_to_be_empty_ = false;
46}
47
Ben Murdochbb1529c2013-08-08 10:24:53 +010048DOMStorageDatabase::~DOMStorageDatabase() {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010049 if (known_to_be_empty_ && !file_path_.empty()) {
50 // Delete the db and any lingering journal file from disk.
51 Close();
Ben Murdocheb525c52013-07-10 11:40:50 +010052 sql::Connection::Delete(file_path_);
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010053 }
54}
55
Ben Murdochbb1529c2013-08-08 10:24:53 +010056void DOMStorageDatabase::ReadAllValues(DOMStorageValuesMap* result) {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010057 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)7d4cd472013-06-19 11:58:07 +010068 (*result)[key] = base::NullableString16(value, false);
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010069 }
70 known_to_be_empty_ = result->empty();
71}
72
Ben Murdochbb1529c2013-08-08 10:24:53 +010073bool DOMStorageDatabase::CommitChanges(bool clear_all_first,
74 const DOMStorageValuesMap& changes) {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010075 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 Murdoch7dbb3d52013-07-17 14:55:54 +010079 !base::PathExists(file_path_);
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010080 }
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 Murdochbb1529c2013-08-08 10:24:53 +010095 DOMStorageValuesMap::const_iterator it = changes.begin();
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010096 for(; it != changes.end(); ++it) {
97 sql::Statement statement;
98 base::string16 key = it->first;
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +010099 base::NullableString16 value = it->second;
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100100 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 Murdochbb1529c2013-08-08 10:24:53 +0100131bool DOMStorageDatabase::LazyOpen(bool create_if_needed) {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100132 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 Murdoch7dbb3d52013-07-17 14:55:54 +0100141 bool database_exists = base::PathExists(file_path_);
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100142
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 Murdochbb1529c2013-08-08 10:24:53 +0100152 db_->set_histogram_tag("DOMStorageDatabase");
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100153
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 Murdochbb1529c2013-08-08 10:24:53 +0100200DOMStorageDatabase::SchemaVersion DOMStorageDatabase::DetectSchemaVersion() {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100201 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 Murdochbb1529c2013-08-08 10:24:53 +0100236bool DOMStorageDatabase::CreateTableV2() {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100237 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 Murdochbb1529c2013-08-08 10:24:53 +0100245bool DOMStorageDatabase::DeleteFileAndRecreate() {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100246 DCHECK(!IsOpen());
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100247 DCHECK(base::PathExists(file_path_));
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100248
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 Murdoch7dbb3d52013-07-17 14:55:54 +0100256 if (!base::DirectoryExists(file_path_) &&
Ben Murdocheb525c52013-07-10 11:40:50 +0100257 sql::Connection::Delete(file_path_)) {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100258 return LazyOpen(true);
Ben Murdocheb525c52013-07-10 11:40:50 +0100259 }
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100260
261 failed_to_open_ = true;
262 return false;
263}
264
Ben Murdochbb1529c2013-08-08 10:24:53 +0100265bool DOMStorageDatabase::UpgradeVersion1To2() {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100266 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 Murdochbb1529c2013-08-08 10:24:53 +0100276 DOMStorageValuesMap values;
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100277 while (statement.Step()) {
278 base::string16 key = statement.ColumnString16(0);
Torne (Richard Coles)7d4cd472013-06-19 11:58:07 +0100279 base::NullableString16 value(statement.ColumnString16(1), false);
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100280 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 Murdochbb1529c2013-08-08 10:24:53 +0100291void DOMStorageDatabase::Close() {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100292 db_.reset(NULL);
293}
294
Ben Murdochbb1529c2013-08-08 10:24:53 +0100295} // namespace content