diff options
author | kinaba@chromium.org <kinaba@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-08-07 06:59:56 +0000 |
---|---|---|
committer | kinaba@chromium.org <kinaba@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-08-07 06:59:56 +0000 |
commit | 799d53bf220e6b818af66fe7cdb5a5af657e2fab (patch) | |
tree | e93d41c322d492931b298dc386a2ca29a4093407 /chrome/browser/chromeos/drive | |
parent | fab290345ad407611d8e6832773b567a68573664 (diff) | |
download | chromium_src-799d53bf220e6b818af66fe7cdb5a5af657e2fab.zip chromium_src-799d53bf220e6b818af66fe7cdb5a5af657e2fab.tar.gz chromium_src-799d53bf220e6b818af66fe7cdb5a5af657e2fab.tar.bz2 |
Add drive::internal::FileWriteWatcher.
This is a part of the effort to enable Drive in the Save-As dialog of
Chrome OS for every call site. For that, we need to care about existing
callers that just simply write to local file paths returned by the dialog.
The new class added in this patch watches specified file path in
Drive cache, and notifies the need for uploading when the
file content is modified.
BUG=140425
R=hashimoto@chromium.org
Review URL: https://codereview.chromium.org/22314009
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@216119 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/chromeos/drive')
3 files changed, 419 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 |