chromium/chrome/browser/ash/file_system_provider/content_cache/content_cache_impl.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/file_system_provider/content_cache/content_cache_impl.h"

#include "base/barrier_callback.h"
#include "base/callback_list.h"
#include "base/files/file.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "chrome/browser/ash/file_system_provider/cloud_file_system.h"
#include "chrome/browser/ash/file_system_provider/content_cache/cache_file_context.h"
#include "chrome/browser/ash/file_system_provider/content_cache/content_cache.h"
#include "chrome/browser/ash/file_system_provider/content_cache/content_lru_cache.h"
#include "chrome/browser/ash/file_system_provider/content_cache/context_database.h"
#include "chrome/browser/ash/file_system_provider/content_cache/local_fd.h"
#include "chrome/browser/ash/file_system_provider/provided_file_system_interface.h"
#include "net/base/io_buffer.h"

namespace ash::file_system_provider {

namespace {

std::map<int, int64_t> GetFilesOnDisk(const base::FilePath& cache_directory) {
  std::map<int, int64_t> files_on_disk;
  if (cache_directory.empty()) {
    return files_on_disk;
  }

  base::FileEnumerator enumerator(cache_directory, /*recursive=*/false,
                                  base::FileEnumerator::FILES);
  while (!enumerator.Next().empty()) {
    base::FileEnumerator::FileInfo info = enumerator.GetInfo();
    base::FilePath path = info.GetName();
    if (base::StartsWith(path.BaseName().value(), "context.db")) {
      // The context database has multiple variants starting with context.db,
      // let's exclude those for now.
      continue;
    }
    int64_t id = -1;
    if (!base::StringToInt64(path.BaseName().value(), &id)) {
      LOG(ERROR) << "Couldn't extract ID from path";
      // TODO(b/335548274): Should we remove these files from disk, or ignore
      // them.
      continue;
    }
    files_on_disk.try_emplace(id, info.GetSize());
  }

  return files_on_disk;
}

bool RemoveFilesOnDiskByPath(std::set<base::FilePath> paths_on_disk_to_remove) {
  bool success = true;
  for (const base::FilePath& path : paths_on_disk_to_remove) {
    if (!base::DeleteFile(path)) {
      LOG(ERROR) << "Couldn't remove file on disk";
      success = false;
    }
  }
  return success;
}

}  // namespace

ContentCacheImpl::ContentCacheImpl(const base::FilePath& root_dir,
                                   BoundContextDatabase context_db,
                                   size_t max_cache_items)
    : root_dir_(root_dir),
      io_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
           base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})),
      context_db_(std::move(context_db)),
      max_cache_items_(max_cache_items) {}

ContentCacheImpl::~ContentCacheImpl() {
  context_db_.Reset();
}

std::unique_ptr<ContentCache> ContentCacheImpl::Create(
    const base::FilePath& root_dir,
    BoundContextDatabase context_db,
    size_t max_cache_items) {
  return std::make_unique<ContentCacheImpl>(root_dir, std::move(context_db),
                                            max_cache_items);
}

void ContentCacheImpl::SetMaxCacheItems(size_t max_cache_items) {
  VLOG(1) << "Cache size changing from " << max_cache_items_ << " items to "
          << max_cache_items << " items";
  max_cache_items_ = max_cache_items;
  EvictExcessItems();
}

void ContentCacheImpl::Notify(ProvidedFileSystemObserver::Changes& changes) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  std::vector<base::FilePath> to_evict;
  for (const auto& change : changes) {
    ContentLRUCache::iterator it = lru_cache_.Peek(change.entry_path);
    if (it == lru_cache_.end()) {
      VLOG(1) << "File is not in cache";
      continue;
    }

    // Evict any deleted items or items with mismatched version tags from the
    // cache.
    if (change.change_type == storage::WatcherManager::ChangeType::DELETED) {
      VLOG(2) << "File is deleted, evict from the cache";
      to_evict.push_back(change.entry_path);
      continue;
    }

    if (!change.cloud_file_info) {
      // All cached files should have a version_tag.
      VLOG(2) << "No version_tag, evict from the cache";
      to_evict.push_back(change.entry_path);
      continue;
    }

    CacheFileContext& ctx = it->second;
    if (change.cloud_file_info->version_tag != ctx.version_tag()) {
      VLOG(2) << "File version is out of date, evict from the cache";
      to_evict.push_back(change.entry_path);
    }
  }
  EvictItems(to_evict);
}

void ContentCacheImpl::ObservedVersionTag(const base::FilePath& entry_path,
                                          const std::string& version_tag) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  ContentLRUCache::iterator it = lru_cache_.Peek(entry_path);
  if (it == lru_cache_.end()) {
    VLOG(1) << "File is not in cache";
    return;
  }

  CacheFileContext& ctx = it->second;
  if (version_tag != ctx.version_tag()) {
    VLOG(2) << "File version is out of date, evict from the cache";
    Evict(entry_path);
  }
}

void ContentCacheImpl::Evict(const base::FilePath& file_path) {
  std::vector<base::FilePath> file_paths = {file_path};
  EvictItems(file_paths);
}

void ContentCacheImpl::RemoveItems(
    const std::vector<base::FilePath>& fsp_paths) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  std::vector<int64_t> item_ids;

  // If possible, remove each item from the disk and the database. Do not wait
  // for each removal to complete. In the case that some removals fail, orphaned
  // items will be cleaned up upon start-up.
  for (const base::FilePath& fsp_path : fsp_paths) {
    ContentLRUCache::iterator it = lru_cache_.Peek(fsp_path);
    if (it == lru_cache_.end()) {
      VLOG(1) << "Context for '" << fsp_path << "' is not in the cache";
      continue;
    }

    CacheFileContext& ctx = it->second;
    if (ctx.HasLocalFDs()) {
      VLOG(2) << "Item '" << fsp_path
              << "' cannot be removed whilst there is an open LocalFD";
      continue;
    }

    if (ctx.path_on_disk().empty()) {
      // TODO(b/339114587): Handle this case better. Remove from the lru_cache
      // immediately and erase from the database.
      VLOG(2) << "Item does not yet have a path on disk";
      continue;
    }

    if (ctx.removal_in_progress()) {
      VLOG(2) << "Item '" << fsp_path << "' is already being removed";
      continue;
    }

    VLOG(1) << "Removing '" << fsp_path << "'";
    ctx.set_removal_in_progress(true);
    item_ids.push_back(ctx.id());

    RemoveItemFromDisk(ctx.path_on_disk(), fsp_path);
  }

  RemoveItemsFromDatabase(item_ids);
}

void ContentCacheImpl::RemoveItemsFromDatabase(std::vector<int64_t>& item_ids) {
  if (item_ids.empty()) {
    return;
  }

  const size_t number_of_items = item_ids.size();

  // Remove items from the database by their ID.
  VLOG(1) << "Attempting to remove " << number_of_items
          << " item(s) from the database";
  context_db_.AsyncCall(&ContextDatabase::RemoveItemsByIds)
      .WithArgs(std::move(item_ids))
      .Then(base::BindOnce(&ContentCacheImpl::OnItemsRemovedFromDatabase,
                           weak_ptr_factory_.GetWeakPtr(), number_of_items));
}

void ContentCacheImpl::OnItemsRemovedFromDatabase(size_t number_of_items,
                                                  bool success) {
  if (success) {
    VLOG(1) << "Removed " << number_of_items << " item(s) from the database";
  } else {
    LOG(ERROR) << "Couldn't remove " << number_of_items
               << " items from database";
  }
  // Now all the items on the disk have been removed, if the database call
  // failed the next time the cache is rebuilt (via `LoadFromDisk`) these items
  // will be attempted to be removed again.
}

void ContentCacheImpl::RemoveItemFromDisk(const base::FilePath& path_on_disk,
                                          const base::FilePath& fsp_path) {
  VLOG(1) << "Attempting to remove " << path_on_disk << " from the disk";
  io_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE, base::BindOnce(&base::DeleteFile, path_on_disk),
      base::BindOnce(&ContentCacheImpl::OnItemRemovedFromDisk,
                     weak_ptr_factory_.GetWeakPtr(), base::OwnedRef(fsp_path)));
}

void ContentCacheImpl::OnItemRemovedFromDisk(const base::FilePath& fsp_path,
                                             bool success) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  ContentLRUCache::iterator it = lru_cache_.Peek(fsp_path);
  if (it == lru_cache_.end()) {
    VLOG(1) << "Context for '" << fsp_path << "' is not in the cache";
    return;
  }

  if (!success) {
    LOG(ERROR) << "Failed to remove item with id " << it->second.id()
               << " from disk";
    return;
  }

  VLOG(1) << "Removed item with id " << it->second.id() << " from disk";
  const int64_t bytes_on_disk = it->second.bytes_on_disk();
  lru_cache_.Erase(it);
  DCHECK_GT(evicted_cache_items_, 0u);
  evicted_cache_items_--;

  // Notify all observers.
  for (auto& observer : observers_) {
    observer.OnItemRemovedFromDisk(fsp_path, bytes_on_disk);
  }
}

void ContentCacheImpl::EvictItems(
    const std::vector<base::FilePath>& file_paths) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  for (const base::FilePath& file_path : file_paths) {
    ContentLRUCache::iterator it = lru_cache_.Peek(file_path);
    if (it == lru_cache_.end()) {
      VLOG(2) << "Context for '" << file_path << "' is not in the cache";
      continue;
    }

    CacheFileContext& ctx = it->second;
    if (ctx.evicted()) {
      VLOG(2) << "Item '" << file_path << "' is already evicted";
      continue;
    }

    VLOG(1) << "Evicting '" << file_path << "'";
    ctx.set_evicted(true);
    evicted_cache_items_++;
    // Notify all observers.
    for (auto& observer : observers_) {
      observer.OnItemEvicted(file_path);
    }
  }
  RemoveItems(file_paths);
}

void ContentCacheImpl::EvictExcessItems() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // The cache size should not include the items that are expected to be evicted
  // as these will get removed on the next removal cycle.
  size_t cache_items_without_evicted_items =
      lru_cache_.size() - evicted_cache_items_;
  if (cache_items_without_evicted_items <= max_cache_items_) {
    VLOG(2) << "No items to evict: {cache_items_without_evicted_items = "
            << cache_items_without_evicted_items
            << ", max_cache_items = " << max_cache_items_ << "}";
    return;
  }

  size_t items_to_evict = cache_items_without_evicted_items - max_cache_items_;
  VLOG(2) << items_to_evict << " items to be evicted, not including "
          << evicted_cache_items_ << " already evicted";

  // Evict items starting from the least-recently-used until the total number of
  // evicted items brings the size of the cache (without these items) to below
  // the `max_cache_items_`.
  ContentLRUCache::reverse_iterator it = lru_cache_.rbegin();
  std::vector<base::FilePath> to_evict;
  while (to_evict.size() < items_to_evict) {
    CacheFileContext& ctx = it->second;
    if (!ctx.evicted()) {
      to_evict.push_back(it->first);
    }
    it++;
  }
  EvictItems(to_evict);
}

void ContentCacheImpl::ReadBytes(
    const OpenedCloudFile& file,
    scoped_refptr<net::IOBuffer> buffer,
    int64_t offset,
    int length,
    ProvidedFileSystemInterface::ReadChunkReceivedCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  VLOG(1) << "ReadBytes {path = '" << file.file_path << "', version_tag = '"
          << file.version_tag << "', offset = '" << offset << "', length = '"
          << length << "'}";

  ContentLRUCache::iterator it = lru_cache_.Peek(file.file_path);
  if (it == lru_cache_.end()) {
    VLOG(1) << "Cache miss: entire file is not in cache";
    callback.Run(/*bytes_read=*/-1, /*has_more=*/false,
                 base::File::FILE_ERROR_NOT_FOUND);
    return;
  }

  CacheFileContext& ctx = it->second;

  if (offset == ctx.bytes_on_disk() && offset == file.bytes_in_cloud) {
    VLOG(1) << "Ignored request: offset is at EOF";
    callback.Run(/*bytes_read=*/0, /*has_more=*/false, base::File::FILE_OK);
    return;
  }

  // In the event the offset exceeds the known `bytes_on_disk` then we can't
  // reliably serve this data from the content cache.
  if (offset >= ctx.bytes_on_disk()) {
    VLOG(1) << "Cache miss: requested byte range {offset = '" << offset
            << "', length = '" << length
            << "'} not available {bytes_on_disk = '" << ctx.bytes_on_disk()
            << "'}";
    callback.Run(/*bytes_read=*/-1, /*has_more=*/false,
                 base::File::FILE_ERROR_NOT_FOUND);
    return;
  }

  if (!ctx.CanGetLocalFD(file)) {
    VLOG(1) << "Cache miss: not possible to read the file on disk";
    callback.Run(/*bytes_read=*/-1, /*has_more=*/false,
                 base::File::FILE_ERROR_NOT_FOUND);
    return;
  }

  // It's possible that the file on disk can't entirely fulfill the offset +
  // length bytes request. In this instance, the callback will be invoked with
  // `bytes_read` (which will be less than length) and it's up to the caller to
  // make a follow up call for the remainder (which will then be served from the
  // underlying FSP).
  VLOG(1) << "Cache hit: Range {offset = '" << offset << "', length = '"
          << length << "', bytes_on_disk = '" << ctx.bytes_on_disk()
          << "'} is available";

  LocalFD& local_fd = ctx.GetLocalFD(file, io_task_runner_);
  local_fd.ReadBytes(
      buffer, offset, length,
      base::BindOnce(&ContentCacheImpl::OnBytesRead,
                     weak_ptr_factory_.GetWeakPtr(), file.file_path, callback));
}

void ContentCacheImpl::OnBytesRead(
    const base::FilePath& file_path,
    ProvidedFileSystemInterface::ReadChunkReceivedCallback callback,
    FileErrorOrBytesRead error_or_bytes_read) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  base::File::Error result = error_or_bytes_read.error_or(base::File::FILE_OK);
  if (result != base::File::FILE_OK) {
    VLOG(2) << "OnBytesRead result: " << base::File::ErrorToString(result);
    callback.Run(/*bytes_read=*/0, /*has_more=*/false, result);
    return;
  }

  ContentLRUCache::iterator it = lru_cache_.Get(file_path);
  DCHECK(it != lru_cache_.end());

  // Update the accessed time to now, but don't wait for the database to return,
  // just fire and forget.
  CacheFileContext& ctx = it->second;
  ctx.set_accessed_time(base::Time::Now());
  context_db_.AsyncCall(&ContextDatabase::UpdateAccessedTime)
      .WithArgs(ctx.id(), ctx.accessed_time())
      .Then(base::BindOnce([](bool success) {
        LOG_IF(ERROR, !success) << "Couldn't update access time on read";
      }));

  int bytes_read = error_or_bytes_read.value();
  VLOG(2) << "OnBytesRead {bytes_read = '" << bytes_read << "'}";
  callback.Run(bytes_read, /*has_more=*/false, base::File::FILE_OK);
}

void ContentCacheImpl::WriteBytes(const OpenedCloudFile& file,
                                  scoped_refptr<net::IOBuffer> buffer,
                                  int64_t offset,
                                  int length,
                                  FileErrorCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  ContentLRUCache::iterator it = lru_cache_.Peek(file.file_path);
  if (it != lru_cache_.end()) {
    WriteBytesToDisk(file, buffer, offset, length, std::move(callback));
    return;
  }

  // The file doesn't exist in the cache yet.
  if (file.version_tag.empty()) {
    VLOG(1) << "Empty version tag can't be written to cache";
    std::move(callback).Run(base::File::FILE_ERROR_INVALID_OPERATION);
    return;
  }

  // Add a new CacheFileContext to the lru_cache.
  VLOG(1) << "Adding '" << file.file_path << "' to the cache";
  it = lru_cache_.Put(
      PathContextPair(file.file_path, CacheFileContext(file.version_tag)));
  EvictExcessItems();

  // Add a new entry to the database then retrieve the ID and use it to create
  // a file on disk before writing the bytes to disk.
  std::unique_ptr<int64_t> inserted_id = std::make_unique<int64_t>(-1);
  context_db_.AsyncCall(&ContextDatabase::AddItem)
      .WithArgs(file.file_path, file.version_tag, it->second.accessed_time(),
                inserted_id.get())
      .Then(base::BindOnce(&ContentCacheImpl::OnFileIdGenerated,
                           weak_ptr_factory_.GetWeakPtr(), file, buffer, offset,
                           length, std::move(callback),
                           std::move(inserted_id)));
  }

  void ContentCacheImpl::WriteBytesToDisk(const OpenedCloudFile& file,
                                          scoped_refptr<net::IOBuffer> buffer,
                                          int64_t offset,
                                          int length,
                                          FileErrorCallback callback) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    ContentLRUCache::iterator it = lru_cache_.Peek(file.file_path);
    if (it == lru_cache_.end()) {
      VLOG(2) << "File removed between WriteBytes and WriteBytesToDisk calls";
      std::move(callback).Run(base::File::FILE_ERROR_INVALID_OPERATION);
      return;
    }
    CacheFileContext& ctx = it->second;

    if (ctx.bytes_on_disk() != offset) {
      VLOG(1) << "Unsupported write offset supplied {bytes_on_disk = '"
              << ctx.bytes_on_disk() << "', offset = '" << offset << "'}";
      std::move(callback).Run(base::File::FILE_ERROR_INVALID_OPERATION);
      return;
    }

    if (!ctx.CanGetLocalFD(file)) {
      VLOG(1) << "Not possible to write to the file on disk";
      std::move(callback).Run(base::File::FILE_ERROR_NOT_FOUND);
      return;
    }

    if (ctx.has_writer()) {
      VLOG(1) << "Writer is in progress already, multi offset writers not "
                 "supported";
      std::move(callback).Run(base::File::FILE_ERROR_IN_USE);
      return;
    }
    ctx.set_has_writer(true);

    auto on_bytes_written_callback = base::BindOnce(
        &ContentCacheImpl::OnBytesWritten, weak_ptr_factory_.GetWeakPtr(),
        file.file_path, offset, length, std::move(callback));

    LocalFD& local_fd = ctx.GetLocalFD(file, io_task_runner_);
    local_fd.WriteBytes(buffer, offset, length,
                        std::move(on_bytes_written_callback));
  }

void ContentCacheImpl::CloseFile(const OpenedCloudFile& file) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  VLOG(1) << "Closing " << file.file_path;
  if (auto it = lru_cache_.Peek(file.file_path); it != lru_cache_.end()) {
    CacheFileContext& ctx = it->second;
    ctx.CloseLocalFD(file.request_id);
    if (ctx.evicted()) {
      // File was evicted when reading. Remove it.
      std::vector<base::FilePath> file_paths = {file.file_path};
      RemoveItems(file_paths);
    }
  }
}

void ContentCacheImpl::OnFileIdGenerated(const OpenedCloudFile& file,
                                         scoped_refptr<net::IOBuffer> buffer,
                                         int64_t offset,
                                         int length,
                                         FileErrorCallback callback,
                                         std::unique_ptr<int64_t> inserted_id,
                                         bool item_add_success) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!item_add_success) {
    LOG(ERROR) << "Failed to add item to the database";
    std::move(callback).Run(base::File::FILE_ERROR_FAILED);
    return;
  }

  ContentLRUCache::iterator it = lru_cache_.Peek(file.file_path);
  // TODO(b/339114587): Handle the case where the context gets removed during
  // the file ID generation.
  DCHECK(it != lru_cache_.end());
  DCHECK(inserted_id);
  DCHECK_GT(*inserted_id, 0);
  CacheFileContext& ctx = it->second;
  ctx.set_id(*inserted_id);
  ctx.set_path_on_disk(GetPathOnDiskFromId((*inserted_id)));

  WriteBytesToDisk(file, buffer, offset, length, std::move(callback));
}

void ContentCacheImpl::OnBytesWritten(const base::FilePath& file_path,
                                      int64_t offset,
                                      int length,
                                      FileErrorCallback callback,
                                      base::File::Error result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  ContentLRUCache::iterator it = lru_cache_.Get(file_path);
  DCHECK(it != lru_cache_.end());

  CacheFileContext& ctx = it->second;
  if (result == base::File::FILE_OK) {
    ctx.set_bytes_on_disk(offset + length);
    ctx.set_accessed_time(base::Time::Now());

    // Keep the accessed time up to date.
    context_db_.AsyncCall(&ContextDatabase::UpdateAccessedTime)
        .WithArgs(ctx.id(), ctx.accessed_time())
        .Then(base::BindOnce([](bool success) {
          LOG_IF(ERROR, !success) << "Couldn't update access time on write";
        }));
  }
  ctx.set_has_writer(false);

  VLOG(2) << "OnBytesWritten: {offset = '" << offset << "', length = '"
          << length << "', result = '" << base::File::ErrorToString(result)
          << "'}";
  std::move(callback).Run(result);
}

const base::FilePath ContentCacheImpl::GetPathOnDiskFromId(int64_t id) {
  return root_dir_.Append(base::NumberToString(id));
}

void ContentCacheImpl::LoadFromDisk(base::OnceClosure callback) {
  // Identify all the files from the `root_dir_`.
  io_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE, base::BindOnce(&GetFilesOnDisk, root_dir_),
      base::BindOnce(&ContentCacheImpl::GotFilesFromDisk,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void ContentCacheImpl::GotFilesFromDisk(base::OnceClosure callback,
                                        std::map<int, int64_t> files_on_disk) {
  // Get all the items from the database.
  context_db_.AsyncCall(&ContextDatabase::GetAllItems)
      .Then(base::BindOnce(&ContentCacheImpl::GotItemsFromContextDatabase,
                           weak_ptr_factory_.GetWeakPtr(), std::move(callback),
                           std::move(files_on_disk)));
}

void ContentCacheImpl::GotItemsFromContextDatabase(
    base::OnceClosure callback,
    std::map<int, int64_t> files_on_disk,
    ContextDatabase::IdToItemMap items_in_db) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  std::set<base::FilePath> paths_on_disk_to_remove;
  std::list<PathContextPair> cached_files;
  for (auto& [id, bytes_on_disk] : files_on_disk) {
    ContextDatabase::IdToItemMap::const_iterator item_it = items_in_db.find(id);
    if (item_it == items_in_db.end()) {
      // Remove files on disk that have no entry in the database.
      paths_on_disk_to_remove.emplace(
          root_dir_.Append(base::NumberToString(id)));
    } else {
      // Create contexts for each non-orphaned file on the disk using the
      // database entry.
      CacheFileContext ctx(item_it->second.version_tag, bytes_on_disk, id,
                           GetPathOnDiskFromId(id));
      ctx.set_accessed_time(item_it->second.accessed_time);
      cached_files.emplace_back(item_it->second.fsp_path, std::move(ctx));
    }
  }

  std::vector<int64_t> ids_in_db_to_remove;
  for (const auto& [id, item] : items_in_db) {
    if (!files_on_disk.contains(id)) {
      // Remove SQL entries that have no file on disk.
      ids_in_db_to_remove.emplace_back(id);
    }
  }

  cached_files.sort([](const PathContextPair& lhs, const PathContextPair& rhs) {
    // The files should be in least-recently used order, the underlying data
    // structure maintains them in order on access but not on initialization so
    // we have to ensure order now. This means the most-recently used is the
    // 0-th item, where eviction will take place on the last element.
    return lhs.second.accessed_time() > rhs.second.accessed_time();
  });

  VLOG(1) << "Initializing content cache with " << cached_files.size()
          << " items";
  lru_cache_.Init(std::move(cached_files));

  const auto barrier_callback = base::BarrierCallback<bool>(
      2, base::BindOnce(&ContentCacheImpl::OnStaleItemsPruned,
                        weak_ptr_factory_.GetWeakPtr(), std::move(callback)));

  VLOG_IF(1, !ids_in_db_to_remove.empty())
      << "Attempting to remove " << ids_in_db_to_remove.size()
      << " item(s) from the database";
  context_db_.AsyncCall(&ContextDatabase::RemoveItemsByIds)
      .WithArgs(ids_in_db_to_remove)
      .Then(barrier_callback);

  VLOG_IF(1, !paths_on_disk_to_remove.empty())
      << "Attempting to remove " << paths_on_disk_to_remove.size()
      << " path(s) from the disk";
  io_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&RemoveFilesOnDiskByPath, paths_on_disk_to_remove),
      barrier_callback);
}

void ContentCacheImpl::OnStaleItemsPruned(base::OnceClosure callback,
                                          std::vector<bool> prune_success) {
  DCHECK_EQ(prune_success.size(), 2u);
  bool db_success = prune_success.at(0);
  bool fs_success = prune_success.at(1);

  LOG_IF(ERROR, !db_success) << "Couldn't remove all stale items from db";
  LOG_IF(ERROR, !fs_success) << "Couldn't remove all stale items from disk";

  // Failing to remove files from db/disk doesn't stop the cache being
  // successfully setup.
  std::move(callback).Run();
}

std::vector<base::FilePath> ContentCacheImpl::GetCachedFilePaths() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  std::vector<base::FilePath> cached_file_paths;
  for (const auto& [file_path, cache_file_context] : lru_cache_) {
    if (!cache_file_context.evicted()) {
      cached_file_paths.push_back(file_path);
    }
  }
  return cached_file_paths;
}

void ContentCacheImpl::AddObserver(ContentCache::Observer* observer) {
  DCHECK(observer);
  observers_.AddObserver(observer);
}

void ContentCacheImpl::RemoveObserver(ContentCache::Observer* observer) {
  DCHECK(observer);
  observers_.RemoveObserver(observer);
}

}  // namespace ash::file_system_provider