chromium/chrome/browser/ash/file_system_provider/cloud_file_system.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/cloud_file_system.h"

#include <memory>
#include <utility>

#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/notreached.h"
#include "base/rand_util.h"
#include "base/strings/string_util.h"
#include "base/timer/timer.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/file_system_provider/cloud_file_info.h"
#include "chrome/browser/ash/file_system_provider/provided_file_system_interface.h"
#include "chrome/browser/ash/file_system_provider/queue.h"
#include "net/base/io_buffer.h"
#include "url/origin.h"

namespace ash::file_system_provider {

namespace {

// The frequency that the FSP syncs with the cloud when the File Manager is a
// watcher.
constexpr base::TimeDelta kFileManagerWatcherInterval = base::Seconds(15);

// TODO(b/317137739): Remove this once a proper API call is introduced.
// Temp custom action to request ODFS sync with the cloud.
constexpr char kODFSSyncWithCloudAction[] = "HIDDEN_SYNC_WITH_CLOUD";

const GURL GetContentCacheURL() {
  return GURL("chrome://content-cache/");
}

std::ostream& operator<<(std::ostream& out,
                         const std::vector<base::FilePath>& entry_paths) {
  for (size_t i = 0; i < entry_paths.size(); ++i) {
    out << entry_paths[i];
    if (i < entry_paths.size() - 1) {
      out << ", ";
    }
  }
  return out;
}

std::ostream& operator<<(std::ostream& out, OpenFileMode mode) {
  switch (mode) {
    case OpenFileMode::OPEN_FILE_MODE_READ:
      return out << "OPEN_FILE_MODE_READ";
    case OpenFileMode::OPEN_FILE_MODE_WRITE:
      return out << "OPEN_FILE_MODE_WRITE";
  }
  NOTREACHED() << "Unknown OpenFileMode: " << mode;
}

std::ostream& operator<<(std::ostream& out,
                         storage::WatcherManager::ChangeType type) {
  using ChangeType = storage::WatcherManager::ChangeType;
  switch (type) {
    case ChangeType::CHANGED:
      return out << "CHANGED";
    case ChangeType::DELETED:
      return out << "DELETED";
  }
  NOTREACHED() << "Unknown ChangeType: " << type;
}

std::ostream& operator<<(std::ostream& out, CloudFileInfo* cloud_file_info) {
  if (!cloud_file_info) {
    return out << "none";
  }
  return out << "{version_tag = '" << cloud_file_info->version_tag << "'}";
}

std::ostream& operator<<(std::ostream& out, EntryMetadata* metadata) {
  if (!metadata) {
    return out << "none";
  }
  out << "{ cloud_file_info = " << metadata->cloud_file_info.get();
  if (metadata->size) {
    out << ", size = '" << *metadata->size << "'";
  }
  return out << "}";
}

std::ostream& operator<<(std::ostream& out,
                         ProvidedFileSystemObserver::Changes* changes) {
  if (!changes) {
    return out << "none";
  }
  for (size_t i = 0; i < changes->size(); ++i) {
    const auto& [entry_path, change_type, cloud_file_info] = (*changes)[i];
    out << entry_path << ": change_type = " << change_type
        << ", cloud_file_info = " << cloud_file_info.get();
    if (i < changes->size() - 1) {
      out << ", ";
    }
  }
  return out;
}

const std::string GetVersionTag(EntryMetadata* metadata) {
  return (metadata && metadata->cloud_file_info)
             ? metadata->cloud_file_info->version_tag
             : "";
}

std::optional<int64_t> GetCloudSize(EntryMetadata* metadata) {
  // If the size doesn't exist, it may return -1, let's avoid this error case.
  if (metadata && metadata->size && *metadata->size > -1) {
    return *metadata->size;
  }
  return std::nullopt;
}

}  // namespace

CloudFileSystem::CloudFileSystem(
    std::unique_ptr<ProvidedFileSystemInterface> file_system)
    : CloudFileSystem(std::move(file_system), nullptr) {}

CloudFileSystem::CloudFileSystem(
    std::unique_ptr<ProvidedFileSystemInterface> file_system,
    CacheManager* cache_manager)
    : file_system_(std::move(file_system)) {
  if (!cache_manager) {
    return;
  }

  cache_manager->InitializeForProvider(
      file_system_->GetFileSystemInfo(),
      base::BindOnce(&CloudFileSystem::OnContentCacheInitialized,
                     weak_ptr_factory_.GetWeakPtr()));
}

CloudFileSystem::~CloudFileSystem() = default;

void CloudFileSystem::OnContentCacheInitialized(
    base::FileErrorOr<std::unique_ptr<ContentCache>> error_or_cache) {
  LOG_IF(ERROR, !error_or_cache.has_value())
      << "Error initializing the content cache: " << error_or_cache.error();
  if (error_or_cache.has_value()) {
    content_cache_ = std::move(error_or_cache.value());
    content_cache_->AddObserver(this);
    for (const base::FilePath& file_path :
         content_cache_->GetCachedFilePaths()) {
      AddWatcherOnCachedFile(file_path);
    }
  }
}

void CloudFileSystem::AddWatcherOnCachedFile(const base::FilePath& file_path) {
  AddWatcherOnCachedFileImpl(file_path, /*attempts=*/0,
                             /*result=*/base::File::FILE_ERROR_SECURITY);
}

void CloudFileSystem::AddWatcherOnCachedFileImpl(
    const base::FilePath& file_path,
    int attempts,
    base::File::Error result) {
  if (result == base::File::FILE_OK) {
    VLOG(1) << "Re-added file watcher on file '" << file_path << "'";
    return;
  }
  if (result != base::File::FILE_ERROR_SECURITY || attempts > 6) {
    LOG(ERROR) << "Failed to add file watcher on file with result: " << result
               << " after " << attempts << " attempts";
    VLOG(2) << "Failed to add file watcher on file '" << file_path
            << "' with result: " << result << " after " << attempts
            << " attempts";
    return;
  }
  // Set a random delay in the interval attempts*[0,2] seconds to stagger
  // AddWatcher requests.
  base::TimeDelta delay = attempts * base::Milliseconds(base::RandInt(1, 2000));
  // Notifications are received though Notify() so no notification_callback
  // is needed. Call this function recursively to continuously retry upon
  // FILE_ERROR_SECURITY errors until the max number of attempts have been made.
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(
          IgnoreResult(&CloudFileSystem::AddWatcher),
          weak_ptr_factory_.GetWeakPtr(), GetContentCacheURL(), file_path,
          /*recursive=*/false, /*persistent=*/false,
          base::BindOnce(&CloudFileSystem::AddWatcherOnCachedFileImpl,
                         weak_ptr_factory_.GetWeakPtr(), file_path,
                         attempts + 1),
          base::DoNothing()),
      delay);
}

AbortCallback CloudFileSystem::RequestUnmount(
    storage::AsyncFileUtil::StatusCallback callback) {
  VLOG(2) << "RequestUnmount {fsid = " << GetFileSystemId() << "}";
  return file_system_->RequestUnmount(std::move(callback));
}

AbortCallback CloudFileSystem::GetMetadata(const base::FilePath& entry_path,
                                            MetadataFieldMask fields,
                                            GetMetadataCallback callback) {
  VLOG(2) << "GetMetadata {fsid = '" << GetFileSystemId() << "', entry_path = '"
          << entry_path << "', fields = '" << fields << "'}";
  fields |= METADATA_FIELD_CLOUD_FILE_INFO;
  return file_system_->GetMetadata(
      entry_path, fields,
      base::BindOnce(&CloudFileSystem::OnGetMetadataCompleted,
                     weak_ptr_factory_.GetWeakPtr(), entry_path,
                     std::move(callback)));
}

AbortCallback CloudFileSystem::GetActions(
    const std::vector<base::FilePath>& entry_paths,
    GetActionsCallback callback) {
  VLOG(2) << "GetActions {fsid = '" << GetFileSystemId() << "', entry_paths = '"
          << entry_paths << "'}";
  return file_system_->GetActions(entry_paths, std::move(callback));
}

AbortCallback CloudFileSystem::ExecuteAction(
    const std::vector<base::FilePath>& entry_paths,
    const std::string& action_id,
    storage::AsyncFileUtil::StatusCallback callback) {
  VLOG(2) << "ExecuteAction {fsid = '" << GetFileSystemId()
          << "', entry_paths = '" << entry_paths << "', action_id = '"
          << action_id << "'}";
  return file_system_->ExecuteAction(entry_paths, action_id,
                                     std::move(callback));
}

AbortCallback CloudFileSystem::ReadDirectory(
    const base::FilePath& directory_path,
    storage::AsyncFileUtil::ReadDirectoryCallback callback) {
  VLOG(1) << "ReadDirectory {fsid = '" << GetFileSystemId()
          << "', directory_path = '" << directory_path << "'}";
  return file_system_->ReadDirectory(directory_path, callback);
}

bool CloudFileSystem::ShouldAttemptToServeReadFileFromCache(
    const OpenedCloudFileMap::const_iterator it) {
  return content_cache_ && it != opened_files_.end() &&
         it->second.mode == OpenFileMode::OPEN_FILE_MODE_READ &&
         !it->second.version_tag.empty() &&
         it->second.bytes_in_cloud.has_value();
}

AbortCallback CloudFileSystem::ReadFile(int file_handle,
                                        net::IOBuffer* buffer,
                                        int64_t offset,
                                        int length,
                                        ReadChunkReceivedCallback callback) {
  VLOG(1) << "ReadFile {fsid = '" << GetFileSystemId() << "', file_handle = '"
          << file_handle << "', offset = '" << offset << "', length = '"
          << length << "'}";

  // In the event the file isn't found in the `opened_files_` map, the content
  // cache hasn't or won't be initialized OR there is an empty `version_tag`,
  // then pass the request directly to the FSP.
  const OpenedCloudFileMap::const_iterator it = opened_files_.find(file_handle);
  if (!ShouldAttemptToServeReadFileFromCache(it)) {
    return file_system_->ReadFile(file_handle, buffer, offset, length,
                                  callback);
  }

  const OpenedCloudFile& opened_cloud_file = it->second;
  scoped_refptr<net::IOBuffer> buffer_ref = base::WrapRefCounted(buffer);
  content_cache_->ReadBytes(
      opened_cloud_file, buffer_ref, offset, length,
      base::BindRepeating(&CloudFileSystem::OnReadFileFromCacheCompleted,
                          weak_ptr_factory_.GetWeakPtr(), file_handle,
                          buffer_ref, offset, length, callback));
  return AbortCallback();
}

void CloudFileSystem::OnReadFileFromCacheCompleted(
    int file_handle,
    scoped_refptr<net::IOBuffer> buffer,
    int64_t offset,
    int length,
    ReadChunkReceivedCallback callback,
    int bytes_read,
    bool has_more,
    base::File::Error result) {
  VLOG(2) << "OnReadFileFromCacheCompleted {fsid = " << GetFileSystemId()
          << ", file_handle = '" << file_handle << "', result = '" << result
          << "}";
  if (result == base::File::FILE_OK) {
    // If the cached read file was successful, ensure that is passed to the
    // caller.
    callback.Run(bytes_read, has_more, result);
    return;
  }

  if (result == base::File::FILE_ERROR_NOT_FOUND) {
    // The file doesn't exist in the cache or is not available, we need to make
    // a cloud request first and attempt to write the result into the cache upon
    // successful return.
    file_system_->ReadFile(
        file_handle, buffer.get(), offset, length,
        base::BindRepeating(&CloudFileSystem::OnReadFileCompleted,
                            weak_ptr_factory_.GetWeakPtr(), file_handle, buffer,
                            offset, length, callback));
    return;
  }

  LOG(ERROR) << "Couldn't read the file from cache";
  file_system_->ReadFile(file_handle, buffer.get(), offset, length,
                         std::move(callback));
}

void CloudFileSystem::OnReadFileCompleted(int file_handle,
                                          scoped_refptr<net::IOBuffer> buffer,
                                          int64_t offset,
                                          int length,
                                          ReadChunkReceivedCallback callback,
                                          int bytes_read,
                                          bool has_more,
                                          base::File::Error result) {
  VLOG(2) << "OnReadFileCompleted {fsid = " << GetFileSystemId()
          << ", file_handle = '" << file_handle << "', result = '" << result
          << ", bytes_read = " << bytes_read << "}";

  const OpenedCloudFileMap::const_iterator it = opened_files_.find(file_handle);
  if (it == opened_files_.end() || result != base::File::FILE_OK ||
      !content_cache_) {
    callback.Run(bytes_read, has_more, result);
    return;
  }

  // The `ReadChunkReceivedCallback` should always respond with the result from
  // the FSP. If the content cache write fails, we should always be serving this
  // from the FSP.
  auto readchunk_success_callback = base::BindRepeating(
      std::move(callback), bytes_read, has_more, base::File::FILE_OK);

  const OpenedCloudFile& opened_cloud_file = it->second;
  content_cache_->WriteBytes(
      opened_cloud_file, buffer, offset, bytes_read,
      base::BindOnce(&CloudFileSystem::OnBytesWrittenToCache,
                     weak_ptr_factory_.GetWeakPtr(),
                     opened_cloud_file.file_path, readchunk_success_callback));
}

void CloudFileSystem::OnBytesWrittenToCache(
    const base::FilePath& file_path,
    base::RepeatingCallback<void()> readchunk_success_callback,
    base::File::Error result) {
  if (result == base::File::FILE_OK) {
    Watchers::const_iterator watcher =
        GetWatchers()->find(WatcherKey(file_path, /*recursive=*/false));
    if (watcher == GetWatchers()->end() ||
        !watcher->second.subscribers.contains(GetContentCacheURL())) {
      // This file is newly added to the cache so watch it to track any changes.
      // Notifications are received though Notify() so no notification_callback
      // is needed.
      AddWatcher(
          GetContentCacheURL(), file_path,
          /*recursive=*/false, /*persistent=*/false,
          base::BindOnce(
              [](const base::FilePath& file_path, base::File::Error result) {
                VLOG(1) << "Added file watcher on '" << file_path
                        << "' result: " << result;
              },
              file_path),
          base::DoNothing());
    }
  }
  readchunk_success_callback.Run();
}

AbortCallback CloudFileSystem::OpenFile(const base::FilePath& file_path,
                                         OpenFileMode mode,
                                         OpenFileCallback callback) {
  VLOG(1) << "OpenFile {fsid = '" << GetFileSystemId() << "', file_path = '"
          << file_path << "', mode = '" << mode << "'}";

  return file_system_->OpenFile(
      file_path, mode,
      base::BindOnce(&CloudFileSystem::OnOpenFileCompleted,
                     weak_ptr_factory_.GetWeakPtr(), file_path, mode,
                     std::move(callback)));
}

AbortCallback CloudFileSystem::CloseFile(
    int file_handle,
    storage::AsyncFileUtil::StatusCallback callback) {
  VLOG(1) << "CloseFile {fsid = '" << GetFileSystemId() << "', file_handle = '"
          << file_handle << "'}";
  return file_system_->CloseFile(
      file_handle, base::BindOnce(&CloudFileSystem::OnCloseFileCompleted,
                                  weak_ptr_factory_.GetWeakPtr(), file_handle,
                                  std::move(callback)));
}

AbortCallback CloudFileSystem::CreateDirectory(
    const base::FilePath& directory_path,
    bool recursive,
    storage::AsyncFileUtil::StatusCallback callback) {
  VLOG(1) << "CreateDirectory {fsid = '" << GetFileSystemId()
          << "', directory_path = '" << directory_path << "', recursive = '"
          << recursive << "'}";
  return file_system_->CreateDirectory(directory_path, recursive,
                                       std::move(callback));
}

AbortCallback CloudFileSystem::DeleteEntry(
    const base::FilePath& entry_path,
    bool recursive,
    storage::AsyncFileUtil::StatusCallback callback) {
  VLOG(1) << "DeleteEntry {fsid = '" << GetFileSystemId() << "', entry_path = '"
          << entry_path << "', recursive = '" << recursive << "'}";
  return file_system_->DeleteEntry(
      entry_path, recursive,
      base::BindOnce(&CloudFileSystem::OnDeleteEntryCompleted,
                     weak_ptr_factory_.GetWeakPtr(), entry_path,
                     std::move(callback)));
}

AbortCallback CloudFileSystem::CreateFile(
    const base::FilePath& file_path,
    storage::AsyncFileUtil::StatusCallback callback) {
  VLOG(1) << "CreateFile {fsid = '" << GetFileSystemId() << "', file_path = '"
          << file_path << "'}";
  return file_system_->CreateFile(file_path, std::move(callback));
}

AbortCallback CloudFileSystem::CopyEntry(
    const base::FilePath& source_path,
    const base::FilePath& target_path,
    storage::AsyncFileUtil::StatusCallback callback) {
  VLOG(1) << "CopyEntry {fsid = '" << GetFileSystemId() << "', source_path = '"
          << source_path << "', target_path = '" << target_path << "'}";
  return file_system_->CopyEntry(source_path, target_path, std::move(callback));
}

AbortCallback CloudFileSystem::WriteFile(
    int file_handle,
    net::IOBuffer* buffer,
    int64_t offset,
    int length,
    storage::AsyncFileUtil::StatusCallback callback) {
  VLOG(1) << "WriteFile {fsid = '" << GetFileSystemId() << "', file_handle = '"
          << file_handle << "', offset = '" << offset << "', length = '"
          << length << "'}";
  return file_system_->WriteFile(
      file_handle, buffer, offset, length,
      base::BindOnce(&CloudFileSystem::OnWriteFileCompleted,
                     weak_ptr_factory_.GetWeakPtr(), file_handle,
                     std::move(callback)));
}

AbortCallback CloudFileSystem::FlushFile(
    int file_handle,
    storage::AsyncFileUtil::StatusCallback callback) {
  VLOG(1) << "FlushFile {fsid = '" << GetFileSystemId() << "', file_handle = '"
          << file_handle << "'}";
  return file_system_->FlushFile(file_handle, std::move(callback));
}

AbortCallback CloudFileSystem::MoveEntry(
    const base::FilePath& source_path,
    const base::FilePath& target_path,
    storage::AsyncFileUtil::StatusCallback callback) {
  VLOG(1) << "MoveEntry {fsid = '" << GetFileSystemId() << "', source_path = '"
          << source_path << "', target_path = '" << target_path << "'}";
  return file_system_->MoveEntry(source_path, target_path, std::move(callback));
}

AbortCallback CloudFileSystem::Truncate(
    const base::FilePath& file_path,
    int64_t length,
    storage::AsyncFileUtil::StatusCallback callback) {
  VLOG(1) << "Truncate {fsid = '" << GetFileSystemId() << "', file_path = '"
          << file_path << "', length = '" << length << "'}";
  return file_system_->Truncate(file_path, length, std::move(callback));
}

AbortCallback CloudFileSystem::AddWatcher(
    const GURL& origin,
    const base::FilePath& entry_path,
    bool recursive,
    bool persistent,
    storage::AsyncFileUtil::StatusCallback callback,
    storage::WatcherManager::NotificationCallback notification_callback) {
  VLOG(2) << "AddWatcher {fsid = '" << GetFileSystemId() << "', origin = '"
          << origin.spec() << "', entry_path = '" << entry_path
          << "', recursive = '" << recursive << "', persistent = '"
          << persistent << "'}";

  // Set timer if the File Manager is a watcher.
  file_manager_watchers_ +=
      file_manager::util::IsFileManagerURL(origin) ? 1 : 0;
  if (file_manager_watchers_ > 0 && !timer_.IsRunning()) {
    timer_.Start(FROM_HERE, kFileManagerWatcherInterval,
                 base::BindRepeating(&CloudFileSystem::OnTimer,
                                     weak_ptr_factory_.GetWeakPtr()));
  }

  return file_system_->AddWatcher(origin, entry_path, recursive, persistent,
                                  std::move(callback),
                                  std::move(notification_callback));
}

void CloudFileSystem::RemoveWatcher(
    const GURL& origin,
    const base::FilePath& entry_path,
    bool recursive,
    storage::AsyncFileUtil::StatusCallback callback) {
  VLOG(2) << "RemoveWatcher {fsid = '" << GetFileSystemId() << "', origin = '"
          << origin.spec() << "', entry_path = '" << entry_path
          << "', recursive = '" << recursive << "'}";

  // Stop timer if the File Manager is not a watcher.
  file_manager_watchers_ -=
      file_manager::util::IsFileManagerURL(origin) ? 1 : 0;
  if (file_manager_watchers_ == 0 && timer_.IsRunning()) {
    timer_.Stop();
  }

  file_system_->RemoveWatcher(origin, entry_path, recursive,
                              std::move(callback));
}

const ProvidedFileSystemInfo& CloudFileSystem::GetFileSystemInfo() const {
  return file_system_->GetFileSystemInfo();
}

OperationRequestManager* CloudFileSystem::GetRequestManager() {
  return file_system_->GetRequestManager();
}

Watchers* CloudFileSystem::GetWatchers() {
  return file_system_->GetWatchers();
}

const OpenedFiles& CloudFileSystem::GetOpenedFiles() const {
  return file_system_->GetOpenedFiles();
}

void CloudFileSystem::AddObserver(ProvidedFileSystemObserver* observer) {
  file_system_->AddObserver(observer);
}

void CloudFileSystem::RemoveObserver(ProvidedFileSystemObserver* observer) {
  file_system_->RemoveObserver(observer);
}

void CloudFileSystem::Notify(
    const base::FilePath& entry_path,
    bool recursive,
    storage::WatcherManager::ChangeType change_type,
    std::unique_ptr<ProvidedFileSystemObserver::Changes> changes,
    const std::string& tag,
    storage::AsyncFileUtil::StatusCallback callback) {
  VLOG(2) << "Notify {fsid = '" << GetFileSystemId() << "', recursive = '"
          << recursive << "', change_type = '" << change_type << "', tag = '"
          << tag << "', changes = {" << changes.get() << "}}";

  if (content_cache_ && changes) {
    content_cache_->Notify(*changes);
  }

  return file_system_->Notify(entry_path, recursive, change_type,
                              std::move(changes), tag, std::move(callback));
}

void CloudFileSystem::Configure(
    storage::AsyncFileUtil::StatusCallback callback) {
  return file_system_->Configure(std::move(callback));
}

base::WeakPtr<ProvidedFileSystemInterface> CloudFileSystem::GetWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

std::unique_ptr<ScopedUserInteraction>
CloudFileSystem::StartUserInteraction() {
  return file_system_->StartUserInteraction();
}

const std::string CloudFileSystem::GetFileSystemId() const {
  return file_system_->GetFileSystemInfo().file_system_id();
}

void CloudFileSystem::OnTimer() {
  VLOG(2) << "OnTimer";
  // TODO(b/317137739): Replace this with a proper API call once one is
  // introduced.
  // Request that the file system syncs with the Cloud. The entry path is
  // insignficant, just pass it root.
  ExecuteAction({base::FilePath("/")}, kODFSSyncWithCloudAction,
                base::BindOnce([](base::File::Error result) {
                  VLOG(1) << "Action " << kODFSSyncWithCloudAction
                          << " completed: " << result;
                }));
}

void CloudFileSystem::OnOpenFileCompleted(
    const base::FilePath& file_path,
    OpenFileMode mode,
    OpenFileCallback callback,
    int file_handle,
    base::File::Error result,
    std::unique_ptr<EntryMetadata> metadata) {
  VLOG(2) << "OnOpenFileCompleted {fsid = " << GetFileSystemId()
          << ", file_handle = '" << file_handle << "', result = '" << result
          << "', metadata = " << metadata.get() << "}";

  if (result == base::File::FILE_OK) {
    const std::string version_tag = GetVersionTag(metadata.get());
    opened_files_.try_emplace(
        file_handle, OpenedCloudFile(file_path, mode, file_handle, version_tag,
                                     GetCloudSize(metadata.get())));
    // Notify the cache with the observed version tag.
    content_cache_->ObservedVersionTag(file_path, version_tag);
  } else if (content_cache_ && result == base::File::FILE_ERROR_NOT_FOUND) {
    // The file doesn't exist on the FSP, evict it from the cache.
    content_cache_->Evict(file_path);
  }
  std::move(callback).Run(file_handle, result, std::move(metadata));
}

void CloudFileSystem::OnCloseFileCompleted(
    int file_handle,
    storage::AsyncFileUtil::StatusCallback callback,
    base::File::Error result) {
  VLOG(2) << "OnCloseFileCompleted {fsid = " << GetFileSystemId()
          << ", file_handle = '" << file_handle << "', result = '" << result
          << "}";
  // Closing is always final. Even if an error happened, we remove it from the
  // list of opened files.
  if (content_cache_) {
    const auto& opened_file = opened_files_.extract(file_handle);
    if (!opened_file.empty()) {
      content_cache_->CloseFile(opened_file.mapped());
    }
  }
  std::move(callback).Run(result);
}

void CloudFileSystem::OnGetMetadataCompleted(
    const base::FilePath& entry_path,
    GetMetadataCallback callback,
    std::unique_ptr<EntryMetadata> entry_metadata,
    base::File::Error result) {
  VLOG(2) << "OnGetMetadataCompleted {fsid = " << GetFileSystemId()
          << ", entry_path = '" << entry_path << "', result = '" << result
          << "', metadata = " << entry_metadata.get() << "}";

  if (content_cache_) {
    if (result == base::File::FILE_ERROR_NOT_FOUND) {
      // The file doesn't exist on the FSP, evict it from the cache.
      content_cache_->Evict(entry_path);
    } else if (result == base::File::FILE_OK) {
      // Notify the cache with the observed version tag.
      content_cache_->ObservedVersionTag(entry_path,
                                         GetVersionTag(entry_metadata.get()));
    }
  }
  std::move(callback).Run(std::move(entry_metadata), result);
}

void CloudFileSystem::OnWriteFileCompleted(
    int file_handle,
    storage::AsyncFileUtil::StatusCallback callback,
    base::File::Error result) {
  VLOG(2) << "OnWriteFileCompleted {fsid = " << GetFileSystemId()
          << ", file_handle = '" << file_handle << "', result = '" << result
          << "}";
  if (content_cache_ && result == base::File::FILE_OK) {
    const auto& opened_file = opened_files_.extract(file_handle);
    if (!opened_file.empty()) {
      // The cached file is now out of date.
      content_cache_->Evict(opened_file.mapped().file_path);
    }
  }
  std::move(callback).Run(result);
}

void CloudFileSystem::OnDeleteEntryCompleted(
    const base::FilePath& entry_path,
    storage::AsyncFileUtil::StatusCallback callback,
    base::File::Error result) {
  VLOG(2) << "OnDeleteEntryCompleted {fsid = " << GetFileSystemId()
          << ", entry_path = '" << entry_path << "', result = '" << result
          << "}";
  if (content_cache_ && result == base::File::FILE_OK) {
    // The cached file should be deleted.
    content_cache_->Evict(entry_path);
  }
  std::move(callback).Run(result);
}

void CloudFileSystem::OnItemEvicted(const base::FilePath& fsp_path) {
  VLOG(1) << fsp_path << " evicted from the content cache";
  RemoveWatcher(
      GetContentCacheURL(), fsp_path, /*recursive=*/false,
      base::BindOnce(
          [](const base::FilePath& fsp_path, base::File::Error result) {
            VLOG(1) << "Removed file watcher on '" << fsp_path
                    << "' result: " << result;
          },
          fsp_path));
}

}  // namespace ash::file_system_provider