// 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/dom_storage_area.h" #include "base/bind.h" #include "base/location.h" #include "base/logging.h" #include "base/time.h" #include "third_party/WebKit/Source/WebKit/chromium/public/platform/WebString.h" #include "webkit/database/database_util.h" #include "webkit/dom_storage/dom_storage_map.h" #include "webkit/dom_storage/dom_storage_namespace.h" #include "webkit/dom_storage/dom_storage_task_runner.h" #include "webkit/dom_storage/dom_storage_types.h" #include "webkit/dom_storage/local_storage_database_adapter.h" #include "webkit/fileapi/file_system_util.h" #include "webkit/glue/webkit_glue.h" using webkit_database::DatabaseUtil; namespace dom_storage { static const int kCommitTimerSeconds = 1; DomStorageArea::CommitBatch::CommitBatch() : clear_all_first(false) { } DomStorageArea::CommitBatch::~CommitBatch() {} // static const FilePath::CharType DomStorageArea::kDatabaseFileExtension[] = FILE_PATH_LITERAL(".localstorage"); // static FilePath DomStorageArea::DatabaseFileNameFromOrigin(const GURL& origin) { std::string filename = fileapi::GetOriginIdentifierFromURL(origin); // There is no FilePath.AppendExtension() method, so start with just the // extension as the filename, and then InsertBeforeExtension the desired // name. return FilePath().Append(kDatabaseFileExtension). InsertBeforeExtensionASCII(filename); } // static GURL DomStorageArea::OriginFromDatabaseFileName(const FilePath& name) { DCHECK(name.MatchesExtension(kDatabaseFileExtension)); WebKit::WebString origin_id = webkit_glue::FilePathToWebString( name.BaseName().RemoveExtension()); return DatabaseUtil::GetOriginFromIdentifier(origin_id); } DomStorageArea::DomStorageArea(const GURL& origin, const FilePath& directory, DomStorageTaskRunner* task_runner) : namespace_id_(kLocalStorageNamespaceId), origin_(origin), directory_(directory), task_runner_(task_runner), map_(new DomStorageMap(kPerAreaQuota + kPerAreaOverQuotaAllowance)), is_initial_import_done_(true), is_shutdown_(false), commit_batches_in_flight_(0) { if (!directory.empty()) { FilePath path = directory.Append(DatabaseFileNameFromOrigin(origin_)); backing_.reset(new LocalStorageDatabaseAdapter(path)); is_initial_import_done_ = false; } } DomStorageArea::DomStorageArea( int64 namespace_id, const std::string& persistent_namespace_id, const GURL& origin, DomStorageTaskRunner* task_runner) : namespace_id_(namespace_id), persistent_namespace_id_(persistent_namespace_id), origin_(origin), task_runner_(task_runner), map_(new DomStorageMap(kPerAreaQuota + kPerAreaOverQuotaAllowance)), is_initial_import_done_(true), is_shutdown_(false), commit_batches_in_flight_(0) { DCHECK(namespace_id != kLocalStorageNamespaceId); } DomStorageArea::~DomStorageArea() { } void DomStorageArea::ExtractValues(ValuesMap* map) { if (is_shutdown_) return; InitialImportIfNeeded(); map_->ExtractValues(map); } unsigned DomStorageArea::Length() { if (is_shutdown_) return 0; InitialImportIfNeeded(); return map_->Length(); } NullableString16 DomStorageArea::Key(unsigned index) { if (is_shutdown_) return NullableString16(true); InitialImportIfNeeded(); return map_->Key(index); } NullableString16 DomStorageArea::GetItem(const string16& key) { if (is_shutdown_) return NullableString16(true); InitialImportIfNeeded(); return map_->GetItem(key); } bool DomStorageArea::SetItem(const string16& key, const string16& value, NullableString16* old_value) { if (is_shutdown_) return false; InitialImportIfNeeded(); if (!map_->HasOneRef()) map_ = map_->DeepCopy(); bool success = map_->SetItem(key, value, old_value); if (success && backing_.get()) { CommitBatch* commit_batch = CreateCommitBatchIfNeeded(); commit_batch->changed_values[key] = NullableString16(value, false); } return success; } bool DomStorageArea::RemoveItem(const string16& key, string16* old_value) { if (is_shutdown_) return false; InitialImportIfNeeded(); if (!map_->HasOneRef()) map_ = map_->DeepCopy(); bool success = map_->RemoveItem(key, old_value); if (success && backing_.get()) { CommitBatch* commit_batch = CreateCommitBatchIfNeeded(); commit_batch->changed_values[key] = NullableString16(true); } return success; } bool DomStorageArea::Clear() { if (is_shutdown_) return false; InitialImportIfNeeded(); if (map_->Length() == 0) return false; map_ = new DomStorageMap(kPerAreaQuota + kPerAreaOverQuotaAllowance); if (backing_.get()) { CommitBatch* commit_batch = CreateCommitBatchIfNeeded(); commit_batch->clear_all_first = true; commit_batch->changed_values.clear(); } return true; } DomStorageArea* DomStorageArea::ShallowCopy( int64 destination_namespace_id, const std::string& destination_persistent_namespace_id) { DCHECK_NE(kLocalStorageNamespaceId, namespace_id_); DCHECK_NE(kLocalStorageNamespaceId, destination_namespace_id); DCHECK(!backing_.get()); // SessionNamespaces aren't stored on disk. DomStorageArea* copy = new DomStorageArea( destination_namespace_id, destination_persistent_namespace_id, origin_, task_runner_); copy->map_ = map_; copy->is_shutdown_ = is_shutdown_; return copy; } bool DomStorageArea::HasUncommittedChanges() const { DCHECK(!is_shutdown_); return commit_batch_.get() || commit_batches_in_flight_; } void DomStorageArea::DeleteOrigin() { DCHECK(!is_shutdown_); if (HasUncommittedChanges()) { // TODO(michaeln): This logically deletes the data immediately, // and in a matter of a second, deletes the rows from the backing // database file, but the file itself will linger until shutdown // or purge time. Ideally, this should delete the file more // quickly. Clear(); return; } map_ = new DomStorageMap(kPerAreaQuota + kPerAreaOverQuotaAllowance); if (backing_.get()) { is_initial_import_done_ = false; backing_->Reset(); backing_->DeleteFiles(); } } void DomStorageArea::PurgeMemory() { DCHECK(!is_shutdown_); if (!is_initial_import_done_ || // We're not using any memory. !backing_.get() || // We can't purge anything. HasUncommittedChanges()) // We leave things alone with changes pending. return; // Drop the in memory cache, we'll reload when needed. is_initial_import_done_ = false; map_ = new DomStorageMap(kPerAreaQuota + kPerAreaOverQuotaAllowance); // Recreate the database object, this frees up the open sqlite connection // and its page cache. backing_->Reset(); } void DomStorageArea::Shutdown() { DCHECK(!is_shutdown_); is_shutdown_ = true; map_ = NULL; if (!backing_.get()) return; bool success = task_runner_->PostShutdownBlockingTask( FROM_HERE, DomStorageTaskRunner::COMMIT_SEQUENCE, base::Bind(&DomStorageArea::ShutdownInCommitSequence, this)); DCHECK(success); } void DomStorageArea::InitialImportIfNeeded() { if (is_initial_import_done_) return; DCHECK_EQ(kLocalStorageNamespaceId, namespace_id_); DCHECK(backing_.get()); ValuesMap initial_values; backing_->ReadAllValues(&initial_values); map_->SwapValues(&initial_values); is_initial_import_done_ = true; } DomStorageArea::CommitBatch* DomStorageArea::CreateCommitBatchIfNeeded() { DCHECK(!is_shutdown_); if (!commit_batch_.get()) { commit_batch_.reset(new CommitBatch()); // Start a timer to commit any changes that accrue in the batch, but only if // no commits are currently in flight. In that case the timer will be // started after the commits have happened. if (!commit_batches_in_flight_) { task_runner_->PostDelayedTask( FROM_HERE, base::Bind(&DomStorageArea::OnCommitTimer, this), base::TimeDelta::FromSeconds(kCommitTimerSeconds)); } } return commit_batch_.get(); } void DomStorageArea::OnCommitTimer() { DCHECK_EQ(kLocalStorageNamespaceId, namespace_id_); if (is_shutdown_) return; DCHECK(backing_.get()); DCHECK(commit_batch_.get()); DCHECK(!commit_batches_in_flight_); // This method executes on the primary sequence, we schedule // a task for immediate execution on the commit sequence. DCHECK(task_runner_->IsRunningOnPrimarySequence()); bool success = task_runner_->PostShutdownBlockingTask( FROM_HERE, DomStorageTaskRunner::COMMIT_SEQUENCE, base::Bind(&DomStorageArea::CommitChanges, this, base::Owned(commit_batch_.release()))); ++commit_batches_in_flight_; DCHECK(success); } void DomStorageArea::CommitChanges(const CommitBatch* commit_batch) { // This method executes on the commit sequence. DCHECK(task_runner_->IsRunningOnCommitSequence()); bool success = backing_->CommitChanges(commit_batch->clear_all_first, commit_batch->changed_values); DCHECK(success); // TODO(michaeln): what if it fails? task_runner_->PostTask( FROM_HERE, base::Bind(&DomStorageArea::OnCommitComplete, this)); } void DomStorageArea::OnCommitComplete() { // We're back on the primary sequence in this method. DCHECK(task_runner_->IsRunningOnPrimarySequence()); if (is_shutdown_) return; --commit_batches_in_flight_; if (commit_batch_.get() && !commit_batches_in_flight_) { // More changes have accrued, restart the timer. task_runner_->PostDelayedTask( FROM_HERE, base::Bind(&DomStorageArea::OnCommitTimer, this), base::TimeDelta::FromSeconds(kCommitTimerSeconds)); } } void DomStorageArea::ShutdownInCommitSequence() { // This method executes on the commit sequence. DCHECK(task_runner_->IsRunningOnCommitSequence()); DCHECK(backing_.get()); if (commit_batch_.get()) { // Commit any changes that accrued prior to the timer firing. bool success = backing_->CommitChanges( commit_batch_->clear_all_first, commit_batch_->changed_values); DCHECK(success); } commit_batch_.reset(); backing_.reset(); } } // namespace dom_storage