blob: 5c017ce53421c1a2d4618cc0dd8f3b777c2717e6 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "storage/browser/quota/quota_database.h"
#include <stddef.h>
#include <stdint.h>
#include <memory>
#include <tuple>
#include <vector>
#include "base/auto_reset.h"
#include "base/containers/contains.h"
#include "base/dcheck_is_on.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/sequence_checker.h"
#include "base/time/clock.h"
#include "components/services/storage/public/cpp/buckets/constants.h"
#include "components/services/storage/public/cpp/quota_error_or.h"
#include "sql/database.h"
#include "sql/error_delegate_util.h"
#include "sql/meta_table.h"
#include "sql/recovery.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "storage/browser/quota/quota_database_migrations.h"
#include "storage/browser/quota/quota_features.h"
#include "storage/browser/quota/quota_internals.mojom.h"
#include "storage/browser/quota/special_storage_policy.h"
#include "url/gurl.h"
using ::blink::StorageKey;
using ::blink::mojom::StorageType;
namespace storage {
namespace {
static const int kDaysInTenYears = 10 * 365;
// Version number of the database schema.
//
// We support migrating the database schema from versions that are at most 2
// years old. Older versions are unsupported, and will cause the database to get
// razed.
//
// Version 1 - 2011-03-17 - http://crrev.com/78521 (unsupported)
// Version 2 - 2011-04-25 - http://crrev.com/82847 (unsupported)
// Version 3 - 2011-07-08 - http://crrev.com/91835 (unsupported)
// Version 4 - 2011-10-17 - http://crrev.com/105822 (unsupported)
// Version 5 - 2015-10-19 - https://crrev.com/354932 (unsupported)
// Version 6 - 2021-04-27 - https://crrev.com/c/2757450 (unsupported)
// Version 7 - 2021-05-20 - https://crrev.com/c/2910136
// Version 8 - 2021-09-01 - https://crrev.com/c/3119831
// Version 9 - 2022-05-13 - https://crrev.com/c/3601253
// Version 10 - 2023-04-10 - https://crrev.com/c/4412082
const int kQuotaDatabaseCurrentSchemaVersion = 10;
const int kQuotaDatabaseCompatibleVersion = 10;
// Definitions for database schema.
const char kBucketTable[] = "buckets";
// Flag to ensure that all existing data for storage keys have been
// registered into the buckets table. Introduced 2022-05 (crrev.com/c/3594211).
const char kBucketsTableBootstrapped[] = "IsBucketsBootstrapped";
const int kCommitIntervalMs = 30000;
base::Clock* g_clock_for_testing = nullptr;
void RecordDatabaseResetHistogram(const DatabaseResetReason reason) {
base::UmaHistogramEnumeration("Quota.QuotaDatabaseReset", reason);
}
// SQL statement fragment for inserting fields into the buckets table.
#define BUCKETS_FIELDS_INSERTER \
" (storage_key, host, type, name, use_count, last_accessed, last_modified," \
" expiration, quota, persistent, durability) " \
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
void BindBucketInitParamsToInsertStatement(const BucketInitParams& params,
StorageType type,
int use_count,
const base::Time& last_accessed,
const base::Time& last_modified,
sql::Statement& statement) {
statement.BindString(0, params.storage_key.Serialize());
statement.BindString(1, params.storage_key.origin().host());
statement.BindInt(2, static_cast<int>(type));
statement.BindString(3, params.name);
statement.BindInt(4, use_count);
statement.BindTime(5, last_accessed);
statement.BindTime(6, last_modified);
statement.BindTime(7, params.expiration);
statement.BindInt64(8, params.quota);
statement.BindBool(9, params.persistent.value_or(false));
int durability = static_cast<int>(
params.durability.value_or(blink::mojom::BucketDurability::kRelaxed));
statement.BindInt(10, durability);
}
// Fields to be retrieved from the database and stored in a
// `BucketTableEntryPtr`.
#define BUCKET_TABLE_ENTRY_FIELDS_SELECTOR \
"id, storage_key, type, name, use_count, last_accessed, last_modified "
mojom::BucketTableEntryPtr BucketTableEntryFromSqlStatement(
sql::Statement& statement) {
mojom::BucketTableEntryPtr entry = mojom::BucketTableEntry::New();
entry->bucket_id = statement.ColumnInt64(0);
entry->storage_key = statement.ColumnString(1);
entry->type = static_cast<blink::mojom::StorageType>(statement.ColumnInt(2));
entry->name = statement.ColumnString(3);
entry->use_count = statement.ColumnInt(4);
entry->last_accessed = statement.ColumnTime(5);
entry->last_modified = statement.ColumnTime(6);
return entry;
}
// Fields to be retrieved from the database and stored in a `BucketInfo`.
#define BUCKET_INFO_FIELDS_SELECTOR \
" id, storage_key, type, name, expiration, quota, persistent, durability "
QuotaErrorOr<BucketInfo> BucketInfoFromSqlStatement(sql::Statement& statement) {
if (!statement.Step()) {
return base::unexpected(statement.Succeeded() ? QuotaError::kNotFound
: QuotaError::kDatabaseError);
}
std::optional<StorageKey> storage_key =
StorageKey::Deserialize(statement.ColumnString(1));
if (!storage_key.has_value()) {
return base::unexpected(QuotaError::kStorageKeyError);
}
return BucketInfo(
BucketId(statement.ColumnInt64(0)), storage_key.value(),
static_cast<StorageType>(statement.ColumnInt(2)),
statement.ColumnString(3), statement.ColumnTime(4),
statement.ColumnInt64(5), statement.ColumnBool(6),
static_cast<blink::mojom::BucketDurability>(statement.ColumnInt(7)));
}
std::set<BucketInfo> BucketInfosFromSqlStatement(sql::Statement& statement) {
std::set<BucketInfo> result;
QuotaErrorOr<BucketInfo> bucket;
while ((bucket = BucketInfoFromSqlStatement(statement)).has_value()) {
result.insert(bucket.value());
}
return result;
}
} // anonymous namespace
const QuotaDatabase::TableSchema QuotaDatabase::kTables[] = {
{kBucketTable,
"(id INTEGER PRIMARY KEY AUTOINCREMENT,"
" storage_key TEXT NOT NULL,"
" host TEXT NOT NULL,"
" type INTEGER NOT NULL,"
" name TEXT NOT NULL,"
" use_count INTEGER NOT NULL,"
" last_accessed INTEGER NOT NULL,"
" last_modified INTEGER NOT NULL,"
" expiration INTEGER NOT NULL,"
" quota INTEGER NOT NULL,"
" persistent INTEGER NOT NULL,"
" durability INTEGER NOT NULL)"
" STRICT"}};
const size_t QuotaDatabase::kTableCount = std::size(QuotaDatabase::kTables);
// static
const QuotaDatabase::IndexSchema QuotaDatabase::kIndexes[] = {
{"buckets_by_storage_key", kBucketTable, "(storage_key, type, name)", true},
{"buckets_by_host", kBucketTable, "(host, type)", false},
{"buckets_by_last_accessed", kBucketTable, "(type, last_accessed)", false},
{"buckets_by_last_modified", kBucketTable, "(type, last_modified)", false},
{"buckets_by_expiration", kBucketTable, "(expiration)", false},
};
const size_t QuotaDatabase::kIndexCount = std::size(QuotaDatabase::kIndexes);
// QuotaDatabase ------------------------------------------------------------
QuotaDatabase::QuotaDatabase(const base::FilePath& profile_path)
: storage_directory_(
profile_path.empty()
? nullptr
: std::make_unique<StorageDirectory>(profile_path)),
db_file_path_(
profile_path.empty()
? base::FilePath()
: storage_directory_->path().AppendASCII(kDatabaseName)),
legacy_db_file_path_(profile_path.empty()
? base::FilePath()
: profile_path.AppendASCII(kDatabaseName)) {
DETACH_FROM_SEQUENCE(sequence_checker_);
}
QuotaDatabase::~QuotaDatabase() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (db_) {
db_->reset_error_callback();
db_->CommitTransaction();
}
}
constexpr char QuotaDatabase::kDatabaseName[];
QuotaErrorOr<BucketInfo> QuotaDatabase::UpdateOrCreateBucket(
const BucketInitParams& params,
int max_bucket_count) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
sqlite_error_code_ = 0;
QuotaErrorOr<BucketInfo> bucket_result =
GetBucket(params.storage_key, params.name, StorageType::kTemporary);
if (!bucket_result.has_value()) {
if (bucket_result.error() == QuotaError::kNotFound) {
bucket_result = CreateBucketInternal(params, StorageType::kTemporary,
max_bucket_count);
}
if (!bucket_result.has_value()) {
bucket_result.error().sqlite_error = sqlite_error_code_;
}
return bucket_result;
}
// Don't bother updating anything if the bucket is expired.
if (!bucket_result->expiration.is_null() &&
(bucket_result->expiration <= GetNow())) {
return bucket_result;
}
// Update the parameters that can be changed.
if (!params.expiration.is_null() &&
(params.expiration != bucket_result->expiration)) {
DCHECK(!bucket_result->is_default());
bucket_result =
UpdateBucketExpiration(bucket_result->id, params.expiration);
DCHECK(bucket_result.has_value());
}
if (params.persistent && (*params.persistent != bucket_result->persistent)) {
DCHECK(!bucket_result->is_default());
bucket_result =
UpdateBucketPersistence(bucket_result->id, *params.persistent);
DCHECK(bucket_result.has_value());
}
return bucket_result;
}
QuotaErrorOr<BucketInfo> QuotaDatabase::GetOrCreateBucketDeprecated(
const BucketInitParams& params,
StorageType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return GetBucket(params.storage_key, params.name, type)
.or_else([&](DetailedQuotaError error) -> QuotaErrorOr<BucketInfo> {
if (error != QuotaError::kNotFound) {
return base::unexpected(error);
}
return CreateBucketInternal(params, type);
});
}
QuotaErrorOr<BucketInfo> QuotaDatabase::CreateBucketForTesting(
const StorageKey& storage_key,
const std::string& bucket_name,
StorageType storage_type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
BucketInitParams params(storage_key, bucket_name);
return CreateBucketInternal(params, storage_type);
}
QuotaErrorOr<BucketInfo> QuotaDatabase::GetBucket(
const StorageKey& storage_key,
const std::string& bucket_name,
blink::mojom::StorageType storage_type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return base::unexpected(open_error);
}
static constexpr char kSql[] =
// clang-format off
"SELECT " BUCKET_INFO_FIELDS_SELECTOR
"FROM buckets "
"WHERE storage_key = ? AND type = ? AND name = ?";
// clang-format on
last_operation_ = "GetBucket";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindString(0, storage_key.Serialize());
statement.BindInt(1, static_cast<int>(storage_type));
statement.BindString(2, bucket_name);
return BucketInfoFromSqlStatement(statement);
}
QuotaErrorOr<BucketInfo> QuotaDatabase::UpdateBucketExpiration(
BucketId bucket,
const base::Time& expiration) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return base::unexpected(open_error);
}
static constexpr char kSql[] =
// clang-format off
"UPDATE buckets "
"SET expiration = ? "
"WHERE id = ? "
"RETURNING " BUCKET_INFO_FIELDS_SELECTOR;
// clang-format on
last_operation_ = "UpdateBucketExpiration";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindTime(0, expiration);
statement.BindInt64(1, bucket.value());
ScheduleCommit();
return BucketInfoFromSqlStatement(statement);
}
QuotaErrorOr<BucketInfo> QuotaDatabase::UpdateBucketPersistence(
BucketId bucket,
bool persistent) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return base::unexpected(open_error);
}
static constexpr char kSql[] =
// clang-format off
"UPDATE buckets "
"SET persistent = ? "
"WHERE id = ? "
"RETURNING " BUCKET_INFO_FIELDS_SELECTOR;
// clang-format on
last_operation_ = "UpdateBucketPersistence";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindBool(0, persistent);
statement.BindInt64(1, bucket.value());
ScheduleCommit();
return BucketInfoFromSqlStatement(statement);
}
QuotaErrorOr<BucketInfo> QuotaDatabase::GetBucketById(BucketId bucket_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return base::unexpected(open_error);
}
static constexpr char kSql[] =
// clang-format off
"SELECT " BUCKET_INFO_FIELDS_SELECTOR
"FROM buckets "
"WHERE id = ?";
// clang-format on
last_operation_ = "GetBucketById";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt64(0, bucket_id.value());
return BucketInfoFromSqlStatement(statement);
}
QuotaErrorOr<std::set<BucketInfo>> QuotaDatabase::GetBucketsForType(
StorageType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return base::unexpected(open_error);
}
static constexpr char kSql[] =
// clang-format off
"SELECT " BUCKET_INFO_FIELDS_SELECTOR
"FROM buckets "
"WHERE type = ?";
// clang-format on
last_operation_ = "GetBucketsForType";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt(0, static_cast<int>(type));
return BucketInfosFromSqlStatement(statement);
}
QuotaErrorOr<std::set<BucketInfo>> QuotaDatabase::GetBucketsForHost(
const std::string& host,
StorageType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return base::unexpected(open_error);
}
static constexpr char kSql[] =
// clang-format off
"SELECT " BUCKET_INFO_FIELDS_SELECTOR
"FROM buckets "
"WHERE host = ? AND type = ?";
// clang-format on
last_operation_ = "GetBucketsForHost";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindString(0, host);
statement.BindInt(1, static_cast<int>(type));
return BucketInfosFromSqlStatement(statement);
}
QuotaErrorOr<std::set<BucketInfo>> QuotaDatabase::GetBucketsForStorageKey(
const StorageKey& storage_key,
StorageType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return base::unexpected(open_error);
}
static constexpr char kSql[] =
// clang-format off
"SELECT " BUCKET_INFO_FIELDS_SELECTOR
"FROM buckets "
"WHERE storage_key = ? AND type = ?";
// clang-format on
last_operation_ = "GetBucketsForStorageKey";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindString(0, storage_key.Serialize());
statement.BindInt(1, static_cast<int>(type));
return BucketInfosFromSqlStatement(statement);
}
QuotaError QuotaDatabase::SetStorageKeyLastAccessTime(
const StorageKey& storage_key,
StorageType type,
base::Time last_accessed) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return open_error;
}
// clang-format off
static constexpr char kSqlReadLastAccessed[] =
"SELECT last_accessed FROM buckets "
"WHERE storage_key = ? AND type = ? AND name = ?";
// clang-format on
last_operation_ = "ReadStorageKeyLastAccessTime";
sql::Statement statement_read(
db_->GetCachedStatement(SQL_FROM_HERE, kSqlReadLastAccessed));
statement_read.BindString(0, storage_key.Serialize());
statement_read.BindInt(1, static_cast<int>(type));
statement_read.BindString(2, kDefaultBucketName);
if (statement_read.Step()) {
base::Time earlier_last_accessed = statement_read.ColumnTime(0);
// We want to record the delta in days between the last_accessed field value
// and the new value so we better understand how often old quota buckets are
// loaded for new use.
if (!earlier_last_accessed.is_null() &&
last_accessed > earlier_last_accessed) {
int days_since_last_accessed =
(last_accessed - earlier_last_accessed).InDays();
if (days_since_last_accessed > 400) {
base::UmaHistogramCustomCounts("Quota.DaysSinceLastAccessed400DaysGT",
days_since_last_accessed, 401,
kDaysInTenYears, 100);
} else {
base::UmaHistogramCustomCounts("Quota.DaysSinceLastAccessed400DaysLTE",
days_since_last_accessed, 1, 400, 100);
}
}
}
// clang-format off
static constexpr char kSqlSetLastAccessed[] =
"UPDATE buckets "
"SET use_count = use_count + 1, last_accessed = ? "
"WHERE storage_key = ? AND type = ? AND name = ?";
// clang-format on
last_operation_ = "SetStorageKeyLastAccessTime";
sql::Statement statement_set(
db_->GetCachedStatement(SQL_FROM_HERE, kSqlSetLastAccessed));
statement_set.BindTime(0, last_accessed);
statement_set.BindString(1, storage_key.Serialize());
statement_set.BindInt(2, static_cast<int>(type));
statement_set.BindString(3, kDefaultBucketName);
if (!statement_set.Run()) {
return QuotaError::kDatabaseError;
}
ScheduleCommit();
return QuotaError::kNone;
}
QuotaError QuotaDatabase::SetBucketLastAccessTime(BucketId bucket_id,
base::Time last_accessed) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!bucket_id.is_null());
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return open_error;
}
// clang-format off
static constexpr char kSql[] =
"UPDATE buckets "
"SET use_count = use_count + 1, last_accessed = ? "
"WHERE id = ?";
// clang-format on
last_operation_ = "SetBucketLastAccessTime";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindTime(0, last_accessed);
statement.BindInt64(1, bucket_id.value());
if (!statement.Run()) {
return QuotaError::kDatabaseError;
}
ScheduleCommit();
return QuotaError::kNone;
}
QuotaError QuotaDatabase::SetBucketLastModifiedTime(BucketId bucket_id,
base::Time last_modified) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!bucket_id.is_null());
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return open_error;
}
static constexpr char kSql[] =
"UPDATE buckets SET last_modified = ? WHERE id = ?";
last_operation_ = "SetBucketLastModifiedTime";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindTime(0, last_modified);
statement.BindInt64(1, bucket_id.value());
if (!statement.Run()) {
return QuotaError::kDatabaseError;
}
ScheduleCommit();
return QuotaError::kNone;
}
QuotaError QuotaDatabase::RegisterInitialStorageKeyInfo(
base::flat_map<StorageType, std::set<StorageKey>> storage_keys_by_type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return open_error;
}
for (const auto& type_and_storage_keys : storage_keys_by_type) {
StorageType storage_type = type_and_storage_keys.first;
for (const auto& storage_key : type_and_storage_keys.second) {
static constexpr char kSql[] =
"INSERT OR IGNORE INTO buckets" BUCKETS_FIELDS_INSERTER;
last_operation_ = "BootstrapDefaultBucket";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
BucketInitParams init_params =
BucketInitParams::ForDefaultBucket(storage_key);
BindBucketInitParamsToInsertStatement(
init_params, storage_type, /*use_count=*/0,
/*last_accessed=*/base::Time(),
/*last_modified=*/base::Time(), statement);
if (!statement.Run()) {
return QuotaError::kDatabaseError;
}
}
}
ScheduleCommit();
return QuotaError::kNone;
}
QuotaErrorOr<mojom::BucketTableEntryPtr> QuotaDatabase::GetBucketInfoForTest(
BucketId bucket_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!bucket_id.is_null());
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return base::unexpected(open_error);
}
static constexpr char kSql[] =
// clang-format off
"SELECT " BUCKET_TABLE_ENTRY_FIELDS_SELECTOR
"FROM buckets "
"WHERE id = ?";
// clang-format on
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt64(0, bucket_id.value());
if (!statement.Step()) {
return base::unexpected(statement.Succeeded() ? QuotaError::kNotFound
: QuotaError::kDatabaseError);
}
std::optional<StorageKey> storage_key =
StorageKey::Deserialize(statement.ColumnString(1));
if (!storage_key.has_value()) {
return base::unexpected(QuotaError::kStorageKeyError);
}
mojom::BucketTableEntryPtr entry =
BucketTableEntryFromSqlStatement(statement);
return entry;
}
QuotaErrorOr<mojom::BucketTableEntryPtr> QuotaDatabase::DeleteBucketData(
const BucketLocator& bucket) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return base::unexpected(open_error);
}
// Doom bucket directory first so data is no longer accessible, even if
// directory deletion fails. `storage_directory_` may be nullptr for
// in-memory only.
if (storage_directory_ && !storage_directory_->DoomBucket(bucket)) {
return base::unexpected(QuotaError::kFileOperationError);
}
static constexpr char kSql[] =
"DELETE FROM buckets WHERE id = ? "
"RETURNING " BUCKET_TABLE_ENTRY_FIELDS_SELECTOR;
last_operation_ = "DeleteBucket";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt64(0, bucket.id.value());
if (!statement.Step()) {
return base::unexpected(QuotaError::kDatabaseError);
}
// Scheduling this commit introduces the chance of inconsistencies
// between the buckets table and data stored on disk in the file system.
// If there is a crash or a battery failure before the transaction is
// committed, the bucket directory may be deleted from the file system,
// while an entry still may exist in the database.
//
// While this is not ideal, this does not introduce any new edge case.
// We should check that bucket IDs have existing associated directories,
// because database corruption could result in invalid bucket IDs.
// TODO(crbug.com/40832940): For handling inconsistencies between the db and
// the file system.
ScheduleCommit();
if (storage_directory_) {
storage_directory_->ClearDoomedBuckets();
}
return BucketTableEntryFromSqlStatement(statement);
}
QuotaErrorOr<std::set<BucketLocator>> QuotaDatabase::GetBucketsForEviction(
StorageType type,
int64_t target_usage,
const std::map<BucketLocator, int64_t>& usage_map,
const std::set<BucketId>& bucket_exceptions,
SpecialStoragePolicy* special_storage_policy) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return base::unexpected(open_error);
}
std::set<BucketLocator> buckets_to_evict;
// clang-format off
static constexpr char kSql[] =
"SELECT id, storage_key, name FROM buckets "
"WHERE type = ? AND persistent = 0 "
"ORDER BY last_accessed";
// clang-format on
last_operation_ = "GetBucketsForEviction";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt(0, static_cast<int>(type));
// The total space used by all buckets marked for eviction.
int64_t total_usage = 0;
while (statement.Step()) {
std::optional<StorageKey> read_storage_key =
StorageKey::Deserialize(statement.ColumnString(1));
if (!read_storage_key.has_value()) {
// TODO(estade): this row needs to be deleted.
continue;
}
BucketId read_bucket_id = BucketId(statement.ColumnInt64(0));
if (base::Contains(bucket_exceptions, read_bucket_id)) {
continue;
}
// Only the default bucket is persisted by `navigator.storage.persist()`.
const bool is_default = statement.ColumnString(2) == kDefaultBucketName;
const GURL read_gurl = read_storage_key->origin().GetURL();
if (is_default && special_storage_policy &&
(special_storage_policy->IsStorageDurable(read_gurl) ||
special_storage_policy->IsStorageUnlimited(read_gurl))) {
continue;
}
BucketLocator locator(read_bucket_id, std::move(read_storage_key).value(),
type, is_default);
const auto& bucket_usage = usage_map.find(locator);
total_usage += (bucket_usage == usage_map.end()) ? 1 : bucket_usage->second;
buckets_to_evict.insert(locator);
if (total_usage >= target_usage) {
break;
}
}
if (buckets_to_evict.empty()) {
return base::unexpected(QuotaError::kNotFound);
}
return buckets_to_evict;
}
QuotaErrorOr<std::set<StorageKey>> QuotaDatabase::GetStorageKeysForType(
StorageType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return base::unexpected(open_error);
}
static constexpr char kSql[] =
"SELECT DISTINCT storage_key FROM buckets WHERE type = ?";
last_operation_ = "GetStorageKeys";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt(0, static_cast<int>(type));
std::set<StorageKey> storage_keys;
while (statement.Step()) {
std::optional<StorageKey> read_storage_key =
StorageKey::Deserialize(statement.ColumnString(0));
if (!read_storage_key.has_value()) {
continue;
}
storage_keys.insert(read_storage_key.value());
}
return storage_keys;
}
QuotaErrorOr<std::set<BucketLocator>> QuotaDatabase::GetBucketsModifiedBetween(
StorageType type,
base::Time begin,
base::Time end) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return base::unexpected(open_error);
}
DCHECK(!begin.is_max());
DCHECK(end != base::Time());
// clang-format off
static constexpr char kSql[] =
"SELECT id, storage_key, name FROM buckets "
"WHERE type = ? AND last_modified >= ? AND last_modified < ?";
// clang-format on
last_operation_ = "GetBucketsModifiedBetween";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt(0, static_cast<int>(type));
statement.BindTime(1, begin);
statement.BindTime(2, end);
std::set<BucketLocator> buckets;
while (statement.Step()) {
std::optional<StorageKey> read_storage_key =
StorageKey::Deserialize(statement.ColumnString(1));
if (!read_storage_key.has_value()) {
continue;
}
buckets.emplace(BucketId(statement.ColumnInt64(0)),
read_storage_key.value(), type,
statement.ColumnString(2) == kDefaultBucketName);
}
return buckets;
}
QuotaErrorOr<std::set<BucketInfo>> QuotaDatabase::GetExpiredBuckets() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return base::unexpected(open_error);
}
// clang-format off
static constexpr char kSql[] =
"SELECT " BUCKET_INFO_FIELDS_SELECTOR
"FROM buckets "
"WHERE expiration > 0 AND expiration < ?";
// clang-format on
last_operation_ = "GetExpired";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindTime(0, GetNow());
return BucketInfosFromSqlStatement(statement);
}
bool QuotaDatabase::IsBootstrapped() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (EnsureOpened() != QuotaError::kNone) {
return false;
}
int flag = 0;
return meta_table_->GetValue(kBucketsTableBootstrapped, &flag) && flag;
}
QuotaError QuotaDatabase::SetIsBootstrapped(bool bootstrap_flag) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return open_error;
}
return meta_table_->SetValue(kBucketsTableBootstrapped, bootstrap_flag)
? QuotaError::kNone
: QuotaError::kDatabaseError;
}
bool QuotaDatabase::RecoverOrRaze(int error_code) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::ignore = sql::Recovery::RecoverIfPossible(
db_.get(), error_code,
sql::Recovery::Strategy::kRecoverWithMetaVersionOrRaze);
db_.reset();
EnsureOpened();
return db_ && db_->is_open();
}
QuotaError QuotaDatabase::CorruptForTesting(
base::OnceCallback<void(const base::FilePath&)> corrupter) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (db_) {
// Commit the long-running transaction.
db_->CommitTransaction();
db_->Close();
}
std::move(corrupter).Run(db_file_path_);
if (!db_) {
return QuotaError::kDatabaseError;
}
if (!OpenDatabase()) {
return QuotaError::kDatabaseError;
}
// Begin a long-running transaction. This matches EnsureOpen().
if (!db_->BeginTransaction()) {
return QuotaError::kDatabaseError;
}
return QuotaError::kNone;
}
void QuotaDatabase::SetDisabledForTesting(bool disable) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
is_disabled_ = disable;
}
// static
base::Time QuotaDatabase::GetNow() {
return g_clock_for_testing ? g_clock_for_testing->Now() : base::Time::Now();
}
// static
void QuotaDatabase::SetClockForTesting(base::Clock* clock) {
g_clock_for_testing = clock;
}
void QuotaDatabase::CommitNow() {
Commit();
}
void QuotaDatabase::Commit() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!db_) {
return;
}
if (timer_.IsRunning()) {
timer_.Stop();
}
last_operation_ = "Commit";
DCHECK_EQ(1, db_->transaction_nesting());
db_->CommitTransaction();
DCHECK_EQ(0, db_->transaction_nesting());
db_->BeginTransaction();
DCHECK_EQ(1, db_->transaction_nesting());
}
void QuotaDatabase::ScheduleCommit() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (timer_.IsRunning()) {
return;
}
timer_.Start(FROM_HERE, base::Milliseconds(kCommitIntervalMs), this,
&QuotaDatabase::Commit);
}
QuotaError QuotaDatabase::EnsureOpened() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (db_) {
return QuotaError::kNone;
}
// If we tried and failed once, don't try again in the same session
// to avoid creating an incoherent mess on disk.
if (is_disabled_) {
return QuotaError::kDatabaseError;
}
sql::DatabaseOptions options{
// The quota database is a critical storage component. If it's corrupted,
// all client-side storage APIs fail, because they don't know where their
// data is stored.
.flush_to_media = true,
.page_size = 4096,
.cache_size = 500,
};
if (base::FeatureList::IsEnabled(features::kDisableQuotaDbFullFSync)) {
options.flush_to_media = false;
}
db_ = std::make_unique<sql::Database>(std::move(options));
meta_table_ = std::make_unique<sql::MetaTable>();
db_->set_histogram_tag("Quota");
db_->set_error_callback(base::BindRepeating(&QuotaDatabase::OnSqliteError,
base::Unretained(this)));
// Migrate an existing database from the old path.
if (!db_file_path_.empty() && !MoveLegacyDatabase()) {
if (ResetStorage()) {
// ResetStorage() has succeeded and database is already open.
return QuotaError::kNone;
}
is_disabled_ = true;
db_.reset();
meta_table_.reset();
return QuotaError::kDatabaseError;
}
if (!OpenDatabase() || !EnsureDatabaseVersion()) {
LOG(ERROR) << "Could not open the quota database, resetting.";
if (!db_file_path_.empty() && ResetStorage()) {
// ResetStorage() has succeeded and database is already open.
return QuotaError::kNone;
}
LOG(ERROR) << "Failed to reset the quota database.";
is_disabled_ = true;
db_.reset();
meta_table_.reset();
return QuotaError::kDatabaseError;
}
// Start a long-running transaction.
DCHECK_EQ(0, db_->transaction_nesting());
db_->BeginTransaction();
return QuotaError::kNone;
}
void QuotaDatabase::OnSqliteError(int sqlite_error_code,
sql::Statement* statement) {
// This check is here to DCHECK the error code in a place that gives a
// useful stack trace.
sql::IsErrorCatastrophic(sqlite_error_code);
sqlite_error_code_ = sqlite_error_code;
// Don't log UMA twice if the same operation manages to cause more than one
// error (this can happen in particular when opening a database).
if (last_operation_) {
sql::UmaHistogramSqliteResult(
std::string("Quota.DatabaseSpecificError.") + *last_operation_,
sqlite_error_code);
last_operation_.reset();
}
if (db_error_callback_) {
db_error_callback_.Run(sqlite_error_code);
}
}
bool QuotaDatabase::MoveLegacyDatabase() {
// Migration was added on 04/2022 (https://crrev.com/c/3513545).
// Cleanup after enough time has passed.
if (base::PathExists(db_file_path_) ||
!base::PathExists(legacy_db_file_path_)) {
return true;
}
if (!base::CreateDirectory(db_file_path_.DirName()) ||
!base::CopyFile(legacy_db_file_path_, db_file_path_)) {
sql::Database::Delete(db_file_path_);
return false;
}
base::FilePath legacy_journal_path =
sql::Database::JournalPath(legacy_db_file_path_);
if (base::PathExists(legacy_journal_path) &&
!base::CopyFile(legacy_journal_path,
sql::Database::JournalPath(db_file_path_))) {
sql::Database::Delete(db_file_path_);
return false;
}
sql::Database::Delete(legacy_db_file_path_);
return true;
}
bool QuotaDatabase::OpenDatabase() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
last_operation_ = "Open";
// Open in memory database.
if (db_file_path_.empty()) {
if (db_->OpenInMemory()) {
return true;
}
RecordDatabaseResetHistogram(DatabaseResetReason::kOpenInMemoryDatabase);
return false;
}
if (!base::CreateDirectory(db_file_path_.DirName())) {
RecordDatabaseResetHistogram(DatabaseResetReason::kCreateDirectory);
return false;
}
if (!db_->Open(db_file_path_)) {
RecordDatabaseResetHistogram(DatabaseResetReason::kOpenDatabase);
return false;
}
db_->Preload();
return true;
}
bool QuotaDatabase::EnsureDatabaseVersion() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!sql::MetaTable::DoesTableExist(db_.get())) {
if (CreateSchema()) {
return true;
}
RecordDatabaseResetHistogram(DatabaseResetReason::kCreateSchema);
return false;
}
if (!meta_table_->Init(db_.get(), kQuotaDatabaseCurrentSchemaVersion,
kQuotaDatabaseCompatibleVersion)) {
RecordDatabaseResetHistogram(DatabaseResetReason::kInitMetaTable);
return false;
}
if (meta_table_->GetCompatibleVersionNumber() >
kQuotaDatabaseCurrentSchemaVersion) {
RecordDatabaseResetHistogram(DatabaseResetReason::kDatabaseVersionTooNew);
LOG(WARNING) << "Quota database is too new.";
return false;
}
if (meta_table_->GetVersionNumber() < kQuotaDatabaseCurrentSchemaVersion) {
if (!QuotaDatabaseMigrations::UpgradeSchema(*this)) {
RecordDatabaseResetHistogram(DatabaseResetReason::kDatabaseMigration);
return false;
}
}
#if DCHECK_IS_ON()
DCHECK(sql::MetaTable::DoesTableExist(db_.get()));
for (const TableSchema& table : kTables) {
DCHECK(db_->DoesTableExist(table.table_name));
}
#endif
return true;
}
bool QuotaDatabase::CreateSchema() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(kinuko): Factor out the common code to create databases.
sql::Transaction transaction(db_.get());
if (!transaction.Begin()) {
return false;
}
if (!meta_table_->Init(db_.get(), kQuotaDatabaseCurrentSchemaVersion,
kQuotaDatabaseCompatibleVersion)) {
return false;
}
for (const TableSchema& table : kTables) {
if (!CreateTable(table)) {
return false;
}
}
for (const IndexSchema& index : kIndexes) {
if (!CreateIndex(index)) {
return false;
}
}
return transaction.Commit();
}
bool QuotaDatabase::CreateTable(const TableSchema& table) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
last_operation_ = "CreateTable";
std::string sql("CREATE TABLE ");
sql += table.table_name;
sql += table.columns;
if (!db_->Execute(sql.c_str())) {
VLOG(1) << "Failed to execute " << sql;
return false;
}
return true;
}
bool QuotaDatabase::CreateIndex(const IndexSchema& index) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::string sql;
if (index.unique) {
sql += "CREATE UNIQUE INDEX ";
} else {
sql += "CREATE INDEX ";
}
sql += index.index_name;
sql += " ON ";
sql += index.table_name;
sql += index.columns;
if (!db_->Execute(sql.c_str())) {
VLOG(1) << "Failed to execute " << sql;
return false;
}
return true;
}
bool QuotaDatabase::ResetStorage() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!db_file_path_.empty());
DCHECK(storage_directory_);
DCHECK(!db_ || !db_->transaction_nesting());
VLOG(1) << "Deleting existing quota data and starting over.";
meta_table_.reset();
db_.reset();
sql::Database::Delete(legacy_db_file_path_);
sql::Database::Delete(db_file_path_);
// Explicit file deletion to try and get consistent deletion across platforms.
base::DeleteFile(legacy_db_file_path_);
base::DeleteFile(db_file_path_);
base::DeleteFile(sql::Database::JournalPath(legacy_db_file_path_));
base::DeleteFile(sql::Database::JournalPath(db_file_path_));
storage_directory_->Doom();
storage_directory_->ClearDoomed();
// So we can't go recursive.
if (is_recreating_) {
return false;
}
base::AutoReset<bool> auto_reset(&is_recreating_, true);
return EnsureOpened() == QuotaError::kNone;
}
QuotaError QuotaDatabase::DumpBucketTable(const BucketTableCallback& callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return open_error;
}
static constexpr char kSql[] =
// clang-format off
"SELECT " BUCKET_TABLE_ENTRY_FIELDS_SELECTOR
"FROM buckets";
// clang-format on
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
while (statement.Step()) {
std::optional<StorageKey> storage_key =
StorageKey::Deserialize(statement.ColumnString(1));
if (!storage_key.has_value()) {
continue;
}
auto entry = BucketTableEntryFromSqlStatement(statement);
if (!callback.Run(std::move(entry))) {
return QuotaError::kNone;
}
}
return statement.Succeeded() ? QuotaError::kNone : QuotaError::kDatabaseError;
}
QuotaErrorOr<BucketInfo> QuotaDatabase::CreateBucketInternal(
const BucketInitParams& params,
StorageType type,
int max_bucket_count) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(crbug.com/40182349): Add DCHECKs for input validation.
QuotaError open_error = EnsureOpened();
if (open_error != QuotaError::kNone) {
return base::unexpected(open_error);
}
// First verify this won't exceed the max bucket count if one is given.
if (max_bucket_count > 0) {
DCHECK_NE(params.name, kDefaultBucketName);
// Note that technically we should be filtering out default buckets when
// counting existing buckets so that the max count only applies to
// non-default buckets. However the precise bucket count is not that
// important and we don't want to perform a lot of string comparisons.
static constexpr char kSql[] =
// clang-format off
"SELECT count(*) "
"FROM buckets "
"WHERE storage_key = ? AND type = ?";
// clang-format on
last_operation_ = "CountBuckets";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindString(0, params.storage_key.Serialize());
statement.BindInt(1, static_cast<int>(type));
if (!statement.Step()) {
return base::unexpected(QuotaError::kDatabaseError);
}
const int64_t current_bucket_count = statement.ColumnInt64(0);
if (current_bucket_count >= max_bucket_count) {
return base::unexpected(QuotaError::kQuotaExceeded);
}
base::UmaHistogramCounts100000("Storage.Buckets.BucketCount",
current_bucket_count + 1);
}
static constexpr char kSql[] =
// clang-format off
"INSERT INTO buckets " BUCKETS_FIELDS_INSERTER
" RETURNING " BUCKET_INFO_FIELDS_SELECTOR;
// clang-format on
last_operation_ = "CreateBucket";
const base::Time now = GetNow();
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
BindBucketInitParamsToInsertStatement(params, type, /*use_count=*/0,
/*last_accessed=*/now,
/*last_modified=*/now, statement);
QuotaErrorOr<BucketInfo> result = BucketInfoFromSqlStatement(statement);
if (result.has_value()) {
CHECK(!statement.Step());
// Commit immediately so that we persist the bucket metadata to disk before
// we inform other services / web apps (via the Buckets API) that we did so.
// Once informed, that promise should persist across power failures.
Commit();
}
return result;
}
void QuotaDatabase::SetDbErrorCallback(
const base::RepeatingCallback<void(int)>& db_error_callback) {
db_error_callback_ = db_error_callback;
}
} // namespace storage