blob: 8ac0a002e597e919a0573a09ce43cfeb2cc7d92c [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/arc/fileapi/arc_documents_provider_root.h"
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/files/file.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/stl_util.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "chrome/browser/chromeos/arc/fileapi/arc_documents_provider_util.h"
#include "content/public/browser/browser_thread.h"
#include "url/gurl.h"
using content::BrowserThread;
using EntryList = storage::AsyncFileUtil::EntryList;
namespace arc {
namespace {
// Computes a file name for a document.
// TODO(crbug.com/675868): Consolidate with the similar logic for Drive.
base::FilePath::StringType GetFileNameForDocument(
const mojom::DocumentPtr& document) {
base::FilePath::StringType filename = document->display_name;
// Replace path separators appearing in the file name.
// Chrome OS is POSIX and kSeparators is "/".
base::ReplaceChars(filename, base::FilePath::kSeparators, "_", &filename);
// Do not allow an empty file name and all-dots file names.
if (filename.empty() ||
filename.find_first_not_of('.', 0) == std::string::npos) {
filename = "_";
}
// Since Chrome detects MIME type from file name extensions, we need to change
// the file name extension of the document if it does not match with its MIME
// type.
// For example, Audio Media Provider presents a music file with its title as
// the file name.
base::FilePath::StringType extension =
base::ToLowerASCII(base::FilePath(filename).Extension());
if (!extension.empty())
extension = extension.substr(1); // Strip the leading dot.
std::vector<base::FilePath::StringType> possible_extensions =
GetExtensionsForArcMimeType(document->mime_type);
if (!possible_extensions.empty() &&
!base::ContainsValue(possible_extensions, extension)) {
filename =
base::FilePath(filename).AddExtension(possible_extensions[0]).value();
}
return filename;
}
} // namespace
// static
const int64_t ArcDocumentsProviderRoot::kInvalidWatcherId = -1;
// static
const uint64_t ArcDocumentsProviderRoot::kInvalidWatcherRequestId = 0;
// static
const ArcDocumentsProviderRoot::WatcherData
ArcDocumentsProviderRoot::kInvalidWatcherData = {kInvalidWatcherId,
kInvalidWatcherRequestId};
ArcDocumentsProviderRoot::ArcDocumentsProviderRoot(
const std::string& authority,
const std::string& root_document_id)
: authority_(authority),
root_document_id_(root_document_id),
weak_ptr_factory_(this) {}
ArcDocumentsProviderRoot::~ArcDocumentsProviderRoot() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (observer_wrapper_)
file_system_operation_runner_util::RemoveObserverOnIOThread(
std::move(observer_wrapper_));
}
void ArcDocumentsProviderRoot::GetFileInfo(
const base::FilePath& path,
const GetFileInfoCallback& callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
ResolveToDocumentId(
path, base::Bind(&ArcDocumentsProviderRoot::GetFileInfoWithDocumentId,
weak_ptr_factory_.GetWeakPtr(), callback));
}
void ArcDocumentsProviderRoot::ReadDirectory(
const base::FilePath& path,
const ReadDirectoryCallback& callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
ResolveToDocumentId(
path, base::Bind(&ArcDocumentsProviderRoot::ReadDirectoryWithDocumentId,
weak_ptr_factory_.GetWeakPtr(), callback));
}
void ArcDocumentsProviderRoot::AddWatcher(
const base::FilePath& path,
const WatcherCallback& watcher_callback,
const StatusCallback& callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (path_to_watcher_data_.count(path)) {
callback.Run(base::File::FILE_ERROR_FAILED);
return;
}
uint64_t watcher_request_id = next_watcher_request_id_++;
path_to_watcher_data_.insert(
std::make_pair(path, WatcherData{kInvalidWatcherId, watcher_request_id}));
ResolveToDocumentId(
path, base::Bind(&ArcDocumentsProviderRoot::AddWatcherWithDocumentId,
weak_ptr_factory_.GetWeakPtr(), path, watcher_request_id,
watcher_callback));
// HACK: Invoke |callback| immediately.
//
// TODO(crbug.com/698624): Remove this hack. It was introduced because Files
// app freezes until AddWatcher() finishes, but it should be handled in Files
// app rather than here.
callback.Run(base::File::FILE_OK);
}
void ArcDocumentsProviderRoot::RemoveWatcher(const base::FilePath& path,
const StatusCallback& callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
auto iter = path_to_watcher_data_.find(path);
if (iter == path_to_watcher_data_.end()) {
callback.Run(base::File::FILE_ERROR_FAILED);
return;
}
int64_t watcher_id = iter->second.id;
path_to_watcher_data_.erase(iter);
if (watcher_id == kInvalidWatcherId) {
// This is an invalid watcher. No need to send a request to the remote
// service.
callback.Run(base::File::FILE_OK);
return;
}
file_system_operation_runner_util::RemoveWatcherOnIOThread(
watcher_id,
base::Bind(&ArcDocumentsProviderRoot::OnWatcherRemoved,
weak_ptr_factory_.GetWeakPtr(), callback));
}
void ArcDocumentsProviderRoot::ResolveToContentUrl(
const base::FilePath& path,
const ResolveToContentUrlCallback& callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
ResolveToDocumentId(
path,
base::Bind(&ArcDocumentsProviderRoot::ResolveToContentUrlWithDocumentId,
weak_ptr_factory_.GetWeakPtr(), callback));
}
void ArcDocumentsProviderRoot::OnWatchersCleared() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// Mark all watchers invalid.
for (auto& entry : path_to_watcher_data_)
entry.second = kInvalidWatcherData;
}
void ArcDocumentsProviderRoot::GetFileInfoWithDocumentId(
const GetFileInfoCallback& callback,
const std::string& document_id) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (document_id.empty()) {
callback.Run(base::File::FILE_ERROR_NOT_FOUND, base::File::Info());
return;
}
// Specially handle the root directory since Files app does not update the
// list of file systems (left pane) until all volumes respond to GetMetadata
// requests to root directories.
if (document_id == root_document_id_) {
base::File::Info info;
info.size = -1;
info.is_directory = true;
info.is_symbolic_link = false;
info.last_modified = info.last_accessed = info.creation_time =
base::Time::UnixEpoch(); // arbitrary
callback.Run(base::File::FILE_OK, info);
return;
}
file_system_operation_runner_util::GetDocumentOnIOThread(
authority_, document_id,
base::Bind(&ArcDocumentsProviderRoot::GetFileInfoWithDocument,
weak_ptr_factory_.GetWeakPtr(), callback));
}
void ArcDocumentsProviderRoot::GetFileInfoWithDocument(
const GetFileInfoCallback& callback,
mojom::DocumentPtr document) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (document.is_null()) {
callback.Run(base::File::FILE_ERROR_NOT_FOUND, base::File::Info());
return;
}
base::File::Info info;
info.size = document->size;
info.is_directory = document->mime_type == kAndroidDirectoryMimeType;
info.is_symbolic_link = false;
info.last_modified = info.last_accessed = info.creation_time =
base::Time::FromJavaTime(document->last_modified);
callback.Run(base::File::FILE_OK, info);
}
void ArcDocumentsProviderRoot::ReadDirectoryWithDocumentId(
const ReadDirectoryCallback& callback,
const std::string& document_id) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (document_id.empty()) {
callback.Run(base::File::FILE_ERROR_NOT_FOUND, EntryList(),
false /* has_more */);
return;
}
ReadDirectoryInternal(
document_id,
base::Bind(
&ArcDocumentsProviderRoot::ReadDirectoryWithNameToThinDocumentMap,
weak_ptr_factory_.GetWeakPtr(), callback));
}
void ArcDocumentsProviderRoot::ReadDirectoryWithNameToThinDocumentMap(
const ReadDirectoryCallback& callback,
base::File::Error error,
NameToThinDocumentMap mapping) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (error != base::File::FILE_OK) {
callback.Run(error, EntryList(), false /* has_more */);
return;
}
EntryList entry_list;
for (const auto& pair : mapping) {
entry_list.emplace_back(pair.first, pair.second.is_directory
? storage::DirectoryEntry::DIRECTORY
: storage::DirectoryEntry::FILE);
}
callback.Run(base::File::FILE_OK, entry_list, false /* has_more */);
}
void ArcDocumentsProviderRoot::AddWatcherWithDocumentId(
const base::FilePath& path,
uint64_t watcher_request_id,
const WatcherCallback& watcher_callback,
const std::string& document_id) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (IsWatcherInflightRequestCanceled(path, watcher_request_id))
return;
if (document_id.empty()) {
DCHECK(path_to_watcher_data_.count(path));
path_to_watcher_data_[path] = kInvalidWatcherData;
return;
}
// Start observing ArcFileSystemOperationRunner if we have not.
if (!observer_wrapper_) {
observer_wrapper_ =
new file_system_operation_runner_util::ObserverIOThreadWrapper(this);
file_system_operation_runner_util::AddObserverOnIOThread(observer_wrapper_);
}
file_system_operation_runner_util::AddWatcherOnIOThread(
authority_, document_id, watcher_callback,
base::Bind(&ArcDocumentsProviderRoot::OnWatcherAdded,
weak_ptr_factory_.GetWeakPtr(), path, watcher_request_id));
}
void ArcDocumentsProviderRoot::OnWatcherAdded(const base::FilePath& path,
uint64_t watcher_request_id,
int64_t watcher_id) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (IsWatcherInflightRequestCanceled(path, watcher_request_id)) {
file_system_operation_runner_util::RemoveWatcherOnIOThread(
watcher_id,
base::Bind(&ArcDocumentsProviderRoot::OnWatcherAddedButRemoved,
weak_ptr_factory_.GetWeakPtr()));
return;
}
DCHECK(path_to_watcher_data_.count(path));
path_to_watcher_data_[path] =
WatcherData{watcher_id < 0 ? kInvalidWatcherId : watcher_id,
kInvalidWatcherRequestId};
}
void ArcDocumentsProviderRoot::OnWatcherAddedButRemoved(bool success) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// Ignore |success|.
}
void ArcDocumentsProviderRoot::OnWatcherRemoved(const StatusCallback& callback,
bool success) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
callback.Run(success ? base::File::FILE_OK : base::File::FILE_ERROR_FAILED);
}
bool ArcDocumentsProviderRoot::IsWatcherInflightRequestCanceled(
const base::FilePath& path,
uint64_t watcher_request_id) const {
auto iter = path_to_watcher_data_.find(path);
return (iter == path_to_watcher_data_.end() ||
iter->second.inflight_request_id != watcher_request_id);
}
void ArcDocumentsProviderRoot::ResolveToContentUrlWithDocumentId(
const ResolveToContentUrlCallback& callback,
const std::string& document_id) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (document_id.empty()) {
callback.Run(GURL());
return;
}
callback.Run(BuildDocumentUrl(authority_, document_id));
}
void ArcDocumentsProviderRoot::ResolveToDocumentId(
const base::FilePath& path,
const ResolveToDocumentIdCallback& callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
std::vector<base::FilePath::StringType> components;
path.GetComponents(&components);
ResolveToDocumentIdRecursively(root_document_id_, components, callback);
}
void ArcDocumentsProviderRoot::ResolveToDocumentIdRecursively(
const std::string& document_id,
const std::vector<base::FilePath::StringType>& components,
const ResolveToDocumentIdCallback& callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (components.empty()) {
callback.Run(document_id);
return;
}
ReadDirectoryInternal(
document_id,
base::Bind(&ArcDocumentsProviderRoot::
ResolveToDocumentIdRecursivelyWithNameToThinDocumentMap,
weak_ptr_factory_.GetWeakPtr(), components, callback));
}
void ArcDocumentsProviderRoot::
ResolveToDocumentIdRecursivelyWithNameToThinDocumentMap(
const std::vector<base::FilePath::StringType>& components,
const ResolveToDocumentIdCallback& callback,
base::File::Error error,
NameToThinDocumentMap mapping) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(!components.empty());
if (error != base::File::FILE_OK) {
callback.Run(std::string());
return;
}
auto iter = mapping.find(components[0]);
if (iter == mapping.end()) {
callback.Run(std::string());
return;
}
ResolveToDocumentIdRecursively(iter->second.document_id,
std::vector<base::FilePath::StringType>(
components.begin() + 1, components.end()),
callback);
}
void ArcDocumentsProviderRoot::ReadDirectoryInternal(
const std::string& document_id,
const ReadDirectoryInternalCallback& callback) {
file_system_operation_runner_util::GetChildDocumentsOnIOThread(
authority_, document_id,
base::Bind(
&ArcDocumentsProviderRoot::ReadDirectoryInternalWithChildDocuments,
weak_ptr_factory_.GetWeakPtr(), callback));
}
void ArcDocumentsProviderRoot::ReadDirectoryInternalWithChildDocuments(
const ReadDirectoryInternalCallback& callback,
base::Optional<std::vector<mojom::DocumentPtr>> maybe_children) {
if (!maybe_children) {
callback.Run(base::File::FILE_ERROR_NOT_FOUND, NameToThinDocumentMap());
return;
}
std::vector<mojom::DocumentPtr>& children = maybe_children.value();
// Sort entries to keep the mapping stable as far as possible.
std::sort(children.begin(), children.end(),
[](const mojom::DocumentPtr& a, const mojom::DocumentPtr& b) {
return a->document_id < b->document_id;
});
NameToThinDocumentMap mapping;
std::map<base::FilePath::StringType, int> suffix_counters;
for (const mojom::DocumentPtr& document : children) {
base::FilePath::StringType filename = GetFileNameForDocument(document);
if (mapping.count(filename) > 0) {
// Resolve a conflict by adding a suffix.
int& suffix_counter = suffix_counters[filename];
while (true) {
++suffix_counter;
std::string suffix = base::StringPrintf(" (%d)", suffix_counter);
base::FilePath::StringType new_filename =
base::FilePath(filename).InsertBeforeExtensionASCII(suffix).value();
if (mapping.count(new_filename) == 0) {
filename = new_filename;
break;
}
}
}
DCHECK_EQ(0u, mapping.count(filename));
mapping[filename] =
ThinDocument{document->document_id,
document->mime_type == kAndroidDirectoryMimeType};
}
callback.Run(base::File::FILE_OK, std::move(mapping));
}
} // namespace arc