Index: chrome/browser/extensions/extension_settings_leveldb_storage.cc |
diff --git a/chrome/browser/extensions/extension_settings_leveldb_storage.cc b/chrome/browser/extensions/extension_settings_leveldb_storage.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..c19c759f218796f8474735ce6d29fcd6c08bfe24 |
--- /dev/null |
+++ b/chrome/browser/extensions/extension_settings_leveldb_storage.cc |
@@ -0,0 +1,517 @@ |
+// Copyright (c) 2011 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 "chrome/browser/extensions/extension_settings_leveldb_storage.h" |
+ |
+#include "base/bind.h" |
+#include "base/compiler_specific.h" |
+#include "base/json/json_reader.h" |
+#include "base/json/json_writer.h" |
+#include "base/logging.h" |
+#include "base/sys_string_conversions.h" |
+#include "content/browser/browser_thread.h" |
+#include "third_party/leveldb/include/leveldb/iterator.h" |
+#include "third_party/leveldb/include/leveldb/write_batch.h" |
+ |
+namespace { |
+ |
+// Generic error message sent to extensions on failure. |
+const char* kGenericOnFailureMessage = "Extension settings failed"; |
+ |
+// General closure type for computing a value on the FILE thread and calling |
+// back on the UI thread. The *OnFileThread methods should run on the file |
+// thread, all others should run on the UI thread. |
+// |
+// Subclasses should implement RunOnFileThread(), manipulating settings_ |
+// or error_, then call either SucceedOnFileThread() or FailOnFileThread(). |
+class ResultClosure { |
+ public: |
+ // Ownership of callback is taken. |
+ ResultClosure(leveldb::DB* db, ExtensionSettingsStorage::Callback* callback) |
+ : db_(db), |
+ settings_(new DictionaryValue()), |
+ callback_(callback) {} |
+ |
+ ~ResultClosure() {} |
+ |
+ void Run() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ BrowserThread::PostTask( |
+ BrowserThread::FILE, |
+ FROM_HERE, |
+ base::Bind(&ResultClosure::RunOnFileThread, base::Unretained(this))); |
+ } |
+ |
+ protected: |
+ virtual void RunOnFileThread() = 0; |
+ |
+ void SucceedOnFileThread() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+ BrowserThread::PostTask( |
+ BrowserThread::UI, |
+ FROM_HERE, |
+ base::Bind(&ResultClosure::Succeed, base::Unretained(this))); |
+ } |
+ |
+ void FailOnFileThread(std::string error) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+ error_ = error; |
+ BrowserThread::PostTask( |
+ BrowserThread::UI, |
+ FROM_HERE, |
+ base::Bind(&ResultClosure::Fail, base::Unretained(this))); |
+ } |
+ |
+ // Helper for the Get() methods; reads a single key from the database using |
+ // an existing leveldb options object. |
+ // Returns whether the read was successful, in which case the given settings |
+ // will have the value set. Otherwise, error will be set. |
+ bool ReadFromDb( |
+ leveldb::ReadOptions options, |
+ const std::string& key, |
+ DictionaryValue* settings, |
+ std::string* error) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+ std::string value_as_json; |
+ leveldb::Status s = db_->Get(options, key, &value_as_json); |
+ if (s.ok()) { |
+ base::JSONReader json_reader; |
+ Value* value = json_reader.JsonToValue(value_as_json, false, false); |
+ if (value != NULL) { |
+ settings->Set(key, value); |
+ return true; |
+ } else { |
+ // TODO(kalman): clear the offending non-JSON value from the database. |
+ LOG(ERROR) << "Invalid JSON: " << value_as_json; |
+ *error = "Invalid JSON"; |
+ return false; |
+ } |
+ } else if (s.IsNotFound()) { |
+ // Despite there being no value, it was still a success. |
+ return true; |
+ } else { |
+ LOG(ERROR) << "Error reading from database: " << s.ToString(); |
+ *error = s.ToString(); |
+ return false; |
+ } |
+ } |
+ |
+ leveldb::DB* db_; |
Matt Perry
2011/06/23 19:14:11
beware of lifetime here. there's no guarantee that
not at google - send to devlin
2011/06/27 08:51:02
Hm. Well this actually should never happen. The
Matt Perry
2011/06/29 18:08:11
I see. So you're saying RunOnFileThread is the onl
not at google - send to devlin
2011/08/03 06:36:51
Done.
|
+ DictionaryValue* settings_; |
+ std::string error_; |
+ |
+ private: |
+ DISALLOW_COPY_AND_ASSIGN(ResultClosure); |
+ |
+ void Succeed() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ callback_->OnSuccess(settings_); |
Matt Perry
2011/06/23 19:14:11
make settings_ a scoped_ptr, and use settings_.rel
not at google - send to devlin
2011/06/27 08:51:02
Done.
|
+ delete this; |
+ } |
+ |
+ void Fail() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ delete settings_; |
+ callback_->OnFailure(error_); |
+ delete this; |
+ } |
+ |
+ scoped_ptr<ExtensionSettingsStorage::Callback> callback_; |
+}; |
+ |
+// Result closure for Get(key). |
+class Get1ResultClosure : public ResultClosure { |
+ public: |
+ Get1ResultClosure( |
+ leveldb::DB* db, |
+ ExtensionSettingsStorage::Callback* callback, |
+ const std::string& key) |
+ : ResultClosure(db, callback), key_(key) {} |
+ |
+ protected: |
+ virtual void RunOnFileThread() OVERRIDE { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+ if (ReadFromDb(leveldb::ReadOptions(), key_, settings_, &error_)) { |
+ SucceedOnFileThread(); |
+ } else { |
+ FailOnFileThread(kGenericOnFailureMessage); |
+ } |
+ } |
+ |
+ private: |
+ std::string key_; |
+}; |
+ |
+// Result closure for Get(keys). |
+class Get2ResultClosure : public ResultClosure { |
+ public: |
+ // Ownership of keys is taken. |
+ Get2ResultClosure( |
+ leveldb::DB* db, |
+ ExtensionSettingsStorage::Callback* callback, |
+ ListValue* keys) |
+ : ResultClosure(db, callback), keys_(keys) {} |
+ |
+ protected: |
+ virtual void RunOnFileThread() OVERRIDE { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+ leveldb::ReadOptions options; |
+ // All interaction with the db is done on the same thread, so snapshotting |
+ // isn't strictly necessary. This is just defensive. |
+ options.snapshot = db_->GetSnapshot(); |
+ bool success = true; |
+ std::string key; |
+ |
+ for (ListValue::const_iterator it = keys_->begin(); |
+ success && it != keys_->end(); ++it) { |
+ if ((*it)->GetAsString(&key)) { |
+ if (!ReadFromDb(options, key, settings_, &error_)) { |
+ success = false; |
+ } |
+ } |
+ } |
+ db_->ReleaseSnapshot(options.snapshot); |
+ |
+ if (success) { |
+ SucceedOnFileThread(); |
+ } else { |
+ FailOnFileThread(kGenericOnFailureMessage); |
+ } |
+ } |
+ |
+ private: |
+ scoped_ptr<ListValue> keys_; |
+}; |
+ |
+// Result closure for Get() . |
+class Get3ResultClosure : public ResultClosure { |
+ public: |
+ Get3ResultClosure( |
+ leveldb::DB* db, |
+ ExtensionSettingsStorage::Callback* callback) |
+ : ResultClosure(db, callback) { |
+ } |
+ |
+ protected: |
+ virtual void RunOnFileThread() OVERRIDE { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+ base::JSONReader json_reader; |
+ leveldb::ReadOptions options = leveldb::ReadOptions(); |
+ // All interaction with the db is done on the same thread, so snapshotting |
+ // isn't strictly necessary. This is just defensive. |
+ options.snapshot = db_->GetSnapshot(); |
+ scoped_ptr<leveldb::Iterator> it(db_->NewIterator(options)); |
+ |
+ for (it->SeekToFirst(); it->Valid(); it->Next()) { |
+ Value* value = |
+ json_reader.JsonToValue(it->value().ToString(), false, false); |
+ if (value != NULL) { |
+ settings_->Set(it->key().ToString(), value); |
+ } else { |
+ // TODO(kalman): clear the offending non-JSON value from the database. |
+ LOG(ERROR) << "Invalid JSON: " << it->value().ToString(); |
+ } |
+ } |
+ db_->ReleaseSnapshot(options.snapshot); |
+ |
+ if (!it->status().ok()) { |
+ LOG(ERROR) << "DB iteration failed: " << it->status().ToString(); |
+ FailOnFileThread(kGenericOnFailureMessage); |
+ } else { |
+ SucceedOnFileThread(); |
+ } |
+ } |
+}; |
+ |
+// Result closure for Set(key, value). |
+class Set1ResultClosure : public ResultClosure { |
+ public: |
+ // Ownership of the value is taken. |
+ Set1ResultClosure( |
+ leveldb::DB* db, |
+ ExtensionSettingsStorage::Callback* callback, |
+ const std::string& key, |
+ Value* value) |
+ : ResultClosure(db, callback), key_(key), value_(value) {} |
+ |
+ protected: |
+ virtual void RunOnFileThread() OVERRIDE { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+ std::string value_as_json; |
+ base::JSONWriter::Write(value_, false, &value_as_json); |
+ leveldb::Status status = |
+ db_->Put(leveldb::WriteOptions(), key_, value_as_json); |
+ if (status.ok()) { |
+ settings_->Set(key_, value_); |
+ SucceedOnFileThread(); |
+ } else { |
+ LOG(WARNING) << "DB write failed: " << status.ToString(); |
+ delete value_; |
+ FailOnFileThread(kGenericOnFailureMessage); |
+ } |
+ } |
+ |
+ private: |
+ std::string key_; |
+ Value* value_; |
Matt Perry
2011/06/23 19:14:11
see comment about settings_
not at google - send to devlin
2011/06/27 08:51:02
Done.
|
+}; |
+ |
+// Result callback for Set(values). |
+class Set2ResultClosure : public ResultClosure { |
+ public: |
+ // Ownership of values is taken. |
+ Set2ResultClosure( |
+ leveldb::DB* db, |
+ ExtensionSettingsStorage::Callback* callback, |
+ DictionaryValue* values) |
+ : ResultClosure(db, callback), values_(values) { |
+ } |
+ |
+ protected: |
+ virtual void RunOnFileThread() OVERRIDE { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+ // Gather keys since a dictionary can't be modified during iteration. |
+ std::vector<std::string> keys; |
+ for (DictionaryValue::key_iterator it = values_->begin_keys(); |
+ it != values_->end_keys(); ++it) { |
+ keys.push_back(*it); |
+ } |
+ // Write values while transferring ownership from values_ to settings_. |
+ std::string value_as_json; |
+ leveldb::WriteBatch batch; |
+ for (unsigned i = 0; i < keys.size(); ++i) { |
+ Value* value = NULL; |
+ values_->RemoveWithoutPathExpansion(keys[i], &value); |
+ base::JSONWriter::Write(value, false, &value_as_json); |
+ batch.Put(keys[i], value_as_json); |
+ settings_->SetWithoutPathExpansion(keys[i], value); |
+ } |
+ DCHECK(values_->empty()); |
+ |
+ leveldb::Status status = db_->Write(leveldb::WriteOptions(), &batch); |
+ if (status.ok()) { |
+ SucceedOnFileThread(); |
+ } else { |
+ LOG(WARNING) << "DB batch write failed: " << status.ToString(); |
+ FailOnFileThread(kGenericOnFailureMessage); |
+ } |
+ } |
+ |
+ private: |
+ scoped_ptr<DictionaryValue> values_; // will be empty on destruction |
+}; |
+ |
+// Result closure for Remove(key). |
+class Remove1ResultClosure : public ResultClosure { |
+ public: |
+ Remove1ResultClosure( |
+ leveldb::DB* db, |
+ ExtensionSettingsStorage::Callback* callback, |
+ const std::string& key) |
+ : ResultClosure(db, callback), key_(key) { |
+ } |
+ |
+ protected: |
+ virtual void RunOnFileThread() OVERRIDE { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+ leveldb::Status status = db_->Delete(leveldb::WriteOptions(), key_); |
+ if (status.ok() || status.IsNotFound()) { |
+ SucceedOnFileThread(); |
+ } else { |
+ LOG(WARNING) << "DB delete failed: " << status.ToString(); |
+ FailOnFileThread(kGenericOnFailureMessage); |
+ } |
+ } |
+ |
+ private: |
+ std::string key_; |
+}; |
+ |
+// Result closure for Remove(keys). |
+class Remove2ResultClosure : public ResultClosure { |
+ public: |
+ // Ownership of keys is taken. |
+ Remove2ResultClosure( |
+ leveldb::DB* db, |
+ ExtensionSettingsStorage::Callback* callback, |
+ ListValue* keys) |
+ : ResultClosure(db, callback), keys_(keys) { |
+ } |
+ |
+ protected: |
+ virtual void RunOnFileThread() OVERRIDE { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+ std::string key; |
+ leveldb::WriteBatch batch; |
+ for (ListValue::const_iterator it = keys_->begin(); |
+ it != keys_->end(); ++it) { |
+ if ((*it)->GetAsString(&key)) { |
+ batch.Delete(key); |
+ } |
+ } |
+ |
+ leveldb::Status status = db_->Write(leveldb::WriteOptions(), &batch); |
+ if (status.ok() || status.IsNotFound()) { |
+ SucceedOnFileThread(); |
+ } else { |
+ LOG(WARNING) << "DB batch delete failed: " << status.ToString(); |
+ FailOnFileThread(kGenericOnFailureMessage); |
+ } |
+ } |
+ |
+ private: |
+ scoped_ptr<ListValue> keys_; |
+}; |
+ |
+// Result closure for Clear(). |
+class ClearResultClosure : public ResultClosure { |
+ public: |
+ ClearResultClosure( |
+ leveldb::DB* db, |
+ ExtensionSettingsStorage::Callback* callback) |
+ : ResultClosure(db, callback) { |
+ } |
+ |
+ protected: |
+ virtual void RunOnFileThread() OVERRIDE { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+ leveldb::ReadOptions options; |
+ // All interaction with the db is done on the same thread, so snapshotting |
+ // isn't strictly necessary. This is just defensive. |
+ options.snapshot = db_->GetSnapshot(); |
+ scoped_ptr<leveldb::Iterator> it(db_->NewIterator(options)); |
+ leveldb::WriteBatch batch; |
+ |
+ for (it->SeekToFirst(); it->Valid(); it->Next()) { |
+ batch.Delete(it->key()); |
+ } |
+ db_->ReleaseSnapshot(options.snapshot); |
+ |
+ if (it->status().ok()) { |
+ leveldb::Status status = db_->Write(leveldb::WriteOptions(), &batch); |
+ if (status.ok() || status.IsNotFound()) { |
+ SucceedOnFileThread(); |
+ } else { |
+ LOG(WARNING) << "Clear failed: " << status.ToString(); |
+ FailOnFileThread(kGenericOnFailureMessage); |
+ } |
+ } else { |
+ LOG(WARNING) << "Clear iteration failed: " << it->status().ToString(); |
+ FailOnFileThread(kGenericOnFailureMessage); |
+ } |
+ } |
+}; |
+ |
+// Deletes a storage area on the FILE thread. |
+class DeleteStorageClosure { |
+ public: |
+ // Ownership of storage is taken. |
+ explicit DeleteStorageClosure(ExtensionSettingsLeveldbStorage* storage) |
+ : storage_(storage) {} |
+ |
+ ~DeleteStorageClosure() {} |
+ |
+ void Run() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ BrowserThread::PostTask( |
+ BrowserThread::FILE, |
+ FROM_HERE, |
+ base::Bind( |
+ &DeleteStorageClosure::RunOnFileThread, base::Unretained(this))); |
+ } |
+ |
+ private: |
+ DISALLOW_COPY_AND_ASSIGN(DeleteStorageClosure); |
+ |
+ void RunOnFileThread() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+ delete this; |
+ } |
+ |
+ scoped_ptr<ExtensionSettingsStorage> storage_; |
+}; |
+ |
+} // namespace |
+ |
+/* static */ |
+ExtensionSettingsLeveldbStorage* ExtensionSettingsLeveldbStorage::Create( |
+ const FilePath& base_path, |
+ const std::string& extension_id) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+ FilePath path = base_path.AppendASCII(extension_id); |
+ |
+#if defined(OS_POSIX) |
+ std::string os_path(path.value()); |
+#elif defined(OS_WIN) |
+ std::string os_path = base::SysWideToUTF8(path.value()); |
+#endif |
+ |
+ leveldb::Options options; |
+ options.create_if_missing = true; |
+ leveldb::DB* db; |
+ leveldb::Status status = leveldb::DB::Open(options, os_path, &db); |
+ if (!status.ok()) { |
+ LOG(WARNING) << "Failed to create leveldb at " << path.value() << |
+ ": " << status.ToString(); |
+ return NULL; |
+ } |
+ return new ExtensionSettingsLeveldbStorage(db); |
+} |
+ |
+void ExtensionSettingsLeveldbStorage::DestroyEventually() { |
+ (new DeleteStorageClosure(this))->Run(); |
Matt Perry
2011/06/23 19:14:11
we have BrowserThread::DeleteSoon to handle this c
not at google - send to devlin
2011/06/27 08:51:02
Done.
|
+} |
+ |
+void ExtensionSettingsLeveldbStorage::Get( |
+ const std::string& key, ExtensionSettingsStorage::Callback* callback) { |
+ (new Get1ResultClosure(db_.get(), callback, key))->Run(); |
+} |
+ |
+void ExtensionSettingsLeveldbStorage::Get( |
+ const ListValue& keys, ExtensionSettingsStorage::Callback* callback) { |
+ (new Get2ResultClosure(db_.get(), callback, keys.DeepCopy()))->Run(); |
+} |
+ |
+void ExtensionSettingsLeveldbStorage::Get( |
+ ExtensionSettingsStorage::Callback* callback) { |
+ (new Get3ResultClosure(db_.get(), callback))->Run(); |
+} |
+ |
+void ExtensionSettingsLeveldbStorage::Set( |
+ const std::string& key, |
+ const Value& value, |
+ ExtensionSettingsStorage::Callback* callback) { |
+ (new Set1ResultClosure(db_.get(), callback, key, value.DeepCopy()))->Run(); |
+} |
+ |
+void ExtensionSettingsLeveldbStorage::Set( |
+ const DictionaryValue& values, |
+ ExtensionSettingsStorage::Callback* callback) { |
+ (new Set2ResultClosure(db_.get(), callback, values.DeepCopy()))->Run(); |
+} |
+ |
+void ExtensionSettingsLeveldbStorage::Remove( |
+ const std::string& key, ExtensionSettingsStorage::Callback *callback) { |
+ (new Remove1ResultClosure(db_.get(), callback, key))->Run(); |
+} |
+ |
+void ExtensionSettingsLeveldbStorage::Remove( |
+ const ListValue& keys, ExtensionSettingsStorage::Callback *callback) { |
+ (new Remove2ResultClosure(db_.get(), callback, keys.DeepCopy()))->Run(); |
+} |
+ |
+void ExtensionSettingsLeveldbStorage::Clear( |
+ ExtensionSettingsStorage::Callback* callback) { |
+ (new ClearResultClosure(db_.get(), callback))->Run(); |
+} |
+ |
+ExtensionSettingsLeveldbStorage::ExtensionSettingsLeveldbStorage( |
+ leveldb::DB* db) |
+ : db_(db) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+} |
+ |
+ExtensionSettingsLeveldbStorage::~ExtensionSettingsLeveldbStorage() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+} |