diff options
Diffstat (limited to 'components/drive')
32 files changed, 8879 insertions, 9 deletions
diff --git a/components/drive/BUILD.gn b/components/drive/BUILD.gn index 4567878..f4596bd 100644 --- a/components/drive/BUILD.gn +++ b/components/drive/BUILD.gn @@ -11,6 +11,8 @@ source_set("drive") { "change_list_loader_observer.h", "change_list_processor.cc", "change_list_processor.h", + "directory_loader.cc", + "directory_loader.h", "drive_api_util.cc", "drive_api_util.h", "drive_app_registry.cc", @@ -31,6 +33,8 @@ source_set("drive") { "file_change.h", "file_errors.cc", "file_errors.h", + "file_system.cc", + "file_system.h", "file_system/copy_operation.cc", "file_system/copy_operation.h", "file_system/create_directory_operation.cc", @@ -63,6 +67,7 @@ source_set("drive") { "file_system_interface.h", "file_system_metadata.cc", "file_system_metadata.h", + "file_system_observer.h", "file_write_watcher.cc", "file_write_watcher.h", "job_list.cc", @@ -73,16 +78,28 @@ source_set("drive") { "job_scheduler.h", "local_file_reader.cc", "local_file_reader.h", + "remove_stale_cache_files.cc", + "remove_stale_cache_files.h", "resource_entry_conversion.cc", "resource_entry_conversion.h", "resource_metadata.cc", "resource_metadata.h", "resource_metadata_storage.cc", "resource_metadata_storage.h", + "search_metadata.cc", + "search_metadata.h", "service/drive_api_service.cc", "service/drive_api_service.h", "service/drive_service_interface.cc", "service/drive_service_interface.h", + "sync/entry_revert_performer.cc", + "sync/entry_revert_performer.h", + "sync/entry_update_performer.cc", + "sync/entry_update_performer.h", + "sync/remove_performer.cc", + "sync/remove_performer.h", + "sync_client.cc", + "sync_client.h", ] deps = [ "//base:base", @@ -113,6 +130,10 @@ source_set("test_support") { sources = [ "drive_test_util.cc", "drive_test_util.h", + "dummy_file_system.cc", + "dummy_file_system.h", + "fake_file_system.cc", + "fake_file_system.h", "fake_free_disk_space_getter.cc", "fake_free_disk_space_getter.h", "service/dummy_drive_service.cc", diff --git a/components/drive/DEPS b/components/drive/DEPS index 9f76475..48aeee1 100644 --- a/components/drive/DEPS +++ b/components/drive/DEPS @@ -23,11 +23,14 @@ specific_include_rules = { r"|create_file_operation_unittest\.cc" r"|download_operation_unittest\.cc" r"|drive_test_util\.h" + r"|entry_revert_performer_unittest\.cc" + r"|entry_update_performer_unittest\.cc" 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"|remove_performer_unittest\.cc" r"|search_operation_unittest\.cc" r"|set_property_operation_unittest\.cc" r"|truncate_operation_unittest\.cc" @@ -37,22 +40,30 @@ specific_include_rules = { # The following test dependencies should be removed to fully componentize this # directory. crbug.com/498951 - r"(change_list_loader_unittest.cc" - 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"(change_list_loader_unittest\.cc" + r"|change_list_processor_unittest\.cc" + r"|directory_loader_unittest\.cc" + r"|fake_file_system_unittest\.cc" + r"|file_cache_unittest\.cc" + r"|file_system_core_util_unittest\.cc" + r"|file_system_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"|remove_stale_cache_files_unittest\.cc" + r"|resource_metadata_storage_unittest\.cc" + r"|resource_metadata_unittest\.cc" + r"|search_metadata_unittest\.cc" + r"|sync_client_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"(fake_file_system\.cc" + r"|file_system_unittest.cc" + r"|file_write_watcher_unittest\.cc" r"|get_file_for_saving_operation_unittest\.cc" r"|operation_test_base\.cc" r")": [ diff --git a/components/drive/directory_loader.cc b/components/drive/directory_loader.cc new file mode 100644 index 0000000..cd0f049 --- /dev/null +++ b/components/drive/directory_loader.cc @@ -0,0 +1,571 @@ +// 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/directory_loader.h" + +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/metrics/histogram.h" +#include "base/strings/string_number_conversions.h" +#include "base/time/time.h" +#include "components/drive/change_list_loader.h" +#include "components/drive/change_list_loader_observer.h" +#include "components/drive/change_list_processor.h" +#include "components/drive/event_logger.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_parser.h" +#include "url/gurl.h" + +namespace drive { +namespace internal { + +namespace { + +// Minimum changestamp gap required to start loading directory. +const int kMinimumChangestampGap = 50; + +FileError CheckLocalState(ResourceMetadata* resource_metadata, + const google_apis::AboutResource& about_resource, + const std::string& local_id, + ResourceEntry* entry, + int64* local_changestamp) { + // Fill My Drive resource ID. + ResourceEntry mydrive; + FileError error = resource_metadata->GetResourceEntryByPath( + util::GetDriveMyDriveRootPath(), &mydrive); + if (error != FILE_ERROR_OK) + return error; + + if (mydrive.resource_id().empty()) { + mydrive.set_resource_id(about_resource.root_folder_id()); + error = resource_metadata->RefreshEntry(mydrive); + if (error != FILE_ERROR_OK) + return error; + } + + // Get entry. + error = resource_metadata->GetResourceEntryById(local_id, entry); + if (error != FILE_ERROR_OK) + return error; + + // Get the local changestamp. + return resource_metadata->GetLargestChangestamp(local_changestamp); +} + +FileError UpdateChangestamp(ResourceMetadata* resource_metadata, + const DirectoryFetchInfo& directory_fetch_info, + base::FilePath* directory_path) { + // Update the directory changestamp. + ResourceEntry directory; + FileError error = resource_metadata->GetResourceEntryById( + directory_fetch_info.local_id(), &directory); + if (error != FILE_ERROR_OK) + return error; + + if (!directory.file_info().is_directory()) + return FILE_ERROR_NOT_A_DIRECTORY; + + directory.mutable_directory_specific_info()->set_changestamp( + directory_fetch_info.changestamp()); + error = resource_metadata->RefreshEntry(directory); + if (error != FILE_ERROR_OK) + return error; + + // Get the directory path. + return resource_metadata->GetFilePath(directory_fetch_info.local_id(), + directory_path); +} + +} // namespace + +struct DirectoryLoader::ReadDirectoryCallbackState { + ReadDirectoryEntriesCallback entries_callback; + FileOperationCallback completion_callback; + std::set<std::string> sent_entry_names; +}; + +// Fetches the resource entries in the directory with |directory_resource_id|. +class DirectoryLoader::FeedFetcher { + public: + FeedFetcher(DirectoryLoader* loader, + const DirectoryFetchInfo& directory_fetch_info, + const std::string& root_folder_id) + : loader_(loader), + directory_fetch_info_(directory_fetch_info), + root_folder_id_(root_folder_id), + weak_ptr_factory_(this) { + } + + ~FeedFetcher() { + } + + void Run(const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + DCHECK(!directory_fetch_info_.resource_id().empty()); + + // Remember the time stamp for usage stats. + start_time_ = base::TimeTicks::Now(); + + loader_->scheduler_->GetFileListInDirectory( + directory_fetch_info_.resource_id(), + base::Bind(&FeedFetcher::OnFileListFetched, + weak_ptr_factory_.GetWeakPtr(), callback)); + } + + private: + void OnFileListFetched(const FileOperationCallback& callback, + google_apis::DriveApiErrorCode status, + scoped_ptr<google_apis::FileList> file_list) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FileError error = GDataToFileError(status); + if (error != FILE_ERROR_OK) { + callback.Run(error); + return; + } + + DCHECK(file_list); + scoped_ptr<ChangeList> change_list(new ChangeList(*file_list)); + GURL next_url = file_list->next_link(); + + ResourceEntryVector* entries = new ResourceEntryVector; + loader_->loader_controller_->ScheduleRun(base::Bind( + base::IgnoreResult( + &base::PostTaskAndReplyWithResult<FileError, FileError>), + loader_->blocking_task_runner_, + FROM_HERE, + base::Bind(&ChangeListProcessor::RefreshDirectory, + loader_->resource_metadata_, + directory_fetch_info_, + base::Passed(&change_list), + entries), + base::Bind(&FeedFetcher::OnDirectoryRefreshed, + weak_ptr_factory_.GetWeakPtr(), + callback, + next_url, + base::Owned(entries)))); + } + + void OnDirectoryRefreshed( + const FileOperationCallback& callback, + const GURL& next_url, + const std::vector<ResourceEntry>* refreshed_entries, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error != FILE_ERROR_OK) { + callback.Run(error); + return; + } + + loader_->SendEntries(directory_fetch_info_.local_id(), *refreshed_entries); + + if (!next_url.is_empty()) { + // There is the remaining result so fetch it. + loader_->scheduler_->GetRemainingFileList( + next_url, + base::Bind(&FeedFetcher::OnFileListFetched, + weak_ptr_factory_.GetWeakPtr(), callback)); + return; + } + + UMA_HISTOGRAM_TIMES("Drive.DirectoryFeedLoadTime", + base::TimeTicks::Now() - start_time_); + + // Note: The fetcher is managed by DirectoryLoader, and the instance + // will be deleted in the callback. Do not touch the fields after this + // invocation. + callback.Run(FILE_ERROR_OK); + } + + DirectoryLoader* loader_; + DirectoryFetchInfo directory_fetch_info_; + std::string root_folder_id_; + base::TimeTicks start_time_; + base::ThreadChecker thread_checker_; + base::WeakPtrFactory<FeedFetcher> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(FeedFetcher); +}; + +DirectoryLoader::DirectoryLoader( + EventLogger* logger, + base::SequencedTaskRunner* blocking_task_runner, + ResourceMetadata* resource_metadata, + JobScheduler* scheduler, + AboutResourceLoader* about_resource_loader, + LoaderController* loader_controller) + : logger_(logger), + blocking_task_runner_(blocking_task_runner), + resource_metadata_(resource_metadata), + scheduler_(scheduler), + about_resource_loader_(about_resource_loader), + loader_controller_(loader_controller), + weak_ptr_factory_(this) { +} + +DirectoryLoader::~DirectoryLoader() { + STLDeleteElements(&fast_fetch_feed_fetcher_set_); +} + +void DirectoryLoader::AddObserver(ChangeListLoaderObserver* observer) { + DCHECK(thread_checker_.CalledOnValidThread()); + observers_.AddObserver(observer); +} + +void DirectoryLoader::RemoveObserver(ChangeListLoaderObserver* observer) { + DCHECK(thread_checker_.CalledOnValidThread()); + observers_.RemoveObserver(observer); +} + +void DirectoryLoader::ReadDirectory( + const base::FilePath& directory_path, + const ReadDirectoryEntriesCallback& entries_callback, + const FileOperationCallback& completion_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!completion_callback.is_null()); + + ResourceEntry* entry = new ResourceEntry; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&ResourceMetadata::GetResourceEntryByPath, + base::Unretained(resource_metadata_), + directory_path, + entry), + base::Bind(&DirectoryLoader::ReadDirectoryAfterGetEntry, + weak_ptr_factory_.GetWeakPtr(), + directory_path, + entries_callback, + completion_callback, + true, // should_try_loading_parent + base::Owned(entry))); +} + +void DirectoryLoader::ReadDirectoryAfterGetEntry( + const base::FilePath& directory_path, + const ReadDirectoryEntriesCallback& entries_callback, + const FileOperationCallback& completion_callback, + bool should_try_loading_parent, + const ResourceEntry* entry, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!completion_callback.is_null()); + + if (error == FILE_ERROR_NOT_FOUND && + should_try_loading_parent && + util::GetDriveGrandRootPath().IsParent(directory_path)) { + // This entry may be found after loading the parent. + ReadDirectory(directory_path.DirName(), + ReadDirectoryEntriesCallback(), + base::Bind(&DirectoryLoader::ReadDirectoryAfterLoadParent, + weak_ptr_factory_.GetWeakPtr(), + directory_path, + entries_callback, + completion_callback)); + return; + } + if (error != FILE_ERROR_OK) { + completion_callback.Run(error); + return; + } + + if (!entry->file_info().is_directory()) { + completion_callback.Run(FILE_ERROR_NOT_A_DIRECTORY); + return; + } + + DirectoryFetchInfo directory_fetch_info( + entry->local_id(), + entry->resource_id(), + entry->directory_specific_info().changestamp()); + + // Register the callback function to be called when it is loaded. + const std::string& local_id = directory_fetch_info.local_id(); + ReadDirectoryCallbackState callback_state; + callback_state.entries_callback = entries_callback; + callback_state.completion_callback = completion_callback; + pending_load_callback_[local_id].push_back(callback_state); + + // If loading task for |local_id| is already running, do nothing. + if (pending_load_callback_[local_id].size() > 1) + return; + + about_resource_loader_->GetAboutResource( + base::Bind(&DirectoryLoader::ReadDirectoryAfterGetAboutResource, + weak_ptr_factory_.GetWeakPtr(), local_id)); +} + +void DirectoryLoader::ReadDirectoryAfterLoadParent( + const base::FilePath& directory_path, + const ReadDirectoryEntriesCallback& entries_callback, + const FileOperationCallback& completion_callback, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!completion_callback.is_null()); + + if (error != FILE_ERROR_OK) { + completion_callback.Run(error); + return; + } + + ResourceEntry* entry = new ResourceEntry; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&ResourceMetadata::GetResourceEntryByPath, + base::Unretained(resource_metadata_), + directory_path, + entry), + base::Bind(&DirectoryLoader::ReadDirectoryAfterGetEntry, + weak_ptr_factory_.GetWeakPtr(), + directory_path, + entries_callback, + completion_callback, + false, // should_try_loading_parent + base::Owned(entry))); +} + +void DirectoryLoader::ReadDirectoryAfterGetAboutResource( + const std::string& local_id, + google_apis::DriveApiErrorCode status, + scoped_ptr<google_apis::AboutResource> about_resource) { + DCHECK(thread_checker_.CalledOnValidThread()); + + FileError error = GDataToFileError(status); + if (error != FILE_ERROR_OK) { + OnDirectoryLoadComplete(local_id, error); + return; + } + + DCHECK(about_resource); + + // Check the current status of local metadata, and start loading if needed. + google_apis::AboutResource* about_resource_ptr = about_resource.get(); + ResourceEntry* entry = new ResourceEntry; + int64* local_changestamp = new int64; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&CheckLocalState, + resource_metadata_, + *about_resource_ptr, + local_id, + entry, + local_changestamp), + base::Bind(&DirectoryLoader::ReadDirectoryAfterCheckLocalState, + weak_ptr_factory_.GetWeakPtr(), + base::Passed(&about_resource), + local_id, + base::Owned(entry), + base::Owned(local_changestamp))); +} + +void DirectoryLoader::ReadDirectoryAfterCheckLocalState( + scoped_ptr<google_apis::AboutResource> about_resource, + const std::string& local_id, + const ResourceEntry* entry, + const int64* local_changestamp, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(about_resource); + + if (error != FILE_ERROR_OK) { + OnDirectoryLoadComplete(local_id, error); + return; + } + // This entry does not exist on the server. + if (entry->resource_id().empty()) { + OnDirectoryLoadComplete(local_id, FILE_ERROR_OK); + return; + } + + int64 remote_changestamp = about_resource->largest_change_id(); + + // Start loading the directory. + int64 directory_changestamp = std::max( + entry->directory_specific_info().changestamp(), *local_changestamp); + + DirectoryFetchInfo directory_fetch_info( + local_id, entry->resource_id(), remote_changestamp); + + // If the directory's changestamp is up-to-date or the global changestamp of + // the metadata DB is new enough (which means the normal changelist loading + // should finish very soon), just schedule to run the callback, as there is no + // need to fetch the directory. + if (directory_changestamp >= remote_changestamp || + *local_changestamp + kMinimumChangestampGap > remote_changestamp) { + OnDirectoryLoadComplete(local_id, FILE_ERROR_OK); + } else { + // Start fetching the directory content, and mark it with the changestamp + // |remote_changestamp|. + LoadDirectoryFromServer(directory_fetch_info); + } +} + +void DirectoryLoader::OnDirectoryLoadComplete(const std::string& local_id, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + + LoadCallbackMap::iterator it = pending_load_callback_.find(local_id); + if (it == pending_load_callback_.end()) + return; + + // No need to read metadata when no one needs entries. + bool needs_to_send_entries = false; + for (size_t i = 0; i < it->second.size(); ++i) { + const ReadDirectoryCallbackState& callback_state = it->second[i]; + if (!callback_state.entries_callback.is_null()) + needs_to_send_entries = true; + } + + if (!needs_to_send_entries) { + OnDirectoryLoadCompleteAfterRead(local_id, NULL, FILE_ERROR_OK); + return; + } + + ResourceEntryVector* entries = new ResourceEntryVector; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&ResourceMetadata::ReadDirectoryById, + base::Unretained(resource_metadata_), local_id, entries), + base::Bind(&DirectoryLoader::OnDirectoryLoadCompleteAfterRead, + weak_ptr_factory_.GetWeakPtr(), + local_id, + base::Owned(entries))); +} + +void DirectoryLoader::OnDirectoryLoadCompleteAfterRead( + const std::string& local_id, + const ResourceEntryVector* entries, + FileError error) { + LoadCallbackMap::iterator it = pending_load_callback_.find(local_id); + if (it != pending_load_callback_.end()) { + DVLOG(1) << "Running callback for " << local_id; + + if (error == FILE_ERROR_OK && entries) + SendEntries(local_id, *entries); + + for (size_t i = 0; i < it->second.size(); ++i) { + const ReadDirectoryCallbackState& callback_state = it->second[i]; + callback_state.completion_callback.Run(error); + } + pending_load_callback_.erase(it); + } +} + +void DirectoryLoader::SendEntries(const std::string& local_id, + const ResourceEntryVector& entries) { + LoadCallbackMap::iterator it = pending_load_callback_.find(local_id); + DCHECK(it != pending_load_callback_.end()); + + for (size_t i = 0; i < it->second.size(); ++i) { + ReadDirectoryCallbackState* callback_state = &it->second[i]; + if (callback_state->entries_callback.is_null()) + continue; + + // Filter out entries which were already sent. + scoped_ptr<ResourceEntryVector> entries_to_send(new ResourceEntryVector); + for (size_t i = 0; i < entries.size(); ++i) { + const ResourceEntry& entry = entries[i]; + if (!callback_state->sent_entry_names.count(entry.base_name())) { + callback_state->sent_entry_names.insert(entry.base_name()); + entries_to_send->push_back(entry); + } + } + callback_state->entries_callback.Run(entries_to_send.Pass()); + } +} + +void DirectoryLoader::LoadDirectoryFromServer( + const DirectoryFetchInfo& directory_fetch_info) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!directory_fetch_info.empty()); + DVLOG(1) << "Start loading directory: " << directory_fetch_info.ToString(); + + const google_apis::AboutResource* about_resource = + about_resource_loader_->cached_about_resource(); + DCHECK(about_resource); + + logger_->Log(logging::LOG_INFO, + "Fast-fetch start: %s; Server changestamp: %s", + directory_fetch_info.ToString().c_str(), + base::Int64ToString( + about_resource->largest_change_id()).c_str()); + + FeedFetcher* fetcher = new FeedFetcher(this, + directory_fetch_info, + about_resource->root_folder_id()); + fast_fetch_feed_fetcher_set_.insert(fetcher); + fetcher->Run( + base::Bind(&DirectoryLoader::LoadDirectoryFromServerAfterLoad, + weak_ptr_factory_.GetWeakPtr(), + directory_fetch_info, + fetcher)); +} + +void DirectoryLoader::LoadDirectoryFromServerAfterLoad( + const DirectoryFetchInfo& directory_fetch_info, + FeedFetcher* fetcher, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!directory_fetch_info.empty()); + + // Delete the fetcher. + fast_fetch_feed_fetcher_set_.erase(fetcher); + delete fetcher; + + logger_->Log(logging::LOG_INFO, + "Fast-fetch complete: %s => %s", + directory_fetch_info.ToString().c_str(), + FileErrorToString(error).c_str()); + + if (error != FILE_ERROR_OK) { + LOG(ERROR) << "Failed to load directory: " + << directory_fetch_info.local_id() + << ": " << FileErrorToString(error); + OnDirectoryLoadComplete(directory_fetch_info.local_id(), error); + return; + } + + // Update changestamp and get the directory path. + base::FilePath* directory_path = new base::FilePath; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&UpdateChangestamp, + resource_metadata_, + directory_fetch_info, + directory_path), + base::Bind( + &DirectoryLoader::LoadDirectoryFromServerAfterUpdateChangestamp, + weak_ptr_factory_.GetWeakPtr(), + directory_fetch_info, + base::Owned(directory_path))); +} + +void DirectoryLoader::LoadDirectoryFromServerAfterUpdateChangestamp( + const DirectoryFetchInfo& directory_fetch_info, + const base::FilePath* directory_path, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + + DVLOG(1) << "Directory loaded: " << directory_fetch_info.ToString(); + OnDirectoryLoadComplete(directory_fetch_info.local_id(), error); + + // Also notify the observers. + if (error == FILE_ERROR_OK && !directory_path->empty()) { + FOR_EACH_OBSERVER(ChangeListLoaderObserver, + observers_, + OnDirectoryReloaded(*directory_path)); + } +} + +} // namespace internal +} // namespace drive diff --git a/components/drive/directory_loader.h b/components/drive/directory_loader.h new file mode 100644 index 0000000..32d72ef --- /dev/null +++ b/components/drive/directory_loader.h @@ -0,0 +1,150 @@ +// 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_DIRECTORY_LOADER_H_ +#define COMPONENTS_DRIVE_DIRECTORY_LOADER_H_ + +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "base/callback.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/observer_list.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" +#include "google_apis/drive/drive_common_callbacks.h" + +namespace base { +class SequencedTaskRunner; +} // namespace base + +namespace google_apis { +class AboutResource; +} // namespace google_apis + +namespace drive { + +class EventLogger; +class JobScheduler; +class ResourceEntry; + +namespace internal { + +class AboutResourceLoader; +class ChangeList; +class ChangeListLoaderObserver; +class DirectoryFetchInfo; +class LoaderController; +class ResourceMetadata; + +// DirectoryLoader is used to load directory contents. +class DirectoryLoader { + public: + DirectoryLoader(EventLogger* logger, + base::SequencedTaskRunner* blocking_task_runner, + ResourceMetadata* resource_metadata, + JobScheduler* scheduler, + AboutResourceLoader* about_resource_loader, + LoaderController* apply_task_controller); + ~DirectoryLoader(); + + // Adds and removes the observer. + void AddObserver(ChangeListLoaderObserver* observer); + void RemoveObserver(ChangeListLoaderObserver* observer); + + // Reads the directory contents. + // |entries_callback| can be null. + // |completion_callback| must not be null. + void ReadDirectory(const base::FilePath& directory_path, + const ReadDirectoryEntriesCallback& entries_callback, + const FileOperationCallback& completion_callback); + + private: + class FeedFetcher; + struct ReadDirectoryCallbackState; + + // Part of ReadDirectory(). + void ReadDirectoryAfterGetEntry( + const base::FilePath& directory_path, + const ReadDirectoryEntriesCallback& entries_callback, + const FileOperationCallback& completion_callback, + bool should_try_loading_parent, + const ResourceEntry* entry, + FileError error); + void ReadDirectoryAfterLoadParent( + const base::FilePath& directory_path, + const ReadDirectoryEntriesCallback& entries_callback, + const FileOperationCallback& completion_callback, + FileError error); + void ReadDirectoryAfterGetAboutResource( + const std::string& local_id, + google_apis::DriveApiErrorCode status, + scoped_ptr<google_apis::AboutResource> about_resource); + void ReadDirectoryAfterCheckLocalState( + scoped_ptr<google_apis::AboutResource> about_resource, + const std::string& local_id, + const ResourceEntry* entry, + const int64* local_changestamp, + FileError error); + + // Part of ReadDirectory(). + // This function should be called when the directory load is complete. + // Flushes the callbacks waiting for the directory to be loaded. + void OnDirectoryLoadComplete(const std::string& local_id, FileError error); + void OnDirectoryLoadCompleteAfterRead(const std::string& local_id, + const ResourceEntryVector* entries, + FileError error); + + // Sends |entries| to the callbacks. + void SendEntries(const std::string& local_id, + const ResourceEntryVector& entries); + + // ================= Implementation for directory loading ================= + // Loads the directory contents from server, and updates the local metadata. + // Runs |callback| when it is finished. + void LoadDirectoryFromServer(const DirectoryFetchInfo& directory_fetch_info); + + // Part of LoadDirectoryFromServer() for a normal directory. + void LoadDirectoryFromServerAfterLoad( + const DirectoryFetchInfo& directory_fetch_info, + FeedFetcher* fetcher, + FileError error); + + // Part of LoadDirectoryFromServer(). + void LoadDirectoryFromServerAfterUpdateChangestamp( + const DirectoryFetchInfo& directory_fetch_info, + const base::FilePath* directory_path, + FileError error); + + EventLogger* logger_; // Not owned. + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + ResourceMetadata* resource_metadata_; // Not owned. + JobScheduler* scheduler_; // Not owned. + AboutResourceLoader* about_resource_loader_; // Not owned. + LoaderController* loader_controller_; // Not owned. + base::ObserverList<ChangeListLoaderObserver> observers_; + typedef std::map<std::string, std::vector<ReadDirectoryCallbackState> > + LoadCallbackMap; + LoadCallbackMap pending_load_callback_; + + // Set of the running feed fetcher for the fast fetch. + std::set<FeedFetcher*> fast_fetch_feed_fetcher_set_; + + 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<DirectoryLoader> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(DirectoryLoader); +}; + +} // namespace internal +} // namespace drive + +#endif // COMPONENTS_DRIVE_DIRECTORY_LOADER_H_ diff --git a/components/drive/directory_loader_unittest.cc b/components/drive/directory_loader_unittest.cc new file mode 100644 index 0000000..20e4ead --- /dev/null +++ b/components/drive/directory_loader_unittest.cc @@ -0,0 +1,252 @@ +// 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/directory_loader.h" + +#include "base/callback_helpers.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/scoped_ptr.h" +#include "base/prefs/testing_pref_service.h" +#include "base/run_loop.h" +#include "base/single_thread_task_runner.h" +#include "base/thread_task_runner_handle.h" +#include "components/drive/change_list_loader.h" +#include "components/drive/change_list_loader_observer.h" +#include "components/drive/drive_test_util.h" +#include "components/drive/event_logger.h" +#include "components/drive/file_cache.h" +#include "components/drive/file_system_core_util.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/test/test_browser_thread_bundle.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 internal { + +namespace { + +class TestDirectoryLoaderObserver : public ChangeListLoaderObserver { + public: + explicit TestDirectoryLoaderObserver(DirectoryLoader* loader) + : loader_(loader) { + loader_->AddObserver(this); + } + + ~TestDirectoryLoaderObserver() override { loader_->RemoveObserver(this); } + + const std::set<base::FilePath>& changed_directories() const { + return changed_directories_; + } + void clear_changed_directories() { changed_directories_.clear(); } + + // ChageListObserver overrides: + void OnDirectoryReloaded(const base::FilePath& directory_path) override { + changed_directories_.insert(directory_path); + } + + private: + DirectoryLoader* loader_; + std::set<base::FilePath> changed_directories_; + + DISALLOW_COPY_AND_ASSIGN(TestDirectoryLoaderObserver); +}; + +void AccumulateReadDirectoryResult(ResourceEntryVector* out_entries, + scoped_ptr<ResourceEntryVector> entries) { + ASSERT_TRUE(entries); + out_entries->insert(out_entries->end(), entries->begin(), entries->end()); +} + +} // namespace + +class DirectoryLoaderTest : public testing::Test { + protected: + void SetUp() override { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + pref_service_.reset(new TestingPrefServiceSimple); + test_util::RegisterDrivePrefs(pref_service_->registry()); + + logger_.reset(new EventLogger); + + drive_service_.reset(new FakeDriveService); + ASSERT_TRUE(test_util::SetUpTestEntries(drive_service_.get())); + + scheduler_.reset(new JobScheduler( + pref_service_.get(), + logger_.get(), + drive_service_.get(), + base::ThreadTaskRunnerHandle::Get().get())); + metadata_storage_.reset(new ResourceMetadataStorage( + temp_dir_.path(), base::ThreadTaskRunnerHandle::Get().get())); + ASSERT_TRUE(metadata_storage_->Initialize()); + + cache_.reset(new FileCache(metadata_storage_.get(), + temp_dir_.path(), + base::ThreadTaskRunnerHandle::Get().get(), + NULL /* free_disk_space_getter */)); + ASSERT_TRUE(cache_->Initialize()); + + metadata_.reset(new ResourceMetadata( + metadata_storage_.get(), cache_.get(), + base::ThreadTaskRunnerHandle::Get().get())); + ASSERT_EQ(FILE_ERROR_OK, metadata_->Initialize()); + + about_resource_loader_.reset(new AboutResourceLoader(scheduler_.get())); + loader_controller_.reset(new LoaderController); + directory_loader_.reset( + new DirectoryLoader(logger_.get(), + base::ThreadTaskRunnerHandle::Get().get(), + metadata_.get(), + scheduler_.get(), + about_resource_loader_.get(), + loader_controller_.get())); + } + + // Adds a new file to the root directory of the service. + scoped_ptr<google_apis::FileResource> AddNewFile(const std::string& title) { + google_apis::DriveApiErrorCode error = google_apis::DRIVE_FILE_ERROR; + scoped_ptr<google_apis::FileResource> entry; + drive_service_->AddNewFile( + "text/plain", + "content text", + drive_service_->GetRootResourceId(), + title, + false, // shared_with_me + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(google_apis::HTTP_CREATED, error); + return entry.Pass(); + } + + content::TestBrowserThreadBundle thread_bundle_; + base::ScopedTempDir temp_dir_; + scoped_ptr<TestingPrefServiceSimple> pref_service_; + scoped_ptr<EventLogger> logger_; + scoped_ptr<FakeDriveService> drive_service_; + scoped_ptr<JobScheduler> scheduler_; + scoped_ptr<ResourceMetadataStorage, + test_util::DestroyHelperForTests> metadata_storage_; + scoped_ptr<FileCache, test_util::DestroyHelperForTests> cache_; + scoped_ptr<ResourceMetadata, test_util::DestroyHelperForTests> metadata_; + scoped_ptr<AboutResourceLoader> about_resource_loader_; + scoped_ptr<LoaderController> loader_controller_; + scoped_ptr<DirectoryLoader> directory_loader_; +}; + +TEST_F(DirectoryLoaderTest, ReadDirectory_GrandRoot) { + TestDirectoryLoaderObserver observer(directory_loader_.get()); + + // Load grand root. + FileError error = FILE_ERROR_FAILED; + ResourceEntryVector entries; + directory_loader_->ReadDirectory( + util::GetDriveGrandRootPath(), + base::Bind(&AccumulateReadDirectoryResult, &entries), + google_apis::test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_EQ(0U, observer.changed_directories().size()); + observer.clear_changed_directories(); + + // My Drive has resource ID. + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryByPath(util::GetDriveMyDriveRootPath(), + &entry)); + EXPECT_EQ(drive_service_->GetRootResourceId(), entry.resource_id()); +} + +TEST_F(DirectoryLoaderTest, ReadDirectory_MyDrive) { + TestDirectoryLoaderObserver observer(directory_loader_.get()); + + // My Drive does not have resource ID yet. + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryByPath(util::GetDriveMyDriveRootPath(), + &entry)); + EXPECT_TRUE(entry.resource_id().empty()); + + // Load My Drive. + FileError error = FILE_ERROR_FAILED; + ResourceEntryVector entries; + directory_loader_->ReadDirectory( + util::GetDriveMyDriveRootPath(), + base::Bind(&AccumulateReadDirectoryResult, &entries), + google_apis::test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_EQ(1U, observer.changed_directories().count( + util::GetDriveMyDriveRootPath())); + + // My Drive has resource ID. + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryByPath(util::GetDriveMyDriveRootPath(), + &entry)); + EXPECT_EQ(drive_service_->GetRootResourceId(), entry.resource_id()); + EXPECT_EQ(drive_service_->about_resource().largest_change_id(), + entry.directory_specific_info().changestamp()); + + // My Drive's child is present. + base::FilePath file_path = + util::GetDriveMyDriveRootPath().AppendASCII("File 1.txt"); + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryByPath(file_path, &entry)); +} + +TEST_F(DirectoryLoaderTest, ReadDirectory_MultipleCalls) { + TestDirectoryLoaderObserver observer(directory_loader_.get()); + + // Load grand root. + FileError error = FILE_ERROR_FAILED; + ResourceEntryVector entries; + directory_loader_->ReadDirectory( + util::GetDriveGrandRootPath(), + base::Bind(&AccumulateReadDirectoryResult, &entries), + google_apis::test_util::CreateCopyResultCallback(&error)); + + // Load grand root again without waiting for the result. + FileError error2 = FILE_ERROR_FAILED; + ResourceEntryVector entries2; + directory_loader_->ReadDirectory( + util::GetDriveGrandRootPath(), + base::Bind(&AccumulateReadDirectoryResult, &entries2), + google_apis::test_util::CreateCopyResultCallback(&error2)); + base::RunLoop().RunUntilIdle(); + + // Callback is called for each method call. + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_EQ(FILE_ERROR_OK, error2); +} + +TEST_F(DirectoryLoaderTest, Lock) { + // Lock the loader. + scoped_ptr<base::ScopedClosureRunner> lock = loader_controller_->GetLock(); + + // Start loading. + TestDirectoryLoaderObserver observer(directory_loader_.get()); + FileError error = FILE_ERROR_FAILED; + ResourceEntryVector entries; + directory_loader_->ReadDirectory( + util::GetDriveMyDriveRootPath(), + base::Bind(&AccumulateReadDirectoryResult, &entries), + google_apis::test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + // Update is pending due to the lock. + EXPECT_TRUE(observer.changed_directories().empty()); + + // Unlock the loader, this should resume the pending udpate. + lock.reset(); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(1U, observer.changed_directories().count( + util::GetDriveMyDriveRootPath())); +} + +} // namespace internal +} // namespace drive diff --git a/components/drive/dummy_file_system.cc b/components/drive/dummy_file_system.cc new file mode 100644 index 0000000..7c92271 --- /dev/null +++ b/components/drive/dummy_file_system.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/dummy_file_system.h" + +namespace drive { + +base::Closure DummyFileSystem::GetFileContent( + const base::FilePath& file_path, + const GetFileContentInitializedCallback& initialized_callback, + const google_apis::GetContentCallback& get_content_callback, + const FileOperationCallback& completion_callback) { + return base::Bind(&base::DoNothing); +} + +} // namespace drive diff --git a/components/drive/dummy_file_system.h b/components/drive/dummy_file_system.h new file mode 100644 index 0000000..f8cdb48 --- /dev/null +++ b/components/drive/dummy_file_system.h @@ -0,0 +1,111 @@ +// 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_DUMMY_FILE_SYSTEM_H_ +#define COMPONENTS_DRIVE_DUMMY_FILE_SYSTEM_H_ + +#include "components/drive/file_system_interface.h" + +namespace drive { + +// Dummy implementation of FileSystemInterface. All functions do nothing. +class DummyFileSystem : public FileSystemInterface { + public: + ~DummyFileSystem() override {} + void AddObserver(FileSystemObserver* observer) override {} + void RemoveObserver(FileSystemObserver* observer) override {} + void CheckForUpdates() override {} + void TransferFileFromLocalToRemote( + const base::FilePath& local_src_file_path, + const base::FilePath& remote_dest_file_path, + const FileOperationCallback& callback) override {} + void OpenFile(const base::FilePath& file_path, + OpenMode open_mode, + const std::string& mime_type, + const OpenFileCallback& callback) override {} + void Copy(const base::FilePath& src_file_path, + const base::FilePath& dest_file_path, + bool preserve_last_modified, + const FileOperationCallback& callback) override {} + void Move(const base::FilePath& src_file_path, + const base::FilePath& dest_file_path, + const FileOperationCallback& callback) override {} + void Remove(const base::FilePath& file_path, + bool is_recursive, + const FileOperationCallback& callback) override {} + void CreateDirectory(const base::FilePath& directory_path, + bool is_exclusive, + bool is_recursive, + const FileOperationCallback& callback) override {} + void CreateFile(const base::FilePath& file_path, + bool is_exclusive, + const std::string& mime_type, + const FileOperationCallback& callback) override {} + void TouchFile(const base::FilePath& file_path, + const base::Time& last_access_time, + const base::Time& last_modified_time, + const FileOperationCallback& callback) override {} + void TruncateFile(const base::FilePath& file_path, + int64 length, + const FileOperationCallback& callback) override {} + void Pin(const base::FilePath& file_path, + const FileOperationCallback& callback) override {} + void Unpin(const base::FilePath& file_path, + const FileOperationCallback& callback) override {} + void GetFile(const base::FilePath& file_path, + const GetFileCallback& callback) override {} + void GetFileForSaving(const base::FilePath& file_path, + const GetFileCallback& callback) override {} + base::Closure GetFileContent( + const base::FilePath& file_path, + const GetFileContentInitializedCallback& initialized_callback, + const google_apis::GetContentCallback& get_content_callback, + const FileOperationCallback& completion_callback) override; + void GetResourceEntry(const base::FilePath& file_path, + const GetResourceEntryCallback& callback) override {} + void ReadDirectory( + const base::FilePath& file_path, + const ReadDirectoryEntriesCallback& entries_callback, + const FileOperationCallback& completion_callback) override {} + void Search(const std::string& search_query, + const GURL& next_link, + const SearchCallback& callback) override {} + void SearchMetadata(const std::string& query, + int options, + int at_most_num_matches, + const SearchMetadataCallback& callback) override {} + void SearchByHashes(const std::set<std::string>& hashes, + const SearchByHashesCallback& callback) override {} + void GetAvailableSpace(const GetAvailableSpaceCallback& callback) override {} + void GetShareUrl(const base::FilePath& file_path, + const GURL& embed_origin, + const GetShareUrlCallback& callback) override {} + void GetMetadata(const GetFilesystemMetadataCallback& callback) override {} + void MarkCacheFileAsMounted(const base::FilePath& drive_file_path, + const MarkMountedCallback& callback) override {} + void MarkCacheFileAsUnmounted( + const base::FilePath& cache_file_path, + const FileOperationCallback& callback) override {} + void AddPermission(const base::FilePath& drive_file_path, + const std::string& email, + google_apis::drive::PermissionRole role, + const FileOperationCallback& callback) override {} + 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) override {} + void Reset(const FileOperationCallback& callback) override {} + void GetPathFromResourceId(const std::string& resource_id, + const GetFilePathCallback& callback) override {} + void FreeDiskSpaceIfNeededFor( + int64 num_bytes, + const FreeDiskSpaceCallback& callback) override {} + void CalculateEvictableCacheSize( + const EvictableCacheSizeCallback& callback) override {} +}; + +} // namespace drive + +#endif // COMPONENTS_DRIVE_DUMMY_FILE_SYSTEM_H_ diff --git a/components/drive/fake_file_system.cc b/components/drive/fake_file_system.cc new file mode 100644 index 0000000..929d693 --- /dev/null +++ b/components/drive/fake_file_system.cc @@ -0,0 +1,420 @@ +// 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/fake_file_system.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/callback.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/logging.h" +#include "components/drive/drive.pb.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system_core_util.h" +#include "components/drive/resource_entry_conversion.h" +#include "components/drive/service/drive_service_interface.h" +#include "content/public/browser/browser_thread.h" +#include "google_apis/drive/drive_api_parser.h" + +namespace drive { +namespace test_util { + +using content::BrowserThread; + +FakeFileSystem::FakeFileSystem(DriveServiceInterface* drive_service) + : drive_service_(drive_service), + weak_ptr_factory_(this) { + CHECK(cache_dir_.CreateUniqueTempDir()); +} + +FakeFileSystem::~FakeFileSystem() { +} + +void FakeFileSystem::AddObserver(FileSystemObserver* observer) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::RemoveObserver(FileSystemObserver* observer) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::CheckForUpdates() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::TransferFileFromLocalToRemote( + const base::FilePath& local_src_file_path, + const base::FilePath& remote_dest_file_path, + const FileOperationCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::OpenFile(const base::FilePath& file_path, + OpenMode open_mode, + const std::string& mime_type, + const OpenFileCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::Copy(const base::FilePath& src_file_path, + const base::FilePath& dest_file_path, + bool preserve_last_modified, + const FileOperationCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::Move(const base::FilePath& src_file_path, + const base::FilePath& dest_file_path, + const FileOperationCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::Remove(const base::FilePath& file_path, + bool is_recursive, + const FileOperationCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::CreateDirectory( + const base::FilePath& directory_path, + bool is_exclusive, + bool is_recursive, + const FileOperationCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::CreateFile(const base::FilePath& file_path, + bool is_exclusive, + const std::string& mime_type, + const FileOperationCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::TouchFile(const base::FilePath& file_path, + const base::Time& last_access_time, + const base::Time& last_modified_time, + const FileOperationCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::TruncateFile(const base::FilePath& file_path, + int64 length, + const FileOperationCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::Pin(const base::FilePath& file_path, + const FileOperationCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::Unpin(const base::FilePath& file_path, + const FileOperationCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::GetFile(const base::FilePath& file_path, + const GetFileCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::GetFileForSaving(const base::FilePath& file_path, + const GetFileCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +base::Closure FakeFileSystem::GetFileContent( + const base::FilePath& file_path, + const GetFileContentInitializedCallback& initialized_callback, + const google_apis::GetContentCallback& get_content_callback, + const FileOperationCallback& completion_callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + GetResourceEntry( + file_path, + base::Bind(&FakeFileSystem::GetFileContentAfterGetResourceEntry, + weak_ptr_factory_.GetWeakPtr(), + initialized_callback, get_content_callback, + completion_callback)); + return base::Bind(&base::DoNothing); +} + +void FakeFileSystem::GetResourceEntry( + const base::FilePath& file_path, + const GetResourceEntryCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + if (file_path == util::GetDriveMyDriveRootPath()) { + // Specialized for the root entry. + drive_service_->GetAboutResource( + base::Bind( + &FakeFileSystem::GetResourceEntryAfterGetAboutResource, + weak_ptr_factory_.GetWeakPtr(), callback)); + return; + } + + // Now, we only support files under my drive. + DCHECK(util::GetDriveMyDriveRootPath().IsParent(file_path)); + GetResourceEntry( + file_path.DirName(), + base::Bind( + &FakeFileSystem::GetResourceEntryAfterGetParentEntryInfo, + weak_ptr_factory_.GetWeakPtr(), file_path.BaseName(), callback)); +} + +void FakeFileSystem::ReadDirectory( + const base::FilePath& file_path, + const ReadDirectoryEntriesCallback& entries_callback, + const FileOperationCallback& completion_callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::Search(const std::string& search_query, + const GURL& next_link, + const SearchCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::SearchMetadata( + const std::string& query, + int options, + int at_most_num_matches, + const SearchMetadataCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::SearchByHashes(const std::set<std::string>& hashes, + const SearchByHashesCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::GetAvailableSpace( + const GetAvailableSpaceCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::GetShareUrl( + const base::FilePath& file_path, + const GURL& embed_origin, + const GetShareUrlCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::GetMetadata( + const GetFilesystemMetadataCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::MarkCacheFileAsMounted( + const base::FilePath& drive_file_path, + const MarkMountedCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::MarkCacheFileAsUnmounted( + const base::FilePath& cache_file_path, + const FileOperationCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::AddPermission(const base::FilePath& drive_file_path, + const std::string& email, + google_apis::drive::PermissionRole role, + const FileOperationCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::SetProperty( + const base::FilePath& drive_file_path, + google_apis::drive::Property::Visibility visibility, + const std::string& key, + const std::string& value, + const FileOperationCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::Reset(const FileOperationCallback& callback) { +} + +void FakeFileSystem::GetPathFromResourceId( + const std::string& resource_id, + const GetFilePathCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::FreeDiskSpaceIfNeededFor( + int64 num_bytes, + const FreeDiskSpaceCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +void FakeFileSystem::CalculateEvictableCacheSize( + const EvictableCacheSizeCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); +} + +// Implementation of GetFileContent. +void FakeFileSystem::GetFileContentAfterGetResourceEntry( + const GetFileContentInitializedCallback& initialized_callback, + const google_apis::GetContentCallback& get_content_callback, + const FileOperationCallback& completion_callback, + FileError error, + scoped_ptr<ResourceEntry> entry) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + if (error != FILE_ERROR_OK) { + completion_callback.Run(error); + return; + } + DCHECK(entry); + + // We're only interested in a file. + if (entry->file_info().is_directory()) { + completion_callback.Run(FILE_ERROR_NOT_A_FILE); + return; + } + + // Fetch google_apis::FileResource for its |download_url|. + drive_service_->GetFileResource( + entry->resource_id(), + base::Bind( + &FakeFileSystem::GetFileContentAfterGetFileResource, + weak_ptr_factory_.GetWeakPtr(), + initialized_callback, + get_content_callback, + completion_callback)); +} + +void FakeFileSystem::GetFileContentAfterGetFileResource( + const GetFileContentInitializedCallback& initialized_callback, + const google_apis::GetContentCallback& get_content_callback, + const FileOperationCallback& completion_callback, + google_apis::DriveApiErrorCode gdata_error, + scoped_ptr<google_apis::FileResource> gdata_entry) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + FileError error = GDataToFileError(gdata_error); + if (error != FILE_ERROR_OK) { + completion_callback.Run(error); + return; + } + DCHECK(gdata_entry); + + scoped_ptr<ResourceEntry> entry(new ResourceEntry); + std::string parent_resource_id; + bool converted = ConvertFileResourceToResourceEntry( + *gdata_entry, entry.get(), &parent_resource_id); + DCHECK(converted); + entry->set_parent_local_id(parent_resource_id); + + base::FilePath cache_path = + cache_dir_.path().AppendASCII(entry->resource_id()); + if (entry->file_specific_info().is_hosted_document()) { + // For hosted documents return a dummy cache without server request. + int result = base::WriteFile(cache_path, "", 0); + DCHECK_EQ(0, result); + } + if (base::PathExists(cache_path)) { + // Cache file is found. + initialized_callback.Run(FILE_ERROR_OK, cache_path, entry.Pass()); + completion_callback.Run(FILE_ERROR_OK); + return; + } + + initialized_callback.Run(FILE_ERROR_OK, base::FilePath(), entry.Pass()); + drive_service_->DownloadFile( + cache_path, + gdata_entry->file_id(), + base::Bind(&FakeFileSystem::GetFileContentAfterDownloadFile, + weak_ptr_factory_.GetWeakPtr(), + completion_callback), + get_content_callback, + google_apis::ProgressCallback()); +} + +void FakeFileSystem::GetFileContentAfterDownloadFile( + const FileOperationCallback& completion_callback, + google_apis::DriveApiErrorCode gdata_error, + const base::FilePath& temp_file) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + completion_callback.Run(GDataToFileError(gdata_error)); +} + +// Implementation of GetResourceEntry. +void FakeFileSystem::GetResourceEntryAfterGetAboutResource( + const GetResourceEntryCallback& callback, + google_apis::DriveApiErrorCode gdata_error, + scoped_ptr<google_apis::AboutResource> about_resource) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + FileError error = GDataToFileError(gdata_error); + if (error != FILE_ERROR_OK) { + callback.Run(error, scoped_ptr<ResourceEntry>()); + return; + } + + DCHECK(about_resource); + scoped_ptr<ResourceEntry> root(new ResourceEntry); + root->mutable_file_info()->set_is_directory(true); + root->set_resource_id(about_resource->root_folder_id()); + root->set_title(util::kDriveMyDriveRootDirName); + callback.Run(error, root.Pass()); +} + +void FakeFileSystem::GetResourceEntryAfterGetParentEntryInfo( + const base::FilePath& base_name, + const GetResourceEntryCallback& callback, + FileError error, + scoped_ptr<ResourceEntry> parent_entry) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + if (error != FILE_ERROR_OK) { + callback.Run(error, scoped_ptr<ResourceEntry>()); + return; + } + + DCHECK(parent_entry); + drive_service_->GetFileListInDirectory( + parent_entry->resource_id(), + base::Bind( + &FakeFileSystem::GetResourceEntryAfterGetFileList, + weak_ptr_factory_.GetWeakPtr(), base_name, callback)); +} + +void FakeFileSystem::GetResourceEntryAfterGetFileList( + const base::FilePath& base_name, + const GetResourceEntryCallback& callback, + google_apis::DriveApiErrorCode gdata_error, + scoped_ptr<google_apis::FileList> file_list) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + FileError error = GDataToFileError(gdata_error); + if (error != FILE_ERROR_OK) { + callback.Run(error, scoped_ptr<ResourceEntry>()); + return; + } + + DCHECK(file_list); + const ScopedVector<google_apis::FileResource>& entries = file_list->items(); + for (size_t i = 0; i < entries.size(); ++i) { + scoped_ptr<ResourceEntry> entry(new ResourceEntry); + std::string parent_resource_id; + bool converted = ConvertFileResourceToResourceEntry( + *entries[i], entry.get(), &parent_resource_id); + DCHECK(converted); + entry->set_parent_local_id(parent_resource_id); + + if (entry->base_name() == base_name.AsUTF8Unsafe()) { + // Found the target entry. + callback.Run(FILE_ERROR_OK, entry.Pass()); + return; + } + } + + callback.Run(FILE_ERROR_NOT_FOUND, scoped_ptr<ResourceEntry>()); +} + +} // namespace test_util +} // namespace drive diff --git a/components/drive/fake_file_system.h b/components/drive/fake_file_system.h new file mode 100644 index 0000000..de5cec3f --- /dev/null +++ b/components/drive/fake_file_system.h @@ -0,0 +1,197 @@ +// 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_FAKE_FILE_SYSTEM_H_ +#define COMPONENTS_DRIVE_FAKE_FILE_SYSTEM_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/scoped_ptr.h" +#include "components/drive/file_errors.h" +#include "components/drive/file_system_interface.h" +#include "google_apis/drive/drive_api_error_codes.h" + +namespace google_apis { + +class AboutResource; +class FileResource; + +} // namespace google_apis + +namespace drive { + +class DriveServiceInterface; +class FileSystemObserver; +class ResourceEntry; + +namespace test_util { + +// This class implements a fake FileSystem which acts like a real Drive +// file system with FakeDriveService, for testing purpose. +// Note that this class doesn't support "caching" at the moment, so the number +// of interactions to the FakeDriveService may be bigger than the real +// implementation. +// Currently most methods are empty (not implemented). +class FakeFileSystem : public FileSystemInterface { + public: + explicit FakeFileSystem(DriveServiceInterface* drive_service); + ~FakeFileSystem() override; + + // FileSystemInterface Overrides. + void AddObserver(FileSystemObserver* observer) override; + void RemoveObserver(FileSystemObserver* observer) override; + void CheckForUpdates() override; + void TransferFileFromLocalToRemote( + const base::FilePath& local_src_file_path, + const base::FilePath& remote_dest_file_path, + const FileOperationCallback& callback) override; + void OpenFile(const base::FilePath& file_path, + OpenMode open_mode, + const std::string& mime_type, + const OpenFileCallback& callback) override; + void Copy(const base::FilePath& src_file_path, + const base::FilePath& dest_file_path, + bool preserve_last_modified, + const FileOperationCallback& callback) override; + void Move(const base::FilePath& src_file_path, + const base::FilePath& dest_file_path, + const FileOperationCallback& callback) override; + void Remove(const base::FilePath& file_path, + bool is_recursive, + const FileOperationCallback& callback) override; + void CreateDirectory(const base::FilePath& directory_path, + bool is_exclusive, + bool is_recursive, + const FileOperationCallback& callback) override; + void CreateFile(const base::FilePath& file_path, + bool is_exclusive, + const std::string& mime_type, + const FileOperationCallback& callback) override; + void TouchFile(const base::FilePath& file_path, + const base::Time& last_access_time, + const base::Time& last_modified_time, + const FileOperationCallback& callback) override; + void TruncateFile(const base::FilePath& file_path, + int64 length, + const FileOperationCallback& callback) override; + void Pin(const base::FilePath& file_path, + const FileOperationCallback& callback) override; + void Unpin(const base::FilePath& file_path, + const FileOperationCallback& callback) override; + void GetFile(const base::FilePath& file_path, + const GetFileCallback& callback) override; + void GetFileForSaving(const base::FilePath& file_path, + const GetFileCallback& callback) override; + base::Closure GetFileContent( + const base::FilePath& file_path, + const GetFileContentInitializedCallback& initialized_callback, + const google_apis::GetContentCallback& get_content_callback, + const FileOperationCallback& completion_callback) override; + void GetResourceEntry(const base::FilePath& file_path, + const GetResourceEntryCallback& callback) override; + void ReadDirectory(const base::FilePath& file_path, + const ReadDirectoryEntriesCallback& entries_callback, + const FileOperationCallback& completion_callback) override; + void Search(const std::string& search_query, + const GURL& next_link, + const SearchCallback& callback) override; + void SearchMetadata(const std::string& query, + int options, + int at_most_num_matches, + const SearchMetadataCallback& callback) override; + void SearchByHashes(const std::set<std::string>& hashes, + const SearchByHashesCallback& callback) override; + void GetAvailableSpace(const GetAvailableSpaceCallback& callback) override; + void GetShareUrl(const base::FilePath& file_path, + const GURL& embed_origin, + const GetShareUrlCallback& callback) override; + void GetMetadata(const GetFilesystemMetadataCallback& callback) override; + void MarkCacheFileAsMounted(const base::FilePath& drive_file_path, + const MarkMountedCallback& callback) override; + void MarkCacheFileAsUnmounted(const base::FilePath& cache_file_path, + const FileOperationCallback& callback) override; + void AddPermission(const base::FilePath& drive_file_path, + const std::string& email, + google_apis::drive::PermissionRole role, + const FileOperationCallback& callback) override; + 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) override; + void Reset(const FileOperationCallback& callback) override; + void GetPathFromResourceId(const std::string& resource_id, + const GetFilePathCallback& callback) override; + void FreeDiskSpaceIfNeededFor(int64 num_bytes, + const FreeDiskSpaceCallback& callback) override; + void CalculateEvictableCacheSize( + const EvictableCacheSizeCallback& callback) override; + + private: + // Helpers of GetFileContent. + // How the method works: + // 1) Gets ResourceEntry of the path. + // 2) Look at if there is a cache file or not. If found return it. + // 3) Otherwise start DownloadFile. + // 4) Runs the |completion_callback| upon the download completion. + void GetFileContentAfterGetResourceEntry( + const GetFileContentInitializedCallback& initialized_callback, + const google_apis::GetContentCallback& get_content_callback, + const FileOperationCallback& completion_callback, + FileError error, + scoped_ptr<ResourceEntry> entry); + void GetFileContentAfterGetFileResource( + const GetFileContentInitializedCallback& initialized_callback, + const google_apis::GetContentCallback& get_content_callback, + const FileOperationCallback& completion_callback, + google_apis::DriveApiErrorCode gdata_error, + scoped_ptr<google_apis::FileResource> gdata_entry); + void GetFileContentAfterDownloadFile( + const FileOperationCallback& completion_callback, + google_apis::DriveApiErrorCode gdata_error, + const base::FilePath& temp_file); + + // Helpers of GetResourceEntry. + // How the method works: + // 1) If the path is root, gets AboutResrouce from the drive service + // and create ResourceEntry. + // 2-1) Otherwise, gets the parent's ResourceEntry by recursive call. + // 2-2) Then, gets the resource list by restricting the parent with its id. + // 2-3) Search the results based on title, and return the ResourceEntry. + // Note that adding suffix (e.g. " (2)") for files sharing a same name is + // not supported in FakeFileSystem. Thus, even if the server has + // files sharing the same name under a directory, the second (or later) + // file cannot be taken with the suffixed name. + void GetResourceEntryAfterGetAboutResource( + const GetResourceEntryCallback& callback, + google_apis::DriveApiErrorCode gdata_error, + scoped_ptr<google_apis::AboutResource> about_resource); + void GetResourceEntryAfterGetParentEntryInfo( + const base::FilePath& base_name, + const GetResourceEntryCallback& callback, + FileError error, + scoped_ptr<ResourceEntry> parent_entry); + void GetResourceEntryAfterGetFileList( + const base::FilePath& base_name, + const GetResourceEntryCallback& callback, + google_apis::DriveApiErrorCode gdata_error, + scoped_ptr<google_apis::FileList> file_list); + + DriveServiceInterface* drive_service_; // Not owned. + base::ScopedTempDir cache_dir_; + + // 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<FakeFileSystem> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(FakeFileSystem); +}; + +} // namespace test_util +} // namespace drive + +#endif // COMPONENTS_DRIVE_FAKE_FILE_SYSTEM_H_ diff --git a/components/drive/fake_file_system_unittest.cc b/components/drive/fake_file_system_unittest.cc new file mode 100644 index 0000000..fcaa82c --- /dev/null +++ b/components/drive/fake_file_system_unittest.cc @@ -0,0 +1,156 @@ +// 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/fake_file_system.h" + +#include "base/files/file_util.h" +#include "base/run_loop.h" +#include "components/drive/file_system_core_util.h" +#include "components/drive/service/fake_drive_service.h" +#include "components/drive/service/test_util.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace test_util { + +class FakeFileSystemTest : public ::testing::Test { + protected: + void SetUp() override { + // Initialize FakeDriveService. + fake_drive_service_.reset(new FakeDriveService); + ASSERT_TRUE(SetUpTestEntries(fake_drive_service_.get())); + + // Create a testee instance. + fake_file_system_.reset(new FakeFileSystem(fake_drive_service_.get())); + } + + content::TestBrowserThreadBundle thread_bundle_; + scoped_ptr<FakeDriveService> fake_drive_service_; + scoped_ptr<FakeFileSystem> fake_file_system_; +}; + +TEST_F(FakeFileSystemTest, GetFileContent) { + FileError initialize_error = FILE_ERROR_FAILED; + scoped_ptr<ResourceEntry> entry; + base::FilePath cache_file_path; + google_apis::test_util::TestGetContentCallback get_content_callback; + FileError completion_error = FILE_ERROR_FAILED; + + const base::FilePath kDriveFile = + util::GetDriveMyDriveRootPath().AppendASCII("File 1.txt"); + + // For the first time, the file should be downloaded from the service. + base::Closure cancel_download = fake_file_system_->GetFileContent( + kDriveFile, + google_apis::test_util::CreateCopyResultCallback( + &initialize_error, &cache_file_path, &entry), + get_content_callback.callback(), + google_apis::test_util::CreateCopyResultCallback(&completion_error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, initialize_error); + EXPECT_TRUE(entry); + + // No cache file is available yet. + EXPECT_TRUE(cache_file_path.empty()); + + // The download should be happened so the |get_content_callback| + // should have the actual data. + std::string content = get_content_callback.GetConcatenatedData(); + EXPECT_EQ(26U, content.size()); + EXPECT_EQ(FILE_ERROR_OK, completion_error); + + initialize_error = FILE_ERROR_FAILED; + entry.reset(); + get_content_callback.mutable_data()->clear(); + completion_error = FILE_ERROR_FAILED; + + // For the second time, the cache file should be found. + cancel_download = fake_file_system_->GetFileContent( + kDriveFile, + google_apis::test_util::CreateCopyResultCallback( + &initialize_error, &cache_file_path, &entry), + get_content_callback.callback(), + google_apis::test_util::CreateCopyResultCallback(&completion_error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, initialize_error); + EXPECT_TRUE(entry); + + // Cache file should be available. + ASSERT_FALSE(cache_file_path.empty()); + + // There should be a cache file so no data should be downloaded. + EXPECT_TRUE(get_content_callback.data().empty()); + EXPECT_EQ(FILE_ERROR_OK, completion_error); + + // Make sure the cached file's content. + std::string cache_file_content; + ASSERT_TRUE( + base::ReadFileToString(cache_file_path, &cache_file_content)); + EXPECT_EQ(content, cache_file_content); +} + +TEST_F(FakeFileSystemTest, GetFileContent_Directory) { + FileError initialize_error = FILE_ERROR_FAILED; + scoped_ptr<ResourceEntry> entry; + base::FilePath cache_file_path; + google_apis::test_util::TestGetContentCallback get_content_callback; + FileError completion_error = FILE_ERROR_FAILED; + base::Closure cancel_download = fake_file_system_->GetFileContent( + util::GetDriveMyDriveRootPath(), + google_apis::test_util::CreateCopyResultCallback( + &initialize_error, &cache_file_path, &entry), + get_content_callback.callback(), + google_apis::test_util::CreateCopyResultCallback(&completion_error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(FILE_ERROR_NOT_A_FILE, completion_error); +} + +TEST_F(FakeFileSystemTest, GetResourceEntry) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<ResourceEntry> entry; + fake_file_system_->GetResourceEntry( + util::GetDriveMyDriveRootPath().AppendASCII( + "Directory 1/Sub Directory Folder"), + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + ASSERT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(entry); + EXPECT_EQ("sub_dir_folder_resource_id", entry->resource_id()); +} + +TEST_F(FakeFileSystemTest, GetResourceEntry_Root) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<ResourceEntry> entry; + fake_file_system_->GetResourceEntry( + util::GetDriveMyDriveRootPath(), + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + ASSERT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(entry); + EXPECT_TRUE(entry->file_info().is_directory()); + EXPECT_EQ(fake_drive_service_->GetRootResourceId(), entry->resource_id()); + EXPECT_EQ(util::kDriveMyDriveRootDirName, entry->title()); +} + +TEST_F(FakeFileSystemTest, GetResourceEntry_Invalid) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<ResourceEntry> entry; + fake_file_system_->GetResourceEntry( + util::GetDriveMyDriveRootPath().AppendASCII("Invalid File Name"), + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + ASSERT_EQ(FILE_ERROR_NOT_FOUND, error); + ASSERT_FALSE(entry); +} + +} // namespace test_util +} // namespace drive diff --git a/components/drive/file_system.cc b/components/drive/file_system.cc new file mode 100644 index 0000000..78670b3 --- /dev/null +++ b/components/drive/file_system.cc @@ -0,0 +1,1060 @@ +// 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.h" + +#include "base/bind.h" +#include "base/files/file_util.h" +#include "base/prefs/pref_service.h" +#include "components/drive/change_list_loader.h" +#include "components/drive/directory_loader.h" +#include "components/drive/drive.pb.h" +#include "components/drive/drive_pref_names.h" +#include "components/drive/file_cache.h" +#include "components/drive/file_change.h" +#include "components/drive/file_system/copy_operation.h" +#include "components/drive/file_system/create_directory_operation.h" +#include "components/drive/file_system/create_file_operation.h" +#include "components/drive/file_system/download_operation.h" +#include "components/drive/file_system/get_file_for_saving_operation.h" +#include "components/drive/file_system/move_operation.h" +#include "components/drive/file_system/open_file_operation.h" +#include "components/drive/file_system/remove_operation.h" +#include "components/drive/file_system/search_operation.h" +#include "components/drive/file_system/set_property_operation.h" +#include "components/drive/file_system/touch_operation.h" +#include "components/drive/file_system/truncate_operation.h" +#include "components/drive/file_system_core_util.h" +#include "components/drive/file_system_observer.h" +#include "components/drive/job_scheduler.h" +#include "components/drive/remove_stale_cache_files.h" +#include "components/drive/resource_entry_conversion.h" +#include "components/drive/search_metadata.h" +#include "components/drive/sync_client.h" +#include "google_apis/drive/drive_api_parser.h" + +namespace drive { +namespace { + +// Gets a ResourceEntry from the metadata, and overwrites its file info when the +// cached file is dirty. +FileError GetLocallyStoredResourceEntry( + internal::ResourceMetadata* resource_metadata, + internal::FileCache* cache, + const base::FilePath& file_path, + ResourceEntry* entry) { + std::string local_id; + FileError error = resource_metadata->GetIdByPath(file_path, &local_id); + if (error != FILE_ERROR_OK) + return error; + + error = resource_metadata->GetResourceEntryById(local_id, entry); + if (error != FILE_ERROR_OK) + return error; + + // For entries that will never be cached, use the original resource entry + // as is. + if (!entry->has_file_specific_info() || + entry->file_specific_info().is_hosted_document()) + return FILE_ERROR_OK; + + // When cache is not found, use the original resource entry as is. + if (!entry->file_specific_info().has_cache_state()) + return FILE_ERROR_OK; + + // When cache is non-dirty and obsolete (old hash), use the original entry. + if (!entry->file_specific_info().cache_state().is_dirty() && + entry->file_specific_info().md5() != + entry->file_specific_info().cache_state().md5()) + return FILE_ERROR_OK; + + // If there's a valid cache, obtain the file info from the cache file itself. + base::FilePath local_cache_path; + error = cache->GetFile(local_id, &local_cache_path); + if (error != FILE_ERROR_OK) + return error; + + base::File::Info file_info; + if (!base::GetFileInfo(local_cache_path, &file_info)) + return FILE_ERROR_NOT_FOUND; + + entry->mutable_file_info()->set_size(file_info.size); + return FILE_ERROR_OK; +} + +// Runs the callback with parameters. +void RunGetResourceEntryCallback(const GetResourceEntryCallback& callback, + scoped_ptr<ResourceEntry> entry, + FileError error) { + DCHECK(!callback.is_null()); + + if (error != FILE_ERROR_OK) + entry.reset(); + callback.Run(error, entry.Pass()); +} + +// Used to implement Pin(). +FileError PinInternal(internal::ResourceMetadata* resource_metadata, + internal::FileCache* cache, + const base::FilePath& file_path, + std::string* local_id) { + FileError error = resource_metadata->GetIdByPath(file_path, local_id); + if (error != FILE_ERROR_OK) + return error; + + ResourceEntry entry; + error = resource_metadata->GetResourceEntryById(*local_id, &entry); + if (error != FILE_ERROR_OK) + return error; + + // TODO(hashimoto): Support pinning directories. crbug.com/127831 + if (entry.file_info().is_directory()) + return FILE_ERROR_NOT_A_FILE; + + return cache->Pin(*local_id); +} + +// Used to implement Unpin(). +FileError UnpinInternal(internal::ResourceMetadata* resource_metadata, + internal::FileCache* cache, + const base::FilePath& file_path, + std::string* local_id) { + FileError error = resource_metadata->GetIdByPath(file_path, local_id); + if (error != FILE_ERROR_OK) + return error; + + return cache->Unpin(*local_id); +} + +// Used to implement MarkCacheFileAsMounted(). +FileError MarkCacheFileAsMountedInternal( + internal::ResourceMetadata* resource_metadata, + internal::FileCache* cache, + const base::FilePath& drive_file_path, + base::FilePath* cache_file_path) { + std::string local_id; + FileError error = resource_metadata->GetIdByPath(drive_file_path, &local_id); + if (error != FILE_ERROR_OK) + return error; + + return cache->MarkAsMounted(local_id, cache_file_path); +} + +// Runs the callback with arguments. +void RunMarkMountedCallback(const MarkMountedCallback& callback, + base::FilePath* cache_file_path, + FileError error) { + DCHECK(!callback.is_null()); + callback.Run(error, *cache_file_path); +} + +// Callback for ResourceMetadata::GetLargestChangestamp. +// |callback| must not be null. +void OnGetLargestChangestamp( + FileSystemMetadata metadata, // Will be modified. + const GetFilesystemMetadataCallback& callback, + const int64* largest_changestamp, + FileError error) { + DCHECK(!callback.is_null()); + + metadata.largest_changestamp = *largest_changestamp; + callback.Run(metadata); +} + +// Thin adapter to map GetFileCallback to FileOperationCallback. +void GetFileCallbackToFileOperationCallbackAdapter( + const FileOperationCallback& callback, + FileError error, + const base::FilePath& unused_file_path, + scoped_ptr<ResourceEntry> unused_entry) { + callback.Run(error); +} + +// Clears |resource_metadata| and |cache|. +FileError ResetOnBlockingPool(internal::ResourceMetadata* resource_metadata, + internal::FileCache* cache) { + FileError error = resource_metadata->Reset(); + if (error != FILE_ERROR_OK) + return error; + return cache->ClearAll() ? FILE_ERROR_OK : FILE_ERROR_FAILED; +} + +// Part of GetPathFromResourceId(). +// Obtains |file_path| from |resource_id|. The function should be run on the +// blocking pool. +FileError GetPathFromResourceIdOnBlockingPool( + internal::ResourceMetadata* resource_metadata, + const std::string& resource_id, + base::FilePath* file_path) { + std::string local_id; + const FileError error = + resource_metadata->GetIdByResourceId(resource_id, &local_id); + if (error != FILE_ERROR_OK) + return error; + return resource_metadata->GetFilePath(local_id, file_path); +} + +// Part of GetPathFromResourceId(). +// Called when GetPathFromResourceIdInBlockingPool is complete. +void GetPathFromResourceIdAfterGetPath(base::FilePath* file_path, + const GetFilePathCallback& callback, + FileError error) { + callback.Run(error, *file_path); +} + +bool FreeDiskSpaceIfNeededForOnBlockingPool(internal::FileCache* cache, + int64 num_bytes) { + return cache->FreeDiskSpaceIfNeededFor(num_bytes); +} + +uint64_t CalculateEvictableCacheSizeOnBlockingPool(internal::FileCache* cache) { + return cache->CalculateEvictableCacheSize(); +} + +// Excludes hosted documents from the given entries. +// Used to implement ReadDirectory(). +void FilterHostedDocuments(const ReadDirectoryEntriesCallback& callback, + scoped_ptr<ResourceEntryVector> entries) { + DCHECK(!callback.is_null()); + + if (entries) { + // TODO(kinaba): Stop handling hide_hosted_docs here. crbug.com/256520. + scoped_ptr<ResourceEntryVector> filtered(new ResourceEntryVector); + for (size_t i = 0; i < entries->size(); ++i) { + if (entries->at(i).file_specific_info().is_hosted_document()) { + continue; + } + filtered->push_back(entries->at(i)); + } + entries.swap(filtered); + } + callback.Run(entries.Pass()); +} + +// Adapter for using FileOperationCallback as google_apis::EntryActionCallback. +void RunFileOperationCallbackAsEntryActionCallback( + const FileOperationCallback& callback, + google_apis::DriveApiErrorCode error) { + callback.Run(GDataToFileError(error)); +} + +// Checks if the |entry|'s hash is included in |hashes|. +bool CheckHashes(const std::set<std::string>& hashes, + const ResourceEntry& entry) { + return hashes.find(entry.file_specific_info().md5()) != hashes.end(); +} + +// Runs |callback| with |error| and the list of HashAndFilePath obtained from +// |original_result|. +void RunSearchByHashesCallback( + const SearchByHashesCallback& callback, + FileError error, + scoped_ptr<MetadataSearchResultVector> original_result) { + std::vector<HashAndFilePath> result; + if (error != FILE_ERROR_OK) { + callback.Run(error, result); + return; + } + for (const auto& search_result : *original_result) { + HashAndFilePath hash_and_path; + hash_and_path.hash = search_result.md5; + hash_and_path.path = search_result.path; + result.push_back(hash_and_path); + } + callback.Run(FILE_ERROR_OK, result); +} + +} // namespace + +struct FileSystem::CreateDirectoryParams { + base::FilePath directory_path; + bool is_exclusive; + bool is_recursive; + FileOperationCallback callback; +}; + +FileSystem::FileSystem(PrefService* pref_service, + EventLogger* logger, + internal::FileCache* cache, + JobScheduler* scheduler, + internal::ResourceMetadata* resource_metadata, + base::SequencedTaskRunner* blocking_task_runner, + base::SingleThreadTaskRunner* file_task_runner, + const base::FilePath& temporary_file_directory) + : pref_service_(pref_service), + logger_(logger), + cache_(cache), + scheduler_(scheduler), + resource_metadata_(resource_metadata), + last_update_check_error_(FILE_ERROR_OK), + blocking_task_runner_(blocking_task_runner), + file_task_runner_(file_task_runner), + temporary_file_directory_(temporary_file_directory), + weak_ptr_factory_(this) { + ResetComponents(); +} + +FileSystem::~FileSystem() { + DCHECK(thread_checker_.CalledOnValidThread()); + + directory_loader_->RemoveObserver(this); + change_list_loader_->RemoveObserver(this); +} + +void FileSystem::Reset(const FileOperationCallback& callback) { + // Discard the current loader and operation objects and renew them. This is to + // avoid that changes initiated before the metadata reset is applied after the + // reset, which may cause an inconsistent state. + // TODO(kinaba): callbacks held in the subcomponents are discarded. We might + // want to have a way to abort and flush callbacks in in-flight operations. + ResetComponents(); + + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&ResetOnBlockingPool, resource_metadata_, cache_), + callback); +} + +void FileSystem::ResetComponents() { + file_system::OperationDelegate* delegate = this; + + about_resource_loader_.reset(new internal::AboutResourceLoader(scheduler_)); + loader_controller_.reset(new internal::LoaderController); + change_list_loader_.reset(new internal::ChangeListLoader( + logger_, + blocking_task_runner_.get(), + resource_metadata_, + scheduler_, + about_resource_loader_.get(), + loader_controller_.get())); + change_list_loader_->AddObserver(this); + directory_loader_.reset(new internal::DirectoryLoader( + logger_, + blocking_task_runner_.get(), + resource_metadata_, + scheduler_, + about_resource_loader_.get(), + loader_controller_.get())); + directory_loader_->AddObserver(this); + + sync_client_.reset(new internal::SyncClient(blocking_task_runner_.get(), + delegate, + scheduler_, + resource_metadata_, + cache_, + loader_controller_.get(), + temporary_file_directory_)); + + copy_operation_.reset( + new file_system::CopyOperation(blocking_task_runner_.get(), + delegate, + scheduler_, + resource_metadata_, + cache_)); + create_directory_operation_.reset(new file_system::CreateDirectoryOperation( + blocking_task_runner_.get(), delegate, resource_metadata_)); + create_file_operation_.reset( + new file_system::CreateFileOperation(blocking_task_runner_.get(), + delegate, + resource_metadata_)); + move_operation_.reset( + new file_system::MoveOperation(blocking_task_runner_.get(), + delegate, + resource_metadata_)); + open_file_operation_.reset( + new file_system::OpenFileOperation(blocking_task_runner_.get(), + delegate, + scheduler_, + resource_metadata_, + cache_, + temporary_file_directory_)); + remove_operation_.reset( + new file_system::RemoveOperation(blocking_task_runner_.get(), + delegate, + resource_metadata_, + cache_)); + touch_operation_.reset(new file_system::TouchOperation( + blocking_task_runner_.get(), delegate, resource_metadata_)); + truncate_operation_.reset( + new file_system::TruncateOperation(blocking_task_runner_.get(), + delegate, + scheduler_, + resource_metadata_, + cache_, + temporary_file_directory_)); + download_operation_.reset( + new file_system::DownloadOperation(blocking_task_runner_.get(), + delegate, + scheduler_, + resource_metadata_, + cache_, + temporary_file_directory_)); + search_operation_.reset(new file_system::SearchOperation( + blocking_task_runner_.get(), scheduler_, resource_metadata_, + loader_controller_.get())); + get_file_for_saving_operation_.reset( + new file_system::GetFileForSavingOperation( + logger_, blocking_task_runner_.get(), file_task_runner_.get(), + delegate, scheduler_, resource_metadata_, cache_, + temporary_file_directory_)); + set_property_operation_.reset(new file_system::SetPropertyOperation( + blocking_task_runner_.get(), delegate, resource_metadata_)); +} + +void FileSystem::CheckForUpdates() { + DCHECK(thread_checker_.CalledOnValidThread()); + DVLOG(1) << "CheckForUpdates"; + + change_list_loader_->CheckForUpdates( + base::Bind(&FileSystem::OnUpdateChecked, weak_ptr_factory_.GetWeakPtr())); +} + +void FileSystem::OnUpdateChecked(FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DVLOG(1) << "CheckForUpdates finished: " << FileErrorToString(error); + last_update_check_time_ = base::Time::Now(); + last_update_check_error_ = error; +} + +void FileSystem::AddObserver(FileSystemObserver* observer) { + DCHECK(thread_checker_.CalledOnValidThread()); + observers_.AddObserver(observer); +} + +void FileSystem::RemoveObserver(FileSystemObserver* observer) { + DCHECK(thread_checker_.CalledOnValidThread()); + observers_.RemoveObserver(observer); +} + +void FileSystem::TransferFileFromLocalToRemote( + const base::FilePath& local_src_file_path, + const base::FilePath& remote_dest_file_path, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + copy_operation_->TransferFileFromLocalToRemote(local_src_file_path, + remote_dest_file_path, + callback); +} + +void FileSystem::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()); + copy_operation_->Copy( + src_file_path, dest_file_path, preserve_last_modified, callback); +} + +void FileSystem::Move(const base::FilePath& src_file_path, + const base::FilePath& dest_file_path, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + move_operation_->Move(src_file_path, dest_file_path, callback); +} + +void FileSystem::Remove(const base::FilePath& file_path, + bool is_recursive, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + remove_operation_->Remove(file_path, is_recursive, callback); +} + +void FileSystem::CreateDirectory( + const base::FilePath& directory_path, + bool is_exclusive, + bool is_recursive, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + CreateDirectoryParams params; + params.directory_path = directory_path; + params.is_exclusive = is_exclusive; + params.is_recursive = is_recursive; + params.callback = callback; + + // Ensure its parent directory is loaded to the local metadata. + ReadDirectory(directory_path.DirName(), + ReadDirectoryEntriesCallback(), + base::Bind(&FileSystem::CreateDirectoryAfterRead, + weak_ptr_factory_.GetWeakPtr(), params)); +} + +void FileSystem::CreateDirectoryAfterRead(const CreateDirectoryParams& params, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!params.callback.is_null()); + + DVLOG_IF(1, error != FILE_ERROR_OK) << "ReadDirectory failed. " + << FileErrorToString(error); + + create_directory_operation_->CreateDirectory( + params.directory_path, params.is_exclusive, params.is_recursive, + params.callback); +} + +void FileSystem::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()); + create_file_operation_->CreateFile( + file_path, is_exclusive, mime_type, callback); +} + +void FileSystem::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()); + touch_operation_->TouchFile( + file_path, last_access_time, last_modified_time, callback); +} + +void FileSystem::TruncateFile(const base::FilePath& file_path, + int64 length, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + truncate_operation_->Truncate(file_path, length, callback); +} + +void FileSystem::Pin(const base::FilePath& file_path, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + std::string* local_id = new std::string; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&PinInternal, resource_metadata_, cache_, file_path, local_id), + base::Bind(&FileSystem::FinishPin, + weak_ptr_factory_.GetWeakPtr(), + callback, + base::Owned(local_id))); +} + +void FileSystem::FinishPin(const FileOperationCallback& callback, + const std::string* local_id, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error == FILE_ERROR_OK) + sync_client_->AddFetchTask(*local_id); + callback.Run(error); +} + +void FileSystem::Unpin(const base::FilePath& file_path, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + std::string* local_id = new std::string; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind( + &UnpinInternal, resource_metadata_, cache_, file_path, local_id), + base::Bind(&FileSystem::FinishUnpin, + weak_ptr_factory_.GetWeakPtr(), + callback, + base::Owned(local_id))); +} + +void FileSystem::FinishUnpin(const FileOperationCallback& callback, + const std::string* local_id, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error == FILE_ERROR_OK) + sync_client_->RemoveFetchTask(*local_id); + callback.Run(error); +} + +void FileSystem::GetFile(const base::FilePath& file_path, + const GetFileCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + download_operation_->EnsureFileDownloadedByPath( + file_path, + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + callback); +} + +void FileSystem::GetFileForSaving(const base::FilePath& file_path, + const GetFileCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + get_file_for_saving_operation_->GetFileForSaving(file_path, callback); +} + +base::Closure FileSystem::GetFileContent( + const base::FilePath& file_path, + const GetFileContentInitializedCallback& initialized_callback, + const google_apis::GetContentCallback& get_content_callback, + const FileOperationCallback& completion_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!initialized_callback.is_null()); + DCHECK(!get_content_callback.is_null()); + DCHECK(!completion_callback.is_null()); + + return download_operation_->EnsureFileDownloadedByPath( + file_path, + ClientContext(USER_INITIATED), + initialized_callback, + get_content_callback, + base::Bind(&GetFileCallbackToFileOperationCallbackAdapter, + completion_callback)); +} + +void FileSystem::GetResourceEntry( + const base::FilePath& file_path, + const GetResourceEntryCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + ReadDirectory(file_path.DirName(), + ReadDirectoryEntriesCallback(), + base::Bind(&FileSystem::GetResourceEntryAfterRead, + weak_ptr_factory_.GetWeakPtr(), + file_path, + callback)); +} + +void FileSystem::GetResourceEntryAfterRead( + const base::FilePath& file_path, + const GetResourceEntryCallback& callback, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + DVLOG_IF(1, error != FILE_ERROR_OK) << "ReadDirectory failed. " + << FileErrorToString(error); + + scoped_ptr<ResourceEntry> entry(new ResourceEntry); + ResourceEntry* entry_ptr = entry.get(); + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&GetLocallyStoredResourceEntry, + resource_metadata_, + cache_, + file_path, + entry_ptr), + base::Bind(&RunGetResourceEntryCallback, callback, base::Passed(&entry))); +} + +void FileSystem::ReadDirectory( + const base::FilePath& directory_path, + const ReadDirectoryEntriesCallback& entries_callback_in, + const FileOperationCallback& completion_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!completion_callback.is_null()); + + const bool hide_hosted_docs = + pref_service_->GetBoolean(prefs::kDisableDriveHostedFiles); + ReadDirectoryEntriesCallback entries_callback = entries_callback_in; + if (!entries_callback.is_null() && hide_hosted_docs) + entries_callback = base::Bind(&FilterHostedDocuments, entries_callback); + + directory_loader_->ReadDirectory( + directory_path, entries_callback, completion_callback); + + // Also start loading all of the user's contents. + change_list_loader_->LoadIfNeeded( + base::Bind(&util::EmptyFileOperationCallback)); +} + +void FileSystem::GetAvailableSpace( + const GetAvailableSpaceCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + about_resource_loader_->GetAboutResource( + base::Bind(&FileSystem::OnGetAboutResource, + weak_ptr_factory_.GetWeakPtr(), + callback)); +} + +void FileSystem::OnGetAboutResource( + const GetAvailableSpaceCallback& callback, + google_apis::DriveApiErrorCode status, + scoped_ptr<google_apis::AboutResource> about_resource) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FileError error = GDataToFileError(status); + if (error != FILE_ERROR_OK) { + callback.Run(error, -1, -1); + return; + } + DCHECK(about_resource); + + callback.Run(FILE_ERROR_OK, about_resource->quota_bytes_total(), + about_resource->quota_bytes_used_aggregate()); +} + +void FileSystem::GetShareUrl(const base::FilePath& file_path, + const GURL& embed_origin, + const GetShareUrlCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + // Resolve the resource id. + ResourceEntry* entry = new ResourceEntry; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&internal::ResourceMetadata::GetResourceEntryByPath, + base::Unretained(resource_metadata_), + file_path, + entry), + base::Bind(&FileSystem::GetShareUrlAfterGetResourceEntry, + weak_ptr_factory_.GetWeakPtr(), + file_path, + embed_origin, + callback, + base::Owned(entry))); +} + +void FileSystem::GetShareUrlAfterGetResourceEntry( + const base::FilePath& file_path, + const GURL& embed_origin, + const GetShareUrlCallback& callback, + ResourceEntry* entry, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error != FILE_ERROR_OK) { + callback.Run(error, GURL()); + return; + } + if (entry->resource_id().empty()) { + // This entry does not exist on the server. Just return. + callback.Run(FILE_ERROR_FAILED, GURL()); + return; + } + + scheduler_->GetShareUrl( + entry->resource_id(), + embed_origin, + ClientContext(USER_INITIATED), + base::Bind(&FileSystem::OnGetResourceEntryForGetShareUrl, + weak_ptr_factory_.GetWeakPtr(), + callback)); +} + +void FileSystem::OnGetResourceEntryForGetShareUrl( + const GetShareUrlCallback& callback, + google_apis::DriveApiErrorCode status, + const GURL& share_url) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FileError error = GDataToFileError(status); + if (error != FILE_ERROR_OK) { + callback.Run(error, GURL()); + return; + } + + if (share_url.is_empty()) { + callback.Run(FILE_ERROR_FAILED, GURL()); + return; + } + + callback.Run(FILE_ERROR_OK, share_url); +} + +void FileSystem::Search(const std::string& search_query, + const GURL& next_link, + const SearchCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + search_operation_->Search(search_query, next_link, callback); +} + +void FileSystem::SearchMetadata(const std::string& query, + int options, + int at_most_num_matches, + const SearchMetadataCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + // TODO(satorux): Stop handling hide_hosted_docs here. crbug.com/256520. + if (pref_service_->GetBoolean(prefs::kDisableDriveHostedFiles)) + options |= SEARCH_METADATA_EXCLUDE_HOSTED_DOCUMENTS; + + drive::internal::SearchMetadata( + blocking_task_runner_, resource_metadata_, query, + base::Bind(&drive::internal::MatchesType, options), at_most_num_matches, + callback); +} + +void FileSystem::SearchByHashes(const std::set<std::string>& hashes, + const SearchByHashesCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + drive::internal::SearchMetadata( + blocking_task_runner_, resource_metadata_, + /* any file name */ "", base::Bind(&CheckHashes, hashes), + std::numeric_limits<size_t>::max(), + base::Bind(&RunSearchByHashesCallback, callback)); +} + +void FileSystem::OnFileChangedByOperation(const FileChange& changed_files) { + DCHECK(thread_checker_.CalledOnValidThread()); + + FOR_EACH_OBSERVER( + FileSystemObserver, observers_, OnFileChanged(changed_files)); +} + +void FileSystem::OnEntryUpdatedByOperation(const ClientContext& context, + const std::string& local_id) { + sync_client_->AddUpdateTask(context, local_id); +} + +void FileSystem::OnDriveSyncError(file_system::DriveSyncErrorType type, + const std::string& local_id) { + base::FilePath* file_path = new base::FilePath; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&internal::ResourceMetadata::GetFilePath, + base::Unretained(resource_metadata_), + local_id, + file_path), + base::Bind(&FileSystem::OnDriveSyncErrorAfterGetFilePath, + weak_ptr_factory_.GetWeakPtr(), + type, + base::Owned(file_path))); +} + +void FileSystem::OnDriveSyncErrorAfterGetFilePath( + file_system::DriveSyncErrorType type, + const base::FilePath* file_path, + FileError error) { + if (error != FILE_ERROR_OK) + return; + FOR_EACH_OBSERVER(FileSystemObserver, + observers_, + OnDriveSyncError(type, *file_path)); +} + +bool FileSystem::WaitForSyncComplete(const std::string& local_id, + const FileOperationCallback& callback) { + return sync_client_->WaitForUpdateTaskToComplete(local_id, callback); +} + +void FileSystem::OnDirectoryReloaded(const base::FilePath& directory_path) { + DCHECK(thread_checker_.CalledOnValidThread()); + + FOR_EACH_OBSERVER( + FileSystemObserver, observers_, OnDirectoryChanged(directory_path)); +} + +void FileSystem::OnFileChanged(const FileChange& changed_files) { + DCHECK(thread_checker_.CalledOnValidThread()); + + FOR_EACH_OBSERVER( + FileSystemObserver, observers_, OnFileChanged(changed_files)); +} + +void FileSystem::OnLoadFromServerComplete() { + DCHECK(thread_checker_.CalledOnValidThread()); + + sync_client_->StartCheckingExistingPinnedFiles(); +} + +void FileSystem::OnInitialLoadComplete() { + DCHECK(thread_checker_.CalledOnValidThread()); + + blocking_task_runner_->PostTask(FROM_HERE, + base::Bind(&internal::RemoveStaleCacheFiles, + cache_, + resource_metadata_)); + sync_client_->StartProcessingBacklog(); +} + +void FileSystem::GetMetadata( + const GetFilesystemMetadataCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FileSystemMetadata metadata; + metadata.refreshing = change_list_loader_->IsRefreshing(); + + // Metadata related to delta update. + metadata.last_update_check_time = last_update_check_time_; + metadata.last_update_check_error = last_update_check_error_; + + int64* largest_changestamp = new int64(0); + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&internal::ResourceMetadata::GetLargestChangestamp, + base::Unretained(resource_metadata_), + largest_changestamp), + base::Bind(&OnGetLargestChangestamp, + metadata, + callback, + base::Owned(largest_changestamp))); +} + +void FileSystem::MarkCacheFileAsMounted( + const base::FilePath& drive_file_path, + const MarkMountedCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + base::FilePath* cache_file_path = new base::FilePath; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&MarkCacheFileAsMountedInternal, + resource_metadata_, + cache_, + drive_file_path, + cache_file_path), + base::Bind( + &RunMarkMountedCallback, callback, base::Owned(cache_file_path))); +} + +void FileSystem::MarkCacheFileAsUnmounted( + const base::FilePath& cache_file_path, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (!cache_->IsUnderFileCacheDirectory(cache_file_path)) { + callback.Run(FILE_ERROR_FAILED); + return; + } + + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&internal::FileCache::MarkAsUnmounted, + base::Unretained(cache_), + cache_file_path), + callback); +} + +void FileSystem::AddPermission(const base::FilePath& drive_file_path, + const std::string& email, + google_apis::drive::PermissionRole role, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + // Resolve the resource id. + ResourceEntry* const entry = new ResourceEntry; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&internal::ResourceMetadata::GetResourceEntryByPath, + base::Unretained(resource_metadata_), + drive_file_path, + entry), + base::Bind(&FileSystem::AddPermissionAfterGetResourceEntry, + weak_ptr_factory_.GetWeakPtr(), + email, + role, + callback, + base::Owned(entry))); +} + +void FileSystem::AddPermissionAfterGetResourceEntry( + const std::string& email, + google_apis::drive::PermissionRole role, + const FileOperationCallback& callback, + ResourceEntry* entry, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (error != FILE_ERROR_OK) { + callback.Run(error); + return; + } + + scheduler_->AddPermission( + entry->resource_id(), + email, + role, + base::Bind(&RunFileOperationCallbackAsEntryActionCallback, callback)); +} + +void FileSystem::SetProperty( + const base::FilePath& drive_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()); + + set_property_operation_->SetProperty(drive_file_path, visibility, key, value, + callback); +} + +void FileSystem::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()); + + open_file_operation_->OpenFile(file_path, open_mode, mime_type, callback); +} + +void FileSystem::GetPathFromResourceId(const std::string& resource_id, + const GetFilePathCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + base::FilePath* const file_path = new base::FilePath(); + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&GetPathFromResourceIdOnBlockingPool, + resource_metadata_, + resource_id, + file_path), + base::Bind(&GetPathFromResourceIdAfterGetPath, + base::Owned(file_path), + callback)); +} + +void FileSystem::FreeDiskSpaceIfNeededFor( + int64 num_bytes, + const FreeDiskSpaceCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), FROM_HERE, + base::Bind(&FreeDiskSpaceIfNeededForOnBlockingPool, cache_, num_bytes), + callback); +} + +void FileSystem::CalculateEvictableCacheSize( + const EvictableCacheSizeCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), FROM_HERE, + base::Bind(&CalculateEvictableCacheSizeOnBlockingPool, cache_), callback); +} +} // namespace drive diff --git a/components/drive/file_system.h b/components/drive/file_system.h new file mode 100644 index 0000000..0c143842 --- /dev/null +++ b/components/drive/file_system.h @@ -0,0 +1,311 @@ +// 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_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_H_ + +#include <string> +#include <vector> + +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "base/threading/thread_checker.h" +#include "components/drive/change_list_loader_observer.h" +#include "components/drive/file_system/operation_delegate.h" +#include "components/drive/file_system_interface.h" +#include "google_apis/drive/drive_api_error_codes.h" + +class PrefService; + +namespace base { +class SequencedTaskRunner; +} // namespace base + +namespace google_apis { +class AboutResource; +class ResourceEntry; +} // namespace google_apis + +namespace drive { +struct ClientContext; +class EventLogger; +class FileCacheEntry; +class FileSystemObserver; +class JobScheduler; + +namespace internal { +class AboutResourceLoader; +class ChangeListLoader; +class DirectoryLoader; +class FileCache; +class LoaderController; +class ResourceMetadata; +class SyncClient; +} // namespace internal + +namespace file_system { +class CopyOperation; +class CreateDirectoryOperation; +class CreateFileOperation; +class DownloadOperation; +class GetFileForSavingOperation; +class MoveOperation; +class OpenFileOperation; +class RemoveOperation; +class SearchOperation; +class SetPropertyOperation; +class TouchOperation; +class TruncateOperation; +} // namespace file_system + +// The production implementation of FileSystemInterface. +class FileSystem : public FileSystemInterface, + public internal::ChangeListLoaderObserver, + public file_system::OperationDelegate { + public: + FileSystem(PrefService* pref_service, + EventLogger* logger, + internal::FileCache* cache, + JobScheduler* scheduler, + internal::ResourceMetadata* resource_metadata, + base::SequencedTaskRunner* blocking_task_runner, + base::SingleThreadTaskRunner* file_task_runner, + const base::FilePath& temporary_file_directory); + ~FileSystem() override; + + // FileSystemInterface overrides. + void AddObserver(FileSystemObserver* observer) override; + void RemoveObserver(FileSystemObserver* observer) override; + void CheckForUpdates() override; + void Search(const std::string& search_query, + const GURL& next_link, + const SearchCallback& callback) override; + void SearchMetadata(const std::string& query, + int options, + int at_most_num_matches, + const SearchMetadataCallback& callback) override; + void SearchByHashes(const std::set<std::string>& hashes, + const SearchByHashesCallback& callback) override; + void TransferFileFromLocalToRemote( + const base::FilePath& local_src_file_path, + const base::FilePath& remote_dest_file_path, + const FileOperationCallback& callback) override; + void OpenFile(const base::FilePath& file_path, + OpenMode open_mode, + const std::string& mime_type, + const OpenFileCallback& callback) override; + void Copy(const base::FilePath& src_file_path, + const base::FilePath& dest_file_path, + bool preserve_last_modified, + const FileOperationCallback& callback) override; + void Move(const base::FilePath& src_file_path, + const base::FilePath& dest_file_path, + const FileOperationCallback& callback) override; + void Remove(const base::FilePath& file_path, + bool is_recursive, + const FileOperationCallback& callback) override; + void CreateDirectory(const base::FilePath& directory_path, + bool is_exclusive, + bool is_recursive, + const FileOperationCallback& callback) override; + void CreateFile(const base::FilePath& file_path, + bool is_exclusive, + const std::string& mime_type, + const FileOperationCallback& callback) override; + void TouchFile(const base::FilePath& file_path, + const base::Time& last_access_time, + const base::Time& last_modified_time, + const FileOperationCallback& callback) override; + void TruncateFile(const base::FilePath& file_path, + int64 length, + const FileOperationCallback& callback) override; + void Pin(const base::FilePath& file_path, + const FileOperationCallback& callback) override; + void Unpin(const base::FilePath& file_path, + const FileOperationCallback& callback) override; + void GetFile(const base::FilePath& file_path, + const GetFileCallback& callback) override; + void GetFileForSaving(const base::FilePath& file_path, + const GetFileCallback& callback) override; + base::Closure GetFileContent( + const base::FilePath& file_path, + const GetFileContentInitializedCallback& initialized_callback, + const google_apis::GetContentCallback& get_content_callback, + const FileOperationCallback& completion_callback) override; + void GetResourceEntry(const base::FilePath& file_path, + const GetResourceEntryCallback& callback) override; + void ReadDirectory(const base::FilePath& directory_path, + const ReadDirectoryEntriesCallback& entries_callback, + const FileOperationCallback& completion_callback) override; + void GetAvailableSpace(const GetAvailableSpaceCallback& callback) override; + void GetShareUrl(const base::FilePath& file_path, + const GURL& embed_origin, + const GetShareUrlCallback& callback) override; + void GetMetadata(const GetFilesystemMetadataCallback& callback) override; + void MarkCacheFileAsMounted(const base::FilePath& drive_file_path, + const MarkMountedCallback& callback) override; + void MarkCacheFileAsUnmounted(const base::FilePath& cache_file_path, + const FileOperationCallback& callback) override; + void AddPermission(const base::FilePath& drive_file_path, + const std::string& email, + google_apis::drive::PermissionRole role, + const FileOperationCallback& callback) override; + 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) override; + void Reset(const FileOperationCallback& callback) override; + void GetPathFromResourceId(const std::string& resource_id, + const GetFilePathCallback& callback) override; + void FreeDiskSpaceIfNeededFor(int64 num_bytes, + const FreeDiskSpaceCallback& callback) override; + void CalculateEvictableCacheSize( + const EvictableCacheSizeCallback& callback) override; + + // file_system::OperationDelegate overrides. + void OnFileChangedByOperation(const FileChange& changed_files) override; + void OnEntryUpdatedByOperation(const ClientContext& context, + const std::string& local_id) override; + void OnDriveSyncError(file_system::DriveSyncErrorType type, + const std::string& local_id) override; + bool WaitForSyncComplete(const std::string& local_id, + const FileOperationCallback& callback) override; + + // ChangeListLoader::Observer overrides. + // Used to propagate events from ChangeListLoader. + void OnDirectoryReloaded(const base::FilePath& directory_path) override; + void OnFileChanged(const FileChange& changed_files) override; + void OnLoadFromServerComplete() override; + void OnInitialLoadComplete() override; + + // Used by tests. + internal::ChangeListLoader* change_list_loader_for_testing() { + return change_list_loader_.get(); + } + internal::SyncClient* sync_client_for_testing() { return sync_client_.get(); } + + private: + struct CreateDirectoryParams; + + // Used for initialization and Reset(). (Re-)initializes sub components that + // need to be recreated during the reset of resource metadata and the cache. + void ResetComponents(); + + // Part of CreateDirectory(). Called after ReadDirectory() + // is called and made sure that the resource metadata is loaded. + void CreateDirectoryAfterRead(const CreateDirectoryParams& params, + FileError error); + + void FinishPin(const FileOperationCallback& callback, + const std::string* local_id, + FileError error); + + void FinishUnpin(const FileOperationCallback& callback, + const std::string* local_id, + FileError error); + + // Callback for handling about resource fetch. + void OnGetAboutResource( + const GetAvailableSpaceCallback& callback, + google_apis::DriveApiErrorCode status, + scoped_ptr<google_apis::AboutResource> about_resource); + + // Part of CheckForUpdates(). Called when + // ChangeListLoader::CheckForUpdates() is complete. + void OnUpdateChecked(FileError error); + + // Part of GetResourceEntry(). + // Called when ReadDirectory() is complete. + void GetResourceEntryAfterRead(const base::FilePath& file_path, + const GetResourceEntryCallback& callback, + FileError error); + + // Part of GetShareUrl. Resolves the resource entry to get the resource it, + // and then uses it to ask for the share url. |callback| must not be null. + void GetShareUrlAfterGetResourceEntry(const base::FilePath& file_path, + const GURL& embed_origin, + const GetShareUrlCallback& callback, + ResourceEntry* entry, + FileError error); + void OnGetResourceEntryForGetShareUrl(const GetShareUrlCallback& callback, + google_apis::DriveApiErrorCode status, + const GURL& share_url); + // Part of AddPermission. + void AddPermissionAfterGetResourceEntry( + const std::string& email, + google_apis::drive::PermissionRole role, + const FileOperationCallback& callback, + ResourceEntry* entry, + FileError error); + + // Part of OnDriveSyncError(). + virtual void OnDriveSyncErrorAfterGetFilePath( + file_system::DriveSyncErrorType type, + const base::FilePath* file_path, + FileError error); + + // Used to get Drive related preferences. + PrefService* pref_service_; + + // Sub components owned by DriveIntegrationService. + EventLogger* logger_; + internal::FileCache* cache_; + JobScheduler* scheduler_; + internal::ResourceMetadata* resource_metadata_; + + // Time of the last update check. + base::Time last_update_check_time_; + + // Error of the last update check. + FileError last_update_check_error_; + + // Used to load about resource. + scoped_ptr<internal::AboutResourceLoader> about_resource_loader_; + + // Used to control ChangeListLoader. + scoped_ptr<internal::LoaderController> loader_controller_; + + // The loader is used to load the change lists. + scoped_ptr<internal::ChangeListLoader> change_list_loader_; + + scoped_ptr<internal::DirectoryLoader> directory_loader_; + + scoped_ptr<internal::SyncClient> sync_client_; + + base::ObserverList<FileSystemObserver> observers_; + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + scoped_refptr<base::SingleThreadTaskRunner> file_task_runner_; + + base::FilePath temporary_file_directory_; + + // Implementation of each file system operation. + scoped_ptr<file_system::CopyOperation> copy_operation_; + scoped_ptr<file_system::CreateDirectoryOperation> create_directory_operation_; + scoped_ptr<file_system::CreateFileOperation> create_file_operation_; + scoped_ptr<file_system::MoveOperation> move_operation_; + scoped_ptr<file_system::OpenFileOperation> open_file_operation_; + scoped_ptr<file_system::RemoveOperation> remove_operation_; + scoped_ptr<file_system::TouchOperation> touch_operation_; + scoped_ptr<file_system::TruncateOperation> truncate_operation_; + scoped_ptr<file_system::DownloadOperation> download_operation_; + scoped_ptr<file_system::SearchOperation> search_operation_; + scoped_ptr<file_system::GetFileForSavingOperation> + get_file_for_saving_operation_; + scoped_ptr<file_system::SetPropertyOperation> set_property_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<FileSystem> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(FileSystem); +}; + +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_H_ diff --git a/components/drive/file_system_observer.h b/components/drive/file_system_observer.h new file mode 100644 index 0000000..cf0d141 --- /dev/null +++ b/components/drive/file_system_observer.h @@ -0,0 +1,41 @@ +// 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_OBSERVER_H_ +#define COMPONENTS_DRIVE_FILE_SYSTEM_OBSERVER_H_ + +#include "components/drive/file_errors.h" +#include "components/drive/file_system/operation_delegate.h" + +namespace base { +class FilePath; +} + +namespace drive { + +class FileChange; + +// Interface for classes that need to observe events from classes implementing +// FileSystemInterface. +// All events are notified on UI thread. +class FileSystemObserver { + public: + // Triggered when a content of a directory has been changed. + // |directory_path| is a virtual directory path (/drive/...) representing + // changed directory. + virtual void OnDirectoryChanged(const base::FilePath& directory_path) {} + virtual void OnFileChanged(const FileChange& file_change) {} + + // Triggared when a specific drive error occurred. + // |type| is a type of the error. |file_name| is a virtual path of the entry. + virtual void OnDriveSyncError(file_system::DriveSyncErrorType type, + const base::FilePath& file_path) {} + + protected: + virtual ~FileSystemObserver() {} +}; + +} // namespace drive + +#endif // COMPONENTS_DRIVE_FILE_SYSTEM_OBSERVER_H_ diff --git a/components/drive/file_system_unittest.cc b/components/drive/file_system_unittest.cc new file mode 100644 index 0000000..a502a2a --- /dev/null +++ b/components/drive/file_system_unittest.cc @@ -0,0 +1,1064 @@ +// 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.h" + +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/scoped_ptr.h" +#include "base/prefs/testing_pref_service.h" +#include "base/run_loop.h" +#include "base/single_thread_task_runner.h" +#include "base/thread_task_runner_handle.h" +#include "components/drive/change_list_loader.h" +#include "components/drive/drive.pb.h" +#include "components/drive/drive_api_util.h" +#include "components/drive/drive_test_util.h" +#include "components/drive/event_logger.h" +#include "components/drive/fake_free_disk_space_getter.h" +#include "components/drive/file_change.h" +#include "components/drive/file_system_core_util.h" +#include "components/drive/file_system_observer.h" +#include "components/drive/job_scheduler.h" +#include "components/drive/service/fake_drive_service.h" +#include "components/drive/service/test_util.h" +#include "components/drive/sync_client.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/test/test_browser_thread_bundle.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 { + +// Counts the number of invocation, and if it increased up to |expected_counter| +// quits the current message loop by calling |quit|. +void AsyncInitializationCallback( + int* counter, int expected_counter, const base::Closure& quit, + FileError error, scoped_ptr<ResourceEntry> entry) { + if (error != FILE_ERROR_OK || !entry) { + // If we hit an error case, quit the message loop immediately. + // Then the expectation in the test case can find it because the actual + // value of |counter| is different from the expected one. + quit.Run(); + return; + } + + (*counter)++; + if (*counter >= expected_counter) + quit.Run(); +} + +bool CompareHashAndFilePath(const HashAndFilePath& a, + const HashAndFilePath& b) { + const int result = a.hash.compare(b.hash); + if (result < 0) + return true; + if (result > 0) + return false; + return a.path.AsUTF8Unsafe().compare(b.path.AsUTF8Unsafe()) < 0; +} + +// This class is used to record directory changes and examine them later. +class MockDirectoryChangeObserver : public FileSystemObserver { + public: + MockDirectoryChangeObserver() {} + ~MockDirectoryChangeObserver() override {} + + // FileSystemObserver overrides. + void OnDirectoryChanged(const base::FilePath& directory_path) override { + changed_directories_.push_back(directory_path); + } + + void OnFileChanged(const FileChange& new_file_change) override { + changed_files_.Apply(new_file_change); + } + + const std::vector<base::FilePath>& changed_directories() const { + return changed_directories_; + } + + const FileChange& changed_files() const { return changed_files_; } + + private: + std::vector<base::FilePath> changed_directories_; + FileChange changed_files_; + DISALLOW_COPY_AND_ASSIGN(MockDirectoryChangeObserver); +}; + +} // namespace + +class FileSystemTest : public testing::Test { + protected: + void SetUp() override { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + pref_service_.reset(new TestingPrefServiceSimple); + test_util::RegisterDrivePrefs(pref_service_->registry()); + + logger_.reset(new EventLogger); + fake_drive_service_.reset(new FakeDriveService); + test_util::SetUpTestEntries(fake_drive_service_.get()); + + fake_free_disk_space_getter_.reset(new FakeFreeDiskSpaceGetter); + + scheduler_.reset(new JobScheduler( + pref_service_.get(), + logger_.get(), + fake_drive_service_.get(), + base::ThreadTaskRunnerHandle::Get().get())); + + mock_directory_observer_.reset(new MockDirectoryChangeObserver); + + SetUpResourceMetadataAndFileSystem(); + } + + void SetUpResourceMetadataAndFileSystem() { + const base::FilePath metadata_dir = temp_dir_.path().AppendASCII("meta"); + ASSERT_TRUE(base::CreateDirectory(metadata_dir)); + metadata_storage_.reset(new internal::ResourceMetadataStorage( + metadata_dir, base::ThreadTaskRunnerHandle::Get().get())); + ASSERT_TRUE(metadata_storage_->Initialize()); + + const base::FilePath cache_dir = temp_dir_.path().AppendASCII("files"); + ASSERT_TRUE(base::CreateDirectory(cache_dir)); + cache_.reset(new internal::FileCache( + metadata_storage_.get(), + cache_dir, + base::ThreadTaskRunnerHandle::Get().get(), + fake_free_disk_space_getter_.get())); + ASSERT_TRUE(cache_->Initialize()); + + resource_metadata_.reset(new internal::ResourceMetadata( + metadata_storage_.get(), cache_.get(), + base::ThreadTaskRunnerHandle::Get())); + ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->Initialize()); + + const base::FilePath temp_file_dir = temp_dir_.path().AppendASCII("tmp"); + ASSERT_TRUE(base::CreateDirectory(temp_file_dir)); + file_task_runner_ = content::BrowserThread::GetMessageLoopProxyForThread( + content::BrowserThread::FILE); + file_system_.reset(new FileSystem( + pref_service_.get(), logger_.get(), cache_.get(), scheduler_.get(), + resource_metadata_.get(), base::ThreadTaskRunnerHandle::Get().get(), + file_task_runner_.get(), temp_file_dir)); + file_system_->AddObserver(mock_directory_observer_.get()); + + // Disable delaying so that the sync starts immediately. + file_system_->sync_client_for_testing()->set_delay_for_testing( + base::TimeDelta::FromSeconds(0)); + } + + // Loads the full resource list via FakeDriveService. + bool LoadFullResourceList() { + FileError error = FILE_ERROR_FAILED; + file_system_->change_list_loader_for_testing()->LoadIfNeeded( + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + return error == FILE_ERROR_OK; + } + + // Gets resource entry by path synchronously. + scoped_ptr<ResourceEntry> GetResourceEntrySync( + const base::FilePath& file_path) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<ResourceEntry> entry; + file_system_->GetResourceEntry( + file_path, + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + + return entry.Pass(); + } + + // Gets directory info by path synchronously. + scoped_ptr<ResourceEntryVector> ReadDirectorySync( + const base::FilePath& file_path) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<ResourceEntryVector> entries(new ResourceEntryVector); + file_system_->ReadDirectory( + file_path, + base::Bind(&AccumulateReadDirectoryResult, entries.get()), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + if (error != FILE_ERROR_OK) + entries.reset(); + return entries.Pass(); + } + + // Used to implement ReadDirectorySync(). + static void AccumulateReadDirectoryResult( + ResourceEntryVector* out_entries, + scoped_ptr<ResourceEntryVector> entries) { + ASSERT_TRUE(entries); + out_entries->insert(out_entries->end(), entries->begin(), entries->end()); + } + + // Returns true if an entry exists at |file_path|. + bool EntryExists(const base::FilePath& file_path) { + return GetResourceEntrySync(file_path); + } + + // Flag for specifying the timestamp of the test filesystem cache. + enum SetUpTestFileSystemParam { + USE_OLD_TIMESTAMP, + USE_SERVER_TIMESTAMP, + }; + + // Sets up a filesystem with directories: drive/root, drive/root/Dir1, + // drive/root/Dir1/SubDir2 and files drive/root/File1, drive/root/Dir1/File2, + // drive/root/Dir1/SubDir2/File3. If |use_up_to_date_timestamp| is true, sets + // the changestamp to that of FakeDriveService, indicating the cache is + // holding the latest file system info. + void SetUpTestFileSystem(SetUpTestFileSystemParam param) { + // Destroy the existing resource metadata to close DB. + resource_metadata_.reset(); + + const base::FilePath metadata_dir = temp_dir_.path().AppendASCII("meta"); + ASSERT_TRUE(base::CreateDirectory(metadata_dir)); + scoped_ptr<internal::ResourceMetadataStorage, + test_util::DestroyHelperForTests> metadata_storage( + new internal::ResourceMetadataStorage( + metadata_dir, base::ThreadTaskRunnerHandle::Get().get())); + + const base::FilePath cache_dir = temp_dir_.path().AppendASCII("files"); + scoped_ptr<internal::FileCache, test_util::DestroyHelperForTests> cache( + new internal::FileCache(metadata_storage.get(), + cache_dir, + base::ThreadTaskRunnerHandle::Get().get(), + fake_free_disk_space_getter_.get())); + + scoped_ptr<internal::ResourceMetadata, test_util::DestroyHelperForTests> + resource_metadata(new internal::ResourceMetadata( + metadata_storage_.get(), cache.get(), + base::ThreadTaskRunnerHandle::Get())); + + ASSERT_EQ(FILE_ERROR_OK, resource_metadata->Initialize()); + + const int64 changestamp = param == USE_SERVER_TIMESTAMP ? + fake_drive_service_->about_resource().largest_change_id() : 1; + ASSERT_EQ(FILE_ERROR_OK, + resource_metadata->SetLargestChangestamp(changestamp)); + + // drive/root + ResourceEntry root; + ASSERT_EQ(FILE_ERROR_OK, resource_metadata->GetResourceEntryByPath( + util::GetDriveMyDriveRootPath(), &root)); + root.set_resource_id(fake_drive_service_->GetRootResourceId()); + ASSERT_EQ(FILE_ERROR_OK, resource_metadata->RefreshEntry(root)); + + std::string local_id; + + // drive/root/File1 + ResourceEntry file1; + file1.set_title("File1"); + file1.set_resource_id("resource_id:File1"); + file1.set_parent_local_id(root.local_id()); + file1.mutable_file_specific_info()->set_md5("md5#1"); + file1.mutable_file_info()->set_is_directory(false); + file1.mutable_file_info()->set_size(1048576); + ASSERT_EQ(FILE_ERROR_OK, resource_metadata->AddEntry(file1, &local_id)); + + // drive/root/Dir1 + ResourceEntry dir1; + dir1.set_title("Dir1"); + dir1.set_resource_id("resource_id:Dir1"); + dir1.set_parent_local_id(root.local_id()); + dir1.mutable_file_info()->set_is_directory(true); + ASSERT_EQ(FILE_ERROR_OK, resource_metadata->AddEntry(dir1, &local_id)); + const std::string dir1_local_id = local_id; + + // drive/root/Dir1/File2 + ResourceEntry file2; + file2.set_title("File2"); + file2.set_resource_id("resource_id:File2"); + file2.set_parent_local_id(dir1_local_id); + file2.mutable_file_specific_info()->set_md5("md5#2"); + file2.mutable_file_info()->set_is_directory(false); + file2.mutable_file_info()->set_size(555); + ASSERT_EQ(FILE_ERROR_OK, resource_metadata->AddEntry(file2, &local_id)); + + // drive/root/Dir1/SubDir2 + ResourceEntry dir2; + dir2.set_title("SubDir2"); + dir2.set_resource_id("resource_id:SubDir2"); + dir2.set_parent_local_id(dir1_local_id); + dir2.mutable_file_info()->set_is_directory(true); + ASSERT_EQ(FILE_ERROR_OK, resource_metadata->AddEntry(dir2, &local_id)); + const std::string dir2_local_id = local_id; + + // drive/root/Dir1/SubDir2/File3 + ResourceEntry file3; + file3.set_title("File3"); + file3.set_resource_id("resource_id:File3"); + file3.set_parent_local_id(dir2_local_id); + file3.mutable_file_specific_info()->set_md5("md5#2"); + file3.mutable_file_info()->set_is_directory(false); + file3.mutable_file_info()->set_size(12345); + ASSERT_EQ(FILE_ERROR_OK, resource_metadata->AddEntry(file3, &local_id)); + + // Recreate resource metadata. + SetUpResourceMetadataAndFileSystem(); + } + + content::TestBrowserThreadBundle thread_bundle_; + base::ScopedTempDir temp_dir_; + // We don't use TestingProfile::GetPrefs() in favor of having less + // dependencies to Profile in general. + scoped_ptr<TestingPrefServiceSimple> pref_service_; + + scoped_ptr<EventLogger> logger_; + scoped_ptr<FakeDriveService> fake_drive_service_; + scoped_ptr<FakeFreeDiskSpaceGetter> fake_free_disk_space_getter_; + scoped_ptr<JobScheduler> scheduler_; + scoped_ptr<MockDirectoryChangeObserver> mock_directory_observer_; + + scoped_ptr<internal::ResourceMetadataStorage, + test_util::DestroyHelperForTests> metadata_storage_; + scoped_ptr<internal::FileCache, test_util::DestroyHelperForTests> cache_; + scoped_ptr<internal::ResourceMetadata, test_util::DestroyHelperForTests> + resource_metadata_; + scoped_refptr<base::SingleThreadTaskRunner> file_task_runner_; + scoped_ptr<FileSystem> file_system_; +}; + +TEST_F(FileSystemTest, SearchByHashes) { + ASSERT_NO_FATAL_FAILURE(SetUpTestFileSystem(USE_SERVER_TIMESTAMP)); + + std::set<std::string> hashes; + FileError error; + std::vector<HashAndFilePath> results; + + hashes.insert("md5#1"); + file_system_->SearchByHashes( + hashes, + google_apis::test_util::CreateCopyResultCallback(&error, &results)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_EQ(1u, results.size()); + EXPECT_EQ(FILE_PATH_LITERAL("drive/root/File1"), results[0].path.value()); + + hashes.clear(); + hashes.insert("md5#2"); + file_system_->SearchByHashes( + hashes, + google_apis::test_util::CreateCopyResultCallback(&error, &results)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_EQ(2u, results.size()); + std::sort(results.begin(), results.end(), &CompareHashAndFilePath); + EXPECT_EQ(FILE_PATH_LITERAL("drive/root/Dir1/File2"), + results[0].path.value()); + EXPECT_EQ(FILE_PATH_LITERAL("drive/root/Dir1/SubDir2/File3"), + results[1].path.value()); + + hashes.clear(); + hashes.insert("md5#1"); + hashes.insert("md5#2"); + file_system_->SearchByHashes( + hashes, + google_apis::test_util::CreateCopyResultCallback(&error, &results)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_EQ(3u, results.size()); + std::sort(results.begin(), results.end(), &CompareHashAndFilePath); + EXPECT_EQ(FILE_PATH_LITERAL("drive/root/File1"), results[0].path.value()); + EXPECT_EQ(FILE_PATH_LITERAL("drive/root/Dir1/File2"), + results[1].path.value()); + EXPECT_EQ(FILE_PATH_LITERAL("drive/root/Dir1/SubDir2/File3"), + results[2].path.value()); +} + +TEST_F(FileSystemTest, Copy) { + base::FilePath src_file_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + base::FilePath dest_file_path(FILE_PATH_LITERAL("drive/root/Copied.txt")); + EXPECT_TRUE(GetResourceEntrySync(src_file_path)); + EXPECT_FALSE(GetResourceEntrySync(dest_file_path)); + + FileError error = FILE_ERROR_FAILED; + file_system_->Copy(src_file_path, + dest_file_path, + false, // preserve_last_modified, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Entry is added on the server. + scoped_ptr<ResourceEntry> entry = GetResourceEntrySync(dest_file_path); + ASSERT_TRUE(entry); + + google_apis::DriveApiErrorCode status = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> server_entry; + fake_drive_service_->GetFileResource( + entry->resource_id(), + google_apis::test_util::CreateCopyResultCallback(&status, &server_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, status); + ASSERT_TRUE(server_entry); + EXPECT_EQ(entry->title(), server_entry->title()); + EXPECT_FALSE(server_entry->IsDirectory()); +} + +TEST_F(FileSystemTest, Move) { + base::FilePath src_file_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + base::FilePath dest_file_path( + FILE_PATH_LITERAL("drive/root/Directory 1/Moved.txt")); + EXPECT_TRUE(GetResourceEntrySync(src_file_path)); + EXPECT_FALSE(GetResourceEntrySync(dest_file_path)); + scoped_ptr<ResourceEntry> parent = + GetResourceEntrySync(dest_file_path.DirName()); + ASSERT_TRUE(parent); + + FileError error = FILE_ERROR_FAILED; + file_system_->Move(src_file_path, + dest_file_path, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Entry is moved on the server. + scoped_ptr<ResourceEntry> entry = GetResourceEntrySync(dest_file_path); + ASSERT_TRUE(entry); + + google_apis::DriveApiErrorCode status = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> server_entry; + fake_drive_service_->GetFileResource( + entry->resource_id(), + google_apis::test_util::CreateCopyResultCallback(&status, &server_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, status); + ASSERT_TRUE(server_entry); + EXPECT_EQ(entry->title(), server_entry->title()); + + ASSERT_FALSE(server_entry->parents().empty()); + EXPECT_EQ(parent->resource_id(), server_entry->parents()[0].file_id()); +} + +TEST_F(FileSystemTest, Remove) { + base::FilePath file_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + scoped_ptr<ResourceEntry> entry = GetResourceEntrySync(file_path); + ASSERT_TRUE(entry); + + FileError error = FILE_ERROR_FAILED; + file_system_->Remove( + file_path, + false, // is_resursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Entry is removed on the server. + google_apis::DriveApiErrorCode status = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> server_entry; + fake_drive_service_->GetFileResource( + entry->resource_id(), + google_apis::test_util::CreateCopyResultCallback(&status, &server_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, status); + ASSERT_TRUE(server_entry); + EXPECT_TRUE(server_entry->labels().is_trashed()); +} + +TEST_F(FileSystemTest, CreateDirectory) { + base::FilePath directory_path(FILE_PATH_LITERAL("drive/root/New Directory")); + EXPECT_FALSE(GetResourceEntrySync(directory_path)); + + FileError error = FILE_ERROR_FAILED; + file_system_->CreateDirectory( + directory_path, + true, // is_exclusive + false, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Directory is created on the server. + scoped_ptr<ResourceEntry> entry = GetResourceEntrySync(directory_path); + ASSERT_TRUE(entry); + + google_apis::DriveApiErrorCode status = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> server_entry; + fake_drive_service_->GetFileResource( + entry->resource_id(), + google_apis::test_util::CreateCopyResultCallback(&status, &server_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, status); + ASSERT_TRUE(server_entry); + EXPECT_EQ(entry->title(), server_entry->title()); + EXPECT_TRUE(server_entry->IsDirectory()); +} + +TEST_F(FileSystemTest, CreateFile) { + base::FilePath file_path(FILE_PATH_LITERAL("drive/root/New File.txt")); + EXPECT_FALSE(GetResourceEntrySync(file_path)); + + FileError error = FILE_ERROR_FAILED; + file_system_->CreateFile( + file_path, + true, // is_exclusive + "text/plain", + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // File is created on the server. + scoped_ptr<ResourceEntry> entry = GetResourceEntrySync(file_path); + ASSERT_TRUE(entry); + + google_apis::DriveApiErrorCode status = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> server_entry; + fake_drive_service_->GetFileResource( + entry->resource_id(), + google_apis::test_util::CreateCopyResultCallback(&status, &server_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, status); + ASSERT_TRUE(server_entry); + EXPECT_EQ(entry->title(), server_entry->title()); + EXPECT_FALSE(server_entry->IsDirectory()); +} + +TEST_F(FileSystemTest, TouchFile) { + base::FilePath file_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + scoped_ptr<ResourceEntry> entry = GetResourceEntrySync(file_path); + ASSERT_TRUE(entry); + + base::Time last_accessed = + base::Time::FromInternalValue(entry->file_info().last_accessed()) + + base::TimeDelta::FromSeconds(1); + base::Time last_modified = + base::Time::FromInternalValue(entry->file_info().last_modified()) + + base::TimeDelta::FromSeconds(1); + + FileError error = FILE_ERROR_FAILED; + file_system_->TouchFile( + file_path, + last_accessed, + last_modified, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // File is touched on the server. + google_apis::DriveApiErrorCode status = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> server_entry; + fake_drive_service_->GetFileResource( + entry->resource_id(), + google_apis::test_util::CreateCopyResultCallback(&status, &server_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, status); + ASSERT_TRUE(server_entry); + EXPECT_EQ(last_accessed, server_entry->last_viewed_by_me_date()); + EXPECT_EQ(last_modified, server_entry->modified_date()); +} + +TEST_F(FileSystemTest, TruncateFile) { + base::FilePath file_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + scoped_ptr<ResourceEntry> entry = GetResourceEntrySync(file_path); + ASSERT_TRUE(entry); + + const int64 kLength = entry->file_info().size() + 100; + + FileError error = FILE_ERROR_FAILED; + file_system_->TruncateFile( + file_path, + kLength, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // File is touched on the server. + google_apis::DriveApiErrorCode status = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> server_entry; + fake_drive_service_->GetFileResource( + entry->resource_id(), + google_apis::test_util::CreateCopyResultCallback(&status, &server_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, status); + ASSERT_TRUE(server_entry); + EXPECT_EQ(kLength, server_entry->file_size()); +} + +TEST_F(FileSystemTest, DuplicatedAsyncInitialization) { + base::RunLoop loop; + + int counter = 0; + const GetResourceEntryCallback& callback = base::Bind( + &AsyncInitializationCallback, &counter, 2, loop.QuitClosure()); + + file_system_->GetResourceEntry( + base::FilePath(FILE_PATH_LITERAL("drive/root")), callback); + file_system_->GetResourceEntry( + base::FilePath(FILE_PATH_LITERAL("drive/root")), callback); + loop.Run(); // Wait to get our result + EXPECT_EQ(2, counter); + + EXPECT_EQ(1, fake_drive_service_->file_list_load_count()); +} + +TEST_F(FileSystemTest, GetGrandRootEntry) { + const base::FilePath kFilePath(FILE_PATH_LITERAL("drive")); + scoped_ptr<ResourceEntry> entry = GetResourceEntrySync(kFilePath); + ASSERT_TRUE(entry); + EXPECT_EQ(util::kDriveGrandRootLocalId, entry->local_id()); +} + +TEST_F(FileSystemTest, GetOtherDirEntry) { + const base::FilePath kFilePath(FILE_PATH_LITERAL("drive/other")); + scoped_ptr<ResourceEntry> entry = GetResourceEntrySync(kFilePath); + ASSERT_TRUE(entry); + EXPECT_EQ(util::kDriveOtherDirLocalId, entry->local_id()); +} + +TEST_F(FileSystemTest, GetMyDriveRoot) { + const base::FilePath kFilePath(FILE_PATH_LITERAL("drive/root")); + scoped_ptr<ResourceEntry> entry = GetResourceEntrySync(kFilePath); + ASSERT_TRUE(entry); + EXPECT_EQ(fake_drive_service_->GetRootResourceId(), entry->resource_id()); + + // After "fast fetch" is done, full resource list is fetched. + EXPECT_EQ(1, fake_drive_service_->file_list_load_count()); +} + +TEST_F(FileSystemTest, GetExistingFile) { + // Simulate the situation that full feed fetching takes very long time, + // to test the recursive "fast fetch" feature is properly working. + fake_drive_service_->set_never_return_all_file_list(true); + + const base::FilePath kFilePath( + FILE_PATH_LITERAL("drive/root/Directory 1/SubDirectory File 1.txt")); + scoped_ptr<ResourceEntry> entry = GetResourceEntrySync(kFilePath); + ASSERT_TRUE(entry); + EXPECT_EQ("subdirectory_file_1_id", entry->resource_id()); + + EXPECT_EQ(1, fake_drive_service_->about_resource_load_count()); + EXPECT_EQ(2, fake_drive_service_->directory_load_count()); + EXPECT_EQ(1, fake_drive_service_->blocked_file_list_load_count()); +} + +TEST_F(FileSystemTest, GetExistingDocument) { + const base::FilePath kFilePath( + FILE_PATH_LITERAL("drive/root/Document 1 excludeDir-test.gdoc")); + scoped_ptr<ResourceEntry> entry = GetResourceEntrySync(kFilePath); + ASSERT_TRUE(entry); + EXPECT_EQ("5_document_resource_id", entry->resource_id()); +} + +TEST_F(FileSystemTest, GetNonExistingFile) { + const base::FilePath kFilePath( + FILE_PATH_LITERAL("drive/root/nonexisting.file")); + scoped_ptr<ResourceEntry> entry = GetResourceEntrySync(kFilePath); + EXPECT_FALSE(entry); +} + +TEST_F(FileSystemTest, GetInSubSubdir) { + const base::FilePath kFilePath( + FILE_PATH_LITERAL("drive/root/Directory 1/Sub Directory Folder/" + "Sub Sub Directory Folder")); + scoped_ptr<ResourceEntry> entry = GetResourceEntrySync(kFilePath); + ASSERT_TRUE(entry); + ASSERT_EQ("sub_sub_directory_folder_id", entry->resource_id()); +} + +TEST_F(FileSystemTest, GetOrphanFile) { + ASSERT_TRUE(LoadFullResourceList()); + + // Entry without parents are placed under "drive/other". + const base::FilePath kFilePath( + FILE_PATH_LITERAL("drive/other/Orphan File 1.txt")); + scoped_ptr<ResourceEntry> entry = GetResourceEntrySync(kFilePath); + ASSERT_TRUE(entry); + EXPECT_EQ("1_orphanfile_resource_id", entry->resource_id()); +} + +TEST_F(FileSystemTest, ReadDirectory_Root) { + // ReadDirectory() should kick off the resource list loading. + scoped_ptr<ResourceEntryVector> entries( + ReadDirectorySync(base::FilePath::FromUTF8Unsafe("drive"))); + // The root directory should be read correctly. + ASSERT_TRUE(entries); + ASSERT_EQ(3U, entries->size()); + + // The found three directories should be /drive/root, /drive/other and + // /drive/trash. + std::set<base::FilePath> found; + for (size_t i = 0; i < entries->size(); ++i) + found.insert(base::FilePath::FromUTF8Unsafe((*entries)[i].title())); + EXPECT_EQ(3U, found.size()); + EXPECT_EQ(1U, found.count(base::FilePath::FromUTF8Unsafe( + util::kDriveMyDriveRootDirName))); + EXPECT_EQ(1U, found.count( + base::FilePath::FromUTF8Unsafe(util::kDriveOtherDirName))); + EXPECT_EQ(1U, found.count( + base::FilePath::FromUTF8Unsafe(util::kDriveTrashDirName))); +} + +TEST_F(FileSystemTest, ReadDirectory_NonRootDirectory) { + // ReadDirectory() should kick off the resource list loading. + scoped_ptr<ResourceEntryVector> entries( + ReadDirectorySync( + base::FilePath::FromUTF8Unsafe("drive/root/Directory 1"))); + // The non root directory should also be read correctly. + // There was a bug (crbug.com/181487), which broke this behavior. + // Make sure this is fixed. + ASSERT_TRUE(entries); + EXPECT_EQ(3U, entries->size()); +} + +TEST_F(FileSystemTest, LoadFileSystemFromUpToDateCache) { + ASSERT_NO_FATAL_FAILURE(SetUpTestFileSystem(USE_SERVER_TIMESTAMP)); + + // Kicks loading of cached file system and query for server update. + EXPECT_TRUE(ReadDirectorySync(util::GetDriveMyDriveRootPath())); + + // SetUpTestFileSystem and FakeDriveService have the same + // changestamp (i.e. the local metadata is up-to-date), so no request for + // new resource list (i.e., call to GetResourceList) should happen. + EXPECT_EQ(0, fake_drive_service_->file_list_load_count()); + + // Since the file system has verified that it holds the latest snapshot, + // it should change its state to "loaded", which admits periodic refresh. + // To test it, call CheckForUpdates and verify it does try to check updates. + const int about_resource_load_count_before = + fake_drive_service_->about_resource_load_count(); + file_system_->CheckForUpdates(); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_LT(about_resource_load_count_before, + fake_drive_service_->about_resource_load_count()); +} + +TEST_F(FileSystemTest, LoadFileSystemFromCacheWhileOffline) { + ASSERT_NO_FATAL_FAILURE(SetUpTestFileSystem(USE_OLD_TIMESTAMP)); + + // Make GetResourceList fail for simulating offline situation. This will + // leave the file system "loaded from cache, but not synced with server" + // state. + fake_drive_service_->set_offline(true); + + // Load the root. + EXPECT_TRUE(ReadDirectorySync(util::GetDriveGrandRootPath())); + // Loading of about resource should not happen as it's offline. + EXPECT_EQ(0, fake_drive_service_->about_resource_load_count()); + + // Load "My Drive". + EXPECT_TRUE(ReadDirectorySync(util::GetDriveMyDriveRootPath())); + EXPECT_EQ(0, fake_drive_service_->about_resource_load_count()); + + // Tests that cached data can be loaded even if the server is not reachable. + EXPECT_TRUE(EntryExists(base::FilePath( + FILE_PATH_LITERAL("drive/root/File1")))); + EXPECT_TRUE(EntryExists(base::FilePath( + FILE_PATH_LITERAL("drive/root/Dir1")))); + EXPECT_TRUE(EntryExists(base::FilePath( + FILE_PATH_LITERAL("drive/root/Dir1/File2")))); + EXPECT_TRUE(EntryExists(base::FilePath( + FILE_PATH_LITERAL("drive/root/Dir1/SubDir2")))); + EXPECT_TRUE(EntryExists(base::FilePath( + FILE_PATH_LITERAL("drive/root/Dir1/SubDir2/File3")))); + + // Since the file system has at least succeeded to load cached snapshot, + // the file system should be able to start periodic refresh. + // To test it, call CheckForUpdates and verify it does try to check + // updates, which will cause directory changes. + fake_drive_service_->set_offline(false); + + file_system_->CheckForUpdates(); + + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(1, fake_drive_service_->about_resource_load_count()); + EXPECT_EQ(1, fake_drive_service_->change_list_load_count()); + + ASSERT_LE(0u, mock_directory_observer_->changed_directories().size()); + ASSERT_LE(1u, mock_directory_observer_->changed_files().size()); +} + +TEST_F(FileSystemTest, ReadDirectoryWhileRefreshing) { + // Use old timestamp so the fast fetch will be performed. + ASSERT_NO_FATAL_FAILURE(SetUpTestFileSystem(USE_OLD_TIMESTAMP)); + + // The list of resources in "drive/root/Dir1" should be fetched. + EXPECT_TRUE(ReadDirectorySync(base::FilePath( + FILE_PATH_LITERAL("drive/root/Dir1")))); + EXPECT_EQ(1, fake_drive_service_->directory_load_count()); + + ASSERT_LE(1u, mock_directory_observer_->changed_directories().size()); +} + +TEST_F(FileSystemTest, GetResourceEntryNonExistentWhileRefreshing) { + // Use old timestamp so the fast fetch will be performed. + ASSERT_NO_FATAL_FAILURE(SetUpTestFileSystem(USE_OLD_TIMESTAMP)); + + // If an entry is not found, parent directory's resource list is fetched. + EXPECT_FALSE(GetResourceEntrySync(base::FilePath( + FILE_PATH_LITERAL("drive/root/Dir1/NonExistentFile")))); + EXPECT_EQ(1, fake_drive_service_->directory_load_count()); + + ASSERT_LE(1u, mock_directory_observer_->changed_directories().size()); +} + +TEST_F(FileSystemTest, CreateDirectoryByImplicitLoad) { + // Intentionally *not* calling LoadFullResourceList(), for testing that + // CreateDirectory ensures the resource list is loaded before it runs. + + base::FilePath existing_directory( + FILE_PATH_LITERAL("drive/root/Directory 1")); + FileError error = FILE_ERROR_FAILED; + file_system_->CreateDirectory( + existing_directory, + true, // is_exclusive + false, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + + // It should fail because is_exclusive is set to true. + EXPECT_EQ(FILE_ERROR_EXISTS, error); +} + +TEST_F(FileSystemTest, CreateDirectoryRecursively) { + // Intentionally *not* calling LoadFullResourceList(), for testing that + // CreateDirectory ensures the resource list is loaded before it runs. + + base::FilePath new_directory( + FILE_PATH_LITERAL("drive/root/Directory 1/a/b/c/d")); + FileError error = FILE_ERROR_FAILED; + file_system_->CreateDirectory( + new_directory, + true, // is_exclusive + true, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + + EXPECT_EQ(FILE_ERROR_OK, error); + + scoped_ptr<ResourceEntry> entry(GetResourceEntrySync(new_directory)); + ASSERT_TRUE(entry); + EXPECT_TRUE(entry->file_info().is_directory()); +} + +TEST_F(FileSystemTest, ReadDirectoryAfterUpdateWhileLoading) { + // Simulate the situation that full feed fetching takes very long time, + // to test the recursive "fast fetch" feature is properly working. + fake_drive_service_->set_never_return_all_file_list(true); + + // On the fake server, create the test directory. + scoped_ptr<google_apis::FileResource> parent; + { + google_apis::DriveApiErrorCode error = google_apis::DRIVE_OTHER_ERROR; + fake_drive_service_->AddNewDirectory( + fake_drive_service_->GetRootResourceId(), "UpdateWhileLoadingTestDir", + AddNewDirectoryOptions(), + google_apis::test_util::CreateCopyResultCallback(&error, &parent)); + base::RunLoop().RunUntilIdle(); + ASSERT_EQ(google_apis::HTTP_CREATED, error); + } + + // Fetch the directory. Currently it is empty. + scoped_ptr<ResourceEntryVector> before = ReadDirectorySync(base::FilePath( + FILE_PATH_LITERAL("drive/root/UpdateWhileLoadingTestDir"))); + ASSERT_TRUE(before); + EXPECT_EQ(0u, before->size()); + + // Create a file in the test directory. + scoped_ptr<google_apis::FileResource> entry; + { + google_apis::DriveApiErrorCode error = google_apis::DRIVE_OTHER_ERROR; + fake_drive_service_->AddNewFile( + "text/plain", + "(dummy data)", + parent->file_id(), + "TestFile", + false, // shared_with_me + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + ASSERT_EQ(google_apis::HTTP_CREATED, error); + } + + // Notify the update to the file system. + file_system_->CheckForUpdates(); + + // Read the directory once again. Although the full feed fetching is not yet + // finished, the "fast fetch" of the directory works and the refreshed content + // is returned. + scoped_ptr<ResourceEntryVector> after = ReadDirectorySync(base::FilePath( + FILE_PATH_LITERAL("drive/root/UpdateWhileLoadingTestDir"))); + ASSERT_TRUE(after); + EXPECT_EQ(1u, after->size()); +} + +TEST_F(FileSystemTest, PinAndUnpin) { + ASSERT_TRUE(LoadFullResourceList()); + + base::FilePath file_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + + // Get the file info. + scoped_ptr<ResourceEntry> entry(GetResourceEntrySync(file_path)); + ASSERT_TRUE(entry); + + // Pin the file. + FileError error = FILE_ERROR_FAILED; + file_system_->Pin(file_path, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + entry = GetResourceEntrySync(file_path); + ASSERT_TRUE(entry); + EXPECT_TRUE(entry->file_specific_info().cache_state().is_pinned()); + EXPECT_TRUE(entry->file_specific_info().cache_state().is_present()); + + // Unpin the file. + error = FILE_ERROR_FAILED; + file_system_->Unpin(file_path, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + entry = GetResourceEntrySync(file_path); + ASSERT_TRUE(entry); + EXPECT_FALSE(entry->file_specific_info().cache_state().is_pinned()); + + // Pinned file gets synced and it results in entry state changes. + ASSERT_EQ(0u, mock_directory_observer_->changed_directories().size()); + ASSERT_EQ(1u, mock_directory_observer_->changed_files().size()); + EXPECT_EQ(1u, + mock_directory_observer_->changed_files().CountDirectory( + base::FilePath(FILE_PATH_LITERAL("drive/root")))); +} + +TEST_F(FileSystemTest, PinAndUnpin_NotSynced) { + ASSERT_TRUE(LoadFullResourceList()); + + base::FilePath file_path(FILE_PATH_LITERAL("drive/root/File 1.txt")); + + // Get the file info. + scoped_ptr<ResourceEntry> entry(GetResourceEntrySync(file_path)); + ASSERT_TRUE(entry); + + // Unpin the file just after pinning. File fetch should be cancelled. + FileError error_pin = FILE_ERROR_FAILED; + file_system_->Pin( + file_path, + google_apis::test_util::CreateCopyResultCallback(&error_pin)); + + FileError error_unpin = FILE_ERROR_FAILED; + file_system_->Unpin( + file_path, + google_apis::test_util::CreateCopyResultCallback(&error_unpin)); + + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error_pin); + EXPECT_EQ(FILE_ERROR_OK, error_unpin); + + // No cache file available because the sync was cancelled by Unpin(). + entry = GetResourceEntrySync(file_path); + ASSERT_TRUE(entry); + EXPECT_FALSE(entry->file_specific_info().cache_state().is_present()); +} + +TEST_F(FileSystemTest, GetAvailableSpace) { + FileError error = FILE_ERROR_OK; + int64 bytes_total; + int64 bytes_used; + file_system_->GetAvailableSpace( + google_apis::test_util::CreateCopyResultCallback( + &error, &bytes_total, &bytes_used)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(6789012345LL, bytes_used); + EXPECT_EQ(9876543210LL, bytes_total); +} + +TEST_F(FileSystemTest, MarkCacheFileAsMountedAndUnmounted) { + ASSERT_TRUE(LoadFullResourceList()); + + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + + // Make the file cached. + FileError error = FILE_ERROR_FAILED; + base::FilePath file_path; + scoped_ptr<ResourceEntry> entry; + file_system_->GetFile( + file_in_root, + google_apis::test_util::CreateCopyResultCallback( + &error, &file_path, &entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Test for mounting. + error = FILE_ERROR_FAILED; + file_path.clear(); + file_system_->MarkCacheFileAsMounted( + file_in_root, + google_apis::test_util::CreateCopyResultCallback(&error, &file_path)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Cannot remove a cache entry while it's being mounted. + EXPECT_EQ(FILE_ERROR_IN_USE, cache_->Remove(entry->local_id())); + + // Test for unmounting. + error = FILE_ERROR_FAILED; + file_system_->MarkCacheFileAsUnmounted( + file_path, + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Now able to remove the cache entry. + EXPECT_EQ(FILE_ERROR_OK, cache_->Remove(entry->local_id())); +} + +TEST_F(FileSystemTest, GetShareUrl) { + ASSERT_TRUE(LoadFullResourceList()); + + const base::FilePath kFileInRoot(FILE_PATH_LITERAL("drive/root/File 1.txt")); + const GURL kEmbedOrigin("chrome-extension://test-id"); + + // Try to fetch the URL for the sharing dialog. + FileError error = FILE_ERROR_FAILED; + GURL share_url; + file_system_->GetShareUrl( + kFileInRoot, + kEmbedOrigin, + google_apis::test_util::CreateCopyResultCallback(&error, &share_url)); + content::RunAllBlockingPoolTasksUntilIdle(); + + // Verify the share url to the sharing dialog. + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_TRUE(share_url.is_valid()); +} + +TEST_F(FileSystemTest, FreeDiskSpaceIfNeededFor) { + ASSERT_TRUE(LoadFullResourceList()); + + base::FilePath file_in_root(FILE_PATH_LITERAL("drive/root/File 1.txt")); + + // Make the file cached. + FileError error = FILE_ERROR_FAILED; + base::FilePath file_path; + scoped_ptr<ResourceEntry> entry; + file_system_->GetFile(file_in_root, + 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()); + + bool available; + file_system_->FreeDiskSpaceIfNeededFor( + 512LL << 40, + google_apis::test_util::CreateCopyResultCallback(&available)); + content::RunAllBlockingPoolTasksUntilIdle(); + ASSERT_FALSE(available); + + entry = GetResourceEntrySync(file_in_root); + ASSERT_TRUE(entry); + EXPECT_FALSE(entry->file_specific_info().cache_state().is_present()); +} + +} // namespace drive diff --git a/components/drive/remove_stale_cache_files.cc b/components/drive/remove_stale_cache_files.cc new file mode 100644 index 0000000..93cab01 --- /dev/null +++ b/components/drive/remove_stale_cache_files.cc @@ -0,0 +1,34 @@ +// 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/remove_stale_cache_files.h" + +#include "base/logging.h" +#include "components/drive/drive.pb.h" +#include "components/drive/file_cache.h" +#include "components/drive/resource_metadata.h" + +namespace drive { +namespace internal { + +void RemoveStaleCacheFiles(FileCache* cache, + ResourceMetadata* resource_metadata) { + scoped_ptr<ResourceMetadata::Iterator> it = resource_metadata->GetIterator(); + for (; !it->IsAtEnd(); it->Advance()) { + const ResourceEntry& entry = it->GetValue(); + const FileCacheEntry& cache_state = + entry.file_specific_info().cache_state(); + // Stale = not dirty but the MD5 does not match. + if (!cache_state.is_dirty() && + cache_state.md5() != entry.file_specific_info().md5()) { + FileError error = cache->Remove(it->GetID()); + LOG_IF(WARNING, error != FILE_ERROR_OK) + << "Failed to remove a stale cache file. resource_id: " + << it->GetID(); + } + } +} + +} // namespace internal +} // namespace drive diff --git a/components/drive/remove_stale_cache_files.h b/components/drive/remove_stale_cache_files.h new file mode 100644 index 0000000..d3e922c --- /dev/null +++ b/components/drive/remove_stale_cache_files.h @@ -0,0 +1,22 @@ +// 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_REMOVE_STALE_CACHE_FILES_H_ +#define COMPONENTS_DRIVE_REMOVE_STALE_CACHE_FILES_H_ + +namespace drive{ +namespace internal { + +class FileCache; +class ResourceMetadata; + +// Removes files from |cache| which are not dirty but the MD5 is obsolete. +// Must be run on the same task runner as |cache| and |resource_metadata| use. +void RemoveStaleCacheFiles(FileCache* cache, + ResourceMetadata* resource_metadata); + +} // namespace internal +} // namespace drive + +#endif // COMPONENTS_DRIVE_REMOVE_STALE_CACHE_FILES_H_ diff --git a/components/drive/remove_stale_cache_files_unittest.cc b/components/drive/remove_stale_cache_files_unittest.cc new file mode 100644 index 0000000..4fa16e6 --- /dev/null +++ b/components/drive/remove_stale_cache_files_unittest.cc @@ -0,0 +1,115 @@ +// 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 <string> + +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/scoped_ptr.h" +#include "base/single_thread_task_runner.h" +#include "base/thread_task_runner_handle.h" +#include "components/drive/drive.pb.h" +#include "components/drive/drive_test_util.h" +#include "components/drive/fake_free_disk_space_getter.h" +#include "components/drive/file_system_core_util.h" +#include "components/drive/remove_stale_cache_files.h" +#include "components/drive/resource_metadata.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace internal { + +class RemoveStaleCacheFilesTest : public testing::Test { + protected: + void SetUp() override { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + + fake_free_disk_space_getter_.reset(new FakeFreeDiskSpaceGetter); + + metadata_storage_.reset(new ResourceMetadataStorage( + temp_dir_.path(), base::ThreadTaskRunnerHandle::Get().get())); + + cache_.reset(new FileCache(metadata_storage_.get(), + temp_dir_.path(), + base::ThreadTaskRunnerHandle::Get().get(), + fake_free_disk_space_getter_.get())); + + resource_metadata_.reset(new ResourceMetadata( + metadata_storage_.get(), cache_.get(), + base::ThreadTaskRunnerHandle::Get())); + + ASSERT_TRUE(metadata_storage_->Initialize()); + ASSERT_TRUE(cache_->Initialize()); + ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->Initialize()); + } + + content::TestBrowserThreadBundle thread_bundle_; + base::ScopedTempDir temp_dir_; + + scoped_ptr<ResourceMetadataStorage, + test_util::DestroyHelperForTests> metadata_storage_; + scoped_ptr<FileCache, test_util::DestroyHelperForTests> cache_; + scoped_ptr<ResourceMetadata, test_util::DestroyHelperForTests> + resource_metadata_; + scoped_ptr<FakeFreeDiskSpaceGetter> fake_free_disk_space_getter_; +}; + +TEST_F(RemoveStaleCacheFilesTest, RemoveStaleCacheFiles) { + base::FilePath dummy_file; + ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir_.path(), &dummy_file)); + std::string md5_metadata("abcdef0123456789"), md5_cache("ABCDEF9876543210"); + + // Create a stale cache file. + ResourceEntry entry; + std::string local_id; + entry.mutable_file_specific_info()->set_md5(md5_metadata); + entry.set_parent_local_id(util::kDriveGrandRootLocalId); + entry.set_title("File.txt"); + EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(entry, &local_id)); + + EXPECT_EQ(FILE_ERROR_OK, + cache_->Store(local_id, md5_cache, dummy_file, + FileCache::FILE_OPERATION_COPY)); + + // Remove stale cache files. + RemoveStaleCacheFiles(cache_.get(), resource_metadata_.get()); + + // Verify that the cache is deleted. + EXPECT_EQ(FILE_ERROR_OK, + resource_metadata_->GetResourceEntryById(local_id, &entry)); + EXPECT_FALSE(entry.file_specific_info().cache_state().is_present()); +} + +TEST_F(RemoveStaleCacheFilesTest, DirtyCacheFiles) { + base::FilePath dummy_file; + ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir_.path(), &dummy_file)); + + // Dirty entry. + std::string md5_metadata("abcdef0123456789"); + + ResourceEntry entry; + std::string local_id; + entry.mutable_file_specific_info()->set_md5(md5_metadata); + entry.set_parent_local_id(util::kDriveGrandRootLocalId); + entry.set_title("file.txt"); + EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(entry, &local_id)); + + EXPECT_EQ(FILE_ERROR_OK, + cache_->Store(local_id, std::string(), dummy_file, + FileCache::FILE_OPERATION_COPY)); + + // Remove stale cache files. + RemoveStaleCacheFiles(cache_.get(), resource_metadata_.get()); + + // Dirty cache should not be removed even though its MD5 doesn't match. + EXPECT_EQ(FILE_ERROR_OK, + resource_metadata_->GetResourceEntryById(local_id, &entry)); + EXPECT_TRUE(entry.file_specific_info().cache_state().is_present()); +} + +} // namespace internal +} // namespace drive diff --git a/components/drive/search_metadata.cc b/components/drive/search_metadata.cc new file mode 100644 index 0000000..381e81f --- /dev/null +++ b/components/drive/search_metadata.cc @@ -0,0 +1,335 @@ +// 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/search_metadata.h" + +#include <algorithm> +#include <queue> + +#include "base/bind.h" +#include "base/i18n/string_search.h" +#include "base/metrics/histogram.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "components/drive/drive_api_util.h" +#include "components/drive/file_system_core_util.h" +#include "net/base/escape.h" + +namespace drive { +namespace internal { + +namespace { + +struct ResultCandidate { + ResultCandidate(const std::string& local_id, + const ResourceEntry& entry, + const std::string& highlighted_base_name) + : local_id(local_id), + entry(entry), + highlighted_base_name(highlighted_base_name) { + } + + std::string local_id; + ResourceEntry entry; + std::string highlighted_base_name; +}; + +// Used to sort the result candidates per the last accessed/modified time. The +// recently accessed/modified files come first. +bool CompareByTimestamp(const ResourceEntry& a, const ResourceEntry& b) { + const PlatformFileInfoProto& a_file_info = a.file_info(); + const PlatformFileInfoProto& b_file_info = b.file_info(); + + if (a_file_info.last_accessed() != b_file_info.last_accessed()) + return a_file_info.last_accessed() > b_file_info.last_accessed(); + + // When the entries have the same last access time (which happens quite often + // because Drive server doesn't set the field until an entry is viewed via + // drive.google.com), we use last modified time as the tie breaker. + return a_file_info.last_modified() > b_file_info.last_modified(); +} + +struct ResultCandidateComparator { + bool operator()(const ResultCandidate* a, const ResultCandidate* b) const { + return CompareByTimestamp(a->entry, b->entry); + } +}; + +// A wrapper of std::priority_queue which deals with pointers of values. +template<typename T, typename Compare> +class ScopedPriorityQueue { + public: + ScopedPriorityQueue() {} + + ~ScopedPriorityQueue() { + while (!empty()) + pop(); + } + + bool empty() const { return queue_.empty(); } + + size_t size() const { return queue_.size(); } + + const T* top() const { return queue_.top(); } + + void push(T* x) { queue_.push(x); } + + void pop() { + // Keep top alive for the pop() call so that debug checks can access + // underlying data (e.g. validating heap property of the priority queue + // will call the comparator). + T* saved_top = queue_.top(); + queue_.pop(); + delete saved_top; + } + + private: + std::priority_queue<T*, std::vector<T*>, Compare> queue_; + + DISALLOW_COPY_AND_ASSIGN(ScopedPriorityQueue); +}; + +// Classifies the given entry as hidden if it's not under specific directories. +class HiddenEntryClassifier { + public: + HiddenEntryClassifier(ResourceMetadata* metadata, + const std::string& mydrive_local_id) + : metadata_(metadata) { + // Only things under My Drive and drive/other are not hidden. + is_hiding_child_[mydrive_local_id] = false; + is_hiding_child_[util::kDriveOtherDirLocalId] = false; + + // Everything else is hidden, including the directories mentioned above + // themselves. + is_hiding_child_[""] = true; + } + + // |result| is set to true if |entry| is hidden. + FileError IsHidden(const ResourceEntry& entry, bool* result) { + // Look up for parents recursively. + std::vector<std::string> undetermined_ids; + undetermined_ids.push_back(entry.parent_local_id()); + + std::map<std::string, bool>::iterator it = + is_hiding_child_.find(undetermined_ids.back()); + for (; it == is_hiding_child_.end(); + it = is_hiding_child_.find(undetermined_ids.back())) { + ResourceEntry parent; + FileError error = + metadata_->GetResourceEntryById(undetermined_ids.back(), &parent); + if (error != FILE_ERROR_OK) + return error; + undetermined_ids.push_back(parent.parent_local_id()); + } + + // Cache the result. + undetermined_ids.pop_back(); // The last one is already in the map. + for (size_t i = 0; i < undetermined_ids.size(); ++i) + is_hiding_child_[undetermined_ids[i]] = it->second; + + *result = it->second; + return FILE_ERROR_OK; + } + + private: + ResourceMetadata* metadata_; + + // local ID to is_hidden map. + std::map<std::string, bool> is_hiding_child_; +}; + +// Used to implement SearchMetadata. +// Adds entry to the result when appropriate. +// In particular, if |query| is non-null, only adds files with the name matching +// the query. +FileError MaybeAddEntryToResult( + ResourceMetadata* resource_metadata, + ResourceMetadata::Iterator* it, + base::i18n::FixedPatternStringSearchIgnoringCaseAndAccents* query, + const SearchMetadataPredicate& predicate, + size_t at_most_num_matches, + HiddenEntryClassifier* hidden_entry_classifier, + ScopedPriorityQueue<ResultCandidate, ResultCandidateComparator>* + result_candidates) { + DCHECK_GE(at_most_num_matches, result_candidates->size()); + + const ResourceEntry& entry = it->GetValue(); + + // If the candidate set is already full, and this |entry| is old, do nothing. + // We perform this check first in order to avoid the costly find-and-highlight + // or FilePath lookup as much as possible. + if (result_candidates->size() == at_most_num_matches && + !CompareByTimestamp(entry, result_candidates->top()->entry)) + return FILE_ERROR_OK; + + // Add |entry| to the result if the entry is eligible for the given + // |options| and matches the query. The base name of the entry must + // contain |query| to match the query. + std::string highlighted; + if (!predicate.Run(entry) || + (query && !FindAndHighlight(entry.base_name(), query, &highlighted))) + return FILE_ERROR_OK; + + // Hidden entry should not be returned. + bool hidden = false; + FileError error = hidden_entry_classifier->IsHidden(entry, &hidden); + if (error != FILE_ERROR_OK || hidden) + return error; + + // Make space for |entry| when appropriate. + if (result_candidates->size() == at_most_num_matches) + result_candidates->pop(); + result_candidates->push(new ResultCandidate(it->GetID(), entry, highlighted)); + return FILE_ERROR_OK; +} + +// Implements SearchMetadata(). +FileError SearchMetadataOnBlockingPool(ResourceMetadata* resource_metadata, + const std::string& query_text, + const SearchMetadataPredicate& predicate, + int at_most_num_matches, + MetadataSearchResultVector* results) { + ScopedPriorityQueue<ResultCandidate, + ResultCandidateComparator> result_candidates; + + // Prepare data structure for searching. + base::i18n::FixedPatternStringSearchIgnoringCaseAndAccents query( + base::UTF8ToUTF16(query_text)); + + // Prepare an object to filter out hidden entries. + ResourceEntry mydrive; + FileError error = resource_metadata->GetResourceEntryByPath( + util::GetDriveMyDriveRootPath(), &mydrive); + if (error != FILE_ERROR_OK) + return error; + HiddenEntryClassifier hidden_entry_classifier(resource_metadata, + mydrive.local_id()); + + // Iterate over entries. + scoped_ptr<ResourceMetadata::Iterator> it = resource_metadata->GetIterator(); + for (; !it->IsAtEnd(); it->Advance()) { + FileError error = MaybeAddEntryToResult( + resource_metadata, it.get(), query_text.empty() ? NULL : &query, + predicate, at_most_num_matches, &hidden_entry_classifier, + &result_candidates); + if (error != FILE_ERROR_OK) + return error; + } + + // Prepare the result. + for (; !result_candidates.empty(); result_candidates.pop()) { + const ResultCandidate& candidate = *result_candidates.top(); + // The path field of entries in result_candidates are empty at this point, + // because we don't want to run the expensive metadata DB look up except for + // the final results. Hence, here we fill the part. + base::FilePath path; + error = resource_metadata->GetFilePath(candidate.local_id, &path); + if (error != FILE_ERROR_OK) + return error; + bool is_directory = candidate.entry.file_info().is_directory(); + results->push_back(MetadataSearchResult( + path, is_directory, candidate.highlighted_base_name, + candidate.entry.file_specific_info().md5())); + } + + // Reverse the order here because |result_candidates| puts the most + // uninteresting candidate at the top. + std::reverse(results->begin(), results->end()); + + return FILE_ERROR_OK; +} + +// Runs the SearchMetadataCallback and updates the histogram. +void RunSearchMetadataCallback(const SearchMetadataCallback& callback, + const base::TimeTicks& start_time, + scoped_ptr<MetadataSearchResultVector> results, + FileError error) { + if (error != FILE_ERROR_OK) + results.reset(); + callback.Run(error, results.Pass()); + + UMA_HISTOGRAM_TIMES("Drive.SearchMetadataTime", + base::TimeTicks::Now() - start_time); +} + +} // namespace + +void SearchMetadata( + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner, + ResourceMetadata* resource_metadata, + const std::string& query, + const SearchMetadataPredicate& predicate, + size_t at_most_num_matches, + const SearchMetadataCallback& callback) { + DCHECK(!callback.is_null()); + + const base::TimeTicks start_time = base::TimeTicks::Now(); + + scoped_ptr<MetadataSearchResultVector> results( + new MetadataSearchResultVector); + MetadataSearchResultVector* results_ptr = results.get(); + base::PostTaskAndReplyWithResult( + blocking_task_runner.get(), FROM_HERE, + base::Bind(&SearchMetadataOnBlockingPool, resource_metadata, query, + predicate, at_most_num_matches, results_ptr), + base::Bind(&RunSearchMetadataCallback, callback, start_time, + base::Passed(&results))); +} + +bool MatchesType(int options, const ResourceEntry& entry) { + if ((options & SEARCH_METADATA_EXCLUDE_HOSTED_DOCUMENTS) && + entry.file_specific_info().is_hosted_document()) + return false; + + if ((options & SEARCH_METADATA_EXCLUDE_DIRECTORIES) && + entry.file_info().is_directory()) + return false; + + if (options & SEARCH_METADATA_SHARED_WITH_ME) + return entry.shared_with_me(); + + if (options & SEARCH_METADATA_OFFLINE) { + if (entry.file_specific_info().is_hosted_document()) { + // Not all hosted documents are cached by Drive offline app. + // https://support.google.com/drive/answer/2375012 + std::string mime_type = entry.file_specific_info().content_mime_type(); + return mime_type == drive::util::kGoogleDocumentMimeType || + mime_type == drive::util::kGoogleSpreadsheetMimeType || + mime_type == drive::util::kGooglePresentationMimeType || + mime_type == drive::util::kGoogleDrawingMimeType; + } else { + return entry.file_specific_info().cache_state().is_present(); + } + } + + return true; +} + +bool FindAndHighlight( + const std::string& text, + base::i18n::FixedPatternStringSearchIgnoringCaseAndAccents* query, + std::string* highlighted_text) { + DCHECK(query); + DCHECK(highlighted_text); + highlighted_text->clear(); + + base::string16 text16 = base::UTF8ToUTF16(text); + size_t match_start = 0; + size_t match_length = 0; + if (!query->Search(text16, &match_start, &match_length)) + return false; + + base::string16 pre = text16.substr(0, match_start); + base::string16 match = text16.substr(match_start, match_length); + base::string16 post = text16.substr(match_start + match_length); + highlighted_text->append(net::EscapeForHTML(base::UTF16ToUTF8(pre))); + highlighted_text->append("<b>"); + highlighted_text->append(net::EscapeForHTML(base::UTF16ToUTF8(match))); + highlighted_text->append("</b>"); + highlighted_text->append(net::EscapeForHTML(base::UTF16ToUTF8(post))); + return true; +} + +} // namespace internal +} // namespace drive diff --git a/components/drive/search_metadata.h b/components/drive/search_metadata.h new file mode 100644 index 0000000..ff8d491 --- /dev/null +++ b/components/drive/search_metadata.h @@ -0,0 +1,65 @@ +// 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_SEARCH_METADATA_H_ +#define COMPONENTS_DRIVE_SEARCH_METADATA_H_ + +#include <string> + +#include "components/drive/file_system_interface.h" + +namespace base { +namespace i18n { +class FixedPatternStringSearchIgnoringCaseAndAccents; +} // namespace i18n +} // namespace base + +namespace drive { +namespace internal { + +class ResourceMetadata; + +typedef base::Callback<bool(const ResourceEntry&)> SearchMetadataPredicate; + +// 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. +// |callback| must not be null. Must be called on UI thread. Empty |query| +// matches any base name. i.e. returns everything. |blocking_task_runner| must +// be the same one as |resource_metadata| uses. +void SearchMetadata( + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner, + ResourceMetadata* resource_metadata, + const std::string& query, + const SearchMetadataPredicate& predicate, + size_t at_most_num_matches, + const SearchMetadataCallback& callback); + +// Returns true if |entry| is eligible for the search |options| and should be +// tested for the match with the query. If +// SEARCH_METADATA_EXCLUDE_HOSTED_DOCUMENTS is requested, the hosted documents +// are skipped. If SEARCH_METADATA_EXCLUDE_DIRECTORIES is requested, the +// directories are skipped. If SEARCH_METADATA_SHARED_WITH_ME is requested, only +// the entries with shared-with-me label will be tested. If +// SEARCH_METADATA_OFFLINE is requested, only hosted documents and cached files +// match with the query. This option can not be used with other options. +bool MatchesType(int options, const ResourceEntry& entry); + +// Finds |query| in |text| while ignoring cases or accents. Cases of non-ASCII +// characters are also ignored; they are compared in the 'Primary Level' of +// http://userguide.icu-project.org/collation/concepts. +// Returns true if |query| is found. |highlighted_text| will have the original +// text with matched portions highlighted with <b> tag (only the first match +// is highlighted). Meta characters are escaped like <. The original +// contents of |highlighted_text| will be lost. +bool FindAndHighlight( + const std::string& text, + base::i18n::FixedPatternStringSearchIgnoringCaseAndAccents* query, + std::string* highlighted_text); + +} // namespace internal +} // namespace drive + +#endif // COMPONENTS_DRIVE_SEARCH_METADATA_H_ diff --git a/components/drive/search_metadata_unittest.cc b/components/drive/search_metadata_unittest.cc new file mode 100644 index 0000000..c37ec23 --- /dev/null +++ b/components/drive/search_metadata_unittest.cc @@ -0,0 +1,500 @@ +// 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/search_metadata.h" + +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/i18n/string_search.h" +#include "base/run_loop.h" +#include "base/single_thread_task_runner.h" +#include "base/strings/utf_string_conversions.h" +#include "base/thread_task_runner_handle.h" +#include "components/drive/drive_api_util.h" +#include "components/drive/drive_test_util.h" +#include "components/drive/fake_free_disk_space_getter.h" +#include "components/drive/file_cache.h" +#include "components/drive/file_system_core_util.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace internal { + +namespace { + +const int kDefaultAtMostNumMatches = 10; + +// A simple wrapper for testing FindAndHighlightWrapper(). It just converts the +// query text parameter to FixedPatternStringSearchIgnoringCaseAndAccents. +bool FindAndHighlightWrapper( + const std::string& text, + const std::string& query_text, + std::string* highlighted_text) { + base::i18n::FixedPatternStringSearchIgnoringCaseAndAccents query( + base::UTF8ToUTF16(query_text)); + return FindAndHighlight(text, &query, highlighted_text); +} + +} // namespace + +class SearchMetadataTest : public testing::Test { + protected: + void SetUp() override { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + fake_free_disk_space_getter_.reset(new FakeFreeDiskSpaceGetter); + + metadata_storage_.reset(new ResourceMetadataStorage( + temp_dir_.path(), base::ThreadTaskRunnerHandle::Get().get())); + ASSERT_TRUE(metadata_storage_->Initialize()); + + cache_.reset(new FileCache(metadata_storage_.get(), + temp_dir_.path(), + base::ThreadTaskRunnerHandle::Get().get(), + fake_free_disk_space_getter_.get())); + ASSERT_TRUE(cache_->Initialize()); + + resource_metadata_.reset( + new ResourceMetadata(metadata_storage_.get(), + cache_.get(), + base::ThreadTaskRunnerHandle::Get().get())); + ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->Initialize()); + + AddEntriesToMetadata(); + } + + void AddEntriesToMetadata() { + base::FilePath temp_file; + EXPECT_TRUE(base::CreateTemporaryFileInDir(temp_dir_.path(), &temp_file)); + const std::string temp_file_md5 = "md5"; + + ResourceEntry entry; + std::string local_id; + + // drive/root + EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetIdByPath( + util::GetDriveMyDriveRootPath(), &local_id)); + const std::string root_local_id = local_id; + + // drive/root/Directory 1 + EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(GetDirectoryEntry( + "Directory 1", "dir1", 1, root_local_id), &local_id)); + const std::string dir1_local_id = local_id; + + // drive/root/Directory 1/SubDirectory File 1.txt + EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(GetFileEntry( + "SubDirectory File 1.txt", "file1a", 2, dir1_local_id), &local_id)); + EXPECT_EQ(FILE_ERROR_OK, cache_->Store( + local_id, temp_file_md5, temp_file, FileCache::FILE_OPERATION_COPY)); + + // drive/root/Directory 1/Shared To The Account Owner.txt + entry = GetFileEntry( + "Shared To The Account Owner.txt", "file1b", 3, dir1_local_id); + entry.set_shared_with_me(true); + EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(entry, &local_id)); + + // drive/root/Directory 2 excludeDir-test + EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(GetDirectoryEntry( + "Directory 2 excludeDir-test", "dir2", 4, root_local_id), &local_id)); + + // drive/root/Slash \xE2\x88\x95 in directory + EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry( + GetDirectoryEntry("Slash \xE2\x88\x95 in directory", "dir3", 5, + root_local_id), &local_id)); + const std::string dir3_local_id = local_id; + + // drive/root/Slash \xE2\x88\x95 in directory/Slash SubDir File.txt + EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(GetFileEntry( + "Slash SubDir File.txt", "file3a", 6, dir3_local_id), &local_id)); + + // drive/root/File 2.txt + EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(GetFileEntry( + "File 2.txt", "file2", 7, root_local_id), &local_id)); + EXPECT_EQ(FILE_ERROR_OK, cache_->Store( + local_id, temp_file_md5, temp_file, FileCache::FILE_OPERATION_COPY)); + + // drive/root/Document 1 excludeDir-test + entry = GetFileEntry( + "Document 1 excludeDir-test", "doc1", 8, root_local_id); + entry.mutable_file_specific_info()->set_is_hosted_document(true); + entry.mutable_file_specific_info()->set_document_extension(".gdoc"); + entry.mutable_file_specific_info()->set_content_mime_type( + drive::util::kGoogleDocumentMimeType); + EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(entry, &local_id)); + } + + ResourceEntry GetFileEntry(const std::string& name, + const std::string& resource_id, + int64 last_accessed, + const std::string& parent_local_id) { + ResourceEntry entry; + entry.set_title(name); + entry.set_resource_id(resource_id); + entry.set_parent_local_id(parent_local_id); + entry.mutable_file_info()->set_last_accessed(last_accessed); + return entry; + } + + ResourceEntry GetDirectoryEntry(const std::string& name, + const std::string& resource_id, + int64 last_accessed, + const std::string& parent_local_id) { + ResourceEntry entry; + entry.set_title(name); + entry.set_resource_id(resource_id); + entry.set_parent_local_id(parent_local_id); + entry.mutable_file_info()->set_last_accessed(last_accessed); + entry.mutable_file_info()->set_is_directory(true); + return entry; + } + + content::TestBrowserThreadBundle thread_bundle_; + base::ScopedTempDir temp_dir_; + scoped_ptr<FakeFreeDiskSpaceGetter> fake_free_disk_space_getter_; + scoped_ptr<ResourceMetadataStorage, + test_util::DestroyHelperForTests> metadata_storage_; + scoped_ptr<ResourceMetadata, test_util::DestroyHelperForTests> + resource_metadata_; + scoped_ptr<FileCache, test_util::DestroyHelperForTests> cache_; +}; + +TEST_F(SearchMetadataTest, SearchMetadata_ZeroMatches) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<MetadataSearchResultVector> result; + + SearchMetadata( + base::ThreadTaskRunnerHandle::Get(), resource_metadata_.get(), + "NonExistent", base::Bind(&MatchesType, SEARCH_METADATA_ALL), + kDefaultAtMostNumMatches, + google_apis::test_util::CreateCopyResultCallback(&error, &result)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(result); + ASSERT_EQ(0U, result->size()); +} + +TEST_F(SearchMetadataTest, SearchMetadata_RegularFile) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<MetadataSearchResultVector> result; + + SearchMetadata( + base::ThreadTaskRunnerHandle::Get(), resource_metadata_.get(), + "SubDirectory File 1.txt", base::Bind(&MatchesType, SEARCH_METADATA_ALL), + kDefaultAtMostNumMatches, + google_apis::test_util::CreateCopyResultCallback(&error, &result)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(result); + ASSERT_EQ(1U, result->size()); + EXPECT_EQ("drive/root/Directory 1/SubDirectory File 1.txt", + result->at(0).path.AsUTF8Unsafe()); +} + +// This test checks if |FindAndHighlightWrapper| does case-insensitive search. +// Tricker test cases for |FindAndHighlightWrapper| can be found below. +TEST_F(SearchMetadataTest, SearchMetadata_CaseInsensitiveSearch) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<MetadataSearchResultVector> result; + + // The query is all in lower case. + SearchMetadata( + base::ThreadTaskRunnerHandle::Get(), resource_metadata_.get(), + "subdirectory file 1.txt", base::Bind(&MatchesType, SEARCH_METADATA_ALL), + kDefaultAtMostNumMatches, + google_apis::test_util::CreateCopyResultCallback(&error, &result)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(result); + ASSERT_EQ(1U, result->size()); + EXPECT_EQ("drive/root/Directory 1/SubDirectory File 1.txt", + result->at(0).path.AsUTF8Unsafe()); +} + +TEST_F(SearchMetadataTest, SearchMetadata_RegularFiles) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<MetadataSearchResultVector> result; + + SearchMetadata( + base::ThreadTaskRunnerHandle::Get(), resource_metadata_.get(), "SubDir", + base::Bind(&MatchesType, SEARCH_METADATA_ALL), kDefaultAtMostNumMatches, + google_apis::test_util::CreateCopyResultCallback(&error, &result)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(result); + ASSERT_EQ(2U, result->size()); + + // All base names should contain "File". The results should be sorted by the + // last accessed time in descending order. + EXPECT_EQ("drive/root/Slash \xE2\x88\x95 in directory/Slash SubDir File.txt", + result->at(0).path.AsUTF8Unsafe()); + EXPECT_EQ("drive/root/Directory 1/SubDirectory File 1.txt", + result->at(1).path.AsUTF8Unsafe()); +} + +TEST_F(SearchMetadataTest, SearchMetadata_AtMostOneFile) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<MetadataSearchResultVector> result; + + // There are two files matching "SubDir" but only one file should be + // returned. + SearchMetadata( + base::ThreadTaskRunnerHandle::Get(), resource_metadata_.get(), "SubDir", + base::Bind(&MatchesType, SEARCH_METADATA_ALL), + 1, // at_most_num_matches + google_apis::test_util::CreateCopyResultCallback(&error, &result)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(result); + ASSERT_EQ(1U, result->size()); + EXPECT_EQ("drive/root/Slash \xE2\x88\x95 in directory/Slash SubDir File.txt", + result->at(0).path.AsUTF8Unsafe()); +} + +TEST_F(SearchMetadataTest, SearchMetadata_Directory) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<MetadataSearchResultVector> result; + + SearchMetadata( + base::ThreadTaskRunnerHandle::Get(), resource_metadata_.get(), + "Directory 1", base::Bind(&MatchesType, SEARCH_METADATA_ALL), + kDefaultAtMostNumMatches, + google_apis::test_util::CreateCopyResultCallback(&error, &result)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(result); + ASSERT_EQ(1U, result->size()); + EXPECT_EQ("drive/root/Directory 1", result->at(0).path.AsUTF8Unsafe()); +} + +TEST_F(SearchMetadataTest, SearchMetadata_HostedDocument) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<MetadataSearchResultVector> result; + + SearchMetadata( + base::ThreadTaskRunnerHandle::Get(), resource_metadata_.get(), "Document", + base::Bind(&MatchesType, SEARCH_METADATA_ALL), kDefaultAtMostNumMatches, + google_apis::test_util::CreateCopyResultCallback(&error, &result)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(result); + ASSERT_EQ(1U, result->size()); + + EXPECT_EQ("drive/root/Document 1 excludeDir-test.gdoc", + result->at(0).path.AsUTF8Unsafe()); +} + +TEST_F(SearchMetadataTest, SearchMetadata_ExcludeHostedDocument) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<MetadataSearchResultVector> result; + + SearchMetadata( + base::ThreadTaskRunnerHandle::Get(), resource_metadata_.get(), "Document", + base::Bind(&MatchesType, SEARCH_METADATA_EXCLUDE_HOSTED_DOCUMENTS), + kDefaultAtMostNumMatches, + google_apis::test_util::CreateCopyResultCallback(&error, &result)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(result); + ASSERT_EQ(0U, result->size()); +} + +TEST_F(SearchMetadataTest, SearchMetadata_SharedWithMe) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<MetadataSearchResultVector> result; + + SearchMetadata( + base::ThreadTaskRunnerHandle::Get(), resource_metadata_.get(), "", + base::Bind(&MatchesType, SEARCH_METADATA_SHARED_WITH_ME), + kDefaultAtMostNumMatches, + google_apis::test_util::CreateCopyResultCallback(&error, &result)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(result); + ASSERT_EQ(1U, result->size()); + EXPECT_EQ("drive/root/Directory 1/Shared To The Account Owner.txt", + result->at(0).path.AsUTF8Unsafe()); +} + +TEST_F(SearchMetadataTest, SearchMetadata_FileAndDirectory) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<MetadataSearchResultVector> result; + + SearchMetadata( + base::ThreadTaskRunnerHandle::Get(), resource_metadata_.get(), + "excludeDir-test", base::Bind(&MatchesType, SEARCH_METADATA_ALL), + kDefaultAtMostNumMatches, + google_apis::test_util::CreateCopyResultCallback(&error, &result)); + + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(result); + ASSERT_EQ(2U, result->size()); + + EXPECT_EQ("drive/root/Document 1 excludeDir-test.gdoc", + result->at(0).path.AsUTF8Unsafe()); + EXPECT_EQ("drive/root/Directory 2 excludeDir-test", + result->at(1).path.AsUTF8Unsafe()); +} + +TEST_F(SearchMetadataTest, SearchMetadata_ExcludeDirectory) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<MetadataSearchResultVector> result; + + SearchMetadata( + base::ThreadTaskRunnerHandle::Get(), resource_metadata_.get(), + "excludeDir-test", + base::Bind(&MatchesType, SEARCH_METADATA_EXCLUDE_DIRECTORIES), + kDefaultAtMostNumMatches, + google_apis::test_util::CreateCopyResultCallback(&error, &result)); + + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(result); + ASSERT_EQ(1U, result->size()); + + EXPECT_EQ("drive/root/Document 1 excludeDir-test.gdoc", + result->at(0).path.AsUTF8Unsafe()); +} + +// "drive", "drive/root", "drive/other" should be excluded. +TEST_F(SearchMetadataTest, SearchMetadata_ExcludeSpecialDirectories) { + const char* const kQueries[] = { "drive", "root", "other" }; + for (size_t i = 0; i < arraysize(kQueries); ++i) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<MetadataSearchResultVector> result; + + const std::string query = kQueries[i]; + SearchMetadata( + base::ThreadTaskRunnerHandle::Get(), resource_metadata_.get(), query, + base::Bind(&MatchesType, SEARCH_METADATA_ALL), kDefaultAtMostNumMatches, + google_apis::test_util::CreateCopyResultCallback(&error, &result)); + + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(result); + ASSERT_TRUE(result->empty()) << ": " << query << " should not match"; + } +} + +TEST_F(SearchMetadataTest, SearchMetadata_Offline) { + FileError error = FILE_ERROR_FAILED; + scoped_ptr<MetadataSearchResultVector> result; + + SearchMetadata( + base::ThreadTaskRunnerHandle::Get(), resource_metadata_.get(), "", + base::Bind(&MatchesType, SEARCH_METADATA_OFFLINE), + kDefaultAtMostNumMatches, + google_apis::test_util::CreateCopyResultCallback(&error, &result)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_EQ(3U, result->size()); + + // This is not included in the cache but is a hosted document. + EXPECT_EQ("drive/root/Document 1 excludeDir-test.gdoc", + result->at(0).path.AsUTF8Unsafe()); + + EXPECT_EQ("drive/root/File 2.txt", + result->at(1).path.AsUTF8Unsafe()); + EXPECT_EQ("drive/root/Directory 1/SubDirectory File 1.txt", + result->at(2).path.AsUTF8Unsafe()); +} + +TEST(SearchMetadataSimpleTest, FindAndHighlight_ZeroMatches) { + std::string highlighted_text; + EXPECT_FALSE(FindAndHighlightWrapper("text", "query", &highlighted_text)); +} + +TEST(SearchMetadataSimpleTest, FindAndHighlight_EmptyText) { + std::string highlighted_text; + EXPECT_FALSE(FindAndHighlightWrapper("", "query", &highlighted_text)); +} + +TEST(SearchMetadataSimpleTest, FindAndHighlight_FullMatch) { + std::string highlighted_text; + EXPECT_TRUE(FindAndHighlightWrapper("hello", "hello", &highlighted_text)); + EXPECT_EQ("<b>hello</b>", highlighted_text); +} + +TEST(SearchMetadataSimpleTest, FindAndHighlight_StartWith) { + std::string highlighted_text; + EXPECT_TRUE(FindAndHighlightWrapper("hello, world", "hello", + &highlighted_text)); + EXPECT_EQ("<b>hello</b>, world", highlighted_text); +} + +TEST(SearchMetadataSimpleTest, FindAndHighlight_EndWith) { + std::string highlighted_text; + EXPECT_TRUE(FindAndHighlightWrapper("hello, world", "world", + &highlighted_text)); + EXPECT_EQ("hello, <b>world</b>", highlighted_text); +} + +TEST(SearchMetadataSimpleTest, FindAndHighlight_InTheMiddle) { + std::string highlighted_text; + EXPECT_TRUE(FindAndHighlightWrapper("yo hello, world", "hello", + &highlighted_text)); + EXPECT_EQ("yo <b>hello</b>, world", highlighted_text); +} + +TEST(SearchMetadataSimpleTest, FindAndHighlight_MultipeMatches) { + std::string highlighted_text; + EXPECT_TRUE(FindAndHighlightWrapper("yoyoyoyoy", "yoy", &highlighted_text)); + // Only the first match is highlighted. + EXPECT_EQ("<b>yoy</b>oyoyoy", highlighted_text); +} + +TEST(SearchMetadataSimpleTest, FindAndHighlight_IgnoreCase) { + std::string highlighted_text; + EXPECT_TRUE(FindAndHighlightWrapper("HeLLo", "hello", &highlighted_text)); + EXPECT_EQ("<b>HeLLo</b>", highlighted_text); +} + +TEST(SearchMetadataSimpleTest, FindAndHighlight_IgnoreCaseNonASCII) { + std::string highlighted_text; + + // Case and accent ignorance in Greek. Find "socra" in "Socra'tes". + EXPECT_TRUE(FindAndHighlightWrapper( + "\xCE\xA3\xCF\x89\xCE\xBA\xCF\x81\xCE\xAC\xCF\x84\xCE\xB7\xCF\x82", + "\xCF\x83\xCF\x89\xCE\xBA\xCF\x81\xCE\xB1", &highlighted_text)); + EXPECT_EQ( + "<b>\xCE\xA3\xCF\x89\xCE\xBA\xCF\x81\xCE\xAC</b>\xCF\x84\xCE\xB7\xCF\x82", + highlighted_text); + + // In Japanese characters. + // Find Hiragana "pi" + "(small)ya" in Katakana "hi" + semi-voiced-mark + "ya" + EXPECT_TRUE(FindAndHighlightWrapper( + "\xE3\x81\xB2\xE3\x82\x9A\xE3\x82\x83\xE3\x83\xBC", + "\xE3\x83\x94\xE3\x83\xA4", + &highlighted_text)); + EXPECT_EQ( + "<b>\xE3\x81\xB2\xE3\x82\x9A\xE3\x82\x83</b>\xE3\x83\xBC", + highlighted_text); +} + +TEST(SearchMetadataSimpleTest, MultiTextBySingleQuery) { + base::i18n::FixedPatternStringSearchIgnoringCaseAndAccents query( + base::UTF8ToUTF16("hello")); + + std::string highlighted_text; + EXPECT_TRUE(FindAndHighlight("hello", &query, &highlighted_text)); + EXPECT_EQ("<b>hello</b>", highlighted_text); + EXPECT_FALSE(FindAndHighlight("goodbye", &query, &highlighted_text)); + EXPECT_TRUE(FindAndHighlight("1hello2", &query, &highlighted_text)); + EXPECT_EQ("1<b>hello</b>2", highlighted_text); +} + +TEST(SearchMetadataSimpleTest, FindAndHighlight_MetaChars) { + std::string highlighted_text; + EXPECT_TRUE(FindAndHighlightWrapper("<hello>", "hello", &highlighted_text)); + EXPECT_EQ("<<b>hello</b>>", highlighted_text); +} + +TEST(SearchMetadataSimpleTest, FindAndHighlight_MoreMetaChars) { + std::string highlighted_text; + EXPECT_TRUE(FindAndHighlightWrapper("a&b&c&d", "b&c", &highlighted_text)); + EXPECT_EQ("a&<b>b&c</b>&d", highlighted_text); +} + +} // namespace internal +} // namespace drive diff --git a/components/drive/sync/entry_revert_performer.cc b/components/drive/sync/entry_revert_performer.cc new file mode 100644 index 0000000..7b99737 --- /dev/null +++ b/components/drive/sync/entry_revert_performer.cc @@ -0,0 +1,178 @@ +// 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/sync/entry_revert_performer.h" + +#include "components/drive/change_list_processor.h" +#include "components/drive/drive.pb.h" +#include "components/drive/drive_api_util.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_entry_conversion.h" +#include "components/drive/resource_metadata.h" +#include "google_apis/drive/drive_api_parser.h" + +namespace drive { +namespace internal { +namespace { + +FileError FinishRevert(ResourceMetadata* metadata, + const std::string& local_id, + google_apis::DriveApiErrorCode status, + scoped_ptr<google_apis::FileResource> file_resource, + FileChange* changed_files) { + ResourceEntry entry; + std::string parent_resource_id; + FileError error = GDataToFileError(status); + switch (error) { + case FILE_ERROR_OK: + if (!ConvertFileResourceToResourceEntry(*file_resource, &entry, + &parent_resource_id)) + return FILE_ERROR_NOT_A_FILE; + break; + + case FILE_ERROR_NOT_FOUND: + entry.set_deleted(true); + break; + + default: + return error; + } + + base::FilePath original_path; + error = metadata->GetFilePath(local_id, &original_path); + if (error != FILE_ERROR_OK) + return error; + + if (entry.deleted()) { + error = metadata->RemoveEntry(local_id); + if (error != FILE_ERROR_OK) + return error; + + changed_files->Update( + original_path, + FileChange::FILE_TYPE_NO_INFO, // Undetermined type for deleted file. + FileChange::CHANGE_TYPE_DELETE); + } else { + changed_files->Update(original_path, entry, FileChange::CHANGE_TYPE_DELETE); + + error = ChangeListProcessor::SetParentLocalIdOfEntry(metadata, &entry, + parent_resource_id); + if (error != FILE_ERROR_OK) + return error; + + entry.set_local_id(local_id); + error = metadata->RefreshEntry(entry); + if (error != FILE_ERROR_OK) + return error; + + base::FilePath new_path; + error = metadata->GetFilePath(local_id, &new_path); + if (error != FILE_ERROR_OK) + return error; + + changed_files->Update(new_path, entry, + FileChange::CHANGE_TYPE_ADD_OR_UPDATE); + } + return FILE_ERROR_OK; +} + +} // namespace + +EntryRevertPerformer::EntryRevertPerformer( + base::SequencedTaskRunner* blocking_task_runner, + file_system::OperationDelegate* delegate, + JobScheduler* scheduler, + ResourceMetadata* metadata) + : blocking_task_runner_(blocking_task_runner), + delegate_(delegate), + scheduler_(scheduler), + metadata_(metadata), + weak_ptr_factory_(this) { +} + +EntryRevertPerformer::~EntryRevertPerformer() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +void EntryRevertPerformer::RevertEntry(const std::string& local_id, + const ClientContext& context, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + scoped_ptr<ResourceEntry> entry(new ResourceEntry); + ResourceEntry* entry_ptr = entry.get(); + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&ResourceMetadata::GetResourceEntryById, + base::Unretained(metadata_), local_id, entry_ptr), + base::Bind(&EntryRevertPerformer::RevertEntryAfterPrepare, + weak_ptr_factory_.GetWeakPtr(), context, callback, + base::Passed(&entry))); +} + +void EntryRevertPerformer::RevertEntryAfterPrepare( + const ClientContext& context, + const FileOperationCallback& callback, + scoped_ptr<ResourceEntry> entry, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error == FILE_ERROR_OK && entry->resource_id().empty()) + error = FILE_ERROR_INVALID_OPERATION; + + if (error != FILE_ERROR_OK) { + callback.Run(error); + return; + } + + scheduler_->GetFileResource( + entry->resource_id(), + context, + base::Bind(&EntryRevertPerformer::RevertEntryAfterGetFileResource, + weak_ptr_factory_.GetWeakPtr(), callback, entry->local_id())); +} + +void EntryRevertPerformer::RevertEntryAfterGetFileResource( + const FileOperationCallback& callback, + const std::string& local_id, + google_apis::DriveApiErrorCode status, + scoped_ptr<google_apis::FileResource> entry) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FileChange* changed_files = new FileChange; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&FinishRevert, + metadata_, + local_id, + status, + base::Passed(&entry), + changed_files), + base::Bind(&EntryRevertPerformer::RevertEntryAfterFinishRevert, + weak_ptr_factory_.GetWeakPtr(), + callback, + base::Owned(changed_files))); +} + +void EntryRevertPerformer::RevertEntryAfterFinishRevert( + const FileOperationCallback& callback, + const FileChange* changed_files, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + delegate_->OnFileChangedByOperation(*changed_files); + + callback.Run(error); +} + +} // namespace internal +} // namespace drive diff --git a/components/drive/sync/entry_revert_performer.h b/components/drive/sync/entry_revert_performer.h new file mode 100644 index 0000000..003c601 --- /dev/null +++ b/components/drive/sync/entry_revert_performer.h @@ -0,0 +1,93 @@ +// 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_SYNC_ENTRY_REVERT_PERFORMER_H_ +#define COMPONENTS_DRIVE_SYNC_ENTRY_REVERT_PERFORMER_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 SequencedTaskRunner; +} // namespace base + +namespace google_apis { +class FileResource; +} // namespace google_apis + +namespace drive { + +class FileChange; +class JobScheduler; +class ResourceEntry; +struct ClientContext; + +namespace file_system { +class OperationDelegate; +} // namespace file_system + +namespace internal { + +class ResourceMetadata; + +// This class is responsible to revert local changes of an entry. +class EntryRevertPerformer { + public: + EntryRevertPerformer(base::SequencedTaskRunner* blocking_task_runner, + file_system::OperationDelegate* delegate, + JobScheduler* scheduler, + ResourceMetadata* metadata); + ~EntryRevertPerformer(); + + // Requests the server for metadata of the entry specified by |local_id| + // and overwrites the locally stored entry with it. + // Invokes |callback| when finished with the result of the operation. + // |callback| must not be null. + void RevertEntry(const std::string& local_id, + const ClientContext& context, + const FileOperationCallback& callback); + + private: + // Part of RevertEntry(). Called after local metadata look up. + void RevertEntryAfterPrepare(const ClientContext& context, + const FileOperationCallback& callback, + scoped_ptr<ResourceEntry> entry, + FileError error); + + // Part of RevertEntry(). Called after GetFileResource is completed. + void RevertEntryAfterGetFileResource( + const FileOperationCallback& callback, + const std::string& local_id, + google_apis::DriveApiErrorCode status, + scoped_ptr<google_apis::FileResource> entry); + + // Part of RevertEntry(). Called after local metadata is updated. + void RevertEntryAfterFinishRevert(const FileOperationCallback& callback, + const FileChange* changed_files, + FileError error); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + file_system::OperationDelegate* delegate_; + JobScheduler* scheduler_; + 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<EntryRevertPerformer> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(EntryRevertPerformer); +}; + +} // namespace internal +} // namespace drive + +#endif // COMPONENTS_DRIVE_SYNC_ENTRY_REVERT_PERFORMER_H_ diff --git a/components/drive/sync/entry_revert_performer_unittest.cc b/components/drive/sync/entry_revert_performer_unittest.cc new file mode 100644 index 0000000..421749a --- /dev/null +++ b/components/drive/sync/entry_revert_performer_unittest.cc @@ -0,0 +1,151 @@ +// 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/sync/entry_revert_performer.h" + +#include "base/task_runner_util.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/resource_metadata.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 internal { + +class EntryRevertPerformerTest : public file_system::OperationTestBase { + protected: + void SetUp() override { + OperationTestBase::SetUp(); + performer_.reset(new EntryRevertPerformer(blocking_task_runner(), + delegate(), + scheduler(), + metadata())); + } + + scoped_ptr<EntryRevertPerformer> performer_; +}; + +TEST_F(EntryRevertPerformerTest, RevertEntry) { + base::FilePath path( + FILE_PATH_LITERAL("drive/root/Directory 1/SubDirectory File 1.txt")); + base::FilePath updated_path(FILE_PATH_LITERAL( + "drive/root/Directory 1/UpdatedSubDirectory File 1.txt")); + + ResourceEntry src_entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(path, &src_entry)); + + // Update local entry. + ResourceEntry updated_entry = src_entry; + updated_entry.set_title("Updated" + src_entry.title()); + updated_entry.set_metadata_edit_state(ResourceEntry::DIRTY); + + FileError error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&ResourceMetadata::RefreshEntry, + base::Unretained(metadata()), updated_entry), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Revert local change. + error = FILE_ERROR_FAILED; + performer_->RevertEntry( + src_entry.local_id(), + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Verify local change is reverted. + ResourceEntry result_entry; + EXPECT_EQ(FILE_ERROR_OK, + GetLocalResourceEntryById(src_entry.local_id(), &result_entry)); + EXPECT_EQ(src_entry.title(), result_entry.title()); + EXPECT_EQ(ResourceEntry::CLEAN, result_entry.metadata_edit_state()); + + EXPECT_EQ(2U, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(path)); + EXPECT_TRUE(delegate()->get_changed_files().count(updated_path)); +} + +TEST_F(EntryRevertPerformerTest, RevertEntry_NotFoundOnServer) { + ResourceEntry my_drive; + EXPECT_EQ(FILE_ERROR_OK, + GetLocalResourceEntry(util::GetDriveMyDriveRootPath(), &my_drive)); + + // Add a new entry with a nonexistent resource ID. + ResourceEntry entry; + entry.set_title("New File.txt"); + entry.set_resource_id("non-existent-resource-id"); + entry.set_parent_local_id(my_drive.local_id()); + + FileError error = FILE_ERROR_FAILED; + std::string local_id; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&ResourceMetadata::AddEntry, + base::Unretained(metadata()), entry, &local_id), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Revert local change. The added entry should be removed. + error = FILE_ERROR_FAILED; + performer_->RevertEntry( + local_id, + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Verify the entry was deleted locally. + EXPECT_EQ(FILE_ERROR_NOT_FOUND, GetLocalResourceEntryById(local_id, &entry)); + + EXPECT_EQ(1U, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().CountDirectory( + util::GetDriveMyDriveRootPath())); +} + +TEST_F(EntryRevertPerformerTest, RevertEntry_TrashedOnServer) { + base::FilePath path( + FILE_PATH_LITERAL("drive/root/Directory 1/SubDirectory File 1.txt")); + + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(path, &entry)); + + // Trash the entry on the server. + google_apis::DriveApiErrorCode gdata_error = google_apis::DRIVE_OTHER_ERROR; + fake_service()->TrashResource( + entry.resource_id(), + google_apis::test_util::CreateCopyResultCallback(&gdata_error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, gdata_error); + + // Revert local change. The entry should be removed. + FileError error = FILE_ERROR_FAILED; + performer_->RevertEntry( + entry.local_id(), + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Verify the entry was deleted locally. + EXPECT_EQ(FILE_ERROR_NOT_FOUND, + GetLocalResourceEntryById(entry.local_id(), &entry)); + + EXPECT_EQ(1U, delegate()->get_changed_files().size()); + EXPECT_TRUE(delegate()->get_changed_files().count(path)); +} + +} // namespace internal +} // namespace drive diff --git a/components/drive/sync/entry_update_performer.cc b/components/drive/sync/entry_update_performer.cc new file mode 100644 index 0000000..5418623 --- /dev/null +++ b/components/drive/sync/entry_update_performer.cc @@ -0,0 +1,450 @@ +// 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/sync/entry_update_performer.h" + +#include <set> + +#include "base/callback_helpers.h" +#include "base/files/file_util.h" +#include "components/drive/change_list_loader.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" +#include "components/drive/sync/entry_revert_performer.h" +#include "components/drive/sync/remove_performer.h" +#include "google_apis/drive/drive_api_parser.h" + +namespace drive { +namespace internal { + +struct EntryUpdatePerformer::LocalState { + LocalState() : cache_file_size(0), should_content_update(false) {} + + ResourceEntry entry; + ResourceEntry parent_entry; + base::FilePath drive_file_path; + base::FilePath cache_file_path; + int64 cache_file_size; + bool should_content_update; +}; + +namespace { + +struct PropertyCompare { + bool operator()(const drive::Property& x, const drive::Property& y) const { + if (x.key() < y.key()) + return true; + if (x.key() > y.key()) + return false; + if (x.value() < y.value()) + return true; + if (y.value() > y.value()) + return false; + return x.visibility() < y.visibility(); + } +}; + +// Looks up ResourceEntry for source entry and its parent. +FileError PrepareUpdate(ResourceMetadata* metadata, + FileCache* cache, + const std::string& local_id, + EntryUpdatePerformer::LocalState* local_state) { + FileError error = metadata->GetResourceEntryById(local_id, + &local_state->entry); + if (error != FILE_ERROR_OK) + return error; + + error = metadata->GetResourceEntryById(local_state->entry.parent_local_id(), + &local_state->parent_entry); + if (error != FILE_ERROR_OK) + return error; + + error = metadata->GetFilePath(local_id, &local_state->drive_file_path); + if (error != FILE_ERROR_OK) + return error; + + if (!local_state->entry.file_info().is_directory() && + !local_state->entry.file_specific_info().cache_state().is_present() && + local_state->entry.resource_id().empty()) { + // Locally created file with no cache file, store an empty file. + base::FilePath empty_file; + if (!base::CreateTemporaryFile(&empty_file)) + return FILE_ERROR_FAILED; + error = cache->Store(local_id, std::string(), empty_file, + FileCache::FILE_OPERATION_MOVE); + if (error != FILE_ERROR_OK) + return error; + error = metadata->GetResourceEntryById(local_id, &local_state->entry); + if (error != FILE_ERROR_OK) + return error; + } + + // Check if content update is needed or not. + if (local_state->entry.file_specific_info().cache_state().is_dirty() && + !cache->IsOpenedForWrite(local_id)) { + // Update cache entry's MD5 if needed. + if (local_state->entry.file_specific_info().cache_state().md5().empty()) { + error = cache->UpdateMd5(local_id); + if (error != FILE_ERROR_OK) + return error; + error = metadata->GetResourceEntryById(local_id, &local_state->entry); + if (error != FILE_ERROR_OK) + return error; + } + + if (local_state->entry.file_specific_info().cache_state().md5() == + local_state->entry.file_specific_info().md5()) { + error = cache->ClearDirty(local_id); + if (error != FILE_ERROR_OK) + return error; + } else { + error = cache->GetFile(local_id, &local_state->cache_file_path); + if (error != FILE_ERROR_OK) + return error; + const bool result = base::GetFileSize(local_state->cache_file_path, + &local_state->cache_file_size); + if (!result) + return FILE_ERROR_FAILED; + local_state->should_content_update = true; + } + } + + // Update metadata_edit_state. + switch (local_state->entry.metadata_edit_state()) { + case ResourceEntry::CLEAN: // Nothing to do. + case ResourceEntry::SYNCING: // Error during the last update. Go ahead. + break; + + case ResourceEntry::DIRTY: + local_state->entry.set_metadata_edit_state(ResourceEntry::SYNCING); + error = metadata->RefreshEntry(local_state->entry); + if (error != FILE_ERROR_OK) + return error; + break; + } + return FILE_ERROR_OK; +} + +FileError FinishUpdate(ResourceMetadata* metadata, + FileCache* cache, + scoped_ptr<EntryUpdatePerformer::LocalState> local_state, + scoped_ptr<google_apis::FileResource> file_resource, + FileChange* changed_files) { + ResourceEntry entry; + FileError error = + metadata->GetResourceEntryById(local_state->entry.local_id(), &entry); + if (error != FILE_ERROR_OK) + return error; + + // When creating new entries, update check may add a new entry with the same + // resource ID before us. If such an entry exists, remove it. + std::string existing_local_id; + error = + metadata->GetIdByResourceId(file_resource->file_id(), &existing_local_id); + + switch (error) { + case FILE_ERROR_OK: + if (existing_local_id != local_state->entry.local_id()) { + base::FilePath existing_entry_path; + error = metadata->GetFilePath(existing_local_id, &existing_entry_path); + if (error != FILE_ERROR_OK) + return error; + error = metadata->RemoveEntry(existing_local_id); + if (error != FILE_ERROR_OK) + return error; + changed_files->Update(existing_entry_path, entry, + FileChange::CHANGE_TYPE_DELETE); + } + break; + case FILE_ERROR_NOT_FOUND: + break; + default: + return error; + } + + // Update metadata_edit_state and MD5. + switch (entry.metadata_edit_state()) { + case ResourceEntry::CLEAN: // Nothing to do. + case ResourceEntry::DIRTY: // Entry was edited again during the update. + break; + + case ResourceEntry::SYNCING: + entry.set_metadata_edit_state(ResourceEntry::CLEAN); + break; + } + if (!entry.file_info().is_directory()) + entry.mutable_file_specific_info()->set_md5(file_resource->md5_checksum()); + entry.set_resource_id(file_resource->file_id()); + + // Keep only those properties which have been added or changed in the proto + // during the update. + std::set<drive::Property, PropertyCompare> synced_properties( + local_state->entry.new_properties().begin(), + local_state->entry.new_properties().end()); + + google::protobuf::RepeatedPtrField<drive::Property> not_synced_properties; + for (const auto& property : entry.new_properties()) { + if (!synced_properties.count(property)) { + Property* const not_synced_property = not_synced_properties.Add(); + not_synced_property->CopyFrom(property); + } + } + entry.mutable_new_properties()->Swap(¬_synced_properties); + + error = metadata->RefreshEntry(entry); + if (error != FILE_ERROR_OK) + return error; + base::FilePath entry_path; + error = metadata->GetFilePath(local_state->entry.local_id(), &entry_path); + if (error != FILE_ERROR_OK) + return error; + changed_files->Update(entry_path, entry, + FileChange::CHANGE_TYPE_ADD_OR_UPDATE); + + // Clear dirty bit unless the file has been edited during update. + if (entry.file_specific_info().cache_state().is_dirty() && + entry.file_specific_info().cache_state().md5() == + entry.file_specific_info().md5()) { + error = cache->ClearDirty(local_state->entry.local_id()); + if (error != FILE_ERROR_OK) + return error; + } + return FILE_ERROR_OK; +} + +} // namespace + +EntryUpdatePerformer::EntryUpdatePerformer( + base::SequencedTaskRunner* blocking_task_runner, + file_system::OperationDelegate* delegate, + JobScheduler* scheduler, + ResourceMetadata* metadata, + FileCache* cache, + LoaderController* loader_controller) + : blocking_task_runner_(blocking_task_runner), + delegate_(delegate), + scheduler_(scheduler), + metadata_(metadata), + cache_(cache), + loader_controller_(loader_controller), + remove_performer_(new RemovePerformer(blocking_task_runner, + delegate, + scheduler, + metadata)), + entry_revert_performer_(new EntryRevertPerformer(blocking_task_runner, + delegate, + scheduler, + metadata)), + weak_ptr_factory_(this) { +} + +EntryUpdatePerformer::~EntryUpdatePerformer() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +void EntryUpdatePerformer::UpdateEntry(const std::string& local_id, + const ClientContext& context, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + scoped_ptr<LocalState> local_state(new LocalState); + LocalState* const local_state_ptr = local_state.get(); + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&PrepareUpdate, metadata_, cache_, local_id, local_state_ptr), + base::Bind(&EntryUpdatePerformer::UpdateEntryAfterPrepare, + weak_ptr_factory_.GetWeakPtr(), context, callback, + base::Passed(&local_state))); +} + +void EntryUpdatePerformer::UpdateEntryAfterPrepare( + const ClientContext& context, + const FileOperationCallback& callback, + scoped_ptr<LocalState> local_state, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error != FILE_ERROR_OK) { + callback.Run(error); + return; + } + + // Trashed entry should be removed. + if (local_state->entry.parent_local_id() == util::kDriveTrashDirLocalId) { + remove_performer_->Remove(local_state->entry.local_id(), context, callback); + return; + } + + // Parent was locally created and needs update. Just return for now. + // This entry should be updated again after the parent update completes. + if (local_state->parent_entry.resource_id().empty() && + local_state->parent_entry.metadata_edit_state() != ResourceEntry::CLEAN) { + callback.Run(FILE_ERROR_OK); + return; + } + + base::Time last_modified = base::Time::FromInternalValue( + local_state->entry.file_info().last_modified()); + base::Time last_accessed = base::Time::FromInternalValue( + local_state->entry.file_info().last_accessed()); + + // Compose a list of new properties from the proto. + google_apis::drive::Properties properties; + for (const auto& proto_property : local_state->entry.new_properties()) { + google_apis::drive::Property property; + switch (proto_property.visibility()) { + case Property_Visibility_PRIVATE: + property.set_visibility( + google_apis::drive::Property::VISIBILITY_PRIVATE); + break; + case Property_Visibility_PUBLIC: + property.set_visibility( + google_apis::drive::Property::VISIBILITY_PUBLIC); + break; + } + property.set_key(proto_property.key()); + property.set_value(proto_property.value()); + properties.push_back(property); + } + + // Perform content update. + if (local_state->should_content_update) { + if (local_state->entry.resource_id().empty()) { + // Not locking the loader intentionally here to avoid making the UI + // unresponsive while uploading large files. + // FinishUpdate() is responsible to resolve conflicts caused by this. + scoped_ptr<base::ScopedClosureRunner> null_loader_lock; + + UploadNewFileOptions options; + options.modified_date = last_modified; + options.last_viewed_by_me_date = last_accessed; + options.properties = properties; + LocalState* const local_state_ptr = local_state.get(); + scheduler_->UploadNewFile( + local_state_ptr->parent_entry.resource_id(), + local_state_ptr->cache_file_size, local_state_ptr->drive_file_path, + local_state_ptr->cache_file_path, local_state_ptr->entry.title(), + local_state_ptr->entry.file_specific_info().content_mime_type(), + options, context, + base::Bind(&EntryUpdatePerformer::UpdateEntryAfterUpdateResource, + weak_ptr_factory_.GetWeakPtr(), context, callback, + base::Passed(&local_state), + base::Passed(&null_loader_lock))); + } else { + UploadExistingFileOptions options; + options.title = local_state->entry.title(); + options.parent_resource_id = local_state->parent_entry.resource_id(); + options.modified_date = last_modified; + options.last_viewed_by_me_date = last_accessed; + options.properties = properties; + LocalState* const local_state_ptr = local_state.get(); + scheduler_->UploadExistingFile( + local_state_ptr->entry.resource_id(), + local_state_ptr->cache_file_size, local_state_ptr->drive_file_path, + local_state_ptr->cache_file_path, + local_state_ptr->entry.file_specific_info().content_mime_type(), + options, context, + base::Bind(&EntryUpdatePerformer::UpdateEntryAfterUpdateResource, + weak_ptr_factory_.GetWeakPtr(), context, callback, + base::Passed(&local_state), + base::Passed(scoped_ptr<base::ScopedClosureRunner>()))); + } + return; + } + + // Create directory. + if (local_state->entry.file_info().is_directory() && + local_state->entry.resource_id().empty()) { + // Lock the loader to avoid race conditions. + scoped_ptr<base::ScopedClosureRunner> loader_lock = + loader_controller_->GetLock(); + + AddNewDirectoryOptions options; + options.modified_date = last_modified; + options.last_viewed_by_me_date = last_accessed; + options.properties = properties; + LocalState* const local_state_ptr = local_state.get(); + scheduler_->AddNewDirectory( + local_state_ptr->parent_entry.resource_id(), + local_state_ptr->entry.title(), options, context, + base::Bind(&EntryUpdatePerformer::UpdateEntryAfterUpdateResource, + weak_ptr_factory_.GetWeakPtr(), context, callback, + base::Passed(&local_state), base::Passed(&loader_lock))); + return; + } + + // No need to perform update. + if (local_state->entry.metadata_edit_state() == ResourceEntry::CLEAN || + local_state->entry.resource_id().empty()) { + callback.Run(FILE_ERROR_OK); + return; + } + + // Perform metadata update. + LocalState* const local_state_ptr = local_state.get(); + scheduler_->UpdateResource( + local_state_ptr->entry.resource_id(), + local_state_ptr->parent_entry.resource_id(), + local_state_ptr->entry.title(), last_modified, last_accessed, properties, + context, + base::Bind(&EntryUpdatePerformer::UpdateEntryAfterUpdateResource, + weak_ptr_factory_.GetWeakPtr(), context, callback, + base::Passed(&local_state), + base::Passed(scoped_ptr<base::ScopedClosureRunner>()))); +} + +void EntryUpdatePerformer::UpdateEntryAfterUpdateResource( + const ClientContext& context, + const FileOperationCallback& callback, + scoped_ptr<LocalState> local_state, + scoped_ptr<base::ScopedClosureRunner> loader_lock, + google_apis::DriveApiErrorCode status, + scoped_ptr<google_apis::FileResource> entry) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (status == google_apis::HTTP_FORBIDDEN) { + // Editing this entry is not allowed, revert local changes. + entry_revert_performer_->RevertEntry(local_state->entry.local_id(), context, + callback); + return; + } + + FileError error = GDataToFileError(status); + if (error != FILE_ERROR_OK) { + callback.Run(error); + return; + } + + FileChange* changed_files = new FileChange; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), FROM_HERE, + base::Bind(&FinishUpdate, metadata_, cache_, base::Passed(&local_state), + base::Passed(&entry), changed_files), + base::Bind(&EntryUpdatePerformer::UpdateEntryAfterFinish, + weak_ptr_factory_.GetWeakPtr(), callback, + base::Owned(changed_files))); +} + +void EntryUpdatePerformer::UpdateEntryAfterFinish( + const FileOperationCallback& callback, + const FileChange* changed_files, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + delegate_->OnFileChangedByOperation(*changed_files); + callback.Run(error); +} + +} // namespace internal +} // namespace drive diff --git a/components/drive/sync/entry_update_performer.h b/components/drive/sync/entry_update_performer.h new file mode 100644 index 0000000..55fad7e --- /dev/null +++ b/components/drive/sync/entry_update_performer.h @@ -0,0 +1,106 @@ +// 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_SYNC_ENTRY_UPDATE_PERFORMER_H_ +#define COMPONENTS_DRIVE_SYNC_ENTRY_UPDATE_PERFORMER_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 ScopedClosureRunner; +class SequencedTaskRunner; +} // namespace base + +namespace google_apis { +class FileResource; +} // namespace google_apis + +namespace drive { + +class FileChange; +class JobScheduler; +class ResourceEntry; +struct ClientContext; + +namespace file_system { +class OperationDelegate; +} // namespace file_system + +namespace internal { + +class EntryRevertPerformer; +class FileCache; +class LoaderController; +class RemovePerformer; +class ResourceMetadata; + +// This class is responsible to perform server side update of an entry. +class EntryUpdatePerformer { + public: + EntryUpdatePerformer(base::SequencedTaskRunner* blocking_task_runner, + file_system::OperationDelegate* delegate, + JobScheduler* scheduler, + ResourceMetadata* metadata, + FileCache* cache, + LoaderController* loader_controller); + ~EntryUpdatePerformer(); + + // Requests the server to update the metadata of the entry specified by + // |local_id| with the locally stored one. + // Invokes |callback| when finished with the result of the operation. + // |callback| must not be null. + void UpdateEntry(const std::string& local_id, + const ClientContext& context, + const FileOperationCallback& callback); + + struct LocalState; + + private: + // Part of UpdateEntry(). Called after local metadata look up. + void UpdateEntryAfterPrepare(const ClientContext& context, + const FileOperationCallback& callback, + scoped_ptr<LocalState> local_state, + FileError error); + + // Part of UpdateEntry(). Called after UpdateResource is completed. + void UpdateEntryAfterUpdateResource( + const ClientContext& context, + const FileOperationCallback& callback, + scoped_ptr<LocalState> local_state, + scoped_ptr<base::ScopedClosureRunner> loader_lock, + google_apis::DriveApiErrorCode status, + scoped_ptr<google_apis::FileResource> entry); + + // Part of UpdateEntry(). Called after FinishUpdate is completed. + void UpdateEntryAfterFinish(const FileOperationCallback& callback, + const FileChange* changed_files, + FileError error); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + file_system::OperationDelegate* delegate_; + JobScheduler* scheduler_; + ResourceMetadata* metadata_; + FileCache* cache_; + LoaderController* loader_controller_; + scoped_ptr<RemovePerformer> remove_performer_; + scoped_ptr<EntryRevertPerformer> entry_revert_performer_; + + 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<EntryUpdatePerformer> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(EntryUpdatePerformer); +}; + +} // namespace internal +} // namespace drive + +#endif // COMPONENTS_DRIVE_SYNC_ENTRY_UPDATE_PERFORMER_H_ diff --git a/components/drive/sync/entry_update_performer_unittest.cc b/components/drive/sync/entry_update_performer_unittest.cc new file mode 100644 index 0000000..145e1ff --- /dev/null +++ b/components/drive/sync/entry_update_performer_unittest.cc @@ -0,0 +1,658 @@ +// 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/sync/entry_update_performer.h" + +#include "base/callback_helpers.h" +#include "base/files/file_util.h" +#include "base/md5.h" +#include "base/task_runner_util.h" +#include "components/drive/drive_api_util.h" +#include "components/drive/file_cache.h" +#include "components/drive/file_system/download_operation.h" +#include "components/drive/file_system/operation_test_base.h" +#include "components/drive/job_scheduler.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 internal { + +class EntryUpdatePerformerTest : public file_system::OperationTestBase { + protected: + void SetUp() override { + OperationTestBase::SetUp(); + performer_.reset(new EntryUpdatePerformer(blocking_task_runner(), + delegate(), + scheduler(), + metadata(), + cache(), + loader_controller())); + } + + // Stores |content| to the cache and mark it as dirty. + FileError StoreAndMarkDirty(const std::string& local_id, + const std::string& content) { + base::FilePath path; + if (!base::CreateTemporaryFileInDir(temp_dir(), &path) || + !google_apis::test_util::WriteStringToFile(path, content)) + return FILE_ERROR_FAILED; + + // Store the file to cache. + FileError error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&FileCache::Store, + base::Unretained(cache()), + local_id, std::string(), path, + FileCache::FILE_OPERATION_COPY), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + return error; + } + + scoped_ptr<EntryUpdatePerformer> performer_; +}; + +TEST_F(EntryUpdatePerformerTest, UpdateEntry) { + base::FilePath src_path( + FILE_PATH_LITERAL("drive/root/Directory 1/SubDirectory File 1.txt")); + base::FilePath dest_path( + FILE_PATH_LITERAL("drive/root/Directory 1/Sub Directory Folder")); + + ResourceEntry src_entry, dest_entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(src_path, &src_entry)); + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(dest_path, &dest_entry)); + + // Update local entry. + base::Time new_last_modified = base::Time::FromInternalValue( + src_entry.file_info().last_modified()) + base::TimeDelta::FromSeconds(1); + base::Time new_last_accessed = base::Time::FromInternalValue( + src_entry.file_info().last_accessed()) + base::TimeDelta::FromSeconds(2); + + src_entry.set_parent_local_id(dest_entry.local_id()); + src_entry.set_title("Moved" + src_entry.title()); + src_entry.mutable_file_info()->set_last_modified( + new_last_modified.ToInternalValue()); + src_entry.mutable_file_info()->set_last_accessed( + new_last_accessed.ToInternalValue()); + src_entry.set_metadata_edit_state(ResourceEntry::DIRTY); + + FileError error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&ResourceMetadata::RefreshEntry, + base::Unretained(metadata()), + src_entry), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Perform server side update. + error = FILE_ERROR_FAILED; + performer_->UpdateEntry( + src_entry.local_id(), + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Verify the file is updated on the server. + google_apis::DriveApiErrorCode gdata_error = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> gdata_entry; + fake_service()->GetFileResource( + src_entry.resource_id(), + google_apis::test_util::CreateCopyResultCallback(&gdata_error, + &gdata_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, gdata_error); + ASSERT_TRUE(gdata_entry); + + EXPECT_EQ(src_entry.title(), gdata_entry->title()); + EXPECT_EQ(new_last_modified, gdata_entry->modified_date()); + EXPECT_EQ(new_last_accessed, gdata_entry->last_viewed_by_me_date()); + + ASSERT_FALSE(gdata_entry->parents().empty()); + EXPECT_EQ(dest_entry.resource_id(), gdata_entry->parents()[0].file_id()); +} + +TEST_F(EntryUpdatePerformerTest, UpdateEntry_SetProperties) { + base::FilePath entry_path( + FILE_PATH_LITERAL("drive/root/Directory 1/SubDirectory File 1.txt")); + + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(entry_path, &entry)); + + Property* const first_property = entry.mutable_new_properties()->Add(); + first_property->set_key("hello"); + first_property->set_value("world"); + first_property->set_visibility(Property_Visibility_PUBLIC); + + Property* const second_property = entry.mutable_new_properties()->Add(); + second_property->set_key("this"); + second_property->set_value("will-change"); + second_property->set_visibility(Property_Visibility_PUBLIC); + entry.set_metadata_edit_state(ResourceEntry::DIRTY); + + FileError error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), FROM_HERE, + base::Bind(&ResourceMetadata::RefreshEntry, base::Unretained(metadata()), + entry), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Perform server side update. + error = FILE_ERROR_FAILED; + performer_->UpdateEntry( + entry.local_id(), ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + + // Add a new property during an update. + Property* const property = entry.mutable_new_properties()->Add(); + property->set_key("tokyo"); + property->set_value("kyoto"); + property->set_visibility(Property_Visibility_PUBLIC); + + // Modify an existing property during an update. + second_property->set_value("changed"); + + // Change the resource entry during an update. + base::PostTaskAndReplyWithResult( + blocking_task_runner(), FROM_HERE, + base::Bind(&ResourceMetadata::RefreshEntry, base::Unretained(metadata()), + entry), + google_apis::test_util::CreateCopyResultCallback(&error)); + + // Wait until the update is fully completed. + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // List of synced properties should be removed from the proto. + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(entry_path, &entry)); + ASSERT_EQ(2, entry.new_properties().size()); + EXPECT_EQ("changed", entry.new_properties().Get(0).value()); + EXPECT_EQ("tokyo", entry.new_properties().Get(1).key()); +} + +// Tests updating metadata of a file with a non-dirty cache file. +TEST_F(EntryUpdatePerformerTest, UpdateEntry_WithNonDirtyCache) { + base::FilePath src_path( + FILE_PATH_LITERAL("drive/root/Directory 1/SubDirectory File 1.txt")); + + // Download the file content to prepare a non-dirty cache file. + file_system::DownloadOperation download_operation( + blocking_task_runner(), delegate(), scheduler(), metadata(), cache(), + temp_dir()); + FileError error = FILE_ERROR_FAILED; + base::FilePath cache_file_path; + scoped_ptr<ResourceEntry> src_entry; + download_operation.EnsureFileDownloadedByPath( + src_path, + ClientContext(USER_INITIATED), + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + google_apis::test_util::CreateCopyResultCallback( + &error, &cache_file_path, &src_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + ASSERT_TRUE(src_entry); + + // Update the entry locally. + src_entry->set_title("Updated" + src_entry->title()); + src_entry->set_metadata_edit_state(ResourceEntry::DIRTY); + + error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&ResourceMetadata::RefreshEntry, + base::Unretained(metadata()), + *src_entry), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Perform server side update. This shouldn't fail. (crbug.com/358590) + error = FILE_ERROR_FAILED; + performer_->UpdateEntry( + src_entry->local_id(), + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Verify the file is updated on the server. + google_apis::DriveApiErrorCode gdata_error = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> gdata_entry; + fake_service()->GetFileResource( + src_entry->resource_id(), + google_apis::test_util::CreateCopyResultCallback(&gdata_error, + &gdata_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, gdata_error); + ASSERT_TRUE(gdata_entry); + EXPECT_EQ(src_entry->title(), gdata_entry->title()); +} + +TEST_F(EntryUpdatePerformerTest, UpdateEntry_NotFound) { + const std::string id = "this ID should result in NOT_FOUND"; + FileError error = FILE_ERROR_FAILED; + performer_->UpdateEntry( + id, ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, error); +} + +TEST_F(EntryUpdatePerformerTest, UpdateEntry_ContentUpdate) { + const base::FilePath kFilePath(FILE_PATH_LITERAL("drive/root/File 1.txt")); + const std::string kResourceId("2_file_resource_id"); + + const std::string local_id = GetLocalId(kFilePath); + EXPECT_FALSE(local_id.empty()); + + const std::string kTestFileContent = "I'm being uploaded! Yay!"; + EXPECT_EQ(FILE_ERROR_OK, StoreAndMarkDirty(local_id, kTestFileContent)); + + int64 original_changestamp = + fake_service()->about_resource().largest_change_id(); + + // The callback will be called upon completion of UpdateEntry(). + FileError error = FILE_ERROR_FAILED; + performer_->UpdateEntry( + local_id, + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Check that the server has received an update. + EXPECT_LT(original_changestamp, + fake_service()->about_resource().largest_change_id()); + + // Check that the file size is updated to that of the updated content. + google_apis::DriveApiErrorCode gdata_error = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> server_entry; + fake_service()->GetFileResource( + kResourceId, + google_apis::test_util::CreateCopyResultCallback(&gdata_error, + &server_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, gdata_error); + EXPECT_EQ(static_cast<int64>(kTestFileContent.size()), + server_entry->file_size()); + + // Make sure that the cache is no longer dirty. + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kFilePath, &entry)); + EXPECT_FALSE(entry.file_specific_info().cache_state().is_dirty()); +} + +TEST_F(EntryUpdatePerformerTest, UpdateEntry_ContentUpdateMd5Check) { + const base::FilePath kFilePath(FILE_PATH_LITERAL("drive/root/File 1.txt")); + const std::string kResourceId("2_file_resource_id"); + + const std::string local_id = GetLocalId(kFilePath); + EXPECT_FALSE(local_id.empty()); + + const std::string kTestFileContent = "I'm being uploaded! Yay!"; + EXPECT_EQ(FILE_ERROR_OK, StoreAndMarkDirty(local_id, kTestFileContent)); + + int64 original_changestamp = + fake_service()->about_resource().largest_change_id(); + + // The callback will be called upon completion of UpdateEntry(). + FileError error = FILE_ERROR_FAILED; + performer_->UpdateEntry( + local_id, + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Check that the server has received an update. + EXPECT_LT(original_changestamp, + fake_service()->about_resource().largest_change_id()); + + // Check that the file size is updated to that of the updated content. + google_apis::DriveApiErrorCode gdata_error = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> server_entry; + fake_service()->GetFileResource( + kResourceId, + google_apis::test_util::CreateCopyResultCallback(&gdata_error, + &server_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, gdata_error); + EXPECT_EQ(static_cast<int64>(kTestFileContent.size()), + server_entry->file_size()); + + // Make sure that the cache is no longer dirty. + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kFilePath, &entry)); + EXPECT_FALSE(entry.file_specific_info().cache_state().is_dirty()); + + // Again mark the cache file dirty. + scoped_ptr<base::ScopedClosureRunner> file_closer; + error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&FileCache::OpenForWrite, + base::Unretained(cache()), + local_id, + &file_closer), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + file_closer.reset(); + + // And call UpdateEntry again. + // In this case, although the file is marked as dirty, but the content + // hasn't been changed. Thus, the actual uploading should be skipped. + original_changestamp = fake_service()->about_resource().largest_change_id(); + error = FILE_ERROR_FAILED; + performer_->UpdateEntry( + local_id, + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + EXPECT_EQ(original_changestamp, + fake_service()->about_resource().largest_change_id()); + + // Make sure that the cache is no longer dirty. + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kFilePath, &entry)); + EXPECT_FALSE(entry.file_specific_info().cache_state().is_dirty()); +} + +TEST_F(EntryUpdatePerformerTest, UpdateEntry_OpenedForWrite) { + const base::FilePath kFilePath(FILE_PATH_LITERAL("drive/root/File 1.txt")); + const std::string kResourceId("2_file_resource_id"); + + const std::string local_id = GetLocalId(kFilePath); + EXPECT_FALSE(local_id.empty()); + + const std::string kTestFileContent = "I'm being uploaded! Yay!"; + EXPECT_EQ(FILE_ERROR_OK, StoreAndMarkDirty(local_id, kTestFileContent)); + + // Emulate a situation where someone is writing to the file. + scoped_ptr<base::ScopedClosureRunner> file_closer; + FileError error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&FileCache::OpenForWrite, + base::Unretained(cache()), + local_id, + &file_closer), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Update. This should not clear the dirty bit. + error = FILE_ERROR_FAILED; + performer_->UpdateEntry( + local_id, + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Make sure that the cache is still dirty. + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kFilePath, &entry)); + EXPECT_TRUE(entry.file_specific_info().cache_state().is_dirty()); + + // Close the file. + file_closer.reset(); + + // Update. This should clear the dirty bit. + error = FILE_ERROR_FAILED; + performer_->UpdateEntry( + local_id, + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Make sure that the cache is no longer dirty. + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kFilePath, &entry)); + EXPECT_FALSE(entry.file_specific_info().cache_state().is_dirty()); +} + +TEST_F(EntryUpdatePerformerTest, UpdateEntry_UploadNewFile) { + // Create a new file locally. + const base::FilePath kFilePath(FILE_PATH_LITERAL("drive/root/New File.txt")); + + ResourceEntry parent; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kFilePath.DirName(), &parent)); + + ResourceEntry entry; + entry.set_parent_local_id(parent.local_id()); + entry.set_title(kFilePath.BaseName().AsUTF8Unsafe()); + entry.mutable_file_specific_info()->set_content_mime_type("text/plain"); + entry.set_metadata_edit_state(ResourceEntry::DIRTY); + + FileError error = FILE_ERROR_FAILED; + std::string local_id; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::ResourceMetadata::AddEntry, + base::Unretained(metadata()), + entry, + &local_id), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Update. This should result in creating a new file on the server. + error = FILE_ERROR_FAILED; + performer_->UpdateEntry( + local_id, + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // The entry got a resource ID. + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kFilePath, &entry)); + EXPECT_FALSE(entry.resource_id().empty()); + EXPECT_EQ(ResourceEntry::CLEAN, entry.metadata_edit_state()); + + // Make sure that the cache is no longer dirty. + EXPECT_FALSE(entry.file_specific_info().cache_state().is_dirty()); + + // Make sure that we really created a file. + google_apis::DriveApiErrorCode status = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> server_entry; + fake_service()->GetFileResource( + entry.resource_id(), + google_apis::test_util::CreateCopyResultCallback(&status, &server_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, status); + ASSERT_TRUE(server_entry); + EXPECT_FALSE(server_entry->IsDirectory()); +} + +TEST_F(EntryUpdatePerformerTest, UpdateEntry_NewFileOpendForWrite) { + // Create a new file locally. + const base::FilePath kFilePath(FILE_PATH_LITERAL("drive/root/New File.txt")); + + ResourceEntry parent; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kFilePath.DirName(), &parent)); + + ResourceEntry entry; + entry.set_parent_local_id(parent.local_id()); + entry.set_title(kFilePath.BaseName().AsUTF8Unsafe()); + entry.mutable_file_specific_info()->set_content_mime_type("text/plain"); + entry.set_metadata_edit_state(ResourceEntry::DIRTY); + + FileError error = FILE_ERROR_FAILED; + std::string local_id; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::ResourceMetadata::AddEntry, + base::Unretained(metadata()), + entry, + &local_id), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + const std::string kTestFileContent = "This is a new file."; + EXPECT_EQ(FILE_ERROR_OK, StoreAndMarkDirty(local_id, kTestFileContent)); + + // Emulate a situation where someone is writing to the file. + scoped_ptr<base::ScopedClosureRunner> file_closer; + error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&FileCache::OpenForWrite, + base::Unretained(cache()), + local_id, + &file_closer), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Update, but no update is performed because the file is opened. + error = FILE_ERROR_FAILED; + performer_->UpdateEntry( + local_id, + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // The entry hasn't got a resource ID yet. + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kFilePath, &entry)); + EXPECT_TRUE(entry.resource_id().empty()); + + // Close the file. + file_closer.reset(); + + // Update. This should result in creating a new file on the server. + error = FILE_ERROR_FAILED; + performer_->UpdateEntry( + local_id, + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // The entry got a resource ID. + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kFilePath, &entry)); + EXPECT_FALSE(entry.resource_id().empty()); + EXPECT_EQ(ResourceEntry::CLEAN, entry.metadata_edit_state()); +} + +TEST_F(EntryUpdatePerformerTest, UpdateEntry_CreateDirectory) { + // Create a new directory locally. + const base::FilePath kPath(FILE_PATH_LITERAL("drive/root/New Directory")); + + ResourceEntry parent; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kPath.DirName(), &parent)); + + ResourceEntry entry; + entry.set_parent_local_id(parent.local_id()); + entry.set_title(kPath.BaseName().AsUTF8Unsafe()); + entry.mutable_file_info()->set_is_directory(true); + entry.set_metadata_edit_state(ResourceEntry::DIRTY); + + FileError error = FILE_ERROR_FAILED; + std::string local_id; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&internal::ResourceMetadata::AddEntry, + base::Unretained(metadata()), + entry, + &local_id), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Update. This should result in creating a new directory on the server. + error = FILE_ERROR_FAILED; + performer_->UpdateEntry( + local_id, + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // The entry got a resource ID. + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kPath, &entry)); + EXPECT_FALSE(entry.resource_id().empty()); + EXPECT_EQ(ResourceEntry::CLEAN, entry.metadata_edit_state()); + + // Make sure that we really created a directory. + google_apis::DriveApiErrorCode status = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> server_entry; + fake_service()->GetFileResource( + entry.resource_id(), + google_apis::test_util::CreateCopyResultCallback(&status, &server_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, status); + ASSERT_TRUE(server_entry); + EXPECT_TRUE(server_entry->IsDirectory()); +} + +TEST_F(EntryUpdatePerformerTest, UpdateEntry_InsufficientPermission) { + base::FilePath src_path( + FILE_PATH_LITERAL("drive/root/Directory 1/SubDirectory File 1.txt")); + + ResourceEntry src_entry; + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(src_path, &src_entry)); + + // Update local entry. + ResourceEntry updated_entry(src_entry); + updated_entry.set_title("Moved" + src_entry.title()); + updated_entry.set_metadata_edit_state(ResourceEntry::DIRTY); + + FileError error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&ResourceMetadata::RefreshEntry, + base::Unretained(metadata()), + updated_entry), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Set user permission to forbid server side update. + EXPECT_EQ(google_apis::HTTP_SUCCESS, fake_service()->SetUserPermission( + src_entry.resource_id(), google_apis::drive::PERMISSION_ROLE_READER)); + + // Try to perform update. + error = FILE_ERROR_FAILED; + performer_->UpdateEntry( + src_entry.local_id(), + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // This should result in reverting the local change. + ResourceEntry result_entry; + EXPECT_EQ(FILE_ERROR_OK, + GetLocalResourceEntryById(src_entry.local_id(), &result_entry)); + EXPECT_EQ(src_entry.title(), result_entry.title()); +} + +} // namespace internal +} // namespace drive diff --git a/components/drive/sync/remove_performer.cc b/components/drive/sync/remove_performer.cc new file mode 100644 index 0000000..499f070 --- /dev/null +++ b/components/drive/sync/remove_performer.cc @@ -0,0 +1,255 @@ +// 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/sync/remove_performer.h" + +#include "base/sequenced_task_runner.h" +#include "components/drive/drive.pb.h" +#include "components/drive/drive_api_util.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 "components/drive/sync/entry_revert_performer.h" +#include "google_apis/drive/drive_api_parser.h" + +namespace drive { +namespace internal { + +namespace { + +// Updates local metadata and after remote unparenting. +FileError UpdateLocalStateAfterUnparent(ResourceMetadata* metadata, + const std::string& local_id) { + ResourceEntry entry; + FileError error = metadata->GetResourceEntryById(local_id, &entry); + if (error != FILE_ERROR_OK) + return error; + entry.set_parent_local_id(util::kDriveOtherDirLocalId); + return metadata->RefreshEntry(entry); +} + +// Utility function to run ResourceMetadata::RemoveEntry from UI thread. +void RemoveEntryOnUIThread(base::SequencedTaskRunner* blocking_task_runner, + ResourceMetadata* metadata, + const std::string& local_id, + const FileOperationCallback& callback) { + base::PostTaskAndReplyWithResult( + blocking_task_runner, + FROM_HERE, + base::Bind(&ResourceMetadata::RemoveEntry, + base::Unretained(metadata), local_id), + callback); +} + +} // namespace + +RemovePerformer::RemovePerformer( + base::SequencedTaskRunner* blocking_task_runner, + file_system::OperationDelegate* delegate, + JobScheduler* scheduler, + ResourceMetadata* metadata) + : blocking_task_runner_(blocking_task_runner), + delegate_(delegate), + scheduler_(scheduler), + metadata_(metadata), + entry_revert_performer_(new EntryRevertPerformer(blocking_task_runner, + delegate, + scheduler, + metadata)), + weak_ptr_factory_(this) { +} + +RemovePerformer::~RemovePerformer() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +// Returns |entry| corresponding to |local_id|. +// Adding to that, removes the entry when it does not exist on the server. +FileError TryToRemoveLocally(ResourceMetadata* metadata, + const std::string& local_id, + ResourceEntry* entry) { + FileError error = metadata->GetResourceEntryById(local_id, entry); + if (error != FILE_ERROR_OK || !entry->resource_id().empty()) + return error; + return metadata->RemoveEntry(local_id); +} + +void RemovePerformer::Remove(const std::string& local_id, + const ClientContext& context, + 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(&TryToRemoveLocally, metadata_, local_id, entry), + base::Bind(&RemovePerformer::RemoveAfterGetResourceEntry, + weak_ptr_factory_.GetWeakPtr(), + context, + callback, + base::Owned(entry))); +} + +void RemovePerformer::RemoveAfterGetResourceEntry( + const ClientContext& context, + const FileOperationCallback& callback, + const ResourceEntry* entry, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (error != FILE_ERROR_OK || entry->resource_id().empty()) { + callback.Run(error); + return; + } + + // To match with the behavior of drive.google.com: + // Removal of shared entries under MyDrive is just removing from the parent. + // The entry will stay in shared-with-me (in other words, in "drive/other".) + // + // TODO(kinaba): to be more precise, we might be better to branch by whether + // or not the current account is an owner of the file. The code below is + // written under the assumption that |shared_with_me| coincides with that. + if (entry->shared_with_me()) { + UnparentResource(context, callback, entry->resource_id(), + entry->local_id()); + } else { + // Otherwise try sending the entry to trash. + TrashResource(context, callback, entry->resource_id(), entry->local_id()); + } +} + +void RemovePerformer::TrashResource(const ClientContext& context, + const FileOperationCallback& callback, + const std::string& resource_id, + const std::string& local_id) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + scheduler_->TrashResource( + resource_id, + context, + base::Bind(&RemovePerformer::TrashResourceAfterUpdateRemoteState, + weak_ptr_factory_.GetWeakPtr(), context, callback, local_id)); +} + +void RemovePerformer::TrashResourceAfterUpdateRemoteState( + const ClientContext& context, + const FileOperationCallback& callback, + const std::string& local_id, + google_apis::DriveApiErrorCode status) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (status == google_apis::HTTP_FORBIDDEN) { + // Editing this entry is not allowed, revert local changes. + entry_revert_performer_->RevertEntry(local_id, context, callback); + delegate_->OnDriveSyncError( + file_system::DRIVE_SYNC_ERROR_DELETE_WITHOUT_PERMISSION, local_id); + return; + } + + FileError error = GDataToFileError(status); + if (error == FILE_ERROR_NOT_FOUND) { // Remove local entry when not found. + RemoveEntryOnUIThread(blocking_task_runner_.get(), metadata_, local_id, + callback); + return; + } + + // Now we're done. If the entry is trashed on the server, it'll be also + // deleted locally on the next update. + callback.Run(error); +} + +void RemovePerformer::UnparentResource(const ClientContext& context, + const FileOperationCallback& callback, + const std::string& resource_id, + const std::string& local_id) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + scheduler_->GetFileResource( + resource_id, + context, + base::Bind(&RemovePerformer::UnparentResourceAfterGetFileResource, + weak_ptr_factory_.GetWeakPtr(), context, callback, local_id)); +} + +void RemovePerformer::UnparentResourceAfterGetFileResource( + const ClientContext& context, + const FileOperationCallback& callback, + const std::string& local_id, + google_apis::DriveApiErrorCode status, + scoped_ptr<google_apis::FileResource> file_resource) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FileError error = GDataToFileError(status); + if (error == FILE_ERROR_NOT_FOUND) { // Remove local entry when not found. + RemoveEntryOnUIThread(blocking_task_runner_.get(), metadata_, local_id, + callback); + return; + } + + if (error != FILE_ERROR_OK) { + callback.Run(error); + return; + } + + ResourceEntry entry; + std::string parent_resource_id; + if (!ConvertFileResourceToResourceEntry(*file_resource, &entry, + &parent_resource_id)) { + callback.Run(FILE_ERROR_NOT_A_FILE); + return; + } + + if (!entry.shared_with_me()) { + // shared_with_me() has changed on the server. + UnparentResourceAfterUpdateRemoteState(callback, local_id, + google_apis::HTTP_CONFLICT); + return; + } + + if (parent_resource_id.empty()) { + // This entry is unparented already. + UnparentResourceAfterUpdateRemoteState(callback, local_id, + google_apis::HTTP_NO_CONTENT); + return; + } + + scheduler_->RemoveResourceFromDirectory( + parent_resource_id, + entry.resource_id(), + context, + base::Bind(&RemovePerformer::UnparentResourceAfterUpdateRemoteState, + weak_ptr_factory_.GetWeakPtr(), callback, local_id)); +} + +void RemovePerformer::UnparentResourceAfterUpdateRemoteState( + const FileOperationCallback& callback, + const std::string& local_id, + google_apis::DriveApiErrorCode status) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FileError error = GDataToFileError(status); + if (error != FILE_ERROR_OK) { + callback.Run(error); + return; + } + + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&UpdateLocalStateAfterUnparent, metadata_, local_id), + callback); +} + +} // namespace internal +} // namespace drive diff --git a/components/drive/sync/remove_performer.h b/components/drive/sync/remove_performer.h new file mode 100644 index 0000000..bce22e0 --- /dev/null +++ b/components/drive/sync/remove_performer.h @@ -0,0 +1,114 @@ +// 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_SYNC_REMOVE_PERFORMER_H_ +#define COMPONENTS_DRIVE_SYNC_REMOVE_PERFORMER_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 SequencedTaskRunner; +} + +namespace google_apis { +class FileResource; +} + +namespace drive { + +class JobScheduler; +class ResourceEntry; +struct ClientContext; + +namespace file_system { +class OperationDelegate; +} // namespace file_system + +namespace internal { + +class EntryRevertPerformer; +class ResourceMetadata; + +// This class encapsulates the drive Remove 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 RemovePerformer { + public: + RemovePerformer(base::SequencedTaskRunner* blocking_task_runner, + file_system::OperationDelegate* delegate, + JobScheduler* scheduler, + ResourceMetadata* metadata); + ~RemovePerformer(); + + // Removes the resource. + // + // |callback| must not be null. + void Remove(const std::string& local_id, + const ClientContext& context, + const FileOperationCallback& callback); + + private: + // Part of Remove(). Called after GetResourceEntry() completion. + void RemoveAfterGetResourceEntry(const ClientContext& context, + const FileOperationCallback& callback, + const ResourceEntry* entry, + FileError error); + + // Requests the server to move the specified resource to the trash. + void TrashResource(const ClientContext& context, + const FileOperationCallback& callback, + const std::string& resource_id, + const std::string& local_id); + + // Part of TrashResource(). Called after server-side removal is done. + void TrashResourceAfterUpdateRemoteState( + const ClientContext& context, + const FileOperationCallback& callback, + const std::string& local_id, + google_apis::DriveApiErrorCode status); + + // Requests the server to detach the specified resource from its parent. + void UnparentResource(const ClientContext& context, + const FileOperationCallback& callback, + const std::string& resource_id, + const std::string& local_id); + + // Part of UnparentResource(). + void UnparentResourceAfterGetFileResource( + const ClientContext& context, + const FileOperationCallback& callback, + const std::string& local_id, + google_apis::DriveApiErrorCode status, + scoped_ptr<google_apis::FileResource> file_resource); + + // Part of UnparentResource(). + void UnparentResourceAfterUpdateRemoteState( + const FileOperationCallback& callback, + const std::string& local_id, + google_apis::DriveApiErrorCode status); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + file_system::OperationDelegate* delegate_; + JobScheduler* scheduler_; + ResourceMetadata* metadata_; + scoped_ptr<EntryRevertPerformer> entry_revert_performer_; + + 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<RemovePerformer> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(RemovePerformer); +}; + +} // namespace internal +} // namespace drive + +#endif // COMPONENTS_DRIVE_SYNC_REMOVE_PERFORMER_H_ diff --git a/components/drive/sync/remove_performer_unittest.cc b/components/drive/sync/remove_performer_unittest.cc new file mode 100644 index 0000000..7404d00 --- /dev/null +++ b/components/drive/sync/remove_performer_unittest.cc @@ -0,0 +1,199 @@ +// 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/sync/remove_performer.h" + +#include "base/task_runner_util.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/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" + +namespace drive { +namespace internal { + +typedef file_system::OperationTestBase RemovePerformerTest; + +TEST_F(RemovePerformerTest, RemoveFile) { + RemovePerformer performer(blocking_task_runner(), delegate(), scheduler(), + metadata()); + + 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)); + performer.Remove(entry.local_id(), + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // 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(); + + performer.Remove(entry.local_id(), + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Verify the file is indeed removed in the server. + google_apis::DriveApiErrorCode gdata_error = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> gdata_entry; + fake_service()->GetFileResource( + resource_id, + google_apis::test_util::CreateCopyResultCallback(&gdata_error, + &gdata_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + ASSERT_EQ(google_apis::HTTP_SUCCESS, gdata_error); + EXPECT_TRUE(gdata_entry->labels().is_trashed()); + + // Try removing non-existing file. + error = FILE_ERROR_FAILED; + performer.Remove("non-existing-id", + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, error); +} + +TEST_F(RemovePerformerTest, RemoveShared) { + RemovePerformer performer(blocking_task_runner(), delegate(), scheduler(), + metadata()); + + const base::FilePath kPathInMyDrive(FILE_PATH_LITERAL( + "drive/root/shared.txt")); + const base::FilePath kPathInOther(FILE_PATH_LITERAL( + "drive/other/shared.txt")); + + // Prepare a shared file to the root folder. + google_apis::DriveApiErrorCode gdata_error = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> gdata_entry; + fake_service()->AddNewFile( + "text/plain", + "dummy content", + fake_service()->GetRootResourceId(), + kPathInMyDrive.BaseName().AsUTF8Unsafe(), + true, // shared_with_me, + google_apis::test_util::CreateCopyResultCallback(&gdata_error, + &gdata_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + ASSERT_EQ(google_apis::HTTP_CREATED, gdata_error); + CheckForUpdates(); + + // Remove it. Locally, the file should be moved to drive/other. + ResourceEntry entry; + ASSERT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kPathInMyDrive, &entry)); + FileError error = FILE_ERROR_FAILED; + performer.Remove(entry.local_id(), + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, + GetLocalResourceEntry(kPathInMyDrive, &entry)); + EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(kPathInOther, &entry)); + + // Remotely, the entry should have lost its parent. + gdata_error = google_apis::DRIVE_OTHER_ERROR; + const std::string resource_id = gdata_entry->file_id(); + fake_service()->GetFileResource( + resource_id, + google_apis::test_util::CreateCopyResultCallback(&gdata_error, + &gdata_entry)); + content::RunAllBlockingPoolTasksUntilIdle(); + ASSERT_EQ(google_apis::HTTP_SUCCESS, gdata_error); + EXPECT_FALSE(gdata_entry->labels().is_trashed()); // It's not deleted. + EXPECT_TRUE(gdata_entry->parents().empty()); +} + +TEST_F(RemovePerformerTest, RemoveLocallyCreatedFile) { + RemovePerformer performer(blocking_task_runner(), delegate(), scheduler(), + metadata()); + + // Add an entry without resource ID. + ResourceEntry entry; + entry.set_title("New File.txt"); + entry.set_parent_local_id(util::kDriveTrashDirLocalId); + + FileError error = FILE_ERROR_FAILED; + std::string local_id; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&ResourceMetadata::AddEntry, + base::Unretained(metadata()), + entry, + &local_id), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Remove the entry. + performer.Remove(local_id, ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + EXPECT_EQ(FILE_ERROR_NOT_FOUND, GetLocalResourceEntryById(local_id, &entry)); +} + +TEST_F(RemovePerformerTest, Remove_InsufficientPermission) { + RemovePerformer performer(blocking_task_runner(), delegate(), scheduler(), + metadata()); + + 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)); + + // Remove locally. + ResourceEntry updated_entry(src_entry); + updated_entry.set_parent_local_id(util::kDriveTrashDirLocalId); + + FileError error = FILE_ERROR_FAILED; + base::PostTaskAndReplyWithResult( + blocking_task_runner(), + FROM_HERE, + base::Bind(&ResourceMetadata::RefreshEntry, + base::Unretained(metadata()), + updated_entry), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Set user permission to forbid server side update. + EXPECT_EQ(google_apis::HTTP_SUCCESS, fake_service()->SetUserPermission( + src_entry.resource_id(), google_apis::drive::PERMISSION_ROLE_READER)); + + // Try to perform remove. + error = FILE_ERROR_FAILED; + performer.Remove(src_entry.local_id(), + ClientContext(USER_INITIATED), + google_apis::test_util::CreateCopyResultCallback(&error)); + content::RunAllBlockingPoolTasksUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // This should result in reverting the local change. + ResourceEntry result_entry; + EXPECT_EQ(FILE_ERROR_OK, + GetLocalResourceEntryById(src_entry.local_id(), &result_entry)); + EXPECT_EQ(src_entry.parent_local_id(), result_entry.parent_local_id()); + + ASSERT_EQ(1U, delegate()->drive_sync_errors().size()); + EXPECT_EQ(file_system::DRIVE_SYNC_ERROR_DELETE_WITHOUT_PERMISSION, + delegate()->drive_sync_errors()[0]); +} + +} // namespace internal +} // namespace drive diff --git a/components/drive/sync_client.cc b/components/drive/sync_client.cc new file mode 100644 index 0000000..e11a6ce --- /dev/null +++ b/components/drive/sync_client.cc @@ -0,0 +1,491 @@ +// 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/sync_client.h" + +#include <vector> + +#include "base/bind.h" +#include "base/thread_task_runner_handle.h" +#include "components/drive/drive.pb.h" +#include "components/drive/file_cache.h" +#include "components/drive/file_system/download_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/sync/entry_update_performer.h" +#include "google_apis/drive/task_util.h" + +namespace drive { +namespace internal { + +namespace { + +// The delay constant is used to delay processing a sync task. We should not +// process SyncTasks immediately for the following reasons: +// +// 1) For fetching, the user may accidentally click on "Make available +// offline" checkbox on a file, and immediately cancel it in a second. +// It's a waste to fetch the file in this case. +// +// 2) For uploading, file writing via HTML5 file system API is performed in +// two steps: 1) truncate a file to 0 bytes, 2) write contents. We +// shouldn't start uploading right after the step 1). Besides, the user +// may edit the same file repeatedly in a short period of time. +// +// TODO(satorux): We should find a way to handle the upload case more nicely, +// and shorten the delay. crbug.com/134774 +const int kDelaySeconds = 1; + +// The delay constant is used to delay retrying a sync task on server errors. +const int kLongDelaySeconds = 600; + +// Iterates entries and appends IDs to |to_fetch| if the file is pinned but not +// fetched (not present locally), to |to_update| if the file needs update. +void CollectBacklog(ResourceMetadata* metadata, + std::vector<std::string>* to_fetch, + std::vector<std::string>* to_update) { + DCHECK(to_fetch); + DCHECK(to_update); + + scoped_ptr<ResourceMetadata::Iterator> it = metadata->GetIterator(); + for (; !it->IsAtEnd(); it->Advance()) { + const std::string& local_id = it->GetID(); + const ResourceEntry& entry = it->GetValue(); + if (entry.parent_local_id() == util::kDriveTrashDirLocalId) { + to_update->push_back(local_id); + continue; + } + + bool should_update = false; + switch (entry.metadata_edit_state()) { + case ResourceEntry::CLEAN: + break; + case ResourceEntry::SYNCING: + case ResourceEntry::DIRTY: + should_update = true; + break; + } + + if (entry.file_specific_info().cache_state().is_pinned() && + !entry.file_specific_info().cache_state().is_present()) + to_fetch->push_back(local_id); + + if (entry.file_specific_info().cache_state().is_dirty()) + should_update = true; + + if (should_update) + to_update->push_back(local_id); + } + DCHECK(!it->HasError()); +} + +// Iterates cache entries and collects IDs of ones with obsolete cache files. +void CheckExistingPinnedFiles(ResourceMetadata* metadata, + FileCache* cache, + std::vector<std::string>* local_ids) { + scoped_ptr<ResourceMetadata::Iterator> it = metadata->GetIterator(); + for (; !it->IsAtEnd(); it->Advance()) { + const ResourceEntry& entry = it->GetValue(); + const FileCacheEntry& cache_state = + entry.file_specific_info().cache_state(); + const std::string& local_id = it->GetID(); + if (!cache_state.is_pinned() || !cache_state.is_present()) + continue; + + // If MD5s don't match, it indicates the local cache file is stale, unless + // the file is dirty (the MD5 is "local"). We should never re-fetch the + // file when we have a locally modified version. + if (entry.file_specific_info().md5() == cache_state.md5() || + cache_state.is_dirty()) + continue; + + FileError error = cache->Remove(local_id); + if (error != FILE_ERROR_OK) { + LOG(WARNING) << "Failed to remove cache entry: " << local_id; + continue; + } + + error = cache->Pin(local_id); + if (error != FILE_ERROR_OK) { + LOG(WARNING) << "Failed to pin cache entry: " << local_id; + continue; + } + + local_ids->push_back(local_id); + } + DCHECK(!it->HasError()); +} + +// Gets the parent entry of the entry specified by the ID. +FileError GetParentResourceEntry(ResourceMetadata* metadata, + const std::string& local_id, + ResourceEntry* parent) { + ResourceEntry entry; + FileError error = metadata->GetResourceEntryById(local_id, &entry); + if (error != FILE_ERROR_OK) + return error; + return metadata->GetResourceEntryById(entry.parent_local_id(), parent); +} + +} // namespace + +SyncClient::SyncTask::SyncTask() + : state(SUSPENDED), context(BACKGROUND), should_run_again(false) {} +SyncClient::SyncTask::~SyncTask() {} + +SyncClient::SyncClient(base::SequencedTaskRunner* blocking_task_runner, + file_system::OperationDelegate* delegate, + JobScheduler* scheduler, + ResourceMetadata* metadata, + FileCache* cache, + LoaderController* loader_controller, + const base::FilePath& temporary_file_directory) + : blocking_task_runner_(blocking_task_runner), + operation_delegate_(delegate), + metadata_(metadata), + cache_(cache), + download_operation_(new file_system::DownloadOperation( + blocking_task_runner, + delegate, + scheduler, + metadata, + cache, + temporary_file_directory)), + entry_update_performer_(new EntryUpdatePerformer(blocking_task_runner, + delegate, + scheduler, + metadata, + cache, + loader_controller)), + delay_(base::TimeDelta::FromSeconds(kDelaySeconds)), + long_delay_(base::TimeDelta::FromSeconds(kLongDelaySeconds)), + weak_ptr_factory_(this) { +} + +SyncClient::~SyncClient() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +void SyncClient::StartProcessingBacklog() { + DCHECK(thread_checker_.CalledOnValidThread()); + + std::vector<std::string>* to_fetch = new std::vector<std::string>; + std::vector<std::string>* to_update = new std::vector<std::string>; + blocking_task_runner_->PostTaskAndReply( + FROM_HERE, + base::Bind(&CollectBacklog, metadata_, to_fetch, to_update), + base::Bind(&SyncClient::OnGetLocalIdsOfBacklog, + weak_ptr_factory_.GetWeakPtr(), + base::Owned(to_fetch), + base::Owned(to_update))); +} + +void SyncClient::StartCheckingExistingPinnedFiles() { + DCHECK(thread_checker_.CalledOnValidThread()); + + std::vector<std::string>* local_ids = new std::vector<std::string>; + blocking_task_runner_->PostTaskAndReply( + FROM_HERE, + base::Bind(&CheckExistingPinnedFiles, + metadata_, + cache_, + local_ids), + base::Bind(&SyncClient::AddFetchTasks, + weak_ptr_factory_.GetWeakPtr(), + base::Owned(local_ids))); +} + +void SyncClient::AddFetchTask(const std::string& local_id) { + DCHECK(thread_checker_.CalledOnValidThread()); + AddFetchTaskInternal(local_id, delay_); +} + +void SyncClient::RemoveFetchTask(const std::string& local_id) { + DCHECK(thread_checker_.CalledOnValidThread()); + + SyncTasks::iterator it = tasks_.find(SyncTasks::key_type(FETCH, local_id)); + if (it == tasks_.end()) + return; + + SyncTask* task = &it->second; + switch (task->state) { + case SUSPENDED: + case PENDING: + OnTaskComplete(FETCH, local_id, FILE_ERROR_ABORT); + break; + case RUNNING: + if (!task->cancel_closure.is_null()) + task->cancel_closure.Run(); + break; + } +} + +void SyncClient::AddUpdateTask(const ClientContext& context, + const std::string& local_id) { + DCHECK(thread_checker_.CalledOnValidThread()); + AddUpdateTaskInternal(context, local_id, delay_); +} + +bool SyncClient:: WaitForUpdateTaskToComplete( + const std::string& local_id, + const FileOperationCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + SyncTasks::iterator it = tasks_.find(SyncTasks::key_type(UPDATE, local_id)); + if (it == tasks_.end()) + return false; + + SyncTask* task = &it->second; + task->waiting_callbacks.push_back(callback); + return true; +} + +base::Closure SyncClient::PerformFetchTask(const std::string& local_id, + const ClientContext& context) { + DCHECK(thread_checker_.CalledOnValidThread()); + return download_operation_->EnsureFileDownloadedByLocalId( + local_id, + context, + GetFileContentInitializedCallback(), + google_apis::GetContentCallback(), + base::Bind(&SyncClient::OnFetchFileComplete, + weak_ptr_factory_.GetWeakPtr(), + local_id)); +} + +void SyncClient::AddFetchTaskInternal(const std::string& local_id, + const base::TimeDelta& delay) { + DCHECK(thread_checker_.CalledOnValidThread()); + + SyncTask task; + task.state = PENDING; + task.context = ClientContext(BACKGROUND); + task.task = base::Bind(&SyncClient::PerformFetchTask, + base::Unretained(this), + local_id); + AddTask(SyncTasks::key_type(FETCH, local_id), task, delay); +} + +base::Closure SyncClient::PerformUpdateTask(const std::string& local_id, + const ClientContext& context) { + DCHECK(thread_checker_.CalledOnValidThread()); + entry_update_performer_->UpdateEntry( + local_id, + context, + base::Bind(&SyncClient::OnTaskComplete, + weak_ptr_factory_.GetWeakPtr(), + UPDATE, + local_id)); + return base::Closure(); +} + +void SyncClient::AddUpdateTaskInternal(const ClientContext& context, + const std::string& local_id, + const base::TimeDelta& delay) { + DCHECK(thread_checker_.CalledOnValidThread()); + + SyncTask task; + task.state = PENDING; + task.context = context; + task.task = base::Bind(&SyncClient::PerformUpdateTask, + base::Unretained(this), + local_id); + AddTask(SyncTasks::key_type(UPDATE, local_id), task, delay); +} + +void SyncClient::AddTask(const SyncTasks::key_type& key, + const SyncTask& task, + const base::TimeDelta& delay) { + DCHECK(thread_checker_.CalledOnValidThread()); + + SyncTasks::iterator it = tasks_.find(key); + if (it != tasks_.end()) { + switch (it->second.state) { + case SUSPENDED: + // Activate the task. + it->second.state = PENDING; + break; + case PENDING: + // The same task will run, do nothing. + return; + case RUNNING: + // Something has changed since the task started. Schedule rerun. + it->second.should_run_again = true; + return; + } + } else { + tasks_[key] = task; + } + DCHECK_EQ(PENDING, task.state); + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::Bind(&SyncClient::StartTask, weak_ptr_factory_.GetWeakPtr(), key), + delay); +} + +void SyncClient::StartTask(const SyncTasks::key_type& key) { + ResourceEntry* parent = new ResourceEntry; + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&GetParentResourceEntry, metadata_, key.second, parent), + base::Bind(&SyncClient::StartTaskAfterGetParentResourceEntry, + weak_ptr_factory_.GetWeakPtr(), + key, + base::Owned(parent))); +} + +void SyncClient::StartTaskAfterGetParentResourceEntry( + const SyncTasks::key_type& key, + const ResourceEntry* parent, + FileError error) { + const SyncType type = key.first; + const std::string& local_id = key.second; + SyncTasks::iterator it = tasks_.find(key); + if (it == tasks_.end()) + return; + + SyncTask* task = &it->second; + switch (task->state) { + case SUSPENDED: + case PENDING: + break; + case RUNNING: // Do nothing. + return; + } + + if (error != FILE_ERROR_OK) { + OnTaskComplete(type, local_id, error); + return; + } + + if (type == UPDATE && + parent->resource_id().empty() && + parent->local_id() != util::kDriveTrashDirLocalId) { + // Parent entry needs to be synced to get a resource ID. + // Suspend the task and register it as a dependent task of the parent. + const SyncTasks::key_type key_parent(type, parent->local_id()); + SyncTasks::iterator it_parent = tasks_.find(key_parent); + if (it_parent == tasks_.end()) { + OnTaskComplete(type, local_id, FILE_ERROR_INVALID_OPERATION); + LOG(WARNING) << "Parent task not found: type = " << type << ", id = " + << local_id << ", parent_id = " << parent->local_id(); + return; + } + task->state = SUSPENDED; + it_parent->second.dependent_tasks.push_back(key); + return; + } + + // Run the task. + task->state = RUNNING; + task->cancel_closure = task->task.Run(task->context); +} + +void SyncClient::OnGetLocalIdsOfBacklog( + const std::vector<std::string>* to_fetch, + const std::vector<std::string>* to_update) { + DCHECK(thread_checker_.CalledOnValidThread()); + + // Give priority to upload tasks over fetch tasks, so that dirty files are + // uploaded as soon as possible. + for (size_t i = 0; i < to_update->size(); ++i) { + const std::string& local_id = (*to_update)[i]; + DVLOG(1) << "Queuing to update: " << local_id; + AddUpdateTask(ClientContext(BACKGROUND), local_id); + } + + for (size_t i = 0; i < to_fetch->size(); ++i) { + const std::string& local_id = (*to_fetch)[i]; + DVLOG(1) << "Queuing to fetch: " << local_id; + AddFetchTaskInternal(local_id, delay_); + } +} + +void SyncClient::AddFetchTasks(const std::vector<std::string>* local_ids) { + DCHECK(thread_checker_.CalledOnValidThread()); + + for (size_t i = 0; i < local_ids->size(); ++i) + AddFetchTask((*local_ids)[i]); +} + +void SyncClient::OnTaskComplete(SyncType type, + const std::string& local_id, + FileError error) { + DCHECK(thread_checker_.CalledOnValidThread()); + + const SyncTasks::key_type key(type, local_id); + SyncTasks::iterator it = tasks_.find(key); + DCHECK(it != tasks_.end()); + + base::TimeDelta retry_delay = base::TimeDelta::FromSeconds(0); + + switch (error) { + case FILE_ERROR_OK: + DVLOG(1) << "Completed: type = " << type << ", id = " << local_id; + break; + case FILE_ERROR_ABORT: + // Ignore it because this is caused by user's cancel operations. + break; + case FILE_ERROR_NO_CONNECTION: + // Run the task again so that we'll retry once the connection is back. + it->second.should_run_again = true; + it->second.context = ClientContext(BACKGROUND); + break; + case FILE_ERROR_SERVICE_UNAVAILABLE: + // Run the task again so that we'll retry once the service is back. + it->second.should_run_again = true; + it->second.context = ClientContext(BACKGROUND); + retry_delay = long_delay_; + operation_delegate_->OnDriveSyncError( + file_system::DRIVE_SYNC_ERROR_SERVICE_UNAVAILABLE, local_id); + break; + default: + operation_delegate_->OnDriveSyncError( + file_system::DRIVE_SYNC_ERROR_MISC, local_id); + LOG(WARNING) << "Failed: type = " << type << ", id = " << local_id + << ": " << FileErrorToString(error); + } + + for (size_t i = 0; i < it->second.waiting_callbacks.size(); ++i) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(it->second.waiting_callbacks[i], error)); + } + it->second.waiting_callbacks.clear(); + + if (it->second.should_run_again) { + DVLOG(1) << "Running again: type = " << type << ", id = " << local_id; + it->second.state = PENDING; + it->second.should_run_again = false; + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::Bind(&SyncClient::StartTask, weak_ptr_factory_.GetWeakPtr(), key), + retry_delay); + } else { + for (size_t i = 0; i < it->second.dependent_tasks.size(); ++i) + StartTask(it->second.dependent_tasks[i]); + tasks_.erase(it); + } +} + +void SyncClient::OnFetchFileComplete(const std::string& local_id, + FileError error, + const base::FilePath& local_path, + scoped_ptr<ResourceEntry> entry) { + DCHECK(thread_checker_.CalledOnValidThread()); + OnTaskComplete(FETCH, local_id, error); + if (error == FILE_ERROR_ABORT) { + // If user cancels download, unpin the file so that we do not sync the file + // again. + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&FileCache::Unpin, base::Unretained(cache_), local_id), + base::Bind(&util::EmptyFileOperationCallback)); + } +} + +} // namespace internal +} // namespace drive diff --git a/components/drive/sync_client.h b/components/drive/sync_client.h new file mode 100644 index 0000000..1c53ddc --- /dev/null +++ b/components/drive/sync_client.h @@ -0,0 +1,199 @@ +// 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_SYNC_CLIENT_H_ +#define COMPONENTS_DRIVE_SYNC_CLIENT_H_ + +#include <map> +#include <string> +#include <vector> + +#include "base/callback.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "base/time/time.h" +#include "components/drive/file_errors.h" +#include "components/drive/job_scheduler.h" +#include "components/drive/resource_metadata.h" + +namespace base { +class SequencedTaskRunner; +} + +namespace drive { + +class FileCacheEntry; +class JobScheduler; +class ResourceEntry; +struct ClientContext; + +namespace file_system { +class DownloadOperation; +class OperationDelegate; +} + +namespace internal { + +class ChangeListLoader; +class EntryUpdatePerformer; +class FileCache; +class LoaderController; +class ResourceMetadata; + +// The SyncClient is used to synchronize pinned files on Drive and the +// cache on the local drive. +// +// If the user logs out before fetching of the pinned files is complete, this +// client resumes fetching operations next time the user logs in, based on +// the states left in the cache. +class SyncClient { + public: + SyncClient(base::SequencedTaskRunner* blocking_task_runner, + file_system::OperationDelegate* delegate, + JobScheduler* scheduler, + ResourceMetadata* metadata, + FileCache* cache, + LoaderController* loader_controller, + const base::FilePath& temporary_file_directory); + virtual ~SyncClient(); + + // Adds a fetch task. + void AddFetchTask(const std::string& local_id); + + // Removes a fetch task. + void RemoveFetchTask(const std::string& local_id); + + // Adds a update task. + void AddUpdateTask(const ClientContext& context, const std::string& local_id); + + // Waits for the update task to complete and runs the callback. + // Returns false if no task is found for the spcecified ID. + bool WaitForUpdateTaskToComplete(const std::string& local_id, + const FileOperationCallback& callback); + + // Starts processing the backlog (i.e. pinned-but-not-filed files and + // dirty-but-not-uploaded files). Kicks off retrieval of the local + // IDs of these files, and then starts the sync loop. + void StartProcessingBacklog(); + + // Starts checking the existing pinned files to see if these are + // up-to-date. If stale files are detected, the local IDs of these files + // are added and the sync loop is started. + void StartCheckingExistingPinnedFiles(); + + // Sets a delay for testing. + void set_delay_for_testing(const base::TimeDelta& delay) { + delay_ = delay; + } + + private: + // Types of sync tasks. + enum SyncType { + FETCH, // Fetch a file from the Drive server. + UPDATE, // Updates an entry's metadata or content on the Drive server. + }; + + // States of sync tasks. + enum SyncState { + SUSPENDED, // Task is currently inactive. + PENDING, // Task is going to run. + RUNNING, // Task is running. + }; + + typedef std::pair<SyncType, std::string> SyncTaskKey; + + struct SyncTask { + SyncTask(); + ~SyncTask(); + SyncState state; + ClientContext context; + base::Callback<base::Closure(const ClientContext& context)> task; + bool should_run_again; + base::Closure cancel_closure; + std::vector<SyncTaskKey> dependent_tasks; + std::vector<FileOperationCallback> waiting_callbacks; + }; + + typedef std::map<SyncTaskKey, SyncTask> SyncTasks; + + // Performs a FETCH task. + base::Closure PerformFetchTask(const std::string& local_id, + const ClientContext& context); + + // Adds a FETCH task. + void AddFetchTaskInternal(const std::string& local_id, + const base::TimeDelta& delay); + + // Performs a UPDATE task. + base::Closure PerformUpdateTask(const std::string& local_id, + const ClientContext& context); + + // Adds a UPDATE task. + void AddUpdateTaskInternal(const ClientContext& context, + const std::string& local_id, + const base::TimeDelta& delay); + + // Adds the given task. If the same task is found, does nothing. + void AddTask(const SyncTasks::key_type& key, + const SyncTask& task, + const base::TimeDelta& delay); + + // Called when a task is ready to start. + void StartTask(const SyncTasks::key_type& key); + void StartTaskAfterGetParentResourceEntry(const SyncTasks::key_type& key, + const ResourceEntry* parent, + FileError error); + + // Called when the local IDs of files in the backlog are obtained. + void OnGetLocalIdsOfBacklog(const std::vector<std::string>* to_fetch, + const std::vector<std::string>* to_update); + + // Adds fetch tasks. + void AddFetchTasks(const std::vector<std::string>* local_ids); + + // Called when a task is completed. + void OnTaskComplete(SyncType type, + const std::string& local_id, + FileError error); + + // Called when the file for |local_id| is fetched. + void OnFetchFileComplete(const std::string& local_id, + FileError error, + const base::FilePath& local_path, + scoped_ptr<ResourceEntry> entry); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + file_system::OperationDelegate* operation_delegate_; + ResourceMetadata* metadata_; + FileCache* cache_; + + // Used to fetch pinned files. + scoped_ptr<file_system::DownloadOperation> download_operation_; + + // Used to update entry metadata. + scoped_ptr<EntryUpdatePerformer> entry_update_performer_; + + // Sync tasks to be processed. + SyncTasks tasks_; + + // The delay is used for delaying processing tasks in AddTask(). + base::TimeDelta delay_; + + // The delay is used for delaying retry of tasks on server errors. + base::TimeDelta long_delay_; + + 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<SyncClient> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(SyncClient); +}; + +} // namespace internal +} // namespace drive + +#endif // COMPONENTS_DRIVE_SYNC_CLIENT_H_ diff --git a/components/drive/sync_client_unittest.cc b/components/drive/sync_client_unittest.cc new file mode 100644 index 0000000..250b85d --- /dev/null +++ b/components/drive/sync_client_unittest.cc @@ -0,0 +1,523 @@ +// 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/sync_client.h" + +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/scoped_ptr.h" +#include "base/prefs/testing_pref_service.h" +#include "base/run_loop.h" +#include "base/single_thread_task_runner.h" +#include "base/test/test_timeouts.h" +#include "base/thread_task_runner_handle.h" +#include "components/drive/change_list_loader.h" +#include "components/drive/drive.pb.h" +#include "components/drive/drive_test_util.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/move_operation.h" +#include "components/drive/file_system/operation_delegate.h" +#include "components/drive/file_system/remove_operation.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 "components/drive/service/fake_drive_service.h" +#include "content/public/test/test_browser_thread_bundle.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 internal { + +namespace { + +// The content of files initially stored in the cache. +const char kLocalContent[] = "Hello!"; + +// The content of files stored in the service. +const char kRemoteContent[] = "World!"; + +// SyncClientTestDriveService will return DRIVE_CANCELLED when a request is +// made with the specified resource ID. +class SyncClientTestDriveService : public ::drive::FakeDriveService { + public: + SyncClientTestDriveService() : download_file_count_(0) {} + + // FakeDriveService override: + google_apis::CancelCallback DownloadFile( + const base::FilePath& local_cache_path, + const std::string& resource_id, + const google_apis::DownloadActionCallback& download_action_callback, + const google_apis::GetContentCallback& get_content_callback, + const google_apis::ProgressCallback& progress_callback) override { + ++download_file_count_; + if (resource_id == resource_id_to_be_cancelled_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(download_action_callback, + google_apis::DRIVE_CANCELLED, + base::FilePath())); + return google_apis::CancelCallback(); + } + if (resource_id == resource_id_to_be_paused_) { + paused_action_ = base::Bind(download_action_callback, + google_apis::DRIVE_OTHER_ERROR, + base::FilePath()); + return google_apis::CancelCallback(); + } + return FakeDriveService::DownloadFile(local_cache_path, + resource_id, + download_action_callback, + get_content_callback, + progress_callback); + } + + int download_file_count() const { return download_file_count_; } + + void set_resource_id_to_be_cancelled(const std::string& resource_id) { + resource_id_to_be_cancelled_ = resource_id; + } + + void set_resource_id_to_be_paused(const std::string& resource_id) { + resource_id_to_be_paused_ = resource_id; + } + + const base::Closure& paused_action() const { return paused_action_; } + + private: + int download_file_count_; + std::string resource_id_to_be_cancelled_; + std::string resource_id_to_be_paused_; + base::Closure paused_action_; +}; + +} // namespace + +class SyncClientTest : public testing::Test { + public: + void SetUp() override { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + + pref_service_.reset(new TestingPrefServiceSimple); + test_util::RegisterDrivePrefs(pref_service_->registry()); + + fake_network_change_notifier_.reset( + new test_util::FakeNetworkChangeNotifier); + + logger_.reset(new EventLogger); + + drive_service_.reset(new SyncClientTestDriveService); + + scheduler_.reset(new JobScheduler( + pref_service_.get(), + logger_.get(), + drive_service_.get(), + base::ThreadTaskRunnerHandle::Get().get())); + + metadata_storage_.reset(new ResourceMetadataStorage( + temp_dir_.path(), base::ThreadTaskRunnerHandle::Get().get())); + ASSERT_TRUE(metadata_storage_->Initialize()); + + cache_.reset(new FileCache(metadata_storage_.get(), + temp_dir_.path(), + base::ThreadTaskRunnerHandle::Get().get(), + NULL /* free_disk_space_getter */)); + ASSERT_TRUE(cache_->Initialize()); + + metadata_.reset(new internal::ResourceMetadata( + metadata_storage_.get(), cache_.get(), + base::ThreadTaskRunnerHandle::Get())); + ASSERT_EQ(FILE_ERROR_OK, metadata_->Initialize()); + + about_resource_loader_.reset(new AboutResourceLoader(scheduler_.get())); + loader_controller_.reset(new LoaderController); + change_list_loader_.reset(new ChangeListLoader( + logger_.get(), + base::ThreadTaskRunnerHandle::Get().get(), + metadata_.get(), + scheduler_.get(), + about_resource_loader_.get(), + loader_controller_.get())); + ASSERT_NO_FATAL_FAILURE(SetUpTestData()); + + sync_client_.reset(new SyncClient(base::ThreadTaskRunnerHandle::Get().get(), + &delegate_, + scheduler_.get(), + metadata_.get(), + cache_.get(), + loader_controller_.get(), + temp_dir_.path())); + + // Disable delaying so that DoSyncLoop() starts immediately. + sync_client_->set_delay_for_testing(base::TimeDelta::FromSeconds(0)); + } + + // Adds a file to the service root and |resource_ids_|. + void AddFileEntry(const std::string& title) { + google_apis::DriveApiErrorCode error = google_apis::DRIVE_FILE_ERROR; + scoped_ptr<google_apis::FileResource> entry; + drive_service_->AddNewFile( + "text/plain", + kRemoteContent, + drive_service_->GetRootResourceId(), + title, + false, // shared_with_me + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + ASSERT_EQ(google_apis::HTTP_CREATED, error); + ASSERT_TRUE(entry); + resource_ids_[title] = entry->file_id(); + } + + // Sets up data for tests. + void SetUpTestData() { + // Prepare a temp file. + base::FilePath temp_file; + EXPECT_TRUE(base::CreateTemporaryFileInDir(temp_dir_.path(), &temp_file)); + ASSERT_TRUE(google_apis::test_util::WriteStringToFile(temp_file, + kLocalContent)); + + // Add file entries to the service. + ASSERT_NO_FATAL_FAILURE(AddFileEntry("foo")); + ASSERT_NO_FATAL_FAILURE(AddFileEntry("bar")); + ASSERT_NO_FATAL_FAILURE(AddFileEntry("baz")); + ASSERT_NO_FATAL_FAILURE(AddFileEntry("fetched")); + ASSERT_NO_FATAL_FAILURE(AddFileEntry("dirty")); + ASSERT_NO_FATAL_FAILURE(AddFileEntry("removed")); + ASSERT_NO_FATAL_FAILURE(AddFileEntry("moved")); + + // Load data from the service to the metadata. + FileError error = FILE_ERROR_FAILED; + change_list_loader_->LoadIfNeeded( + google_apis::test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Prepare 3 pinned-but-not-present files. + EXPECT_EQ(FILE_ERROR_OK, cache_->Pin(GetLocalId("foo"))); + EXPECT_EQ(FILE_ERROR_OK, cache_->Pin(GetLocalId("bar"))); + EXPECT_EQ(FILE_ERROR_OK, cache_->Pin(GetLocalId("baz"))); + + // Prepare a pinned-and-fetched file. + const std::string md5_fetched = "md5"; + EXPECT_EQ(FILE_ERROR_OK, + cache_->Store(GetLocalId("fetched"), md5_fetched, + temp_file, FileCache::FILE_OPERATION_COPY)); + EXPECT_EQ(FILE_ERROR_OK, cache_->Pin(GetLocalId("fetched"))); + + // Prepare a pinned-and-fetched-and-dirty file. + EXPECT_EQ(FILE_ERROR_OK, + cache_->Store(GetLocalId("dirty"), std::string(), + temp_file, FileCache::FILE_OPERATION_COPY)); + EXPECT_EQ(FILE_ERROR_OK, cache_->Pin(GetLocalId("dirty"))); + + // Prepare a removed file. + file_system::RemoveOperation remove_operation( + base::ThreadTaskRunnerHandle::Get().get(), &delegate_, metadata_.get(), + cache_.get()); + remove_operation.Remove( + util::GetDriveMyDriveRootPath().AppendASCII("removed"), + false, // is_recursive + google_apis::test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + + // Prepare a moved file. + file_system::MoveOperation move_operation( + base::ThreadTaskRunnerHandle::Get().get(), &delegate_, metadata_.get()); + move_operation.Move( + util::GetDriveMyDriveRootPath().AppendASCII("moved"), + util::GetDriveMyDriveRootPath().AppendASCII("moved_new_title"), + google_apis::test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(FILE_ERROR_OK, error); + } + + protected: + std::string GetLocalId(const std::string& title) { + EXPECT_EQ(1U, resource_ids_.count(title)); + std::string local_id; + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetIdByResourceId(resource_ids_[title], &local_id)); + return local_id; + } + + content::TestBrowserThreadBundle thread_bundle_; + base::ScopedTempDir temp_dir_; + scoped_ptr<TestingPrefServiceSimple> pref_service_; + scoped_ptr<test_util::FakeNetworkChangeNotifier> + fake_network_change_notifier_; + scoped_ptr<EventLogger> logger_; + scoped_ptr<SyncClientTestDriveService> drive_service_; + file_system::OperationDelegate delegate_; + scoped_ptr<JobScheduler> scheduler_; + scoped_ptr<ResourceMetadataStorage, + test_util::DestroyHelperForTests> metadata_storage_; + scoped_ptr<FileCache, test_util::DestroyHelperForTests> cache_; + scoped_ptr<ResourceMetadata, test_util::DestroyHelperForTests> metadata_; + scoped_ptr<AboutResourceLoader> about_resource_loader_; + scoped_ptr<LoaderController> loader_controller_; + scoped_ptr<ChangeListLoader> change_list_loader_; + scoped_ptr<SyncClient> sync_client_; + + std::map<std::string, std::string> resource_ids_; // Name-to-id map. +}; + +TEST_F(SyncClientTest, StartProcessingBacklog) { + sync_client_->StartProcessingBacklog(); + base::RunLoop().RunUntilIdle(); + + ResourceEntry entry; + // Pinned files get downloaded. + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryById(GetLocalId("foo"), &entry)); + EXPECT_TRUE(entry.file_specific_info().cache_state().is_present()); + + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryById(GetLocalId("bar"), &entry)); + EXPECT_TRUE(entry.file_specific_info().cache_state().is_present()); + + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryById(GetLocalId("baz"), &entry)); + EXPECT_TRUE(entry.file_specific_info().cache_state().is_present()); + + // Dirty file gets uploaded. + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryById(GetLocalId("dirty"), &entry)); + EXPECT_FALSE(entry.file_specific_info().cache_state().is_dirty()); + + // Removed entry is not found. + google_apis::DriveApiErrorCode status = google_apis::DRIVE_OTHER_ERROR; + scoped_ptr<google_apis::FileResource> server_entry; + drive_service_->GetFileResource( + resource_ids_["removed"], + google_apis::test_util::CreateCopyResultCallback(&status, &server_entry)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, status); + ASSERT_TRUE(server_entry); + EXPECT_TRUE(server_entry->labels().is_trashed()); + + // Moved entry was moved. + status = google_apis::DRIVE_OTHER_ERROR; + drive_service_->GetFileResource( + resource_ids_["moved"], + google_apis::test_util::CreateCopyResultCallback(&status, &server_entry)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(google_apis::HTTP_SUCCESS, status); + ASSERT_TRUE(server_entry); + EXPECT_EQ("moved_new_title", server_entry->title()); +} + +TEST_F(SyncClientTest, AddFetchTask) { + sync_client_->AddFetchTask(GetLocalId("foo")); + base::RunLoop().RunUntilIdle(); + + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryById(GetLocalId("foo"), &entry)); + EXPECT_TRUE(entry.file_specific_info().cache_state().is_present()); +} + +TEST_F(SyncClientTest, AddFetchTaskAndCancelled) { + // Trigger fetching of a file which results in cancellation. + drive_service_->set_resource_id_to_be_cancelled(resource_ids_["foo"]); + sync_client_->AddFetchTask(GetLocalId("foo")); + base::RunLoop().RunUntilIdle(); + + // The file should be unpinned if the user wants the download to be cancelled. + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryById(GetLocalId("foo"), &entry)); + EXPECT_FALSE(entry.file_specific_info().cache_state().is_pinned()); +} + +TEST_F(SyncClientTest, RemoveFetchTask) { + sync_client_->AddFetchTask(GetLocalId("foo")); + sync_client_->AddFetchTask(GetLocalId("bar")); + sync_client_->AddFetchTask(GetLocalId("baz")); + + sync_client_->RemoveFetchTask(GetLocalId("foo")); + sync_client_->RemoveFetchTask(GetLocalId("baz")); + base::RunLoop().RunUntilIdle(); + + // Only "bar" should be fetched. + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryById(GetLocalId("foo"), &entry)); + EXPECT_FALSE(entry.file_specific_info().cache_state().is_present()); + + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryById(GetLocalId("bar"), &entry)); + EXPECT_TRUE(entry.file_specific_info().cache_state().is_present()); + + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryById(GetLocalId("baz"), &entry)); + EXPECT_FALSE(entry.file_specific_info().cache_state().is_present()); + +} + +TEST_F(SyncClientTest, ExistingPinnedFiles) { + // Start checking the existing pinned files. This will collect the resource + // IDs of pinned files, with stale local cache files. + sync_client_->StartCheckingExistingPinnedFiles(); + base::RunLoop().RunUntilIdle(); + + // "fetched" and "dirty" are the existing pinned files. + // The non-dirty one should be synced, but the dirty one should not. + base::FilePath cache_file; + std::string content; + EXPECT_EQ(FILE_ERROR_OK, cache_->GetFile(GetLocalId("fetched"), &cache_file)); + EXPECT_TRUE(base::ReadFileToString(cache_file, &content)); + EXPECT_EQ(kRemoteContent, content); + content.clear(); + + EXPECT_EQ(FILE_ERROR_OK, cache_->GetFile(GetLocalId("dirty"), &cache_file)); + EXPECT_TRUE(base::ReadFileToString(cache_file, &content)); + EXPECT_EQ(kLocalContent, content); +} + +TEST_F(SyncClientTest, RetryOnDisconnection) { + // Let the service go down. + drive_service_->set_offline(true); + // Change the network connection state after some delay, to test that + // FILE_ERROR_NO_CONNECTION is handled by SyncClient correctly. + // Without this delay, JobScheduler will keep the jobs unrun and SyncClient + // will receive no error. + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::Bind(&test_util::FakeNetworkChangeNotifier::SetConnectionType, + base::Unretained(fake_network_change_notifier_.get()), + net::NetworkChangeNotifier::CONNECTION_NONE), + TestTimeouts::tiny_timeout()); + + // Try fetch and upload. + sync_client_->AddFetchTask(GetLocalId("foo")); + sync_client_->AddUpdateTask(ClientContext(USER_INITIATED), + GetLocalId("dirty")); + base::RunLoop().RunUntilIdle(); + + // Not yet fetched nor uploaded. + ResourceEntry entry; + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryById(GetLocalId("foo"), &entry)); + EXPECT_FALSE(entry.file_specific_info().cache_state().is_present()); + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryById(GetLocalId("dirty"), &entry)); + EXPECT_TRUE(entry.file_specific_info().cache_state().is_dirty()); + + // Switch to online. + fake_network_change_notifier_->SetConnectionType( + net::NetworkChangeNotifier::CONNECTION_WIFI); + drive_service_->set_offline(false); + base::RunLoop().RunUntilIdle(); + + // Fetched and uploaded. + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryById(GetLocalId("foo"), &entry)); + EXPECT_TRUE(entry.file_specific_info().cache_state().is_present()); + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryById(GetLocalId("dirty"), &entry)); + EXPECT_FALSE(entry.file_specific_info().cache_state().is_dirty()); +} + +TEST_F(SyncClientTest, ScheduleRerun) { + // Add a fetch task for "foo", this should result in being paused. + drive_service_->set_resource_id_to_be_paused(resource_ids_["foo"]); + sync_client_->AddFetchTask(GetLocalId("foo")); + base::RunLoop().RunUntilIdle(); + + // While the first task is paused, add a task again. + // This results in scheduling rerun of the task. + sync_client_->AddFetchTask(GetLocalId("foo")); + base::RunLoop().RunUntilIdle(); + + // Resume the paused task. + drive_service_->set_resource_id_to_be_paused(std::string()); + ASSERT_FALSE(drive_service_->paused_action().is_null()); + drive_service_->paused_action().Run(); + base::RunLoop().RunUntilIdle(); + + // Task should be run twice. + EXPECT_EQ(2, drive_service_->download_file_count()); +} + +TEST_F(SyncClientTest, Dependencies) { + // Create directories locally. + const base::FilePath kPath1(FILE_PATH_LITERAL("drive/root/dir1")); + const base::FilePath kPath2 = kPath1.AppendASCII("dir2"); + + ResourceEntry parent; + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryByPath(kPath1.DirName(), &parent)); + + ResourceEntry entry1; + entry1.set_parent_local_id(parent.local_id()); + entry1.set_title(kPath1.BaseName().AsUTF8Unsafe()); + entry1.mutable_file_info()->set_is_directory(true); + entry1.set_metadata_edit_state(ResourceEntry::DIRTY); + std::string local_id1; + EXPECT_EQ(FILE_ERROR_OK, metadata_->AddEntry(entry1, &local_id1)); + + ResourceEntry entry2; + entry2.set_parent_local_id(local_id1); + entry2.set_title(kPath2.BaseName().AsUTF8Unsafe()); + entry2.mutable_file_info()->set_is_directory(true); + entry2.set_metadata_edit_state(ResourceEntry::DIRTY); + std::string local_id2; + EXPECT_EQ(FILE_ERROR_OK, metadata_->AddEntry(entry2, &local_id2)); + + // Start syncing the child first. + sync_client_->AddUpdateTask(ClientContext(USER_INITIATED), local_id2); + // Start syncing the parent later. + sync_client_->AddUpdateTask(ClientContext(USER_INITIATED), local_id1); + base::RunLoop().RunUntilIdle(); + + // Both entries are synced. + EXPECT_EQ(FILE_ERROR_OK, metadata_->GetResourceEntryById(local_id1, &entry1)); + EXPECT_EQ(ResourceEntry::CLEAN, entry1.metadata_edit_state()); + EXPECT_EQ(FILE_ERROR_OK, metadata_->GetResourceEntryById(local_id2, &entry2)); + EXPECT_EQ(ResourceEntry::CLEAN, entry2.metadata_edit_state()); +} + +TEST_F(SyncClientTest, WaitForUpdateTaskToComplete) { + // Create a directory locally. + const base::FilePath kPath(FILE_PATH_LITERAL("drive/root/dir1")); + + ResourceEntry parent; + EXPECT_EQ(FILE_ERROR_OK, + metadata_->GetResourceEntryByPath(kPath.DirName(), &parent)); + + ResourceEntry entry; + entry.set_parent_local_id(parent.local_id()); + entry.set_title(kPath.BaseName().AsUTF8Unsafe()); + entry.mutable_file_info()->set_is_directory(true); + entry.set_metadata_edit_state(ResourceEntry::DIRTY); + std::string local_id; + EXPECT_EQ(FILE_ERROR_OK, metadata_->AddEntry(entry, &local_id)); + + // Sync task is not yet avialable. + FileError error = FILE_ERROR_FAILED; + EXPECT_FALSE(sync_client_->WaitForUpdateTaskToComplete( + local_id, google_apis::test_util::CreateCopyResultCallback(&error))); + + // Start syncing the directory and wait for it to complete. + sync_client_->AddUpdateTask(ClientContext(USER_INITIATED), local_id); + + EXPECT_TRUE(sync_client_->WaitForUpdateTaskToComplete( + local_id, google_apis::test_util::CreateCopyResultCallback(&error))); + + base::RunLoop().RunUntilIdle(); + + // The callback is called. + EXPECT_EQ(FILE_ERROR_OK, error); +} + +} // namespace internal +} // namespace drive |