summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--chrome/browser/chromeos/drive/file_write_watcher.cc225
-rw-r--r--chrome/browser/chromeos/drive/file_write_watcher.h75
-rw-r--r--chrome/browser/chromeos/drive/file_write_watcher_unittest.cc119
-rw-r--r--chrome/chrome_browser_chromeos.gypi2
-rw-r--r--chrome/chrome_tests_unit.gypi1
5 files changed, 422 insertions, 0 deletions
diff --git a/chrome/browser/chromeos/drive/file_write_watcher.cc b/chrome/browser/chromeos/drive/file_write_watcher.cc
new file mode 100644
index 0000000..97c64d4
--- /dev/null
+++ b/chrome/browser/chromeos/drive/file_write_watcher.cc
@@ -0,0 +1,225 @@
+// 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 "chrome/browser/chromeos/drive/file_write_watcher.h"
+
+#include <map>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/files/file_path_watcher.h"
+#include "base/stl_util.h"
+#include "base/timer/timer.h"
+#include "chrome/browser/chromeos/drive/file_system/operation_observer.h"
+#include "chrome/browser/chromeos/drive/logging.h"
+#include "chrome/browser/google_apis/task_util.h"
+#include "content/public/browser/browser_thread.h"
+
+using content::BrowserThread;
+
+namespace drive {
+namespace internal {
+
+namespace {
+const int64 kWriteEventDelayInSeconds = 5;
+} // namespace
+
+// base::FileWatcher needs to live in a thread that is allowed to do File IO
+// and has a TYPE_IO message loop: that is, FILE thread. This class bridges the
+// UI thread and FILE thread, and does all the main tasks in the FILE thread.
+class FileWriteWatcher::FileWriteWatcherImpl {
+ public:
+ FileWriteWatcherImpl();
+
+ // Forwards the call to DestoryOnFileThread(). This method must be used to
+ // destruct the instance.
+ void Destroy();
+
+ // Forwards the call to StartWatchOnFileThread(). |callback| is called back
+ // on the caller (UI) thread when the watch has started.
+ // |on_write_event_callback| is called when a write has happened to the path.
+ void StartWatch(const base::FilePath& path,
+ const StartWatchCallback& callback,
+ const base::Closure& on_write_event_callback);
+
+ void set_delay(base::TimeDelta delay) { delay_ = delay; }
+
+ private:
+ ~FileWriteWatcherImpl();
+
+ void DestroyOnFileThread();
+
+ void StartWatchOnFileThread(const base::FilePath& path,
+ const StartWatchCallback& callback,
+ const base::Closure& on_write_event_callback);
+
+ void OnWriteEvent(const base::FilePath& path, bool error);
+
+ void InvokeCallback(const base::FilePath& path);
+
+ struct PathWatchInfo {
+ base::Closure on_write_event_callback;
+ base::FilePathWatcher watcher;
+ base::Timer timer;
+
+ explicit PathWatchInfo(const base::Closure& callback)
+ : on_write_event_callback(callback),
+ timer(false /* retain_closure_on_reset */, false /* is_repeating */) {
+ }
+ };
+
+ base::TimeDelta delay_;
+ std::map<base::FilePath, PathWatchInfo*> watchers_;
+
+ // Note: This should remain the last member so it'll be destroyed and
+ // invalidate its weak pointers before any other members are destroyed.
+ base::WeakPtrFactory<FileWriteWatcherImpl> weak_ptr_factory_;
+ DISALLOW_COPY_AND_ASSIGN(FileWriteWatcherImpl);
+};
+
+FileWriteWatcher::FileWriteWatcherImpl::FileWriteWatcherImpl()
+ : delay_(base::TimeDelta::FromSeconds(kWriteEventDelayInSeconds)),
+ weak_ptr_factory_(this) {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+}
+
+void FileWriteWatcher::FileWriteWatcherImpl::Destroy() {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+
+ // Just forwarding the call to FILE thread.
+ BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE)->PostTask(
+ FROM_HERE,
+ base::Bind(&FileWriteWatcherImpl::DestroyOnFileThread,
+ base::Unretained(this)));
+}
+
+void FileWriteWatcher::FileWriteWatcherImpl::StartWatch(
+ const base::FilePath& path,
+ const StartWatchCallback& callback,
+ const base::Closure& on_write_event_callback) {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+
+ // Forwarding the call to FILE thread and relaying the |callback|.
+ BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE)->PostTask(
+ FROM_HERE,
+ base::Bind(&FileWriteWatcherImpl::StartWatchOnFileThread,
+ base::Unretained(this),
+ path,
+ google_apis::CreateRelayCallback(callback),
+ google_apis::CreateRelayCallback(on_write_event_callback)));
+}
+
+FileWriteWatcher::FileWriteWatcherImpl::~FileWriteWatcherImpl() {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
+
+ STLDeleteContainerPairSecondPointers(watchers_.begin(), watchers_.end());
+}
+
+void FileWriteWatcher::FileWriteWatcherImpl::DestroyOnFileThread() {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
+
+ delete this;
+}
+
+void FileWriteWatcher::FileWriteWatcherImpl::StartWatchOnFileThread(
+ const base::FilePath& path,
+ const StartWatchCallback& callback,
+ const base::Closure& on_write_event_callback) {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
+ util::Log(logging::LOG_INFO, "Started watching modification to %s.",
+ path.AsUTF8Unsafe().c_str());
+
+ std::map<base::FilePath, PathWatchInfo*>::iterator it = watchers_.find(path);
+ if (it != watchers_.end()) {
+ // Do nothing if we are already watching the path.
+ callback.Run(true);
+ return;
+ }
+
+ // Start watching |path|.
+ scoped_ptr<PathWatchInfo> info(new PathWatchInfo(on_write_event_callback));
+ bool ok = info->watcher.Watch(
+ path,
+ false, // recursive
+ base::Bind(&FileWriteWatcherImpl::OnWriteEvent,
+ weak_ptr_factory_.GetWeakPtr()));
+ watchers_[path] = info.release();
+ callback.Run(ok);
+}
+
+void FileWriteWatcher::FileWriteWatcherImpl::OnWriteEvent(
+ const base::FilePath& path,
+ bool error) {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
+ util::Log(logging::LOG_INFO, "Detected modification to %s.",
+ path.AsUTF8Unsafe().c_str());
+
+ if (error)
+ return;
+
+ std::map<base::FilePath, PathWatchInfo*>::iterator it = watchers_.find(path);
+ DCHECK(it != watchers_.end());
+
+ // Heuristics for detecting the end of successive write operations.
+ // Delay running on_write_event_callback by |delay_| time, and if OnWriteEvent
+ // is called again in the period, the timer is reset. In other words, we
+ // invoke callback when |delay_| has passed after the last OnWriteEvent().
+ it->second->timer.Start(FROM_HERE,
+ delay_,
+ base::Bind(&FileWriteWatcherImpl::InvokeCallback,
+ weak_ptr_factory_.GetWeakPtr(),
+ path));
+}
+
+void FileWriteWatcher::FileWriteWatcherImpl::InvokeCallback(
+ const base::FilePath& path) {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
+ util::Log(logging::LOG_INFO, "Finished watching modification to %s.",
+ path.AsUTF8Unsafe().c_str());
+
+ std::map<base::FilePath, PathWatchInfo*>::iterator it = watchers_.find(path);
+ DCHECK(it != watchers_.end());
+
+ base::Closure callback = it->second->on_write_event_callback;
+ delete it->second;
+ watchers_.erase(it);
+
+ callback.Run();
+}
+
+FileWriteWatcher::FileWriteWatcher(file_system::OperationObserver* observer)
+ : watcher_impl_(new FileWriteWatcherImpl),
+ operation_observer_(observer),
+ weak_ptr_factory_(this) {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+}
+
+FileWriteWatcher::~FileWriteWatcher() {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+}
+
+void FileWriteWatcher::StartWatch(const base::FilePath& file_path,
+ const std::string& resource_id,
+ const StartWatchCallback& callback) {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+
+ watcher_impl_->StartWatch(file_path,
+ callback,
+ base::Bind(&FileWriteWatcher::OnWriteEvent,
+ weak_ptr_factory_.GetWeakPtr(),
+ resource_id));
+}
+
+void FileWriteWatcher::DisableDelayForTesting() {
+ watcher_impl_->set_delay(base::TimeDelta());
+}
+
+void FileWriteWatcher::OnWriteEvent(const std::string& resource_id) {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+
+ operation_observer_->OnCacheFileUploadNeededByOperation(resource_id);
+}
+
+} // namespace internal
+} // namespace drive
diff --git a/chrome/browser/chromeos/drive/file_write_watcher.h b/chrome/browser/chromeos/drive/file_write_watcher.h
new file mode 100644
index 0000000..91c947d
--- /dev/null
+++ b/chrome/browser/chromeos/drive/file_write_watcher.h
@@ -0,0 +1,75 @@
+// 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 CHROME_BROWSER_CHROMEOS_DRIVE_FILE_WRITE_WATCHER_H_
+#define CHROME_BROWSER_CHROMEOS_DRIVE_FILE_WRITE_WATCHER_H_
+
+#include "base/callback_forward.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "chrome/browser/chromeos/drive/file_system_util.h"
+
+namespace base {
+class FilePath;
+} // namespace base
+
+namespace drive {
+
+namespace file_system {
+class OperationObserver;
+} // namespace file_system
+
+namespace internal {
+
+typedef base::Callback<void(bool)> StartWatchCallback;
+
+// The class watches modification to Drive files in the cache directory.
+// This is used for returning a local writable snapshot of Drive files from the
+// Save-As file dialog, so that the callers of the dialog can save to Drive
+// without any special handling about Drive.
+class FileWriteWatcher {
+ public:
+ explicit FileWriteWatcher(file_system::OperationObserver* observer);
+ ~FileWriteWatcher();
+
+ // Starts watching the modification to |path|. When it successfully started
+ // watching, it runs |callback| by passing true as the argument. Or if it
+ // failed, the callback is run with false.
+ // When modification is detected, it is notified to the |observer| passed to
+ // the constructor by calling OnCacheFileUploadNeededByOperation(resource_id).
+ //
+ // Currently, the modification is watched in "one-shot" manner. That is, once
+ // a modification is notified, the watch is deactivated for freeing system
+ // resources. As a heuristic to capture the real end of write operations that
+ // might be done by several chunked writes, the notification is fired after
+ // 5 seconds has passed after the last write operation is detected.
+ //
+ // TODO(kinaba): investigate the possibility to continuously watch the whole
+ // cache directory.
+ void StartWatch(const base::FilePath& path,
+ const std::string& resource_id,
+ const StartWatchCallback& callback);
+
+ // For testing purpose, stops inserting delay between the write detection and
+ // notification to the observer.
+ void DisableDelayForTesting();
+
+ private:
+ // Invoked when a modification is observed.
+ void OnWriteEvent(const std::string& resource_id);
+
+ class FileWriteWatcherImpl;
+ scoped_ptr<FileWriteWatcherImpl, util::DestroyHelper> watcher_impl_;
+ file_system::OperationObserver* operation_observer_;
+
+ // 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<FileWriteWatcher> weak_ptr_factory_;
+ DISALLOW_COPY_AND_ASSIGN(FileWriteWatcher);
+};
+
+} // namespace internal
+} // namespace drive
+
+#endif // CHROME_BROWSER_CHROMEOS_DRIVE_FILE_WRITE_WATCHER_H_
diff --git a/chrome/browser/chromeos/drive/file_write_watcher_unittest.cc b/chrome/browser/chromeos/drive/file_write_watcher_unittest.cc
new file mode 100644
index 0000000..23be2a8
--- /dev/null
+++ b/chrome/browser/chromeos/drive/file_write_watcher_unittest.cc
@@ -0,0 +1,119 @@
+// 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 "chrome/browser/chromeos/drive/file_write_watcher.h"
+
+#include <set>
+
+#include "base/bind.h"
+#include "base/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/run_loop.h"
+#include "chrome/browser/chromeos/drive/file_system/operation_observer.h"
+#include "content/public/test/test_browser_thread_bundle.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace drive {
+namespace internal {
+
+namespace {
+
+class TestObserver : public file_system::OperationObserver {
+ public:
+ // After all the resource_ids in |expected_upload| are notified for the
+ // need of uploading, runs |quit_closure|. Also checks that each id is
+ // notified only once.
+ TestObserver(const std::set<std::string>& expected_upload,
+ const base::Closure& quit_closure)
+ : expected_upload_(expected_upload),
+ quit_closure_(quit_closure) {
+ }
+
+ virtual void OnDirectoryChangedByOperation(
+ const base::FilePath& path) OVERRIDE {}
+
+ virtual void OnCacheFileUploadNeededByOperation(
+ const std::string& resource_id) OVERRIDE {
+ EXPECT_EQ(1U, expected_upload_.count(resource_id)) << resource_id;
+ expected_upload_.erase(resource_id);
+ if (expected_upload_.empty())
+ quit_closure_.Run();
+ }
+
+ private:
+ std::set<std::string> expected_upload_;
+ base::Closure quit_closure_;
+};
+
+// Writes something on the file at |path|.
+void WriteSomethingAfterStartWatch(const base::FilePath& path,
+ bool watch_success) {
+ EXPECT_TRUE(watch_success) << path.value();
+
+ const char kDummy[] = "hello";
+ ASSERT_TRUE(file_util::WriteFile(path, kDummy, arraysize(kDummy)));
+}
+
+class FileWriteWatcherTest : public testing::Test {
+ protected:
+ // The test requires UI thread (FileWriteWatcher DCHECKs that its public
+ // interface is running on UI thread) and FILE thread (Linux version of
+ // base::FilePathWatcher needs to live on an IOAllowed thread with TYPE_IO,
+ // which is FILE thread in the production environment).
+ //
+ // By using the IO_MAINLOOP test thread bundle, the main thread is used
+ // both as UI and FILE thread, with TYPE_IO message loop.
+ FileWriteWatcherTest()
+ : thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP) {
+ }
+
+ virtual void SetUp() OVERRIDE {
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+ }
+
+ base::FilePath GetTempPath(const std::string& name) {
+ return temp_dir_.path().Append(name);
+ }
+
+ private:
+ content::TestBrowserThreadBundle thread_bundle_;
+ base::ScopedTempDir temp_dir_;
+};
+
+} // namespace
+
+TEST_F(FileWriteWatcherTest, WatchThreeFiles) {
+ std::set<std::string> expected;
+ expected.insert("1");
+ expected.insert("2");
+ expected.insert("3");
+
+ base::RunLoop loop;
+ TestObserver observer(expected, loop.QuitClosure());
+
+ // Set up the watcher.
+ FileWriteWatcher watcher(&observer);
+ watcher.DisableDelayForTesting();
+
+ // Start watching and running.
+ base::FilePath path1 = GetTempPath("foo.txt");
+ base::FilePath path2 = GetTempPath("bar.png");
+ base::FilePath path3 = GetTempPath("buz.doc");
+ base::FilePath path4 = GetTempPath("mya.mp3");
+ watcher.StartWatch(path1, "1",
+ base::Bind(&WriteSomethingAfterStartWatch, path1));
+ watcher.StartWatch(path2, "2",
+ base::Bind(&WriteSomethingAfterStartWatch, path2));
+ watcher.StartWatch(path3, "3",
+ base::Bind(&WriteSomethingAfterStartWatch, path3));
+
+ // Unwatched write. It shouldn't be notified.
+ WriteSomethingAfterStartWatch(path4, true);
+
+ // The loop should quit if all the three paths are notified to be written.
+ loop.Run();
+}
+
+} // namespace internal
+} // namespace drive
diff --git a/chrome/chrome_browser_chromeos.gypi b/chrome/chrome_browser_chromeos.gypi
index 36eabb0..cd5fba8 100644
--- a/chrome/chrome_browser_chromeos.gypi
+++ b/chrome/chrome_browser_chromeos.gypi
@@ -275,6 +275,8 @@
'browser/chromeos/drive/file_task_executor.h',
'browser/chromeos/drive/file_write_helper.cc',
'browser/chromeos/drive/file_write_helper.h',
+ 'browser/chromeos/drive/file_write_watcher.cc',
+ 'browser/chromeos/drive/file_write_watcher.h',
'browser/chromeos/drive/fileapi_worker.cc',
'browser/chromeos/drive/fileapi_worker.h',
'browser/chromeos/drive/job_list.cc',
diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi
index 4168ac6..5de1502 100644
--- a/chrome/chrome_tests_unit.gypi
+++ b/chrome/chrome_tests_unit.gypi
@@ -629,6 +629,7 @@
'browser/chromeos/drive/file_system_unittest.cc',
'browser/chromeos/drive/file_system_util_unittest.cc',
'browser/chromeos/drive/file_write_helper_unittest.cc',
+ 'browser/chromeos/drive/file_write_watcher_unittest.cc',
'browser/chromeos/drive/fileapi_worker_unittest.cc',
'browser/chromeos/drive/job_queue_unittest.cc',
'browser/chromeos/drive/job_scheduler_unittest.cc',