blob: ab4d5f837b282f2a1b17b4fdebf9e1ced50dfed6 [file] [log] [blame]
// 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/file_util.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/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(
int64 namespace_id, const GURL& origin,
const FilePath& directory, DomStorageTaskRunner* task_runner)
: namespace_id_(namespace_id), 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 (namespace_id == kLocalStorageNamespaceId && !directory.empty()) {
FilePath path = directory.Append(DatabaseFileNameFromOrigin(origin_));
backing_.reset(new DomStorageDatabase(path));
is_initial_import_done_ = false;
}
}
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) {
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, origin_,
FilePath(), 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(new DomStorageDatabase(backing_->file_path()));
file_util::Delete(backing_->file_path(), false);
file_util::Delete(
DomStorageDatabase::GetJournalFilePath(backing_->file_path()), false);
}
}
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(new DomStorageDatabase(backing_->file_path()));
}
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