Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(24)

Unified Diff: webkit/dom_storage/session_storage_database.cc

Issue 9963107: Persist sessionStorage on disk. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: . Created 8 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: webkit/dom_storage/session_storage_database.cc
diff --git a/webkit/dom_storage/session_storage_database.cc b/webkit/dom_storage/session_storage_database.cc
new file mode 100644
index 0000000000000000000000000000000000000000..4aadcc64bb64aa6c1300bfdd7954476803ab0292
--- /dev/null
+++ b/webkit/dom_storage/session_storage_database.cc
@@ -0,0 +1,448 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "webkit/dom_storage/session_storage_database.h"
+
+#include "base/file_util.h"
+#include "base/stringprintf.h"
+#include "base/string_number_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "third_party/leveldatabase/src/include/leveldb/db.h"
+#include "third_party/leveldatabase/src/include/leveldb/iterator.h"
+#include "third_party/leveldatabase/src/include/leveldb/status.h"
+#include "third_party/leveldatabase/src/include/leveldb/options.h"
+#include "third_party/leveldatabase/src/include/leveldb/write_batch.h"
+
+// Layout of the database:
+// | key | value |
+// ---------------------------------------------------
+// | area-1 (1 = namespace id) | dummy | start of area-1-* keys
+// | area-1-origin1 | 1 (mapid) |
+// | area-1-origin2 | 2 |
+// | area-2 | dummy |
+// | area-2-origin1 | 1 | shallow copy
+// | area-3 | |
+// | area-3-origin1 | 3 | deep copy
+// | map-1 | 2 (refcount) | start of map-1-* keys
+// | map-1-a | b | a = b in map 1
+// | ... | |
+// | next-map-id | 4 |
michaeln 2012/04/12 01:48:46 Do we need to ensure that session namespace_ids in
marja 2012/04/19 10:20:50 Done. In the latest patch, I made it so that Sessi
michaeln 2012/04/22 21:43:34 Sounds like a plausible approach.
+
+namespace {
+
+// Helper functions for creating the keys needed for the schema.
+std::string AreaStartKey(const std::string& namespace_id) {
michaeln 2012/04/12 01:48:46 Maybe NamespaceStartKey() for clarity?
marja 2012/04/19 10:20:50 Done.
+ return base::StringPrintf("area-%s", namespace_id.c_str());
+}
+
+std::string AreaStartKey(int64 namespace_id) {
michaeln 2012/04/12 01:48:46 ditto
marja 2012/04/19 10:20:50 Done.
+ return AreaStartKey(base::Int64ToString(namespace_id));
+}
+
+std::string AreaKey(const std::string& namespace_id,
+ const std::string& origin) {
+ return base::StringPrintf("area-%s-%s", namespace_id.c_str(), origin.c_str());
+}
+
+std::string AreaKey(int64 namespace_id, const GURL& origin) {
+ return AreaKey(base::Int64ToString(namespace_id), origin.spec());
+}
+
+std::string MapStartKey(const std::string& map_id) {
michaeln 2012/04/12 01:48:46 Maybe MapRefcountKey for clarity?
marja 2012/04/19 10:20:50 Done.
+ return base::StringPrintf("map-%s", map_id.c_str());
+}
+
+std::string MapKey(const std::string& map_id, const std::string& key) {
+ return base::StringPrintf("map-%s-%s", map_id.c_str(), key.c_str());
+}
+
+std::string NextMapIdKey() {
+ return "next-map-id";
+}
+
+} // namespace
+
+namespace dom_storage {
+
+SessionStorageDatabase::SessionStorageDatabase(const FilePath& file_path)
+ : DomStorageDatabase(file_path) { }
+
+SessionStorageDatabase::~SessionStorageDatabase() { }
+
+void SessionStorageDatabase::ReadAllValues(int64 namespace_id,
+ const GURL& origin,
+ ValuesMap* result) {
+ // We don't create a database if it doesn't exist. In that case, there is
+ // nothing to be added to the result.
+ if (!LazyOpen(false))
+ return;
+ // Check if there is map for namespace_id-origin.
+ std::string area_key = AreaKey(namespace_id, origin);
+ std::string map_id;
+ leveldb::Status s = db_->Get(leveldb::ReadOptions(), area_key, &map_id);
michaeln 2012/04/12 01:48:46 How should we handle genuine leveldb errors in thi
marja 2012/04/19 10:20:50 Yes, I'll add proper error handling when I'm more
+ if (!s.ok() || map_id.empty())
+ return;
+ ReadValuesInMap(map_id, result);
+}
+
+bool SessionStorageDatabase::CommitChanges(int64 namespace_id,
+ const GURL& origin,
+ bool clear_all_first,
+ const ValuesMap& changes) {
+ if (!LazyOpen(!changes.empty())) {
+ // If we're being asked to commit changes that will result in an
+ // empty database, we return true if the database file doesn't exist.
+ return clear_all_first && changes.empty() &&
+ !file_util::PathExists(file_path_);
+ }
+ leveldb::WriteBatch batch;
+ // Ensure that the key area-N (see the schema above) exists.
+ batch.Put(AreaStartKey(namespace_id), "");
+
+ std::string area_key = AreaKey(namespace_id, origin);
+ std::string map_id;
+ leveldb::Status s;
+ s = db_->Get(leveldb::ReadOptions(), area_key, &map_id);
+ DCHECK(s.ok() || s.IsNotFound());
+ if (s.IsNotFound()) {
michaeln 2012/04/12 01:48:46 style nit: dont need {}'s for one-liners
marja 2012/04/19 10:20:50 Done.
+ CreateNewMap(area_key, &batch, &map_id);
+ } else if (clear_all_first) {
+ DeleteValuesInMap(map_id, &batch);
+ }
+
+ WriteValuesToMap(map_id, changes, &batch);
+
+ s = db_->Write(leveldb::WriteOptions(), &batch);
+ DCHECK(s.ok());
+ return true;
+}
+
+bool SessionStorageDatabase::ShallowCopy(int64 namespace_id,
+ const GURL& origin,
+ int64 new_namespace_id) {
+ // When doing a shallow copy, an existing map is associated with |origin| in
+ // |namespace_id| and its ref count is increased.
michaeln 2012/04/12 01:48:46 When we a Clone() a session namespace, we're going
marja 2012/04/19 10:20:50 Done. For this, I needed an uglyish hack for commi
+
+ // Example, data before shallow copy:
+ // | area-1 (1 = namespace id) | dummy |
+ // | area-1-origin1 | 1 (mapid) |
+ // | map-1 | 1 (refcount) |
+ // | map-1-a | b | a = b in map 1
+
+ // Example, data after shallow copy:
+ // | area-1 (1 = namespace id) | dummy |
+ // | area-1-origin1 | 1 (mapid) |
+ // | area-2 | dummy |
+ // | area-2-origin1 | 1 (mapid) | references the same map
+ // | map-1 | 2 (inc. refcount) |
+ // | map-1-a | b | a = b in map 1
+
+ if (!LazyOpen(true)) {
+ return false;
+ }
+ leveldb::Status s;
+ std::string old_area_key = AreaKey(namespace_id, origin);
+ std::string new_area_key = AreaKey(new_namespace_id, origin);
+ leveldb::WriteBatch batch;
+ batch.Put(AreaStartKey(new_namespace_id), "");
+ std::string map_id;
+ s = db_->Get(leveldb::ReadOptions(), old_area_key, &map_id);
+ DCHECK(s.ok());
+ batch.Put(new_area_key, map_id);
+ // Increase the ref count for the map.
+ std::string map_key = MapStartKey(map_id);
+ std::string old_ref_count_string;
+ s = db_->Get(leveldb::ReadOptions(), map_key, &old_ref_count_string);
+ DCHECK(s.ok());
+ int64 old_ref_count;
+ bool conversion_ok =
+ base::StringToInt64(old_ref_count_string, &old_ref_count);
+ DCHECK(conversion_ok);
+ batch.Put(map_key, base::Int64ToString(++old_ref_count));
+ s = db_->Write(leveldb::WriteOptions(), &batch);
+ DCHECK(s.ok());
+ return true;
+}
+
+bool SessionStorageDatabase::DeepCopy(int64 namespace_id,
+ const GURL& origin) {
+ // When doing a shallow copy, an existing map is associated with |origin| in
+ // |namespace_id| and its ref count is increased.
+
+ // Example, data before shallow copy:
+ // | area-1 (1 = namespace id) | dummy |
+ // | area-1-origin1 | 1 (mapid) |
+ // | area-2 | dummy |
+ // | area-2-origin1 | 1 (mapid) | references the same map
+ // | map-1 | 2 (refcount) |
+ // | map-1-a | b | a = b in map 1
+
+ // Example, data before shallow copy:
+ // | area-1 (1 = namespace id) | dummy |
+ // | area-1-origin1 | 1 (mapid) |
+ // | area-2 | dummy |
+ // | area-2-origin1 | 2 (mapid) | references the new map
+ // | map-1 | 1 (dec. refcount) |
+ // | map-1-a | b | a = b in map 1
+ // | map-2 | 1 (refcount) |
+ // | map-2-a | b | a = b in map 2
+
+ if (!LazyOpen(true)) {
+ return false;
+ }
+
+ leveldb::Status s;
+ std::string area_key = AreaKey(namespace_id, origin);
+ leveldb::WriteBatch batch;
+ std::string old_map_id;
+ s = db_->Get(leveldb::ReadOptions(), area_key, &old_map_id);
+ DCHECK(s.ok());
+ std::string new_map_id;
+ CreateNewMap(area_key, &batch, &new_map_id);
+
+ DecreaseRefCount(old_map_id, &batch);
+
+ // Copy the values in the map.
+ ValuesMap values;
+ ReadValuesInMap(old_map_id, &values);
+ WriteValuesToMap(new_map_id, values, &batch);
+
+ s = db_->Write(leveldb::WriteOptions(), &batch);
+ DCHECK(s.ok());
+ return true;
+}
+
+void SessionStorageDatabase::DeleteOrigin(int64 namespace_id,
+ const GURL& origin) {
+ if (!LazyOpen(false)) {
+ // No need to create the database if it doesn't exist.
+ return;
+ }
+ leveldb::WriteBatch batch;
+ DeleteOrigin(base::Int64ToString(namespace_id), origin.spec(), &batch);
+ leveldb::Status s = db_->Write(leveldb::WriteOptions(), &batch);
+ DCHECK(s.ok());
+}
+
+void SessionStorageDatabase::DeleteNamespace(int64 namespace_id) {
+ if (!LazyOpen(false)) {
+ // No need to create the database if it doesn't exist.
+ return;
+ }
+ leveldb::WriteBatch batch;
+ DeleteNamespace(base::Int64ToString(namespace_id), &batch);
+ leveldb::Status s = db_->Write(leveldb::WriteOptions(), &batch);
+ DCHECK(s.ok());
+}
+
+void SessionStorageDatabase::DeleteLeftoverData() {
michaeln 2012/04/12 01:48:46 As coded, this method appears to delete everything
marja 2012/04/19 10:20:50 I was planning to do the "protect some origins fro
+ if (!LazyOpen(false)) {
+ // No need to create the database if it doesn't exist.
+ return;
+ }
+ leveldb::WriteBatch batch;
+ scoped_ptr<leveldb::Iterator> it(db_->NewIterator(leveldb::ReadOptions()));
+ for (it->SeekToFirst(); it->Valid(); it->Next()) {
+ // If is of the form "area-<namespaceid>", delete the corresponding
+ // namespace.
+ std::string key = it->key().ToString();
+ if (key.find("area-") != 0) {
+ // Itereated past the namespaces.
+ break;
+ }
+ size_t second_dash = key.find('-', 5);
+ if (second_dash == std::string::npos) {
+ std::string namespace_id = key.substr(5);
+ DeleteNamespace(namespace_id, &batch);
+ }
+ }
+ leveldb::Status s = db_->Write(leveldb::WriteOptions(), &batch);
+ DCHECK(s.ok());
+}
+
+bool SessionStorageDatabase::LazyOpen(bool create_if_needed) {
+ if (failed_to_open_) {
+ // Don't try to open a database that we know has failed
+ // already.
+ return false;
+ }
+
+ if (IsOpen())
+ return true;
+
+ bool directory_exists = file_util::PathExists(file_path_);
+
+ if (!directory_exists && !create_if_needed) {
+ // If the directory doesn't exist already and we haven't been asked to
+ // create a file on disk, then we don't bother opening the database. This
+ // means we wait until we absolutely need to put something onto disk before
+ // we do so.
+ return false;
+ }
+
+ leveldb::Options options;
+ // The directory exists but a valid leveldb database might not exist inside it
+ // (e.g., a subset of the needed fiels might be missing). Handle this
+ // situation gracefully by creating the database now.
michaeln 2012/04/12 01:48:46 tzik recently added logic to handle this sort of s
marja 2012/04/19 10:20:50 Do you mean the recovery options in FileSystemDire
+ options.create_if_missing = true;
+ leveldb::Status s;
+ leveldb::DB* db;
+#if defined(OS_WIN)
+ s = leveldb::DB::Open(options, WideToUTF8(file_path_.value()), &db);
+#elif defined(OS_POSIX)
+ s = leveldb::DB::Open(options, file_path_.value(), &db);
+#endif
+ if (!s.ok()) {
+ LOG(WARNING) << "Failed to open leveldb in " << file_path_.value()
+ << ", error: " << s.ToString();
+ DCHECK(db == NULL);
+ failed_to_open_ = true;
+ return false;
+ }
+
+ db_.reset(db);
+ return true;
+}
+
+bool SessionStorageDatabase::IsOpen() const {
+ return db_.get();
+}
+
+void SessionStorageDatabase::ReadValuesInMap(const std::string& map_id,
+ ValuesMap* result) {
+ scoped_ptr<leveldb::Iterator> it(db_->NewIterator(leveldb::ReadOptions()));
+ std::string map_start_key = MapStartKey(map_id);
+ // Skip the dummy entry, then start iterating the keys in the map.
+ for (it->Seek(map_start_key), it->Next(); it->Valid(); it->Next()) {
michaeln 2012/04/12 01:48:46 is it valid to call Next() on an invalid iterator,
marja 2012/04/19 10:20:50 Done. (It's not ok to call Next() on an invalid it
+ // Key is of the form "map-<mapid>-<key>".
+ std::string key = it->key().ToString();
+ size_t second_dash = key.find('-', 4);
+ if (second_dash == std::string::npos ||
+ key.substr(4, second_dash - 4) != map_id) {
+ // Iterated beyond the keys in this map.
+ break;
+ }
+ string16 key16 = UTF8ToUTF16(key.substr(second_dash + 1));
+ // Convert the raw data stored in std::string (it->value()) to raw data
+ // stored in string16.
+ // FIXME(marja): Add tests.
+ string16 value;
+ size_t len = it->value().size() / sizeof(char16);
+ value.resize(len);
+ value.assign(reinterpret_cast<const char16*>(it->value().data()), len);
+ (*result)[key16] = NullableString16(value, false);
+ }
+}
+
+bool SessionStorageDatabase::CreateNewMap(const std::string& area_key,
+ leveldb::WriteBatch* batch,
+ std::string* map_id) {
+ // Create a new map.
+ std::string next_map_id_key = NextMapIdKey();
+ leveldb::Status s = db_->Get(leveldb::ReadOptions(), next_map_id_key, map_id);
+ DCHECK(s.ok() || s.IsNotFound());
+ int64 next_map_id = 0;
+ if (s.IsNotFound()) {
+ *map_id = "0";
+ } else {
+ bool conversion_ok = base::StringToInt64(*map_id, &next_map_id);
+ DCHECK(conversion_ok);
+ // FIXME(marja): What to do if the database is corrupt?
+ }
+ batch->Put(next_map_id_key, base::Int64ToString(++next_map_id));
+ batch->Put(area_key, *map_id);
+ batch->Put(MapStartKey(*map_id), base::Int64ToString(1));
+ return true;
+}
+
+void SessionStorageDatabase::WriteValuesToMap(const std::string& map_id,
+ const ValuesMap& values,
+ leveldb::WriteBatch* batch) {
+ for(ValuesMap::const_iterator it = values.begin(); it != values.end(); ++it) {
+ NullableString16 value = it->second;
+ std::string key = MapKey(map_id, UTF16ToUTF8(it->first));
+ if (value.is_null()) {
+ batch->Delete(key);
+ } else {
+ // Convert the raw data stored in string16 to raw data stored in
+ // std::string.
+ // FIXME(marja): Add tests.
+ const char* data = reinterpret_cast<const char*>(value.string().data());
+ size_t size = value.string().size() * 2;
+ batch->Put(key, std::string(data, size));
+ }
+ }
+}
+
+void SessionStorageDatabase::DecreaseRefCount(const std::string& map_id,
+ leveldb::WriteBatch* batch) {
+ // Decrease the ref count for the map.
+ std::string map_key = MapStartKey(map_id);
+ std::string ref_count_string;
+ leveldb::Status s =
+ db_->Get(leveldb::ReadOptions(), map_key, &ref_count_string);
+ DCHECK(s.ok());
+ int64 ref_count;
+ bool conversion_ok = base::StringToInt64(ref_count_string, &ref_count);
+ DCHECK(conversion_ok);
+ if (--ref_count > 0) {
+ batch->Put(map_key, base::Int64ToString(ref_count));
+ } else {
+ // Clear all keys in the map.
+ DeleteValuesInMap(map_id, batch);
michaeln 2012/04/12 01:48:46 This method name is a little decieving since it al
marja 2012/04/19 10:20:50 Done. DeleteValuesInMap -> ClearMap.
+ batch->Delete(map_key);
+ }
+}
+
+void SessionStorageDatabase::DeleteValuesInMap(const std::string& map_id,
+ leveldb::WriteBatch* batch) {
+ ValuesMap values;
+ ReadValuesInMap(map_id, &values);
michaeln 2012/04/12 01:48:46 Might be nice to read the keys only since that's a
marja 2012/04/19 10:20:50 Done. (Only reading the keys, not the RangeDelete.
+ for (ValuesMap::const_iterator it = values.begin(); it != values.end(); ++it)
+ batch->Delete(MapKey(map_id, UTF16ToUTF8(it->first)));
+}
+
+void SessionStorageDatabase::DeleteOrigin(const std::string& namespace_id,
+ const std::string& origin,
+ leveldb::WriteBatch* batch) {
+ std::string area_key = AreaKey(namespace_id, origin);
+ std::string map_id;
+ leveldb::Status s = db_->Get(leveldb::ReadOptions(), area_key, &map_id);
+ DCHECK(s.ok() || s.IsNotFound());
+ if (s.IsNotFound())
+ return; // Nothing to delete.
+ DecreaseRefCount(map_id, batch);
+ batch->Delete(area_key);
+}
+
+void SessionStorageDatabase::DeleteNamespace(const std::string& namespace_id,
+ leveldb::WriteBatch* batch) {
+ std::string area_start_key = AreaStartKey(namespace_id);
+ // Skip the dummy entry, then start iterating the origins in the area.
michaeln 2012/04/12 01:48:46 The "origins in the area" is a confusing comment s
marja 2012/04/19 10:20:50 Done. (Fixed the comment, AreaStartKey is now Name
+ scoped_ptr<leveldb::Iterator> it(db_->NewIterator(leveldb::ReadOptions()));
+ for (it->Seek(area_start_key), it->Next(); it->Valid(); it->Next()) {
+ // Key is of the form "area-<namespaceid>-<origin>".
+ std::string key = it->key().ToString();
+ size_t second_dash = key.find('-', 5);
+ if (second_dash == std::string::npos ||
+ key.substr(5, second_dash - 5) != namespace_id) {
+ // Iterated beyond the keys in this map.
+ break;
+ }
+ std::string origin = key.substr(second_dash + 1);
+ DeleteOrigin(namespace_id, origin, batch);
+ }
+ batch->Delete(area_start_key);
+}
+
+// FIXME(marja): Remove this (or dump more intelligently).
+void SessionStorageDatabase::DumpData() const {
+ scoped_ptr<leveldb::Iterator> it(db_->NewIterator(leveldb::ReadOptions()));
+ LOG(WARNING) << "Dumping session storage";
+ for (it->SeekToFirst(); it->Valid(); it->Next())
+ LOG(WARNING) << it->key().ToString() << ": " << it->value().ToString();
+ LOG(WARNING) << "Dumping session storage complete";
+}
+
+} // namespace dom_storage

Powered by Google App Engine
This is Rietveld 408576698