diff options
author | lukasza <lukasza@chromium.org> | 2015-08-27 14:04:34 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-08-27 21:05:12 +0000 |
commit | 3fb2262ad354f56e9860d51f7b73a2edbf7c782a (patch) | |
tree | 3ed44a3a1a75a1baf1f80466269e935d3e8af4d3 /components/drive | |
parent | fb6a9e2b690bb29089fbb608a2e0720fa40835f3 (diff) | |
download | chromium_src-3fb2262ad354f56e9860d51f7b73a2edbf7c782a.zip chromium_src-3fb2262ad354f56e9860d51f7b73a2edbf7c782a.tar.gz chromium_src-3fb2262ad354f56e9860d51f7b73a2edbf7c782a.tar.bz2 |
Move chrome/browser/chromeos/drive/file_system (+deps) into components/drive.
Files moved from chrome/browser/chromeos/drive into components/drive:
- all the files under drive/file_system/...
- file_system_interface*
- file_system_metadata*
- file_write_watcher*
- resource_metadata*
- resource_metadata_storage*
Other changes:
- Refactoring to limit usage of cryptohome::kMinFreeSpaceInBytes to only
drive/file_cache.h
Test steps:
1. Verify that things still build via GYP.
$ GYP_DEFINES="use_goma=1 chromeos=1 component=shared_library" gclient sync
$ ninja -C out/Debug -j 150 chrome unit_tests \
interactive_ui_tests browser_tests drive
2. Verify that things still build via GN.
$ gn gen out/Default --args='target_os="chromeos" use_goma=true is_component_build=true'
$ ninja -C out/Default -j 150 chrome unit_tests \
interactive_ui_tests browser_tests components/drive
TEST=Please see "Test steps" above.
BUG=257943, 498951
TBR=stevejb@chromium.org and kinuko@chromium.org
Review URL: https://codereview.chromium.org/1318543003
Cr-Commit-Position: refs/heads/master@{#345984}
Diffstat (limited to 'components/drive')
52 files changed, 7819 insertions, 23 deletions
diff --git a/components/drive/BUILD.gn b/components/drive/BUILD.gn index 93f8320..4567878 100644 --- a/components/drive/BUILD.gn +++ b/components/drive/BUILD.gn @@ -31,8 +31,40 @@ source_set("drive") { "file_change.h", "file_errors.cc", "file_errors.h", + "file_system/copy_operation.cc", + "file_system/copy_operation.h", + "file_system/create_directory_operation.cc", + "file_system/create_directory_operation.h", + "file_system/create_file_operation.cc", + "file_system/create_file_operation.h", + "file_system/download_operation.cc", + "file_system/download_operation.h", + "file_system/get_file_for_saving_operation.cc", + "file_system/get_file_for_saving_operation.h", + "file_system/move_operation.cc", + "file_system/move_operation.h", + "file_system/open_file_operation.cc", + "file_system/open_file_operation.h", + "file_system/operation_delegate.cc", + "file_system/operation_delegate.h", + "file_system/remove_operation.cc", + "file_system/remove_operation.h", + "file_system/search_operation.cc", + "file_system/search_operation.h", + "file_system/set_property_operation.cc", + "file_system/set_property_operation.h", + "file_system/touch_operation.cc", + "file_system/touch_operation.h", + "file_system/truncate_operation.cc", + "file_system/truncate_operation.h", "file_system_core_util.cc", "file_system_core_util.h", + "file_system_interface.cc", + "file_system_interface.h", + "file_system_metadata.cc", + "file_system_metadata.h", + "file_write_watcher.cc", + "file_write_watcher.h", "job_list.cc", "job_list.h", "job_queue.cc", diff --git a/components/drive/DEPS b/components/drive/DEPS index 888602d..9f76475 100644 --- a/components/drive/DEPS +++ b/components/drive/DEPS @@ -18,7 +18,19 @@ specific_include_rules = { # The following test dependencies should be removed to fully componentize this # directory. crbug.com/498951 - r"(drive_test_util\.h" + r"(copy_operation_unittest\.cc" + r"|create_directory_operation_unittest\.cc" + r"|create_file_operation_unittest\.cc" + r"|download_operation_unittest\.cc" + r"|drive_test_util\.h" + r"|get_file_for_saving_operation_unittest\.cc" + r"|move_operation_unittest\.cc" + r"|open_file_operation_unittest\.cc" + r"|operation_test_base\.cc" + r"|remove_operation_unittest\.cc" + r"|search_operation_unittest\.cc" + r"|set_property_operation_unittest\.cc" + r"|truncate_operation_unittest\.cc" r")": [ "+content/public/test/test_utils.h", ], @@ -29,18 +41,27 @@ specific_include_rules = { r"|change_list_processor_unittest.cc" r"|file_cache_unittest.cc" r"|file_system_core_util_unittest.cc" + r"|file_write_watcher_unittest.cc" r"|job_scheduler_unittest.cc" + r"|operation_test_base\.h" r"|resource_metadata_storage_unittest.cc" r"|resource_metadata_unittest.cc" r")": [ "+content/public/test/test_browser_thread_bundle.h", ], + # The following test dependencies should be removed to fully componentize this + # directory. crbug.com/498951 + r"(file_write_watcher_unittest\.cc" + r"|get_file_for_saving_operation_unittest\.cc" + r"|operation_test_base\.cc" + r")": [ + "+content/public/browser/browser_thread.h", + ], + # The dependency below is ok and can stay here for the long-term, because it # is guarded by #if defined(OS_CHROMEOS) in the source code. - r"(drive_test_util.h" - r"|file_cache.cc" - r")": [ + "file_cache\.h": [ "+third_party/cros_system_api/constants/cryptohome.h", ], } diff --git a/components/drive/drive_test_util.h b/components/drive/drive_test_util.h index 41b5acb..327dc15 100644 --- a/components/drive/drive_test_util.h +++ b/components/drive/drive_test_util.h @@ -7,15 +7,13 @@ #include <string> +#include "components/drive/file_cache.h" #include "content/public/test/test_utils.h" #include "google_apis/drive/test_util.h" #include "net/base/completion_callback.h" #include "net/base/io_buffer.h" #include "net/base/network_change_notifier.h" #include "net/base/test_completion_callback.h" -#if defined(OS_CHROMEOS) -#include "third_party/cros_system_api/constants/cryptohome.h" -#endif class PrefRegistrySimple; @@ -28,11 +26,7 @@ namespace drive { namespace test_util { // Disk space size used by FakeFreeDiskSpaceGetter. -#if defined(OS_CHROMEOS) -const int64 kLotsOfSpace = cryptohome::kMinFreeSpaceInBytes * 10; -#else -const int64 kLotsOfSpace = 5ull * 1024ull * 1024ull * 1024ull; // 5GB -#endif +const int64 kLotsOfSpace = drive::internal::kMinFreeSpaceInBytes * 10; // Helper to destroy objects which needs Destroy() to be called on destruction. // Note: When using this helper, you should destruct objects before diff --git a/components/drive/file_cache.cc b/components/drive/file_cache.cc index 0638844..572316c 100644 --- a/components/drive/file_cache.cc +++ b/components/drive/file_cache.cc @@ -25,9 +25,6 @@ #include "net/base/filename_util.h" #include "net/base/mime_sniffer.h" #include "net/base/mime_util.h" -#if defined(OS_CHROMEOS) -#include "third_party/cros_system_api/constants/cryptohome.h" -#endif namespace drive { namespace internal { @@ -581,12 +578,7 @@ bool FileCache::HasEnoughSpaceFor(int64 num_bytes, free_space = base::SysInfo::AmountOfFreeDiskSpace(path); // Subtract this as if this portion does not exist. -#if defined(OS_CHROMEOS) - const int64 kMinFreeBytes = cryptohome::kMinFreeSpaceInBytes; -#else - const int64 kMinFreeBytes = 512ull * 1024ull * 1024ull; // 512MB -#endif - free_space -= kMinFreeBytes; + free_space -= drive::internal::kMinFreeSpaceInBytes; return (free_space >= num_bytes); } diff --git a/components/drive/file_cache.h b/components/drive/file_cache.h index c4345ad..b205a77 100644 --- a/components/drive/file_cache.h +++ b/components/drive/file_cache.h @@ -15,6 +15,9 @@ #include "base/threading/thread_checker.h" #include "components/drive/file_errors.h" #include "components/drive/resource_metadata_storage.h" +#if defined(OS_CHROMEOS) +#include "third_party/cros_system_api/constants/cryptohome.h" +#endif namespace base { class ScopedClosureRunner; @@ -25,6 +28,12 @@ namespace drive { namespace internal { +#if defined(OS_CHROMEOS) + const int64 kMinFreeSpaceInBytes = cryptohome::kMinFreeSpaceInBytes; +#else + const int64 kMinFreeSpaceInBytes = 512ull * 1024ull * 1024ull; // 512MB +#endif + // Interface class used for getting the free disk space. Tests can inject an // implementation that reports fake free disk space. class FreeDiskSpaceGetterInterface { @@ -67,7 +76,7 @@ class FileCache { bool IsUnderFileCacheDirectory(const base::FilePath& path) const; // Frees up disk space to store a file with |num_bytes| size content, while - // keeping cryptohome::kMinFreeSpaceInBytes bytes on the disk, if needed. + // keeping drive::internal::kMinFreeSpaceInBytes bytes on the disk, if needed. // Returns true if we successfully manage to have enough space, otherwise // false. bool FreeDiskSpaceIfNeededFor(int64 num_bytes); @@ -157,7 +166,8 @@ class FileCache { void DestroyOnBlockingPool(); // Returns true if we have sufficient space to store the given number of - // bytes, while keeping cryptohome::kMinFreeSpaceInBytes bytes on the disk. + // bytes, while keeping drive::internal::kMinFreeSpaceInBytes bytes on the + // disk. bool HasEnoughSpaceFor(int64 num_bytes, const base::FilePath& path); // Renames cache files from old "prefix:id.md5" format to the new format. diff --git a/components/drive/file_system/copy_operation.cc b/components/drive/file_system/copy_operation.cc new file mode 100644 index 0000000..595e8e1 --- /dev/null +++ b/components/drive/file_system/copy_operation.cc @@ -0,0 +1,671 @@ +// Copyright (c) 2012 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 "components/drive/file_system/copy_operation.h" + +#include <string> + +#include "base/task_runner_util.h" +#include "components/drive/drive.pb.h" +#include "components/drive/drive_api_util.h" +#include "components/drive/file_cache.h" +#include "components/drive/file_change.h" +#include "components/drive/file_system/create_file_operation.h" +#include "components/drive/file_system/operation_delegate.h" +#include "components/drive/file_system_core_util.h" +#include "components/drive/job_scheduler.h" +#include "components/drive/resource_entry_conversion.h" +#include "components/drive/resource_metadata.h" +#include "google_apis/drive/drive_api_parser.h" + +namespace drive { +namespace file_system { + +struct CopyOperation::CopyParams { + base::FilePath src_file_path; + base::FilePath dest_file_path; + bool preserve_last_modified; + FileOperationCallback callback; + ResourceEntry src_entry; + ResourceEntry parent_entry; +}; + +// Enum for categorizing where a gdoc represented by a JSON file exists. +enum JsonGdocLocationType { + NOT_IN_METADATA, + IS_ORPHAN, + HAS_PARENT, +}; + +struct CopyOperation::TransferJsonGdocParams { + TransferJsonGdocParams(const FileOperationCallback& callback, + const std::string& resource_id, + const ResourceEntry& parent_entry, + const std::string& new_title) + : callback(callback), + resource_id(resource_id), + parent_resource_id(parent_entry.resource_id()), + parent_local_id(parent_entry.local_id()), + new_title(new_title), + location_type(NOT_IN_METADATA) { + } + // Parameters supplied or calculated from operation arguments. + const FileOperationCallback callback; + const std::string resource_id; + const std::string parent_resource_id; + const std::string parent_local_id; + const std::string new_title; + + // Values computed during operation. + JsonGdocLocationType location_type; // types where the gdoc file is located. + std::string local_id; // the local_id of the file (if exists in metadata.) + base::FilePath changed_path; +}; + +namespace { + +FileError TryToCopyLocally(internal::ResourceMetadata* metadata, + internal::FileCache* cache, + CopyOperation::CopyParams* params, + std::vector<std::string>* updated_local_ids, + bool* directory_changed, + bool* should_copy_on_server) { + FileError error = metadata->GetResourceEntryByPath(params->src_file_path, + ¶ms->src_entry); + if (error != FILE_ERROR_OK) + return error; + + error = metadata->GetResourceEntryByPath(params->dest_file_path.DirName(), + ¶ms->parent_entry); + if (error != FILE_ERROR_OK) + return error; + + if (!params->parent_entry.file_info().is_directory()) + return FILE_ERROR_NOT_A_DIRECTORY; + + // Drive File System doesn't support recursive copy. + if (params->src_entry.file_info().is_directory()) + return FILE_ERROR_NOT_A_FILE; + + // Check destination. + ResourceEntry dest_entry; + error = metadata->GetResourceEntryByPath(params->dest_file_path, &dest_entry); + switch (error) { + case FILE_ERROR_OK: + // File API spec says it is an error to try to "copy a file to a path + // occupied by a directory". + if (dest_entry.file_info().is_directory()) + return FILE_ERROR_INVALID_OPERATION; + + // Move the existing entry to the trash. + dest_entry.set_parent_local_id(util::kDriveTrashDirLocalId); + error = metadata->RefreshEntry(dest_entry); + if (error != FILE_ERROR_OK) + return error; + updated_local_ids->push_back(dest_entry.local_id()); + *directory_changed = true; + break; + case FILE_ERROR_NOT_FOUND: + break; + default: + return error; + } + + // If the cache file is not present and the entry exists on the server, + // server side copy should be used. + if (!params->src_entry.file_specific_info().cache_state().is_present() && + !params->src_entry.resource_id().empty()) { + *should_copy_on_server = true; + return FILE_ERROR_OK; + } + + // Copy locally. + ResourceEntry entry; + const int64 now = base::Time::Now().ToInternalValue(); + entry.set_title(params->dest_file_path.BaseName().AsUTF8Unsafe()); + entry.set_parent_local_id(params->parent_entry.local_id()); + entry.mutable_file_specific_info()->set_content_mime_type( + params->src_entry.file_specific_info().content_mime_type()); + entry.set_metadata_edit_state(ResourceEntry::DIRTY); + entry.set_modification_date(base::Time::Now().ToInternalValue()); + entry.mutable_file_info()->set_last_modified( + params->preserve_last_modified ? + params->src_entry.file_info().last_modified() : now); + entry.mutable_file_info()->set_last_accessed(now); + + std::string local_id; + error = metadata->AddEntry(entry, &local_id); + if (error != FILE_ERROR_OK) + return error; + updated_local_ids->push_back(local_id); + *directory_changed = true; + + if (!params->src_entry.file_specific_info().cache_state().is_present()) { + DCHECK(params->src_entry.resource_id().empty()); + // Locally created empty file may have no cache file. + return FILE_ERROR_OK; + } + + base::FilePath cache_file_path; + error = cache->GetFile(params->src_entry.local_id(), &cache_file_path); + if (error != FILE_ERROR_OK) + return error; + + return cache->Store(local_id, std::string(), cache_file_path, + internal::FileCache::FILE_OPERATION_COPY); +} + +// Stores the entry returned from the server and returns its path. +FileError UpdateLocalStateForServerSideOperation( + internal::ResourceMetadata* metadata, + scoped_ptr<google_apis::FileResource> file_resource, + ResourceEntry* entry, + base::FilePath* file_path) { + DCHECK(file_resource); + + std::string parent_resource_id; + if (!ConvertFileResourceToResourceEntry( + *file_resource, entry, &parent_resource_id) || + parent_resource_id.empty()) + return FILE_ERROR_NOT_A_FILE; + + std::string parent_local_id; + FileError error = metadata->GetIdByResourceId(parent_resource_id, + &parent_local_id); + if (error != FILE_ERROR_OK) + return error; + entry->set_parent_local_id(parent_local_id); + + std::string local_id; + error = metadata->AddEntry(*entry, &local_id); + // Depending on timing, the metadata may have inserted via change list + // already. So, FILE_ERROR_EXISTS is not an error. + if (error == FILE_ERROR_EXISTS) + error = metadata->GetIdByResourceId(entry->resource_id(), &local_id); + + if (error != FILE_ERROR_OK) + return error; + + return metadata->GetFilePath(local_id, file_path); +} + +// Stores the file at |local_file_path| to the cache as a content of entry at +// |remote_dest_path|, and marks it dirty. +FileError UpdateLocalStateForScheduleTransfer( + internal::ResourceMetadata* metadata, + internal::FileCache* cache, + const base::FilePath& local_src_path, + const base::FilePath& remote_dest_path, + ResourceEntry* entry, + std::string* local_id) { + FileError error = metadata->GetIdByPath(remote_dest_path, local_id); + if (error != FILE_ERROR_OK) + return error; + + error = metadata->GetResourceEntryById(*local_id, entry); + if (error != FILE_ERROR_OK) + return error; + + return cache->Store(*local_id, std::string(), local_src_path, + internal::FileCache::FILE_OPERATION_COPY); +} + +// Gets the file size of the |local_path|, and the ResourceEntry for the parent +// of |remote_path| to prepare the necessary information for transfer. +FileError PrepareTransferFileFromLocalToRemote( + internal::ResourceMetadata* metadata, + const base::FilePath& local_src_path, + const base::FilePath& remote_dest_path, + std::string* gdoc_resource_id, + ResourceEntry* parent_entry) { + FileError error = metadata->GetResourceEntryByPath( + remote_dest_path.DirName(), parent_entry); + if (error != FILE_ERROR_OK) + return error; + + // The destination's parent must be a directory. + if (!parent_entry->file_info().is_directory()) + return FILE_ERROR_NOT_A_DIRECTORY; + + // Try to parse GDoc File and extract the resource id, if necessary. + // Failing isn't problem. It'd be handled as a regular file, then. + if (util::HasHostedDocumentExtension(local_src_path)) + *gdoc_resource_id = util::ReadResourceIdFromGDocFile(local_src_path); + return FILE_ERROR_OK; +} + +// Performs local work before server-side work for transferring JSON-represented +// gdoc files. +FileError LocalWorkForTransferJsonGdocFile( + internal::ResourceMetadata* metadata, + CopyOperation::TransferJsonGdocParams* params) { + std::string local_id; + FileError error = metadata->GetIdByResourceId(params->resource_id, &local_id); + if (error != FILE_ERROR_OK) { + params->location_type = NOT_IN_METADATA; + return error == FILE_ERROR_NOT_FOUND ? FILE_ERROR_OK : error; + } + + ResourceEntry entry; + error = metadata->GetResourceEntryById(local_id, &entry); + if (error != FILE_ERROR_OK) + return error; + params->local_id = entry.local_id(); + + if (entry.parent_local_id() == util::kDriveOtherDirLocalId) { + params->location_type = IS_ORPHAN; + entry.set_title(params->new_title); + entry.set_parent_local_id(params->parent_local_id); + entry.set_metadata_edit_state(ResourceEntry::DIRTY); + entry.set_modification_date(base::Time::Now().ToInternalValue()); + error = metadata->RefreshEntry(entry); + if (error != FILE_ERROR_OK) + return error; + return metadata->GetFilePath(local_id, ¶ms->changed_path); + } + + params->location_type = HAS_PARENT; + return FILE_ERROR_OK; +} + +} // namespace + +CopyOperation::CopyOperation(base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + JobScheduler* scheduler, + internal::ResourceMetadata* metadata, + internal::FileCache* cache) + : blocking_task_runner_(blocking_task_runner), + delegate_(delegate), + scheduler_(scheduler), + metadata_(metadata), + cache_(cache), + create_file_operation_(new CreateFileOperation(blocking_task_runner, + delegate, + metadata)), + weak_ptr_factory_(this) { +} + +CopyOperation::~CopyOperation() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +void CopyOperation::Copy(const base::FilePath& src_file_path, + const base::FilePath& dest_file_path, + bool preserve_last_modified, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + CopyParams* params = new CopyParams; + params->src_file_path = src_file_path; + params->dest_file_path = dest_file_path; + params->preserve_last_modified = preserve_last_modified; + params->callback = callback; + + std::vector<std::string>* updated_local_ids = new std::vector<std::string>; + bool* directory_changed = new bool(false); + bool* should_copy_on_server = new bool(false); + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&TryToCopyLocally, metadata_, cache_, params, + updated_local_ids, directory_changed, should_copy_on_server), + base::Bind(&CopyOperation::CopyAfterTryToCopyLocally, + weak_ptr_factory_.GetWeakPtr(), base::Owned(params), + base::Owned(updated_local_ids), base::Owned(directory_changed), + base::Owned(should_copy_on_server))); +} + +void CopyOperation::CopyAfterTryToCopyLocally( + const CopyParams* params, + const std::vector<std::string>* updated_local_ids, + const bool* directory_changed, + const bool* should_copy_on_server, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!params->callback.is_null()); + + for (const auto& id : *updated_local_ids) { + // Syncing for copy should be done in background, so pass the BACKGROUND + // context. See: crbug.com/420278. + delegate_->OnEntryUpdatedByOperation(ClientContext(BACKGROUND), id); + } + + if (*directory_changed) { + FileChange changed_file; + DCHECK(!params->src_entry.file_info().is_directory()); + changed_file.Update(params->dest_file_path, FileChange::FILE_TYPE_FILE, + FileChange::CHANGE_TYPE_ADD_OR_UPDATE); + delegate_->OnFileChangedByOperation(changed_file); + } + + if (error != FILE_ERROR_OK || !*should_copy_on_server) { + params->callback.Run(error); + return; + } + + if (params->parent_entry.resource_id().empty()) { + // Parent entry may be being synced. + const bool waiting = delegate_->WaitForSyncComplete( + params->parent_entry.local_id(), + base::Bind(&CopyOperation::CopyAfterParentSync, + weak_ptr_factory_.GetWeakPtr(), *params)); + if (!waiting) + params->callback.Run(FILE_ERROR_NOT_FOUND); + } else { + CopyAfterGetParentResourceId(*params, ¶ms->parent_entry, FILE_ERROR_OK); + } +} + +void CopyOperation::CopyAfterParentSync(const CopyParams& params, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!params.callback.is_null()); + + if (error != FILE_ERROR_OK) { + params.callback.Run(error); + return; + } + + ResourceEntry* parent = new ResourceEntry; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&internal::ResourceMetadata::GetResourceEntryById, + base::Unretained(metadata_), + params.parent_entry.local_id(), + parent), + base::Bind(&CopyOperation::CopyAfterGetParentResourceId, + weak_ptr_factory_.GetWeakPtr(), + params, + base::Owned(parent))); +} + +void CopyOperation::CopyAfterGetParentResourceId(const CopyParams& params, + const ResourceEntry* parent, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!params.callback.is_null()); + + if (error != FILE_ERROR_OK) { + params.callback.Run(error); + return; + } + + base::FilePath new_title = params.dest_file_path.BaseName(); + if (params.src_entry.file_specific_info().is_hosted_document()) { + // Drop the document extension, which should not be in the title. + // TODO(yoshiki): Remove this code with crbug.com/223304. + new_title = new_title.RemoveExtension(); + } + + base::Time last_modified = + params.preserve_last_modified ? + base::Time::FromInternalValue( + params.src_entry.file_info().last_modified()) : base::Time(); + + CopyResourceOnServer( + params.src_entry.resource_id(), parent->resource_id(), + new_title.AsUTF8Unsafe(), last_modified, params.callback); +} + +void CopyOperation::TransferFileFromLocalToRemote( + const base::FilePath& local_src_path, + const base::FilePath& remote_dest_path, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + std::string* gdoc_resource_id = new std::string; + ResourceEntry* parent_entry = new ResourceEntry; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind( + &PrepareTransferFileFromLocalToRemote, + metadata_, local_src_path, remote_dest_path, + gdoc_resource_id, parent_entry), + base::Bind( + &CopyOperation::TransferFileFromLocalToRemoteAfterPrepare, + weak_ptr_factory_.GetWeakPtr(), + local_src_path, remote_dest_path, callback, + base::Owned(gdoc_resource_id), base::Owned(parent_entry))); +} + +void CopyOperation::TransferFileFromLocalToRemoteAfterPrepare( + const base::FilePath& local_src_path, + const base::FilePath& remote_dest_path, + const FileOperationCallback& callback, + std::string* gdoc_resource_id, + ResourceEntry* parent_entry, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error != FILE_ERROR_OK) { + callback.Run(error); + return; + } + + // For regular files, schedule the transfer. + if (gdoc_resource_id->empty()) { + ScheduleTransferRegularFile(local_src_path, remote_dest_path, callback); + return; + } + + // GDoc file may contain a resource ID in the old format. + const std::string canonicalized_resource_id = + util::CanonicalizeResourceId(*gdoc_resource_id); + + // Drop the document extension, which should not be in the title. + // TODO(yoshiki): Remove this code with crbug.com/223304. + const std::string new_title = + remote_dest_path.BaseName().RemoveExtension().AsUTF8Unsafe(); + + // This is uploading a JSON file representing a hosted document. + TransferJsonGdocParams* params = new TransferJsonGdocParams( + callback, canonicalized_resource_id, *parent_entry, new_title); + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&LocalWorkForTransferJsonGdocFile, metadata_, params), + base::Bind(&CopyOperation::TransferJsonGdocFileAfterLocalWork, + weak_ptr_factory_.GetWeakPtr(), base::Owned(params))); +} + +void CopyOperation::TransferJsonGdocFileAfterLocalWork( + TransferJsonGdocParams* params, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (error != FILE_ERROR_OK) { + params->callback.Run(error); + return; + } + + switch (params->location_type) { + // When |resource_id| is found in the local metadata and it has a specific + // parent folder, we assume the user's intention is to copy the document and + // thus perform the server-side copy operation. + case HAS_PARENT: + CopyResourceOnServer(params->resource_id, + params->parent_resource_id, + params->new_title, + base::Time(), + params->callback); + break; + // When |resource_id| has no parent, we just set the new destination folder + // as the parent, for sharing the document between the original source. + // This reparenting is already done in LocalWorkForTransferJsonGdocFile(). + case IS_ORPHAN: { + DCHECK(!params->changed_path.empty()); + // Syncing for copy should be done in background, so pass the BACKGROUND + // context. See: crbug.com/420278. + delegate_->OnEntryUpdatedByOperation(ClientContext(BACKGROUND), + params->local_id); + + FileChange changed_file; + changed_file.Update( + params->changed_path, + FileChange::FILE_TYPE_FILE, // This must be a hosted document. + FileChange::CHANGE_TYPE_ADD_OR_UPDATE); + delegate_->OnFileChangedByOperation(changed_file); + params->callback.Run(error); + break; + } + // When the |resource_id| is not in the local metadata, assume it to be a + // document just now shared on the server but not synced locally. + // Same as the IS_ORPHAN case, we want to deal the case by setting parent, + // but this time we need to resort to server side operation. + case NOT_IN_METADATA: + scheduler_->UpdateResource( + params->resource_id, params->parent_resource_id, params->new_title, + base::Time(), base::Time(), google_apis::drive::Properties(), + ClientContext(USER_INITIATED), + base::Bind(&CopyOperation::UpdateAfterServerSideOperation, + weak_ptr_factory_.GetWeakPtr(), params->callback)); + break; + } +} + +void CopyOperation::CopyResourceOnServer( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + scheduler_->CopyResource( + resource_id, parent_resource_id, new_title, last_modified, + base::Bind(&CopyOperation::UpdateAfterServerSideOperation, + weak_ptr_factory_.GetWeakPtr(), + callback)); +} + +void CopyOperation::UpdateAfterServerSideOperation( + const FileOperationCallback& callback, + google_apis::DriveApiErrorCode status, + scoped_ptr<google_apis::FileResource> entry) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FileError error = GDataToFileError(status); + if (error != FILE_ERROR_OK) { + callback.Run(error); + return; + } + + ResourceEntry* resource_entry = new ResourceEntry; + + // The copy on the server side is completed successfully. Update the local + // metadata. + base::FilePath* file_path = new base::FilePath; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&UpdateLocalStateForServerSideOperation, + metadata_, + base::Passed(&entry), + resource_entry, + file_path), + base::Bind(&CopyOperation::UpdateAfterLocalStateUpdate, + weak_ptr_factory_.GetWeakPtr(), + callback, + base::Owned(file_path), + base::Owned(resource_entry))); +} + +void CopyOperation::UpdateAfterLocalStateUpdate( + const FileOperationCallback& callback, + base::FilePath* file_path, + const ResourceEntry* entry, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error == FILE_ERROR_OK) { + FileChange changed_file; + changed_file.Update(*file_path, *entry, + FileChange::CHANGE_TYPE_ADD_OR_UPDATE); + delegate_->OnFileChangedByOperation(changed_file); + } + callback.Run(error); +} + +void CopyOperation::ScheduleTransferRegularFile( + const base::FilePath& local_src_path, + const base::FilePath& remote_dest_path, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + create_file_operation_->CreateFile( + remote_dest_path, + false, // Not exclusive (OK even if a file already exists). + std::string(), // no specific mime type; CreateFile should guess it. + base::Bind(&CopyOperation::ScheduleTransferRegularFileAfterCreate, + weak_ptr_factory_.GetWeakPtr(), + local_src_path, remote_dest_path, callback)); +} + +void CopyOperation::ScheduleTransferRegularFileAfterCreate( + const base::FilePath& local_src_path, + const base::FilePath& remote_dest_path, + const FileOperationCallback& callback, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error != FILE_ERROR_OK) { + callback.Run(error); + return; + } + + std::string* local_id = new std::string; + ResourceEntry* entry = new ResourceEntry; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&UpdateLocalStateForScheduleTransfer, + metadata_, + cache_, + local_src_path, + remote_dest_path, + entry, + local_id), + base::Bind( + &CopyOperation::ScheduleTransferRegularFileAfterUpdateLocalState, + weak_ptr_factory_.GetWeakPtr(), + callback, + remote_dest_path, + base::Owned(entry), + base::Owned(local_id))); +} + +void CopyOperation::ScheduleTransferRegularFileAfterUpdateLocalState( + const FileOperationCallback& callback, + const base::FilePath& remote_dest_path, + const ResourceEntry* entry, + std::string* local_id, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error == FILE_ERROR_OK) { + FileChange changed_file; + changed_file.Update(remote_dest_path, *entry, + FileChange::CHANGE_TYPE_ADD_OR_UPDATE); + delegate_->OnFileChangedByOperation(changed_file); + // Syncing for copy should be done in background, so pass the BACKGROUND + // context. See: crbug.com/420278. + delegate_->OnEntryUpdatedByOperation(ClientContext(BACKGROUND), *local_id); + } + callback.Run(error); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/copy_operation.h b/components/drive/file_system/copy_operation.h new file mode 100644 index 0000000..1f92bd4 --- /dev/null +++ b/components/drive/file_system/copy_operation.h @@ -0,0 +1,181 @@ +// Copyright (c) 2012 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_COPY_OPERATION_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_COPY_OPERATION_H_ + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "components/drive/file_errors.h" +#include "google_apis/drive/drive_api_error_codes.h" + +namespace base { +class FilePath; +class SequencedTaskRunner; +class Time; +} // namespace base + +namespace google_apis { +class FileResource; +} // namespace google_apis + +namespace drive { + +class JobScheduler; +class ResourceEntry; + +namespace internal { +class FileCache; +class ResourceMetadata; +} // namespace internal + +namespace file_system { + +class CreateFileOperation; +class OperationDelegate; + +// This class encapsulates the drive Copy function. It is responsible for +// sending the request to the drive API, then updating the local state and +// metadata to reflect the new state. +class CopyOperation { + public: + CopyOperation(base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + JobScheduler* scheduler, + internal::ResourceMetadata* metadata, + internal::FileCache* cache); + ~CopyOperation(); + + // Performs the copy operation on the file at drive path |src_file_path| + // with a target of |dest_file_path|. + // If |preserve_last_modified| is set to true, this tries to preserve + // last modified time stamp. This is supported only on Drive API v2. + // Invokes |callback| when finished with the result of the operation. + // |callback| must not be null. + void Copy(const base::FilePath& src_file_path, + const base::FilePath& dest_file_path, + bool preserve_last_modified, + const FileOperationCallback& callback); + + // Initiates transfer of |local_src_file_path| to |remote_dest_file_path|. + // |local_src_file_path| must be a file from the local file system. + // |remote_dest_file_path| is the virtual destination path within Drive file + // system. + // + // |callback| must not be null. + void TransferFileFromLocalToRemote( + const base::FilePath& local_src_file_path, + const base::FilePath& remote_dest_file_path, + const FileOperationCallback& callback); + + // Params for Copy(). + struct CopyParams; + + // Params for TransferJsonGdocFileAfterLocalWork. + struct TransferJsonGdocParams; + + private: + // Part of Copy(). Called after trying to copy locally. + void CopyAfterTryToCopyLocally( + const CopyParams* params, + const std::vector<std::string>* updated_local_ids, + const bool* directory_changed, + const bool* should_copy_on_server, + FileError error); + + // Part of Copy(). Called after the parent entry gets synced. + void CopyAfterParentSync(const CopyParams& params, FileError error); + + // Part of Copy(). Called after the parent resource ID is resolved. + void CopyAfterGetParentResourceId(const CopyParams& params, + const ResourceEntry* parent, + FileError error); + + // Part of TransferFileFromLocalToRemote(). Called after preparation is done. + // |gdoc_resource_id| and |parent_resource_id| is available only if the file + // is JSON GDoc file. + void TransferFileFromLocalToRemoteAfterPrepare( + const base::FilePath& local_src_path, + const base::FilePath& remote_dest_path, + const FileOperationCallback& callback, + std::string* gdoc_resource_id, + ResourceEntry* parent_entry, + FileError error); + + // Part of TransferFileFromLocalToRemote(). + void TransferJsonGdocFileAfterLocalWork(TransferJsonGdocParams* params, + FileError error); + + // Copies resource with |resource_id| into the directory |parent_resource_id| + // with renaming it to |new_title|. + void CopyResourceOnServer(const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const FileOperationCallback& callback); + + // Part of CopyResourceOnServer and TransferFileFromLocalToRemote. + // Called after server side operation is done. + void UpdateAfterServerSideOperation( + const FileOperationCallback& callback, + google_apis::DriveApiErrorCode status, + scoped_ptr<google_apis::FileResource> entry); + + // Part of CopyResourceOnServer and TransferFileFromLocalToRemote. + // Called after local state update is done. + void UpdateAfterLocalStateUpdate(const FileOperationCallback& callback, + base::FilePath* file_path, + const ResourceEntry* entry, + FileError error); + + // Creates an empty file on the server at |remote_dest_path| to ensure + // the location, stores a file at |local_file_path| in cache and marks it + // dirty, so that SyncClient will upload the data later. + void ScheduleTransferRegularFile(const base::FilePath& local_src_path, + const base::FilePath& remote_dest_path, + const FileOperationCallback& callback); + + // Part of ScheduleTransferRegularFile(). Called after file creation. + void ScheduleTransferRegularFileAfterCreate( + const base::FilePath& local_src_path, + const base::FilePath& remote_dest_path, + const FileOperationCallback& callback, + FileError error); + + // Part of ScheduleTransferRegularFile(). Called after updating local state + // is completed. + void ScheduleTransferRegularFileAfterUpdateLocalState( + const FileOperationCallback& callback, + const base::FilePath& remote_dest_path, + const ResourceEntry* entry, + std::string* local_id, + FileError error); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + OperationDelegate* delegate_; + JobScheduler* scheduler_; + internal::ResourceMetadata* metadata_; + internal::FileCache* cache_; + + // Uploading a new file is internally implemented by creating a dirty file. + scoped_ptr<CreateFileOperation> create_file_operation_; + + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate the weak pointers before any other members are destroyed. + base::WeakPtrFactory<CopyOperation> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(CopyOperation); +}; + +} // namespace file_system +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_COPY_OPERATION_H_ diff --git a/components/drive/file_system/copy_operation_unittest.cc b/components/drive/file_system/copy_operation_unittest.cc new file mode 100644 index 0000000..3491746 --- /dev/null +++ b/components/drive/file_system/copy_operation_unittest.cc @@ -0,0 +1,517 @@ +// Copyright 2013 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 "components/drive/file_system/copy_operation.h" + +#include "base/files/file_util.h" +#include "base/task_runner_util.h" +#include "components/drive/drive_api_util.h" +#include "components/drive/file_cache.h" +#include "components/drive/file_change.h" +#include "components/drive/file_system/operation_test_base.h" +#include "components/drive/file_system_core_util.h" +#include "components/drive/resource_metadata.h" +#include "components/drive/service/fake_drive_service.h" +#include "content/public/test/test_utils.h" +#include "google_apis/drive/drive_api_parser.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace file_system { + +namespace { + +// Used to handle WaitForSyncComplete() calls. +bool CopyWaitForSyncCompleteArguments(std::string* out_local_id, + FileOperationCallback* out_callback, + const std::string& local_id, + const FileOperationCallback& callback) { + *out_local_id = local_id; + *out_callback = callback; + return true; +} + +} // namespace + +class CopyOperationTest : public OperationTestBase { + protected: + void SetUp() override { + OperationTestBase::SetUp(); + operation_.reset(new CopyOperation( + blocking_task_runner(), delegate(), scheduler(), metadata(), cache())); + } + + scoped_ptr<CopyOperation> operation_; +}; + +TEST_F(CopyOperationTest, TransferFileFromLocalToRemote_RegularFile) { + const base::FilePath local_src_path = temp_dir().AppendASCII("local.txt"); + const base::FilePath remote_dest_path( + FILE_PATH_LITERAL("drive/root/remote.txt")); + + // Prepare a local file. + ASSERT_TRUE( + google_apis::test_util::WriteStringToFile(local_src_path, "hello")); + // Confirm that the remote file does not exist. + ResourceEntry entry; + ASSERT_EQ(FILE_ERROR_NOT_FOUND, + GetLocalResourceEntry(remote_dest_path, &entry)); + + // Transfer the local file to Drive. + FileError error = FILE_ERROR_FAILED; + operation_->TransferFileFromLocalToRemote( + local_src_path, + remote_dest_path, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // TransferFileFromLocalToRemote stores a copy of the local file in the cache, + // marks it dirty and requests the observer to upload the file. + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(remote_dest_path, &entry)); + EXPECT_EQ(1U, delegate()->updated_local_ids().count(entry.local_id())); + EXPECT_TRUE(entry.file_specific_info().cache_state().is_present()); + EXPECT_TRUE(entry.file_specific_info().cache_state().is_dirty()); + + EXPECT_EQ(1U, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(remote_dest_path)); +} + +TEST_F(CopyOperationTest, TransferFileFromLocalToRemote_Overwrite) { + const base::FilePath local_src_path = temp_dir().AppendASCII("local.txt"); + const base::FilePath remote_dest_path( + FILE_PATH_LITERAL("drive/root/File 1.txt")); + + // Prepare a local file. + EXPECT_TRUE( + google_apis::test_util::WriteStringToFile(local_src_path, "hello")); + // Confirm that the remote file exists. + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(remote_dest_path, &entry)); + + // Transfer the local file to Drive. + FileError error = FILE_ERROR_FAILED; + operation_->TransferFileFromLocalToRemote( + local_src_path, + remote_dest_path, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // TransferFileFromLocalToRemote stores a copy of the local file in the cache, + // marks it dirty and requests the observer to upload the file. + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(remote_dest_path, &entry)); + EXPECT_EQ(1U, delegate()->updated_local_ids().count(entry.local_id())); + EXPECT_TRUE(entry.file_specific_info().cache_state().is_present()); + EXPECT_TRUE(entry.file_specific_info().cache_state().is_dirty()); + + EXPECT_EQ(1U, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(remote_dest_path)); +} + +TEST_F(CopyOperationTest, + TransferFileFromLocalToRemote_ExistingHostedDocument) { + const base::FilePath local_src_path = temp_dir().AppendASCII("local.gdoc"); + const base::FilePath remote_dest_path(FILE_PATH_LITERAL( + "drive/root/Directory 1/copied.gdoc")); + + // Prepare a local file, which is a json file of a hosted document, which + // matches "drive/root/Document 1 excludeDir-test". + ASSERT_TRUE(util::CreateGDocFile( + local_src_path, + GURL("https://3_document_self_link/5_document_resource_id"), + "5_document_resource_id")); + + ResourceEntry entry; + ASSERT_EQ(FILE_ERROR_NOT_FOUND, + GetLocalResourceEntry(remote_dest_path, &entry)); + + // Transfer the local file to Drive. + FileError error = FILE_ERROR_FAILED; + operation_->TransferFileFromLocalToRemote( + local_src_path, + remote_dest_path, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(remote_dest_path, &entry)); + + EXPECT_EQ(1U, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(remote_dest_path)); + // New copy is created. + EXPECT_NE("5_document_resource_id", entry.resource_id()); +} + +TEST_F(CopyOperationTest, TransferFileFromLocalToRemote_OrphanHostedDocument) { + const base::FilePath local_src_path = temp_dir().AppendASCII("local.gdoc"); + const base::FilePath remote_dest_path(FILE_PATH_LITERAL( + "drive/root/Directory 1/moved.gdoc")); + + // Prepare a local file, which is a json file of a hosted document, which + // matches "drive/other/Orphan Document". + ASSERT_TRUE(util::CreateGDocFile( + local_src_path, + GURL("https://3_document_self_link/orphan_doc_1"), + "orphan_doc_1")); + + ResourceEntry entry; + ASSERT_EQ(FILE_ERROR_NOT_FOUND, + GetLocalResourceEntry(remote_dest_path, &entry)); + + // Transfer the local file to Drive. + FileError error = FILE_ERROR_FAILED; + operation_->TransferFileFromLocalToRemote( + local_src_path, + remote_dest_path, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(remote_dest_path, &entry)); + EXPECT_EQ(ResourceEntry::DIRTY, entry.metadata_edit_state()); + EXPECT_TRUE(delegate()->updated_local_ids().count(entry.local_id())); + + EXPECT_EQ(1U, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(remote_dest_path)); + // The original document got new parent. + EXPECT_EQ("orphan_doc_1", entry.resource_id()); +} + +TEST_F(CopyOperationTest, TransferFileFromLocalToRemote_NewHostedDocument) { + const base::FilePath local_src_path = temp_dir().AppendASCII("local.gdoc"); + const base::FilePath remote_dest_path(FILE_PATH_LITERAL( + "drive/root/Directory 1/moved.gdoc")); + + // Create a hosted document on the server that is not synced to local yet. + google_apis::DriveApiErrorCode gdata_error = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> new_gdoc_entry; + fake_service()->AddNewFile( + "application/vnd.google-apps.document", "", "", "title", true, + google_apis::test_util::CreateCopyResultCallback(&gdata_error, + &new_gdoc_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + ASSERT_EQ(google_apis::HTTP_CREATED, gdata_error); + + // Prepare a local file, which is a json file of the added hosted document. + ASSERT_TRUE(util::CreateGDocFile( + local_src_path, + GURL("https://3_document_self_link/" + new_gdoc_entry->file_id()), + new_gdoc_entry->file_id())); + + ResourceEntry entry; + ASSERT_EQ(FILE_ERROR_NOT_FOUND, + GetLocalResourceEntry(remote_dest_path, &entry)); + + // Transfer the local file to Drive. + FileError error = FILE_ERROR_FAILED; + operation_->TransferFileFromLocalToRemote( + local_src_path, + remote_dest_path, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(remote_dest_path, &entry)); + + EXPECT_EQ(1U, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(remote_dest_path)); + // The original document got new parent. + EXPECT_EQ(new_gdoc_entry->file_id(), entry.resource_id()); +} + +TEST_F(CopyOperationTest, CopyNotExistingFile) { + base::FilePath src_path(FILE_PATH_LITERAL("drive/root/Dummy file.txt")); + base::FilePath dest_path(FILE_PATH_LITERAL("drive/root/Test.log")); + + ResourceEntry entry; + ASSERT_EQ(FILE_ERROR_NOT_FOUND, GetLocalResourceEntry(src_path, &entry)); + + FileError error = FILE_ERROR_OK; + operation_->Copy(src_path, + dest_path, + false, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, error); + + EXPECT_EQ(FILE_ERROR_NOT_FOUND, GetLocalResourceEntry(src_path, &entry)); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, GetLocalResourceEntry(dest_path, &entry)); + EXPECT_TRUE(delegate()->get_changed_files().empty()); +} + +TEST_F(CopyOperationTest, CopyFileToNonExistingDirectory) { + base::FilePath src_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + base::FilePath dest_path(FILE_PATH_LITERAL("drive/root/Dummy/Test.log")); + + ResourceEntry entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(src_path, &entry)); + ASSERT_EQ(FILE_ERROR_NOT_FOUND, + GetLocalResourceEntry(dest_path.DirName(), &entry)); + + FileError error = FILE_ERROR_OK; + operation_->Copy(src_path, + dest_path, + false, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, error); + + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(src_path, &entry)); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, GetLocalResourceEntry(dest_path, &entry)); + EXPECT_TRUE(delegate()->get_changed_files().empty()); +} + +// Test the case where the parent of the destination path is an existing file, +// not a directory. +TEST_F(CopyOperationTest, CopyFileToInvalidPath) { + base::FilePath src_path(FILE_PATH_LITERAL( + "drive/root/Document 1 excludeDir-test.gdoc")); + base::FilePath dest_path(FILE_PATH_LITERAL( + "drive/root/Duplicate Name.txt/Document 1 excludeDir-test.gdoc")); + + ResourceEntry entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(src_path, &entry)); + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(dest_path.DirName(), &entry)); + ASSERT_FALSE(entry.file_info().is_directory()); + + FileError error = FILE_ERROR_OK; + operation_->Copy(src_path, + dest_path, + false, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_A_DIRECTORY, error); + + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(src_path, &entry)); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, GetLocalResourceEntry(dest_path, &entry)); + EXPECT_TRUE(delegate()->get_changed_files().empty()); +} + +TEST_F(CopyOperationTest, CopyDirtyFile) { + base::FilePath src_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + base::FilePath dest_path(FILE_PATH_LITERAL( + "drive/root/Directory 1/New File.txt")); + + ResourceEntry src_entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(src_path, &src_entry)); + + // Store a dirty cache file. + base::FilePath temp_file; + EXPECT_TRUE(base::CreateTemporaryFileInDir(temp_dir(), &temp_file)); + std::string contents = "test content"; + EXPECT_TRUE(google_apis::test_util::WriteStringToFile(temp_file, contents)); + FileError error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::FileCache::Store, + base::Unretained(cache()), + src_entry.local_id(), + std::string(), + temp_file, + internal::FileCache::FILE_OPERATION_MOVE), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Copy. + operation_->Copy(src_path, + dest_path, + false, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + ResourceEntry dest_entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(dest_path, &dest_entry)); + EXPECT_EQ(ResourceEntry::DIRTY, dest_entry.metadata_edit_state()); + + EXPECT_EQ(1u, delegate()->updated_local_ids().size()); + EXPECT_TRUE(delegate()->updated_local_ids().count(dest_entry.local_id())); + EXPECT_EQ(1u, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(dest_path)); + + // Copied cache file should be dirty. + EXPECT_TRUE(dest_entry.file_specific_info().cache_state().is_dirty()); + + // File contents should match. + base::FilePath cache_file_path; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::FileCache::GetFile, + base::Unretained(cache()), + dest_entry.local_id(), + &cache_file_path), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + std::string copied_contents; + EXPECT_TRUE(base::ReadFileToString(cache_file_path, &copied_contents)); + EXPECT_EQ(contents, copied_contents); +} + +TEST_F(CopyOperationTest, CopyFileOverwriteFile) { + base::FilePath src_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + base::FilePath dest_path(FILE_PATH_LITERAL( + "drive/root/Directory 1/SubDirectory File 1.txt")); + + ResourceEntry old_dest_entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(dest_path, &old_dest_entry)); + + FileError error = FILE_ERROR_OK; + operation_->Copy(src_path, + dest_path, + false, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + ResourceEntry new_dest_entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(dest_path, &new_dest_entry)); + + EXPECT_EQ(1u, delegate()->updated_local_ids().size()); + EXPECT_TRUE(delegate()->updated_local_ids().count(old_dest_entry.local_id())); + EXPECT_EQ(1u, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(dest_path)); +} + +TEST_F(CopyOperationTest, CopyFileOverwriteDirectory) { + base::FilePath src_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + base::FilePath dest_path(FILE_PATH_LITERAL("drive/root/Directory 1")); + + FileError error = FILE_ERROR_OK; + operation_->Copy(src_path, + dest_path, + false, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_INVALID_OPERATION, error); +} + +TEST_F(CopyOperationTest, CopyDirectory) { + base::FilePath src_path(FILE_PATH_LITERAL("drive/root/Directory 1")); + base::FilePath dest_path(FILE_PATH_LITERAL("drive/root/New Directory")); + + ResourceEntry entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(src_path, &entry)); + ASSERT_TRUE(entry.file_info().is_directory()); + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(dest_path.DirName(), &entry)); + ASSERT_TRUE(entry.file_info().is_directory()); + + FileError error = FILE_ERROR_OK; + operation_->Copy(src_path, + dest_path, + false, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_A_FILE, error); +} + +TEST_F(CopyOperationTest, PreserveLastModified) { + base::FilePath src_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + base::FilePath dest_path(FILE_PATH_LITERAL("drive/root/File 2.txt")); + + ResourceEntry entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(src_path, &entry)); + ASSERT_EQ(FILE_ERROR_OK, + GetLocalResourceEntry(dest_path.DirName(), &entry)); + + FileError error = FILE_ERROR_OK; + operation_->Copy(src_path, + dest_path, + true, // Preserve last modified. + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + ResourceEntry entry2; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(src_path, &entry)); + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(dest_path, &entry2)); + EXPECT_EQ(entry.file_info().last_modified(), + entry2.file_info().last_modified()); +} + +TEST_F(CopyOperationTest, WaitForSyncComplete) { + // Create a directory locally. + base::FilePath src_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + base::FilePath directory_path(FILE_PATH_LITERAL("drive/root/New Directory")); + base::FilePath dest_path = directory_path.AppendASCII("File 1.txt"); + + ResourceEntry directory_parent; + EXPECT_EQ(FILE_ERROR_OK, + GetLocalResourceEntry(directory_path.DirName(), &directory_parent)); + + ResourceEntry directory; + directory.set_parent_local_id(directory_parent.local_id()); + directory.set_title(directory_path.BaseName().AsUTF8Unsafe()); + directory.mutable_file_info()->set_is_directory(true); + directory.set_metadata_edit_state(ResourceEntry::DIRTY); + + std::string directory_local_id; + FileError error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::ResourceMetadata::AddEntry, + base::Unretained(metadata()), directory, &directory_local_id), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Try to copy a file to the new directory which lacks resource ID. + // This should result in waiting for the directory to sync. + std::string waited_local_id; + FileOperationCallback pending_callback; + delegate()->set_wait_for_sync_complete_handler( + base::Bind(&CopyWaitForSyncCompleteArguments, + &waited_local_id, &pending_callback)); + + FileError copy_error = FILE_ERROR_FAILED; + operation_->Copy(src_path, + dest_path, + true, // Preserve last modified. + google_apis::test_util::CreateCopyResultCallback( + ©_error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(directory_local_id, waited_local_id); + ASSERT_FALSE(pending_callback.is_null()); + + // Add a new directory to the server and store the resource ID locally. + google_apis::DriveApiErrorCode status = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> file_resource; + fake_service()->AddNewDirectory( + directory_parent.resource_id(), directory.title(), + AddNewDirectoryOptions(), + google_apis::test_util::CreateCopyResultCallback(&status, + &file_resource)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(google_apis::HTTP_CREATED, status); + ASSERT_TRUE(file_resource); + + directory.set_local_id(directory_local_id); + directory.set_resource_id(file_resource->file_id()); + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::ResourceMetadata::RefreshEntry, + base::Unretained(metadata()), directory), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Resume the copy operation. + pending_callback.Run(FILE_ERROR_OK); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, copy_error); + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(dest_path, &entry)); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/create_directory_operation.cc b/components/drive/file_system/create_directory_operation.cc new file mode 100644 index 0000000..a06ef5b --- /dev/null +++ b/components/drive/file_system/create_directory_operation.cc @@ -0,0 +1,182 @@ +// Copyright (c) 2013 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 "components/drive/file_system/create_directory_operation.h" + +#include "components/drive/drive.pb.h" +#include "components/drive/file_change.h" +#include "components/drive/file_system/operation_delegate.h" +#include "components/drive/file_system_core_util.h" +#include "components/drive/job_scheduler.h" +#include "components/drive/resource_metadata.h" + +namespace drive { +namespace file_system { + +namespace { + +FileError CreateDirectoryRecursively(internal::ResourceMetadata* metadata, + const std::string& parent_local_id, + const base::FilePath& relative_file_path, + std::set<std::string>* updated_local_ids, + FileChange* changed_files) { + // Split the first component and remaining ones of |relative_file_path|. + std::vector<base::FilePath::StringType> components; + relative_file_path.GetComponents(&components); + DCHECK(!components.empty()); + base::FilePath title(components[0]); + base::FilePath remaining_path; + title.AppendRelativePath(relative_file_path, &remaining_path); + + ResourceEntry entry; + const base::Time now = base::Time::Now(); + entry.set_title(title.AsUTF8Unsafe()); + entry.mutable_file_info()->set_is_directory(true); + entry.mutable_file_info()->set_last_modified(now.ToInternalValue()); + entry.mutable_file_info()->set_last_accessed(now.ToInternalValue()); + entry.set_parent_local_id(parent_local_id); + entry.set_metadata_edit_state(ResourceEntry::DIRTY); + entry.set_modification_date(base::Time::Now().ToInternalValue()); + + std::string local_id; + FileError error = metadata->AddEntry(entry, &local_id); + if (error != FILE_ERROR_OK) + return error; + + base::FilePath path; + error = metadata->GetFilePath(local_id, &path); + if (error != FILE_ERROR_OK) + return error; + + updated_local_ids->insert(local_id); + DCHECK(changed_files); + changed_files->Update(path, FileChange::FILE_TYPE_DIRECTORY, + FileChange::CHANGE_TYPE_ADD_OR_UPDATE); + + if (remaining_path.empty()) // All directories are created successfully. + return FILE_ERROR_OK; + + // Create descendant directories. + return CreateDirectoryRecursively( + metadata, local_id, remaining_path, updated_local_ids, changed_files); +} + +FileError UpdateLocalState(internal::ResourceMetadata* metadata, + const base::FilePath& directory_path, + bool is_exclusive, + bool is_recursive, + std::set<std::string>* updated_local_ids, + FileChange* changed_files) { + // Get the existing deepest entry. + std::vector<base::FilePath::StringType> components; + directory_path.GetComponents(&components); + + if (components.empty() || + components[0] != util::GetDriveGrandRootPath().value()) + return FILE_ERROR_NOT_FOUND; + + base::FilePath existing_deepest_path(components[0]); + std::string local_id = util::kDriveGrandRootLocalId; + for (size_t i = 1; i < components.size(); ++i) { + const std::string component = base::FilePath(components[i]).AsUTF8Unsafe(); + std::string child_local_id; + FileError error = + metadata->GetChildId(local_id, component, &child_local_id); + if (error == FILE_ERROR_NOT_FOUND) + break; + if (error != FILE_ERROR_OK) + return error; + existing_deepest_path = existing_deepest_path.Append(components[i]); + local_id = child_local_id; + } + + ResourceEntry entry; + FileError error = metadata->GetResourceEntryById(local_id, &entry); + if (error != FILE_ERROR_OK) + return error; + + if (!entry.file_info().is_directory()) + return FILE_ERROR_NOT_A_DIRECTORY; + + if (directory_path == existing_deepest_path) + return is_exclusive ? FILE_ERROR_EXISTS : FILE_ERROR_OK; + + // If it is not recursive creation, the found directory must be the direct + // parent of |directory_path| to ensure creating exact one directory. + if (!is_recursive && existing_deepest_path != directory_path.DirName()) + return FILE_ERROR_NOT_FOUND; + + // Create directories under the found directory. + base::FilePath remaining_path; + existing_deepest_path.AppendRelativePath(directory_path, &remaining_path); + return CreateDirectoryRecursively(metadata, + entry.local_id(), + remaining_path, + updated_local_ids, + changed_files); +} + +} // namespace + +CreateDirectoryOperation::CreateDirectoryOperation( + base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + internal::ResourceMetadata* metadata) + : blocking_task_runner_(blocking_task_runner), + delegate_(delegate), + metadata_(metadata), + weak_ptr_factory_(this) { +} + +CreateDirectoryOperation::~CreateDirectoryOperation() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +void CreateDirectoryOperation::CreateDirectory( + const base::FilePath& directory_path, + bool is_exclusive, + bool is_recursive, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + std::set<std::string>* updated_local_ids = new std::set<std::string>; + FileChange* changed_files(new FileChange); + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&UpdateLocalState, + metadata_, + directory_path, + is_exclusive, + is_recursive, + updated_local_ids, + changed_files), + base::Bind( + &CreateDirectoryOperation::CreateDirectoryAfterUpdateLocalState, + weak_ptr_factory_.GetWeakPtr(), + callback, + base::Owned(updated_local_ids), + base::Owned(changed_files))); +} + +void CreateDirectoryOperation::CreateDirectoryAfterUpdateLocalState( + const FileOperationCallback& callback, + const std::set<std::string>* updated_local_ids, + const FileChange* changed_files, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + for (const auto& id : *updated_local_ids) { + delegate_->OnEntryUpdatedByOperation(ClientContext(USER_INITIATED), id); + } + + delegate_->OnFileChangedByOperation(*changed_files); + + callback.Run(error); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/create_directory_operation.h b/components/drive/file_system/create_directory_operation.h new file mode 100644 index 0000000..8467db9 --- /dev/null +++ b/components/drive/file_system/create_directory_operation.h @@ -0,0 +1,79 @@ +// Copyright (c) 2013 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_CREATE_DIRECTORY_OPERATION_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_CREATE_DIRECTORY_OPERATION_H_ + +#include <set> + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "components/drive/file_errors.h" + +namespace base { +class FilePath; +class SequencedTaskRunner; +} // namespace base + +namespace drive { + +class FileChange; +class ResourceEntry; + +namespace internal { +class ResourceMetadata; +} // namespace internal + +namespace file_system { + +class OperationDelegate; + +// This class encapsulates the drive Create Directory function. It is +// responsible for sending the request to the drive API, then updating the +// local state and metadata to reflect the new state. +class CreateDirectoryOperation { + public: + CreateDirectoryOperation(base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + internal::ResourceMetadata* metadata); + ~CreateDirectoryOperation(); + + // Creates a new directory at |directory_path|. + // If |is_exclusive| is true, an error is raised in case a directory exists + // already at the |directory_path|. + // If |is_recursive| is true, the invocation creates parent directories as + // needed just like mkdir -p does. + // Invokes |callback| when finished with the result of the operation. + // |callback| must not be null. + void CreateDirectory(const base::FilePath& directory_path, + bool is_exclusive, + bool is_recursive, + const FileOperationCallback& callback); + + private: + // Part of CreateDirectory(). Called after UpdateLocalState(). + void CreateDirectoryAfterUpdateLocalState( + const FileOperationCallback& callback, + const std::set<std::string>* updated_local_ids, + const FileChange* changed_directories, + FileError error); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + OperationDelegate* delegate_; + internal::ResourceMetadata* metadata_; + + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate the weak pointers before any other members are destroyed. + base::WeakPtrFactory<CreateDirectoryOperation> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(CreateDirectoryOperation); +}; + +} // namespace file_system +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_CREATE_DIRECTORY_OPERATION_H_ diff --git a/components/drive/file_system/create_directory_operation_unittest.cc b/components/drive/file_system/create_directory_operation_unittest.cc new file mode 100644 index 0000000..df70157 --- /dev/null +++ b/components/drive/file_system/create_directory_operation_unittest.cc @@ -0,0 +1,126 @@ +// Copyright (c) 2013 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 "components/drive/file_system/create_directory_operation.h" + +#include "components/drive/file_system/operation_test_base.h" +#include "content/public/test/test_utils.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace file_system { + +class CreateDirectoryOperationTest : public OperationTestBase { + protected: + // Returns FILE_ERROR_OK if a directory is found at |path|. + FileError FindDirectory(const base::FilePath& path) { + ResourceEntry entry; + FileError error = GetLocalResourceEntry(path, &entry); + if (error == FILE_ERROR_OK && !entry.file_info().is_directory()) + error = FILE_ERROR_NOT_A_DIRECTORY; + return error; + } +}; + +TEST_F(CreateDirectoryOperationTest, CreateDirectory) { + CreateDirectoryOperation operation(blocking_task_runner(), + delegate(), + metadata()); + + const base::FilePath kExistingFile( + FILE_PATH_LITERAL("drive/root/File 1.txt")); + const base::FilePath kExistingDirectory( + FILE_PATH_LITERAL("drive/root/Directory 1")); + const base::FilePath kNewDirectory1( + FILE_PATH_LITERAL("drive/root/New Directory")); + const base::FilePath kNewDirectory2 = + kNewDirectory1.AppendASCII("New Directory 2/a/b/c"); + + // Create a new directory, not recursively. + EXPECT_EQ(FILE_ERROR_NOT_FOUND, FindDirectory(kNewDirectory1)); + + FileError error = FILE_ERROR_FAILED; + operation.CreateDirectory( + kNewDirectory1, + true, // is_exclusive + false, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_EQ(FILE_ERROR_OK, FindDirectory(kNewDirectory1)); + EXPECT_EQ(1U, delegate()->get_changed_files().size()); + EXPECT_EQ( + 1U, + delegate()->get_changed_files().CountDirectory(kNewDirectory1.DirName())); + + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kNewDirectory1, &entry)); + EXPECT_EQ(ResourceEntry::DIRTY, entry.metadata_edit_state()); + EXPECT_TRUE(entry.file_info().is_directory()); + EXPECT_FALSE(base::Time::FromInternalValue( + entry.file_info().last_modified()).is_null()); + EXPECT_FALSE(base::Time::FromInternalValue( + entry.file_info().last_accessed()).is_null()); + EXPECT_EQ(1U, delegate()->updated_local_ids().size()); + EXPECT_EQ(1U, delegate()->updated_local_ids().count(entry.local_id())); + + // Create a new directory recursively. + EXPECT_EQ(FILE_ERROR_NOT_FOUND, FindDirectory(kNewDirectory2)); + operation.CreateDirectory( + kNewDirectory2, + true, // is_exclusive + false, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, error); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, FindDirectory(kNewDirectory2)); + + operation.CreateDirectory( + kNewDirectory2, + true, // is_exclusive + true, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_EQ(FILE_ERROR_OK, FindDirectory(kNewDirectory2)); + + // Try to create an existing directory. + operation.CreateDirectory( + kExistingDirectory, + true, // is_exclusive + false, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_EXISTS, error); + + operation.CreateDirectory( + kExistingDirectory, + false, // is_exclusive + false, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Try to create a directory with a path for an existing file. + operation.CreateDirectory( + kExistingFile, + false, // is_exclusive + true, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_A_DIRECTORY, error); + + // Try to create a directory under a file. + operation.CreateDirectory( + kExistingFile.AppendASCII("New Directory"), + false, // is_exclusive + true, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_A_DIRECTORY, error); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/create_file_operation.cc b/components/drive/file_system/create_file_operation.cc new file mode 100644 index 0000000..19a238e --- /dev/null +++ b/components/drive/file_system/create_file_operation.cc @@ -0,0 +1,139 @@ +// Copyright 2013 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 "components/drive/file_system/create_file_operation.h" + +#include <string> + +#include "base/files/file_util.h" +#include "components/drive/drive.pb.h" +#include "components/drive/file_change.h" +#include "components/drive/file_system/operation_delegate.h" +#include "components/drive/job_scheduler.h" +#include "components/drive/resource_metadata.h" +#include "net/base/mime_util.h" + +namespace drive { +namespace file_system { + +namespace { + +const char kMimeTypeOctetStream[] = "application/octet-stream"; + +// Updates local state. +FileError UpdateLocalState(internal::ResourceMetadata* metadata, + const base::FilePath& file_path, + const std::string& mime_type_in, + ResourceEntry* entry) { + DCHECK(metadata); + + FileError error = metadata->GetResourceEntryByPath(file_path, entry); + if (error == FILE_ERROR_OK) + return FILE_ERROR_EXISTS; + + if (error != FILE_ERROR_NOT_FOUND) + return error; + + // If parent path is not a directory, it is an error. + ResourceEntry parent; + if (metadata->GetResourceEntryByPath( + file_path.DirName(), &parent) != FILE_ERROR_OK || + !parent.file_info().is_directory()) + return FILE_ERROR_NOT_A_DIRECTORY; + + // If mime_type is not set or "application/octet-stream", guess from the + // |file_path|. If it is still unsure, use octet-stream by default. + std::string mime_type = mime_type_in; + if ((mime_type.empty() || mime_type == kMimeTypeOctetStream) && + !net::GetMimeTypeFromFile(file_path, &mime_type)) + mime_type = kMimeTypeOctetStream; + + // Add the entry to the local resource metadata. + const base::Time now = base::Time::Now(); + entry->mutable_file_info()->set_last_modified(now.ToInternalValue()); + entry->mutable_file_info()->set_last_accessed(now.ToInternalValue()); + entry->set_title(file_path.BaseName().AsUTF8Unsafe()); + entry->set_parent_local_id(parent.local_id()); + entry->set_metadata_edit_state(ResourceEntry::DIRTY); + entry->set_modification_date(base::Time::Now().ToInternalValue()); + entry->mutable_file_specific_info()->set_content_mime_type(mime_type); + + std::string local_id; + error = metadata->AddEntry(*entry, &local_id); + entry->set_local_id(local_id); + return error; +} + +} // namespace + +CreateFileOperation::CreateFileOperation( + base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + internal::ResourceMetadata* metadata) + : blocking_task_runner_(blocking_task_runner), + delegate_(delegate), + metadata_(metadata), + weak_ptr_factory_(this) { +} + +CreateFileOperation::~CreateFileOperation() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +void CreateFileOperation::CreateFile(const base::FilePath& file_path, + bool is_exclusive, + const std::string& mime_type, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + ResourceEntry* entry = new ResourceEntry; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&UpdateLocalState, + metadata_, + file_path, + mime_type, + entry), + base::Bind(&CreateFileOperation::CreateFileAfterUpdateLocalState, + weak_ptr_factory_.GetWeakPtr(), + callback, + file_path, + is_exclusive, + base::Owned(entry))); +} + +void CreateFileOperation::CreateFileAfterUpdateLocalState( + const FileOperationCallback& callback, + const base::FilePath& file_path, + bool is_exclusive, + ResourceEntry* entry, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error == FILE_ERROR_EXISTS) { + // Error if an exclusive mode is requested, or the entry is not a file. + error = (is_exclusive || + entry->file_info().is_directory() || + entry->file_specific_info().is_hosted_document()) ? + FILE_ERROR_EXISTS : FILE_ERROR_OK; + } else if (error == FILE_ERROR_OK) { + DCHECK(!entry->file_info().is_directory()); + + // Notify delegate if the file was newly created. + FileChange changed_file; + changed_file.Update(file_path, FileChange::FILE_TYPE_FILE, + FileChange::CHANGE_TYPE_ADD_OR_UPDATE); + delegate_->OnFileChangedByOperation(changed_file); + // Synchronize in the background. + delegate_->OnEntryUpdatedByOperation(ClientContext(BACKGROUND), + entry->local_id()); + } + callback.Run(error); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/create_file_operation.h b/components/drive/file_system/create_file_operation.h new file mode 100644 index 0000000..b43132f --- /dev/null +++ b/components/drive/file_system/create_file_operation.h @@ -0,0 +1,76 @@ +// Copyright 2013 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_CREATE_FILE_OPERATION_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_CREATE_FILE_OPERATION_H_ + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "components/drive/file_errors.h" + +namespace base { +class FilePath; +class SequencedTaskRunner; +} // namespace base + +namespace drive { + +namespace internal { +class ResourceMetadata; +} // namespace internal + +class ResourceEntry; + +namespace file_system { + +class OperationDelegate; + +// This class encapsulates the drive CreateFile function. It is responsible for +// sending the request to the drive API, then updating the local state and +// metadata to reflect the new state. +class CreateFileOperation { + public: + CreateFileOperation(base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + internal::ResourceMetadata* metadata); + ~CreateFileOperation(); + + // Creates an empty file at |file_path|. When the file + // already exists at that path, the operation fails if |is_exclusive| is true, + // and it succeeds without doing anything if the flag is false. + // If |mime_type| is non-empty, it is used as the mime type of the entry. If + // the parameter is empty, the type is guessed from |file_path|. + // + // |callback| must not be null. + void CreateFile(const base::FilePath& file_path, + bool is_exclusive, + const std::string& mime_type, + const FileOperationCallback& callback); + + private: + // Part of CreateFile(). Called after the updating local state is completed. + void CreateFileAfterUpdateLocalState(const FileOperationCallback& callback, + const base::FilePath& file_path, + bool is_exclusive, + ResourceEntry* entry, + FileError error); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + OperationDelegate* delegate_; + internal::ResourceMetadata* metadata_; + + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate the weak pointers before any other members are destroyed. + base::WeakPtrFactory<CreateFileOperation> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(CreateFileOperation); +}; + +} // namespace file_system +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_CREATE_FILE_OPERATION_H_ diff --git a/components/drive/file_system/create_file_operation_unittest.cc b/components/drive/file_system/create_file_operation_unittest.cc new file mode 100644 index 0000000..79b4456 --- /dev/null +++ b/components/drive/file_system/create_file_operation_unittest.cc @@ -0,0 +1,161 @@ +// Copyright 2013 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 "components/drive/file_system/create_file_operation.h" + +#include "components/drive/file_change.h" +#include "components/drive/file_system/operation_test_base.h" +#include "content/public/test/test_utils.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace file_system { + +typedef OperationTestBase CreateFileOperationTest; + +TEST_F(CreateFileOperationTest, CreateFile) { + CreateFileOperation operation(blocking_task_runner(), + delegate(), + metadata()); + + const base::FilePath kFilePath(FILE_PATH_LITERAL("drive/root/New File.txt")); + FileError error = FILE_ERROR_FAILED; + operation.CreateFile( + kFilePath, + true, // is_exclusive + std::string(), // no predetermined mime type + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kFilePath, &entry)); + EXPECT_EQ(ResourceEntry::DIRTY, entry.metadata_edit_state()); + EXPECT_FALSE(base::Time::FromInternalValue( + entry.file_info().last_modified()).is_null()); + EXPECT_FALSE(base::Time::FromInternalValue( + entry.file_info().last_accessed()).is_null()); + + EXPECT_EQ(1u, delegate()->get_changed_files().size()); + EXPECT_EQ(1u, delegate()->get_changed_files().count(kFilePath)); + EXPECT_EQ(1u, delegate()->updated_local_ids().size()); + EXPECT_EQ(1u, delegate()->updated_local_ids().count(entry.local_id())); +} + +TEST_F(CreateFileOperationTest, CreateFileIsExclusive) { + CreateFileOperation operation(blocking_task_runner(), + delegate(), + metadata()); + + const base::FilePath kExistingFile( + FILE_PATH_LITERAL("drive/root/File 1.txt")); + const base::FilePath kExistingDirectory( + FILE_PATH_LITERAL("drive/root/Directory 1")); + const base::FilePath kNonExistingFile( + FILE_PATH_LITERAL("drive/root/Directory 1/not exist.png")); + const base::FilePath kFileInNonExistingDirectory( + FILE_PATH_LITERAL("drive/root/not exist/not exist.png")); + + // Create fails if is_exclusive = true and a file exists. + FileError error = FILE_ERROR_FAILED; + operation.CreateFile( + kExistingFile, + true, // is_exclusive + std::string(), // no predetermined mime type + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_EXISTS, error); + + // Create succeeds if is_exclusive = false and a file exists. + operation.CreateFile( + kExistingFile, + false, // is_exclusive + std::string(), // no predetermined mime type + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Create fails if a directory existed even when is_exclusive = false. + operation.CreateFile( + kExistingDirectory, + false, // is_exclusive + std::string(), // no predetermined mime type + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_EXISTS, error); + + // Create succeeds if no entry exists. + operation.CreateFile( + kNonExistingFile, + true, // is_exclusive + std::string(), // no predetermined mime type + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Create fails if the parent directory does not exist. + operation.CreateFile( + kFileInNonExistingDirectory, + false, // is_exclusive + std::string(), // no predetermined mime type + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_A_DIRECTORY, error); +} + +TEST_F(CreateFileOperationTest, CreateFileMimeType) { + CreateFileOperation operation(blocking_task_runner(), + delegate(), + metadata()); + + const base::FilePath kPng1(FILE_PATH_LITERAL("drive/root/1.png")); + const base::FilePath kPng2(FILE_PATH_LITERAL("drive/root/2.png")); + const base::FilePath kUnknown(FILE_PATH_LITERAL("drive/root/3.unknown")); + const std::string kSpecialMimeType("application/x-createfile-test"); + + FileError error = FILE_ERROR_FAILED; + operation.CreateFile( + kPng1, + false, // is_exclusive + std::string(), // no predetermined mime type + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // If no mime type is specified, it is guessed from the file name. + ResourceEntry entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kPng1, &entry)); + EXPECT_EQ("image/png", entry.file_specific_info().content_mime_type()); + + error = FILE_ERROR_FAILED; + operation.CreateFile( + kPng2, + false, // is_exclusive + kSpecialMimeType, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // If the mime type is explicitly set, respect it. + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kPng2, &entry)); + EXPECT_EQ(kSpecialMimeType, entry.file_specific_info().content_mime_type()); + + error = FILE_ERROR_FAILED; + operation.CreateFile( + kUnknown, + false, // is_exclusive + std::string(), // no predetermined mime type + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // If the mime type is not set and unknown, default to octet-stream. + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kUnknown, &entry)); + EXPECT_EQ("application/octet-stream", + entry.file_specific_info().content_mime_type()); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/download_operation.cc b/components/drive/file_system/download_operation.cc new file mode 100644 index 0000000..150009c --- /dev/null +++ b/components/drive/file_system/download_operation.cc @@ -0,0 +1,540 @@ +// Copyright 2013 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 "components/drive/file_system/download_operation.h" + +#include "base/callback_helpers.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/logging.h" +#include "base/task_runner_util.h" +#include "components/drive/drive.pb.h" +#include "components/drive/file_cache.h" +#include "components/drive/file_change.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system/operation_delegate.h" +#include "components/drive/file_system_core_util.h" +#include "components/drive/job_scheduler.h" +#include "components/drive/resource_metadata.h" +#include "google_apis/drive/drive_api_error_codes.h" + +namespace drive { +namespace file_system { +namespace { + +// Generates an unused file path with |extension| to |out_path|, as a descendant +// of |dir|, with its parent directory created. +bool GeneratesUniquePathWithExtension( + const base::FilePath& dir, + const base::FilePath::StringType& extension, + base::FilePath* out_path) { + base::FilePath subdir; + if (!base::CreateTemporaryDirInDir(dir, base::FilePath::StringType(), + &subdir)) { + return false; + } + *out_path = subdir.Append(FILE_PATH_LITERAL("tmp") + extension); + return true; +} + +// Prepares for downloading the file. Allocates the enough space for the file +// in the cache. +// If succeeded, returns FILE_ERROR_OK with |temp_download_file| storing the +// path to the file in the cache. +FileError PrepareForDownloadFile(internal::FileCache* cache, + int64 expected_file_size, + const base::FilePath& temporary_file_directory, + base::FilePath* temp_download_file) { + DCHECK(cache); + DCHECK(temp_download_file); + + // Ensure enough space in the cache. + if (!cache->FreeDiskSpaceIfNeededFor(expected_file_size)) + return FILE_ERROR_NO_LOCAL_SPACE; + + return base::CreateTemporaryFileInDir( + temporary_file_directory, + temp_download_file) ? FILE_ERROR_OK : FILE_ERROR_FAILED; +} + +// If the resource is a hosted document, creates a JSON file representing the +// resource locally, and returns FILE_ERROR_OK with |cache_file_path| storing +// the path to the JSON file. +// If the resource is a regular file and its local cache is available, +// returns FILE_ERROR_OK with |cache_file_path| storing the path to the +// cache file. +// If the resource is a regular file but its local cache is NOT available, +// returns FILE_ERROR_OK, but |cache_file_path| is kept empty. +// Otherwise returns error code. +FileError CheckPreConditionForEnsureFileDownloaded( + internal::ResourceMetadata* metadata, + internal::FileCache* cache, + const base::FilePath& temporary_file_directory, + const std::string& local_id, + ResourceEntry* entry, + base::FilePath* cache_file_path, + base::FilePath* temp_download_file_path) { + DCHECK(metadata); + DCHECK(cache); + DCHECK(cache_file_path); + + FileError error = metadata->GetResourceEntryById(local_id, entry); + if (error != FILE_ERROR_OK) + return error; + + if (entry->file_info().is_directory()) + return FILE_ERROR_NOT_A_FILE; + + // For a hosted document, we create a special JSON file to represent the + // document instead of fetching the document content in one of the exported + // formats. The JSON file contains the edit URL and resource ID of the + // document. + if (entry->file_specific_info().is_hosted_document()) { + base::FilePath::StringType extension = base::FilePath::FromUTF8Unsafe( + entry->file_specific_info().document_extension()).value(); + base::FilePath gdoc_file_path; + base::File::Info file_info; + // We add the gdoc file extension in the temporary file, so that in cross + // profile drag-and-drop between Drive folders, the destination profiles's + // CopyOperation can detect the special JSON file only by the path. + if (!GeneratesUniquePathWithExtension(temporary_file_directory, + extension, + &gdoc_file_path) || + !util::CreateGDocFile(gdoc_file_path, + GURL(entry->file_specific_info().alternate_url()), + entry->resource_id()) || + !base::GetFileInfo(gdoc_file_path, + reinterpret_cast<base::File::Info*>(&file_info))) + return FILE_ERROR_FAILED; + + *cache_file_path = gdoc_file_path; + entry->mutable_file_info()->set_size(file_info.size); + return FILE_ERROR_OK; + } + + if (!entry->file_specific_info().cache_state().is_present()) { + // This file has no cache file. + if (!entry->resource_id().empty()) { + // This entry exists on the server, leave |cache_file_path| empty to + // start download. + return PrepareForDownloadFile(cache, entry->file_info().size(), + temporary_file_directory, + temp_download_file_path); + } + + // This entry does not exist on the server, store an empty file and mark it + // as dirty. + base::FilePath empty_file; + if (!base::CreateTemporaryFileInDir(temporary_file_directory, &empty_file)) + return FILE_ERROR_FAILED; + error = cache->Store(local_id, std::string(), empty_file, + internal::FileCache::FILE_OPERATION_MOVE); + if (error != FILE_ERROR_OK) + return error; + + error = metadata->GetResourceEntryById(local_id, entry); + if (error != FILE_ERROR_OK) + return error; + } + + // Leave |cache_file_path| empty when the stored file is obsolete and has no + // local modification. + if (!entry->file_specific_info().cache_state().is_dirty() && + entry->file_specific_info().md5() != + entry->file_specific_info().cache_state().md5()) { + return PrepareForDownloadFile(cache, entry->file_info().size(), + temporary_file_directory, + temp_download_file_path); + } + + // Fill |cache_file_path| with the path to the cached file. + error = cache->GetFile(local_id, cache_file_path); + if (error != FILE_ERROR_OK) + return error; + + // If the cache file is to be returned as the download result, the file info + // of the cache needs to be returned via |entry|. + // TODO(kinaba): crbug.com/246469. The logic below is similar to that in + // drive::FileSystem::CheckLocalModificationAndRun. We should merge them. + base::File::Info file_info; + if (base::GetFileInfo(*cache_file_path, &file_info)) + entry->mutable_file_info()->set_size(file_info.size); + + return FILE_ERROR_OK; +} + +struct CheckPreconditionForEnsureFileDownloadedParams { + internal::ResourceMetadata* metadata; + internal::FileCache* cache; + base::FilePath temporary_file_directory; +}; + +// Calls CheckPreConditionForEnsureFileDownloaded() with the entry specified by +// the given ID. Also fills |drive_file_path| with the path of the entry. +FileError CheckPreConditionForEnsureFileDownloadedByLocalId( + const CheckPreconditionForEnsureFileDownloadedParams& params, + const std::string& local_id, + base::FilePath* drive_file_path, + base::FilePath* cache_file_path, + base::FilePath* temp_download_file_path, + ResourceEntry* entry) { + FileError error = params.metadata->GetFilePath(local_id, drive_file_path); + if (error != FILE_ERROR_OK) + return error; + return CheckPreConditionForEnsureFileDownloaded( + params.metadata, params.cache, params.temporary_file_directory, local_id, + entry, cache_file_path, temp_download_file_path); +} + +// Calls CheckPreConditionForEnsureFileDownloaded() with the entry specified by +// the given file path. +FileError CheckPreConditionForEnsureFileDownloadedByPath( + const CheckPreconditionForEnsureFileDownloadedParams& params, + const base::FilePath& file_path, + base::FilePath* cache_file_path, + base::FilePath* temp_download_file_path, + ResourceEntry* entry) { + std::string local_id; + FileError error = params.metadata->GetIdByPath(file_path, &local_id); + if (error != FILE_ERROR_OK) + return error; + return CheckPreConditionForEnsureFileDownloaded( + params.metadata, params.cache, params.temporary_file_directory, local_id, + entry, cache_file_path, temp_download_file_path); +} + +// Stores the downloaded file at |downloaded_file_path| into |cache|. +// If succeeded, returns FILE_ERROR_OK with |cache_file_path| storing the +// path to the cache file. +// If failed, returns an error code with deleting |downloaded_file_path|. +FileError UpdateLocalStateForDownloadFile( + internal::ResourceMetadata* metadata, + internal::FileCache* cache, + const ResourceEntry& entry_before_download, + google_apis::DriveApiErrorCode gdata_error, + const base::FilePath& downloaded_file_path, + ResourceEntry* entry_after_update, + base::FilePath* cache_file_path) { + DCHECK(cache); + + // Downloaded file should be deleted on errors. + base::ScopedClosureRunner file_deleter(base::Bind( + base::IgnoreResult(&base::DeleteFile), + downloaded_file_path, false /* recursive */)); + + FileError error = GDataToFileError(gdata_error); + if (error != FILE_ERROR_OK) + return error; + + const std::string& local_id = entry_before_download.local_id(); + + // Do not overwrite locally edited file with server side contents. + ResourceEntry entry; + error = metadata->GetResourceEntryById(local_id, &entry); + if (error != FILE_ERROR_OK) + return error; + if (entry.file_specific_info().cache_state().is_dirty()) + return FILE_ERROR_IN_USE; + + // Here the download is completed successfully, so store it into the cache. + error = cache->Store(local_id, + entry_before_download.file_specific_info().md5(), + downloaded_file_path, + internal::FileCache::FILE_OPERATION_MOVE); + if (error != FILE_ERROR_OK) + return error; + base::Closure unused_file_deleter_closure = file_deleter.Release(); + + error = metadata->GetResourceEntryById(local_id, entry_after_update); + if (error != FILE_ERROR_OK) + return error; + + return cache->GetFile(local_id, cache_file_path); +} + +} // namespace + +class DownloadOperation::DownloadParams { + public: + DownloadParams( + const GetFileContentInitializedCallback initialized_callback, + const google_apis::GetContentCallback get_content_callback, + const GetFileCallback completion_callback, + scoped_ptr<ResourceEntry> entry) + : initialized_callback_(initialized_callback), + get_content_callback_(get_content_callback), + completion_callback_(completion_callback), + entry_(entry.Pass()), + was_cancelled_(false), + weak_ptr_factory_(this) { + DCHECK(!completion_callback_.is_null()); + DCHECK(entry_); + } + + base::Closure GetCancelClosure() { + return base::Bind(&DownloadParams::Cancel, weak_ptr_factory_.GetWeakPtr()); + } + + void OnCacheFileFound(const base::FilePath& cache_file_path) { + if (!initialized_callback_.is_null()) { + initialized_callback_.Run(FILE_ERROR_OK, cache_file_path, + make_scoped_ptr(new ResourceEntry(*entry_))); + } + completion_callback_.Run(FILE_ERROR_OK, cache_file_path, entry_.Pass()); + } + + void OnStartDownloading(const base::Closure& cancel_download_closure) { + cancel_download_closure_ = cancel_download_closure; + if (initialized_callback_.is_null()) { + return; + } + + DCHECK(entry_); + initialized_callback_.Run(FILE_ERROR_OK, base::FilePath(), + make_scoped_ptr(new ResourceEntry(*entry_))); + } + + void OnError(FileError error) const { + completion_callback_.Run( + error, base::FilePath(), scoped_ptr<ResourceEntry>()); + } + + void OnDownloadCompleted(const base::FilePath& cache_file_path, + scoped_ptr<ResourceEntry> entry) const { + completion_callback_.Run(FILE_ERROR_OK, cache_file_path, entry.Pass()); + } + + const google_apis::GetContentCallback& get_content_callback() const { + return get_content_callback_; + } + + const ResourceEntry& entry() const { return *entry_; } + + bool was_cancelled() const { return was_cancelled_; } + + private: + void Cancel() { + was_cancelled_ = true; + if (!cancel_download_closure_.is_null()) + cancel_download_closure_.Run(); + } + + const GetFileContentInitializedCallback initialized_callback_; + const google_apis::GetContentCallback get_content_callback_; + const GetFileCallback completion_callback_; + + scoped_ptr<ResourceEntry> entry_; + base::Closure cancel_download_closure_; + bool was_cancelled_; + + base::WeakPtrFactory<DownloadParams> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(DownloadParams); +}; + +DownloadOperation::DownloadOperation( + base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + JobScheduler* scheduler, + internal::ResourceMetadata* metadata, + internal::FileCache* cache, + const base::FilePath& temporary_file_directory) + : blocking_task_runner_(blocking_task_runner), + delegate_(delegate), + scheduler_(scheduler), + metadata_(metadata), + cache_(cache), + temporary_file_directory_(temporary_file_directory), + weak_ptr_factory_(this) { +} + +DownloadOperation::~DownloadOperation() { +} + +base::Closure DownloadOperation::EnsureFileDownloadedByLocalId( + const std::string& local_id, + const ClientContext& context, + const GetFileContentInitializedCallback& initialized_callback, + const google_apis::GetContentCallback& get_content_callback, + const GetFileCallback& completion_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!completion_callback.is_null()); + + CheckPreconditionForEnsureFileDownloadedParams params; + params.metadata = metadata_; + params.cache = cache_; + params.temporary_file_directory = temporary_file_directory_; + base::FilePath* drive_file_path = new base::FilePath; + base::FilePath* cache_file_path = new base::FilePath; + base::FilePath* temp_download_file_path = new base::FilePath; + ResourceEntry* entry = new ResourceEntry; + scoped_ptr<DownloadParams> download_params(new DownloadParams( + initialized_callback, get_content_callback, completion_callback, + make_scoped_ptr(entry))); + base::Closure cancel_closure = download_params->GetCancelClosure(); + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&CheckPreConditionForEnsureFileDownloadedByLocalId, + params, + local_id, + drive_file_path, + cache_file_path, + temp_download_file_path, + entry), + base::Bind(&DownloadOperation::EnsureFileDownloadedAfterCheckPreCondition, + weak_ptr_factory_.GetWeakPtr(), + base::Passed(&download_params), + context, + base::Owned(drive_file_path), + base::Owned(cache_file_path), + base::Owned(temp_download_file_path))); + return cancel_closure; +} + +base::Closure DownloadOperation::EnsureFileDownloadedByPath( + const base::FilePath& file_path, + const ClientContext& context, + const GetFileContentInitializedCallback& initialized_callback, + const google_apis::GetContentCallback& get_content_callback, + const GetFileCallback& completion_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!completion_callback.is_null()); + + CheckPreconditionForEnsureFileDownloadedParams params; + params.metadata = metadata_; + params.cache = cache_; + params.temporary_file_directory = temporary_file_directory_; + base::FilePath* drive_file_path = new base::FilePath(file_path); + base::FilePath* cache_file_path = new base::FilePath; + base::FilePath* temp_download_file_path = new base::FilePath; + ResourceEntry* entry = new ResourceEntry; + scoped_ptr<DownloadParams> download_params(new DownloadParams( + initialized_callback, get_content_callback, completion_callback, + make_scoped_ptr(entry))); + base::Closure cancel_closure = download_params->GetCancelClosure(); + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&CheckPreConditionForEnsureFileDownloadedByPath, + params, + file_path, + cache_file_path, + temp_download_file_path, + entry), + base::Bind(&DownloadOperation::EnsureFileDownloadedAfterCheckPreCondition, + weak_ptr_factory_.GetWeakPtr(), + base::Passed(&download_params), + context, + base::Owned(drive_file_path), + base::Owned(cache_file_path), + base::Owned(temp_download_file_path))); + return cancel_closure; +} + +void DownloadOperation::EnsureFileDownloadedAfterCheckPreCondition( + scoped_ptr<DownloadParams> params, + const ClientContext& context, + base::FilePath* drive_file_path, + base::FilePath* cache_file_path, + base::FilePath* temp_download_file_path, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(params); + DCHECK(drive_file_path); + DCHECK(cache_file_path); + + if (error != FILE_ERROR_OK) { + // During precondition check, an error is found. + params->OnError(error); + return; + } + + if (!cache_file_path->empty()) { + // The cache file is found. + params->OnCacheFileFound(*cache_file_path); + return; + } + + if (params->was_cancelled()) { + params->OnError(FILE_ERROR_ABORT); + return; + } + + DCHECK(!params->entry().resource_id().empty()); + DownloadParams* params_ptr = params.get(); + JobID id = scheduler_->DownloadFile( + *drive_file_path, + params_ptr->entry().file_info().size(), + *temp_download_file_path, + params_ptr->entry().resource_id(), + context, + base::Bind(&DownloadOperation::EnsureFileDownloadedAfterDownloadFile, + weak_ptr_factory_.GetWeakPtr(), + *drive_file_path, + base::Passed(¶ms)), + params_ptr->get_content_callback()); + + // Notify via |initialized_callback| if necessary. + params_ptr->OnStartDownloading( + base::Bind(&DownloadOperation::CancelJob, + weak_ptr_factory_.GetWeakPtr(), id)); +} + +void DownloadOperation::EnsureFileDownloadedAfterDownloadFile( + const base::FilePath& drive_file_path, + scoped_ptr<DownloadParams> params, + google_apis::DriveApiErrorCode gdata_error, + const base::FilePath& downloaded_file_path) { + DCHECK(thread_checker_.CalledOnValidThread()); + + DownloadParams* params_ptr = params.get(); + ResourceEntry* entry_after_update = new ResourceEntry; + base::FilePath* cache_file_path = new base::FilePath; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&UpdateLocalStateForDownloadFile, + metadata_, + cache_, + params_ptr->entry(), + gdata_error, + downloaded_file_path, + entry_after_update, + cache_file_path), + base::Bind(&DownloadOperation::EnsureFileDownloadedAfterUpdateLocalState, + weak_ptr_factory_.GetWeakPtr(), + drive_file_path, + base::Passed(¶ms), + base::Passed(make_scoped_ptr(entry_after_update)), + base::Owned(cache_file_path))); +} + +void DownloadOperation::EnsureFileDownloadedAfterUpdateLocalState( + const base::FilePath& file_path, + scoped_ptr<DownloadParams> params, + scoped_ptr<ResourceEntry> entry_after_update, + base::FilePath* cache_file_path, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (error != FILE_ERROR_OK) { + params->OnError(error); + return; + } + DCHECK(!entry_after_update->file_info().is_directory()); + + FileChange changed_files; + changed_files.Update(file_path, FileChange::FILE_TYPE_FILE, + FileChange::CHANGE_TYPE_ADD_OR_UPDATE); + // Storing to cache changes the "offline available" status, hence notify. + delegate_->OnFileChangedByOperation(changed_files); + params->OnDownloadCompleted(*cache_file_path, entry_after_update.Pass()); +} + +void DownloadOperation::CancelJob(JobID job_id) { + scheduler_->CancelJob(job_id); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/download_operation.h b/components/drive/file_system/download_operation.h new file mode 100644 index 0000000..e45902b --- /dev/null +++ b/components/drive/file_system/download_operation.h @@ -0,0 +1,133 @@ +// Copyright 2013 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_DOWNLOAD_OPERATION_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_DOWNLOAD_OPERATION_H_ + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "base/threading/thread_checker.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system_interface.h" +#include "components/drive/job_list.h" +#include "google_apis/drive/drive_api_error_codes.h" + +namespace base { +class FilePath; +class SequencedTaskRunner; +} // namespace base + +namespace google_apis { +class ResourceEntry; +} // namespace google_apis + +namespace drive { + +class JobScheduler; +class ResourceEntry; +struct ClientContext; + +namespace internal { +class FileCache; +class ResourceMetadata; +} // namespace internal + +namespace file_system { + +class OperationDelegate; + +class DownloadOperation { + public: + DownloadOperation(base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + JobScheduler* scheduler, + internal::ResourceMetadata* metadata, + internal::FileCache* cache, + const base::FilePath& temporary_file_directory); + ~DownloadOperation(); + + // Ensures that the file content specified by |local_id| is locally + // downloaded and returns a closure to cancel the task. + // For hosted documents, this method may create a JSON file representing the + // file. + // For regular files, if the locally cached file is found, returns it. + // If not found, start to download the file from the server. + // When a JSON file is created, the cache file is found or downloading is + // being started, |initialized_callback| is called with |local_file| + // for JSON file or the cache file, or with |cancel_download_closure| for + // downloading. + // During the downloading |get_content_callback| will be called periodically + // with the downloaded content. + // Upon completion or an error is found, |completion_callback| will be called. + // |initialized_callback| and |get_content_callback| can be null if not + // needed. + // |completion_callback| must not be null. + base::Closure EnsureFileDownloadedByLocalId( + const std::string& local_id, + const ClientContext& context, + const GetFileContentInitializedCallback& initialized_callback, + const google_apis::GetContentCallback& get_content_callback, + const GetFileCallback& completion_callback); + + // Does the same thing as EnsureFileDownloadedByLocalId for the file + // specified by |file_path|. + base::Closure EnsureFileDownloadedByPath( + const base::FilePath& file_path, + const ClientContext& context, + const GetFileContentInitializedCallback& initialized_callback, + const google_apis::GetContentCallback& get_content_callback, + const GetFileCallback& completion_callback); + + private: + // Parameters for EnsureFileDownloaded. + class DownloadParams; + + // Part of EnsureFileDownloaded(). Called upon the completion of precondition + // check. + void EnsureFileDownloadedAfterCheckPreCondition( + scoped_ptr<DownloadParams> params, + const ClientContext& context, + base::FilePath* drive_file_path, + base::FilePath* cache_file_path, + base::FilePath* temp_download_file_path, + FileError error); + + // Part of EnsureFileDownloaded(). Called after the actual downloading. + void EnsureFileDownloadedAfterDownloadFile( + const base::FilePath& drive_file_path, + scoped_ptr<DownloadParams> params, + google_apis::DriveApiErrorCode gdata_error, + const base::FilePath& downloaded_file_path); + + // Part of EnsureFileDownloaded(). Called after updating local state is + // completed. + void EnsureFileDownloadedAfterUpdateLocalState( + const base::FilePath& file_path, + scoped_ptr<DownloadParams> params, + scoped_ptr<ResourceEntry> entry_after_update, + base::FilePath* cache_file_path, + FileError error); + + // Cancels the job with |job_id| in the scheduler. + void CancelJob(JobID job_id); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + OperationDelegate* delegate_; + JobScheduler* scheduler_; + internal::ResourceMetadata* metadata_; + internal::FileCache* cache_; + const base::FilePath temporary_file_directory_; + + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate its weak pointers before any other members are destroyed. + base::WeakPtrFactory<DownloadOperation> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(DownloadOperation); +}; + +} // namespace file_system +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_DOWNLOAD_OPERATION_H_ diff --git a/components/drive/file_system/download_operation_unittest.cc b/components/drive/file_system/download_operation_unittest.cc new file mode 100644 index 0000000..b4dcf53 --- /dev/null +++ b/components/drive/file_system/download_operation_unittest.cc @@ -0,0 +1,504 @@ +// Copyright 2013 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 "components/drive/file_system/download_operation.h" + +#include "base/files/file_util.h" +#include "base/task_runner_util.h" +#include "components/drive/fake_free_disk_space_getter.h" +#include "components/drive/file_cache.h" +#include "components/drive/file_change.h" +#include "components/drive/file_system/operation_test_base.h" +#include "components/drive/file_system_core_util.h" +#include "components/drive/job_scheduler.h" +#include "components/drive/service/fake_drive_service.h" +#include "content/public/test/test_utils.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace file_system { + +class DownloadOperationTest : public OperationTestBase { + protected: + void SetUp() override { + OperationTestBase::SetUp(); + + operation_.reset(new DownloadOperation( + blocking_task_runner(), delegate(), scheduler(), metadata(), cache(), + temp_dir())); + } + + scoped_ptr<DownloadOperation> operation_; +}; + +TEST_F(DownloadOperationTest, + EnsureFileDownloadedByPath_FromServer_EnoughSpace) { + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &src_entry)); + const int64 file_size = src_entry.file_info().size(); + + // Pretend we have enough space. + fake_free_disk_space_getter()->set_default_value( + file_size + drive::internal::kMinFreeSpaceInBytes); + + FileError error = FILE_ERROR_FAILED; + base::FilePath file_path; + scoped_ptr<ResourceEntry> entry; + operation_->EnsureFileDownloadedByPath( + file_in_root, + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(entry); + EXPECT_FALSE(entry->file_specific_info().is_hosted_document()); + + // The transfered file is cached and the change of "offline available" + // attribute is notified. + EXPECT_EQ(1U, delegate()->get_changed_files().size()); + EXPECT_EQ(1U, delegate()->get_changed_files().count(file_in_root)); +} + +TEST_F(DownloadOperationTest, + EnsureFileDownloadedByPath_FromServer_NoSpaceAtAll) { + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + + // Pretend we have no space at all. + fake_free_disk_space_getter()->set_default_value(0); + + FileError error = FILE_ERROR_OK; + base::FilePath file_path; + scoped_ptr<ResourceEntry> entry; + operation_->EnsureFileDownloadedByPath( + file_in_root, + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_NO_LOCAL_SPACE, error); +} + +TEST_F(DownloadOperationTest, + EnsureFileDownloadedByPath_FromServer_NoEnoughSpaceButCanFreeUp) { + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &src_entry)); + const int64 file_size = src_entry.file_info().size(); + + // Make another file cached. + // This file's cache file will be removed to free up the disk space. + base::FilePath cached_file( + FILE_PATH_LITERAL("drive/root/Duplicate Name.txt")); + FileError error = FILE_ERROR_FAILED; + base::FilePath file_path; + scoped_ptr<ResourceEntry> entry; + operation_->EnsureFileDownloadedByPath( + cached_file, + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(entry); + EXPECT_TRUE(entry->file_specific_info().cache_state().is_present()); + + // Pretend we have no space first (checked before downloading a file), + // but then start reporting we have space. This is to emulate that + // the disk space was freed up by removing temporary files. + fake_free_disk_space_getter()->set_default_value( + file_size + drive::internal::kMinFreeSpaceInBytes); + fake_free_disk_space_getter()->PushFakeValue(0); + + operation_->EnsureFileDownloadedByPath( + file_in_root, + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(entry); + EXPECT_FALSE(entry->file_specific_info().is_hosted_document()); + + // The transfered file is cached and the change of "offline available" + // attribute is notified. + EXPECT_EQ(2U, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(file_in_root)); + EXPECT_TRUE(delegate()->get_changed_files().count(cached_file)); + + // The cache for the other file should be removed in order to free up space. + ResourceEntry cached_file_entry; + EXPECT_EQ(FILE_ERROR_OK, + GetLocalResourceEntry(cached_file, &cached_file_entry)); + EXPECT_FALSE( + cached_file_entry.file_specific_info().cache_state().is_present()); +} + +TEST_F(DownloadOperationTest, + EnsureFileDownloadedByPath_FromServer_EnoughSpaceButBecomeFull) { + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &src_entry)); + const int64 file_size = src_entry.file_info().size(); + + // Pretend we have enough space first (checked before downloading a file), + // but then start reporting we have not enough space. This is to emulate that + // the disk space becomes full after the file is downloaded for some reason + // (ex. the actual file was larger than the expected size). + fake_free_disk_space_getter()->PushFakeValue( + file_size + drive::internal::kMinFreeSpaceInBytes); + fake_free_disk_space_getter()->set_default_value( + drive::internal::kMinFreeSpaceInBytes - 1); + + FileError error = FILE_ERROR_OK; + base::FilePath file_path; + scoped_ptr<ResourceEntry> entry; + operation_->EnsureFileDownloadedByPath( + file_in_root, + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_NO_LOCAL_SPACE, error); +} + +TEST_F(DownloadOperationTest, EnsureFileDownloadedByPath_FromCache) { + base::FilePath temp_file; + ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir(), &temp_file)); + + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &src_entry)); + + // Store something as cached version of this file. + FileError error = FILE_ERROR_OK; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::FileCache::Store, + base::Unretained(cache()), + GetLocalId(file_in_root), + src_entry.file_specific_info().md5(), + temp_file, + internal::FileCache::FILE_OPERATION_COPY), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + base::FilePath file_path; + scoped_ptr<ResourceEntry> entry; + operation_->EnsureFileDownloadedByPath( + file_in_root, + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(entry); + EXPECT_FALSE(entry->file_specific_info().is_hosted_document()); +} + +TEST_F(DownloadOperationTest, EnsureFileDownloadedByPath_HostedDocument) { + base::FilePath file_in_root(FILE_PATH_LITERAL( + "drive/root/Document 1 excludeDir-test.gdoc")); + + FileError error = FILE_ERROR_FAILED; + base::FilePath file_path; + scoped_ptr<ResourceEntry> entry; + operation_->EnsureFileDownloadedByPath( + file_in_root, + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(entry); + EXPECT_TRUE(entry->file_specific_info().is_hosted_document()); + EXPECT_FALSE(file_path.empty()); + + EXPECT_EQ(GURL(entry->file_specific_info().alternate_url()), + util::ReadUrlFromGDocFile(file_path)); + EXPECT_EQ(entry->resource_id(), util::ReadResourceIdFromGDocFile(file_path)); + EXPECT_EQ(FILE_PATH_LITERAL(".gdoc"), file_path.Extension()); +} + +TEST_F(DownloadOperationTest, EnsureFileDownloadedByLocalId) { + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &src_entry)); + + FileError error = FILE_ERROR_OK; + base::FilePath file_path; + scoped_ptr<ResourceEntry> entry; + operation_->EnsureFileDownloadedByLocalId( + GetLocalId(file_in_root), + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(entry); + EXPECT_FALSE(entry->file_specific_info().is_hosted_document()); + + // The transfered file is cached and the change of "offline available" + // attribute is notified. + EXPECT_EQ(1U, delegate()->get_changed_files().size()); + EXPECT_EQ(1U, delegate()->get_changed_files().count(file_in_root)); +} + +TEST_F(DownloadOperationTest, + EnsureFileDownloadedByPath_WithGetContentCallback) { + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + + { + FileError initialized_error = FILE_ERROR_FAILED; + scoped_ptr<ResourceEntry> entry, entry_dontcare; + base::FilePath local_path, local_path_dontcare; + google_apis::test_util::TestGetContentCallback get_content_callback; + FileError completion_error = FILE_ERROR_FAILED; + base::Closure cancel_download = operation_->EnsureFileDownloadedByPath( + file_in_root, + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback( + &initialized_error, &local_path, &entry), + get_content_callback.callback(), + google_apis::test_util::CreateCopyResultCallback( + &completion_error, &local_path_dontcare, &entry_dontcare)); + content::RunAllBlockingPoolTasksUntilIdle(); + + // For the first time, file is downloaded from the remote server. + // In this case, |local_path| is empty. + EXPECT_EQ(FILE_ERROR_OK, initialized_error); + ASSERT_TRUE(entry); + ASSERT_TRUE(local_path.empty()); + EXPECT_FALSE(cancel_download.is_null()); + // Content is available through the second callback argument. + EXPECT_EQ(static_cast<size_t>(entry->file_info().size()), + get_content_callback.GetConcatenatedData().size()); + EXPECT_EQ(FILE_ERROR_OK, completion_error); + + // The transfered file is cached and the change of "offline available" + // attribute is notified. + EXPECT_EQ(1U, delegate()->get_changed_files().size()); + EXPECT_EQ(1U, delegate()->get_changed_files().count(file_in_root)); + } + + { + FileError initialized_error = FILE_ERROR_FAILED; + scoped_ptr<ResourceEntry> entry, entry_dontcare; + base::FilePath local_path, local_path_dontcare; + google_apis::test_util::TestGetContentCallback get_content_callback; + FileError completion_error = FILE_ERROR_FAILED; + base::Closure cancel_download = operation_->EnsureFileDownloadedByPath( + file_in_root, + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback( + &initialized_error, &local_path, &entry), + get_content_callback.callback(), + google_apis::test_util::CreateCopyResultCallback( + &completion_error, &local_path_dontcare, &entry_dontcare)); + content::RunAllBlockingPoolTasksUntilIdle(); + + // Try second download. In this case, the file should be cached, so + // |local_path| should not be empty. + EXPECT_EQ(FILE_ERROR_OK, initialized_error); + ASSERT_TRUE(entry); + ASSERT_TRUE(!local_path.empty()); + EXPECT_FALSE(cancel_download.is_null()); + // The content is available from the cache file. + EXPECT_TRUE(get_content_callback.data().empty()); + int64 local_file_size = 0; + base::GetFileSize(local_path, &local_file_size); + EXPECT_EQ(entry->file_info().size(), local_file_size); + EXPECT_EQ(FILE_ERROR_OK, completion_error); + } +} + +TEST_F(DownloadOperationTest, EnsureFileDownloadedByLocalId_FromCache) { + base::FilePath temp_file; + ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir(), &temp_file)); + + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &src_entry)); + + // Store something as cached version of this file. + FileError error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::FileCache::Store, + base::Unretained(cache()), + GetLocalId(file_in_root), + src_entry.file_specific_info().md5(), + temp_file, + internal::FileCache::FILE_OPERATION_COPY), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // The file is obtained from the cache. + // Hence the downloading should work even if the drive service is offline. + fake_service()->set_offline(true); + + base::FilePath file_path; + scoped_ptr<ResourceEntry> entry; + operation_->EnsureFileDownloadedByLocalId( + GetLocalId(file_in_root), + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(entry); + EXPECT_FALSE(entry->file_specific_info().is_hosted_document()); +} + +TEST_F(DownloadOperationTest, EnsureFileDownloadedByPath_DirtyCache) { + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &src_entry)); + + // Prepare a dirty file to store to cache that has a different size than + // stored in resource metadata. + base::FilePath dirty_file = temp_dir().AppendASCII("dirty.txt"); + size_t dirty_size = src_entry.file_info().size() + 10; + google_apis::test_util::WriteStringToFile(dirty_file, + std::string(dirty_size, 'x')); + + // Store the file as a cache, marking it to be dirty. + FileError error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::FileCache::Store, + base::Unretained(cache()), + GetLocalId(file_in_root), + std::string(), + dirty_file, + internal::FileCache::FILE_OPERATION_COPY), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Record values passed to GetFileContentInitializedCallback(). + FileError init_error; + base::FilePath init_path; + scoped_ptr<ResourceEntry> init_entry; + base::FilePath file_path; + scoped_ptr<ResourceEntry> entry; + base::Closure cancel_callback = operation_->EnsureFileDownloadedByPath( + file_in_root, + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback( + &init_error, &init_path, &init_entry), + google_apis::GetContentCallback(), + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + // Check that the result of local modification is propagated. + EXPECT_EQ(static_cast<int64>(dirty_size), init_entry->file_info().size()); + EXPECT_EQ(static_cast<int64>(dirty_size), entry->file_info().size()); +} + +TEST_F(DownloadOperationTest, EnsureFileDownloadedByPath_LocallyCreatedFile) { + // Add a new file with an empty resource ID. + base::FilePath file_path(FILE_PATH_LITERAL("drive/root/New File.txt")); + ResourceEntry parent; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_path.DirName(), &parent)); + + ResourceEntry new_file; + new_file.set_title("New File.txt"); + new_file.set_parent_local_id(parent.local_id()); + + FileError error = FILE_ERROR_FAILED; + std::string local_id; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::ResourceMetadata::AddEntry, + base::Unretained(metadata()), + new_file, + &local_id), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Empty cache file should be returned. + base::FilePath cache_file_path; + scoped_ptr<ResourceEntry> entry; + operation_->EnsureFileDownloadedByPath( + file_path, + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + google_apis::test_util::CreateCopyResultCallback( + &error, &cache_file_path, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + int64 cache_file_size = 0; + EXPECT_TRUE(base::GetFileSize(cache_file_path, &cache_file_size)); + EXPECT_EQ(static_cast<int64>(0), cache_file_size); + ASSERT_TRUE(entry); + EXPECT_EQ(cache_file_size, entry->file_info().size()); +} + +TEST_F(DownloadOperationTest, CancelBeforeDownloadStarts) { + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &src_entry)); + + // Start operation. + FileError error = FILE_ERROR_OK; + base::FilePath file_path; + scoped_ptr<ResourceEntry> entry; + base::Closure cancel_closure = operation_->EnsureFileDownloadedByLocalId( + GetLocalId(file_in_root), + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &entry)); + + // Cancel immediately. + ASSERT_FALSE(cancel_closure.is_null()); + cancel_closure.Run(); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_ABORT, error); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/get_file_for_saving_operation.cc b/components/drive/file_system/get_file_for_saving_operation.cc new file mode 100644 index 0000000..30aefd6 --- /dev/null +++ b/components/drive/file_system/get_file_for_saving_operation.cc @@ -0,0 +1,206 @@ +// Copyright 2013 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 "components/drive/file_system/get_file_for_saving_operation.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/callback_helpers.h" +#include "components/drive/event_logger.h" +#include "components/drive/file_cache.h" +#include "components/drive/file_system/create_file_operation.h" +#include "components/drive/file_system/download_operation.h" +#include "components/drive/file_system/operation_delegate.h" +#include "components/drive/file_write_watcher.h" +#include "components/drive/job_scheduler.h" + +namespace drive { +namespace file_system { + +namespace { + +FileError OpenCacheFileForWrite( + internal::ResourceMetadata* metadata, + internal::FileCache* cache, + const std::string& local_id, + scoped_ptr<base::ScopedClosureRunner>* file_closer, + ResourceEntry* entry) { + FileError error = cache->OpenForWrite(local_id, file_closer); + if (error != FILE_ERROR_OK) + return error; + return metadata->GetResourceEntryById(local_id, entry); +} + +} // namespace + +GetFileForSavingOperation::GetFileForSavingOperation( + EventLogger* logger, + base::SequencedTaskRunner* blocking_task_runner, + base::SingleThreadTaskRunner* file_task_runner, + OperationDelegate* delegate, + JobScheduler* scheduler, + internal::ResourceMetadata* metadata, + internal::FileCache* cache, + const base::FilePath& temporary_file_directory) + : logger_(logger), + create_file_operation_( + new CreateFileOperation(blocking_task_runner, delegate, metadata)), + download_operation_(new DownloadOperation(blocking_task_runner, + delegate, + scheduler, + metadata, + cache, + temporary_file_directory)), + file_write_watcher_(new internal::FileWriteWatcher(file_task_runner)), + blocking_task_runner_(blocking_task_runner), + delegate_(delegate), + metadata_(metadata), + cache_(cache), + weak_ptr_factory_(this) { +} + +GetFileForSavingOperation::~GetFileForSavingOperation() { +} + +void GetFileForSavingOperation::GetFileForSaving( + const base::FilePath& file_path, + const GetFileCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + create_file_operation_->CreateFile( + file_path, + false, // error_if_already_exists + std::string(), // no specific mime type + base::Bind(&GetFileForSavingOperation::GetFileForSavingAfterCreate, + weak_ptr_factory_.GetWeakPtr(), + file_path, + callback)); +} + +void GetFileForSavingOperation::GetFileForSavingAfterCreate( + const base::FilePath& file_path, + const GetFileCallback& callback, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error != FILE_ERROR_OK) { + callback.Run(error, base::FilePath(), scoped_ptr<ResourceEntry>()); + return; + } + + download_operation_->EnsureFileDownloadedByPath( + file_path, + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + base::Bind(&GetFileForSavingOperation::GetFileForSavingAfterDownload, + weak_ptr_factory_.GetWeakPtr(), + callback)); +} + +void GetFileForSavingOperation::GetFileForSavingAfterDownload( + const GetFileCallback& callback, + FileError error, + const base::FilePath& cache_path, + scoped_ptr<ResourceEntry> entry) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error != FILE_ERROR_OK) { + callback.Run(error, base::FilePath(), scoped_ptr<ResourceEntry>()); + return; + } + + const std::string& local_id = entry->local_id(); + ResourceEntry* entry_ptr = entry.get(); + scoped_ptr<base::ScopedClosureRunner>* file_closer = + new scoped_ptr<base::ScopedClosureRunner>; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&OpenCacheFileForWrite, + metadata_, + cache_, + local_id, + file_closer, + entry_ptr), + base::Bind(&GetFileForSavingOperation::GetFileForSavingAfterOpenForWrite, + weak_ptr_factory_.GetWeakPtr(), + callback, + cache_path, + base::Passed(&entry), + base::Owned(file_closer))); +} + +void GetFileForSavingOperation::GetFileForSavingAfterOpenForWrite( + const GetFileCallback& callback, + const base::FilePath& cache_path, + scoped_ptr<ResourceEntry> entry, + scoped_ptr<base::ScopedClosureRunner>* file_closer, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error != FILE_ERROR_OK) { + callback.Run(error, base::FilePath(), scoped_ptr<ResourceEntry>()); + return; + } + + const std::string& local_id = entry->local_id(); + file_write_watcher_->StartWatch( + cache_path, + base::Bind(&GetFileForSavingOperation::GetFileForSavingAfterWatch, + weak_ptr_factory_.GetWeakPtr(), + callback, + cache_path, + base::Passed(&entry)), + base::Bind(&GetFileForSavingOperation::OnWriteEvent, + weak_ptr_factory_.GetWeakPtr(), + local_id, + base::Passed(file_closer))); +} + +void GetFileForSavingOperation::GetFileForSavingAfterWatch( + const GetFileCallback& callback, + const base::FilePath& cache_path, + scoped_ptr<ResourceEntry> entry, + bool success) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + logger_->Log(logging::LOG_INFO, "Started watching modification to %s [%s].", + entry->local_id().c_str(), + success ? "ok" : "fail"); + + if (!success) { + callback.Run(FILE_ERROR_FAILED, + base::FilePath(), scoped_ptr<ResourceEntry>()); + return; + } + + callback.Run(FILE_ERROR_OK, cache_path, entry.Pass()); +} + +void GetFileForSavingOperation::OnWriteEvent( + const std::string& local_id, + scoped_ptr<base::ScopedClosureRunner> file_closer) { + logger_->Log(logging::LOG_INFO, "Detected modification to %s.", + local_id.c_str()); + + delegate_->OnEntryUpdatedByOperation(ClientContext(USER_INITIATED), local_id); + + // Clients may have enlarged the file. By FreeDiskpSpaceIfNeededFor(0), + // we try to ensure (0 + the-minimum-safe-margin = 512MB as of now) space. + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(base::IgnoreResult( + base::Bind(&internal::FileCache::FreeDiskSpaceIfNeededFor, + base::Unretained(cache_), + 0)))); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/get_file_for_saving_operation.h b/components/drive/file_system/get_file_for_saving_operation.h new file mode 100644 index 0000000..9b2fc7c --- /dev/null +++ b/components/drive/file_system/get_file_for_saving_operation.h @@ -0,0 +1,109 @@ +// Copyright 2013 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_GET_FILE_FOR_SAVING_OPERATION_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_GET_FILE_FOR_SAVING_OPERATION_H_ + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system_interface.h" + +namespace base { +class FilePath; +class ScopedClosureRunner; +class SequencedTaskRunner; +} // namespace base + +namespace drive { +namespace internal { +class FileCache; +class FileWriteWatcher; +class ResourceMetadata; +} // namespace internal + +class EventLogger; +class JobScheduler; +class ResourceEntry; + +namespace file_system { + +class CreateFileOperation; +class DownloadOperation; +class OperationDelegate; + +// Implements GetFileForSaving() operation that prepares a local cache for +// a Drive file whose next modification is monitored and notified to the +// OperationDelegate. +// TODO(kinaba): crbug.com/269424: we might want to monitor all the changes +// to the cache directory, not just the one immediately after the save dialog. +class GetFileForSavingOperation { + public: + GetFileForSavingOperation(EventLogger* logger, + base::SequencedTaskRunner* blocking_task_runner, + base::SingleThreadTaskRunner* file_task_runner, + OperationDelegate* delegate, + JobScheduler* scheduler, + internal::ResourceMetadata* metadata, + internal::FileCache* cache, + const base::FilePath& temporary_file_directory); + ~GetFileForSavingOperation(); + + // Makes sure that |file_path| in the file system is available in the local + // cache, and marks it as dirty. The next modification to the cache file is + // watched and is automatically notified to the delegate. If the entry is not + // present in the file system, it is created. + void GetFileForSaving(const base::FilePath& file_path, + const GetFileCallback& callback); + + internal::FileWriteWatcher* file_write_watcher_for_testing() { + return file_write_watcher_.get(); + } + + private: + void GetFileForSavingAfterCreate(const base::FilePath& file_path, + const GetFileCallback& callback, + FileError error); + void GetFileForSavingAfterDownload(const GetFileCallback& callback, + FileError error, + const base::FilePath& cache_path, + scoped_ptr<ResourceEntry> entry); + void GetFileForSavingAfterOpenForWrite( + const GetFileCallback& callback, + const base::FilePath& cache_path, + scoped_ptr<ResourceEntry> entry, + scoped_ptr<base::ScopedClosureRunner>* file_closer, + FileError error); + void GetFileForSavingAfterWatch(const GetFileCallback& callback, + const base::FilePath& cache_path, + scoped_ptr<ResourceEntry> entry, + bool success); + // Called when the cache file for |local_id| is written. + void OnWriteEvent(const std::string& local_id, + scoped_ptr<base::ScopedClosureRunner> file_closer); + + EventLogger* logger_; + scoped_ptr<CreateFileOperation> create_file_operation_; + scoped_ptr<DownloadOperation> download_operation_; + scoped_ptr<internal::FileWriteWatcher> file_write_watcher_; + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + OperationDelegate* delegate_; + internal::ResourceMetadata* metadata_; + internal::FileCache* cache_; + + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate the weak pointers before any other members are destroyed. + base::WeakPtrFactory<GetFileForSavingOperation> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(GetFileForSavingOperation); +}; + +} // namespace file_system +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_GET_FILE_FOR_SAVING_OPERATION_H_ diff --git a/components/drive/file_system/get_file_for_saving_operation_unittest.cc b/components/drive/file_system/get_file_for_saving_operation_unittest.cc new file mode 100644 index 0000000..984d0b6 --- /dev/null +++ b/components/drive/file_system/get_file_for_saving_operation_unittest.cc @@ -0,0 +1,158 @@ +// Copyright 2013 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 "components/drive/file_system/get_file_for_saving_operation.h" + +#include "base/callback.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/run_loop.h" +#include "base/task_runner_util.h" +#include "components/drive/drive.pb.h" +#include "components/drive/file_change.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system/operation_test_base.h" +#include "components/drive/file_write_watcher.h" +#include "components/drive/job_scheduler.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/test/test_utils.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace file_system { + +namespace { + +// If OnCacheFileUploadNeededByOperation is called, records the local ID and +// calls |quit_closure|. +class TestDelegate : public OperationDelegate { + public: + void set_quit_closure(const base::Closure& quit_closure) { + quit_closure_ = quit_closure; + } + + const std::string& updated_local_id() const { + return updated_local_id_; + } + + // OperationDelegate overrides. + void OnEntryUpdatedByOperation(const ClientContext& /* context */, + const std::string& local_id) override { + updated_local_id_ = local_id; + if (!quit_closure_.is_null()) + quit_closure_.Run(); + } + + private: + std::string updated_local_id_; + base::Closure quit_closure_; +}; + +} // namespace + +class GetFileForSavingOperationTest : public OperationTestBase { + protected: + // FileWriteWatcher requires TYPE_IO message loop to run. + GetFileForSavingOperationTest() + : OperationTestBase(content::TestBrowserThreadBundle::IO_MAINLOOP) { + } + + void SetUp() override { + OperationTestBase::SetUp(); + + file_task_runner_ = content::BrowserThread::GetMessageLoopProxyForThread( + content::BrowserThread::FILE); + + operation_.reset(new GetFileForSavingOperation( + logger(), blocking_task_runner(), file_task_runner_.get(), &delegate_, + scheduler(), metadata(), cache(), temp_dir())); + operation_->file_write_watcher_for_testing()->DisableDelayForTesting(); + } + + TestDelegate delegate_; + scoped_ptr<GetFileForSavingOperation> operation_; + scoped_refptr<base::SingleThreadTaskRunner> file_task_runner_; +}; + +TEST_F(GetFileForSavingOperationTest, GetFileForSaving_Exist) { + base::FilePath drive_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(drive_path, &src_entry)); + + // Run the operation. + FileError error = FILE_ERROR_FAILED; + scoped_ptr<ResourceEntry> entry; + base::FilePath local_path; + operation_->GetFileForSaving( + drive_path, + google_apis::test_util::CreateCopyResultCallback( + &error, &local_path, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + + // Checks that the file is retrieved. + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(entry); + EXPECT_EQ(src_entry.resource_id(), entry->resource_id()); + + // Checks that it presents in cache and marked dirty. + EXPECT_TRUE(entry->file_specific_info().cache_state().is_present()); + EXPECT_TRUE(entry->file_specific_info().cache_state().is_dirty()); + + // Write something to the cache and checks that the event is reported. + { + base::RunLoop run_loop; + delegate_.set_quit_closure(run_loop.QuitClosure()); + google_apis::test_util::WriteStringToFile(local_path, "hello"); + run_loop.Run(); + EXPECT_EQ(GetLocalId(drive_path), delegate_.updated_local_id()); + } +} + +TEST_F(GetFileForSavingOperationTest, GetFileForSaving_NotExist) { + base::FilePath drive_path(FILE_PATH_LITERAL("drive/root/NotExist.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_NOT_FOUND, + GetLocalResourceEntry(drive_path, &src_entry)); + + // Run the operation. + FileError error = FILE_ERROR_FAILED; + scoped_ptr<ResourceEntry> entry; + base::FilePath local_path; + operation_->GetFileForSaving( + drive_path, + google_apis::test_util::CreateCopyResultCallback( + &error, &local_path, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + + // Checks that the file is created and retrieved. + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(drive_path, &src_entry)); + int64 size = -1; + EXPECT_TRUE(base::GetFileSize(local_path, &size)); + EXPECT_EQ(0, size); +} + +TEST_F(GetFileForSavingOperationTest, GetFileForSaving_Directory) { + base::FilePath drive_path(FILE_PATH_LITERAL("drive/root/Directory 1")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(drive_path, &src_entry)); + ASSERT_TRUE(src_entry.file_info().is_directory()); + + // Run the operation. + FileError error = FILE_ERROR_FAILED; + scoped_ptr<ResourceEntry> entry; + base::FilePath local_path; + operation_->GetFileForSaving( + drive_path, + google_apis::test_util::CreateCopyResultCallback( + &error, &local_path, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + + // Checks that an error is returned. + EXPECT_EQ(FILE_ERROR_EXISTS, error); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/move_operation.cc b/components/drive/file_system/move_operation.cc new file mode 100644 index 0000000..ff1edd6 --- /dev/null +++ b/components/drive/file_system/move_operation.cc @@ -0,0 +1,121 @@ +// Copyright (c) 2012 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 "components/drive/file_system/move_operation.h" + +#include "base/sequenced_task_runner.h" +#include "components/drive/drive.pb.h" +#include "components/drive/file_change.h" +#include "components/drive/file_system/operation_delegate.h" +#include "components/drive/job_scheduler.h" +#include "components/drive/resource_metadata.h" + +namespace drive { +namespace file_system { +namespace { + +// Looks up ResourceEntry for source entry and the destination directory. +FileError UpdateLocalState(internal::ResourceMetadata* metadata, + const base::FilePath& src_path, + const base::FilePath& dest_path, + FileChange* changed_files, + std::string* local_id) { + ResourceEntry entry; + FileError error = metadata->GetResourceEntryByPath(src_path, &entry); + if (error != FILE_ERROR_OK) + return error; + *local_id = entry.local_id(); + + ResourceEntry parent_entry; + error = metadata->GetResourceEntryByPath(dest_path.DirName(), &parent_entry); + if (error != FILE_ERROR_OK) + return error; + + // The parent must be a directory. + if (!parent_entry.file_info().is_directory()) + return FILE_ERROR_NOT_A_DIRECTORY; + + changed_files->Update(src_path, entry, FileChange::CHANGE_TYPE_DELETE); + + // Strip the extension for a hosted document if necessary. + const std::string new_extension = + base::FilePath(dest_path.Extension()).AsUTF8Unsafe(); + const bool has_hosted_document_extension = + entry.has_file_specific_info() && + entry.file_specific_info().is_hosted_document() && + new_extension == entry.file_specific_info().document_extension(); + const std::string new_title = + has_hosted_document_extension ? + dest_path.BaseName().RemoveExtension().AsUTF8Unsafe() : + dest_path.BaseName().AsUTF8Unsafe(); + + entry.set_title(new_title); + entry.set_parent_local_id(parent_entry.local_id()); + entry.set_metadata_edit_state(ResourceEntry::DIRTY); + entry.set_modification_date(base::Time::Now().ToInternalValue()); + error = metadata->RefreshEntry(entry); + if (error != FILE_ERROR_OK) + return error; + + changed_files->Update(dest_path, entry, + FileChange::CHANGE_TYPE_ADD_OR_UPDATE); + return FILE_ERROR_OK; +} + +} // namespace + +MoveOperation::MoveOperation(base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + internal::ResourceMetadata* metadata) + : blocking_task_runner_(blocking_task_runner), + delegate_(delegate), + metadata_(metadata), + weak_ptr_factory_(this) { +} + +MoveOperation::~MoveOperation() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +void MoveOperation::Move(const base::FilePath& src_file_path, + const base::FilePath& dest_file_path, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FileChange* changed_files = new FileChange; + std::string* local_id = new std::string; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&UpdateLocalState, + metadata_, + src_file_path, + dest_file_path, + changed_files, + local_id), + base::Bind(&MoveOperation::MoveAfterUpdateLocalState, + weak_ptr_factory_.GetWeakPtr(), + callback, + base::Owned(changed_files), + base::Owned(local_id))); +} + +void MoveOperation::MoveAfterUpdateLocalState( + const FileOperationCallback& callback, + const FileChange* changed_files, + const std::string* local_id, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + if (error == FILE_ERROR_OK) { + // Notify the change of directory. + delegate_->OnFileChangedByOperation(*changed_files); + delegate_->OnEntryUpdatedByOperation(ClientContext(USER_INITIATED), + *local_id); + } + callback.Run(error); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/move_operation.h b/components/drive/file_system/move_operation.h new file mode 100644 index 0000000..db6cee8 --- /dev/null +++ b/components/drive/file_system/move_operation.h @@ -0,0 +1,79 @@ +// Copyright (c) 2012 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_MOVE_OPERATION_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_MOVE_OPERATION_H_ + +#include <set> + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "components/drive/file_errors.h" +#include "google_apis/drive/drive_api_error_codes.h" + +namespace base { +class FilePath; +class SequencedTaskRunner; +} // namespace base + +namespace google_apis { +class ResourceEntry; +} // namespace google_apis + +namespace drive { + +class FileChange; +class ResourceEntry; + +namespace internal { +class ResourceMetadata; +} // namespace internal + +namespace file_system { + +class OperationDelegate; + +// This class encapsulates the drive Move function. It is responsible for +// updating the local metadata entry. +class MoveOperation { + public: + MoveOperation(base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + internal::ResourceMetadata* metadata); + ~MoveOperation(); + + // Performs the move operation on the file at drive path |src_file_path| + // with a target of |dest_file_path|. + // Invokes |callback| when finished with the result of the operation. + // |callback| must not be null. + void Move(const base::FilePath& src_file_path, + const base::FilePath& dest_file_path, + const FileOperationCallback& callback); + + private: + // Part of Move(). Called after updating the local state. + void MoveAfterUpdateLocalState(const FileOperationCallback& callback, + const FileChange* changed_file, + const std::string* local_id, + FileError error); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + OperationDelegate* delegate_; + internal::ResourceMetadata* metadata_; + + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate the weak pointers before any other members are destroyed. + base::WeakPtrFactory<MoveOperation> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(MoveOperation); +}; + +} // namespace file_system +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_MOVE_OPERATION_H_ diff --git a/components/drive/file_system/move_operation_unittest.cc b/components/drive/file_system/move_operation_unittest.cc new file mode 100644 index 0000000..4f097b3 --- /dev/null +++ b/components/drive/file_system/move_operation_unittest.cc @@ -0,0 +1,141 @@ +// Copyright 2013 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 "components/drive/file_system/move_operation.h" + +#include "components/drive/file_change.h" +#include "components/drive/file_system/operation_test_base.h" +#include "content/public/test/test_utils.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace file_system { + +class MoveOperationTest : public OperationTestBase { + protected: + void SetUp() override { + OperationTestBase::SetUp(); + operation_.reset(new MoveOperation(blocking_task_runner(), + delegate(), + metadata())); + } + + scoped_ptr<MoveOperation> operation_; +}; + +TEST_F(MoveOperationTest, MoveFileInSameDirectory) { + const base::FilePath src_path( + FILE_PATH_LITERAL("drive/root/Directory 1/SubDirectory File 1.txt")); + const base::FilePath dest_path( + FILE_PATH_LITERAL("drive/root/Directory 1/Test.log")); + + ResourceEntry src_entry, dest_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(src_path, &src_entry)); + ASSERT_EQ(FILE_ERROR_NOT_FOUND, + GetLocalResourceEntry(dest_path, &dest_entry)); + + FileError error = FILE_ERROR_FAILED; + operation_->Move(src_path, + dest_path, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(dest_path, &dest_entry)); + EXPECT_EQ(src_entry.local_id(), dest_entry.local_id()); + EXPECT_EQ(ResourceEntry::DIRTY, dest_entry.metadata_edit_state()); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, GetLocalResourceEntry(src_path, &src_entry)); + + EXPECT_EQ(2U, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(src_path)); + EXPECT_TRUE(delegate()->get_changed_files().count(dest_path)); + + EXPECT_EQ(1U, delegate()->updated_local_ids().size()); + EXPECT_TRUE(delegate()->updated_local_ids().count(src_entry.local_id())); +} + +TEST_F(MoveOperationTest, MoveFileFromRootToSubDirectory) { + base::FilePath src_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + base::FilePath dest_path( + FILE_PATH_LITERAL("drive/root/Directory 1/Test.log")); + + ResourceEntry src_entry, dest_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(src_path, &src_entry)); + ASSERT_EQ(FILE_ERROR_NOT_FOUND, + GetLocalResourceEntry(dest_path, &dest_entry)); + + FileError error = FILE_ERROR_FAILED; + operation_->Move(src_path, + dest_path, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(dest_path, &dest_entry)); + EXPECT_EQ(src_entry.local_id(), dest_entry.local_id()); + EXPECT_EQ(ResourceEntry::DIRTY, dest_entry.metadata_edit_state()); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, GetLocalResourceEntry(src_path, &src_entry)); + + EXPECT_EQ(2U, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(src_path)); + EXPECT_TRUE(delegate()->get_changed_files().count(dest_path)); + + EXPECT_EQ(1U, delegate()->updated_local_ids().size()); + EXPECT_TRUE(delegate()->updated_local_ids().count(src_entry.local_id())); +} + +TEST_F(MoveOperationTest, MoveNotExistingFile) { + base::FilePath src_path(FILE_PATH_LITERAL("drive/root/Dummy file.txt")); + base::FilePath dest_path(FILE_PATH_LITERAL("drive/root/Test.log")); + + FileError error = FILE_ERROR_OK; + operation_->Move(src_path, + dest_path, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, error); + + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_NOT_FOUND, GetLocalResourceEntry(src_path, &entry)); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, GetLocalResourceEntry(dest_path, &entry)); +} + +TEST_F(MoveOperationTest, MoveFileToNonExistingDirectory) { + base::FilePath src_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + base::FilePath dest_path(FILE_PATH_LITERAL("drive/root/Dummy/Test.log")); + + FileError error = FILE_ERROR_OK; + operation_->Move(src_path, + dest_path, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, error); + + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(src_path, &entry)); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, GetLocalResourceEntry(dest_path, &entry)); +} + +// Test the case where the parent of |dest_file_path| is a existing file, +// not a directory. +TEST_F(MoveOperationTest, MoveFileToInvalidPath) { + base::FilePath src_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + base::FilePath dest_path( + FILE_PATH_LITERAL("drive/root/Duplicate Name.txt/Test.log")); + + FileError error = FILE_ERROR_OK; + operation_->Move(src_path, + dest_path, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_A_DIRECTORY, error); + + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(src_path, &entry)); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, GetLocalResourceEntry(dest_path, &entry)); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/open_file_operation.cc b/components/drive/file_system/open_file_operation.cc new file mode 100644 index 0000000..54da778 --- /dev/null +++ b/components/drive/file_system/open_file_operation.cc @@ -0,0 +1,185 @@ +// Copyright 2013 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 "components/drive/file_system/open_file_operation.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/callback_helpers.h" +#include "base/files/file_path.h" +#include "base/logging.h" +#include "base/task_runner_util.h" +#include "base/thread_task_runner_handle.h" +#include "components/drive/drive.pb.h" +#include "components/drive/file_cache.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system/create_file_operation.h" +#include "components/drive/file_system/download_operation.h" +#include "components/drive/file_system/operation_delegate.h" +#include "components/drive/job_scheduler.h" + +namespace drive { +namespace file_system { + +OpenFileOperation::OpenFileOperation( + base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + JobScheduler* scheduler, + internal::ResourceMetadata* metadata, + internal::FileCache* cache, + const base::FilePath& temporary_file_directory) + : blocking_task_runner_(blocking_task_runner), + delegate_(delegate), + cache_(cache), + create_file_operation_(new CreateFileOperation( + blocking_task_runner, delegate, metadata)), + download_operation_(new DownloadOperation( + blocking_task_runner, delegate, scheduler, + metadata, cache, temporary_file_directory)), + weak_ptr_factory_(this) { +} + +OpenFileOperation::~OpenFileOperation() { +} + +void OpenFileOperation::OpenFile(const base::FilePath& file_path, + OpenMode open_mode, + const std::string& mime_type, + const OpenFileCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + switch (open_mode) { + case OPEN_FILE: + // It is not necessary to create a new file even if not exists. + // So call OpenFileAfterCreateFile directly with FILE_ERROR_OK + // to skip file creation. + OpenFileAfterCreateFile(file_path, callback, FILE_ERROR_OK); + break; + case CREATE_FILE: + create_file_operation_->CreateFile( + file_path, + true, // exclusive: fail if already exists + mime_type, + base::Bind(&OpenFileOperation::OpenFileAfterCreateFile, + weak_ptr_factory_.GetWeakPtr(), file_path, callback)); + break; + case OPEN_OR_CREATE_FILE: + create_file_operation_->CreateFile( + file_path, + false, // not-exclusive + mime_type, + base::Bind(&OpenFileOperation::OpenFileAfterCreateFile, + weak_ptr_factory_.GetWeakPtr(), file_path, callback)); + break; + } +} + +void OpenFileOperation::OpenFileAfterCreateFile( + const base::FilePath& file_path, + const OpenFileCallback& callback, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error != FILE_ERROR_OK) { + callback.Run(error, base::FilePath(), base::Closure()); + return; + } + + download_operation_->EnsureFileDownloadedByPath( + file_path, + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + base::Bind( + &OpenFileOperation::OpenFileAfterFileDownloaded, + weak_ptr_factory_.GetWeakPtr(), callback)); +} + +void OpenFileOperation::OpenFileAfterFileDownloaded( + const OpenFileCallback& callback, + FileError error, + const base::FilePath& local_file_path, + scoped_ptr<ResourceEntry> entry) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error == FILE_ERROR_OK) { + DCHECK(entry); + DCHECK(entry->has_file_specific_info()); + if (entry->file_specific_info().is_hosted_document()) + // No support for opening a hosted document. + error = FILE_ERROR_INVALID_OPERATION; + } + + if (error != FILE_ERROR_OK) { + callback.Run(error, base::FilePath(), base::Closure()); + return; + } + + scoped_ptr<base::ScopedClosureRunner>* file_closer = + new scoped_ptr<base::ScopedClosureRunner>; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&internal::FileCache::OpenForWrite, + base::Unretained(cache_), + entry->local_id(), + file_closer), + base::Bind(&OpenFileOperation::OpenFileAfterOpenForWrite, + weak_ptr_factory_.GetWeakPtr(), + local_file_path, + entry->local_id(), + callback, + base::Owned(file_closer))); +} + +void OpenFileOperation::OpenFileAfterOpenForWrite( + const base::FilePath& local_file_path, + const std::string& local_id, + const OpenFileCallback& callback, + scoped_ptr<base::ScopedClosureRunner>* file_closer, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error != FILE_ERROR_OK) { + callback.Run(error, base::FilePath(), base::Closure()); + return; + } + + ++open_files_[local_id]; + callback.Run(error, local_file_path, + base::Bind(&OpenFileOperation::CloseFile, + weak_ptr_factory_.GetWeakPtr(), + local_id, + base::Passed(file_closer))); +} + +void OpenFileOperation::CloseFile( + const std::string& local_id, + scoped_ptr<base::ScopedClosureRunner> file_closer) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK_GT(open_files_[local_id], 0); + + if (--open_files_[local_id] == 0) { + // All clients closes this file, so notify to upload the file. + open_files_.erase(local_id); + delegate_->OnEntryUpdatedByOperation(ClientContext(USER_INITIATED), + local_id); + + // Clients may have enlarged the file. By FreeDiskpSpaceIfNeededFor(0), + // we try to ensure (0 + the-minimum-safe-margin = 512MB as of now) space. + blocking_task_runner_->PostTask( + FROM_HERE, + base::Bind(base::IgnoreResult( + base::Bind(&internal::FileCache::FreeDiskSpaceIfNeededFor, + base::Unretained(cache_), + 0)))); + } +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/open_file_operation.h b/components/drive/file_system/open_file_operation.h new file mode 100644 index 0000000..4f0f554 --- /dev/null +++ b/components/drive/file_system/open_file_operation.h @@ -0,0 +1,109 @@ +// Copyright 2013 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_OPEN_FILE_OPERATION_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_OPEN_FILE_OPERATION_H_ + +#include <map> + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system_interface.h" + +namespace base { +class FilePath; +class ScopedClosureRunner; +class SequencedTaskRunner; +} // namespace base + +namespace drive { + +class JobScheduler; +class ResourceEntry; + +namespace internal { +class ResourceMetadata; +class FileCache; +} // namespace internal + +namespace file_system { + +class CreateFileOperation; +class DownloadOperation; +class OperationDelegate; + +class OpenFileOperation { + public: + OpenFileOperation(base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + JobScheduler* scheduler, + internal::ResourceMetadata* metadata, + internal::FileCache* cache, + const base::FilePath& temporary_file_directory); + ~OpenFileOperation(); + + // Opens the file at |file_path|. + // If the file is not actually downloaded, this method starts + // to download it to the cache, and then runs |callback| upon the + // completion with the path to the local cache file. + // See also the definition of OpenMode for its meaning. + // If |mime_type| is non empty and the file is created by this OpenFile() + // call, the mime type is used as the file's property. + // |callback| must not be null. + void OpenFile(const base::FilePath& file_path, + OpenMode open_mode, + const std::string& mime_type, + const OpenFileCallback& callback); + + private: + // Part of OpenFile(). Called after file creation is completed. + void OpenFileAfterCreateFile(const base::FilePath& file_path, + const OpenFileCallback& callback, + FileError error); + + // Part of OpenFile(). Called after file downloading is completed. + void OpenFileAfterFileDownloaded(const OpenFileCallback& callback, + FileError error, + const base::FilePath& local_file_path, + scoped_ptr<ResourceEntry> entry); + + // Part of OpenFile(). Called after opening the cache file. + void OpenFileAfterOpenForWrite( + const base::FilePath& local_file_path, + const std::string& local_id, + const OpenFileCallback& callback, + scoped_ptr<base::ScopedClosureRunner>* file_closer, + FileError error); + + // Closes the file with |local_id|. + void CloseFile(const std::string& local_id, + scoped_ptr<base::ScopedClosureRunner> file_closer); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + OperationDelegate* delegate_; + internal::FileCache* cache_; + + scoped_ptr<CreateFileOperation> create_file_operation_; + scoped_ptr<DownloadOperation> download_operation_; + + // The map from local id for an opened file to the number how many times + // the file is opened. + std::map<std::string, int> open_files_; + + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate its weak pointers before any other members are destroyed. + base::WeakPtrFactory<OpenFileOperation> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(OpenFileOperation); +}; + +} // namespace file_system +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_OPEN_FILE_OPERATION_H_ diff --git a/components/drive/file_system/open_file_operation_unittest.cc b/components/drive/file_system/open_file_operation_unittest.cc new file mode 100644 index 0000000..2e6f674 --- /dev/null +++ b/components/drive/file_system/open_file_operation_unittest.cc @@ -0,0 +1,257 @@ +// Copyright 2013 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 "components/drive/file_system/open_file_operation.h" + +#include <map> + +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/memory/scoped_ptr.h" +#include "base/task_runner_util.h" +#include "components/drive/drive.pb.h" +#include "components/drive/file_cache.h" +#include "components/drive/file_change.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system/operation_test_base.h" +#include "content/public/test/test_utils.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace file_system { + +class OpenFileOperationTest : public OperationTestBase { + protected: + void SetUp() override { + OperationTestBase::SetUp(); + + operation_.reset(new OpenFileOperation( + blocking_task_runner(), delegate(), scheduler(), metadata(), cache(), + temp_dir())); + } + + scoped_ptr<OpenFileOperation> operation_; +}; + +TEST_F(OpenFileOperationTest, OpenExistingFile) { + const base::FilePath file_in_root( + FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &src_entry)); + const int64 file_size = src_entry.file_info().size(); + + FileError error = FILE_ERROR_FAILED; + base::FilePath file_path; + base::Closure close_callback; + operation_->OpenFile( + file_in_root, + OPEN_FILE, + std::string(), // mime_type + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &close_callback)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(base::PathExists(file_path)); + int64 local_file_size; + ASSERT_TRUE(base::GetFileSize(file_path, &local_file_size)); + EXPECT_EQ(file_size, local_file_size); + + ASSERT_FALSE(close_callback.is_null()); + close_callback.Run(); + EXPECT_EQ(1U, delegate()->updated_local_ids().count(src_entry.local_id())); +} + +TEST_F(OpenFileOperationTest, OpenNonExistingFile) { + const base::FilePath file_in_root( + FILE_PATH_LITERAL("drive/root/not-exist.txt")); + + FileError error = FILE_ERROR_FAILED; + base::FilePath file_path; + base::Closure close_callback; + operation_->OpenFile( + file_in_root, + OPEN_FILE, + std::string(), // mime_type + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &close_callback)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, error); + EXPECT_TRUE(close_callback.is_null()); +} + +TEST_F(OpenFileOperationTest, CreateExistingFile) { + const base::FilePath file_in_root( + FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &src_entry)); + + FileError error = FILE_ERROR_FAILED; + base::FilePath file_path; + base::Closure close_callback; + operation_->OpenFile( + file_in_root, + CREATE_FILE, + std::string(), // mime_type + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &close_callback)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_EXISTS, error); + EXPECT_TRUE(close_callback.is_null()); +} + +TEST_F(OpenFileOperationTest, CreateNonExistingFile) { + const base::FilePath file_in_root( + FILE_PATH_LITERAL("drive/root/not-exist.txt")); + + FileError error = FILE_ERROR_FAILED; + base::FilePath file_path; + base::Closure close_callback; + operation_->OpenFile( + file_in_root, + CREATE_FILE, + std::string(), // mime_type + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &close_callback)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(1U, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(file_in_root)); + + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(base::PathExists(file_path)); + int64 local_file_size; + ASSERT_TRUE(base::GetFileSize(file_path, &local_file_size)); + EXPECT_EQ(0, local_file_size); // Should be an empty file. + + ASSERT_FALSE(close_callback.is_null()); + close_callback.Run(); + EXPECT_EQ(1U, + delegate()->updated_local_ids().count(GetLocalId(file_in_root))); +} + +TEST_F(OpenFileOperationTest, OpenOrCreateExistingFile) { + const base::FilePath file_in_root( + FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &src_entry)); + const int64 file_size = src_entry.file_info().size(); + + FileError error = FILE_ERROR_FAILED; + base::FilePath file_path; + base::Closure close_callback; + operation_->OpenFile( + file_in_root, + OPEN_OR_CREATE_FILE, + std::string(), // mime_type + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &close_callback)); + content::RunAllBlockingPoolTasksUntilIdle(); + + // Notified because 'available offline' status of the existing file changes. + EXPECT_EQ(1U, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(file_in_root)); + + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(base::PathExists(file_path)); + int64 local_file_size; + ASSERT_TRUE(base::GetFileSize(file_path, &local_file_size)); + EXPECT_EQ(file_size, local_file_size); + + ASSERT_FALSE(close_callback.is_null()); + close_callback.Run(); + EXPECT_EQ(1U, delegate()->updated_local_ids().count(src_entry.local_id())); + + ResourceEntry result_entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &result_entry)); + EXPECT_TRUE(result_entry.file_specific_info().cache_state().is_present()); + EXPECT_TRUE(result_entry.file_specific_info().cache_state().is_dirty()); +} + +TEST_F(OpenFileOperationTest, OpenOrCreateNonExistingFile) { + const base::FilePath file_in_root( + FILE_PATH_LITERAL("drive/root/not-exist.txt")); + + FileError error = FILE_ERROR_FAILED; + base::FilePath file_path; + base::Closure close_callback; + operation_->OpenFile( + file_in_root, + OPEN_OR_CREATE_FILE, + std::string(), // mime_type + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &close_callback)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(base::PathExists(file_path)); + int64 local_file_size; + ASSERT_TRUE(base::GetFileSize(file_path, &local_file_size)); + EXPECT_EQ(0, local_file_size); // Should be an empty file. + + ASSERT_FALSE(close_callback.is_null()); + close_callback.Run(); + EXPECT_EQ(1U, + delegate()->updated_local_ids().count(GetLocalId(file_in_root))); +} + +TEST_F(OpenFileOperationTest, OpenFileTwice) { + const base::FilePath file_in_root( + FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &src_entry)); + const int64 file_size = src_entry.file_info().size(); + + FileError error = FILE_ERROR_FAILED; + base::FilePath file_path; + base::Closure close_callback; + operation_->OpenFile( + file_in_root, + OPEN_FILE, + std::string(), // mime_type + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &close_callback)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(base::PathExists(file_path)); + int64 local_file_size; + ASSERT_TRUE(base::GetFileSize(file_path, &local_file_size)); + EXPECT_EQ(file_size, local_file_size); + + // Open again. + error = FILE_ERROR_FAILED; + base::Closure close_callback2; + operation_->OpenFile( + file_in_root, + OPEN_FILE, + std::string(), // mime_type + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &close_callback2)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(base::PathExists(file_path)); + ASSERT_TRUE(base::GetFileSize(file_path, &local_file_size)); + EXPECT_EQ(file_size, local_file_size); + + ASSERT_FALSE(close_callback.is_null()); + ASSERT_FALSE(close_callback2.is_null()); + + close_callback.Run(); + + // There still remains a client opening the file, so it shouldn't be + // uploaded yet. + EXPECT_TRUE(delegate()->updated_local_ids().empty()); + + close_callback2.Run(); + + // Here, all the clients close the file, so it should be uploaded then. + EXPECT_EQ(1U, delegate()->updated_local_ids().count(src_entry.local_id())); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/operation_delegate.cc b/components/drive/file_system/operation_delegate.cc new file mode 100644 index 0000000..f6e2dad --- /dev/null +++ b/components/drive/file_system/operation_delegate.cc @@ -0,0 +1,17 @@ +// Copyright 2014 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 "components/drive/file_system/operation_delegate.h" + +namespace drive { +namespace file_system { + +bool OperationDelegate::WaitForSyncComplete( + const std::string& local_id, + const FileOperationCallback& callback) { + return false; +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/operation_delegate.h b/components/drive/file_system/operation_delegate.h new file mode 100644 index 0000000..24f50a8 --- /dev/null +++ b/components/drive/file_system/operation_delegate.h @@ -0,0 +1,59 @@ +// Copyright 2014 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_OPERATION_DELEGATE_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_OPERATION_DELEGATE_H_ + +#include "components/drive/file_errors.h" + +namespace base { +class FilePath; +} + +namespace drive { + +struct ClientContext; +class FileChange; + +namespace file_system { + +// Error type of sync client. +// Keep it synced with "DriveSyncErrorType" in file_manager_private.idl. +enum DriveSyncErrorType { + // Request to delete a file without permission. + DRIVE_SYNC_ERROR_DELETE_WITHOUT_PERMISSION, + // Google Drive is temporary unavailable. + DRIVE_SYNC_ERROR_SERVICE_UNAVAILABLE, + // Errors other than above ones. No fallback is provided for the error. + DRIVE_SYNC_ERROR_MISC, +}; + +// Passes notifications from Drive operations back to the file system. +class OperationDelegate { + public: + // Sent when a content of a directory has been changed. + // |directory_path| is a virtual directory path representing the + // changed directory. + virtual void OnFileChangedByOperation(const FileChange& changed_files) {} + + // Sent when an entry is updated and sync is needed. The passed |context| is + // used for syncing. + virtual void OnEntryUpdatedByOperation(const ClientContext& context, + const std::string& local_id) {} + + // Sent when a specific drive sync error occurred. + // |local_id| is the local ID of the resource entry. + virtual void OnDriveSyncError(DriveSyncErrorType type, + const std::string& local_id) {} + + // Waits for the sync task to complete and runs the callback. + // Returns false if no task is found for the spcecified ID. + virtual bool WaitForSyncComplete(const std::string& local_id, + const FileOperationCallback& callback); +}; + +} // namespace file_system +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_OPERATION_DELEGATE_H_ diff --git a/components/drive/file_system/operation_test_base.cc b/components/drive/file_system/operation_test_base.cc new file mode 100644 index 0000000..603aed4 --- /dev/null +++ b/components/drive/file_system/operation_test_base.cc @@ -0,0 +1,195 @@ +// Copyright 2013 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 "components/drive/file_system/operation_test_base.h" + +#include "base/prefs/testing_pref_service.h" +#include "base/threading/sequenced_worker_pool.h" +#include "components/drive/change_list_loader.h" +#include "components/drive/event_logger.h" +#include "components/drive/fake_free_disk_space_getter.h" +#include "components/drive/file_cache.h" +#include "components/drive/file_change.h" +#include "components/drive/file_system/operation_delegate.h" +#include "components/drive/job_scheduler.h" +#include "components/drive/resource_metadata.h" +#include "components/drive/service/fake_drive_service.h" +#include "components/drive/service/test_util.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/test/test_utils.h" +#include "google_apis/drive/test_util.h" + +namespace drive { +namespace file_system { + +OperationTestBase::LoggingDelegate::LoggingDelegate() { +} + +OperationTestBase::LoggingDelegate::~LoggingDelegate() { +} + +void OperationTestBase::LoggingDelegate::OnFileChangedByOperation( + const FileChange& changed_files) { + changed_files_.Apply(changed_files); +} + +void OperationTestBase::LoggingDelegate::OnEntryUpdatedByOperation( + const ClientContext& /* client_context */, + const std::string& local_id) { + updated_local_ids_.insert(local_id); +} + +void OperationTestBase::LoggingDelegate::OnDriveSyncError( + DriveSyncErrorType type, const std::string& local_id) { + drive_sync_errors_.push_back(type); +} + +bool OperationTestBase::LoggingDelegate::WaitForSyncComplete( + const std::string& local_id, + const FileOperationCallback& callback) { + return wait_for_sync_complete_handler_.is_null() ? + false : wait_for_sync_complete_handler_.Run(local_id, callback); +} + +OperationTestBase::OperationTestBase() { +} + +OperationTestBase::OperationTestBase(int test_thread_bundle_options) + : thread_bundle_(test_thread_bundle_options) { +} + +OperationTestBase::~OperationTestBase() { +} + +void OperationTestBase::SetUp() { + scoped_refptr<base::SequencedWorkerPool> pool = + content::BrowserThread::GetBlockingPool(); + blocking_task_runner_ = + pool->GetSequencedTaskRunner(pool->GetSequenceToken()); + + pref_service_.reset(new TestingPrefServiceSimple); + test_util::RegisterDrivePrefs(pref_service_->registry()); + + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + + logger_.reset(new EventLogger); + + fake_drive_service_.reset(new FakeDriveService); + ASSERT_TRUE(test_util::SetUpTestEntries(fake_drive_service_.get())); + + scheduler_.reset(new JobScheduler( + pref_service_.get(), + logger_.get(), + fake_drive_service_.get(), + blocking_task_runner_.get())); + + metadata_storage_.reset(new internal::ResourceMetadataStorage( + temp_dir_.path(), blocking_task_runner_.get())); + bool success = false; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&internal::ResourceMetadataStorage::Initialize, + base::Unretained(metadata_storage_.get())), + google_apis::test_util::CreateCopyResultCallback(&success)); + content::RunAllBlockingPoolTasksUntilIdle(); + ASSERT_TRUE(success); + + fake_free_disk_space_getter_.reset(new FakeFreeDiskSpaceGetter); + cache_.reset(new internal::FileCache(metadata_storage_.get(), + temp_dir_.path(), + blocking_task_runner_.get(), + fake_free_disk_space_getter_.get())); + success = false; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&internal::FileCache::Initialize, + base::Unretained(cache_.get())), + google_apis::test_util::CreateCopyResultCallback(&success)); + content::RunAllBlockingPoolTasksUntilIdle(); + ASSERT_TRUE(success); + + metadata_.reset(new internal::ResourceMetadata(metadata_storage_.get(), + cache_.get(), + blocking_task_runner_)); + + FileError error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&internal::ResourceMetadata::Initialize, + base::Unretained(metadata_.get())), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + ASSERT_EQ(FILE_ERROR_OK, error); + + // Makes sure the FakeDriveService's content is loaded to the metadata_. + about_resource_loader_.reset(new internal::AboutResourceLoader( + scheduler_.get())); + loader_controller_.reset(new internal::LoaderController); + change_list_loader_.reset(new internal::ChangeListLoader( + logger_.get(), + blocking_task_runner_.get(), + metadata_.get(), + scheduler_.get(), + about_resource_loader_.get(), + loader_controller_.get())); + change_list_loader_->LoadIfNeeded( + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + ASSERT_EQ(FILE_ERROR_OK, error); +} + +FileError OperationTestBase::GetLocalResourceEntry(const base::FilePath& path, + ResourceEntry* entry) { + FileError error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::ResourceMetadata::GetResourceEntryByPath, + base::Unretained(metadata()), path, entry), + base::Bind(google_apis::test_util::CreateCopyResultCallback(&error))); + content::RunAllBlockingPoolTasksUntilIdle(); + return error; +} + +FileError OperationTestBase::GetLocalResourceEntryById( + const std::string& local_id, + ResourceEntry* entry) { + FileError error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::ResourceMetadata::GetResourceEntryById, + base::Unretained(metadata()), local_id, entry), + base::Bind(google_apis::test_util::CreateCopyResultCallback(&error))); + content::RunAllBlockingPoolTasksUntilIdle(); + return error; +} + +std::string OperationTestBase::GetLocalId(const base::FilePath& path) { + std::string local_id; + FileError error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::ResourceMetadata::GetIdByPath, + base::Unretained(metadata()), path, &local_id), + base::Bind(google_apis::test_util::CreateCopyResultCallback(&error))); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error) << path.value(); + return local_id; +} + +FileError OperationTestBase::CheckForUpdates() { + FileError error = FILE_ERROR_FAILED; + change_list_loader_->CheckForUpdates( + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + return error; +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/operation_test_base.h b/components/drive/file_system/operation_test_base.h new file mode 100644 index 0000000..bfdece00 --- /dev/null +++ b/components/drive/file_system/operation_test_base.h @@ -0,0 +1,166 @@ +// Copyright 2013 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_OPERATION_TEST_BASE_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_OPERATION_TEST_BASE_H_ + +#include <set> + +#include "base/files/scoped_temp_dir.h" +#include "components/drive/drive.pb.h" +#include "components/drive/drive_test_util.h" +#include "components/drive/file_change.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system/operation_delegate.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "testing/gtest/include/gtest/gtest.h" + +class TestingPrefServiceSimple; + +namespace base { +class SequencedTaskRunner; +} // namespace base + +namespace drive { +struct ClientContext; +class EventLogger; +class FakeDriveService; +class FakeFreeDiskSpaceGetter; +class JobScheduler; + +namespace internal { +class AboutResourceLoader; +class ChangeListLoader; +class FileCache; +class LoaderController; +class ResourceMetadata; +class ResourceMetadataStorage; +} // namespace internal + +namespace file_system { + +// Base fixture class for testing Drive file system operations. It sets up the +// basic set of Drive internal classes (ResourceMetadata, Cache, etc) on top of +// FakeDriveService for testing. +class OperationTestBase : public testing::Test { + protected: + // OperationDelegate that records all the events. + class LoggingDelegate : public OperationDelegate { + public: + typedef base::Callback<bool( + const std::string& local_id, + const FileOperationCallback& callback)> WaitForSyncCompleteHandler; + + LoggingDelegate(); + ~LoggingDelegate(); + + // OperationDelegate overrides. + void OnFileChangedByOperation(const FileChange& changed_files) override; + void OnEntryUpdatedByOperation(const ClientContext& context, + const std::string& local_id) override; + void OnDriveSyncError(DriveSyncErrorType type, + const std::string& local_id) override; + bool WaitForSyncComplete(const std::string& local_id, + const FileOperationCallback& callback) override; + + // Gets the set of changed paths. + const FileChange& get_changed_files() { return changed_files_; } + + // Gets the set of updated local IDs. + const std::set<std::string>& updated_local_ids() const { + return updated_local_ids_; + } + + // Gets the list of drive sync errors. + const std::vector<DriveSyncErrorType>& drive_sync_errors() const { + return drive_sync_errors_; + } + + // Sets the callback used to handle WaitForSyncComplete() method calls. + void set_wait_for_sync_complete_handler( + const WaitForSyncCompleteHandler& wait_for_sync_complete_handler) { + wait_for_sync_complete_handler_ = wait_for_sync_complete_handler; + } + + private: + FileChange changed_files_; + std::set<std::string> updated_local_ids_; + std::vector<DriveSyncErrorType> drive_sync_errors_; + WaitForSyncCompleteHandler wait_for_sync_complete_handler_; + }; + + OperationTestBase(); + explicit OperationTestBase(int test_thread_bundle_options); + ~OperationTestBase() override; + + // testing::Test overrides. + void SetUp() override; + + // Returns the path of the temporary directory for putting test files. + base::FilePath temp_dir() const { return temp_dir_.path(); } + + // Synchronously gets the resource entry corresponding to the path from local + // ResourceMetadta. + FileError GetLocalResourceEntry(const base::FilePath& path, + ResourceEntry* entry); + + // Synchronously gets the resource entry corresponding to the ID from local + // ResourceMetadta. + FileError GetLocalResourceEntryById(const std::string& local_id, + ResourceEntry* entry); + + // Gets the local ID of the entry specified by the path. + std::string GetLocalId(const base::FilePath& path); + + // Synchronously updates |metadata_| by fetching the change feed from the + // |fake_service_|. + FileError CheckForUpdates(); + + // Accessors for the components. + FakeDriveService* fake_service() { + return fake_drive_service_.get(); + } + EventLogger* logger() { return logger_.get(); } + LoggingDelegate* delegate() { return &delegate_; } + JobScheduler* scheduler() { return scheduler_.get(); } + base::SequencedTaskRunner* blocking_task_runner() { + return blocking_task_runner_.get(); + } + FakeFreeDiskSpaceGetter* fake_free_disk_space_getter() { + return fake_free_disk_space_getter_.get(); + } + internal::FileCache* cache() { return cache_.get(); } + internal::ResourceMetadata* metadata() { return metadata_.get(); } + internal::LoaderController* loader_controller() { + return loader_controller_.get(); + } + internal::ChangeListLoader* change_list_loader() { + return change_list_loader_.get(); + } + + private: + content::TestBrowserThreadBundle thread_bundle_; + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + scoped_ptr<TestingPrefServiceSimple> pref_service_; + base::ScopedTempDir temp_dir_; + + LoggingDelegate delegate_; + scoped_ptr<EventLogger> logger_; + scoped_ptr<FakeDriveService> fake_drive_service_; + scoped_ptr<JobScheduler> scheduler_; + scoped_ptr<internal::ResourceMetadataStorage, + test_util::DestroyHelperForTests> metadata_storage_; + scoped_ptr<FakeFreeDiskSpaceGetter> fake_free_disk_space_getter_; + scoped_ptr<internal::FileCache, test_util::DestroyHelperForTests> cache_; + scoped_ptr<internal::ResourceMetadata, test_util::DestroyHelperForTests> + metadata_; + scoped_ptr<internal::AboutResourceLoader> about_resource_loader_; + scoped_ptr<internal::LoaderController> loader_controller_; + scoped_ptr<internal::ChangeListLoader> change_list_loader_; +}; + +} // namespace file_system +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_OPERATION_TEST_BASE_H_ diff --git a/components/drive/file_system/remove_operation.cc b/components/drive/file_system/remove_operation.cc new file mode 100644 index 0000000..9cfc56c --- /dev/null +++ b/components/drive/file_system/remove_operation.cc @@ -0,0 +1,127 @@ +// Copyright (c) 2012 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 "components/drive/file_system/remove_operation.h" + +#include "base/sequenced_task_runner.h" +#include "components/drive/drive.pb.h" +#include "components/drive/file_cache.h" +#include "components/drive/file_change.h" +#include "components/drive/file_system/operation_delegate.h" +#include "components/drive/file_system_core_util.h" +#include "components/drive/job_scheduler.h" +#include "components/drive/resource_metadata.h" + +namespace drive { +namespace file_system { + +namespace { + +// Removes cache file and moves the metadata entry to the trash. +FileError UpdateLocalState(internal::ResourceMetadata* metadata, + internal::FileCache* cache, + const base::FilePath& path, + bool is_recursive, + std::string* local_id, + ResourceEntry* entry, + base::FilePath* changed_path) { + FileError error = metadata->GetIdByPath(path, local_id); + if (error != FILE_ERROR_OK) + return error; + + error = metadata->GetResourceEntryById(*local_id, entry); + if (error != FILE_ERROR_OK) + return error; + + if (entry->file_info().is_directory() && !is_recursive) { + // Check emptiness of the directory. + ResourceEntryVector entries; + error = metadata->ReadDirectoryByPath(path, &entries); + if (error != FILE_ERROR_OK) + return error; + if (!entries.empty()) + return FILE_ERROR_NOT_EMPTY; + } + + error = cache->Remove(*local_id); + if (error != FILE_ERROR_OK) + return error; + + *changed_path = path; + + // Move to the trash. + entry->set_parent_local_id(util::kDriveTrashDirLocalId); + return metadata->RefreshEntry(*entry); +} + +} // namespace + +RemoveOperation::RemoveOperation( + base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + internal::ResourceMetadata* metadata, + internal::FileCache* cache) + : blocking_task_runner_(blocking_task_runner), + delegate_(delegate), + metadata_(metadata), + cache_(cache), + weak_ptr_factory_(this) { +} + +RemoveOperation::~RemoveOperation() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +void RemoveOperation::Remove(const base::FilePath& path, + bool is_recursive, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + std::string* local_id = new std::string; + base::FilePath* changed_path = new base::FilePath; + ResourceEntry* entry = new ResourceEntry; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&UpdateLocalState, + metadata_, + cache_, + path, + is_recursive, + local_id, + entry, + changed_path), + base::Bind(&RemoveOperation::RemoveAfterUpdateLocalState, + weak_ptr_factory_.GetWeakPtr(), + callback, + base::Owned(local_id), + base::Owned(entry), + base::Owned(changed_path))); +} + +void RemoveOperation::RemoveAfterUpdateLocalState( + const FileOperationCallback& callback, + const std::string* local_id, + const ResourceEntry* entry, + const base::FilePath* changed_path, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (!changed_path->empty()) { + FileChange changed_file; + changed_file.Update(*changed_path, *entry, FileChange::CHANGE_TYPE_DELETE); + if (error == FILE_ERROR_OK) { + delegate_->OnFileChangedByOperation(changed_file); + delegate_->OnEntryUpdatedByOperation(ClientContext(USER_INITIATED), + *local_id); + } + } + + callback.Run(error); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/remove_operation.h b/components/drive/file_system/remove_operation.h new file mode 100644 index 0000000..538a91c --- /dev/null +++ b/components/drive/file_system/remove_operation.h @@ -0,0 +1,77 @@ +// Copyright (c) 2012 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_REMOVE_OPERATION_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_REMOVE_OPERATION_H_ + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "components/drive/file_errors.h" +#include "google_apis/drive/drive_api_error_codes.h" + +namespace base { +class FilePath; +class SequencedTaskRunner; +} // namespace base + +namespace drive { + +class ResourceEntry; + +namespace internal { +class FileCache; +class ResourceMetadata; +} // namespace internal + +namespace file_system { + +class OperationDelegate; + +// This class encapsulates the drive Remove function. It is responsible for +// moving the removed entry to the trash. +class RemoveOperation { + public: + RemoveOperation(base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + internal::ResourceMetadata* metadata, + internal::FileCache* cache); + ~RemoveOperation(); + + // Removes the resource at |path|. If |path| is a directory and |is_recursive| + // is set, it recursively removes all the descendants. If |is_recursive| is + // not set, it succeeds only when the directory is empty. + // + // |callback| must not be null. + void Remove(const base::FilePath& path, + bool is_recursive, + const FileOperationCallback& callback); + + private: + // Part of Remove(). Called after UpdateLocalState() completion. + void RemoveAfterUpdateLocalState(const FileOperationCallback& callback, + const std::string* local_id, + const ResourceEntry* entry, + const base::FilePath* changed_directory_path, + FileError error); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + OperationDelegate* delegate_; + internal::ResourceMetadata* metadata_; + internal::FileCache* cache_; + + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate the weak pointers before any other members are destroyed. + base::WeakPtrFactory<RemoveOperation> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(RemoveOperation); +}; + +} // namespace file_system +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_REMOVE_OPERATION_H_ diff --git a/components/drive/file_system/remove_operation_unittest.cc b/components/drive/file_system/remove_operation_unittest.cc new file mode 100644 index 0000000..5401275 --- /dev/null +++ b/components/drive/file_system/remove_operation_unittest.cc @@ -0,0 +1,132 @@ +// Copyright 2013 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 "components/drive/file_system/remove_operation.h" + +#include "components/drive/file_change.h" +#include "components/drive/file_system/operation_test_base.h" +#include "components/drive/file_system_core_util.h" +#include "content/public/test/test_utils.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace file_system { + +typedef OperationTestBase RemoveOperationTest; + +TEST_F(RemoveOperationTest, RemoveFile) { + RemoveOperation operation(blocking_task_runner(), delegate(), metadata(), + cache()); + + base::FilePath nonexisting_file( + FILE_PATH_LITERAL("drive/root/Dummy file.txt")); + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + base::FilePath file_in_subdir( + FILE_PATH_LITERAL("drive/root/Directory 1/SubDirectory File 1.txt")); + + // Remove a file in root. + ResourceEntry entry; + FileError error = FILE_ERROR_FAILED; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &entry)); + operation.Remove(file_in_root, + false, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, GetLocalResourceEntry(file_in_root, &entry)); + + const std::string id_file_in_root = entry.local_id(); + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntryById(id_file_in_root, &entry)); + EXPECT_EQ(util::kDriveTrashDirLocalId, entry.parent_local_id()); + + // Remove a file in subdirectory. + error = FILE_ERROR_FAILED; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_subdir, &entry)); + const std::string resource_id = entry.resource_id(); + + operation.Remove(file_in_subdir, + false, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, + GetLocalResourceEntry(file_in_subdir, &entry)); + + const std::string id_file_in_subdir = entry.local_id(); + EXPECT_EQ(FILE_ERROR_OK, + GetLocalResourceEntryById(id_file_in_subdir, &entry)); + EXPECT_EQ(util::kDriveTrashDirLocalId, entry.parent_local_id()); + + // Try removing non-existing file. + error = FILE_ERROR_FAILED; + ASSERT_EQ(FILE_ERROR_NOT_FOUND, + GetLocalResourceEntry(nonexisting_file, &entry)); + operation.Remove(nonexisting_file, + false, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, error); + + // Verify delegate notifications. + EXPECT_EQ(2U, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(file_in_root)); + EXPECT_TRUE(delegate()->get_changed_files().count(file_in_subdir)); + + EXPECT_EQ(2U, delegate()->updated_local_ids().size()); + EXPECT_TRUE(delegate()->updated_local_ids().count(id_file_in_root)); + EXPECT_TRUE(delegate()->updated_local_ids().count(id_file_in_subdir)); +} + +TEST_F(RemoveOperationTest, RemoveDirectory) { + RemoveOperation operation(blocking_task_runner(), delegate(), metadata(), + cache()); + + base::FilePath empty_dir(FILE_PATH_LITERAL( + "drive/root/Directory 1/Sub Directory Folder/Sub Sub Directory Folder")); + base::FilePath non_empty_dir(FILE_PATH_LITERAL( + "drive/root/Directory 1")); + base::FilePath file_in_non_empty_dir(FILE_PATH_LITERAL( + "drive/root/Directory 1/SubDirectory File 1.txt")); + + // Empty directory can be removed even with is_recursive = false. + FileError error = FILE_ERROR_FAILED; + ResourceEntry entry; + operation.Remove(empty_dir, + false, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, + GetLocalResourceEntry(empty_dir, &entry)); + + // Non-empty directory, cannot. + error = FILE_ERROR_FAILED; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(non_empty_dir, &entry)); + operation.Remove(non_empty_dir, + false, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_EMPTY, error); + EXPECT_EQ(FILE_ERROR_OK, + GetLocalResourceEntry(non_empty_dir, &entry)); + + // With is_recursive = true, it can be deleted, however. Descendant entries + // are removed together. + error = FILE_ERROR_FAILED; + ASSERT_EQ(FILE_ERROR_OK, + GetLocalResourceEntry(file_in_non_empty_dir, &entry)); + operation.Remove(non_empty_dir, + true, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, + GetLocalResourceEntry(non_empty_dir, &entry)); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, + GetLocalResourceEntry(file_in_non_empty_dir, &entry)); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/search_operation.cc b/components/drive/file_system/search_operation.cc new file mode 100644 index 0000000..a0be870 --- /dev/null +++ b/components/drive/file_system/search_operation.cc @@ -0,0 +1,180 @@ +// Copyright 2013 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 "components/drive/file_system/search_operation.h" + +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/callback.h" +#include "base/task_runner_util.h" +#include "components/drive/change_list_loader.h" +#include "components/drive/drive_api_util.h" +#include "components/drive/file_system_core_util.h" +#include "components/drive/job_scheduler.h" +#include "components/drive/resource_entry_conversion.h" +#include "components/drive/resource_metadata.h" +#include "google_apis/drive/drive_api_parser.h" +#include "url/gurl.h" + +namespace drive { +namespace file_system { +namespace { + +// Computes the path of each item in |file_list| returned from the server +// and stores to |result|, by using |resource_metadata|. If the metadata is not +// up-to-date and did not contain an item, adds the item to "drive/other" for +// temporally assigning a path. +FileError ResolveSearchResultOnBlockingPool( + internal::ResourceMetadata* resource_metadata, + scoped_ptr<google_apis::FileList> file_list, + std::vector<SearchResultInfo>* result) { + DCHECK(resource_metadata); + DCHECK(result); + + const ScopedVector<google_apis::FileResource>& entries = file_list->items(); + result->reserve(entries.size()); + for (size_t i = 0; i < entries.size(); ++i) { + std::string local_id; + FileError error = resource_metadata->GetIdByResourceId( + entries[i]->file_id(), &local_id); + + ResourceEntry entry; + if (error == FILE_ERROR_OK) + error = resource_metadata->GetResourceEntryById(local_id, &entry); + + if (error == FILE_ERROR_NOT_FOUND) { + std::string original_parent_id; + if (!ConvertFileResourceToResourceEntry(*entries[i], &entry, + &original_parent_id)) + continue; // Skip non-file entries. + + // The result is absent in local resource metadata. This can happen if + // the metadata is not synced to the latest server state yet. In that + // case, we temporarily add the file to the special "drive/other" + // directory in order to assign a path, which is needed to access the + // file through FileSystem API. + // + // It will be moved to the right place when the metadata gets synced + // in normal loading process in ChangeListProcessor. + entry.set_parent_local_id(util::kDriveOtherDirLocalId); + error = resource_metadata->AddEntry(entry, &local_id); + } + if (error != FILE_ERROR_OK) + return error; + base::FilePath path; + error = resource_metadata->GetFilePath(local_id, &path); + if (error != FILE_ERROR_OK) + return error; + result->push_back(SearchResultInfo(path, entry.file_info().is_directory())); + } + + return FILE_ERROR_OK; +} + +} // namespace + +SearchOperation::SearchOperation( + base::SequencedTaskRunner* blocking_task_runner, + JobScheduler* scheduler, + internal::ResourceMetadata* metadata, + internal::LoaderController* loader_controller) + : blocking_task_runner_(blocking_task_runner), + scheduler_(scheduler), + metadata_(metadata), + loader_controller_(loader_controller), + weak_ptr_factory_(this) { +} + +SearchOperation::~SearchOperation() { +} + +void SearchOperation::Search(const std::string& search_query, + const GURL& next_link, + const SearchCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (next_link.is_empty()) { + // This is first request for the |search_query|. + scheduler_->Search( + search_query, + base::Bind(&SearchOperation::SearchAfterGetFileList, + weak_ptr_factory_.GetWeakPtr(), callback)); + } else { + // There is the remaining result so fetch it. + scheduler_->GetRemainingFileList( + next_link, + base::Bind(&SearchOperation::SearchAfterGetFileList, + weak_ptr_factory_.GetWeakPtr(), callback)); + } +} + +void SearchOperation::SearchAfterGetFileList( + const SearchCallback& callback, + google_apis::DriveApiErrorCode gdata_error, + scoped_ptr<google_apis::FileList> file_list) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FileError error = GDataToFileError(gdata_error); + if (error != FILE_ERROR_OK) { + callback.Run(error, GURL(), scoped_ptr<std::vector<SearchResultInfo> >()); + return; + } + + DCHECK(file_list); + + GURL next_url = file_list->next_link(); + + scoped_ptr<std::vector<SearchResultInfo> > result( + new std::vector<SearchResultInfo>); + if (file_list->items().empty()) { + // Short cut. If the resource entry is empty, we don't need to refresh + // the resource metadata. + callback.Run(FILE_ERROR_OK, next_url, result.Pass()); + return; + } + + // ResolveSearchResultOnBlockingPool() may add entries newly created on the + // server to the local metadata. + // This may race with sync tasks so we should ask LoaderController here. + std::vector<SearchResultInfo>* result_ptr = result.get(); + loader_controller_->ScheduleRun(base::Bind( + base::IgnoreResult( + &base::PostTaskAndReplyWithResult<FileError, FileError>), + blocking_task_runner_, + FROM_HERE, + base::Bind(&ResolveSearchResultOnBlockingPool, + metadata_, + base::Passed(&file_list), + result_ptr), + base::Bind(&SearchOperation::SearchAfterResolveSearchResult, + weak_ptr_factory_.GetWeakPtr(), + callback, + next_url, + base::Passed(&result)))); +} + +void SearchOperation::SearchAfterResolveSearchResult( + const SearchCallback& callback, + const GURL& next_link, + scoped_ptr<std::vector<SearchResultInfo> > result, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + DCHECK(result); + + if (error != FILE_ERROR_OK) { + callback.Run(error, GURL(), scoped_ptr<std::vector<SearchResultInfo> >()); + return; + } + + callback.Run(error, next_link, result.Pass()); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/search_operation.h b/components/drive/file_system/search_operation.h new file mode 100644 index 0000000..920c52b --- /dev/null +++ b/components/drive/file_system/search_operation.h @@ -0,0 +1,86 @@ +// Copyright 2013 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_SEARCH_OPERATION_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_SEARCH_OPERATION_H_ + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system_interface.h" +#include "google_apis/drive/drive_api_error_codes.h" + +namespace base { +class SequencedTaskRunner; +} // namespace base + +namespace google_apis { +class FileList; +} // namespace google_apis + +namespace drive { + +class JobScheduler; + +namespace internal { +class LoaderController; +class ResourceMetadata; +} // namespace internal + +namespace file_system { + +// This class encapsulates the drive Search function. It is responsible for +// sending the request to the drive API. +class SearchOperation { + public: + SearchOperation(base::SequencedTaskRunner* blocking_task_runner, + JobScheduler* scheduler, + internal::ResourceMetadata* metadata, + internal::LoaderController* loader_controller); + ~SearchOperation(); + + // Performs server side content search operation for |search_query|. If + // |next_link| is set, this is the search result url that will be fetched. + // Upon completion, |callback| will be called with the result. This is + // implementation of FileSystemInterface::Search() method. + // + // |callback| must not be null. + void Search(const std::string& search_query, + const GURL& next_link, + const SearchCallback& callback); + + private: + // Part of Search(), called after the FileList is fetched from the server. + void SearchAfterGetFileList( + const SearchCallback& callback, + google_apis::DriveApiErrorCode gdata_error, + scoped_ptr<google_apis::FileList> file_list); + + // Part of Search(), called after |result| is filled on the blocking pool. + void SearchAfterResolveSearchResult( + const SearchCallback& callback, + const GURL& next_link, + scoped_ptr<std::vector<SearchResultInfo> > result, + FileError error); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + JobScheduler* scheduler_; + internal::ResourceMetadata* metadata_; + internal::LoaderController* loader_controller_; + + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate the weak pointers before any other members are destroyed. + base::WeakPtrFactory<SearchOperation> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(SearchOperation); +}; + +} // namespace file_system +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_SEARCH_OPERATION_H_ diff --git a/components/drive/file_system/search_operation_unittest.cc b/components/drive/file_system/search_operation_unittest.cc new file mode 100644 index 0000000..83ab746 --- /dev/null +++ b/components/drive/file_system/search_operation_unittest.cc @@ -0,0 +1,157 @@ +// Copyright 2013 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 "components/drive/file_system/search_operation.h" + +#include "base/callback_helpers.h" +#include "components/drive/change_list_loader.h" +#include "components/drive/file_system/operation_test_base.h" +#include "components/drive/service/fake_drive_service.h" +#include "content/public/test/test_utils.h" +#include "google_apis/drive/drive_api_parser.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace file_system { + +typedef OperationTestBase SearchOperationTest; + +TEST_F(SearchOperationTest, ContentSearch) { + SearchOperation operation(blocking_task_runner(), scheduler(), metadata(), + loader_controller()); + + std::set<std::string> expected_results; + expected_results.insert( + "drive/root/Directory 1/Sub Directory Folder/Sub Sub Directory Folder"); + expected_results.insert("drive/root/Directory 1/Sub Directory Folder"); + expected_results.insert("drive/root/Directory 1/SubDirectory File 1.txt"); + expected_results.insert("drive/root/Directory 1"); + expected_results.insert("drive/root/Directory 2 excludeDir-test"); + + FileError error = FILE_ERROR_FAILED; + GURL next_link; + scoped_ptr<std::vector<SearchResultInfo> > results; + + operation.Search("Directory", GURL(), + google_apis::test_util::CreateCopyResultCallback( + &error, &next_link, &results)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_TRUE(next_link.is_empty()); + EXPECT_EQ(expected_results.size(), results->size()); + for (size_t i = 0; i < results->size(); i++) { + EXPECT_TRUE(expected_results.count(results->at(i).path.AsUTF8Unsafe())) + << results->at(i).path.AsUTF8Unsafe(); + } +} + +TEST_F(SearchOperationTest, ContentSearchWithNewEntry) { + SearchOperation operation(blocking_task_runner(), scheduler(), metadata(), + loader_controller()); + + // Create a new directory in the drive service. + google_apis::DriveApiErrorCode status = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> server_entry; + fake_service()->AddNewDirectory( + fake_service()->GetRootResourceId(), "New Directory 1!", + AddNewDirectoryOptions(), + google_apis::test_util::CreateCopyResultCallback(&status, &server_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + ASSERT_EQ(google_apis::HTTP_CREATED, status); + + // As the result of the first Search(), only entries in the current file + // system snapshot are expected to be returned in the "right" path. New + // entries like "New Directory 1!" is temporarily added to "drive/other". + std::set<std::string> expected_results; + expected_results.insert("drive/root/Directory 1"); + expected_results.insert("drive/other/New Directory 1!"); + + FileError error = FILE_ERROR_FAILED; + GURL next_link; + scoped_ptr<std::vector<SearchResultInfo> > results; + + operation.Search("\"Directory 1\"", GURL(), + google_apis::test_util::CreateCopyResultCallback( + &error, &next_link, &results)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_TRUE(next_link.is_empty()); + ASSERT_EQ(expected_results.size(), results->size()); + for (size_t i = 0; i < results->size(); i++) { + EXPECT_TRUE(expected_results.count(results->at(i).path.AsUTF8Unsafe())) + << results->at(i).path.AsUTF8Unsafe(); + } + + // Load the change from FakeDriveService. + ASSERT_EQ(FILE_ERROR_OK, CheckForUpdates()); + + // Now the new entry must be reported to be in the right directory. + expected_results.clear(); + expected_results.insert("drive/root/Directory 1"); + expected_results.insert("drive/root/New Directory 1!"); + error = FILE_ERROR_FAILED; + operation.Search("\"Directory 1\"", GURL(), + google_apis::test_util::CreateCopyResultCallback( + &error, &next_link, &results)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_TRUE(next_link.is_empty()); + ASSERT_EQ(expected_results.size(), results->size()); + for (size_t i = 0; i < results->size(); i++) { + EXPECT_TRUE(expected_results.count(results->at(i).path.AsUTF8Unsafe())) + << results->at(i).path.AsUTF8Unsafe(); + } +} + +TEST_F(SearchOperationTest, ContentSearchEmptyResult) { + SearchOperation operation(blocking_task_runner(), scheduler(), metadata(), + loader_controller()); + + FileError error = FILE_ERROR_FAILED; + GURL next_link; + scoped_ptr<std::vector<SearchResultInfo> > results; + + operation.Search("\"no-match query\"", GURL(), + google_apis::test_util::CreateCopyResultCallback( + &error, &next_link, &results)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_TRUE(next_link.is_empty()); + EXPECT_EQ(0U, results->size()); +} + +TEST_F(SearchOperationTest, Lock) { + SearchOperation operation(blocking_task_runner(), scheduler(), metadata(), + loader_controller()); + + // Lock. + scoped_ptr<base::ScopedClosureRunner> lock = loader_controller()->GetLock(); + + // Search does not return the result as long as lock is alive. + FileError error = FILE_ERROR_FAILED; + GURL next_link; + scoped_ptr<std::vector<SearchResultInfo> > results; + + operation.Search("\"Directory 1\"", GURL(), + google_apis::test_util::CreateCopyResultCallback( + &error, &next_link, &results)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_FAILED, error); + EXPECT_FALSE(results); + + // Unlock, this should resume the pending search. + lock.reset(); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(results); + EXPECT_EQ(1u, results->size()); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/set_property_operation.cc b/components/drive/file_system/set_property_operation.cc new file mode 100644 index 0000000..0620f2e --- /dev/null +++ b/components/drive/file_system/set_property_operation.cc @@ -0,0 +1,122 @@ +// Copyright 2015 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 "components/drive/file_system/set_property_operation.h" + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/sequenced_task_runner.h" +#include "components/drive/drive.pb.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system/operation_delegate.h" +#include "components/drive/job_scheduler.h" +#include "components/drive/resource_metadata.h" + +namespace drive { +namespace file_system { + +namespace { + +// Adds the property to resource entry. Overwrites existing property if exists. +// If no change has been made (same key, visibility and value is already added) +// then FILE_ERROR_EXISTS is returned. +FileError UpdateLocalState(internal::ResourceMetadata* metadata, + const base::FilePath& file_path, + google_apis::drive::Property::Visibility visibility, + const std::string& key, + const std::string& value, + ResourceEntry* entry) { + using google_apis::drive::Property; + FileError error = metadata->GetResourceEntryByPath(file_path, entry); + if (error != FILE_ERROR_OK) + return error; + + Property_Visibility proto_visibility = Property_Visibility_PRIVATE; + switch (visibility) { + case Property::VISIBILITY_PRIVATE: + proto_visibility = Property_Visibility_PRIVATE; + break; + case Property::VISIBILITY_PUBLIC: + proto_visibility = Property_Visibility_PUBLIC; + break; + } + + ::drive::Property* property_to_update = nullptr; + for (auto& property : *entry->mutable_new_properties()) { + if (property.visibility() == proto_visibility && property.key() == key) { + // Exactly the same property exists, so don't update the local state. + if (property.value() == value) + return FILE_ERROR_EXISTS; + property_to_update = &property; + break; + } + } + + // If no property to update has been found, then add a new one. + if (!property_to_update) + property_to_update = entry->mutable_new_properties()->Add(); + + property_to_update->set_visibility(proto_visibility); + property_to_update->set_key(key); + property_to_update->set_value(value); + entry->set_metadata_edit_state(ResourceEntry::DIRTY); + entry->set_modification_date(base::Time::Now().ToInternalValue()); + + return metadata->RefreshEntry(*entry); +} + +} // namespace + +SetPropertyOperation::SetPropertyOperation( + base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + internal::ResourceMetadata* metadata) + : blocking_task_runner_(blocking_task_runner), + delegate_(delegate), + metadata_(metadata), + weak_ptr_factory_(this) { +} + +SetPropertyOperation::~SetPropertyOperation() { +} + +void SetPropertyOperation::SetProperty( + const base::FilePath& file_path, + google_apis::drive::Property::Visibility visibility, + const std::string& key, + const std::string& value, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + ResourceEntry* entry = new ResourceEntry; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), FROM_HERE, + base::Bind(&UpdateLocalState, metadata_, file_path, visibility, key, + value, entry), + base::Bind(&SetPropertyOperation::SetPropertyAfterUpdateLocalState, + weak_ptr_factory_.GetWeakPtr(), callback, base::Owned(entry))); +} + +void SetPropertyOperation::SetPropertyAfterUpdateLocalState( + const FileOperationCallback& callback, + const ResourceEntry* entry, + FileError result) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (result == FILE_ERROR_OK) { + // Do not notify about the file change, as properties are write only and + // cannot be read, so there is no visible change. + delegate_->OnEntryUpdatedByOperation(ClientContext(USER_INITIATED), + entry->local_id()); + } + + // Even if exists, return success, as the set property operation always + // overwrites existing values. + callback.Run(result == FILE_ERROR_EXISTS ? FILE_ERROR_OK : result); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/set_property_operation.h b/components/drive/file_system/set_property_operation.h new file mode 100644 index 0000000..2e04441 --- /dev/null +++ b/components/drive/file_system/set_property_operation.h @@ -0,0 +1,70 @@ +// Copyright 2015 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_SET_PROPERTY_OPERATION_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_SET_PROPERTY_OPERATION_H_ + +#include <string> + +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "components/drive/file_errors.h" +#include "google_apis/drive/drive_api_requests.h" + +namespace base { +class FilePath; +class SequencedTaskRunner; +} // namespace base + +namespace drive { +namespace internal { +class ResourceMetadata; +} // namespace internal + +class ResourceEntry; + +namespace file_system { + +class OperationDelegate; + +class SetPropertyOperation { + public: + SetPropertyOperation(base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + internal::ResourceMetadata* metadata); + ~SetPropertyOperation(); + + // Sets the |key| property on the entry at |drive_file_path| with the + // specified |visibility|. If the property already exists, it will be + // overwritten. + void SetProperty(const base::FilePath& drive_file_path, + google_apis::drive::Property::Visibility visibility, + const std::string& key, + const std::string& value, + const FileOperationCallback& callback); + + private: + // Part of SetProperty(). Runs after updating the local state. + void SetPropertyAfterUpdateLocalState(const FileOperationCallback& callback, + const ResourceEntry* entry, + FileError result); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + OperationDelegate* delegate_; + internal::ResourceMetadata* metadata_; + + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate the weak pointers before any other members are destroyed. + base::WeakPtrFactory<SetPropertyOperation> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(SetPropertyOperation); +}; + +} // namespace file_system +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_SET_PROPERTY_OPERATION_H_ diff --git a/components/drive/file_system/set_property_operation_unittest.cc b/components/drive/file_system/set_property_operation_unittest.cc new file mode 100644 index 0000000..093fbb2 --- /dev/null +++ b/components/drive/file_system/set_property_operation_unittest.cc @@ -0,0 +1,153 @@ +// Copyright 2015 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 "components/drive/file_system/set_property_operation.h" + +#include "base/files/file_path.h" +#include "components/drive/drive.pb.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system/operation_test_base.h" +#include "content/public/test/test_utils.h" +#include "google_apis/drive/drive_api_requests.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace file_system { +namespace { + +const base::FilePath::CharType kTestPath[] = + FILE_PATH_LITERAL("drive/root/File 1.txt"); +const char kTestKey[] = "key"; +const char kTestValue[] = "value"; +const char kTestAnotherValue[] = "another-value"; + +} // namespace + +typedef OperationTestBase SetPropertyOperationTest; + +TEST_F(SetPropertyOperationTest, SetProperty) { + SetPropertyOperation operation(blocking_task_runner(), delegate(), + metadata()); + + const base::FilePath test_path(kTestPath); + FileError result = FILE_ERROR_FAILED; + operation.SetProperty( + test_path, google_apis::drive::Property::Visibility::VISIBILITY_PRIVATE, + kTestKey, kTestValue, + google_apis::test_util::CreateCopyResultCallback(&result)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, result); + + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(test_path, &entry)); + EXPECT_EQ(ResourceEntry::DIRTY, entry.metadata_edit_state()); + ASSERT_EQ(1, entry.new_properties().size()); + const drive::Property property = entry.new_properties().Get(0); + EXPECT_EQ(Property_Visibility_PRIVATE, property.visibility()); + EXPECT_EQ(kTestKey, property.key()); + EXPECT_EQ(kTestValue, property.value()); + + EXPECT_EQ(0u, delegate()->get_changed_files().size()); + EXPECT_FALSE(delegate()->get_changed_files().count(test_path)); + + EXPECT_EQ(1u, delegate()->updated_local_ids().size()); + EXPECT_TRUE(delegate()->updated_local_ids().count(entry.local_id())); +} + +TEST_F(SetPropertyOperationTest, SetProperty_Duplicate) { + SetPropertyOperation operation(blocking_task_runner(), delegate(), + metadata()); + + const base::FilePath test_path(kTestPath); + FileError result = FILE_ERROR_FAILED; + operation.SetProperty( + test_path, google_apis::drive::Property::Visibility::VISIBILITY_PRIVATE, + kTestKey, kTestValue, + google_apis::test_util::CreateCopyResultCallback(&result)); + operation.SetProperty( + test_path, google_apis::drive::Property::Visibility::VISIBILITY_PRIVATE, + kTestKey, kTestValue, + google_apis::test_util::CreateCopyResultCallback(&result)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, result); + + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(test_path, &entry)); + EXPECT_EQ(1, entry.new_properties().size()); +} + +TEST_F(SetPropertyOperationTest, SetProperty_Overwrite) { + SetPropertyOperation operation(blocking_task_runner(), delegate(), + metadata()); + + const base::FilePath test_path(kTestPath); + FileError result = FILE_ERROR_FAILED; + operation.SetProperty( + test_path, google_apis::drive::Property::Visibility::VISIBILITY_PUBLIC, + kTestKey, kTestValue, + google_apis::test_util::CreateCopyResultCallback(&result)); + operation.SetProperty( + test_path, google_apis::drive::Property::Visibility::VISIBILITY_PUBLIC, + kTestKey, kTestAnotherValue, + google_apis::test_util::CreateCopyResultCallback(&result)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, result); + + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(test_path, &entry)); + ASSERT_EQ(1, entry.new_properties().size()); + const drive::Property property = entry.new_properties().Get(0); + EXPECT_EQ(Property_Visibility_PUBLIC, property.visibility()); + EXPECT_EQ(kTestKey, property.key()); + EXPECT_EQ(kTestAnotherValue, property.value()); +} + +TEST_F(SetPropertyOperationTest, SetProperty_DifferentVisibilities) { + SetPropertyOperation operation(blocking_task_runner(), delegate(), + metadata()); + + { + const base::FilePath test_path(kTestPath); + FileError result = FILE_ERROR_FAILED; + operation.SetProperty( + test_path, google_apis::drive::Property::Visibility::VISIBILITY_PRIVATE, + kTestKey, kTestValue, + google_apis::test_util::CreateCopyResultCallback(&result)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, result); + + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(test_path, &entry)); + ASSERT_EQ(1, entry.new_properties().size()); + const drive::Property property = entry.new_properties().Get(0); + EXPECT_EQ(Property_Visibility_PRIVATE, property.visibility()); + EXPECT_EQ(kTestKey, property.key()); + EXPECT_EQ(kTestValue, property.value()); + } + + // Insert another property with the same key, same value but different + // visibility. + { + const base::FilePath test_path(kTestPath); + FileError result = FILE_ERROR_FAILED; + operation.SetProperty( + test_path, google_apis::drive::Property::Visibility::VISIBILITY_PUBLIC, + kTestKey, kTestAnotherValue, + google_apis::test_util::CreateCopyResultCallback(&result)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, result); + + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(test_path, &entry)); + ASSERT_EQ(2, entry.new_properties().size()); + const drive::Property property = entry.new_properties().Get(1); + EXPECT_EQ(Property_Visibility_PUBLIC, property.visibility()); + EXPECT_EQ(kTestKey, property.key()); + EXPECT_EQ(kTestAnotherValue, property.value()); + } +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/touch_operation.cc b/components/drive/file_system/touch_operation.cc new file mode 100644 index 0000000..63272d7 --- /dev/null +++ b/components/drive/file_system/touch_operation.cc @@ -0,0 +1,96 @@ +// Copyright 2013 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 "components/drive/file_system/touch_operation.h" + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/sequenced_task_runner.h" +#include "base/time/time.h" +#include "components/drive/file_change.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system/operation_delegate.h" +#include "components/drive/job_scheduler.h" +#include "components/drive/resource_metadata.h" + +namespace drive { +namespace file_system { + +namespace { + +// Updates the timestamps of the entry specified by |file_path|. +FileError UpdateLocalState(internal::ResourceMetadata* metadata, + const base::FilePath& file_path, + const base::Time& last_access_time, + const base::Time& last_modified_time, + ResourceEntry* entry) { + FileError error = metadata->GetResourceEntryByPath(file_path, entry); + if (error != FILE_ERROR_OK) + return error; + + PlatformFileInfoProto* file_info = entry->mutable_file_info(); + if (!last_access_time.is_null()) + file_info->set_last_accessed(last_access_time.ToInternalValue()); + if (!last_modified_time.is_null()) + file_info->set_last_modified(last_modified_time.ToInternalValue()); + entry->set_metadata_edit_state(ResourceEntry::DIRTY); + entry->set_modification_date(base::Time::Now().ToInternalValue()); + return metadata->RefreshEntry(*entry); +} + +} // namespace + +TouchOperation::TouchOperation(base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + internal::ResourceMetadata* metadata) + : blocking_task_runner_(blocking_task_runner), + delegate_(delegate), + metadata_(metadata), + weak_ptr_factory_(this) { +} + +TouchOperation::~TouchOperation() { +} + +void TouchOperation::TouchFile(const base::FilePath& file_path, + const base::Time& last_access_time, + const base::Time& last_modified_time, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + ResourceEntry* entry = new ResourceEntry; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), FROM_HERE, + base::Bind(&UpdateLocalState, metadata_, file_path, last_access_time, + last_modified_time, entry), + base::Bind(&TouchOperation::TouchFileAfterUpdateLocalState, + weak_ptr_factory_.GetWeakPtr(), file_path, callback, + base::Owned(entry))); +} + +void TouchOperation::TouchFileAfterUpdateLocalState( + const base::FilePath& file_path, + const FileOperationCallback& callback, + const ResourceEntry* entry, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FileChange changed_files; + changed_files.Update(file_path, entry->file_info().is_directory() + ? FileChange::FILE_TYPE_DIRECTORY + : FileChange::FILE_TYPE_FILE, + FileChange::CHANGE_TYPE_ADD_OR_UPDATE); + + if (error == FILE_ERROR_OK) { + delegate_->OnFileChangedByOperation(changed_files); + delegate_->OnEntryUpdatedByOperation(ClientContext(USER_INITIATED), + entry->local_id()); + } + callback.Run(error); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/touch_operation.h b/components/drive/file_system/touch_operation.h new file mode 100644 index 0000000..46c53e7 --- /dev/null +++ b/components/drive/file_system/touch_operation.h @@ -0,0 +1,68 @@ +// Copyright 2013 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_TOUCH_OPERATION_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_TOUCH_OPERATION_H_ + +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "components/drive/file_errors.h" + +namespace base { +class FilePath; +class SequencedTaskRunner; +class Time; +} // namespace base + +namespace drive { +namespace internal { +class ResourceMetadata; +} // namespace internal + +class ResourceEntry; + +namespace file_system { + +class OperationDelegate; + +class TouchOperation { + public: + TouchOperation(base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + internal::ResourceMetadata* metadata); + ~TouchOperation(); + + // Touches the file by updating last access time and last modified time. + // Upon completion, invokes |callback|. + // |last_access_time|, |last_modified_time| and |callback| must not be null. + void TouchFile(const base::FilePath& file_path, + const base::Time& last_access_time, + const base::Time& last_modified_time, + const FileOperationCallback& callback); + + private: + // Part of TouchFile(). Runs after updating the local state. + void TouchFileAfterUpdateLocalState(const base::FilePath& file_path, + const FileOperationCallback& callback, + const ResourceEntry* entry, + FileError error); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + OperationDelegate* delegate_; + internal::ResourceMetadata* metadata_; + + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate the weak pointers before any other members are destroyed. + base::WeakPtrFactory<TouchOperation> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(TouchOperation); +}; + +} // namespace file_system +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_TOUCH_OPERATION_H_ diff --git a/components/drive/file_system/touch_operation_unittest.cc b/components/drive/file_system/touch_operation_unittest.cc new file mode 100644 index 0000000..c7b9cfe --- /dev/null +++ b/components/drive/file_system/touch_operation_unittest.cc @@ -0,0 +1,59 @@ +// Copyright 2013 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 "components/drive/file_system/touch_operation.h" + +#include "base/files/file_path.h" +#include "base/time/time.h" +#include "components/drive/drive.pb.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system/operation_test_base.h" +#include "components/drive/resource_metadata.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace file_system { + +typedef OperationTestBase TouchOperationTest; + +TEST_F(TouchOperationTest, TouchFile) { + TouchOperation operation(blocking_task_runner(), + delegate(), + metadata()); + + const base::FilePath kTestPath(FILE_PATH_LITERAL("drive/root/File 1.txt")); + const base::Time::Exploded kLastAccessTime = { + 2012, 7, 0, 19, 15, 59, 13, 123 + }; + const base::Time::Exploded kLastModifiedTime = { + 2013, 7, 0, 19, 15, 59, 13, 123 + }; + + FileError error = FILE_ERROR_FAILED; + operation.TouchFile( + kTestPath, + base::Time::FromUTCExploded(kLastAccessTime), + base::Time::FromUTCExploded(kLastModifiedTime), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kTestPath, &entry)); + EXPECT_EQ(base::Time::FromUTCExploded(kLastAccessTime), + base::Time::FromInternalValue(entry.file_info().last_accessed())); + EXPECT_EQ(base::Time::FromUTCExploded(kLastModifiedTime), + base::Time::FromInternalValue(entry.file_info().last_modified())); + EXPECT_EQ(ResourceEntry::DIRTY, entry.metadata_edit_state()); + + EXPECT_EQ(1U, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(kTestPath)); + + EXPECT_EQ(1U, delegate()->updated_local_ids().size()); + EXPECT_TRUE(delegate()->updated_local_ids().count(entry.local_id())); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/truncate_operation.cc b/components/drive/file_system/truncate_operation.cc new file mode 100644 index 0000000..7ab44b4 --- /dev/null +++ b/components/drive/file_system/truncate_operation.cc @@ -0,0 +1,145 @@ +// Copyright 2013 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 "components/drive/file_system/truncate_operation.h" + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/files/file.h" +#include "base/files/file_path.h" +#include "base/logging.h" +#include "base/sequenced_task_runner.h" +#include "base/task_runner_util.h" +#include "base/thread_task_runner_handle.h" +#include "components/drive/drive.pb.h" +#include "components/drive/file_cache.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system/download_operation.h" +#include "components/drive/file_system/operation_delegate.h" +#include "components/drive/job_scheduler.h" + +namespace drive { +namespace file_system { +namespace { + +// Truncates the local file at |local_cache_path| to the |length| bytes, +// then marks the resource is dirty on |cache|. +FileError TruncateOnBlockingPool(internal::ResourceMetadata* metadata, + internal::FileCache* cache, + const std::string& local_id, + const base::FilePath& local_cache_path, + int64 length) { + DCHECK(metadata); + DCHECK(cache); + + scoped_ptr<base::ScopedClosureRunner> file_closer; + FileError error = cache->OpenForWrite(local_id, &file_closer); + if (error != FILE_ERROR_OK) + return error; + + base::File file(local_cache_path, + base::File::FLAG_OPEN | base::File::FLAG_WRITE); + if (!file.IsValid()) + return FILE_ERROR_FAILED; + + if (!file.SetLength(length)) + return FILE_ERROR_FAILED; + + return FILE_ERROR_OK; +} + +} // namespace + +TruncateOperation::TruncateOperation( + base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + JobScheduler* scheduler, + internal::ResourceMetadata* metadata, + internal::FileCache* cache, + const base::FilePath& temporary_file_directory) + : blocking_task_runner_(blocking_task_runner), + delegate_(delegate), + metadata_(metadata), + cache_(cache), + download_operation_(new DownloadOperation(blocking_task_runner, + delegate, + scheduler, + metadata, + cache, + temporary_file_directory)), + weak_ptr_factory_(this) { +} + +TruncateOperation::~TruncateOperation() { +} + +void TruncateOperation::Truncate(const base::FilePath& file_path, + int64 length, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (length < 0) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, FILE_ERROR_INVALID_OPERATION)); + return; + } + + // TODO(kinaba): http://crbug.com/132780. + // Optimize the cases for small |length|, at least for |length| == 0. + download_operation_->EnsureFileDownloadedByPath( + file_path, + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + base::Bind(&TruncateOperation::TruncateAfterEnsureFileDownloadedByPath, + weak_ptr_factory_.GetWeakPtr(), length, callback)); +} + +void TruncateOperation::TruncateAfterEnsureFileDownloadedByPath( + int64 length, + const FileOperationCallback& callback, + FileError error, + const base::FilePath& local_file_path, + scoped_ptr<ResourceEntry> entry) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error != FILE_ERROR_OK) { + callback.Run(error); + return; + } + DCHECK(entry); + DCHECK(entry->has_file_specific_info()); + + if (entry->file_specific_info().is_hosted_document()) { + callback.Run(FILE_ERROR_INVALID_OPERATION); + return; + } + + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&TruncateOnBlockingPool, + metadata_, cache_, entry->local_id(), local_file_path, length), + base::Bind( + &TruncateOperation::TruncateAfterTruncateOnBlockingPool, + weak_ptr_factory_.GetWeakPtr(), entry->local_id(), callback)); +} + +void TruncateOperation::TruncateAfterTruncateOnBlockingPool( + const std::string& local_id, + const FileOperationCallback& callback, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + delegate_->OnEntryUpdatedByOperation(ClientContext(USER_INITIATED), local_id); + + callback.Run(error); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system/truncate_operation.h b/components/drive/file_system/truncate_operation.h new file mode 100644 index 0000000..ff22806 --- /dev/null +++ b/components/drive/file_system/truncate_operation.h @@ -0,0 +1,88 @@ +// Copyright 2013 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_TRUNCATE_OPERATION_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_TRUNCATE_OPERATION_H_ + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "components/drive/file_errors.h" + +namespace base { +class FilePath; +class SequencedTaskRunner; +} // namespace base + +namespace drive { + +class JobScheduler; +class ResourceEntry; + +namespace internal { +class FileCache; +class ResourceMetadata; +} // namespace internal + +namespace file_system { + +class OperationDelegate; +class DownloadOperation; + +// This class encapsulates the drive Truncate function. It is responsible for +// fetching the content from the Drive server if necessary, truncating the +// file content actually, and then notifying the file is locally modified and +// that it is necessary to upload the file to the server. +class TruncateOperation { + public: + TruncateOperation(base::SequencedTaskRunner* blocking_task_runner, + OperationDelegate* delegate, + JobScheduler* scheduler, + internal::ResourceMetadata* metadata, + internal::FileCache* cache, + const base::FilePath& temporary_file_directory); + ~TruncateOperation(); + + // Performs the truncate operation on the file at drive path |file_path| to + // |length| bytes. Invokes |callback| when finished with the result of the + // operation. |callback| must not be null. + void Truncate(const base::FilePath& file_path, + int64 length, + const FileOperationCallback& callback); + private: + // Part of Truncate(). Called after EnsureFileDownloadedByPath() is complete. + void TruncateAfterEnsureFileDownloadedByPath( + int64 length, + const FileOperationCallback& callback, + FileError error, + const base::FilePath& local_file_path, + scoped_ptr<ResourceEntry> resource_entry); + + // Part of Truncate(). Called after TruncateOnBlockingPool() is complete. + void TruncateAfterTruncateOnBlockingPool( + const std::string& local_id, + const FileOperationCallback& callback, + FileError error); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + OperationDelegate* delegate_; + internal::ResourceMetadata* metadata_; + internal::FileCache* cache_; + + scoped_ptr<DownloadOperation> download_operation_; + + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate the weak pointers before any other members are destroyed. + base::WeakPtrFactory<TruncateOperation> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(TruncateOperation); +}; + +} // namespace file_system +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_TRUNCATE_OPERATION_H_ diff --git a/components/drive/file_system/truncate_operation_unittest.cc b/components/drive/file_system/truncate_operation_unittest.cc new file mode 100644 index 0000000..31c5ad5 --- /dev/null +++ b/components/drive/file_system/truncate_operation_unittest.cc @@ -0,0 +1,134 @@ +// Copyright 2013 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 "components/drive/file_system/truncate_operation.h" + +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/task_runner_util.h" +#include "components/drive/drive.pb.h" +#include "components/drive/fake_free_disk_space_getter.h" +#include "components/drive/file_system/operation_test_base.h" +#include "content/public/test/test_utils.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace file_system { + +class TruncateOperationTest : public OperationTestBase { + protected: + void SetUp() override { + OperationTestBase::SetUp(); + + operation_.reset(new TruncateOperation( + blocking_task_runner(), delegate(), scheduler(), + metadata(), cache(), temp_dir())); + } + + scoped_ptr<TruncateOperation> operation_; +}; + +TEST_F(TruncateOperationTest, Truncate) { + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &src_entry)); + const int64 file_size = src_entry.file_info().size(); + + // Make sure the file has at least 2 bytes. + ASSERT_GE(file_size, 2); + + FileError error = FILE_ERROR_FAILED; + operation_->Truncate( + file_in_root, + 1, // Truncate to 1 byte. + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + base::FilePath local_path; + error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::FileCache::GetFile, + base::Unretained(cache()), + GetLocalId(file_in_root), &local_path), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + ASSERT_EQ(FILE_ERROR_OK, error); + + // The local file should be truncated. + int64 local_file_size = 0; + base::GetFileSize(local_path, &local_file_size); + EXPECT_EQ(1, local_file_size); +} + +TEST_F(TruncateOperationTest, NegativeSize) { + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &src_entry)); + const int64 file_size = src_entry.file_info().size(); + + // Make sure the file has at least 2 bytes. + ASSERT_GE(file_size, 2); + + FileError error = FILE_ERROR_FAILED; + operation_->Truncate( + file_in_root, + -1, // Truncate to "-1" byte. + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_INVALID_OPERATION, error); +} + +TEST_F(TruncateOperationTest, HostedDocument) { + base::FilePath file_in_root(FILE_PATH_LITERAL( + "drive/root/Document 1 excludeDir-test.gdoc")); + + FileError error = FILE_ERROR_FAILED; + operation_->Truncate( + file_in_root, + 1, // Truncate to 1 byte. + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_INVALID_OPERATION, error); +} + +TEST_F(TruncateOperationTest, Extend) { + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + ResourceEntry src_entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(file_in_root, &src_entry)); + const int64 file_size = src_entry.file_info().size(); + + FileError error = FILE_ERROR_FAILED; + operation_->Truncate( + file_in_root, + file_size + 10, // Extend to 10 bytes. + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + base::FilePath local_path; + error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::FileCache::GetFile, + base::Unretained(cache()), + GetLocalId(file_in_root), &local_path), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + ASSERT_EQ(FILE_ERROR_OK, error); + + // The local file should be truncated. + std::string content; + ASSERT_TRUE(base::ReadFileToString(local_path, &content)); + + EXPECT_EQ(file_size + 10, static_cast<int64>(content.size())); + // All trailing 10 bytes should be '\0'. + EXPECT_EQ(std::string(10, '\0'), content.substr(file_size)); +} + +} // namespace file_system +} // namespace drive diff --git a/components/drive/file_system_interface.cc b/components/drive/file_system_interface.cc new file mode 100644 index 0000000..931b680 --- /dev/null +++ b/components/drive/file_system_interface.cc @@ -0,0 +1,20 @@ +// Copyright 2015 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 "components/drive/file_system_interface.h" + +namespace drive { + +MetadataSearchResult::MetadataSearchResult( + const base::FilePath& path, + bool is_directory, + const std::string& highlighted_base_name, + const std::string& md5) + : path(path), + is_directory(is_directory), + highlighted_base_name(highlighted_base_name), + md5(md5) { +} + +} // namespace drive diff --git a/components/drive/file_system_interface.h b/components/drive/file_system_interface.h new file mode 100644 index 0000000..aa6b099 --- /dev/null +++ b/components/drive/file_system_interface.h @@ -0,0 +1,474 @@ +// Copyright (c) 2012 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_INTERFACE_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_INTERFACE_H_ + +#include <string> +#include <vector> + +#include "base/memory/scoped_ptr.h" +#include "components/drive/drive.pb.h" +#include "components/drive/file_system_metadata.h" +#include "components/drive/resource_metadata.h" +#include "google_apis/drive/base_requests.h" +#include "google_apis/drive/drive_api_requests.h" + +namespace drive { + +class FileSystemObserver; + +// Information about search result returned by Search Async callback. +// This is data needed to create a file system entry that will be used by file +// browser. +struct SearchResultInfo { + SearchResultInfo(const base::FilePath& path, bool is_directory) + : path(path), is_directory(is_directory) {} + + base::FilePath path; + bool is_directory; +}; + +// File path and its MD5 hash obtained from drive API. +struct HashAndFilePath { + std::string hash; + base::FilePath path; +}; + +// Struct to represent a search result for SearchMetadata(). +struct MetadataSearchResult { + MetadataSearchResult(const base::FilePath& path, + bool is_directory, + const std::string& highlighted_base_name, + const std::string& md5); + + // The two members are used to create FileEntry object. + base::FilePath path; + bool is_directory; + + // The base name to be displayed in the UI. The parts matched the search + // query are highlighted with <b> tag. Meta characters are escaped like < + // + // Why HTML? we could instead provide matched ranges using pairs of + // integers, but this is fragile as we'll eventually converting strings + // from UTF-8 (StringValue in base/values.h uses std::string) to UTF-16 + // when sending strings from C++ to JavaScript. + // + // Why <b> instead of <strong>? Because <b> is shorter. + std::string highlighted_base_name; + + // MD5 hash of the file. + std::string md5; +}; + +typedef std::vector<MetadataSearchResult> MetadataSearchResultVector; + +// Used to get a resource entry from the file system. +// If |error| is not FILE_ERROR_OK, |entry_info| is set to NULL. +typedef base::Callback<void(FileError error, + scoped_ptr<ResourceEntry> entry)> + GetResourceEntryCallback; + +// Used to get files from the file system. +typedef base::Callback<void(FileError error, + const base::FilePath& file_path, + scoped_ptr<ResourceEntry> entry)> GetFileCallback; + +// Used to get file content from the file system. +// If the file content is available in local cache, |local_file| is filled with +// the path to the cache file. If the file content starts to be downloaded from +// the server, |local_file| is empty. +typedef base::Callback<void(FileError error, + const base::FilePath& local_file, + scoped_ptr<ResourceEntry> entry)> + GetFileContentInitializedCallback; + +// Used to get list of entries under a directory. +typedef base::Callback<void(scoped_ptr<ResourceEntryVector> entries)> + ReadDirectoryEntriesCallback; + +// Used to get drive content search results. +// If |error| is not FILE_ERROR_OK, |result_paths| is empty. +typedef base::Callback<void( + FileError error, + const GURL& next_link, + scoped_ptr<std::vector<SearchResultInfo> > result_paths)> SearchCallback; + +// Callback for SearchMetadata(). On success, |error| is FILE_ERROR_OK, and +// |result| contains the search result. +typedef base::Callback<void( + FileError error, + scoped_ptr<MetadataSearchResultVector> result)> SearchMetadataCallback; + +// Callback for SearchByHashesCallback. On success, vector contains hash and +// corresponding files. The vector can include multiple entries for one hash. +typedef base::Callback<void(FileError, const std::vector<HashAndFilePath>&)> + SearchByHashesCallback; + +// Used to open files from the file system. |file_path| is the path on the local +// file system for the opened file. +// If |close_callback| is not null, it must be called when the +// modification to the cache is done. Otherwise, Drive file system does not +// pick up the file for uploading. +// |close_callback| must not be called more than once. +typedef base::Callback<void(FileError error, + const base::FilePath& file_path, + const base::Closure& close_callback)> + OpenFileCallback; + +// Used to get available space for the account from Drive. +typedef base::Callback<void(FileError error, + int64 bytes_total, + int64 bytes_used)> GetAvailableSpaceCallback; + +// Used to get the url to the sharing dialog. +typedef base::Callback<void(FileError error, + const GURL& share_url)> GetShareUrlCallback; + +// Used to get filesystem metadata. +typedef base::Callback<void(const FileSystemMetadata&)> + GetFilesystemMetadataCallback; + +// Used to mark cached files mounted. +typedef base::Callback<void(FileError error, + const base::FilePath& file_path)> + MarkMountedCallback; + +// Used to get file path. +typedef base::Callback<void(FileError error, const base::FilePath& file_path)> + GetFilePathCallback; + +// Used to free space. +typedef base::Callback<void(bool)> FreeDiskSpaceCallback; + +// Used for returning result of calculated cache size. +typedef base::Callback<void(uint64_t)> EvictableCacheSizeCallback; + +// The mode of opening a file. +enum OpenMode { + // Open the file if exists. If not, failed. + OPEN_FILE, + + // Create a new file if not exists, and then open it. If exists, failed. + CREATE_FILE, + + // Open the file if exists. If not, create a new file and then open it. + OPEN_OR_CREATE_FILE, +}; + +// Option enum to control eligible entries for SearchMetadata(). +// SEARCH_METADATA_ALL is the default to investigate all the entries. +// SEARCH_METADATA_EXCLUDE_HOSTED_DOCUMENTS excludes the hosted documents. +// SEARCH_METADATA_EXCLUDE_DIRECTORIES excludes the directories from the result. +// SEARCH_METADATA_SHARED_WITH_ME targets only "shared-with-me" entries. +// SEARCH_METADATA_OFFLINE targets only "offline" entries. This option can not +// be used with other options. +enum SearchMetadataOptions { + SEARCH_METADATA_ALL = 0, + SEARCH_METADATA_EXCLUDE_HOSTED_DOCUMENTS = 1, + SEARCH_METADATA_EXCLUDE_DIRECTORIES = 1 << 1, + SEARCH_METADATA_SHARED_WITH_ME = 1 << 2, + SEARCH_METADATA_OFFLINE = 1 << 3, +}; + +// Drive file system abstraction layer. +// The interface is defined to make FileSystem mockable. +class FileSystemInterface { + public: + virtual ~FileSystemInterface() {} + + // Adds and removes the observer. + virtual void AddObserver(FileSystemObserver* observer) = 0; + virtual void RemoveObserver(FileSystemObserver* observer) = 0; + + // Checks for updates on the server. + virtual void CheckForUpdates() = 0; + + // Initiates transfer of |local_src_file_path| to |remote_dest_file_path|. + // |local_src_file_path| must be a file from the local file system. + // |remote_dest_file_path| is the virtual destination path within Drive file + // system. + // + // |callback| must not be null. + virtual void TransferFileFromLocalToRemote( + const base::FilePath& local_src_file_path, + const base::FilePath& remote_dest_file_path, + const FileOperationCallback& callback) = 0; + + // Retrieves a file at the virtual path |file_path| on the Drive file system + // onto the cache, and mark it dirty. The local path to the cache file is + // returned to |callback|. After opening the file, both read and write + // on the file can be done with normal local file operations. + // If |mime_type| is set and the file is newly created, the mime type is + // set to the specified value. If |mime_type| is empty, it is guessed from + // |file_path|. + // + // |callback| must not be null. + virtual void OpenFile(const base::FilePath& file_path, + OpenMode open_mode, + const std::string& mime_type, + const OpenFileCallback& callback) = 0; + + // Copies |src_file_path| to |dest_file_path| on the file system. + // |src_file_path| can be a hosted document (see limitations below). + // |dest_file_path| is expected to be of the same type of |src_file_path| + // (i.e. if |src_file_path| is a file, |dest_file_path| will be created as + // a file). + // If |preserve_last_modified| is set to true, the last modified time will be + // preserved. This feature is only supported on Drive API v2 protocol because + // GData WAPI doesn't support updating modification time. + // + // This method also has the following assumptions/limitations that may be + // relaxed or addressed later: + // - |src_file_path| cannot be a regular file (i.e. non-hosted document) + // or a directory. + // - |dest_file_path| must not exist. + // - The parent of |dest_file_path| must already exist. + // + // The file entries represented by |src_file_path| and the parent directory + // of |dest_file_path| need to be present in the in-memory representation + // of the file system. + // + // |callback| must not be null. + virtual void Copy(const base::FilePath& src_file_path, + const base::FilePath& dest_file_path, + bool preserve_last_modified, + const FileOperationCallback& callback) = 0; + + // Moves |src_file_path| to |dest_file_path| on the file system. + // |src_file_path| can be a file (regular or hosted document) or a directory. + // |dest_file_path| is expected to be of the same type of |src_file_path| + // (i.e. if |src_file_path| is a file, |dest_file_path| will be created as + // a file). + // + // This method also has the following assumptions/limitations that may be + // relaxed or addressed later: + // - |dest_file_path| must not exist. + // - The parent of |dest_file_path| must already exist. + // + // The file entries represented by |src_file_path| and the parent directory + // of |dest_file_path| need to be present in the in-memory representation + // of the file system. + // + // |callback| must not be null. + virtual void Move(const base::FilePath& src_file_path, + const base::FilePath& dest_file_path, + const FileOperationCallback& callback) = 0; + + // Removes |file_path| from the file system. If |is_recursive| is set and + // |file_path| represents a directory, we will also delete all of its + // contained children elements. The file entry represented by |file_path| + // needs to be present in in-memory representation of the file system that + // in order to be removed. + // + // |callback| must not be null. + virtual void Remove(const base::FilePath& file_path, + bool is_recursive, + const FileOperationCallback& callback) = 0; + + // Creates new directory under |directory_path|. If |is_exclusive| is true, + // an error is raised in case a directory is already present at the + // |directory_path|. If |is_recursive| is true, the call creates parent + // directories as needed just like mkdir -p does. + // + // |callback| must not be null. + virtual void CreateDirectory(const base::FilePath& directory_path, + bool is_exclusive, + bool is_recursive, + const FileOperationCallback& callback) = 0; + + // Creates a file at |file_path|. If the flag |is_exclusive| is true, an + // error is raised when a file already exists at the path. It is + // an error if a directory or a hosted document is already present at the + // path, or the parent directory of the path is not present yet. + // If |mime_type| is set and the file is newly created, the mime type is + // set to the specified value. If |mime_type| is empty, it is guessed from + // |file_path|. + // + // |callback| must not be null. + virtual void CreateFile(const base::FilePath& file_path, + bool is_exclusive, + const std::string& mime_type, + const FileOperationCallback& callback) = 0; + + // Touches the file at |file_path| by updating the timestamp to + // |last_access_time| and |last_modified_time|. + // Upon completion, invokes |callback|. + // Note that, differently from unix touch command, this doesn't create a file + // if the target file doesn't exist. + // + // |last_access_time|, |last_modified_time| and |callback| must not be null. + virtual void TouchFile(const base::FilePath& file_path, + const base::Time& last_access_time, + const base::Time& last_modified_time, + const FileOperationCallback& callback) = 0; + + // Truncates the file content at |file_path| to the |length|. + // + // |callback| must not be null. + virtual void TruncateFile(const base::FilePath& file_path, + int64 length, + const FileOperationCallback& callback) = 0; + + // Pins a file at |file_path|. + // + // |callback| must not be null. + virtual void Pin(const base::FilePath& file_path, + const FileOperationCallback& callback) = 0; + + // Unpins a file at |file_path|. + // + // |callback| must not be null. + virtual void Unpin(const base::FilePath& file_path, + const FileOperationCallback& callback) = 0; + + // Makes sure that |file_path| in the file system is available in the local + // cache. If the file is not cached, the file will be downloaded. The entry + // needs to be present in the file system. + // + // Returns the cache path and entry info to |callback|. It must not be null. + virtual void GetFile(const base::FilePath& file_path, + const GetFileCallback& callback) = 0; + + // Makes sure that |file_path| in the file system is available in the local + // cache, and mark it as dirty. The next modification to the cache file is + // watched and is automatically uploaded to the server. If the entry is not + // present in the file system, it is created. + // + // Returns the cache path and entry info to |callback|. It must not be null. + virtual void GetFileForSaving(const base::FilePath& file_path, + const GetFileCallback& callback) = 0; + + // Gets a file by the given |file_path| and returns a closure to cancel the + // task. + // Calls |initialized_callback| when either: + // 1) The cached file (or JSON file for hosted file) is found, or + // 2) Starting to download the file from drive server. + // In case of 2), the given FilePath is empty, and |get_content_callback| is + // called repeatedly with downloaded content following the + // |initialized_callback| invocation. + // |completion_callback| is invoked if an error is found, or the operation + // is successfully done. + // |initialized_callback|, |get_content_callback| and |completion_callback| + // must not be null. + virtual base::Closure GetFileContent( + const base::FilePath& file_path, + const GetFileContentInitializedCallback& initialized_callback, + const google_apis::GetContentCallback& get_content_callback, + const FileOperationCallback& completion_callback) = 0; + + // Finds an entry (a file or a directory) by |file_path|. This call will also + // retrieve and refresh file system content from server and disk cache. + // + // |callback| must not be null. + virtual void GetResourceEntry(const base::FilePath& file_path, + const GetResourceEntryCallback& callback) = 0; + + // Finds and reads a directory by |file_path|. This call will also retrieve + // and refresh file system content from server and disk cache. + // |entries_callback| can be a null callback when not interested in entries. + // + // |completion_callback| must not be null. + virtual void ReadDirectory( + const base::FilePath& file_path, + const ReadDirectoryEntriesCallback& entries_callback, + const FileOperationCallback& completion_callback) = 0; + + // Does server side content search for |search_query|. + // If |next_link| is set, this is the search result url that will be + // fetched. Search results will be returned as a list of results' + // |SearchResultInfo| structs, which contains file's path and is_directory + // flag. + // + // |callback| must not be null. + virtual void Search(const std::string& search_query, + const GURL& next_link, + const SearchCallback& callback) = 0; + + // Searches the local resource metadata, and returns the entries + // |at_most_num_matches| that contain |query| in their base names. Search is + // done in a case-insensitive fashion. The eligible entries are selected based + // on the given |options|, which is a bit-wise OR of SearchMetadataOptions. + // SEARCH_METADATA_EXCLUDE_HOSTED_DOCUMENTS will be automatically added based + // on the preference. |callback| must not be null. Must be called on UI + // thread. Empty |query| matches any base name. i.e. returns everything. + virtual void SearchMetadata(const std::string& query, + int options, + int at_most_num_matches, + const SearchMetadataCallback& callback) = 0; + + // Searches the local resource metadata, and returns the entries that have the + // given |hashes|. The list of resource entries are passed to |callback|. The + // item of the list can be null if the corresponding file is not found. + // |callback| must not be null. + virtual void SearchByHashes(const std::set<std::string>& hashes, + const SearchByHashesCallback& callback) = 0; + + // Fetches the user's Account Metadata to find out current quota information + // and returns it to the callback. + virtual void GetAvailableSpace(const GetAvailableSpaceCallback& callback) = 0; + + // Fetches the url to the sharing dialog to be embedded in |embed_origin|, + // for the specified file or directory. |callback| must not be null. + virtual void GetShareUrl( + const base::FilePath& file_path, + const GURL& embed_origin, + const GetShareUrlCallback& callback) = 0; + + // Returns miscellaneous metadata of the file system like the largest + // timestamp. Used in chrome:drive-internals. |callback| must not be null. + virtual void GetMetadata( + const GetFilesystemMetadataCallback& callback) = 0; + + // Marks the cached file as mounted, and runs |callback| upon completion. + // If succeeded, the cached file path will be passed to the |callback|. + // |callback| must not be null. + virtual void MarkCacheFileAsMounted(const base::FilePath& drive_file_path, + const MarkMountedCallback& callback) = 0; + + // Marks the cached file as unmounted, and runs |callback| upon completion. + // Note that this method expects that the |cached_file_path| is the path + // returned by MarkCacheFileAsMounted(). + // |callback| must not be null. + virtual void MarkCacheFileAsUnmounted( + const base::FilePath& cache_file_path, + const FileOperationCallback& callback) = 0; + + // Adds permission as |role| to |email| for the entry at |drive_file_path|. + // |callback| must not be null. + virtual void AddPermission(const base::FilePath& drive_file_path, + const std::string& email, + google_apis::drive::PermissionRole role, + const FileOperationCallback& callback) = 0; + + // Sets the |key| property on the file or directory at |drive_file_path| with + // the specified |visibility|. If already exists, then it will be overwritten. + virtual void SetProperty(const base::FilePath& drive_file_path, + google_apis::drive::Property::Visibility visibility, + const std::string& key, + const std::string& value, + const FileOperationCallback& callback) = 0; + + // Resets local data. + virtual void Reset(const FileOperationCallback& callback) = 0; + + // Finds a path of an entry (a file or a directory) by |resource_id|. + virtual void GetPathFromResourceId(const std::string& resource_id, + const GetFilePathCallback& callback) = 0; + + // Free drive caches if needed to secure given available spaces. |callback| + // takes whether given bytes are available or not. + virtual void FreeDiskSpaceIfNeededFor( + int64 num_bytes, + const FreeDiskSpaceCallback& callback) = 0; + + // Calculates evictable cache size. + // |callback| must not be null. + virtual void CalculateEvictableCacheSize( + const EvictableCacheSizeCallback& callback) = 0; +}; + +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_INTERFACE_H_ diff --git a/components/drive/file_system_metadata.cc b/components/drive/file_system_metadata.cc new file mode 100644 index 0000000..7c53d53 --- /dev/null +++ b/components/drive/file_system_metadata.cc @@ -0,0 +1,18 @@ +// Copyright (c) 2012 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 "components/drive/file_system_metadata.h" + +namespace drive { + +FileSystemMetadata::FileSystemMetadata() + : largest_changestamp(0), + refreshing(false), + last_update_check_error(FILE_ERROR_OK) { +} + +FileSystemMetadata::~FileSystemMetadata() { +} + +} // namespace drive diff --git a/components/drive/file_system_metadata.h b/components/drive/file_system_metadata.h new file mode 100644 index 0000000..199f106 --- /dev/null +++ b/components/drive/file_system_metadata.h @@ -0,0 +1,35 @@ +// Copyright (c) 2012 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. + +#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_METADATA_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_METADATA_H_ + +#include "base/basictypes.h" +#include "base/time/time.h" +#include "components/drive/file_errors.h" + +namespace drive { + +// Metadata of FileSystem. Used by FileSystem::GetMetadata(). +struct FileSystemMetadata { + FileSystemMetadata(); + ~FileSystemMetadata(); + + // The largest changestamp that the file system holds (may be different + // from the one on the server) + int64 largest_changestamp; + + // True if the resource metadata is now being fetched from the server. + bool refreshing; + + // Time of the last update check. + base::Time last_update_check_time; + + // Error code of the last update check. + FileError last_update_check_error; +}; + +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_METADATA_H_ diff --git a/components/drive/file_write_watcher.cc b/components/drive/file_write_watcher.cc new file mode 100644 index 0000000..c1b4815 --- /dev/null +++ b/components/drive/file_write_watcher.cc @@ -0,0 +1,207 @@ +// Copyright 2013 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 "components/drive/file_write_watcher.h" + +#include <map> +#include <vector> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/files/file_path_watcher.h" +#include "base/stl_util.h" +#include "base/timer/timer.h" +#include "google_apis/drive/task_util.h" + +namespace drive { +namespace internal { + +namespace { +const int64 kWriteEventDelayInSeconds = 5; +} // namespace + +// base::FileWatcher needs to live in a thread that is allowed to do File IO +// and has a TYPE_IO message loop: that is, FILE thread. This class bridges the +// UI thread and FILE thread, and does all the main tasks in the FILE thread. +class FileWriteWatcher::FileWriteWatcherImpl { + public: + explicit FileWriteWatcherImpl(base::SingleThreadTaskRunner* file_task_runner); + + // Forwards the call to DestoryOnFileThread(). This method must be used to + // destruct the instance. + void Destroy(); + + // Forwards the call to StartWatchOnFileThread(). |on_start_callback| is + // called back on the caller (UI) thread when the watch has started. + // |on_write_callback| is called when a write has happened to the path. + void StartWatch(const base::FilePath& path, + const StartWatchCallback& on_start_callback, + const base::Closure& on_write_callback); + + void set_delay(base::TimeDelta delay) { delay_ = delay; } + + private: + ~FileWriteWatcherImpl(); + + void DestroyOnFileThread(); + + void StartWatchOnFileThread(const base::FilePath& path, + const StartWatchCallback& on_start_callback, + const base::Closure& on_write_callback); + + void OnWriteEvent(const base::FilePath& path, bool error); + + void InvokeCallback(const base::FilePath& path); + + struct PathWatchInfo { + std::vector<base::Closure> on_write_callbacks; + base::FilePathWatcher watcher; + base::Timer timer; + + explicit PathWatchInfo(const base::Closure& on_write_callback) + : on_write_callbacks(1, on_write_callback), + timer(false /* retain_closure_on_reset */, false /* is_repeating */) { + } + }; + + base::TimeDelta delay_; + std::map<base::FilePath, PathWatchInfo*> watchers_; + scoped_refptr<base::SingleThreadTaskRunner> file_task_runner_; + + base::ThreadChecker thread_checker_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate its weak pointers before any other members are destroyed. + base::WeakPtrFactory<FileWriteWatcherImpl> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(FileWriteWatcherImpl); +}; + +FileWriteWatcher::FileWriteWatcherImpl::FileWriteWatcherImpl( + base::SingleThreadTaskRunner* file_task_runner) + : delay_(base::TimeDelta::FromSeconds(kWriteEventDelayInSeconds)), + file_task_runner_(file_task_runner), + weak_ptr_factory_(this) { +} + +void FileWriteWatcher::FileWriteWatcherImpl::Destroy() { + DCHECK(thread_checker_.CalledOnValidThread()); + + // Just forwarding the call to FILE thread. + file_task_runner_->PostTask( + FROM_HERE, base::Bind(&FileWriteWatcherImpl::DestroyOnFileThread, + base::Unretained(this))); +} + +void FileWriteWatcher::FileWriteWatcherImpl::StartWatch( + const base::FilePath& path, + const StartWatchCallback& on_start_callback, + const base::Closure& on_write_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + // Forwarding the call to FILE thread and relaying the |callback|. + file_task_runner_->PostTask( + FROM_HERE, + base::Bind(&FileWriteWatcherImpl::StartWatchOnFileThread, + base::Unretained(this), path, + google_apis::CreateRelayCallback(on_start_callback), + google_apis::CreateRelayCallback(on_write_callback))); +} + +FileWriteWatcher::FileWriteWatcherImpl::~FileWriteWatcherImpl() { + DCHECK(file_task_runner_->BelongsToCurrentThread()); + + STLDeleteContainerPairSecondPointers(watchers_.begin(), watchers_.end()); +} + +void FileWriteWatcher::FileWriteWatcherImpl::DestroyOnFileThread() { + DCHECK(file_task_runner_->BelongsToCurrentThread()); + + delete this; +} + +void FileWriteWatcher::FileWriteWatcherImpl::StartWatchOnFileThread( + const base::FilePath& path, + const StartWatchCallback& on_start_callback, + const base::Closure& on_write_callback) { + DCHECK(file_task_runner_->BelongsToCurrentThread()); + + std::map<base::FilePath, PathWatchInfo*>::iterator it = watchers_.find(path); + if (it != watchers_.end()) { + // We are already watching the path. + on_start_callback.Run(true); + it->second->on_write_callbacks.push_back(on_write_callback); + return; + } + + // Start watching |path|. + scoped_ptr<PathWatchInfo> info(new PathWatchInfo(on_write_callback)); + bool ok = info->watcher.Watch( + path, + false, // recursive + base::Bind(&FileWriteWatcherImpl::OnWriteEvent, + weak_ptr_factory_.GetWeakPtr())); + watchers_[path] = info.release(); + on_start_callback.Run(ok); +} + +void FileWriteWatcher::FileWriteWatcherImpl::OnWriteEvent( + const base::FilePath& path, + bool error) { + DCHECK(file_task_runner_->BelongsToCurrentThread()); + + if (error) + return; + + std::map<base::FilePath, PathWatchInfo*>::iterator it = watchers_.find(path); + DCHECK(it != watchers_.end()); + + // Heuristics for detecting the end of successive write operations. + // Delay running on_write_event_callback by |delay_| time, and if OnWriteEvent + // is called again in the period, the timer is reset. In other words, we + // invoke callback when |delay_| has passed after the last OnWriteEvent(). + it->second->timer.Start(FROM_HERE, + delay_, + base::Bind(&FileWriteWatcherImpl::InvokeCallback, + weak_ptr_factory_.GetWeakPtr(), + path)); +} + +void FileWriteWatcher::FileWriteWatcherImpl::InvokeCallback( + const base::FilePath& path) { + DCHECK(file_task_runner_->BelongsToCurrentThread()); + + std::map<base::FilePath, PathWatchInfo*>::iterator it = watchers_.find(path); + DCHECK(it != watchers_.end()); + + std::vector<base::Closure> callbacks; + callbacks.swap(it->second->on_write_callbacks); + delete it->second; + watchers_.erase(it); + + for (size_t i = 0; i < callbacks.size(); ++i) + callbacks[i].Run(); +} + +FileWriteWatcher::FileWriteWatcher( + base::SingleThreadTaskRunner* file_task_runner) + : watcher_impl_(new FileWriteWatcherImpl(file_task_runner)) { +} + +FileWriteWatcher::~FileWriteWatcher() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +void FileWriteWatcher::StartWatch(const base::FilePath& file_path, + const StartWatchCallback& on_start_callback, + const base::Closure& on_write_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + watcher_impl_->StartWatch(file_path, on_start_callback, on_write_callback); +} + +void FileWriteWatcher::DisableDelayForTesting() { + watcher_impl_->set_delay(base::TimeDelta()); +} + +} // namespace internal +} // namespace drive diff --git a/components/drive/file_write_watcher.h b/components/drive/file_write_watcher.h new file mode 100644 index 0000000..719a8b9 --- /dev/null +++ b/components/drive/file_write_watcher.h @@ -0,0 +1,66 @@ +// Copyright 2013 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. + +#ifndef COMPONENTS_DRIVE_FILE_WRITE_WATCHER_H_ +#define COMPONENTS_DRIVE_FILE_WRITE_WATCHER_H_ + +#include "base/callback_forward.h" +#include "base/memory/scoped_ptr.h" +#include "base/threading/thread_checker.h" +#include "components/drive/file_system_core_util.h" + +namespace base { +class FilePath; +class SingleThreadTaskRunner; +} // namespace base + +namespace drive { + +namespace internal { + +typedef base::Callback<void(bool)> StartWatchCallback; + +// The class watches modification to Drive files in the cache directory. +// This is used for returning a local writable snapshot of Drive files from the +// Save-As file dialog, so that the callers of the dialog can save to Drive +// without any special handling about Drive. +class FileWriteWatcher { + public: + explicit FileWriteWatcher(base::SingleThreadTaskRunner* file_task_runner); + ~FileWriteWatcher(); + + // Starts watching the modification to |path|. When it successfully started + // watching, it runs |on_start_callback| by passing true as the argument. + // Or if it failed, the callback is run with false. + // Detected modification is notified by calling |on_write_callback|. + // + // Currently, the modification is watched in "one-shot" manner. That is, once + // a modification is notified, the watch is deactivated for freeing system + // resources. As a heuristic to capture the real end of write operations that + // might be done by several chunked writes, the notification is fired after + // 5 seconds has passed after the last write operation is detected. + // + // TODO(kinaba): investigate the possibility to continuously watch the whole + // cache directory. http://crbug.com/269424 + void StartWatch(const base::FilePath& path, + const StartWatchCallback& on_start_callback, + const base::Closure& on_write_callback); + + // For testing purpose, stops inserting delay between the write detection and + // notification to the |on_write_callback|. + void DisableDelayForTesting(); + + private: + class FileWriteWatcherImpl; + scoped_ptr<FileWriteWatcherImpl, util::DestroyHelper> watcher_impl_; + + base::ThreadChecker thread_checker_; + + DISALLOW_COPY_AND_ASSIGN(FileWriteWatcher); +}; + +} // namespace internal +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_WRITE_WATCHER_H_ diff --git a/components/drive/file_write_watcher_unittest.cc b/components/drive/file_write_watcher_unittest.cc new file mode 100644 index 0000000..abd9d51 --- /dev/null +++ b/components/drive/file_write_watcher_unittest.cc @@ -0,0 +1,122 @@ +// Copyright 2013 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 "components/drive/file_write_watcher.h" + +#include <set> + +#include "base/bind.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/run_loop.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace internal { + +namespace { + +class TestObserver { + public: + // After all the resource_ids in |expected_upload| are notified for the + // need of uploading, runs |quit_closure|. Also checks that each id is + // notified only once. + TestObserver(const std::set<std::string>& expected_upload, + const base::Closure& quit_closure) + : expected_upload_(expected_upload), + quit_closure_(quit_closure) { + } + + void OnWrite(const std::string& id) { + EXPECT_EQ(1U, expected_upload_.count(id)) << id; + expected_upload_.erase(id); + if (expected_upload_.empty()) + quit_closure_.Run(); + } + + private: + std::set<std::string> expected_upload_; + base::Closure quit_closure_; +}; + +// Writes something on the file at |path|. +void WriteSomethingAfterStartWatch(const base::FilePath& path, + bool watch_success) { + EXPECT_TRUE(watch_success) << path.value(); + + const char kDummy[] = "hello"; + ASSERT_TRUE(base::WriteFile(path, kDummy, arraysize(kDummy))); +} + +class FileWriteWatcherTest : public testing::Test { + protected: + // The test requires UI thread (FileWriteWatcher DCHECKs that its public + // interface is running on UI thread) and FILE thread (Linux version of + // base::FilePathWatcher needs to live on an IOAllowed thread with TYPE_IO, + // which is FILE thread in the production environment). + // + // By using the IO_MAINLOOP test thread bundle, the main thread is used + // both as UI and FILE thread, with TYPE_IO message loop. + FileWriteWatcherTest() + : thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP) { + } + + void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); } + + base::FilePath GetTempPath(const std::string& name) { + return temp_dir_.path().Append(name); + } + + private: + content::TestBrowserThreadBundle thread_bundle_; + base::ScopedTempDir temp_dir_; +}; + +} // namespace + +TEST_F(FileWriteWatcherTest, WatchThreeFiles) { + std::set<std::string> expected; + expected.insert("1"); + expected.insert("2"); + expected.insert("3"); + + base::RunLoop loop; + TestObserver observer(expected, loop.QuitClosure()); + + // Set up the watcher. + scoped_refptr<base::SingleThreadTaskRunner> file_task_runner = + content::BrowserThread::GetMessageLoopProxyForThread( + content::BrowserThread::FILE); + FileWriteWatcher watcher(file_task_runner.get()); + watcher.DisableDelayForTesting(); + + // Start watching and running. + base::FilePath path1 = GetTempPath("foo.txt"); + base::FilePath path2 = GetTempPath("bar.png"); + base::FilePath path3 = GetTempPath("buz.doc"); + base::FilePath path4 = GetTempPath("mya.mp3"); + watcher.StartWatch( + path1, + base::Bind(&WriteSomethingAfterStartWatch, path1), + base::Bind(&TestObserver::OnWrite, base::Unretained(&observer), "1")); + watcher.StartWatch( + path2, + base::Bind(&WriteSomethingAfterStartWatch, path2), + base::Bind(&TestObserver::OnWrite, base::Unretained(&observer), "2")); + watcher.StartWatch( + path3, + base::Bind(&WriteSomethingAfterStartWatch, path3), + base::Bind(&TestObserver::OnWrite, base::Unretained(&observer), "3")); + + // Unwatched write. It shouldn't be notified. + WriteSomethingAfterStartWatch(path4, true); + + // The loop should quit if all the three paths are notified to be written. + loop.Run(); +} + +} // namespace internal +} // namespace drive |