| // 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 |