| // Copyright 2025 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "components/persistent_cache/backend_storage.h" | |
| #include <algorithm> | |
| #include <functional> | |
| #include <utility> | |
| #include <vector> | |
| #include "base/check.h" | |
| #include "base/files/file_enumerator.h" | |
| #include "base/files/file_util.h" | |
| #include "base/numerics/clamped_math.h" | |
| #include "base/time/time.h" | |
| #include "build/build_config.h" | |
| #include "components/persistent_cache/backend.h" | |
| #include "components/persistent_cache/backend_type.h" | |
| #include "components/persistent_cache/pending_backend.h" | |
| #include "components/persistent_cache/persistent_cache.h" | |
| #if !BUILDFLAG(IS_FUCHSIA) | |
| #include "components/persistent_cache/sqlite/backend_storage_delegate.h" | |
| #endif | |
| namespace persistent_cache { | |
| namespace { | |
| std::unique_ptr<BackendStorage::Delegate> MakeDelegateOfType( | |
| BackendType backend_type) { | |
| #if BUILDFLAG(IS_FUCHSIA) | |
| return nullptr; | |
| #else | |
| switch (backend_type) { | |
| case BackendType::kSqlite: | |
| return std::make_unique<sqlite::BackendStorageDelegate>(); | |
| } | |
| #endif | |
| } | |
| // Deletes the contents of `directory` without deleting `directory` itself. | |
| void DeleteDirectoryContents(const base::FilePath& directory) { | |
| base::FileEnumerator(directory, /*recursive=*/false, | |
| base::FileEnumerator::NAMES_ONLY) | |
| .ForEach([](const base::FilePath& path) { | |
| base::DeletePathRecursively(path); | |
| }); | |
| } | |
| } // namespace | |
| BackendStorage::BackendStorage(Client client, | |
| BackendType backend_type, | |
| base::FilePath directory) | |
| : BackendStorage(client, | |
| MakeDelegateOfType(backend_type), | |
| std::move(directory)) {} | |
| BackendStorage::BackendStorage(Client client, | |
| std::unique_ptr<Delegate> delegate, | |
| base::FilePath directory) | |
| : client_(client), | |
| delegate_(std::move(delegate)), | |
| directory_(std::move(directory)) { | |
| CHECK(!directory_.empty()); | |
| is_valid_ = delegate_ && base::CreateDirectory(directory_); | |
| } | |
| BackendStorage::~BackendStorage() = default; | |
| base::expected<PendingBackend, TransactionError> | |
| BackendStorage::MakePendingBackend(const base::FilePath& base_name, | |
| bool single_connection, | |
| bool journal_mode_wal) { | |
| return is_valid_ ? delegate_->MakePendingBackend(client_, directory_, | |
| base_name, single_connection, | |
| journal_mode_wal) | |
| : base::unexpected(TransactionError::kPermanent); | |
| } | |
| base::expected<std::unique_ptr<Backend>, TransactionError> | |
| BackendStorage::MakeBackend(const base::FilePath& base_name, | |
| bool single_connection, | |
| bool journal_mode_wal) { | |
| return is_valid_ ? delegate_->MakeBackend(client_, directory_, base_name, | |
| single_connection, journal_mode_wal) | |
| : base::unexpected(TransactionError::kPermanent); | |
| } | |
| std::optional<PendingBackend> BackendStorage::ShareReadOnlyConnection( | |
| const base::FilePath& base_name, | |
| const PersistentCache& cache) { | |
| return is_valid_ ? delegate_->ShareReadOnlyConnection(directory_, base_name, | |
| cache.backend()) | |
| : std::nullopt; | |
| } | |
| std::optional<PendingBackend> BackendStorage::ShareReadWriteConnection( | |
| const base::FilePath& base_name, | |
| const PersistentCache& cache) { | |
| return is_valid_ ? delegate_->ShareReadWriteConnection(directory_, base_name, | |
| cache.backend()) | |
| : std::nullopt; | |
| } | |
| void BackendStorage::DeleteAllFiles() { | |
| if (is_valid_) { | |
| // All files are opened with FLAG_WIN_SHARE_DELETE, so it is possible to | |
| // delete them even if any are still open. The parent directory will not | |
| // be deleted in this case, but that's okay. | |
| // TODO(https://crbug.com/377475540): On Windows, a file cannot be marked | |
| // for deletion while it is mapped into a process's address space. If WAL | |
| // mode is used, we will need to investigate if the wal-index ("-shm") file | |
| // can always be opened with `FLAG_DELETE_ON_CLOSE` so that it is | |
| // unconditionally deleted when the DB is closed. | |
| // https://sqlite.org/walformat.html#shm indicates that this should be safe. | |
| DeleteDirectoryContents(directory_); | |
| } | |
| } | |
| void BackendStorage::DeleteFiles(const base::FilePath& base_name) { | |
| if (is_valid_) { | |
| delegate_->DeleteFiles(client_, directory_, base_name); | |
| } | |
| } | |
| BackendStorage::FootprintReductionResult | |
| BackendStorage::BringDownTotalFootprintOfFiles(int64_t target_footprint) { | |
| if (!is_valid_) { | |
| return {0, 0}; | |
| } | |
| // Measure the size of the directory while collecting the basenames and | |
| // last modified times of each of the backend's files. | |
| int64_t total_footprint = 0; | |
| std::vector<std::pair<base::FilePath, base::Time>> base_names; | |
| base::FileEnumerator(directory_, /*recursive=*/false, | |
| base::FileEnumerator::FILES) | |
| .ForEach([&total_footprint, &base_names, | |
| &delegate = *delegate_](const base::FilePath& file_path) { | |
| base::File::Info info; | |
| // Ignore errors -- info.last_modified will be zero, so this will look | |
| // like the oldest file; first to be deleted. | |
| base::GetFileInfo(file_path, &info); | |
| // Only target files managed by the backend for deletion. | |
| if (auto base_name = delegate.GetBaseName(file_path); | |
| !base_name.empty()) { | |
| base_names.emplace_back(std::move(base_name), info.last_modified); | |
| } | |
| // All files count towards measured footprint. | |
| total_footprint = base::ClampAdd(total_footprint, info.size); | |
| }); | |
| // Nothing to do. | |
| if (total_footprint <= target_footprint) { | |
| return FootprintReductionResult{.current_footprint = total_footprint}; | |
| } | |
| // Sort the collected base names oldest to newest. | |
| std::ranges::sort(base_names, std::ranges::less(), | |
| &std::pair<base::FilePath, base::Time>::second); | |
| // Delete files until reaching the desired target. | |
| const int64_t size_of_necessary_deletes = total_footprint - target_footprint; | |
| int64_t deleted_size = 0; | |
| for (const auto& [base_name, last_modified_time] : base_names) { | |
| deleted_size = base::ClampAdd( | |
| deleted_size, delegate_->DeleteFiles(client_, directory_, base_name)); | |
| if (deleted_size >= size_of_necessary_deletes) { | |
| break; | |
| } | |
| } | |
| return FootprintReductionResult{ | |
| .current_footprint = total_footprint - deleted_size, | |
| .number_of_bytes_deleted = deleted_size}; | |
| } | |
| } // namespace persistent_cache |