Wait for parent directory sync before performing server-side copy

Add SyncClient::WaitForUpdateTaskToComplete().
Use the new method from CoypOperation.

BUG=384213
TEST=unit_tests

Review URL: https://codereview.chromium.org/372713004

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@284877 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/browser/chromeos/drive/file_system.cc b/chrome/browser/chromeos/drive/file_system.cc
index 3665383..1669177e 100644
--- a/chrome/browser/chromeos/drive/file_system.cc
+++ b/chrome/browser/chromeos/drive/file_system.cc
@@ -827,6 +827,11 @@
                     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(BrowserThread::CurrentlyOn(BrowserThread::UI));
 
diff --git a/chrome/browser/chromeos/drive/file_system.h b/chrome/browser/chromeos/drive/file_system.h
index f16c7bf..b4e14d6 100644
--- a/chrome/browser/chromeos/drive/file_system.h
+++ b/chrome/browser/chromeos/drive/file_system.h
@@ -168,6 +168,9 @@
   virtual void OnEntryUpdatedByOperation(const std::string& local_id) OVERRIDE;
   virtual void OnDriveSyncError(file_system::DriveSyncErrorType type,
                                 const std::string& local_id) OVERRIDE;
+  virtual bool WaitForSyncComplete(
+      const std::string& local_id,
+      const FileOperationCallback& callback) OVERRIDE;
 
   // ChangeListLoader::Observer overrides.
   // Used to propagate events from ChangeListLoader.
diff --git a/chrome/browser/chromeos/drive/file_system/copy_operation.cc b/chrome/browser/chromeos/drive/file_system/copy_operation.cc
index 0e42f7b..a0055f86 100644
--- a/chrome/browser/chromeos/drive/file_system/copy_operation.cc
+++ b/chrome/browser/chromeos/drive/file_system/copy_operation.cc
@@ -351,21 +351,66 @@
     return;
   }
 
-  base::FilePath new_title = params->dest_file_path.BaseName();
-  if (params->src_entry.file_specific_info().is_hosted_document()) {
+  if (params->parent_entry.resource_id().empty()) {
+    // Parent entry may be being synced.
+    const bool waiting = observer_->WaitForSyncComplete(
+        params->parent_entry.local_id(),
+        base::Bind(&CopyOperation::CopyAfterParentSync,
+                   weak_ptr_factory_.GetWeakPtr(), *params));
+    if (!waiting)
+      params->callback.Run(FILE_ERROR_NOT_FOUND);
+  } else {
+    CopyAfterGetParentResourceId(*params, &params->parent_entry, FILE_ERROR_OK);
+  }
+}
+
+void CopyOperation::CopyAfterParentSync(const CopyParams& params,
+                                        FileError error) {
+  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+  DCHECK(!params.callback.is_null());
+
+  if (error != FILE_ERROR_OK) {
+    params.callback.Run(error);
+    return;
+  }
+
+  ResourceEntry* parent = new ResourceEntry;
+  base::PostTaskAndReplyWithResult(
+      blocking_task_runner_,
+      FROM_HERE,
+      base::Bind(&internal::ResourceMetadata::GetResourceEntryById,
+                 base::Unretained(metadata_),
+                 params.parent_entry.local_id(), parent),
+      base::Bind(&CopyOperation::CopyAfterGetParentResourceId,
+                 weak_ptr_factory_.GetWeakPtr(), params, base::Owned(parent)));
+}
+
+void CopyOperation::CopyAfterGetParentResourceId(const CopyParams& params,
+                                                 const ResourceEntry* parent,
+                                                 FileError error) {
+  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+  DCHECK(!params.callback.is_null());
+
+  if (error != FILE_ERROR_OK) {
+    params.callback.Run(error);
+    return;
+  }
+
+  base::FilePath new_title = params.dest_file_path.BaseName();
+  if (params.src_entry.file_specific_info().is_hosted_document()) {
     // Drop the document extension, which should not be in the title.
     // TODO(yoshiki): Remove this code with crbug.com/223304.
     new_title = new_title.RemoveExtension();
   }
 
   base::Time last_modified =
-      params->preserve_last_modified ?
+      params.preserve_last_modified ?
       base::Time::FromInternalValue(
-          params->src_entry.file_info().last_modified()) : base::Time();
+          params.src_entry.file_info().last_modified()) : base::Time();
 
   CopyResourceOnServer(
-      params->src_entry.resource_id(), params->parent_entry.resource_id(),
-      new_title.AsUTF8Unsafe(), last_modified, params->callback);
+      params.src_entry.resource_id(), parent->resource_id(),
+      new_title.AsUTF8Unsafe(), last_modified, params.callback);
 }
 
 void CopyOperation::TransferFileFromLocalToRemote(
diff --git a/chrome/browser/chromeos/drive/file_system/copy_operation.h b/chrome/browser/chromeos/drive/file_system/copy_operation.h
index e0c2385..ea9df03 100644
--- a/chrome/browser/chromeos/drive/file_system/copy_operation.h
+++ b/chrome/browser/chromeos/drive/file_system/copy_operation.h
@@ -89,6 +89,14 @@
       const bool* should_copy_on_server,
       FileError error);
 
+  // Part of Copy(). Called after the parent entry gets synced.
+  void CopyAfterParentSync(const CopyParams& params, FileError error);
+
+  // Part of Copy(). Called after the parent resource ID is resolved.
+  void CopyAfterGetParentResourceId(const CopyParams& params,
+                                    const ResourceEntry* parent,
+                                    FileError error);
+
   // Part of TransferFileFromLocalToRemote(). Called after preparation is done.
   // |gdoc_resource_id| and |parent_resource_id| is available only if the file
   // is JSON GDoc file.
diff --git a/chrome/browser/chromeos/drive/file_system/copy_operation_unittest.cc b/chrome/browser/chromeos/drive/file_system/copy_operation_unittest.cc
index 85e60567..5d4fa1cc 100644
--- a/chrome/browser/chromeos/drive/file_system/copy_operation_unittest.cc
+++ b/chrome/browser/chromeos/drive/file_system/copy_operation_unittest.cc
@@ -10,6 +10,7 @@
 #include "chrome/browser/chromeos/drive/file_change.h"
 #include "chrome/browser/chromeos/drive/file_system/operation_test_base.h"
 #include "chrome/browser/chromeos/drive/file_system_util.h"
+#include "chrome/browser/chromeos/drive/resource_metadata.h"
 #include "chrome/browser/drive/drive_api_util.h"
 #include "chrome/browser/drive/fake_drive_service.h"
 #include "content/public/test/test_utils.h"
@@ -20,6 +21,20 @@
 namespace drive {
 namespace file_system {
 
+namespace {
+
+// Used to handle WaitForSyncComplete() calls.
+bool CopyWaitForSyncCompleteArguments(std::string* out_local_id,
+                                      FileOperationCallback* out_callback,
+                                      const std::string& local_id,
+                                      const FileOperationCallback& callback) {
+  *out_local_id = local_id;
+  *out_callback = callback;
+  return true;
+}
+
+}  // namespace
+
 class CopyOperationTest : public OperationTestBase {
  protected:
   virtual void SetUp() OVERRIDE {
@@ -426,5 +441,83 @@
             entry2.file_info().last_modified());
 }
 
+TEST_F(CopyOperationTest, WaitForSyncComplete) {
+  // Create a directory locally.
+  base::FilePath src_path(FILE_PATH_LITERAL("drive/root/File 1.txt"));
+  base::FilePath directory_path(FILE_PATH_LITERAL("drive/root/New Directory"));
+  base::FilePath dest_path = directory_path.AppendASCII("File 1.txt");
+
+  ResourceEntry directory_parent;
+  EXPECT_EQ(FILE_ERROR_OK,
+            GetLocalResourceEntry(directory_path.DirName(), &directory_parent));
+
+  ResourceEntry directory;
+  directory.set_parent_local_id(directory_parent.local_id());
+  directory.set_title(directory_path.BaseName().AsUTF8Unsafe());
+  directory.mutable_file_info()->set_is_directory(true);
+  directory.set_metadata_edit_state(ResourceEntry::DIRTY);
+
+  std::string directory_local_id;
+  FileError error = FILE_ERROR_FAILED;
+  base::PostTaskAndReplyWithResult(
+      blocking_task_runner(),
+      FROM_HERE,
+      base::Bind(&internal::ResourceMetadata::AddEntry,
+                 base::Unretained(metadata()), directory, &directory_local_id),
+      google_apis::test_util::CreateCopyResultCallback(&error));
+  content::RunAllBlockingPoolTasksUntilIdle();
+  EXPECT_EQ(FILE_ERROR_OK, error);
+
+  // Try to copy a file to the new directory which lacks resource ID.
+  // This should result in waiting for the directory to sync.
+  std::string waited_local_id;
+  FileOperationCallback pending_callback;
+  observer()->set_wait_for_sync_complete_handler(
+      base::Bind(&CopyWaitForSyncCompleteArguments,
+                 &waited_local_id, &pending_callback));
+
+  FileError copy_error = FILE_ERROR_FAILED;
+  operation_->Copy(src_path,
+                   dest_path,
+                   true,  // Preserve last modified.
+                   google_apis::test_util::CreateCopyResultCallback(
+                       &copy_error));
+  content::RunAllBlockingPoolTasksUntilIdle();
+  EXPECT_EQ(directory_local_id, waited_local_id);
+  ASSERT_FALSE(pending_callback.is_null());
+
+  // Add a new directory to the server and store the resource ID locally.
+  google_apis::GDataErrorCode status = google_apis::GDATA_OTHER_ERROR;
+  scoped_ptr<google_apis::FileResource> file_resource;
+  fake_service()->AddNewDirectory(
+      directory_parent.resource_id(),
+      directory.title(),
+      DriveServiceInterface::AddNewDirectoryOptions(),
+      google_apis::test_util::CreateCopyResultCallback(
+          &status, &file_resource));
+  content::RunAllBlockingPoolTasksUntilIdle();
+  EXPECT_EQ(google_apis::HTTP_CREATED, status);
+  ASSERT_TRUE(file_resource);
+
+  directory.set_local_id(directory_local_id);
+  directory.set_resource_id(file_resource->file_id());
+  base::PostTaskAndReplyWithResult(
+      blocking_task_runner(),
+      FROM_HERE,
+      base::Bind(&internal::ResourceMetadata::RefreshEntry,
+                 base::Unretained(metadata()), directory),
+      google_apis::test_util::CreateCopyResultCallback(&error));
+  content::RunAllBlockingPoolTasksUntilIdle();
+  EXPECT_EQ(FILE_ERROR_OK, error);
+
+  // Resume the copy operation.
+  pending_callback.Run(FILE_ERROR_OK);
+  content::RunAllBlockingPoolTasksUntilIdle();
+
+  EXPECT_EQ(FILE_ERROR_OK, copy_error);
+  ResourceEntry entry;
+  EXPECT_EQ(FILE_ERROR_OK, GetLocalResourceEntry(dest_path, &entry));
+}
+
 }  // namespace file_system
 }  // namespace drive
diff --git a/chrome/browser/chromeos/drive/file_system/operation_observer.cc b/chrome/browser/chromeos/drive/file_system/operation_observer.cc
new file mode 100644
index 0000000..79f0f86
--- /dev/null
+++ b/chrome/browser/chromeos/drive/file_system/operation_observer.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 "chrome/browser/chromeos/drive/file_system/operation_observer.h"
+
+namespace drive {
+namespace file_system {
+
+bool OperationObserver::WaitForSyncComplete(
+    const std::string& local_id,
+    const FileOperationCallback& callback) {
+  return false;
+}
+
+}  // namespace file_system
+}  // namespace drive
diff --git a/chrome/browser/chromeos/drive/file_system/operation_observer.h b/chrome/browser/chromeos/drive/file_system/operation_observer.h
index 8fd8a1c40..ffd378c 100644
--- a/chrome/browser/chromeos/drive/file_system/operation_observer.h
+++ b/chrome/browser/chromeos/drive/file_system/operation_observer.h
@@ -5,6 +5,8 @@
 #ifndef CHROME_BROWSER_CHROMEOS_DRIVE_FILE_SYSTEM_OPERATION_OBSERVER_H_
 #define CHROME_BROWSER_CHROMEOS_DRIVE_FILE_SYSTEM_OPERATION_OBSERVER_H_
 
+#include "chrome/browser/chromeos/drive/file_errors.h"
+
 namespace base {
 class FilePath;
 }
@@ -26,6 +28,7 @@
 };
 
 // Passes notifications from Drive operations back to the file system.
+// TODO(hashimoto): Give this class a more appropriate name.
 class OperationObserver {
  public:
   // Sent when a content of a directory has been changed.
@@ -40,6 +43,11 @@
   // |local_id| is the local ID of the resource entry.
   virtual void OnDriveSyncError(DriveSyncErrorType type,
                                 const std::string& local_id) {}
+
+  // Waits for the sync task to complete and runs the callback.
+  // Returns false if no task is found for the spcecified ID.
+  virtual bool WaitForSyncComplete(const std::string& local_id,
+                                   const FileOperationCallback& callback);
 };
 
 }  // namespace file_system
diff --git a/chrome/browser/chromeos/drive/file_system/operation_test_base.cc b/chrome/browser/chromeos/drive/file_system/operation_test_base.cc
index 65af4b6d..9950bca 100644
--- a/chrome/browser/chromeos/drive/file_system/operation_test_base.cc
+++ b/chrome/browser/chromeos/drive/file_system/operation_test_base.cc
@@ -44,6 +44,13 @@
   drive_sync_errors_.push_back(type);
 }
 
+bool OperationTestBase::LoggingObserver::WaitForSyncComplete(
+    const std::string& local_id,
+    const FileOperationCallback& callback) {
+  return wait_for_sync_complete_handler_.is_null() ?
+      false : wait_for_sync_complete_handler_.Run(local_id, callback);
+}
+
 OperationTestBase::OperationTestBase() {
 }
 
diff --git a/chrome/browser/chromeos/drive/file_system/operation_test_base.h b/chrome/browser/chromeos/drive/file_system/operation_test_base.h
index 26f9d023..7a657e5 100644
--- a/chrome/browser/chromeos/drive/file_system/operation_test_base.h
+++ b/chrome/browser/chromeos/drive/file_system/operation_test_base.h
@@ -48,6 +48,10 @@
   // OperationObserver that records all the events.
   class LoggingObserver : public OperationObserver {
    public:
+    typedef base::Callback<bool(
+        const std::string& local_id,
+        const FileOperationCallback& callback)> WaitForSyncCompleteHandler;
+
     LoggingObserver();
     ~LoggingObserver();
 
@@ -58,6 +62,9 @@
         const std::string& local_id) OVERRIDE;
     virtual void OnDriveSyncError(DriveSyncErrorType type,
                                   const std::string& local_id) OVERRIDE;
+    virtual bool WaitForSyncComplete(
+        const std::string& local_id,
+        const FileOperationCallback& callback) OVERRIDE;
 
     // Gets the set of changed paths.
     const FileChange& get_changed_files() { return changed_files_; }
@@ -72,10 +79,17 @@
       return drive_sync_errors_;
     }
 
+    // Sets the callback used to handle WaitForSyncComplete() method calls.
+    void set_wait_for_sync_complete_handler(
+        const WaitForSyncCompleteHandler& wait_for_sync_complete_handler) {
+      wait_for_sync_complete_handler_ = wait_for_sync_complete_handler;
+    }
+
    private:
     FileChange changed_files_;
     std::set<std::string> updated_local_ids_;
     std::vector<DriveSyncErrorType> drive_sync_errors_;
+    WaitForSyncCompleteHandler wait_for_sync_complete_handler_;
   };
 
   OperationTestBase();
diff --git a/chrome/browser/chromeos/drive/sync_client.cc b/chrome/browser/chromeos/drive/sync_client.cc
index 52049d52..51b2b42 100644
--- a/chrome/browser/chromeos/drive/sync_client.cc
+++ b/chrome/browser/chromeos/drive/sync_client.cc
@@ -232,6 +232,20 @@
   AddUpdateTaskInternal(context, local_id, delay_);
 }
 
+bool SyncClient:: WaitForUpdateTaskToComplete(
+    const std::string& local_id,
+    const FileOperationCallback& callback) {
+  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+
+  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(BrowserThread::CurrentlyOn(BrowserThread::UI));
@@ -439,6 +453,12 @@
                    << ": " << FileErrorToString(error);
   }
 
+  for (size_t i = 0; i < it->second.waiting_callbacks.size(); ++i) {
+    base::MessageLoopProxy::current()->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;
diff --git a/chrome/browser/chromeos/drive/sync_client.h b/chrome/browser/chromeos/drive/sync_client.h
index 0efccd6..4fc5d1fe 100644
--- a/chrome/browser/chromeos/drive/sync_client.h
+++ b/chrome/browser/chromeos/drive/sync_client.h
@@ -67,6 +67,11 @@
   // 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.
@@ -82,9 +87,6 @@
     delay_ = delay;
   }
 
-  // Starts the sync loop if it's not running.
-  void StartSyncLoop();
-
  private:
   // Types of sync tasks.
   enum SyncType {
@@ -110,6 +112,7 @@
     bool should_run_again;
     base::Closure cancel_closure;
     std::vector<SyncTaskKey> dependent_tasks;
+    std::vector<FileOperationCallback> waiting_callbacks;
   };
 
   typedef std::map<SyncTaskKey, SyncTask> SyncTasks;
diff --git a/chrome/browser/chromeos/drive/sync_client_unittest.cc b/chrome/browser/chromeos/drive/sync_client_unittest.cc
index cb56dd28..31000814 100644
--- a/chrome/browser/chromeos/drive/sync_client_unittest.cc
+++ b/chrome/browser/chromeos/drive/sync_client_unittest.cc
@@ -489,5 +489,38 @@
   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