blob: 946ae45ebb6ee1898955e223bde0765dbc63c2d1 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/indexed_db/indexed_db_context_impl.h"
#include <algorithm>
#include <compare>
#include <functional>
#include <iterator>
#include <memory>
#include <ostream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "base/barrier_callback.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/files/file.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/not_fatal_until.h"
#include "base/numerics/clamped_math.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_features.h"
#include "base/task/task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/trace_event/base_tracing.h"
#include "base/trace_event/common/trace_event_common.h"
#include "base/types/expected.h"
#include "base/types/strong_alias.h"
#include "build/build_config.h"
#include "components/services/storage/privileged/mojom/indexed_db_control.mojom-shared.h"
#include "components/services/storage/privileged/mojom/indexed_db_internals_types.mojom.h"
#include "components/services/storage/public/cpp/buckets/bucket_info.h"
#include "components/services/storage/public/cpp/buckets/bucket_init_params.h"
#include "components/services/storage/public/cpp/buckets/bucket_locator.h"
#include "components/services/storage/public/cpp/constants.h"
#include "components/services/storage/public/cpp/quota_error_or.h"
#include "components/services/storage/public/mojom/quota_client.mojom.h"
#include "components/services/storage/public/mojom/storage_policy_update.mojom.h"
#include "content/browser/indexed_db/file_path_util.h"
#include "content/browser/indexed_db/indexed_db_database_error.h"
#include "content/browser/indexed_db/indexed_db_leveldb_coding.h"
#include "content/browser/indexed_db/instance/bucket_context.h"
#include "mojo/public/cpp/bindings/pending_associated_receiver.h"
#include "mojo/public/cpp/bindings/pending_associated_remote.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "mojo/public/cpp/bindings/struct_ptr.h"
#include "net/base/schemeful_site.h"
#include "storage/browser/quota/quota_client_type.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "storage/common/database/database_identifier.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/indexeddb/indexeddb.mojom-shared.h"
#include "third_party/blink/public/mojom/indexeddb/indexeddb.mojom.h"
#include "third_party/zlib/google/zip.h"
#include "url/origin.h"
namespace content::indexed_db {
using blink::StorageKey;
using storage::BucketLocator;
// `IdbPrioritizeForegroundClients` affects relative ordering of transactions
// for a single client. This feature affects which backends are run at a higher
// task priority. See crbug.com/329221141
BASE_FEATURE(kIdbExpediteBackendProcessingForForegroundClients,
"IdbExpediteBackendProcessingForForegroundClients",
base::FEATURE_DISABLED_BY_DEFAULT);
namespace {
base::TaskPriority GetBaseTaskPriority() {
if (base::FeatureList::IsEnabled(
kIdbExpediteBackendProcessingForForegroundClients)) {
return base::TaskPriority::USER_BLOCKING;
}
return base::FeatureList::IsEnabled(base::kUseUtilityThreadGroup)
? base::TaskPriority::USER_BLOCKING
: base::TaskPriority::USER_VISIBLE;
}
// Creates a task runner suitable for use by the main IDB task runner.
scoped_refptr<base::UpdateableSequencedTaskRunner> CreateMainTaskRunner() {
return base::ThreadPool::CreateUpdateableSequencedTaskRunner(
{base::MayBlock(), GetBaseTaskPriority(), base::WithBaseSyncPrimitives(),
// BLOCK_SHUTDOWN to support clearing session-only storage.
base::TaskShutdownBehavior::BLOCK_SHUTDOWN});
}
// Creates a task runner suitable for use by a backing store. See
// https://crbug.com/329221141 for notes on task priority.
scoped_refptr<base::UpdateableSequencedTaskRunner> CreateBucketTaskRunner() {
return base::ThreadPool::CreateUpdateableSequencedTaskRunner(
{base::MayBlock(), GetBaseTaskPriority(), base::WithBaseSyncPrimitives(),
// BLOCK_SHUTDOWN to support clearing session-only storage.
base::TaskShutdownBehavior::BLOCK_SHUTDOWN});
}
bool IsAllowedPath(const std::vector<base::FilePath>& allowed_paths,
const base::FilePath& candidate_path) {
for (const base::FilePath& allowed_path : allowed_paths) {
if (candidate_path == allowed_path || allowed_path.IsParent(candidate_path))
return true;
}
return false;
}
// Used to field IDBFactory requests when the quota system failed to
// find/return a bucket.
class MissingBucketErrorEndpoint : public blink::mojom::IDBFactory {
public:
MissingBucketErrorEndpoint() = default;
~MissingBucketErrorEndpoint() override = default;
// blink::mojom::IDBFactory implementation:
void GetDatabaseInfo(GetDatabaseInfoCallback callback) override {
std::move(callback).Run(
{}, blink::mojom::IDBError::New(
blink::mojom::IDBException::kUnknownError, u"Internal error."));
}
void Open(mojo::PendingAssociatedRemote<blink::mojom::IDBFactoryClient>
factory_client,
mojo::PendingAssociatedRemote<blink::mojom::IDBDatabaseCallbacks>
database_callbacks_remote,
const std::u16string& name,
int64_t version,
mojo::PendingAssociatedReceiver<blink::mojom::IDBTransaction>
transaction_receiver,
int64_t transaction_id,
int scheduling_priority) override {
mojo::AssociatedRemote<blink::mojom::IDBFactoryClient> remote(
std::move(factory_client));
remote->Error(blink::mojom::IDBException::kUnknownError,
u"Internal error.");
}
void DeleteDatabase(mojo::PendingAssociatedRemote<
blink::mojom::IDBFactoryClient> factory_client,
const std::u16string& name,
bool force_close) override {
mojo::AssociatedRemote<blink::mojom::IDBFactoryClient> remote(
std::move(factory_client));
remote->Error(blink::mojom::IDBException::kUnknownError,
u"Internal error.");
}
};
// Getting all the bucket details requires multiple asynchronous steps.
// `IndexedDBContextImpl::ContinueGetAllBucketsDetails` is invoked after
// asynchronously retrieving buckets from the quota manager, whereas
// `FinishGetAllBucketsDetails` is invoked after retrieving details from
// individual bucket contexts.
void FinishGetAllBucketsDetails(
base::OnceCallback<void(std::vector<storage::mojom::IdbOriginMetadataPtr>)>
callback,
std::vector<storage::mojom::IdbBucketMetadataPtr> infos) {
std::map<url::Origin,
std::map<blink::StorageKey,
std::vector<storage::mojom::IdbBucketMetadataPtr>>>
origin_map;
for (storage::mojom::IdbBucketMetadataPtr& info : infos) {
if (info) {
StorageKey storage_key = info->bucket_locator.storage_key;
origin_map[storage_key.origin()][storage_key].push_back(std::move(info));
}
}
std::vector<storage::mojom::IdbOriginMetadataPtr> origins;
for (auto& [origin_url, top_level_site_map] : origin_map) {
storage::mojom::IdbOriginMetadataPtr origin_metadata =
storage::mojom::IdbOriginMetadata::New();
origin_metadata->origin = origin_url;
for (auto& [storage_key, buckets] : top_level_site_map) {
storage::mojom::IdbStorageKeyMetadataPtr storage_key_metadata =
storage::mojom::IdbStorageKeyMetadata::New();
// Sort by name alphabetically but with the default bucket always first.
std::sort(
buckets.begin(), buckets.end(),
[](const storage::mojom::IdbBucketMetadataPtr& b1,
const storage::mojom::IdbBucketMetadataPtr& b2) {
return (b1->bucket_locator.is_default) ||
(!b2->bucket_locator.is_default && b1->name < b2->name);
});
storage_key_metadata->top_level_site = storage_key.top_level_site();
storage_key_metadata->serialized_storage_key = storage_key.Serialize();
storage_key_metadata->buckets = std::move(buckets);
origin_metadata->storage_keys.push_back(std::move(storage_key_metadata));
}
std::sort(origin_metadata->storage_keys.begin(),
origin_metadata->storage_keys.end());
origins.push_back(std::move(origin_metadata));
}
std::sort(origins.begin(), origins.end());
std::move(callback).Run(std::move(origins));
}
} // namespace
IndexedDBContextImpl::IndexedDBContextImpl(
const base::FilePath& base_data_path,
scoped_refptr<storage::QuotaManagerProxy> quota_manager_proxy,
mojo::PendingRemote<storage::mojom::BlobStorageContext>
blob_storage_context,
mojo::PendingRemote<storage::mojom::FileSystemAccessContext>
file_system_access_context,
scoped_refptr<base::SequencedTaskRunner> custom_task_runner)
: idb_task_runner_(custom_task_runner ? custom_task_runner
: CreateMainTaskRunner()),
base_data_path_(base_data_path.empty() ? base::FilePath()
: base_data_path),
quota_manager_proxy_(std::move(quota_manager_proxy)),
quota_client_receiver_(&quota_client_wrapper_),
force_single_thread_(!!custom_task_runner) {
TRACE_EVENT0("IndexedDB", "init");
// QuotaManagerProxy::RegisterClient() must be called during construction
// until crbug.com/1182630 is fixed.
mojo::PendingRemote<storage::mojom::QuotaClient> quota_client_remote;
mojo::PendingReceiver<storage::mojom::QuotaClient> quota_client_receiver =
quota_client_remote.InitWithNewPipeAndPassReceiver();
quota_manager_proxy_->RegisterClient(
std::move(quota_client_remote),
storage::QuotaClientType::kIndexedDatabase);
IDBTaskRunner()->PostTask(
FROM_HERE, base::BindOnce(&IndexedDBContextImpl::BindPipesOnIDBSequence,
weak_factory_.GetWeakPtr(),
std::move(quota_client_receiver),
std::move(blob_storage_context),
std::move(file_system_access_context)));
}
void IndexedDBContextImpl::BindPipesOnIDBSequence(
mojo::PendingReceiver<storage::mojom::QuotaClient>
pending_quota_client_receiver,
mojo::PendingRemote<storage::mojom::BlobStorageContext>
pending_blob_storage_context,
mojo::PendingRemote<storage::mojom::FileSystemAccessContext>
pending_file_system_access_context) {
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
if (pending_quota_client_receiver) {
quota_client_receiver_.Bind(std::move(pending_quota_client_receiver));
}
if (pending_blob_storage_context) {
blob_storage_context_.Bind(std::move(pending_blob_storage_context));
}
if (pending_file_system_access_context) {
file_system_access_context_.Bind(
std::move(pending_file_system_access_context));
}
}
void IndexedDBContextImpl::BindControlOnIDBSequence(
mojo::PendingReceiver<storage::mojom::IndexedDBControl> control) {
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
// We cannot run this in the constructor it needs to be async, but the async
// tasks might not finish before the destructor runs.
InitializeFromFilesIfNeeded(base::DoNothing());
control_receivers_.Add(this, std::move(control));
}
void IndexedDBContextImpl::BindControl(
mojo::PendingReceiver<storage::mojom::IndexedDBControl> control) {
IDBTaskRunner()->PostTask(
FROM_HERE,
base::BindOnce(&IndexedDBContextImpl::BindControlOnIDBSequence,
weak_factory_.GetWeakPtr(), std::move(control)));
}
void IndexedDBContextImpl::BindIndexedDB(
const BucketLocator& bucket_locator,
const storage::BucketClientInfo& client_info,
mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
client_state_checker_remote,
mojo::PendingReceiver<blink::mojom::IDBFactory> receiver) {
auto on_got_bucket = base::BindOnce(
&IndexedDBContextImpl::BindIndexedDBImpl, weak_factory_.GetWeakPtr(),
client_info, std::move(client_state_checker_remote), std::move(receiver));
if (bucket_locator.is_default) {
// If it's for a default bucket, `bucket_locator` will be a placeholder
// without an ID, meaning the bucket still needs to be created.
quota_manager_proxy_->UpdateOrCreateBucket(
storage::BucketInitParams::ForDefaultBucket(bucket_locator.storage_key),
idb_task_runner_, std::move(on_got_bucket));
} else {
// Query the database to make sure the bucket still exists.
quota_manager_proxy_->GetBucketById(bucket_locator.id, idb_task_runner_,
std::move(on_got_bucket));
}
}
void IndexedDBContextImpl::BindIndexedDBImpl(
const storage::BucketClientInfo& client_info,
mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
client_state_checker_remote,
mojo::PendingReceiver<blink::mojom::IDBFactory> pending_receiver,
storage::QuotaErrorOr<storage::BucketInfo> bucket_info) {
std::optional<storage::BucketInfo> bucket;
if (bucket_info.has_value()) {
bucket = bucket_info.value();
}
if (bucket) {
EnsureBucketContext(*bucket, GetDataPath(bucket->ToBucketLocator()));
auto iter = bucket_contexts_.find(bucket->id);
CHECK(iter != bucket_contexts_.end(), base::NotFatalUntil::M130);
iter->second.AsyncCall(&BucketContext::AddReceiver)
.WithArgs(client_info, std::move(client_state_checker_remote),
std::move(pending_receiver));
} else {
mojo::MakeSelfOwnedReceiver(std::make_unique<MissingBucketErrorEndpoint>(),
std::move(pending_receiver));
}
}
void IndexedDBContextImpl::DeleteBucketData(const BucketLocator& bucket_locator,
DeleteBucketDataCallback callback) {
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
DCHECK_EQ(bucket_locator.type, blink::mojom::StorageType::kTemporary);
DCHECK(!callback.is_null());
ForceClose(
bucket_locator.id,
storage::mojom::ForceCloseReason::FORCE_CLOSE_DELETE_ORIGIN,
base::BindOnce(&IndexedDBContextImpl::DidForceCloseForDeleteBucketData,
weak_factory_.GetWeakPtr(), bucket_locator,
std::move(callback)));
}
void IndexedDBContextImpl::DidForceCloseForDeleteBucketData(
const storage::BucketLocator& bucket_locator,
DeleteBucketDataCallback callback) {
if (in_memory()) {
bucket_set_.erase(bucket_locator);
bucket_size_map_.erase(bucket_locator);
std::move(callback).Run(blink::mojom::QuotaStatusCode::kOk);
return;
}
if (!base::DirectoryExists(GetDataPath(bucket_locator))) {
std::move(callback).Run(blink::mojom::QuotaStatusCode::kOk);
return;
}
bool success = std::ranges::all_of(GetStoragePaths(bucket_locator),
&base::DeletePathRecursively);
NotifyOfBucketModification(bucket_locator);
if (success) {
bucket_set_.erase(bucket_locator);
bucket_size_map_.erase(bucket_locator);
}
std::move(callback).Run(success ? blink::mojom::QuotaStatusCode::kOk
: blink::mojom::QuotaStatusCode::kUnknown);
}
void IndexedDBContextImpl::ForceClose(storage::BucketId bucket_id,
storage::mojom::ForceCloseReason reason,
base::OnceClosure closure) {
const bool doom =
reason == storage::mojom::ForceCloseReason::FORCE_CLOSE_DELETE_ORIGIN;
auto iter = bucket_contexts_.find(bucket_id);
if (iter != bucket_contexts_.end()) {
iter->second.AsyncCall(&BucketContext::ForceClose)
.WithArgs(doom)
.Then(std::move(closure));
} else {
std::move(closure).Run();
}
}
void IndexedDBContextImpl::StartMetadataRecording(
storage::BucketId bucket_id,
StartMetadataRecordingCallback callback) {
auto iter = bucket_contexts_.find(bucket_id);
if (iter != bucket_contexts_.end()) {
iter->second.AsyncCall(&BucketContext::StartMetadataRecording)
.Then(std::move(callback));
} else {
pending_bucket_recording_.insert(bucket_id);
std::move(callback).Run();
}
}
void IndexedDBContextImpl::StopMetadataRecording(
storage::BucketId bucket_id,
StopMetadataRecordingCallback callback) {
pending_bucket_recording_.erase(bucket_id);
auto iter = bucket_contexts_.find(bucket_id);
if (iter != bucket_contexts_.end()) {
iter->second.AsyncCall(&BucketContext::StopMetadataRecording)
.Then(std::move(callback));
} else {
std::move(callback).Run({});
}
}
void IndexedDBContextImpl::DownloadBucketData(
storage::BucketId bucket_id,
DownloadBucketDataCallback callback) {
bool success = false;
auto bucket_locator = LookUpBucket(bucket_id);
// Make sure the database hasn't been deleted.
if (!bucket_locator) {
std::move(callback).Run(success, base::FilePath(), base::FilePath());
return;
}
ForceClose(bucket_id,
storage::mojom::ForceCloseReason::FORCE_CLOSE_INTERNALS_PAGE,
base::DoNothing());
base::ScopedTempDir temp_dir;
if (!temp_dir.CreateUniqueTempDir()) {
std::move(callback).Run(success, base::FilePath(), base::FilePath());
return;
}
// This will need to get cleaned up after the download has completed.
base::FilePath temp_path = temp_dir.Take();
std::string storage_key_id =
storage::GetIdentifierFromOrigin(bucket_locator->storage_key.origin());
base::FilePath zip_path = temp_path.AppendASCII(storage_key_id)
.AddExtension(FILE_PATH_LITERAL("zip"));
std::vector<base::FilePath> paths = GetStoragePaths(*bucket_locator);
zip::ZipWithFilterCallback(GetDataPath(*bucket_locator), zip_path,
base::BindRepeating(IsAllowedPath, paths));
success = true;
std::move(callback).Run(success, temp_path, zip_path);
}
void IndexedDBContextImpl::GetAllBucketsDetails(
GetAllBucketsDetailsCallback callback) {
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
InitializeFromFilesIfNeeded(base::BindOnce(
[](base::WeakPtr<IndexedDBContextImpl> handler,
GetAllBucketsDetailsCallback callback) {
if (!handler) {
return;
}
auto collect_buckets =
base::BarrierCallback<storage::QuotaErrorOr<storage::BucketInfo>>(
handler->bucket_set_.size(),
base::BindOnce(
&IndexedDBContextImpl::ContinueGetAllBucketsDetails,
handler, std::move(callback)));
for (const BucketLocator& bucket_locator : handler->bucket_set_) {
handler->quota_manager_proxy_->GetBucketById(
bucket_locator.id, handler->idb_task_runner_, collect_buckets);
}
},
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void IndexedDBContextImpl::ContinueGetAllBucketsDetails(
GetAllBucketsDetailsCallback callback,
std::vector<storage::QuotaErrorOr<storage::BucketInfo>> bucket_infos) {
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
// This barrier receives the bucket info from individual bucket contexts and
// invokes the next step in the process, `FinishGetAllBucketsDetails`.
auto barrier = base::BarrierCallback<storage::mojom::IdbBucketMetadataPtr>(
bucket_infos.size(),
base::BindOnce(&FinishGetAllBucketsDetails,
base::BindOnce(std::move(callback), in_memory())));
// Iterate over existing bucket contexts, pre-fill some data into the bucket
// info struct, and invoke `FillInBucketMetadata`.
for (const auto& quota_error_or_bucket_info : bucket_infos) {
if (!quota_error_or_bucket_info.has_value()) {
barrier.Run({});
continue;
}
const storage::BucketInfo& bucket_info = quota_error_or_bucket_info.value();
const BucketLocator bucket_locator = bucket_info.ToBucketLocator();
storage::mojom::IdbBucketMetadataPtr info =
storage::mojom::IdbBucketMetadata::New();
info->bucket_locator = bucket_locator;
info->name = bucket_info.name;
if (!in_memory()) {
// Size for in-memory DBs will be filled in
// `BucketContext::FillInMetadata()`.
info->size = static_cast<double>(GetBucketDiskUsage(bucket_locator));
}
info->last_modified = GetBucketLastModified(bucket_locator);
if (!in_memory()) {
info->paths = GetStoragePaths(bucket_locator);
}
FillInBucketMetadata(std::move(info), barrier);
}
}
void IndexedDBContextImpl::SetForceKeepSessionState() {
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
force_keep_session_state_ = true;
}
void IndexedDBContextImpl::ApplyPolicyUpdates(
std::vector<storage::mojom::StoragePolicyUpdatePtr> policy_updates) {
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
for (const storage::mojom::StoragePolicyUpdatePtr& update : policy_updates) {
if (!update->purge_on_shutdown) {
origins_to_purge_on_shutdown_.erase(update->origin);
} else {
origins_to_purge_on_shutdown_.insert(update->origin);
}
}
}
void IndexedDBContextImpl::BindTestInterface(
mojo::PendingReceiver<storage::mojom::IndexedDBControlTest> receiver) {
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
test_receivers_.Add(this, std::move(receiver));
}
void IndexedDBContextImpl::AddObserver(
mojo::PendingRemote<storage::mojom::IndexedDBObserver> observer) {
IDBTaskRunner()->PostTask(
FROM_HERE,
base::BindOnce(
[](base::WeakPtr<IndexedDBContextImpl> context,
mojo::PendingRemote<storage::mojom::IndexedDBObserver> observer) {
if (context) {
context->observers_.Add(std::move(observer));
}
},
weak_factory_.GetWeakPtr(), std::move(observer)));
}
void IndexedDBContextImpl::GetBaseDataPathForTesting(
GetBaseDataPathForTestingCallback callback) {
std::move(callback).Run(GetLegacyDataPath());
}
void IndexedDBContextImpl::GetFilePathForTesting(
const BucketLocator& bucket_locator,
GetFilePathForTestingCallback callback) {
std::move(callback).Run(GetLevelDBPath(bucket_locator));
}
void IndexedDBContextImpl::ResetCachesForTesting(base::OnceClosure callback) {
bucket_set_.clear();
bucket_size_map_.clear();
std::move(callback).Run();
}
void IndexedDBContextImpl::WriteToIndexedDBForTesting(
const BucketLocator& bucket_locator,
const std::string& key,
const std::string& value,
base::OnceClosure callback) {
DCHECK(BucketContextExists(bucket_locator.id));
bucket_contexts_.find(bucket_locator.id)
->second.AsyncCall(&BucketContext::WriteToIndexedDBForTesting)
.WithArgs(key, value)
.Then(std::move(callback));
}
void IndexedDBContextImpl::GetPathForBlobForTesting(
const BucketLocator& bucket_locator,
int64_t database_id,
int64_t blob_number,
GetPathForBlobForTestingCallback callback) {
std::move(callback).Run(indexed_db::GetBlobFileNameForKey(
GetBlobStorePath(bucket_locator), database_id, blob_number));
}
void IndexedDBContextImpl::CompactBackingStoreForTesting(
const BucketLocator& bucket_locator,
base::OnceClosure callback) {
bucket_contexts_.find(bucket_locator.id)
->second.AsyncCall(&BucketContext::CompactBackingStoreForTesting)
.Then(std::move(callback));
}
void IndexedDBContextImpl::GetUsageForTesting(
GetUsageForTestingCallback callback) {
if (in_memory()) {
DCHECK_EQ(1U, bucket_contexts_.size());
GetInMemorySize(bucket_contexts_.begin()->first, std::move(callback));
return;
}
int64_t total_size = 0;
for (const BucketLocator& bucket : bucket_set_) {
total_size += GetBucketDiskUsage(bucket);
}
std::move(callback).Run(total_size);
}
void IndexedDBContextImpl::GetSchedulingPriorityForTesting(
GetSchedulingPriorityForTestingCallback callback) {
if (bucket_contexts_.empty()) {
std::move(callback).Run(std::nullopt);
return;
}
bucket_contexts_.begin()
->second.AsyncCall(&BucketContext::CalculateSchedulingPriority)
.Then(std::move(callback));
}
void IndexedDBContextImpl::BindMockFailureSingletonForTesting(
mojo::PendingReceiver<storage::mojom::MockFailureInjector> receiver) {
pending_failure_injector_ = std::move(receiver);
}
void IndexedDBContextImpl::GetDatabaseKeysForTesting(
GetDatabaseKeysForTestingCallback callback) {
std::move(callback).Run(SchemaVersionKey::Encode(), DataVersionKey::Encode());
}
std::optional<BucketLocator> IndexedDBContextImpl::LookUpBucket(
storage::BucketId bucket_id) {
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
auto bucket_locator =
std::ranges::find(bucket_set_, bucket_id, &BucketLocator::id);
if (bucket_locator == bucket_set_.end()) {
return std::nullopt;
}
return *bucket_locator;
}
int64_t IndexedDBContextImpl::GetBucketDiskUsage(
const BucketLocator& bucket_locator) {
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
DCHECK(!in_memory());
if (!LookUpBucket(bucket_locator.id)) {
return 0;
}
bool write_in_progress = false;
const auto iter = bucket_size_map_.find(bucket_locator);
if (iter != bucket_size_map_.end()) {
if (iter->second >= 0) {
return iter->second;
}
write_in_progress = true;
}
const int64_t value = ReadUsageFromDisk(bucket_locator, write_in_progress);
CHECK_GE(value, 0);
bucket_size_map_[bucket_locator] = value;
return value;
}
base::Time IndexedDBContextImpl::GetBucketLastModified(
const BucketLocator& bucket_locator) {
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
if (!LookUpBucket(bucket_locator.id)) {
return base::Time();
}
// Only used by indexeddb-internals; not worth the complexity to implement.
if (in_memory()) {
return base::Time();
}
base::FilePath idb_directory = GetLevelDBPath(bucket_locator);
base::File::Info info;
if (base::GetFileInfo(idb_directory, &info)) {
return info.last_modified;
}
return base::Time();
}
std::vector<base::FilePath> IndexedDBContextImpl::GetStoragePaths(
const BucketLocator& bucket_locator) const {
std::vector<base::FilePath> paths = {GetLevelDBPath(bucket_locator),
GetBlobStorePath(bucket_locator)};
return paths;
}
base::FilePath IndexedDBContextImpl::GetDataPath(
const BucketLocator& bucket_locator) const {
if (in_memory()) {
return base::FilePath();
}
if (indexed_db::ShouldUseLegacyFilePath(bucket_locator)) {
// First-party idb files for the default, for legacy reasons, are stored at:
// {{storage_partition_path}}/IndexedDB/
// TODO(crbug.com/40221733): Migrate all first party buckets to the new
// path.
return GetLegacyDataPath();
}
// Third-party idb files are stored at:
// {{storage_partition_path}}/WebStorage/{{bucket_id}}/IndexedDB/
return quota_manager_proxy_->GetClientBucketPath(
bucket_locator, storage::QuotaClientType::kIndexedDatabase);
}
const base::FilePath IndexedDBContextImpl::GetLegacyDataPath() const {
return base_data_path_.empty()
? base_data_path_
: base_data_path_.Append(storage::kIndexedDbDirectory);
}
const base::FilePath IndexedDBContextImpl::GetFirstPartyDataPathForTesting()
const {
return GetLegacyDataPath();
}
void IndexedDBContextImpl::OnFilesWritten(const BucketLocator& bucket_locator,
bool flushed) {
NotifyOfBucketModification(bucket_locator);
if (!flushed) {
// A negative value indicates "not cached, and LevelDB file write is
// potentially in progress". See `bucket_size_map_` docs.
bucket_size_map_[bucket_locator] = -1;
}
}
void IndexedDBContextImpl::NotifyIndexedDBContentChanged(
const BucketLocator& bucket_locator,
const std::u16string& database_name,
const std::u16string& object_store_name) {
for (auto& observer : observers_) {
observer->OnIndexedDBContentChanged(bucket_locator, database_name,
object_store_name);
}
}
IndexedDBContextImpl::~IndexedDBContextImpl() {
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
// Invalidate the weak pointers that bind `on_ready_for_destruction` (among
// other callbacks) so that `ForceClose()` below doesn't mutate
// `bucket_contexts_` while it's being iterated.
weak_factory_.InvalidateWeakPtrs();
for (auto& [bucket_id, context] : bucket_contexts_) {
context.AsyncCall(&BucketContext::ForceClose).WithArgs(/*doom=*/false);
}
bucket_contexts_.clear();
task_runner_limiters_.clear();
// Shutdown won't go through `ShutdownOnIDBSequence()` for in-memory DBs and
// in some tests.
if (!shutdown_start_time_.is_null()) {
base::UmaHistogramTimes("IndexedDB.ContextShutdownDuration",
base::TimeTicks::Now() - shutdown_start_time_);
}
}
void IndexedDBContextImpl::ShutdownOnIDBSequence(base::TimeTicks start_time) {
// `this` will be destroyed when this method returns.
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
shutdown_start_time_ = start_time;
if (force_keep_session_state_) {
return;
}
// Clear session-only databases.
if (origins_to_purge_on_shutdown_.empty()) {
return;
}
for (const BucketLocator& bucket_locator : bucket_set_) {
// Delete the storage if its origin matches one of the origins to purge, or
// if it is third-party and the top-level site is same-site with one of
// those origins.
bool delete_bucket = base::Contains(origins_to_purge_on_shutdown_,
bucket_locator.storage_key.origin());
if (!delete_bucket && bucket_locator.storage_key.IsThirdPartyContext()) {
delete_bucket = std::ranges::any_of(
origins_to_purge_on_shutdown_, [&](const url::Origin& origin) {
return bucket_locator.storage_key.top_level_site().IsSameSiteWith(
origin);
});
}
if (delete_bucket) {
ForceClose(bucket_locator.id, {},
base::BindOnce(
[](std::vector<base::FilePath> paths) {
std::ranges::for_each(paths,
&base::DeletePathRecursively);
},
GetStoragePaths(bucket_locator)));
}
}
}
// static
void IndexedDBContextImpl::Shutdown(
std::unique_ptr<IndexedDBContextImpl> context) {
IndexedDBContextImpl* context_ptr = context.get();
// Important: This function is NOT called on the IDB Task Runner. All variable
// access must be thread-safe.
if (context->in_memory()) {
context_ptr->IDBTaskRunner()->DeleteSoon(FROM_HERE, std::move(context));
return;
}
context_ptr->IDBTaskRunner()->PostTask(
FROM_HERE,
base::BindOnce(
&IndexedDBContextImpl::InitializeFromFilesIfNeeded,
base::Unretained(context_ptr),
base::BindOnce(&IndexedDBContextImpl::ShutdownOnIDBSequence,
std::move(context), base::TimeTicks::Now())));
}
base::FilePath IndexedDBContextImpl::GetBlobStorePath(
const BucketLocator& bucket_locator) const {
DCHECK(!in_memory());
return GetDataPath(bucket_locator)
.Append(indexed_db::GetBlobStoreFileName(bucket_locator));
}
base::FilePath IndexedDBContextImpl::GetLevelDBPath(
const BucketLocator& bucket_locator) const {
DCHECK(!in_memory());
return GetDataPath(bucket_locator)
.Append(indexed_db::GetLevelDBFileName(bucket_locator));
}
int64_t IndexedDBContextImpl::ReadUsageFromDisk(
const BucketLocator& bucket_locator,
bool write_in_progress) const {
DCHECK(!in_memory());
#if BUILDFLAG(IS_WIN)
// Touch all files in the LevelDB directory to update directory entry
// metadata. See note for `bucket_size_map_` about why this is necessary.
if (write_in_progress) {
const base::FilePath leveldb_dir = GetLevelDBPath(bucket_locator);
base::FileEnumerator file_iter(leveldb_dir, /*recursive=*/true,
base::FileEnumerator::FILES);
for (base::FilePath file_path = file_iter.Next(); !file_path.empty();
file_path = file_iter.Next()) {
base::File file(
file_path, base::File::FLAG_OPEN | base::File::FLAG_WIN_SHARE_DELETE);
}
}
#endif
int64_t total_size = 0;
for (const base::FilePath& path : GetStoragePaths(bucket_locator))
total_size += base::ComputeDirectorySize(path);
return total_size;
}
void IndexedDBContextImpl::NotifyOfBucketModification(
const BucketLocator& bucket_locator) {
// This method is called very frequently, for example after every transaction
// commits. Recalculating disk usage is expensive and often unnecessary (e.g.
// when many transactions commit in a row). Therefore, use a null delta to
// notify the quota system to invalidate its cache but defer updates to
// `bucket_size_map_`.
bucket_size_map_.erase(bucket_locator);
quota_manager_proxy_->NotifyBucketModified(
storage::QuotaClientType::kIndexedDatabase, bucket_locator,
/*delta=*/std::nullopt, base::Time::Now(),
base::SequencedTaskRunner::GetCurrentDefault(), base::DoNothing());
for (auto& observer : observers_) {
observer->OnIndexedDBListChanged(bucket_locator);
}
}
void IndexedDBContextImpl::InitializeFromFilesIfNeeded(
base::OnceClosure callback) {
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
if (did_initialize_from_files_) {
std::move(callback).Run();
return;
}
std::map<StorageKey, base::FilePath> storage_key_to_file_path =
FindLegacyIndexedDBFiles();
std::map<storage::BucketId, base::FilePath> bucket_id_to_file_path =
FindIndexedDBFiles();
if (storage_key_to_file_path.empty() && bucket_id_to_file_path.empty()) {
did_initialize_from_files_ = true;
std::move(callback).Run();
return;
}
const bool running_initialize_from_files =
on_initialize_from_files_callbacks_.size() > 0;
on_initialize_from_files_callbacks_.push_back(std::move(callback));
if (running_initialize_from_files) {
return;
}
using Barrier = base::RepeatingCallback<void(std::optional<BucketLocator>)>;
Barrier barrier = base::BarrierCallback<std::optional<BucketLocator>>(
storage_key_to_file_path.size() + bucket_id_to_file_path.size(),
base::BindOnce(
[](base::WeakPtr<IndexedDBContextImpl> context,
const std::vector<std::optional<BucketLocator>>& bucket_locators) {
DCHECK(context);
for (const std::optional<BucketLocator>& locator :
bucket_locators) {
if (locator) {
context->bucket_set_.insert(*locator);
}
}
context->did_initialize_from_files_ = true;
for (base::OnceClosure& callback :
context->on_initialize_from_files_callbacks_) {
std::move(callback).Run();
if (!context) {
return;
}
}
context->on_initialize_from_files_callbacks_.clear();
},
weak_factory_.GetWeakPtr()));
auto on_lookup_done = base::BindRepeating(
[](Barrier barrier,
storage::QuotaErrorOr<storage::BucketInfo> bucket_info) {
barrier.Run(bucket_info.has_value()
? std::make_optional(bucket_info->ToBucketLocator())
: std::nullopt);
},
barrier);
for (const auto& [storage_key, file_path] : storage_key_to_file_path) {
quota_manager_proxy_->UpdateOrCreateBucket(
storage::BucketInitParams::ForDefaultBucket(storage_key),
idb_task_runner_, on_lookup_done);
}
for (const auto& [bucket_id, file_path] : bucket_id_to_file_path) {
quota_manager_proxy_->GetBucketById(bucket_id, idb_task_runner_,
on_lookup_done);
}
}
void IndexedDBContextImpl::ForceInitializeFromFilesForTesting(
ForceInitializeFromFilesForTestingCallback callback) {
did_initialize_from_files_ = false;
InitializeFromFilesIfNeeded(std::move(callback));
}
std::map<StorageKey, base::FilePath>
IndexedDBContextImpl::FindLegacyIndexedDBFiles() const {
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
base::FilePath data_path = GetLegacyDataPath();
if (data_path.empty())
return {};
std::map<StorageKey, base::FilePath> storage_key_to_file_path;
base::FileEnumerator file_enumerator(data_path, /*recursive=*/false,
base::FileEnumerator::DIRECTORIES);
for (base::FilePath file_path = file_enumerator.Next(); !file_path.empty();
file_path = file_enumerator.Next()) {
if (file_path.Extension() != indexed_db::kLevelDBExtension ||
file_path.RemoveExtension().Extension() !=
indexed_db::kIndexedDBExtension) {
continue;
}
std::string origin_id =
file_path.BaseName().RemoveExtension().RemoveExtension().MaybeAsASCII();
url::Origin origin = storage::GetOriginFromIdentifier(origin_id);
if (origin.opaque()) {
continue;
}
storage_key_to_file_path[StorageKey::CreateFirstParty(origin)] = file_path;
}
return storage_key_to_file_path;
}
std::map<storage::BucketId, base::FilePath>
IndexedDBContextImpl::FindIndexedDBFiles() const {
DCHECK(IDBTaskRunner()->RunsTasksInCurrentSequence());
std::map<storage::BucketId, base::FilePath> bucket_id_to_file_path;
if (base_data_path_.empty())
return bucket_id_to_file_path;
base::FilePath third_party_path =
base_data_path_.Append(storage::kWebStorageDirectory);
base::FileEnumerator file_enumerator(third_party_path, /*recursive=*/true,
base::FileEnumerator::DIRECTORIES);
for (base::FilePath file_path = file_enumerator.Next(); !file_path.empty();
file_path = file_enumerator.Next()) {
if (file_path.BaseName().Extension() == indexed_db::kLevelDBExtension &&
file_path.BaseName().RemoveExtension().value() ==
indexed_db::kIndexedDBFile &&
file_path.DirName().BaseName().value() ==
storage::kIndexedDbDirectory) {
int64_t raw_bucket_id = 0;
bool success = base::StringToInt64(
file_path.DirName().DirName().BaseName().value(), &raw_bucket_id);
if (success && raw_bucket_id > 0) {
bucket_id_to_file_path[storage::BucketId::FromUnsafeValue(
raw_bucket_id)] = file_path;
}
}
}
return bucket_id_to_file_path;
}
void IndexedDBContextImpl::GetInMemorySize(
storage::BucketId bucket_id,
base::OnceCallback<void(int64_t)> on_got_size) const {
auto iter = bucket_contexts_.find(bucket_id);
if (iter == bucket_contexts_.end()) {
std::move(on_got_size).Run(0);
} else {
iter->second.AsyncCall(&BucketContext::GetInMemorySize)
.Then(std::move(on_got_size));
}
}
std::vector<storage::BucketId>
IndexedDBContextImpl::GetOpenBucketIdsForTesting() const {
std::vector<storage::BucketId> output;
output.reserve(bucket_contexts_.size());
for (const auto& [bucket_id, bucket_context] : bucket_contexts_) {
output.push_back(bucket_id);
}
return output;
}
base::SequenceBound<BucketContext>*
IndexedDBContextImpl::GetBucketContextForTesting(const storage::BucketId& id) {
auto it = bucket_contexts_.find(id);
if (it != bucket_contexts_.end()) {
return &it->second;
}
return nullptr;
}
void IndexedDBContextImpl::FillInBucketMetadata(
storage::mojom::IdbBucketMetadataPtr info,
base::OnceCallback<void(storage::mojom::IdbBucketMetadataPtr)> result) {
if (!BucketContextExists(info->bucket_locator.id)) {
std::move(result).Run(std::move(info));
return;
}
bucket_contexts_.find(info->bucket_locator.id)
->second.AsyncCall(&BucketContext::FillInMetadata)
.WithArgs(std::move(info))
.Then(std::move(result));
}
void IndexedDBContextImpl::DestroyBucketContext(
storage::BucketLocator bucket_locator) {
bucket_contexts_.erase(bucket_locator.id);
task_runner_limiters_[bucket_locator.storage_key.top_level_site()]
.active_bucket_count--;
}
void IndexedDBContextImpl::EnsureBucketContext(
const storage::BucketInfo& bucket,
const base::FilePath& data_directory) {
TRACE_EVENT0("IndexedDB", "indexed_db::EnsureBucketContext");
if (BucketContextExists(bucket.id)) {
return;
}
const BucketLocator bucket_locator = bucket.ToBucketLocator();
BucketContext::Delegate bucket_delegate;
bucket_delegate.on_ready_for_destruction = base::BindPostTask(
idb_task_runner_,
base::BindOnce(&IndexedDBContextImpl::DestroyBucketContext,
weak_factory_.GetWeakPtr(), bucket_locator));
bucket_delegate.on_content_changed = base::BindPostTask(
idb_task_runner_,
base::BindRepeating(&IndexedDBContextImpl::NotifyIndexedDBContentChanged,
weak_factory_.GetWeakPtr(), bucket_locator));
bucket_delegate.on_files_written = base::BindPostTask(
idb_task_runner_,
base::BindRepeating(&IndexedDBContextImpl::OnFilesWritten,
weak_factory_.GetWeakPtr(), bucket_locator));
mojo::PendingRemote<storage::mojom::BlobStorageContext>
cloned_blob_storage_context;
// May be null in unit tests.
if (blob_storage_context_) {
blob_storage_context_->Clone(
cloned_blob_storage_context.InitWithNewPipeAndPassReceiver());
}
mojo::PendingRemote<storage::mojom::FileSystemAccessContext> fsa_context;
// May be null in unit tests.
if (file_system_access_context_) {
file_system_access_context_->Clone(
fsa_context.InitWithNewPipeAndPassReceiver());
}
// See docs above `TaskRunnerLimiter`.
scoped_refptr<base::SequencedTaskRunner> bucket_task_runner;
scoped_refptr<base::UpdateableSequencedTaskRunner>
updateable_bucket_task_runner;
TaskRunnerLimiter& task_runner_limiter =
task_runner_limiters_[bucket_locator.storage_key.top_level_site()];
static int kTaskRunnerCountLimit = base::SysInfo::NumberOfProcessors();
if (++task_runner_limiter.active_bucket_count > kTaskRunnerCountLimit) {
// The overflow task runner will never be set to the highest priority.
if (!task_runner_limiter.overflow_task_runner) {
scoped_refptr<base::UpdateableSequencedTaskRunner> overflow_task_runner =
CreateBucketTaskRunner();
overflow_task_runner->UpdatePriority(base::TaskPriority::USER_VISIBLE);
task_runner_limiter.overflow_task_runner =
std::move(overflow_task_runner);
}
bucket_task_runner = task_runner_limiter.overflow_task_runner;
} else {
updateable_bucket_task_runner = CreateBucketTaskRunner();
bucket_task_runner = updateable_bucket_task_runner;
}
if (!base::FeatureList::IsEnabled(
kIdbExpediteBackendProcessingForForegroundClients)) {
updateable_bucket_task_runner.reset();
}
const auto& [iter, inserted] = bucket_contexts_.emplace(
bucket_locator.id,
base::SequenceBound<BucketContext>(
force_single_thread_ ? IDBTaskRunner()
: std::move(bucket_task_runner),
bucket, data_directory, std::move(bucket_delegate),
updateable_bucket_task_runner, quota_manager_proxy_,
std::move(cloned_blob_storage_context), std::move(fsa_context)));
DCHECK(inserted);
if (pending_failure_injector_) {
iter->second.AsyncCall(&BucketContext::BindMockFailureSingletonForTesting)
.WithArgs(std::move(pending_failure_injector_));
}
// Start metadata recording on the context if it was pending.
if (pending_bucket_recording_.erase(bucket_locator.id)) {
iter->second.AsyncCall(&BucketContext::StartMetadataRecording);
}
bucket_set_.insert(bucket_locator);
}
void IndexedDBContextImpl::GetBucketUsage(const BucketLocator& bucket,
GetBucketUsageCallback callback) {
DCHECK_EQ(bucket.type, blink::mojom::StorageType::kTemporary);
if (in_memory()) {
GetInMemorySize(bucket.id, std::move(callback));
} else {
std::move(callback).Run(GetBucketDiskUsage(bucket));
}
}
void IndexedDBContextImpl::GetDefaultStorageKeys(
GetDefaultStorageKeysCallback callback) {
std::vector<StorageKey> storage_keys;
storage_keys.reserve(bucket_set_.size());
for (const BucketLocator& bucket_locator : bucket_set_) {
storage_keys.push_back(bucket_locator.storage_key);
}
std::move(callback).Run(std::move(storage_keys));
}
void IndexedDBContextImpl::PerformStorageCleanup(
PerformStorageCleanupCallback callback) {
std::move(callback).Run();
}
bool IndexedDBContextImpl::BucketContextExists(storage::BucketId bucket_id) {
return bucket_contexts_.find(bucket_id) != bucket_contexts_.end();
}
IndexedDBContextImpl::TaskRunnerLimiter::TaskRunnerLimiter() = default;
IndexedDBContextImpl::TaskRunnerLimiter::~TaskRunnerLimiter() = default;
} // namespace content::indexed_db