blob: c0e1dcb8c1e38e18db2d19d58ad31938a428826e [file] [log] [blame]
// 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/logging.h"
#include "base/notreached.h"
#include "base/strings/string_piece.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/provided_file_system_interface.h"
#include "chrome/browser/ash/file_system_provider/queue.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/");
}
const base::FilePath RootFilePath() {
return base::FilePath("/");
}
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_NORETURN() << "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_NORETURN() << "Unknown ChangeType: " << type;
}
std::ostream& operator<<(std::ostream& out,
ProvidedFileSystemObserver::Changes changes) {
if (changes.empty()) {
return out << "none";
}
for (size_t i = 0; i < changes.size(); ++i) {
const auto& [entry_path, change_type] = changes[i];
out << entry_path << ": " << change_type;
if (i < changes.size() - 1) {
out << ", ";
}
}
return out;
}
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 << "'}";
}
const std::string GetVersionTag(CloudFileInfo* cloud_file_info) {
return (cloud_file_info) ? cloud_file_info->version_tag : "";
}
} // 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()));
// Add watcher to keep content cache up to date. Notifications are received
// though Notify() so no notification_callback is needed.
AddWatcher(GetContentCacheURL(), RootFilePath(),
/*recursive=*/true, /*persistent=*/false,
base::BindOnce([](base::File::Error result) {
VLOG(1) << "Added file watcher on root: " << result;
}),
base::DoNothing());
}
CloudFileSystem::~CloudFileSystem() {
if (content_cache_) {
RemoveWatcher(GetContentCacheURL(), RootFilePath(),
/*recursive=*/true,
base::BindOnce([](base::File::Error result) {
VLOG(1) << "Removed file watcher on root: " << result;
}));
}
}
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());
}
}
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, 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();
}
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);
}
// Attempt to read the file from the content cache, in the event
// `StartReadBytes` succeeds, an actual read of the underlying FD will be
// kicked off, for the purposes of this method it has finished successfully.
// TODO(b/331691461): Fallback to serving the file from the FSP if the read
// from the cache fails.
const OpenedCloudFile& opened_cloud_file = it->second;
if (content_cache_->StartReadBytes(opened_cloud_file, buffer, offset, length,
callback)) {
return AbortCallback();
}
// The file doesn't exist in the cache, we need to make a cloud request
// first and write the result into the cache upon successful return.
return file_system_->ReadFile(
file_handle, buffer, offset, length,
base::BindRepeating(&CloudFileSystem::OnReadFileCompleted,
weak_ptr_factory_.GetWeakPtr(), file_handle, buffer,
offset, length, callback));
}
void CloudFileSystem::OnReadFileCompleted(int file_handle,
net::IOBuffer* buffer,
int64_t offset,
int length,
ReadChunkReceivedCallback callback,
int bytes_read,
bool has_more,
base::File::Error result) {
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);
if (!content_cache_->StartWriteBytes(
it->second, buffer, offset, bytes_read,
base::BindOnce(&CloudFileSystem::OnBytesWrittenToCache,
weak_ptr_factory_.GetWeakPtr(),
readchunk_success_callback))) {
readchunk_success_callback.Run();
}
}
void CloudFileSystem::OnBytesWrittenToCache(
base::RepeatingCallback<void()> readchunk_success_callback,
base::File::Error result) {
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, 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,
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 ? *changes : ProvidedFileSystemObserver::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<CloudFileInfo> cloud_file_info) {
VLOG(1) << "OnOpenFileCompleted {fsid = " << GetFileSystemId()
<< ", file_handle = '" << file_handle << "', result = '" << result
<< "', cloud_file_info = " << cloud_file_info.get() << "}";
if (result == base::File::FILE_OK) {
opened_files_.try_emplace(
file_handle,
OpenedCloudFile(file_path, mode, GetVersionTag(cloud_file_info.get())));
}
std::move(callback).Run(file_handle, result, std::move(cloud_file_info));
}
void CloudFileSystem::OnCloseFileCompleted(
int file_handle,
storage::AsyncFileUtil::StatusCallback callback,
base::File::Error result) {
// Closing is always final. Even if an error happened, we remove it from the
// list of opened files.
opened_files_.erase(file_handle);
std::move(callback).Run(result);
}
} // namespace ash::file_system_provider