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"
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"
16 const base::FilePath::CharType kJournal
[] = FILE_PATH_LITERAL("-journal");
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.
38 DOMStorageDatabase::DOMStorageDatabase() {
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.
52 sql::Connection::Delete(file_path_
);
56 void DOMStorageDatabase::ReadAllValues(DOMStorageValuesMap
* result
) {
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);
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())
87 if (clear_all_first
) {
88 if (!db_
->Execute("DELETE FROM ItemTable"))
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
);
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;
114 DCHECK(statement
.is_valid());
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();
127 known_to_be_empty_
= old_known_to_be_empty
;
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
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
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;
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;
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!
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
) {
188 } else if (current_version
== V1
) {
189 if (UpgradeVersion1To2())
194 // This is the exceptional case - to try and recover we'll attempt
195 // to delete the file and start again.
197 return DeleteFileAndRecreate();
200 DOMStorageDatabase::SchemaVersion
DOMStorageDatabase::DetectSchemaVersion() {
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
)
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"))
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
)
224 switch (statement
.DeclaredColumnType(1)) {
225 case sql::COLUMN_TYPE_BLOB
:
227 case sql::COLUMN_TYPE_TEXT
:
234 bool DOMStorageDatabase::CreateTableV2() {
238 "CREATE TABLE ItemTable ("
239 "key TEXT UNIQUE ON CONFLICT REPLACE, "
240 "value BLOB NOT NULL ON CONFLICT FAIL)");
243 bool DOMStorageDatabase::DeleteFileAndRecreate() {
245 DCHECK(base::PathExists(file_path_
));
247 // We should only try and do this once.
248 if (tried_to_recreate_
)
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;
263 bool DOMStorageDatabase::UpgradeVersion1To2() {
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);
281 sql::Transaction
migration(db_
.get());
282 return migration
.Begin() &&
283 db_
->Execute("DROP TABLE ItemTable") &&
285 CommitChanges(false, values
) &&
289 void DOMStorageDatabase::Close() {
293 } // namespace content