summaryrefslogtreecommitdiffstats
path: root/components/drive
diff options
context:
space:
mode:
authorlukasza <lukasza@chromium.org>2015-08-20 18:13:24 -0700
committerCommit bot <commit-bot@chromium.org>2015-08-21 01:13:59 +0000
commit6364a0299330a1ac060f0794f4638f82c0dc5cc2 (patch)
tree38c2645500cb3be95f73187aaa211f210dd3ce1a /components/drive
parent85cc8532e314967a8ec308bc7fed379f3ad9d80a (diff)
downloadchromium_src-6364a0299330a1ac060f0794f4638f82c0dc5cc2.zip
chromium_src-6364a0299330a1ac060f0794f4638f82c0dc5cc2.tar.gz
chromium_src-6364a0299330a1ac060f0794f4638f82c0dc5cc2.tar.bz2
Move chrome/browser/chromeos/drive/resource* (+deps) into components/drive.
Files moved from chrome/browser/chromeos/drive into components/drive: - fake_free_disk_space_getter* - file_cache* - file_system_core_util* - resource_entry_conversion* - resource_metadata* - resource_metadata_storage* Other changes: - Removed unneeded includes where needed and possible. - Hidden usage of cryptohome::kMinFreeSpaceInBytes behind OS_CHROMEOS ifdefs and provided an equivalent constant shared by other OS-es. - Hidden usage of base::SetPosixFilePermissions behind OS_CHROMEOS ifdef as it was only needed to support cros_disks service on ChromeOS. - Moved 3 functions back from file_system_core_util into file_system_core. The 3 functions are ChromeOS-specific (dealing with where GDrive is mounted on ChromeOS) and moving them back helps avoid adding a new dependency on components/drive from chromeos directories. - When adding a third_party/leveldatabase dependency to gyp/gn/deps files under components/drive, I realized that gn is not consistent with gyp, by not explicitly listing a dependency on third_party/cacheinvalidation in components/drive/BUILD.gn. Test steps: 1. Verify that things still build via GYP (and unit tests pass). $ GYP_DEFINES="use_goma=1 gomadir=... chromeos=1" gclient sync $ ninja -C out/Debug -j 150 chrome unit_tests \ interactive_ui_tests browser_tests drive $ out/Debug/unit_tests 2. Verify that things still build via GN. $ gn gen out/Default --args='target_os="chromeos" use_goma=true' $ ninja -C out/Default -j 150 chrome unit_tests \ interactive_ui_tests browser_tests components/drive TEST=Please see "Test steps" above. BUG=257943, 498951 TBR=mtomasz@chromium.org, satorux@chromium.org, phajdan.jr@chromium.org, rockot@chromium.org, stevenjb@chromium.org, Review URL: https://codereview.chromium.org/1296483003 Cr-Commit-Position: refs/heads/master@{#344637}
Diffstat (limited to 'components/drive')
-rw-r--r--components/drive/BUILD.gn14
-rw-r--r--components/drive/DEPS11
-rw-r--r--components/drive/fake_free_disk_space_getter.cc31
-rw-r--r--components/drive/fake_free_disk_space_getter.h44
-rw-r--r--components/drive/file_cache.cc626
-rw-r--r--components/drive/file_cache.h196
-rw-r--r--components/drive/file_cache_unittest.cc562
-rw-r--r--components/drive/file_system_core_util.cc142
-rw-r--r--components/drive/file_system_core_util.h90
-rw-r--r--components/drive/file_system_core_util_unittest.cc99
-rw-r--r--components/drive/resource_entry_conversion.cc141
-rw-r--r--components/drive/resource_entry_conversion.h53
-rw-r--r--components/drive/resource_entry_conversion_unittest.cc374
-rw-r--r--components/drive/resource_metadata.cc607
-rw-r--r--components/drive/resource_metadata.h146
-rw-r--r--components/drive/resource_metadata_storage.cc1064
-rw-r--r--components/drive/resource_metadata_storage.h172
-rw-r--r--components/drive/resource_metadata_storage_unittest.cc633
-rw-r--r--components/drive/resource_metadata_unittest.cc709
19 files changed, 5712 insertions, 2 deletions
diff --git a/components/drive/BUILD.gn b/components/drive/BUILD.gn
index 37193a3..08a9814 100644
--- a/components/drive/BUILD.gn
+++ b/components/drive/BUILD.gn
@@ -20,10 +20,14 @@ source_set("drive") {
"drive_uploader.h",
"event_logger.cc",
"event_logger.h",
+ "file_cache.cc",
+ "file_cache.h",
"file_change.cc",
"file_change.h",
"file_errors.cc",
"file_errors.h",
+ "file_system_core_util.cc",
+ "file_system_core_util.h",
"job_list.cc",
"job_list.h",
"job_queue.cc",
@@ -32,6 +36,12 @@ source_set("drive") {
"job_scheduler.h",
"local_file_reader.cc",
"local_file_reader.h",
+ "resource_entry_conversion.cc",
+ "resource_entry_conversion.h",
+ "resource_metadata.cc",
+ "resource_metadata.h",
+ "resource_metadata_storage.cc",
+ "resource_metadata_storage.h",
"service/drive_api_service.cc",
"service/drive_api_service.h",
"service/drive_service_interface.cc",
@@ -46,6 +56,8 @@ source_set("drive") {
"//google_apis:google_apis",
"//net:net",
+ "//third_party/cacheinvalidation:cacheinvalidation",
+ "//third_party/leveldatabase:leveldatabase",
"//third_party/re2:re2",
]
public_deps = [
@@ -64,6 +76,8 @@ source_set("test_support") {
sources = [
"drive_test_util.cc",
"drive_test_util.h",
+ "fake_free_disk_space_getter.cc",
+ "fake_free_disk_space_getter.h",
"service/dummy_drive_service.cc",
"service/dummy_drive_service.h",
"service/fake_drive_service.cc",
diff --git a/components/drive/DEPS b/components/drive/DEPS
index 2d849cf..7c978cd 100644
--- a/components/drive/DEPS
+++ b/components/drive/DEPS
@@ -4,6 +4,7 @@ include_rules = [
"+google_apis",
"+google/cacheinvalidation/types.pb.h",
"+net",
+ "+third_party/leveldatabase",
"+third_party/re2",
]
@@ -24,14 +25,20 @@ specific_include_rules = {
# The following test dependencies should be removed to fully componentize this
# directory. crbug.com/498951
- r"(job_scheduler_unittest.cc"
+ r"(file_cache_unittest.cc"
+ r"|file_system_core_util_unittest.cc"
+ r"|job_scheduler_unittest.cc"
+ r"|resource_metadata_storage_unittest.cc"
+ r"|resource_metadata_unittest.cc"
r")": [
"+content/public/test/test_browser_thread_bundle.h",
],
# The dependency below is ok and can stay here for the long-term, because it
# is guarded by #if defined(OS_CHROMEOS) in the source code.
- "drive_test_util\.h": [
+ r"(drive_test_util.h"
+ r"|file_cache.cc"
+ r")": [
"+third_party/cros_system_api/constants/cryptohome.h",
],
}
diff --git a/components/drive/fake_free_disk_space_getter.cc b/components/drive/fake_free_disk_space_getter.cc
new file mode 100644
index 0000000..22df758
--- /dev/null
+++ b/components/drive/fake_free_disk_space_getter.cc
@@ -0,0 +1,31 @@
+// 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/fake_free_disk_space_getter.h"
+
+#include "components/drive/drive_test_util.h"
+
+namespace drive {
+
+FakeFreeDiskSpaceGetter::FakeFreeDiskSpaceGetter()
+ : default_value_(test_util::kLotsOfSpace) {
+}
+
+FakeFreeDiskSpaceGetter::~FakeFreeDiskSpaceGetter() {
+}
+
+void FakeFreeDiskSpaceGetter::PushFakeValue(int64 value) {
+ fake_values_.push_back(value);
+}
+
+int64 FakeFreeDiskSpaceGetter::AmountOfFreeDiskSpace() {
+ if (fake_values_.empty())
+ return default_value_;
+
+ const int64 value = fake_values_.front();
+ fake_values_.pop_front();
+ return value;
+}
+
+} // namespace drive
diff --git a/components/drive/fake_free_disk_space_getter.h b/components/drive/fake_free_disk_space_getter.h
new file mode 100644
index 0000000..ba6dd3d
--- /dev/null
+++ b/components/drive/fake_free_disk_space_getter.h
@@ -0,0 +1,44 @@
+// 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_FAKE_FREE_DISK_SPACE_GETTER_H_
+#define COMPONENTS_DRIVE_FAKE_FREE_DISK_SPACE_GETTER_H_
+
+#include <list>
+
+#include "base/basictypes.h"
+#include "components/drive/file_cache.h"
+
+namespace drive {
+
+// This class is used to report fake free disk space. In particular, this
+// class can be used to simulate a case where disk is full, or nearly full.
+class FakeFreeDiskSpaceGetter : public internal::FreeDiskSpaceGetterInterface {
+ public:
+ FakeFreeDiskSpaceGetter();
+ ~FakeFreeDiskSpaceGetter() override;
+
+ void set_default_value(int64 value) { default_value_ = value; }
+
+ // Pushes the given value to the back of the fake value list.
+ //
+ // If the fake value list is empty, AmountOfFreeDiskSpace() will return
+ // |default_value_| repeatedly.
+ // Otherwise, AmountOfFreeDiskSpace() will return the value at the front of
+ // the list and removes it from the list.
+ void PushFakeValue(int64 value);
+
+ // FreeDiskSpaceGetterInterface overrides.
+ int64 AmountOfFreeDiskSpace() override;
+
+ private:
+ std::list<int64> fake_values_;
+ int64 default_value_;
+
+ DISALLOW_COPY_AND_ASSIGN(FakeFreeDiskSpaceGetter);
+};
+
+} // namespace drive
+
+#endif // COMPONENTS_DRIVE_FAKE_FREE_DISK_SPACE_GETTER_H_
diff --git a/components/drive/file_cache.cc b/components/drive/file_cache.cc
new file mode 100644
index 0000000..749dd8f
--- /dev/null
+++ b/components/drive/file_cache.cc
@@ -0,0 +1,626 @@
+// 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_cache.h"
+
+#include <vector>
+
+#include "base/bind.h"
+#include "base/bind_helpers.h"
+#include "base/callback_helpers.h"
+#include "base/files/file_enumerator.h"
+#include "base/files/file_util.h"
+#include "base/location.h"
+#include "base/logging.h"
+#include "base/metrics/histogram.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "base/sys_info.h"
+#include "components/drive/drive.pb.h"
+#include "components/drive/drive_api_util.h"
+#include "components/drive/file_system_core_util.h"
+#include "components/drive/resource_metadata_storage.h"
+#include "google_apis/drive/task_util.h"
+#include "net/base/filename_util.h"
+#include "net/base/mime_sniffer.h"
+#include "net/base/mime_util.h"
+#if defined(OS_CHROMEOS)
+#include "third_party/cros_system_api/constants/cryptohome.h"
+#endif
+
+namespace drive {
+namespace internal {
+namespace {
+
+// Returns ID extracted from the path.
+std::string GetIdFromPath(const base::FilePath& path) {
+ return util::UnescapeCacheFileName(path.BaseName().AsUTF8Unsafe());
+}
+
+} // namespace
+
+FileCache::FileCache(ResourceMetadataStorage* storage,
+ const base::FilePath& cache_file_directory,
+ base::SequencedTaskRunner* blocking_task_runner,
+ FreeDiskSpaceGetterInterface* free_disk_space_getter)
+ : cache_file_directory_(cache_file_directory),
+ blocking_task_runner_(blocking_task_runner),
+ storage_(storage),
+ free_disk_space_getter_(free_disk_space_getter),
+ weak_ptr_factory_(this) {
+ DCHECK(blocking_task_runner_.get());
+}
+
+FileCache::~FileCache() {
+ // Must be on the sequenced worker pool, as |metadata_| must be deleted on
+ // the sequenced worker pool.
+ AssertOnSequencedWorkerPool();
+}
+
+base::FilePath FileCache::GetCacheFilePath(const std::string& id) const {
+ return cache_file_directory_.Append(
+ base::FilePath::FromUTF8Unsafe(util::EscapeCacheFileName(id)));
+}
+
+void FileCache::AssertOnSequencedWorkerPool() {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+}
+
+bool FileCache::IsUnderFileCacheDirectory(const base::FilePath& path) const {
+ return cache_file_directory_.IsParent(path);
+}
+
+bool FileCache::FreeDiskSpaceIfNeededFor(int64 num_bytes) {
+ AssertOnSequencedWorkerPool();
+
+ // Do nothing and return if we have enough space.
+ if (HasEnoughSpaceFor(num_bytes, cache_file_directory_))
+ return true;
+
+ // Otherwise, try to free up the disk space.
+ DVLOG(1) << "Freeing up disk space for " << num_bytes;
+
+ // Remove all entries unless specially marked.
+ scoped_ptr<ResourceMetadataStorage::Iterator> it = storage_->GetIterator();
+ for (; !it->IsAtEnd(); it->Advance()) {
+ if (it->GetValue().file_specific_info().has_cache_state() &&
+ !it->GetValue().file_specific_info().cache_state().is_pinned() &&
+ !it->GetValue().file_specific_info().cache_state().is_dirty() &&
+ !mounted_files_.count(it->GetID())) {
+ ResourceEntry entry(it->GetValue());
+ entry.mutable_file_specific_info()->clear_cache_state();
+ storage_->PutEntry(entry);
+ }
+ }
+ if (it->HasError())
+ return false;
+
+ // Remove all files which have no corresponding cache entries.
+ base::FileEnumerator enumerator(cache_file_directory_,
+ false, // not recursive
+ base::FileEnumerator::FILES);
+ ResourceEntry entry;
+ for (base::FilePath current = enumerator.Next(); !current.empty();
+ current = enumerator.Next()) {
+ std::string id = GetIdFromPath(current);
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error == FILE_ERROR_NOT_FOUND ||
+ (error == FILE_ERROR_OK &&
+ !entry.file_specific_info().cache_state().is_present()))
+ base::DeleteFile(current, false /* recursive */);
+ else if (error != FILE_ERROR_OK)
+ return false;
+ }
+
+ // Check the disk space again.
+ return HasEnoughSpaceFor(num_bytes, cache_file_directory_);
+}
+
+FileError FileCache::GetFile(const std::string& id,
+ base::FilePath* cache_file_path) {
+ AssertOnSequencedWorkerPool();
+ DCHECK(cache_file_path);
+
+ ResourceEntry entry;
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+ if (!entry.file_specific_info().cache_state().is_present())
+ return FILE_ERROR_NOT_FOUND;
+
+ *cache_file_path = GetCacheFilePath(id);
+ return FILE_ERROR_OK;
+}
+
+FileError FileCache::Store(const std::string& id,
+ const std::string& md5,
+ const base::FilePath& source_path,
+ FileOperationType file_operation_type) {
+ AssertOnSequencedWorkerPool();
+
+ ResourceEntry entry;
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ int64 file_size = 0;
+ if (file_operation_type == FILE_OPERATION_COPY) {
+ if (!base::GetFileSize(source_path, &file_size)) {
+ LOG(WARNING) << "Couldn't get file size for: " << source_path.value();
+ return FILE_ERROR_FAILED;
+ }
+ }
+ if (!FreeDiskSpaceIfNeededFor(file_size))
+ return FILE_ERROR_NO_LOCAL_SPACE;
+
+ // If file is mounted, return error.
+ if (mounted_files_.count(id))
+ return FILE_ERROR_IN_USE;
+
+ base::FilePath dest_path = GetCacheFilePath(id);
+ bool success = false;
+ switch (file_operation_type) {
+ case FILE_OPERATION_MOVE:
+ success = base::Move(source_path, dest_path);
+ break;
+ case FILE_OPERATION_COPY:
+ success = base::CopyFile(source_path, dest_path);
+ break;
+ default:
+ NOTREACHED();
+ }
+
+ if (!success) {
+ LOG(ERROR) << "Failed to store: "
+ << "source_path = " << source_path.value() << ", "
+ << "dest_path = " << dest_path.value() << ", "
+ << "file_operation_type = " << file_operation_type;
+ return FILE_ERROR_FAILED;
+ }
+
+ // Now that file operations have completed, update metadata.
+ FileCacheEntry* cache_state =
+ entry.mutable_file_specific_info()->mutable_cache_state();
+ cache_state->set_md5(md5);
+ cache_state->set_is_present(true);
+ if (md5.empty())
+ cache_state->set_is_dirty(true);
+ return storage_->PutEntry(entry);
+}
+
+FileError FileCache::Pin(const std::string& id) {
+ AssertOnSequencedWorkerPool();
+
+ ResourceEntry entry;
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+ entry.mutable_file_specific_info()->mutable_cache_state()->set_is_pinned(
+ true);
+ return storage_->PutEntry(entry);
+}
+
+FileError FileCache::Unpin(const std::string& id) {
+ AssertOnSequencedWorkerPool();
+
+ // Unpinning a file means its entry must exist in cache.
+ ResourceEntry entry;
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ // Now that file operations have completed, update metadata.
+ if (entry.file_specific_info().cache_state().is_present()) {
+ entry.mutable_file_specific_info()->mutable_cache_state()->set_is_pinned(
+ false);
+ } else {
+ // Remove the existing entry if we are unpinning a non-present file.
+ entry.mutable_file_specific_info()->clear_cache_state();
+ }
+ error = storage_->PutEntry(entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ // Now it's a chance to free up space if needed.
+ FreeDiskSpaceIfNeededFor(0);
+
+ return FILE_ERROR_OK;
+}
+
+FileError FileCache::MarkAsMounted(const std::string& id,
+ base::FilePath* cache_file_path) {
+ AssertOnSequencedWorkerPool();
+ DCHECK(cache_file_path);
+
+ // Get cache entry associated with the id and md5
+ ResourceEntry entry;
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+ if (!entry.file_specific_info().cache_state().is_present())
+ return FILE_ERROR_NOT_FOUND;
+
+ if (mounted_files_.count(id))
+ return FILE_ERROR_INVALID_OPERATION;
+
+ base::FilePath path = GetCacheFilePath(id);
+
+#if defined(OS_CHROMEOS)
+ // Ensure the file is readable to cros_disks. See crbug.com/236994.
+ if (!base::SetPosixFilePermissions(
+ path,
+ base::FILE_PERMISSION_READ_BY_USER |
+ base::FILE_PERMISSION_WRITE_BY_USER |
+ base::FILE_PERMISSION_READ_BY_GROUP |
+ base::FILE_PERMISSION_READ_BY_OTHERS))
+ return FILE_ERROR_FAILED;
+#endif
+
+ mounted_files_.insert(id);
+
+ *cache_file_path = path;
+ return FILE_ERROR_OK;
+}
+
+FileError FileCache::OpenForWrite(
+ const std::string& id,
+ scoped_ptr<base::ScopedClosureRunner>* file_closer) {
+ AssertOnSequencedWorkerPool();
+
+ // Marking a file dirty means its entry and actual file blob must exist in
+ // cache.
+ ResourceEntry entry;
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+ if (!entry.file_specific_info().cache_state().is_present()) {
+ LOG(WARNING) << "Can't mark dirty a file that wasn't cached: " << id;
+ return FILE_ERROR_NOT_FOUND;
+ }
+
+ entry.mutable_file_specific_info()->mutable_cache_state()->set_is_dirty(true);
+ entry.mutable_file_specific_info()->mutable_cache_state()->clear_md5();
+ error = storage_->PutEntry(entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ write_opened_files_[id]++;
+ file_closer->reset(new base::ScopedClosureRunner(
+ base::Bind(&google_apis::RunTaskWithTaskRunner,
+ blocking_task_runner_,
+ base::Bind(&FileCache::CloseForWrite,
+ weak_ptr_factory_.GetWeakPtr(),
+ id))));
+ return FILE_ERROR_OK;
+}
+
+bool FileCache::IsOpenedForWrite(const std::string& id) {
+ AssertOnSequencedWorkerPool();
+ return write_opened_files_.count(id) != 0;
+}
+
+FileError FileCache::UpdateMd5(const std::string& id) {
+ AssertOnSequencedWorkerPool();
+
+ if (IsOpenedForWrite(id))
+ return FILE_ERROR_IN_USE;
+
+ ResourceEntry entry;
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+ if (!entry.file_specific_info().cache_state().is_present())
+ return FILE_ERROR_NOT_FOUND;
+
+ const std::string& md5 =
+ util::GetMd5Digest(GetCacheFilePath(id), &in_shutdown_);
+ if (in_shutdown_.IsSet())
+ return FILE_ERROR_ABORT;
+ if (md5.empty())
+ return FILE_ERROR_NOT_FOUND;
+
+ entry.mutable_file_specific_info()->mutable_cache_state()->set_md5(md5);
+ return storage_->PutEntry(entry);
+}
+
+FileError FileCache::ClearDirty(const std::string& id) {
+ AssertOnSequencedWorkerPool();
+
+ if (IsOpenedForWrite(id))
+ return FILE_ERROR_IN_USE;
+
+ // Clearing a dirty file means its entry and actual file blob must exist in
+ // cache.
+ ResourceEntry entry;
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+ if (!entry.file_specific_info().cache_state().is_present()) {
+ LOG(WARNING) << "Can't clear dirty state of a file that wasn't cached: "
+ << id;
+ return FILE_ERROR_NOT_FOUND;
+ }
+
+ // If a file is not dirty (it should have been marked dirty via OpenForWrite),
+ // clearing its dirty state is an invalid operation.
+ if (!entry.file_specific_info().cache_state().is_dirty()) {
+ LOG(WARNING) << "Can't clear dirty state of a non-dirty file: " << id;
+ return FILE_ERROR_INVALID_OPERATION;
+ }
+
+ entry.mutable_file_specific_info()->mutable_cache_state()->set_is_dirty(
+ false);
+ return storage_->PutEntry(entry);
+}
+
+FileError FileCache::Remove(const std::string& id) {
+ AssertOnSequencedWorkerPool();
+
+ ResourceEntry entry;
+
+ // If entry doesn't exist, nothing to do.
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error == FILE_ERROR_NOT_FOUND)
+ return FILE_ERROR_OK;
+ if (error != FILE_ERROR_OK)
+ return error;
+ if (!entry.file_specific_info().has_cache_state())
+ return FILE_ERROR_OK;
+
+ // Cannot delete a mounted file.
+ if (mounted_files_.count(id))
+ return FILE_ERROR_IN_USE;
+
+ // Delete the file.
+ base::FilePath path = GetCacheFilePath(id);
+ if (!base::DeleteFile(path, false /* recursive */))
+ return FILE_ERROR_FAILED;
+
+ // Now that all file operations have completed, remove from metadata.
+ entry.mutable_file_specific_info()->clear_cache_state();
+ return storage_->PutEntry(entry);
+}
+
+bool FileCache::ClearAll() {
+ AssertOnSequencedWorkerPool();
+
+ // Remove files.
+ base::FileEnumerator enumerator(cache_file_directory_,
+ false, // not recursive
+ base::FileEnumerator::FILES);
+ for (base::FilePath file = enumerator.Next(); !file.empty();
+ file = enumerator.Next())
+ base::DeleteFile(file, false /* recursive */);
+
+ return true;
+}
+
+bool FileCache::Initialize() {
+ AssertOnSequencedWorkerPool();
+
+ // Older versions do not clear MD5 when marking entries dirty.
+ // Clear MD5 of all dirty entries to deal with old data.
+ scoped_ptr<ResourceMetadataStorage::Iterator> it = storage_->GetIterator();
+ for (; !it->IsAtEnd(); it->Advance()) {
+ if (it->GetValue().file_specific_info().cache_state().is_dirty()) {
+ ResourceEntry new_entry(it->GetValue());
+ new_entry.mutable_file_specific_info()->mutable_cache_state()->
+ clear_md5();
+ if (storage_->PutEntry(new_entry) != FILE_ERROR_OK)
+ return false;
+ }
+ }
+ if (it->HasError())
+ return false;
+
+ if (!RenameCacheFilesToNewFormat())
+ return false;
+ return true;
+}
+
+void FileCache::Destroy() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ in_shutdown_.Set();
+
+ // Destroy myself on the blocking pool.
+ // Note that base::DeletePointer<> cannot be used as the destructor of this
+ // class is private.
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&FileCache::DestroyOnBlockingPool, base::Unretained(this)));
+}
+
+void FileCache::DestroyOnBlockingPool() {
+ AssertOnSequencedWorkerPool();
+ delete this;
+}
+
+bool FileCache::RecoverFilesFromCacheDirectory(
+ const base::FilePath& dest_directory,
+ const ResourceMetadataStorage::RecoveredCacheInfoMap&
+ recovered_cache_info) {
+ int file_number = 1;
+
+ base::FileEnumerator enumerator(cache_file_directory_,
+ false, // not recursive
+ base::FileEnumerator::FILES);
+ for (base::FilePath current = enumerator.Next(); !current.empty();
+ current = enumerator.Next()) {
+ const std::string& id = GetIdFromPath(current);
+ ResourceEntry entry;
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error != FILE_ERROR_OK && error != FILE_ERROR_NOT_FOUND)
+ return false;
+ if (error == FILE_ERROR_OK &&
+ entry.file_specific_info().cache_state().is_present()) {
+ // This file is managed by FileCache, no need to recover it.
+ continue;
+ }
+
+ // If a cache entry which is non-dirty and has matching MD5 is found in
+ // |recovered_cache_entries|, it means the current file is already uploaded
+ // to the server. Just delete it instead of recovering it.
+ ResourceMetadataStorage::RecoveredCacheInfoMap::const_iterator it =
+ recovered_cache_info.find(id);
+ if (it != recovered_cache_info.end()) {
+ // Due to the DB corruption, cache info might be recovered from old
+ // revision. Perform MD5 check even when is_dirty is false just in case.
+ if (!it->second.is_dirty &&
+ it->second.md5 == util::GetMd5Digest(current, &in_shutdown_)) {
+ base::DeleteFile(current, false /* recursive */);
+ continue;
+ }
+ }
+
+ // Read file contents to sniff mime type.
+ std::vector<char> content(net::kMaxBytesToSniff);
+ const int read_result =
+ base::ReadFile(current, &content[0], content.size());
+ if (read_result < 0) {
+ LOG(WARNING) << "Cannot read: " << current.value();
+ return false;
+ }
+ if (read_result == 0) // Skip empty files.
+ continue;
+
+ // Use recovered file name if available, otherwise decide file name with
+ // sniffed mime type.
+ base::FilePath dest_base_name(FILE_PATH_LITERAL("file"));
+ std::string mime_type;
+ if (it != recovered_cache_info.end() && !it->second.title.empty()) {
+ // We can use a file name recovered from the trashed DB.
+ dest_base_name = base::FilePath::FromUTF8Unsafe(it->second.title);
+ } else if (net::SniffMimeType(&content[0], read_result,
+ net::FilePathToFileURL(current),
+ std::string(), &mime_type) ||
+ net::SniffMimeTypeFromLocalData(&content[0], read_result,
+ &mime_type)) {
+ // Change base name for common mime types.
+ if (net::MatchesMimeType("image/*", mime_type)) {
+ dest_base_name = base::FilePath(FILE_PATH_LITERAL("image"));
+ } else if (net::MatchesMimeType("video/*", mime_type)) {
+ dest_base_name = base::FilePath(FILE_PATH_LITERAL("video"));
+ } else if (net::MatchesMimeType("audio/*", mime_type)) {
+ dest_base_name = base::FilePath(FILE_PATH_LITERAL("audio"));
+ }
+
+ // Estimate extension from mime type.
+ std::vector<base::FilePath::StringType> extensions;
+ base::FilePath::StringType extension;
+ if (net::GetPreferredExtensionForMimeType(mime_type, &extension))
+ extensions.push_back(extension);
+ else
+ net::GetExtensionsForMimeType(mime_type, &extensions);
+
+ // Add extension if possible.
+ if (!extensions.empty())
+ dest_base_name = dest_base_name.AddExtension(extensions[0]);
+ }
+
+ // Add file number to the file name and move.
+ const base::FilePath& dest_path = dest_directory.Append(dest_base_name)
+ .InsertBeforeExtensionASCII(base::StringPrintf("%08d", file_number++));
+ if (!base::CreateDirectory(dest_directory) ||
+ !base::Move(current, dest_path)) {
+ LOG(WARNING) << "Failed to move: " << current.value()
+ << " to " << dest_path.value();
+ return false;
+ }
+ }
+ UMA_HISTOGRAM_COUNTS("Drive.NumberOfCacheFilesRecoveredAfterDBCorruption",
+ file_number - 1);
+ return true;
+}
+
+FileError FileCache::MarkAsUnmounted(const base::FilePath& file_path) {
+ AssertOnSequencedWorkerPool();
+ DCHECK(IsUnderFileCacheDirectory(file_path));
+
+ std::string id = GetIdFromPath(file_path);
+
+ // Get the entry associated with the id.
+ ResourceEntry entry;
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ std::set<std::string>::iterator it = mounted_files_.find(id);
+ if (it == mounted_files_.end())
+ return FILE_ERROR_INVALID_OPERATION;
+
+ mounted_files_.erase(it);
+ return FILE_ERROR_OK;
+}
+
+bool FileCache::HasEnoughSpaceFor(int64 num_bytes,
+ const base::FilePath& path) {
+ int64 free_space = 0;
+ if (free_disk_space_getter_)
+ free_space = free_disk_space_getter_->AmountOfFreeDiskSpace();
+ else
+ free_space = base::SysInfo::AmountOfFreeDiskSpace(path);
+
+ // Subtract this as if this portion does not exist.
+#if defined(OS_CHROMEOS)
+ const int64 kMinFreeBytes = cryptohome::kMinFreeSpaceInBytes;
+#else
+ const int64 kMinFreeBytes = 512ull * 1024ull * 1024ull; // 512MB
+#endif
+ free_space -= kMinFreeBytes;
+ return (free_space >= num_bytes);
+}
+
+bool FileCache::RenameCacheFilesToNewFormat() {
+ base::FileEnumerator enumerator(cache_file_directory_,
+ false, // not recursive
+ base::FileEnumerator::FILES);
+ for (base::FilePath current = enumerator.Next(); !current.empty();
+ current = enumerator.Next()) {
+ base::FilePath new_path = current.RemoveExtension();
+ if (!new_path.Extension().empty()) {
+ // Delete files with multiple extensions.
+ if (!base::DeleteFile(current, false /* recursive */))
+ return false;
+ continue;
+ }
+ const std::string& id = GetIdFromPath(new_path);
+ new_path = GetCacheFilePath(util::CanonicalizeResourceId(id));
+ if (new_path != current && !base::Move(current, new_path))
+ return false;
+ }
+ return true;
+}
+
+void FileCache::CloseForWrite(const std::string& id) {
+ AssertOnSequencedWorkerPool();
+
+ std::map<std::string, int>::iterator it = write_opened_files_.find(id);
+ if (it == write_opened_files_.end())
+ return;
+
+ DCHECK_LT(0, it->second);
+ --it->second;
+ if (it->second == 0)
+ write_opened_files_.erase(it);
+
+ // Update last modified date.
+ ResourceEntry entry;
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error != FILE_ERROR_OK) {
+ LOG(ERROR) << "Failed to get entry: " << id << ", "
+ << FileErrorToString(error);
+ return;
+ }
+ entry.mutable_file_info()->set_last_modified(
+ base::Time::Now().ToInternalValue());
+ error = storage_->PutEntry(entry);
+ if (error != FILE_ERROR_OK) {
+ LOG(ERROR) << "Failed to put entry: " << id << ", "
+ << FileErrorToString(error);
+ }
+}
+
+} // namespace internal
+} // namespace drive
diff --git a/components/drive/file_cache.h b/components/drive/file_cache.h
new file mode 100644
index 0000000..a523b41
--- /dev/null
+++ b/components/drive/file_cache.h
@@ -0,0 +1,196 @@
+// 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_CACHE_H_
+#define COMPONENTS_DRIVE_FILE_CACHE_H_
+
+#include <set>
+#include <string>
+
+#include "base/files/file_path.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/synchronization/cancellation_flag.h"
+#include "base/threading/thread_checker.h"
+#include "components/drive/file_errors.h"
+#include "components/drive/resource_metadata_storage.h"
+
+namespace base {
+class ScopedClosureRunner;
+class SequencedTaskRunner;
+} // namespace base
+
+namespace drive {
+
+namespace internal {
+
+// Interface class used for getting the free disk space. Tests can inject an
+// implementation that reports fake free disk space.
+class FreeDiskSpaceGetterInterface {
+ public:
+ virtual ~FreeDiskSpaceGetterInterface() {}
+ virtual int64 AmountOfFreeDiskSpace() = 0;
+};
+
+// FileCache is used to maintain cache states of FileSystem.
+//
+// All non-static public member functions, unless mentioned otherwise (see
+// GetCacheFilePath() for example), should be run with |blocking_task_runner|.
+class FileCache {
+ public:
+ // Enum defining type of file operation e.g. copy or move, etc.
+ enum FileOperationType {
+ FILE_OPERATION_MOVE = 0,
+ FILE_OPERATION_COPY,
+ };
+
+ // |cache_file_directory| stores cached files.
+ //
+ // |blocking_task_runner| indicates the blocking worker pool for cache
+ // operations. All operations on this FileCache must be run on this runner.
+ // Must not be null.
+ //
+ // |free_disk_space_getter| is used to inject a custom free disk space
+ // getter for testing. NULL must be passed for production code.
+ //
+ // Must be called on the UI thread.
+ FileCache(ResourceMetadataStorage* storage,
+ const base::FilePath& cache_file_directory,
+ base::SequencedTaskRunner* blocking_task_runner,
+ FreeDiskSpaceGetterInterface* free_disk_space_getter);
+
+ // Returns true if the given path is under drive cache directory, i.e.
+ // <user_profile_dir>/GCache/v1
+ //
+ // Can be called on any thread.
+ bool IsUnderFileCacheDirectory(const base::FilePath& path) const;
+
+ // Frees up disk space to store a file with |num_bytes| size content, while
+ // keeping cryptohome::kMinFreeSpaceInBytes bytes on the disk, if needed.
+ // Returns true if we successfully manage to have enough space, otherwise
+ // false.
+ bool FreeDiskSpaceIfNeededFor(int64 num_bytes);
+
+ // Checks if file corresponding to |id| exists in cache, and returns
+ // FILE_ERROR_OK with |cache_file_path| storing the path to the file.
+ // |cache_file_path| must not be null.
+ FileError GetFile(const std::string& id, base::FilePath* cache_file_path);
+
+ // Stores |source_path| as a cache of the remote content of the file
+ // with |id| and |md5|.
+ // Pass an empty string as MD5 to mark the entry as dirty.
+ FileError Store(const std::string& id,
+ const std::string& md5,
+ const base::FilePath& source_path,
+ FileOperationType file_operation_type);
+
+ // Pins the specified entry.
+ FileError Pin(const std::string& id);
+
+ // Unpins the specified entry.
+ FileError Unpin(const std::string& id);
+
+ // Sets the state of the cache entry corresponding to |id| as mounted.
+ FileError MarkAsMounted(const std::string& id,
+ base::FilePath* cache_file_path);
+
+ // Sets the state of the cache entry corresponding to file_path as unmounted.
+ FileError MarkAsUnmounted(const base::FilePath& file_path);
+
+ // Opens the cache file corresponding to |id| for write. |file_closer| should
+ // be kept alive until writing finishes.
+ // This method must be called before writing to cache files.
+ FileError OpenForWrite(const std::string& id,
+ scoped_ptr<base::ScopedClosureRunner>* file_closer);
+
+ // Returns true if the cache file corresponding to |id| is write-opened.
+ bool IsOpenedForWrite(const std::string& id);
+
+ // Calculates MD5 of the cache file and updates the stored value.
+ FileError UpdateMd5(const std::string& id);
+
+ // Clears dirty state of the specified entry.
+ FileError ClearDirty(const std::string& id);
+
+ // Removes the specified cache entry and delete cache files if available.
+ FileError Remove(const std::string& id);
+
+ // Removes all the files in the cache directory.
+ bool ClearAll();
+
+ // Initializes the cache. Returns true on success.
+ bool Initialize();
+
+ // Destroys this cache. This function posts a task to the blocking task
+ // runner to safely delete the object.
+ // Must be called on the UI thread.
+ void Destroy();
+
+ // Moves files in the cache directory which are not managed by FileCache to
+ // |dest_directory|.
+ // |recovered_cache_info| should contain cache info recovered from the trashed
+ // metadata DB. It is used to ignore non-dirty files.
+ bool RecoverFilesFromCacheDirectory(
+ const base::FilePath& dest_directory,
+ const ResourceMetadataStorage::RecoveredCacheInfoMap&
+ recovered_cache_info);
+
+ private:
+ friend class FileCacheTest;
+
+ ~FileCache();
+
+ // Returns absolute path of the file if it were cached or to be cached.
+ //
+ // Can be called on any thread.
+ base::FilePath GetCacheFilePath(const std::string& id) const;
+
+ // Checks whether the current thread is on the right sequenced worker pool
+ // with the right sequence ID. If not, DCHECK will fail.
+ void AssertOnSequencedWorkerPool();
+
+ // Destroys the cache on the blocking pool.
+ void DestroyOnBlockingPool();
+
+ // Returns true if we have sufficient space to store the given number of
+ // bytes, while keeping cryptohome::kMinFreeSpaceInBytes bytes on the disk.
+ bool HasEnoughSpaceFor(int64 num_bytes, const base::FilePath& path);
+
+ // Renames cache files from old "prefix:id.md5" format to the new format.
+ // TODO(hashimoto): Remove this method at some point.
+ bool RenameCacheFilesToNewFormat();
+
+ // This method must be called after writing to a cache file.
+ // Used to implement OpenForWrite().
+ void CloseForWrite(const std::string& id);
+
+ const base::FilePath cache_file_directory_;
+
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_;
+
+ base::CancellationFlag in_shutdown_;
+
+ ResourceMetadataStorage* storage_;
+
+ FreeDiskSpaceGetterInterface* free_disk_space_getter_; // Not owned.
+
+ // IDs of files being write-opened.
+ std::map<std::string, int> write_opened_files_;
+
+ // IDs of files marked mounted.
+ std::set<std::string> mounted_files_;
+
+ base::ThreadChecker thread_checker_;
+
+ // Note: This should remain the last member so it'll be destroyed and
+ // invalidate its weak pointers before any other members are destroyed.
+ // This object should be accessed only on |blocking_task_runner_|.
+ base::WeakPtrFactory<FileCache> weak_ptr_factory_;
+ DISALLOW_COPY_AND_ASSIGN(FileCache);
+};
+
+} // namespace internal
+} // namespace drive
+
+#endif // COMPONENTS_DRIVE_FILE_CACHE_H_
diff --git a/components/drive/file_cache_unittest.cc b/components/drive/file_cache_unittest.cc
new file mode 100644
index 0000000..c9ecc1c
--- /dev/null
+++ b/components/drive/file_cache_unittest.cc
@@ -0,0 +1,562 @@
+// 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_cache.h"
+
+#include <string>
+#include <vector>
+
+#include "base/callback_helpers.h"
+#include "base/files/file_enumerator.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/md5.h"
+#include "base/path_service.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/resource_metadata_storage.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 {
+namespace {
+
+const char kCacheFileDirectory[] = "files";
+
+} // namespace
+
+// Tests FileCache methods working with the blocking task runner.
+class FileCacheTest : public testing::Test {
+ protected:
+ void SetUp() override {
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+ const base::FilePath metadata_dir = temp_dir_.path().AppendASCII("meta");
+ cache_files_dir_ = temp_dir_.path().AppendASCII(kCacheFileDirectory);
+
+ ASSERT_TRUE(base::CreateDirectory(metadata_dir));
+ ASSERT_TRUE(base::CreateDirectory(cache_files_dir_));
+
+ fake_free_disk_space_getter_.reset(new FakeFreeDiskSpaceGetter);
+
+ metadata_storage_.reset(new ResourceMetadataStorage(
+ metadata_dir,
+ base::ThreadTaskRunnerHandle::Get().get()));
+ ASSERT_TRUE(metadata_storage_->Initialize());
+
+ cache_.reset(new FileCache(
+ metadata_storage_.get(),
+ cache_files_dir_,
+ base::ThreadTaskRunnerHandle::Get().get(),
+ fake_free_disk_space_getter_.get()));
+ ASSERT_TRUE(cache_->Initialize());
+ }
+
+ static bool RenameCacheFilesToNewFormat(FileCache* cache) {
+ return cache->RenameCacheFilesToNewFormat();
+ }
+
+ content::TestBrowserThreadBundle thread_bundle_;
+ base::ScopedTempDir temp_dir_;
+ base::FilePath cache_files_dir_;
+
+ scoped_ptr<ResourceMetadataStorage, test_util::DestroyHelperForTests>
+ metadata_storage_;
+ scoped_ptr<FileCache, test_util::DestroyHelperForTests> cache_;
+ scoped_ptr<FakeFreeDiskSpaceGetter> fake_free_disk_space_getter_;
+};
+
+TEST_F(FileCacheTest, RecoverFilesFromCacheDirectory) {
+ base::FilePath dir_source_root;
+ EXPECT_TRUE(PathService::Get(base::DIR_SOURCE_ROOT, &dir_source_root));
+ const base::FilePath src_path =
+ dir_source_root.AppendASCII("chrome/test/data/chromeos/drive/image.png");
+
+ // Store files. This file should not be moved.
+ ResourceEntry entry;
+ entry.set_local_id("id_foo");
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->PutEntry(entry));
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Store("id_foo", "md5", src_path,
+ FileCache::FILE_OPERATION_COPY));
+
+ // Set up files in the cache directory. These files should be moved.
+ const base::FilePath file_directory =
+ temp_dir_.path().AppendASCII(kCacheFileDirectory);
+ ASSERT_TRUE(base::CopyFile(src_path, file_directory.AppendASCII("id_bar")));
+ ASSERT_TRUE(base::CopyFile(src_path, file_directory.AppendASCII("id_baz")));
+
+ // Insert a dirty entry with "id_baz" to |recovered_cache_info|.
+ // This should not prevent the file from being recovered.
+ ResourceMetadataStorage::RecoveredCacheInfoMap recovered_cache_info;
+ recovered_cache_info["id_baz"].is_dirty = true;
+ recovered_cache_info["id_baz"].title = "baz.png";
+
+ // Recover files.
+ const base::FilePath dest_directory = temp_dir_.path().AppendASCII("dest");
+ EXPECT_TRUE(cache_->RecoverFilesFromCacheDirectory(dest_directory,
+ recovered_cache_info));
+
+ // Only two files should be recovered.
+ EXPECT_TRUE(base::PathExists(dest_directory));
+ // base::FileEnumerator does not guarantee the order.
+ if (base::PathExists(dest_directory.AppendASCII("baz00000001.png"))) {
+ EXPECT_TRUE(base::ContentsEqual(
+ src_path,
+ dest_directory.AppendASCII("baz00000001.png")));
+ EXPECT_TRUE(base::ContentsEqual(
+ src_path,
+ dest_directory.AppendASCII("image00000002.png")));
+ } else {
+ EXPECT_TRUE(base::ContentsEqual(
+ src_path,
+ dest_directory.AppendASCII("image00000001.png")));
+ EXPECT_TRUE(base::ContentsEqual(
+ src_path,
+ dest_directory.AppendASCII("baz00000002.png")));
+ }
+ EXPECT_FALSE(base::PathExists(
+ dest_directory.AppendASCII("image00000003.png")));
+}
+
+TEST_F(FileCacheTest, FreeDiskSpaceIfNeededFor) {
+ base::FilePath src_file;
+ ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir_.path(), &src_file));
+
+ // Store a file as a 'temporary' file and remember the path.
+ const std::string id_tmp = "id_tmp", md5_tmp = "md5_tmp";
+
+ ResourceEntry entry;
+ entry.set_local_id(id_tmp);
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->PutEntry(entry));
+ ASSERT_EQ(FILE_ERROR_OK,
+ cache_->Store(id_tmp, md5_tmp, src_file,
+ FileCache::FILE_OPERATION_COPY));
+ base::FilePath tmp_path;
+ ASSERT_EQ(FILE_ERROR_OK, cache_->GetFile(id_tmp, &tmp_path));
+
+ // Store a file as a pinned file and remember the path.
+ const std::string id_pinned = "id_pinned", md5_pinned = "md5_pinned";
+ entry.Clear();
+ entry.set_local_id(id_pinned);
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->PutEntry(entry));
+ ASSERT_EQ(FILE_ERROR_OK,
+ cache_->Store(id_pinned, md5_pinned, src_file,
+ FileCache::FILE_OPERATION_COPY));
+ ASSERT_EQ(FILE_ERROR_OK, cache_->Pin(id_pinned));
+ base::FilePath pinned_path;
+ ASSERT_EQ(FILE_ERROR_OK, cache_->GetFile(id_pinned, &pinned_path));
+
+ // Call FreeDiskSpaceIfNeededFor().
+ fake_free_disk_space_getter_->set_default_value(test_util::kLotsOfSpace);
+ fake_free_disk_space_getter_->PushFakeValue(0);
+ const int64 kNeededBytes = 1;
+ EXPECT_TRUE(cache_->FreeDiskSpaceIfNeededFor(kNeededBytes));
+
+ // Only 'temporary' file gets removed.
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id_tmp, &entry));
+ EXPECT_FALSE(entry.file_specific_info().cache_state().is_present());
+ EXPECT_FALSE(base::PathExists(tmp_path));
+
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id_pinned, &entry));
+ EXPECT_TRUE(entry.file_specific_info().cache_state().is_present());
+ EXPECT_TRUE(base::PathExists(pinned_path));
+
+ // Returns false when disk space cannot be freed.
+ fake_free_disk_space_getter_->set_default_value(0);
+ EXPECT_FALSE(cache_->FreeDiskSpaceIfNeededFor(kNeededBytes));
+}
+
+TEST_F(FileCacheTest, GetFile) {
+ const base::FilePath src_file_path = temp_dir_.path().Append("test.dat");
+ const std::string src_contents = "test";
+ EXPECT_TRUE(google_apis::test_util::WriteStringToFile(src_file_path,
+ src_contents));
+ std::string id("id1");
+ std::string md5(base::MD5String(src_contents));
+
+ const base::FilePath cache_file_directory =
+ temp_dir_.path().AppendASCII(kCacheFileDirectory);
+
+ // Try to get an existing file from cache.
+ ResourceEntry entry;
+ entry.set_local_id(id);
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->PutEntry(entry));
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Store(id, md5, src_file_path,
+ FileCache::FILE_OPERATION_COPY));
+ base::FilePath cache_file_path;
+ EXPECT_EQ(FILE_ERROR_OK, cache_->GetFile(id, &cache_file_path));
+ EXPECT_EQ(
+ cache_file_directory.AppendASCII(util::EscapeCacheFileName(id)).value(),
+ cache_file_path.value());
+
+ std::string contents;
+ EXPECT_TRUE(base::ReadFileToString(cache_file_path, &contents));
+ EXPECT_EQ(src_contents, contents);
+
+ // Get file from cache with different id.
+ id = "id2";
+ entry.Clear();
+ entry.set_local_id(id);
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->PutEntry(entry));
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, cache_->GetFile(id, &cache_file_path));
+
+ // Pin a non-existent file.
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Pin(id));
+
+ // Get the non-existent pinned file from cache.
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, cache_->GetFile(id, &cache_file_path));
+
+ // Get a previously pinned and stored file from cache.
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Store(id, md5, src_file_path,
+ FileCache::FILE_OPERATION_COPY));
+
+ EXPECT_EQ(FILE_ERROR_OK, cache_->GetFile(id, &cache_file_path));
+ EXPECT_EQ(
+ cache_file_directory.AppendASCII(util::EscapeCacheFileName(id)).value(),
+ cache_file_path.value());
+
+ contents.clear();
+ EXPECT_TRUE(base::ReadFileToString(cache_file_path, &contents));
+ EXPECT_EQ(src_contents, contents);
+}
+
+TEST_F(FileCacheTest, Store) {
+ const base::FilePath src_file_path = temp_dir_.path().Append("test.dat");
+ const std::string src_contents = "test";
+ EXPECT_TRUE(google_apis::test_util::WriteStringToFile(src_file_path,
+ src_contents));
+ std::string id("id");
+ std::string md5(base::MD5String(src_contents));
+
+ // Store a file.
+ ResourceEntry entry;
+ entry.set_local_id(id);
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->PutEntry(entry));
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Store(
+ id, md5, src_file_path, FileCache::FILE_OPERATION_COPY));
+
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id, &entry));
+ EXPECT_TRUE(entry.file_specific_info().cache_state().is_present());
+ EXPECT_EQ(md5, entry.file_specific_info().cache_state().md5());
+
+ base::FilePath cache_file_path;
+ EXPECT_EQ(FILE_ERROR_OK, cache_->GetFile(id, &cache_file_path));
+ EXPECT_TRUE(base::ContentsEqual(src_file_path, cache_file_path));
+
+ // Store a non-existent file.
+ EXPECT_EQ(FILE_ERROR_FAILED, cache_->Store(
+ id, md5, base::FilePath::FromUTF8Unsafe("non_existent_file"),
+ FileCache::FILE_OPERATION_COPY));
+
+ // Passing empty MD5 marks the entry as dirty.
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Store(
+ id, std::string(), src_file_path, FileCache::FILE_OPERATION_COPY));
+
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id, &entry));
+ EXPECT_TRUE(entry.file_specific_info().cache_state().is_present());
+ EXPECT_TRUE(entry.file_specific_info().cache_state().md5().empty());
+ EXPECT_TRUE(entry.file_specific_info().cache_state().is_dirty());
+
+ // No free space available.
+ fake_free_disk_space_getter_->set_default_value(0);
+
+ EXPECT_EQ(FILE_ERROR_NO_LOCAL_SPACE, cache_->Store(
+ id, md5, src_file_path, FileCache::FILE_OPERATION_COPY));
+}
+
+TEST_F(FileCacheTest, PinAndUnpin) {
+ const base::FilePath src_file_path = temp_dir_.path().Append("test.dat");
+ const std::string src_contents = "test";
+ EXPECT_TRUE(google_apis::test_util::WriteStringToFile(src_file_path,
+ src_contents));
+ std::string id("id_present");
+ std::string md5(base::MD5String(src_contents));
+
+ // Store a file.
+ ResourceEntry entry;
+ entry.set_local_id(id);
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->PutEntry(entry));
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Store(
+ id, md5, src_file_path, FileCache::FILE_OPERATION_COPY));
+
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id, &entry));
+ EXPECT_FALSE(entry.file_specific_info().cache_state().is_pinned());
+
+ // Pin the existing file.
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Pin(id));
+
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id, &entry));
+ EXPECT_TRUE(entry.file_specific_info().cache_state().is_pinned());
+
+ // Unpin the file.
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Unpin(id));
+
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id, &entry));
+ EXPECT_FALSE(entry.file_specific_info().cache_state().is_pinned());
+
+ // Pin a non-present file.
+ std::string id_non_present = "id_non_present";
+ entry.Clear();
+ entry.set_local_id(id_non_present);
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->PutEntry(entry));
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Pin(id_non_present));
+
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id_non_present, &entry));
+ EXPECT_TRUE(entry.file_specific_info().cache_state().is_pinned());
+
+ // Unpin the previously pinned non-existent file.
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Unpin(id_non_present));
+
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id_non_present, &entry));
+ EXPECT_FALSE(entry.file_specific_info().has_cache_state());
+
+ // Unpin a file that doesn't exist in cache and is not pinned.
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, cache_->Unpin("id_non_existent"));
+}
+
+TEST_F(FileCacheTest, MountUnmount) {
+ const base::FilePath src_file_path = temp_dir_.path().Append("test.dat");
+ const std::string src_contents = "test";
+ EXPECT_TRUE(google_apis::test_util::WriteStringToFile(src_file_path,
+ src_contents));
+ std::string id("id_present");
+ std::string md5(base::MD5String(src_contents));
+
+ // Store a file.
+ ResourceEntry entry;
+ entry.set_local_id(id);
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->PutEntry(entry));
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Store(
+ id, md5, src_file_path, FileCache::FILE_OPERATION_COPY));
+
+ // Mark the file mounted.
+ base::FilePath cache_file_path;
+ EXPECT_EQ(FILE_ERROR_OK, cache_->MarkAsMounted(id, &cache_file_path));
+
+ // Try to remove it.
+ EXPECT_EQ(FILE_ERROR_IN_USE, cache_->Remove(id));
+
+ // Clear mounted state of the file.
+ EXPECT_EQ(FILE_ERROR_OK, cache_->MarkAsUnmounted(cache_file_path));
+
+ // Try to remove again.
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Remove(id));
+}
+
+TEST_F(FileCacheTest, OpenForWrite) {
+ // Prepare a file.
+ base::FilePath src_file;
+ ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir_.path(), &src_file));
+
+ const std::string id = "id";
+ ResourceEntry entry;
+ entry.set_local_id(id);
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->PutEntry(entry));
+ ASSERT_EQ(FILE_ERROR_OK, cache_->Store(id, "md5", src_file,
+ FileCache::FILE_OPERATION_COPY));
+ EXPECT_EQ(0, entry.file_info().last_modified());
+
+ // Entry is not dirty nor opened.
+ EXPECT_FALSE(cache_->IsOpenedForWrite(id));
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id, &entry));
+ EXPECT_FALSE(entry.file_specific_info().cache_state().is_dirty());
+
+ // Open (1).
+ scoped_ptr<base::ScopedClosureRunner> file_closer1;
+ EXPECT_EQ(FILE_ERROR_OK, cache_->OpenForWrite(id, &file_closer1));
+ EXPECT_TRUE(cache_->IsOpenedForWrite(id));
+
+ // Entry is dirty.
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id, &entry));
+ EXPECT_TRUE(entry.file_specific_info().cache_state().is_dirty());
+
+ // Open (2).
+ scoped_ptr<base::ScopedClosureRunner> file_closer2;
+ EXPECT_EQ(FILE_ERROR_OK, cache_->OpenForWrite(id, &file_closer2));
+ EXPECT_TRUE(cache_->IsOpenedForWrite(id));
+
+ // Close (1).
+ file_closer1.reset();
+ base::RunLoop().RunUntilIdle();
+ EXPECT_TRUE(cache_->IsOpenedForWrite(id));
+
+ // last_modified is updated.
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id, &entry));
+ EXPECT_NE(0, entry.file_info().last_modified());
+
+ // Close (2).
+ file_closer2.reset();
+ base::RunLoop().RunUntilIdle();
+ EXPECT_FALSE(cache_->IsOpenedForWrite(id));
+
+ // Try to open non-existent file.
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND,
+ cache_->OpenForWrite("nonexistent_id", &file_closer1));
+}
+
+TEST_F(FileCacheTest, UpdateMd5) {
+ // Store test data.
+ const base::FilePath src_file_path = temp_dir_.path().Append("test.dat");
+ const std::string contents_before = "before";
+ EXPECT_TRUE(google_apis::test_util::WriteStringToFile(src_file_path,
+ contents_before));
+ std::string id("id1");
+ ResourceEntry entry;
+ entry.set_local_id(id);
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->PutEntry(entry));
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Store(id, base::MD5String(contents_before),
+ src_file_path,
+ FileCache::FILE_OPERATION_COPY));
+
+ // Modify the cache file.
+ scoped_ptr<base::ScopedClosureRunner> file_closer;
+ EXPECT_EQ(FILE_ERROR_OK, cache_->OpenForWrite(id, &file_closer));
+ base::FilePath cache_file_path;
+ EXPECT_EQ(FILE_ERROR_OK, cache_->GetFile(id, &cache_file_path));
+ const std::string contents_after = "after";
+ EXPECT_TRUE(google_apis::test_util::WriteStringToFile(cache_file_path,
+ contents_after));
+
+ // Cannot update MD5 of an opend file.
+ EXPECT_EQ(FILE_ERROR_IN_USE, cache_->UpdateMd5(id));
+
+ // Close file.
+ file_closer.reset();
+ base::RunLoop().RunUntilIdle();
+
+ // MD5 was cleared by OpenForWrite().
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id, &entry));
+ EXPECT_TRUE(entry.file_specific_info().cache_state().md5().empty());
+
+ // Update MD5.
+ EXPECT_EQ(FILE_ERROR_OK, cache_->UpdateMd5(id));
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id, &entry));
+ EXPECT_EQ(base::MD5String(contents_after),
+ entry.file_specific_info().cache_state().md5());
+}
+
+TEST_F(FileCacheTest, ClearDirty) {
+ // Prepare a file.
+ base::FilePath src_file;
+ ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir_.path(), &src_file));
+
+ const std::string id = "id";
+ ResourceEntry entry;
+ entry.set_local_id(id);
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->PutEntry(entry));
+ ASSERT_EQ(FILE_ERROR_OK, cache_->Store(id, "md5", src_file,
+ FileCache::FILE_OPERATION_COPY));
+
+ // Open the file.
+ scoped_ptr<base::ScopedClosureRunner> file_closer;
+ EXPECT_EQ(FILE_ERROR_OK, cache_->OpenForWrite(id, &file_closer));
+
+ // Entry is dirty.
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id, &entry));
+ EXPECT_TRUE(entry.file_specific_info().cache_state().is_dirty());
+
+ // Cannot clear the dirty bit of an opened entry.
+ EXPECT_EQ(FILE_ERROR_IN_USE, cache_->ClearDirty(id));
+
+ // Close the file and clear the dirty bit.
+ file_closer.reset();
+ base::RunLoop().RunUntilIdle();
+ EXPECT_EQ(FILE_ERROR_OK, cache_->ClearDirty(id));
+
+ // Entry is not dirty.
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->GetEntry(id, &entry));
+ EXPECT_FALSE(entry.file_specific_info().cache_state().is_dirty());
+}
+
+TEST_F(FileCacheTest, Remove) {
+ const base::FilePath src_file_path = temp_dir_.path().Append("test.dat");
+ const std::string src_contents = "test";
+ EXPECT_TRUE(google_apis::test_util::WriteStringToFile(src_file_path,
+ src_contents));
+ std::string id("id");
+ std::string md5(base::MD5String(src_contents));
+
+ // First store a file to cache.
+ ResourceEntry entry;
+ entry.set_local_id(id);
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->PutEntry(entry));
+ base::FilePath src_file;
+ ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir_.path(), &src_file));
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Store(
+ id, md5, src_file_path, FileCache::FILE_OPERATION_COPY));
+
+ base::FilePath cache_file_path;
+ EXPECT_EQ(FILE_ERROR_OK, cache_->GetFile(id, &cache_file_path));
+
+ // Then try to remove existing file from cache.
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Remove(id));
+ EXPECT_FALSE(base::PathExists(cache_file_path));
+}
+
+TEST_F(FileCacheTest, RenameCacheFilesToNewFormat) {
+ const base::FilePath file_directory =
+ temp_dir_.path().AppendASCII(kCacheFileDirectory);
+
+ // File with an old style "<prefix>:<ID>.<MD5>" name.
+ ASSERT_TRUE(google_apis::test_util::WriteStringToFile(
+ file_directory.AppendASCII("file:id_koo.md5"), "koo"));
+
+ // File with multiple extensions should be removed.
+ ASSERT_TRUE(google_apis::test_util::WriteStringToFile(
+ file_directory.AppendASCII("id_kyu.md5.mounted"), "kyu (mounted)"));
+ ASSERT_TRUE(google_apis::test_util::WriteStringToFile(
+ file_directory.AppendASCII("id_kyu.md5"), "kyu"));
+
+ // Rename and verify the result.
+ EXPECT_TRUE(RenameCacheFilesToNewFormat(cache_.get()));
+ std::string contents;
+ EXPECT_TRUE(base::ReadFileToString(file_directory.AppendASCII("id_koo"),
+ &contents));
+ EXPECT_EQ("koo", contents);
+ contents.clear();
+ EXPECT_TRUE(base::ReadFileToString(file_directory.AppendASCII("id_kyu"),
+ &contents));
+ EXPECT_EQ("kyu", contents);
+
+ // Rename again.
+ EXPECT_TRUE(RenameCacheFilesToNewFormat(cache_.get()));
+
+ // Files with new style names are not affected.
+ contents.clear();
+ EXPECT_TRUE(base::ReadFileToString(file_directory.AppendASCII("id_koo"),
+ &contents));
+ EXPECT_EQ("koo", contents);
+ contents.clear();
+ EXPECT_TRUE(base::ReadFileToString(file_directory.AppendASCII("id_kyu"),
+ &contents));
+ EXPECT_EQ("kyu", contents);
+}
+
+TEST_F(FileCacheTest, ClearAll) {
+ const std::string id("1a2b");
+ const std::string md5("abcdef0123456789");
+
+ // Store an existing file.
+ ResourceEntry entry;
+ entry.set_local_id(id);
+ EXPECT_EQ(FILE_ERROR_OK, metadata_storage_->PutEntry(entry));
+ base::FilePath src_file;
+ ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir_.path(), &src_file));
+ ASSERT_EQ(FILE_ERROR_OK,
+ cache_->Store(id, md5, src_file, FileCache::FILE_OPERATION_COPY));
+
+ // Clear cache.
+ EXPECT_TRUE(cache_->ClearAll());
+
+ // Verify that the cache is removed.
+ EXPECT_TRUE(base::IsDirectoryEmpty(cache_files_dir_));
+}
+
+} // namespace internal
+} // namespace drive
diff --git a/components/drive/file_system_core_util.cc b/components/drive/file_system_core_util.cc
new file mode 100644
index 0000000..1e9f00b
--- /dev/null
+++ b/components/drive/file_system_core_util.cc
@@ -0,0 +1,142 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/drive/file_system_core_util.h"
+
+#include <string>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/bind.h"
+#include "base/bind_helpers.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/i18n/icu_string_conversions.h"
+#include "base/json/json_file_value_serializer.h"
+#include "base/logging.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/prefs/pref_service.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "base/thread_task_runner_handle.h"
+#include "base/threading/sequenced_worker_pool.h"
+#include "components/drive/drive.pb.h"
+#include "components/drive/drive_pref_names.h"
+#include "components/drive/job_list.h"
+
+namespace drive {
+namespace util {
+
+namespace {
+
+std::string ReadStringFromGDocFile(const base::FilePath& file_path,
+ const std::string& key) {
+ const int64 kMaxGDocSize = 4096;
+ int64 file_size = 0;
+ if (!base::GetFileSize(file_path, &file_size) || file_size > kMaxGDocSize) {
+ LOG(WARNING) << "File too large to be a GDoc file " << file_path.value();
+ return std::string();
+ }
+
+ JSONFileValueDeserializer reader(file_path);
+ std::string error_message;
+ scoped_ptr<base::Value> root_value(reader.Deserialize(NULL, &error_message));
+ if (!root_value) {
+ LOG(WARNING) << "Failed to parse " << file_path.value() << " as JSON."
+ << " error = " << error_message;
+ return std::string();
+ }
+
+ base::DictionaryValue* dictionary_value = NULL;
+ std::string result;
+ if (!root_value->GetAsDictionary(&dictionary_value) ||
+ !dictionary_value->GetString(key, &result)) {
+ LOG(WARNING) << "No value for the given key is stored in "
+ << file_path.value() << ". key = " << key;
+ return std::string();
+ }
+
+ return result;
+}
+
+} // namespace
+
+const base::FilePath& GetDriveGrandRootPath() {
+ CR_DEFINE_STATIC_LOCAL(
+ base::FilePath, grand_root_path,
+ (base::FilePath::FromUTF8Unsafe(kDriveGrandRootDirName)));
+ return grand_root_path;
+}
+
+const base::FilePath& GetDriveMyDriveRootPath() {
+ CR_DEFINE_STATIC_LOCAL(
+ base::FilePath, drive_root_path,
+ (GetDriveGrandRootPath().AppendASCII(kDriveMyDriveRootDirName)));
+ return drive_root_path;
+}
+
+std::string EscapeCacheFileName(const std::string& filename) {
+ // This is based on net/base/escape.cc: net::(anonymous namespace)::Escape
+ std::string escaped;
+ for (size_t i = 0; i < filename.size(); ++i) {
+ char c = filename[i];
+ if (c == '%' || c == '.' || c == '/') {
+ base::StringAppendF(&escaped, "%%%02X", c);
+ } else {
+ escaped.push_back(c);
+ }
+ }
+ return escaped;
+}
+
+std::string UnescapeCacheFileName(const std::string& filename) {
+ std::string unescaped;
+ for (size_t i = 0; i < filename.size(); ++i) {
+ char c = filename[i];
+ if (c == '%' && i + 2 < filename.length()) {
+ c = (base::HexDigitToInt(filename[i + 1]) << 4) +
+ base::HexDigitToInt(filename[i + 2]);
+ i += 2;
+ }
+ unescaped.push_back(c);
+ }
+ return unescaped;
+}
+
+std::string NormalizeFileName(const std::string& input) {
+ DCHECK(base::IsStringUTF8(input));
+
+ std::string output;
+ if (!base::ConvertToUtf8AndNormalize(input, base::kCodepageUTF8, &output))
+ output = input;
+ base::ReplaceChars(output, "/", "_", &output);
+ if (!output.empty() && output.find_first_not_of('.', 0) == std::string::npos)
+ output = "_";
+ return output;
+}
+
+void EmptyFileOperationCallback(FileError error) {
+}
+
+bool CreateGDocFile(const base::FilePath& file_path,
+ const GURL& url,
+ const std::string& resource_id) {
+ std::string content =
+ base::StringPrintf("{\"url\": \"%s\", \"resource_id\": \"%s\"}",
+ url.spec().c_str(), resource_id.c_str());
+ return base::WriteFile(file_path, content.data(), content.size()) ==
+ static_cast<int>(content.size());
+}
+
+GURL ReadUrlFromGDocFile(const base::FilePath& file_path) {
+ return GURL(ReadStringFromGDocFile(file_path, "url"));
+}
+
+std::string ReadResourceIdFromGDocFile(const base::FilePath& file_path) {
+ return ReadStringFromGDocFile(file_path, "resource_id");
+}
+
+} // namespace util
+} // namespace drive
diff --git a/components/drive/file_system_core_util.h b/components/drive/file_system_core_util.h
new file mode 100644
index 0000000..c273cbe
--- /dev/null
+++ b/components/drive/file_system_core_util.h
@@ -0,0 +1,90 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_DRIVE_FILE_SYSTEM_CORE_UTIL_H_
+#define COMPONENTS_DRIVE_FILE_SYSTEM_CORE_UTIL_H_
+
+#include <string>
+
+#include "base/callback_forward.h"
+#include "base/files/file_path.h"
+#include "components/drive/file_errors.h"
+#include "url/gurl.h"
+
+namespace drive {
+
+class DriveAppRegistry;
+class DriveServiceInterface;
+class FileSystemInterface;
+
+namespace util {
+
+// "drive" diretory's local ID is fixed to this value.
+const char kDriveGrandRootLocalId[] = "<drive>";
+
+// "drive/other" diretory's local ID is fixed to this value.
+const char kDriveOtherDirLocalId[] = "<other>";
+
+// "drive/trash" diretory's local ID is fixed to this value.
+const char kDriveTrashDirLocalId[] = "<trash>";
+
+// The directory names used for the Google Drive file system tree. These names
+// are used in URLs for the file manager, hence user-visible.
+const char kDriveGrandRootDirName[] = "drive";
+const char kDriveMyDriveRootDirName[] = "root";
+const char kDriveOtherDirName[] = "other";
+const char kDriveTrashDirName[] = "trash";
+
+// Returns the path of the top root of the pseudo tree.
+const base::FilePath& GetDriveGrandRootPath();
+
+// Returns the path of the directory representing "My Drive".
+const base::FilePath& GetDriveMyDriveRootPath();
+
+// Escapes a file name in Drive cache.
+// Replaces percent ('%'), period ('.') and slash ('/') with %XX (hex)
+std::string EscapeCacheFileName(const std::string& filename);
+
+// Unescapes a file path in Drive cache.
+// This is the inverse of EscapeCacheFileName.
+std::string UnescapeCacheFileName(const std::string& filename);
+
+// Converts the given string to a form suitable as a file name. Specifically,
+// - Normalizes in Unicode Normalization Form C.
+// - Replaces slashes '/' with '_'.
+// - Replaces the whole input with "_" if the all input characters are '.'.
+// |input| must be a valid UTF-8 encoded string.
+std::string NormalizeFileName(const std::string& input);
+
+// Does nothing with |error|. Used with functions taking FileOperationCallback.
+void EmptyFileOperationCallback(FileError error);
+
+// Helper to destroy objects which needs Destroy() to be called on destruction.
+struct DestroyHelper {
+ template <typename T>
+ void operator()(T* object) const {
+ if (object)
+ object->Destroy();
+ }
+};
+
+// Creates a GDoc file with given values.
+//
+// GDoc files are used to represent hosted documents on local filesystems.
+// A GDoc file contains a JSON whose content is a URL to view the document and
+// a resource ID of the entry.
+bool CreateGDocFile(const base::FilePath& file_path,
+ const GURL& url,
+ const std::string& resource_id);
+
+// Reads URL from a GDoc file.
+GURL ReadUrlFromGDocFile(const base::FilePath& file_path);
+
+// Reads resource ID from a GDoc file.
+std::string ReadResourceIdFromGDocFile(const base::FilePath& file_path);
+
+} // namespace util
+} // namespace drive
+
+#endif // COMPONENTS_DRIVE_FILE_SYSTEM_CORE_UTIL_H_
diff --git a/components/drive/file_system_core_util_unittest.cc b/components/drive/file_system_core_util_unittest.cc
new file mode 100644
index 0000000..663da28
--- /dev/null
+++ b/components/drive/file_system_core_util_unittest.cc
@@ -0,0 +1,99 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/drive/file_system_core_util.h"
+
+#include <vector>
+
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/single_thread_task_runner.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/thread_task_runner_handle.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 util {
+
+class FileSystemUtilTest : public testing::Test {
+ content::TestBrowserThreadBundle thread_bundle_;
+};
+
+TEST_F(FileSystemUtilTest, EscapeUnescapeCacheFileName) {
+ const std::string kUnescapedFileName(
+ "tmp:`~!@#$%^&*()-_=+[{|]}\\\\;\',<.>/?");
+ const std::string kEscapedFileName(
+ "tmp:`~!@#$%25^&*()-_=+[{|]}\\\\;\',<%2E>%2F?");
+ EXPECT_EQ(kEscapedFileName, EscapeCacheFileName(kUnescapedFileName));
+ EXPECT_EQ(kUnescapedFileName, UnescapeCacheFileName(kEscapedFileName));
+}
+
+TEST_F(FileSystemUtilTest, NormalizeFileName) {
+ EXPECT_EQ("", NormalizeFileName(""));
+ EXPECT_EQ("foo", NormalizeFileName("foo"));
+ // Slash
+ EXPECT_EQ("foo_zzz", NormalizeFileName("foo/zzz"));
+ EXPECT_EQ("___", NormalizeFileName("///"));
+ // Japanese hiragana "hi" + semi-voiced-mark is normalized to "pi".
+ EXPECT_EQ("\xE3\x81\xB4", NormalizeFileName("\xE3\x81\xB2\xE3\x82\x9A"));
+ // Dot
+ EXPECT_EQ("_", NormalizeFileName("."));
+ EXPECT_EQ("_", NormalizeFileName(".."));
+ EXPECT_EQ("_", NormalizeFileName("..."));
+ EXPECT_EQ(".bashrc", NormalizeFileName(".bashrc"));
+ EXPECT_EQ("._", NormalizeFileName("./"));
+}
+
+TEST_F(FileSystemUtilTest, GDocFile) {
+ base::ScopedTempDir temp_dir;
+ ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+ GURL url(
+ "https://docs.google.com/document/d/"
+ "1YsCnrMxxgp7LDdtlFDt-WdtEIth89vA9inrILtvK-Ug/edit");
+ std::string resource_id("1YsCnrMxxgp7LDdtlFDt-WdtEIth89vA9inrILtvK-Ug");
+
+ // Read and write gdoc.
+ base::FilePath file = temp_dir.path().AppendASCII("test.gdoc");
+ EXPECT_TRUE(CreateGDocFile(file, url, resource_id));
+ EXPECT_EQ(url, ReadUrlFromGDocFile(file));
+ EXPECT_EQ(resource_id, ReadResourceIdFromGDocFile(file));
+
+ // Read and write gsheet.
+ file = temp_dir.path().AppendASCII("test.gsheet");
+ EXPECT_TRUE(CreateGDocFile(file, url, resource_id));
+ EXPECT_EQ(url, ReadUrlFromGDocFile(file));
+ EXPECT_EQ(resource_id, ReadResourceIdFromGDocFile(file));
+
+ // Read and write gslides.
+ file = temp_dir.path().AppendASCII("test.gslides");
+ EXPECT_TRUE(CreateGDocFile(file, url, resource_id));
+ EXPECT_EQ(url, ReadUrlFromGDocFile(file));
+ EXPECT_EQ(resource_id, ReadResourceIdFromGDocFile(file));
+
+ // Read and write gdraw.
+ file = temp_dir.path().AppendASCII("test.gdraw");
+ EXPECT_TRUE(CreateGDocFile(file, url, resource_id));
+ EXPECT_EQ(url, ReadUrlFromGDocFile(file));
+ EXPECT_EQ(resource_id, ReadResourceIdFromGDocFile(file));
+
+ // Read and write gtable.
+ file = temp_dir.path().AppendASCII("test.gtable");
+ EXPECT_TRUE(CreateGDocFile(file, url, resource_id));
+ EXPECT_EQ(url, ReadUrlFromGDocFile(file));
+ EXPECT_EQ(resource_id, ReadResourceIdFromGDocFile(file));
+
+ // Non GDoc file.
+ file = temp_dir.path().AppendASCII("test.txt");
+ std::string data = "Hello world!";
+ EXPECT_TRUE(google_apis::test_util::WriteStringToFile(file, data));
+ EXPECT_TRUE(ReadUrlFromGDocFile(file).is_empty());
+ EXPECT_TRUE(ReadResourceIdFromGDocFile(file).empty());
+}
+
+} // namespace util
+} // namespace drive
diff --git a/components/drive/resource_entry_conversion.cc b/components/drive/resource_entry_conversion.cc
new file mode 100644
index 0000000..a4fa83d
--- /dev/null
+++ b/components/drive/resource_entry_conversion.cc
@@ -0,0 +1,141 @@
+// 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/resource_entry_conversion.h"
+
+#include <string>
+
+#include "base/logging.h"
+#include "base/time/time.h"
+#include "components/drive/drive.pb.h"
+#include "components/drive/drive_api_util.h"
+#include "components/drive/file_system_core_util.h"
+#include "google_apis/drive/drive_api_parser.h"
+
+namespace drive {
+
+bool ConvertChangeResourceToResourceEntry(
+ const google_apis::ChangeResource& input,
+ ResourceEntry* out_entry,
+ std::string* out_parent_resource_id) {
+ DCHECK(out_entry);
+ DCHECK(out_parent_resource_id);
+
+ ResourceEntry converted;
+ std::string parent_resource_id;
+ if (input.file() &&
+ !ConvertFileResourceToResourceEntry(*input.file(), &converted,
+ &parent_resource_id))
+ return false;
+
+ converted.set_resource_id(input.file_id());
+ converted.set_deleted(converted.deleted() || input.is_deleted());
+ converted.set_modification_date(input.modification_date().ToInternalValue());
+
+ out_entry->Swap(&converted);
+ swap(*out_parent_resource_id, parent_resource_id);
+ return true;
+}
+
+bool ConvertFileResourceToResourceEntry(
+ const google_apis::FileResource& input,
+ ResourceEntry* out_entry,
+ std::string* out_parent_resource_id) {
+ DCHECK(out_entry);
+ DCHECK(out_parent_resource_id);
+ ResourceEntry converted;
+
+ // For regular files, the 'filename' and 'title' attribute in the metadata
+ // may be different (e.g. due to rename). To be consistent with the web
+ // interface and other client to use the 'title' attribute, instead of
+ // 'filename', as the file name in the local snapshot.
+ converted.set_title(input.title());
+ converted.set_base_name(util::NormalizeFileName(converted.title()));
+ converted.set_resource_id(input.file_id());
+
+ // Gets parent Resource ID. On drive.google.com, a file can have multiple
+ // parents or no parent, but we are forcing a tree-shaped structure (i.e. no
+ // multi-parent or zero-parent entries). Therefore the first found "parent" is
+ // used for the entry. Tracked in http://crbug.com/158904.
+ std::string parent_resource_id;
+ if (!input.parents().empty())
+ parent_resource_id = input.parents()[0].file_id();
+
+ converted.set_deleted(input.labels().is_trashed());
+ converted.set_shared_with_me(!input.shared_with_me_date().is_null());
+ converted.set_shared(input.shared());
+
+ PlatformFileInfoProto* file_info = converted.mutable_file_info();
+
+ file_info->set_last_modified(input.modified_date().ToInternalValue());
+ // If the file has never been viewed (last_viewed_by_me_date().is_null() ==
+ // true), then we will set the last_accessed field in the protocol buffer to
+ // 0.
+ file_info->set_last_accessed(
+ input.last_viewed_by_me_date().ToInternalValue());
+ file_info->set_creation_time(input.created_date().ToInternalValue());
+
+ if (input.IsDirectory()) {
+ file_info->set_is_directory(true);
+ } else {
+ FileSpecificInfo* file_specific_info =
+ converted.mutable_file_specific_info();
+ if (!input.IsHostedDocument()) {
+ file_info->set_size(input.file_size());
+ file_specific_info->set_md5(input.md5_checksum());
+ file_specific_info->set_is_hosted_document(false);
+ } else {
+ // Attach .g<something> extension to hosted documents so we can special
+ // case their handling in UI.
+ // TODO(satorux): Figure out better way how to pass input info like kind
+ // to UI through the File API stack.
+ const std::string document_extension =
+ drive::util::GetHostedDocumentExtension(input.mime_type());
+ file_specific_info->set_document_extension(document_extension);
+ converted.set_base_name(
+ util::NormalizeFileName(converted.title() + document_extension));
+
+ // We don't know the size of hosted docs and it does not matter since
+ // it has no effect on the quota.
+ file_info->set_size(0);
+ file_specific_info->set_is_hosted_document(true);
+ }
+ file_info->set_is_directory(false);
+ file_specific_info->set_content_mime_type(input.mime_type());
+
+ if (!input.alternate_link().is_empty())
+ file_specific_info->set_alternate_url(input.alternate_link().spec());
+
+ const int64 image_width = input.image_media_metadata().width();
+ if (image_width != -1)
+ file_specific_info->set_image_width(image_width);
+
+ const int64 image_height = input.image_media_metadata().height();
+ if (image_height != -1)
+ file_specific_info->set_image_height(image_height);
+
+ const int64 image_rotation = input.image_media_metadata().rotation();
+ if (image_rotation != -1)
+ file_specific_info->set_image_rotation(image_rotation);
+ }
+
+ out_entry->Swap(&converted);
+ swap(*out_parent_resource_id, parent_resource_id);
+ return true;
+}
+
+void ConvertResourceEntryToFileInfo(const ResourceEntry& entry,
+ base::File::Info* file_info) {
+ file_info->size = entry.file_info().size();
+ file_info->is_directory = entry.file_info().is_directory();
+ file_info->is_symbolic_link = entry.file_info().is_symbolic_link();
+ file_info->last_modified = base::Time::FromInternalValue(
+ entry.file_info().last_modified());
+ file_info->last_accessed = base::Time::FromInternalValue(
+ entry.file_info().last_accessed());
+ file_info->creation_time = base::Time::FromInternalValue(
+ entry.file_info().creation_time());
+}
+
+} // namespace drive
diff --git a/components/drive/resource_entry_conversion.h b/components/drive/resource_entry_conversion.h
new file mode 100644
index 0000000..2de228e
--- /dev/null
+++ b/components/drive/resource_entry_conversion.h
@@ -0,0 +1,53 @@
+// 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_RESOURCE_ENTRY_CONVERSION_H_
+#define COMPONENTS_DRIVE_RESOURCE_ENTRY_CONVERSION_H_
+
+#include <string>
+
+#include "base/files/file.h"
+
+namespace google_apis {
+class ChangeResource;
+class FileResource;
+}
+
+namespace drive {
+
+class ResourceEntry;
+
+// Converts a google_apis::ChangeResource into a drive::ResourceEntry.
+// If the conversion succeeded, return true and sets the result to |out_entry|.
+// |out_parent_resource_id| will be set to the resource ID of the parent entry.
+// If failed, it returns false and keeps output arguments untouched.
+//
+// Every entry is guaranteed to have one parent resource ID in ResourceMetadata.
+// This requirement is needed to represent contents in Drive as a file system
+// tree, and achieved as follows:
+//
+// 1) Entries without parents are allowed on drive.google.com. These entries are
+// collected to "drive/other", and have "drive/other" as the parent.
+//
+// 2) Entries with multiple parents are allowed on drive.google.com. For these
+// entries, the first parent is chosen.
+bool ConvertChangeResourceToResourceEntry(
+ const google_apis::ChangeResource& input,
+ ResourceEntry* out_entry,
+ std::string* out_parent_resource_id);
+
+// Converts a google_apis::FileResource into a drive::ResourceEntry.
+// Also see the comment for ConvertChangeResourceToResourceEntry above.
+bool ConvertFileResourceToResourceEntry(
+ const google_apis::FileResource& input,
+ ResourceEntry* out_entry,
+ std::string* out_parent_resource_id);
+
+// Converts the resource entry to the platform file info.
+void ConvertResourceEntryToFileInfo(const ResourceEntry& entry,
+ base::File::Info* file_info);
+
+} // namespace drive
+
+#endif // COMPONENTS_DRIVE_RESOURCE_ENTRY_CONVERSION_H_
diff --git a/components/drive/resource_entry_conversion_unittest.cc b/components/drive/resource_entry_conversion_unittest.cc
new file mode 100644
index 0000000..b3fe252
--- /dev/null
+++ b/components/drive/resource_entry_conversion_unittest.cc
@@ -0,0 +1,374 @@
+// 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/resource_entry_conversion.h"
+
+#include "base/time/time.h"
+#include "components/drive/drive.pb.h"
+#include "components/drive/drive_api_util.h"
+#include "google_apis/drive/drive_api_parser.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace drive {
+
+namespace {
+
+base::Time GetTestTime() {
+ // 2011-12-14-T00:40:47.330Z
+ base::Time::Exploded exploded;
+ exploded.year = 2011;
+ exploded.month = 12;
+ exploded.day_of_month = 14;
+ exploded.day_of_week = 2; // Tuesday
+ exploded.hour = 0;
+ exploded.minute = 40;
+ exploded.second = 47;
+ exploded.millisecond = 330;
+ return base::Time::FromUTCExploded(exploded);
+}
+
+} // namespace
+
+TEST(ResourceEntryConversionTest, ConvertToResourceEntry_File) {
+ google_apis::FileResource file_resource;
+ file_resource.set_title("File 1.mp3");
+ file_resource.set_file_id("resource_id");
+ file_resource.set_created_date(GetTestTime());
+ file_resource.set_modified_date(
+ GetTestTime() + base::TimeDelta::FromSeconds(10));
+ file_resource.set_mime_type("audio/mpeg");
+ file_resource.set_alternate_link(GURL("https://file_link_alternate"));
+ file_resource.set_file_size(892721);
+ file_resource.set_md5_checksum("3b4382ebefec6e743578c76bbd0575ce");
+
+ ResourceEntry entry;
+ std::string parent_resource_id;
+ EXPECT_TRUE(ConvertFileResourceToResourceEntry(
+ file_resource, &entry, &parent_resource_id));
+
+ EXPECT_EQ(file_resource.title(), entry.title());
+ EXPECT_EQ(file_resource.title(), entry.base_name());
+ EXPECT_EQ(file_resource.file_id(), entry.resource_id());
+ EXPECT_EQ("", parent_resource_id);
+
+ EXPECT_FALSE(entry.deleted());
+ EXPECT_FALSE(entry.shared_with_me());
+ EXPECT_FALSE(entry.shared());
+
+ EXPECT_EQ(file_resource.modified_date().ToInternalValue(),
+ entry.file_info().last_modified());
+ // Last accessed value equal to 0 means that the file has never been viewed.
+ EXPECT_EQ(0, entry.file_info().last_accessed());
+ EXPECT_EQ(file_resource.created_date().ToInternalValue(),
+ entry.file_info().creation_time());
+
+ EXPECT_EQ(file_resource.mime_type(),
+ entry.file_specific_info().content_mime_type());
+ EXPECT_FALSE(entry.file_specific_info().is_hosted_document());
+ EXPECT_EQ(file_resource.alternate_link().spec(),
+ entry.file_specific_info().alternate_url());
+
+ // Regular file specific fields.
+ EXPECT_EQ(file_resource.file_size(), entry.file_info().size());
+ EXPECT_EQ(file_resource.md5_checksum(), entry.file_specific_info().md5());
+ EXPECT_FALSE(entry.file_info().is_directory());
+}
+
+TEST(ResourceEntryConversionTest,
+ ConvertFileResourceToResourceEntry_HostedDocument) {
+ google_apis::FileResource file_resource;
+ file_resource.set_title("Document 1");
+ file_resource.set_file_id("resource_id");
+ file_resource.set_created_date(GetTestTime());
+ file_resource.set_modified_date(
+ GetTestTime() + base::TimeDelta::FromSeconds(10));
+ file_resource.set_last_viewed_by_me_date(
+ GetTestTime() + base::TimeDelta::FromSeconds(20));
+ file_resource.set_mime_type(util::kGoogleDocumentMimeType);
+ file_resource.set_alternate_link(GURL("https://file_link_alternate"));
+ // Do not set file size to represent a hosted document.
+
+ ResourceEntry entry;
+ std::string parent_resource_id;
+ EXPECT_TRUE(ConvertFileResourceToResourceEntry(
+ file_resource, &entry, &parent_resource_id));
+
+ EXPECT_EQ(file_resource.title(), entry.title());
+ EXPECT_EQ(file_resource.title() + ".gdoc",
+ entry.base_name()); // The suffix added.
+ EXPECT_EQ(".gdoc", entry.file_specific_info().document_extension());
+ EXPECT_EQ(file_resource.file_id(), entry.resource_id());
+ EXPECT_EQ("", parent_resource_id);
+
+ EXPECT_FALSE(entry.deleted());
+ EXPECT_FALSE(entry.shared_with_me());
+ EXPECT_FALSE(entry.shared());
+
+ EXPECT_EQ(file_resource.modified_date().ToInternalValue(),
+ entry.file_info().last_modified());
+ EXPECT_EQ(file_resource.last_viewed_by_me_date().ToInternalValue(),
+ entry.file_info().last_accessed());
+ EXPECT_EQ(file_resource.created_date().ToInternalValue(),
+ entry.file_info().creation_time());
+
+ EXPECT_EQ(file_resource.mime_type(),
+ entry.file_specific_info().content_mime_type());
+ EXPECT_TRUE(entry.file_specific_info().is_hosted_document());
+ EXPECT_EQ(file_resource.alternate_link().spec(),
+ entry.file_specific_info().alternate_url());
+
+ // The size should be 0 for a hosted document.
+ EXPECT_EQ(0, entry.file_info().size());
+ EXPECT_FALSE(entry.file_info().is_directory());
+}
+
+TEST(ResourceEntryConversionTest,
+ ConvertFileResourceToResourceEntry_Directory) {
+ google_apis::FileResource file_resource;
+ file_resource.set_title("Folder");
+ file_resource.set_file_id("resource_id");
+ file_resource.set_created_date(GetTestTime());
+ file_resource.set_modified_date(
+ GetTestTime() + base::TimeDelta::FromSeconds(10));
+ file_resource.set_last_viewed_by_me_date(
+ GetTestTime() + base::TimeDelta::FromSeconds(20));
+ file_resource.set_mime_type(util::kDriveFolderMimeType);
+
+ google_apis::ParentReference parent;
+ parent.set_file_id("parent_resource_id");
+ file_resource.mutable_parents()->push_back(parent);
+
+ ResourceEntry entry;
+ std::string parent_resource_id;
+ EXPECT_TRUE(ConvertFileResourceToResourceEntry(
+ file_resource, &entry, &parent_resource_id));
+
+ EXPECT_EQ(file_resource.title(), entry.title());
+ EXPECT_EQ(file_resource.title(), entry.base_name());
+ EXPECT_EQ(file_resource.file_id(), entry.resource_id());
+ // The parent resource ID should be obtained as this is a sub directory
+ // under a non-root directory.
+ EXPECT_EQ(parent.file_id(), parent_resource_id);
+
+ EXPECT_FALSE(entry.deleted());
+ EXPECT_FALSE(entry.shared_with_me());
+ EXPECT_FALSE(entry.shared());
+
+ EXPECT_EQ(file_resource.modified_date().ToInternalValue(),
+ entry.file_info().last_modified());
+ EXPECT_EQ(file_resource.last_viewed_by_me_date().ToInternalValue(),
+ entry.file_info().last_accessed());
+ EXPECT_EQ(file_resource.created_date().ToInternalValue(),
+ entry.file_info().creation_time());
+
+ EXPECT_TRUE(entry.file_info().is_directory());
+}
+
+TEST(ResourceEntryConversionTest,
+ ConvertFileResourceToResourceEntry_DeletedHostedDocument) {
+ google_apis::FileResource file_resource;
+ file_resource.set_title("Document 1");
+ file_resource.set_file_id("resource_id");
+ file_resource.set_created_date(GetTestTime());
+ file_resource.set_modified_date(
+ GetTestTime() + base::TimeDelta::FromSeconds(10));
+ file_resource.set_last_viewed_by_me_date(
+ GetTestTime() + base::TimeDelta::FromSeconds(20));
+ file_resource.set_mime_type(util::kGoogleDocumentMimeType);
+ file_resource.set_alternate_link(GURL("https://file_link_alternate"));
+ file_resource.mutable_labels()->set_trashed(true);
+
+ ResourceEntry entry;
+ std::string parent_resource_id;
+ EXPECT_TRUE(ConvertFileResourceToResourceEntry(
+ file_resource, &entry, &parent_resource_id));
+
+ EXPECT_EQ(file_resource.title(), entry.title());
+ EXPECT_EQ(file_resource.title() + ".gdoc", entry.base_name());
+ EXPECT_EQ(file_resource.file_id(), entry.resource_id());
+ EXPECT_EQ("", parent_resource_id);
+
+ EXPECT_TRUE(entry.deleted()); // The document was deleted.
+ EXPECT_FALSE(entry.shared_with_me());
+ EXPECT_FALSE(entry.shared());
+
+ EXPECT_EQ(file_resource.modified_date().ToInternalValue(),
+ entry.file_info().last_modified());
+ EXPECT_EQ(file_resource.last_viewed_by_me_date().ToInternalValue(),
+ entry.file_info().last_accessed());
+ EXPECT_EQ(file_resource.created_date().ToInternalValue(),
+ entry.file_info().creation_time());
+
+ EXPECT_EQ(file_resource.mime_type(),
+ entry.file_specific_info().content_mime_type());
+ EXPECT_TRUE(entry.file_specific_info().is_hosted_document());
+ EXPECT_EQ(file_resource.alternate_link().spec(),
+ entry.file_specific_info().alternate_url());
+
+ // The size should be 0 for a hosted document.
+ EXPECT_EQ(0, entry.file_info().size());
+}
+
+TEST(ResourceEntryConversionTest, ConvertChangeResourceToResourceEntry) {
+ google_apis::ChangeResource change_resource;
+ change_resource.set_file(make_scoped_ptr(new google_apis::FileResource));
+ change_resource.set_file_id("resource_id");
+ change_resource.set_modification_date(GetTestTime());
+
+ google_apis::FileResource* file_resource = change_resource.mutable_file();
+ file_resource->set_title("File 1.mp3");
+ file_resource->set_file_id("resource_id");
+ // Set dummy file size to declare that this is a regular file.
+ file_resource->set_file_size(12345);
+
+ ResourceEntry entry;
+ std::string parent_resource_id;
+ EXPECT_TRUE(ConvertChangeResourceToResourceEntry(
+ change_resource, &entry, &parent_resource_id));
+
+ EXPECT_EQ(change_resource.file_id(), entry.resource_id());
+ EXPECT_EQ(change_resource.modification_date().ToInternalValue(),
+ entry.modification_date());
+
+ EXPECT_EQ(file_resource->title(), entry.title());
+ EXPECT_EQ(file_resource->title(), entry.base_name());
+ EXPECT_EQ("", parent_resource_id);
+
+ EXPECT_FALSE(entry.deleted());
+}
+
+TEST(ResourceEntryConversionTest,
+ ConvertChangeResourceToResourceEntry_Trashed) {
+ google_apis::ChangeResource change_resource;
+ change_resource.set_file(make_scoped_ptr(new google_apis::FileResource));
+ change_resource.set_file_id("resource_id");
+ change_resource.set_modification_date(GetTestTime());
+
+ google_apis::FileResource* file_resource = change_resource.mutable_file();
+ file_resource->set_title("File 1.mp3");
+ file_resource->set_file_id("resource_id");
+ // Set dummy file size to declare that this is a regular file.
+ file_resource->set_file_size(12345);
+ file_resource->mutable_labels()->set_trashed(true);
+
+ ResourceEntry entry;
+ std::string parent_resource_id;
+ EXPECT_TRUE(ConvertChangeResourceToResourceEntry(
+ change_resource, &entry, &parent_resource_id));
+
+ EXPECT_EQ(change_resource.file_id(), entry.resource_id());
+ EXPECT_EQ(change_resource.modification_date().ToInternalValue(),
+ entry.modification_date());
+
+ EXPECT_EQ(file_resource->title(), entry.title());
+ EXPECT_EQ(file_resource->title(), entry.base_name());
+ EXPECT_EQ("", parent_resource_id);
+
+ EXPECT_TRUE(entry.deleted());
+}
+
+TEST(ResourceEntryConversionTest,
+ ConvertChangeResourceToResourceEntry_Deleted) {
+ google_apis::ChangeResource change_resource;
+ change_resource.set_deleted(true);
+ change_resource.set_file_id("resource_id");
+ change_resource.set_modification_date(GetTestTime());
+
+ ResourceEntry entry;
+ std::string parent_resource_id;
+ EXPECT_TRUE(ConvertChangeResourceToResourceEntry(
+ change_resource, &entry, &parent_resource_id));
+
+ EXPECT_EQ(change_resource.file_id(), entry.resource_id());
+ EXPECT_EQ("", parent_resource_id);
+
+ EXPECT_TRUE(entry.deleted());
+
+ EXPECT_EQ(change_resource.modification_date().ToInternalValue(),
+ entry.modification_date());
+}
+
+TEST(ResourceEntryConversionTest,
+ ConvertFileResourceToResourceEntry_SharedWithMeEntry) {
+ google_apis::FileResource file_resource;
+ file_resource.set_shared(true);
+ file_resource.set_shared_with_me_date(GetTestTime());
+
+ ResourceEntry entry;
+ std::string parent_resource_id;
+ EXPECT_TRUE(ConvertFileResourceToResourceEntry(
+ file_resource, &entry, &parent_resource_id));
+ EXPECT_TRUE(entry.shared_with_me());
+ EXPECT_TRUE(entry.shared());
+}
+
+TEST(ResourceEntryConversionTest, ToPlatformFileInfo) {
+ ResourceEntry entry;
+ entry.mutable_file_info()->set_size(12345);
+ entry.mutable_file_info()->set_is_directory(true);
+ entry.mutable_file_info()->set_is_symbolic_link(true);
+ entry.mutable_file_info()->set_creation_time(999);
+ entry.mutable_file_info()->set_last_modified(123456789);
+ entry.mutable_file_info()->set_last_accessed(987654321);
+
+ base::File::Info file_info;
+ ConvertResourceEntryToFileInfo(entry, &file_info);
+ EXPECT_EQ(entry.file_info().size(), file_info.size);
+ EXPECT_EQ(entry.file_info().is_directory(), file_info.is_directory);
+ EXPECT_EQ(entry.file_info().is_symbolic_link(), file_info.is_symbolic_link);
+ EXPECT_EQ(base::Time::FromInternalValue(entry.file_info().creation_time()),
+ file_info.creation_time);
+ EXPECT_EQ(base::Time::FromInternalValue(entry.file_info().last_modified()),
+ file_info.last_modified);
+ EXPECT_EQ(base::Time::FromInternalValue(entry.file_info().last_accessed()),
+ file_info.last_accessed);
+}
+
+TEST(ResourceEntryConversionTest,
+ ConvertFileResourceToResourceEntry_ImageMediaMetadata) {
+ google_apis::FileResource entry_all_fields;
+ google_apis::FileResource entry_zero_fields;
+ google_apis::FileResource entry_no_fields;
+
+ entry_all_fields.mutable_image_media_metadata()->set_width(640);
+ entry_all_fields.mutable_image_media_metadata()->set_height(480);
+ entry_all_fields.mutable_image_media_metadata()->set_rotation(90);
+
+ entry_zero_fields.mutable_image_media_metadata()->set_width(0);
+ entry_zero_fields.mutable_image_media_metadata()->set_height(0);
+ entry_zero_fields.mutable_image_media_metadata()->set_rotation(0);
+
+ {
+ ResourceEntry entry;
+ std::string parent_resource_id;
+ EXPECT_TRUE(ConvertFileResourceToResourceEntry(
+ entry_all_fields, &entry, &parent_resource_id));
+ EXPECT_EQ(640, entry.file_specific_info().image_width());
+ EXPECT_EQ(480, entry.file_specific_info().image_height());
+ EXPECT_EQ(90, entry.file_specific_info().image_rotation());
+ }
+ {
+ ResourceEntry entry;
+ std::string parent_resource_id;
+ EXPECT_TRUE(ConvertFileResourceToResourceEntry(
+ entry_zero_fields, &entry, &parent_resource_id));
+ EXPECT_TRUE(entry.file_specific_info().has_image_width());
+ EXPECT_TRUE(entry.file_specific_info().has_image_height());
+ EXPECT_TRUE(entry.file_specific_info().has_image_rotation());
+ EXPECT_EQ(0, entry.file_specific_info().image_width());
+ EXPECT_EQ(0, entry.file_specific_info().image_height());
+ EXPECT_EQ(0, entry.file_specific_info().image_rotation());
+ }
+ {
+ ResourceEntry entry;
+ std::string parent_resource_id;
+ EXPECT_TRUE(ConvertFileResourceToResourceEntry(
+ entry_no_fields, &entry, &parent_resource_id));
+ EXPECT_FALSE(entry.file_specific_info().has_image_width());
+ EXPECT_FALSE(entry.file_specific_info().has_image_height());
+ EXPECT_FALSE(entry.file_specific_info().has_image_rotation());
+ }
+}
+
+} // namespace drive
diff --git a/components/drive/resource_metadata.cc b/components/drive/resource_metadata.cc
new file mode 100644
index 0000000..25250ff
--- /dev/null
+++ b/components/drive/resource_metadata.cc
@@ -0,0 +1,607 @@
+// 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/resource_metadata.h"
+
+#include "base/bind.h"
+#include "base/bind_helpers.h"
+#include "base/guid.h"
+#include "base/location.h"
+#include "base/rand_util.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
+#include "base/sys_info.h"
+#include "components/drive/drive.pb.h"
+#include "components/drive/file_cache.h"
+#include "components/drive/file_system_core_util.h"
+#include "components/drive/resource_metadata_storage.h"
+
+namespace drive {
+namespace internal {
+namespace {
+
+// Returns true if enough disk space is available for DB operation.
+// TODO(hashimoto): Merge this with FileCache's FreeDiskSpaceGetterInterface.
+bool EnoughDiskSpaceIsAvailableForDBOperation(const base::FilePath& path) {
+ const int64 kRequiredDiskSpaceInMB = 128; // 128 MB seems to be large enough.
+ return base::SysInfo::AmountOfFreeDiskSpace(path) >=
+ kRequiredDiskSpaceInMB * (1 << 20);
+}
+
+// Returns a file name with a uniquifier appended. (e.g. "File (1).txt")
+std::string GetUniquifiedName(const std::string& name, int uniquifier) {
+ base::FilePath name_path = base::FilePath::FromUTF8Unsafe(name);
+ name_path = name_path.InsertBeforeExtensionASCII(
+ base::StringPrintf(" (%d)", uniquifier));
+ return name_path.AsUTF8Unsafe();
+}
+
+// Returns true when there is no entry with the specified name under the parent
+// other than the specified entry.
+FileError EntryCanUseName(ResourceMetadataStorage* storage,
+ const std::string& parent_local_id,
+ const std::string& local_id,
+ const std::string& base_name,
+ bool* result) {
+ std::string existing_entry_id;
+ FileError error = storage->GetChild(parent_local_id, base_name,
+ &existing_entry_id);
+ if (error == FILE_ERROR_OK)
+ *result = existing_entry_id == local_id;
+ else if (error == FILE_ERROR_NOT_FOUND)
+ *result = true;
+ else
+ return error;
+ return FILE_ERROR_OK;
+}
+
+// Returns true when the ID is used by an immutable entry.
+bool IsImmutableEntry(const std::string& id) {
+ return id == util::kDriveGrandRootLocalId ||
+ id == util::kDriveOtherDirLocalId ||
+ id == util::kDriveTrashDirLocalId;
+}
+
+} // namespace
+
+ResourceMetadata::ResourceMetadata(
+ ResourceMetadataStorage* storage,
+ FileCache* cache,
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner)
+ : blocking_task_runner_(blocking_task_runner),
+ storage_(storage),
+ cache_(cache) {
+}
+
+FileError ResourceMetadata::Initialize() {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+ return SetUpDefaultEntries();
+}
+
+void ResourceMetadata::Destroy() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&ResourceMetadata::DestroyOnBlockingPool,
+ base::Unretained(this)));
+}
+
+FileError ResourceMetadata::Reset() {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+
+ if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path()))
+ return FILE_ERROR_NO_LOCAL_SPACE;
+
+ FileError error = storage_->SetLargestChangestamp(0);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ // Remove all root entries.
+ scoped_ptr<Iterator> it = GetIterator();
+ for (; !it->IsAtEnd(); it->Advance()) {
+ if (it->GetValue().parent_local_id().empty()) {
+ error = RemoveEntryRecursively(it->GetID());
+ if (error != FILE_ERROR_OK)
+ return error;
+ }
+ }
+ if (it->HasError())
+ return FILE_ERROR_FAILED;
+
+ return SetUpDefaultEntries();
+}
+
+ResourceMetadata::~ResourceMetadata() {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+}
+
+FileError ResourceMetadata::SetUpDefaultEntries() {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+
+ // Initialize "/drive".
+ ResourceEntry entry;
+ FileError error = storage_->GetEntry(util::kDriveGrandRootLocalId, &entry);
+ if (error == FILE_ERROR_NOT_FOUND) {
+ ResourceEntry root;
+ root.mutable_file_info()->set_is_directory(true);
+ root.set_local_id(util::kDriveGrandRootLocalId);
+ root.set_title(util::kDriveGrandRootDirName);
+ root.set_base_name(util::kDriveGrandRootDirName);
+ error = storage_->PutEntry(root);
+ if (error != FILE_ERROR_OK)
+ return error;
+ } else if (error == FILE_ERROR_OK) {
+ if (!entry.resource_id().empty()) {
+ // Old implementations used kDriveGrandRootLocalId as a resource ID.
+ entry.clear_resource_id();
+ error = storage_->PutEntry(entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+ }
+ } else {
+ return error;
+ }
+
+ // Initialize "/drive/other".
+ error = storage_->GetEntry(util::kDriveOtherDirLocalId, &entry);
+ if (error == FILE_ERROR_NOT_FOUND) {
+ ResourceEntry other_dir;
+ other_dir.mutable_file_info()->set_is_directory(true);
+ other_dir.set_local_id(util::kDriveOtherDirLocalId);
+ other_dir.set_parent_local_id(util::kDriveGrandRootLocalId);
+ other_dir.set_title(util::kDriveOtherDirName);
+ error = PutEntryUnderDirectory(other_dir);
+ if (error != FILE_ERROR_OK)
+ return error;
+ } else if (error == FILE_ERROR_OK) {
+ if (!entry.resource_id().empty()) {
+ // Old implementations used kDriveOtherDirLocalId as a resource ID.
+ entry.clear_resource_id();
+ error = storage_->PutEntry(entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+ }
+ } else {
+ return error;
+ }
+
+ // Initialize "drive/trash".
+ error = storage_->GetEntry(util::kDriveTrashDirLocalId, &entry);
+ if (error == FILE_ERROR_NOT_FOUND) {
+ ResourceEntry trash_dir;
+ trash_dir.mutable_file_info()->set_is_directory(true);
+ trash_dir.set_local_id(util::kDriveTrashDirLocalId);
+ trash_dir.set_parent_local_id(util::kDriveGrandRootLocalId);
+ trash_dir.set_title(util::kDriveTrashDirName);
+ error = PutEntryUnderDirectory(trash_dir);
+ if (error != FILE_ERROR_OK)
+ return error;
+ } else if (error != FILE_ERROR_OK) {
+ return error;
+ }
+
+ // Initialize "drive/root".
+ std::string child_id;
+ error = storage_->GetChild(
+ util::kDriveGrandRootLocalId, util::kDriveMyDriveRootDirName, &child_id);
+ if (error == FILE_ERROR_NOT_FOUND) {
+ ResourceEntry mydrive;
+ mydrive.mutable_file_info()->set_is_directory(true);
+ mydrive.set_parent_local_id(util::kDriveGrandRootLocalId);
+ mydrive.set_title(util::kDriveMyDriveRootDirName);
+
+ std::string local_id;
+ error = AddEntry(mydrive, &local_id);
+ if (error != FILE_ERROR_OK)
+ return error;
+ } else if (error != FILE_ERROR_OK) {
+ return error;
+ }
+ return FILE_ERROR_OK;
+}
+
+void ResourceMetadata::DestroyOnBlockingPool() {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+ delete this;
+}
+
+FileError ResourceMetadata::GetLargestChangestamp(int64* out_value) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+ return storage_->GetLargestChangestamp(out_value);
+}
+
+FileError ResourceMetadata::SetLargestChangestamp(int64 value) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+
+ if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path()))
+ return FILE_ERROR_NO_LOCAL_SPACE;
+
+ return storage_->SetLargestChangestamp(value);
+}
+
+FileError ResourceMetadata::AddEntry(const ResourceEntry& entry,
+ std::string* out_id) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+ DCHECK(entry.local_id().empty());
+
+ if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path()))
+ return FILE_ERROR_NO_LOCAL_SPACE;
+
+ ResourceEntry parent;
+ FileError error = storage_->GetEntry(entry.parent_local_id(), &parent);
+ if (error != FILE_ERROR_OK)
+ return error;
+ if (!parent.file_info().is_directory())
+ return FILE_ERROR_NOT_A_DIRECTORY;
+
+ // Multiple entries with the same resource ID should not be present.
+ std::string local_id;
+ ResourceEntry existing_entry;
+ if (!entry.resource_id().empty()) {
+ error = storage_->GetIdByResourceId(entry.resource_id(), &local_id);
+ if (error == FILE_ERROR_OK)
+ error = storage_->GetEntry(local_id, &existing_entry);
+
+ if (error == FILE_ERROR_OK)
+ return FILE_ERROR_EXISTS;
+ else if (error != FILE_ERROR_NOT_FOUND)
+ return error;
+ }
+
+ // Generate unique local ID when needed.
+ // We don't check for ID collisions as its probability is extremely low.
+ if (local_id.empty())
+ local_id = base::GenerateGUID();
+
+ ResourceEntry new_entry(entry);
+ new_entry.set_local_id(local_id);
+
+ error = PutEntryUnderDirectory(new_entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ *out_id = local_id;
+ return FILE_ERROR_OK;
+}
+
+FileError ResourceMetadata::RemoveEntry(const std::string& id) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+
+ if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path()))
+ return FILE_ERROR_NO_LOCAL_SPACE;
+
+ // Disallow deletion of default entries.
+ if (IsImmutableEntry(id))
+ return FILE_ERROR_ACCESS_DENIED;
+
+ ResourceEntry entry;
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ return RemoveEntryRecursively(id);
+}
+
+FileError ResourceMetadata::GetResourceEntryById(const std::string& id,
+ ResourceEntry* out_entry) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+ DCHECK(!id.empty());
+ DCHECK(out_entry);
+
+ return storage_->GetEntry(id, out_entry);
+}
+
+FileError ResourceMetadata::GetResourceEntryByPath(const base::FilePath& path,
+ ResourceEntry* out_entry) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+ DCHECK(out_entry);
+
+ std::string id;
+ FileError error = GetIdByPath(path, &id);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ return GetResourceEntryById(id, out_entry);
+}
+
+FileError ResourceMetadata::ReadDirectoryByPath(
+ const base::FilePath& path,
+ ResourceEntryVector* out_entries) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+ DCHECK(out_entries);
+
+ std::string id;
+ FileError error = GetIdByPath(path, &id);
+ if (error != FILE_ERROR_OK)
+ return error;
+ return ReadDirectoryById(id, out_entries);
+}
+
+FileError ResourceMetadata::ReadDirectoryById(
+ const std::string& id,
+ ResourceEntryVector* out_entries) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+ DCHECK(out_entries);
+
+ ResourceEntry entry;
+ FileError error = GetResourceEntryById(id, &entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ if (!entry.file_info().is_directory())
+ return FILE_ERROR_NOT_A_DIRECTORY;
+
+ std::vector<std::string> children;
+ error = storage_->GetChildren(id, &children);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ ResourceEntryVector entries(children.size());
+ for (size_t i = 0; i < children.size(); ++i) {
+ error = storage_->GetEntry(children[i], &entries[i]);
+ if (error != FILE_ERROR_OK)
+ return error;
+ }
+ out_entries->swap(entries);
+ return FILE_ERROR_OK;
+}
+
+FileError ResourceMetadata::RefreshEntry(const ResourceEntry& entry) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+
+ if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path()))
+ return FILE_ERROR_NO_LOCAL_SPACE;
+
+ ResourceEntry old_entry;
+ FileError error = storage_->GetEntry(entry.local_id(), &old_entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ if (IsImmutableEntry(entry.local_id()) ||
+ old_entry.file_info().is_directory() != // Reject incompatible input.
+ entry.file_info().is_directory())
+ return FILE_ERROR_INVALID_OPERATION;
+
+ if (!entry.resource_id().empty()) {
+ // Multiple entries cannot share the same resource ID.
+ std::string local_id;
+ FileError error = GetIdByResourceId(entry.resource_id(), &local_id);
+ switch (error) {
+ case FILE_ERROR_OK:
+ if (local_id != entry.local_id())
+ return FILE_ERROR_INVALID_OPERATION;
+ break;
+
+ case FILE_ERROR_NOT_FOUND:
+ break;
+
+ default:
+ return error;
+ }
+ }
+
+ // Make sure that the new parent exists and it is a directory.
+ ResourceEntry new_parent;
+ error = storage_->GetEntry(entry.parent_local_id(), &new_parent);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ if (!new_parent.file_info().is_directory())
+ return FILE_ERROR_NOT_A_DIRECTORY;
+
+ // Do not overwrite cache states.
+ // Cache state should be changed via FileCache.
+ ResourceEntry updated_entry(entry);
+ if (old_entry.file_specific_info().has_cache_state()) {
+ *updated_entry.mutable_file_specific_info()->mutable_cache_state() =
+ old_entry.file_specific_info().cache_state();
+ } else if (updated_entry.file_specific_info().has_cache_state()) {
+ updated_entry.mutable_file_specific_info()->clear_cache_state();
+ }
+ // Remove from the old parent and add it to the new parent with the new data.
+ return PutEntryUnderDirectory(updated_entry);
+}
+
+FileError ResourceMetadata::GetSubDirectoriesRecursively(
+ const std::string& id,
+ std::set<base::FilePath>* sub_directories) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+
+ std::vector<std::string> children;
+ FileError error = storage_->GetChildren(id, &children);
+ if (error != FILE_ERROR_OK)
+ return error;
+ for (size_t i = 0; i < children.size(); ++i) {
+ ResourceEntry entry;
+ error = storage_->GetEntry(children[i], &entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+ if (entry.file_info().is_directory()) {
+ base::FilePath path;
+ error = GetFilePath(children[i], &path);
+ if (error != FILE_ERROR_OK)
+ return error;
+ sub_directories->insert(path);
+ GetSubDirectoriesRecursively(children[i], sub_directories);
+ }
+ }
+ return FILE_ERROR_OK;
+}
+
+FileError ResourceMetadata::GetChildId(const std::string& parent_local_id,
+ const std::string& base_name,
+ std::string* out_child_id) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+ return storage_->GetChild(parent_local_id, base_name, out_child_id);
+}
+
+scoped_ptr<ResourceMetadata::Iterator> ResourceMetadata::GetIterator() {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+
+ return storage_->GetIterator();
+}
+
+FileError ResourceMetadata::GetFilePath(const std::string& id,
+ base::FilePath* out_file_path) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+
+ ResourceEntry entry;
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ base::FilePath path;
+ if (!entry.parent_local_id().empty()) {
+ error = GetFilePath(entry.parent_local_id(), &path);
+ if (error != FILE_ERROR_OK)
+ return error;
+ } else if (entry.local_id() != util::kDriveGrandRootLocalId) {
+ DVLOG(1) << "Entries not under the grand root don't have paths.";
+ return FILE_ERROR_NOT_FOUND;
+ }
+ path = path.Append(base::FilePath::FromUTF8Unsafe(entry.base_name()));
+ *out_file_path = path;
+ return FILE_ERROR_OK;
+}
+
+FileError ResourceMetadata::GetIdByPath(const base::FilePath& file_path,
+ std::string* out_id) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+
+ // Start from the root.
+ std::vector<base::FilePath::StringType> components;
+ file_path.GetComponents(&components);
+ if (components.empty() ||
+ components[0] != util::GetDriveGrandRootPath().value())
+ return FILE_ERROR_NOT_FOUND;
+
+ // Iterate over the remaining components.
+ std::string id = util::kDriveGrandRootLocalId;
+ for (size_t i = 1; i < components.size(); ++i) {
+ const std::string component = base::FilePath(components[i]).AsUTF8Unsafe();
+ std::string child_id;
+ FileError error = storage_->GetChild(id, component, &child_id);
+ if (error != FILE_ERROR_OK)
+ return error;
+ id = child_id;
+ }
+ *out_id = id;
+ return FILE_ERROR_OK;
+}
+
+FileError ResourceMetadata::GetIdByResourceId(const std::string& resource_id,
+ std::string* out_local_id) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+ return storage_->GetIdByResourceId(resource_id, out_local_id);
+}
+
+FileError ResourceMetadata::PutEntryUnderDirectory(const ResourceEntry& entry) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+ DCHECK(!entry.local_id().empty());
+ DCHECK(!entry.parent_local_id().empty());
+
+ std::string base_name;
+ FileError error = GetDeduplicatedBaseName(entry, &base_name);
+ if (error != FILE_ERROR_OK)
+ return error;
+ ResourceEntry updated_entry(entry);
+ updated_entry.set_base_name(base_name);
+ return storage_->PutEntry(updated_entry);
+}
+
+FileError ResourceMetadata::GetDeduplicatedBaseName(
+ const ResourceEntry& entry,
+ std::string* base_name) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+ DCHECK(!entry.parent_local_id().empty());
+ DCHECK(!entry.title().empty());
+
+ // The entry name may have been changed due to prior name de-duplication.
+ // We need to first restore the file name based on the title before going
+ // through name de-duplication again when it is added to another directory.
+ *base_name = entry.title();
+ if (entry.has_file_specific_info() &&
+ entry.file_specific_info().is_hosted_document()) {
+ *base_name += entry.file_specific_info().document_extension();
+ }
+ *base_name = util::NormalizeFileName(*base_name);
+
+ // If |base_name| is not used, just return it.
+ bool can_use_name = false;
+ FileError error = EntryCanUseName(storage_, entry.parent_local_id(),
+ entry.local_id(), *base_name,
+ &can_use_name);
+ if (error != FILE_ERROR_OK || can_use_name)
+ return error;
+
+ // Find an unused number with binary search.
+ int smallest_known_unused_modifier = 1;
+ while (true) {
+ error = EntryCanUseName(storage_, entry.parent_local_id(), entry.local_id(),
+ GetUniquifiedName(*base_name,
+ smallest_known_unused_modifier),
+ &can_use_name);
+ if (error != FILE_ERROR_OK)
+ return error;
+ if (can_use_name)
+ break;
+
+ const int delta = base::RandInt(1, smallest_known_unused_modifier);
+ if (smallest_known_unused_modifier <= INT_MAX - delta) {
+ smallest_known_unused_modifier += delta;
+ } else { // No luck finding an unused number. Try again.
+ smallest_known_unused_modifier = 1;
+ }
+ }
+
+ int largest_known_used_modifier = 1;
+ while (smallest_known_unused_modifier - largest_known_used_modifier > 1) {
+ const int modifier = largest_known_used_modifier +
+ (smallest_known_unused_modifier - largest_known_used_modifier) / 2;
+
+ error = EntryCanUseName(storage_, entry.parent_local_id(), entry.local_id(),
+ GetUniquifiedName(*base_name, modifier),
+ &can_use_name);
+ if (error != FILE_ERROR_OK)
+ return error;
+ if (can_use_name) {
+ smallest_known_unused_modifier = modifier;
+ } else {
+ largest_known_used_modifier = modifier;
+ }
+ }
+ *base_name = GetUniquifiedName(*base_name, smallest_known_unused_modifier);
+ return FILE_ERROR_OK;
+}
+
+FileError ResourceMetadata::RemoveEntryRecursively(const std::string& id) {
+ DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
+
+ ResourceEntry entry;
+ FileError error = storage_->GetEntry(id, &entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ if (entry.file_info().is_directory()) {
+ std::vector<std::string> children;
+ error = storage_->GetChildren(id, &children);
+ if (error != FILE_ERROR_OK)
+ return error;
+ for (size_t i = 0; i < children.size(); ++i) {
+ error = RemoveEntryRecursively(children[i]);
+ if (error != FILE_ERROR_OK)
+ return error;
+ }
+ }
+
+ error = cache_->Remove(id);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ return storage_->RemoveEntry(id);
+}
+
+} // namespace internal
+} // namespace drive
diff --git a/components/drive/resource_metadata.h b/components/drive/resource_metadata.h
new file mode 100644
index 0000000..3fe2751
--- /dev/null
+++ b/components/drive/resource_metadata.h
@@ -0,0 +1,146 @@
+// 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_RESOURCE_METADATA_H_
+#define COMPONENTS_DRIVE_RESOURCE_METADATA_H_
+
+#include <set>
+#include <string>
+#include <vector>
+
+#include "base/files/file_path.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/threading/thread_checker.h"
+#include "components/drive/file_errors.h"
+#include "components/drive/resource_metadata_storage.h"
+
+namespace base {
+class SequencedTaskRunner;
+}
+
+namespace drive {
+
+typedef std::vector<ResourceEntry> ResourceEntryVector;
+
+namespace internal {
+
+class FileCache;
+
+// Storage for Drive Metadata.
+// All methods except the constructor and Destroy() function must be run with
+// |blocking_task_runner| unless otherwise noted.
+class ResourceMetadata {
+ public:
+ typedef ResourceMetadataStorage::Iterator Iterator;
+
+ ResourceMetadata(
+ ResourceMetadataStorage* storage,
+ FileCache* cache,
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner);
+
+ // Initializes this object.
+ // This method should be called before any other methods.
+ FileError Initialize() WARN_UNUSED_RESULT;
+
+ // Destroys this object. This method posts a task to |blocking_task_runner_|
+ // to safely delete this object.
+ // Must be called on the UI thread.
+ void Destroy();
+
+ // Resets this object.
+ FileError Reset();
+
+ // Returns the largest changestamp.
+ FileError GetLargestChangestamp(int64* out_value);
+
+ // Sets the largest changestamp.
+ FileError SetLargestChangestamp(int64 value);
+
+ // Adds |entry| to the metadata tree based on its parent_local_id.
+ FileError AddEntry(const ResourceEntry& entry, std::string* out_id);
+
+ // Removes entry with |id| from its parent.
+ FileError RemoveEntry(const std::string& id);
+
+ // Finds an entry (a file or a directory) by |id|.
+ FileError GetResourceEntryById(const std::string& id,
+ ResourceEntry* out_entry);
+
+ // Synchronous version of GetResourceEntryByPathOnUIThread().
+ FileError GetResourceEntryByPath(const base::FilePath& file_path,
+ ResourceEntry* out_entry);
+
+ // Finds and reads a directory by |file_path|.
+ FileError ReadDirectoryByPath(const base::FilePath& file_path,
+ ResourceEntryVector* out_entries);
+
+ // Finds and reads a directory by |id|.
+ FileError ReadDirectoryById(const std::string& id,
+ ResourceEntryVector* out_entries);
+
+ // Replaces an existing entry with the same local ID as |entry|.
+ FileError RefreshEntry(const ResourceEntry& entry);
+
+ // Recursively gets directories under the entry pointed to by |id|.
+ FileError GetSubDirectoriesRecursively(
+ const std::string& id,
+ std::set<base::FilePath>* sub_directories);
+
+ // Returns the id of the resource named |base_name| directly under
+ // the directory with |parent_local_id|.
+ // If not found, empty string will be returned.
+ FileError GetChildId(const std::string& parent_local_id,
+ const std::string& base_name,
+ std::string* out_child_id);
+
+ // Returns an object to iterate over entries.
+ scoped_ptr<Iterator> GetIterator();
+
+ // Returns virtual file path of the entry.
+ FileError GetFilePath(const std::string& id, base::FilePath* out_file_path);
+
+ // Returns ID of the entry at the given path.
+ FileError GetIdByPath(const base::FilePath& file_path, std::string* out_id);
+
+ // Returns the local ID associated with the given resource ID.
+ FileError GetIdByResourceId(const std::string& resource_id,
+ std::string* out_local_id);
+
+ private:
+ // Note: Use Destroy() to delete this object.
+ ~ResourceMetadata();
+
+ // Sets up entries which should be present by default.
+ FileError SetUpDefaultEntries();
+
+ // Used to implement Destroy().
+ void DestroyOnBlockingPool();
+
+ // Puts an entry under its parent directory. Removes the child from the old
+ // parent if there is. This method will also do name de-duplication to ensure
+ // that the exposed presentation path does not have naming conflicts. Two
+ // files with the same name "Foo" will be renamed to "Foo (1)" and "Foo (2)".
+ FileError PutEntryUnderDirectory(const ResourceEntry& entry);
+
+ // Returns an unused base name for |entry|.
+ FileError GetDeduplicatedBaseName(const ResourceEntry& entry,
+ std::string* base_name);
+
+ // Removes the entry and its descendants.
+ FileError RemoveEntryRecursively(const std::string& id);
+
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_;
+
+ ResourceMetadataStorage* storage_;
+ FileCache* cache_;
+
+ base::ThreadChecker thread_checker_;
+
+ DISALLOW_COPY_AND_ASSIGN(ResourceMetadata);
+};
+
+} // namespace internal
+} // namespace drive
+
+#endif // COMPONENTS_DRIVE_RESOURCE_METADATA_H_
diff --git a/components/drive/resource_metadata_storage.cc b/components/drive/resource_metadata_storage.cc
new file mode 100644
index 0000000..fbba7a9
--- /dev/null
+++ b/components/drive/resource_metadata_storage.cc
@@ -0,0 +1,1064 @@
+// 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/resource_metadata_storage.h"
+
+#include <map>
+#include <set>
+
+#include "base/bind.h"
+#include "base/containers/hash_tables.h"
+#include "base/files/file_util.h"
+#include "base/location.h"
+#include "base/logging.h"
+#include "base/metrics/histogram.h"
+#include "base/metrics/sparse_histogram.h"
+#include "base/sequenced_task_runner.h"
+#include "base/threading/thread_restrictions.h"
+#include "components/drive/drive.pb.h"
+#include "components/drive/drive_api_util.h"
+#include "third_party/leveldatabase/env_chromium.h"
+#include "third_party/leveldatabase/src/include/leveldb/db.h"
+#include "third_party/leveldatabase/src/include/leveldb/write_batch.h"
+
+namespace drive {
+namespace internal {
+
+namespace {
+
+// Enum to describe DB initialization status.
+enum DBInitStatus {
+ DB_INIT_SUCCESS,
+ DB_INIT_NOT_FOUND,
+ DB_INIT_CORRUPTION,
+ DB_INIT_IO_ERROR,
+ DB_INIT_FAILED,
+ DB_INIT_INCOMPATIBLE,
+ DB_INIT_BROKEN,
+ DB_INIT_OPENED_EXISTING_DB,
+ DB_INIT_CREATED_NEW_DB,
+ DB_INIT_REPLACED_EXISTING_DB_WITH_NEW_DB,
+ DB_INIT_MAX_VALUE,
+};
+
+// Enum to describe DB validity check failure reason.
+enum CheckValidityFailureReason {
+ CHECK_VALIDITY_FAILURE_INVALID_HEADER,
+ CHECK_VALIDITY_FAILURE_BROKEN_ID_ENTRY,
+ CHECK_VALIDITY_FAILURE_BROKEN_ENTRY,
+ CHECK_VALIDITY_FAILURE_INVALID_LOCAL_ID,
+ CHECK_VALIDITY_FAILURE_INVALID_PARENT_ID,
+ CHECK_VALIDITY_FAILURE_BROKEN_CHILD_MAP,
+ CHECK_VALIDITY_FAILURE_CHILD_ENTRY_COUNT_MISMATCH,
+ CHECK_VALIDITY_FAILURE_ITERATOR_ERROR,
+ CHECK_VALIDITY_FAILURE_MAX_VALUE,
+};
+
+// The name of the DB which stores the metadata.
+const base::FilePath::CharType kResourceMapDBName[] =
+ FILE_PATH_LITERAL("resource_metadata_resource_map.db");
+
+// The name of the DB which couldn't be opened, but is preserved just in case.
+const base::FilePath::CharType kPreservedResourceMapDBName[] =
+ FILE_PATH_LITERAL("resource_metadata_preserved_resource_map.db");
+
+// The name of the DB which couldn't be opened, and was replaced with a new one.
+const base::FilePath::CharType kTrashedResourceMapDBName[] =
+ FILE_PATH_LITERAL("resource_metadata_trashed_resource_map.db");
+
+// Meant to be a character which never happen to be in real IDs.
+const char kDBKeyDelimeter = '\0';
+
+// String used as a suffix of a key for a cache entry.
+const char kCacheEntryKeySuffix[] = "CACHE";
+
+// String used as a prefix of a key for a resource-ID-to-local-ID entry.
+const char kIdEntryKeyPrefix[] = "ID";
+
+// Returns a string to be used as the key for the header.
+std::string GetHeaderDBKey() {
+ std::string key;
+ key.push_back(kDBKeyDelimeter);
+ key.append("HEADER");
+ return key;
+}
+
+// Returns true if |key| is a key for a child entry.
+bool IsChildEntryKey(const leveldb::Slice& key) {
+ return !key.empty() && key[key.size() - 1] == kDBKeyDelimeter;
+}
+
+// Returns true if |key| is a key for a cache entry.
+bool IsCacheEntryKey(const leveldb::Slice& key) {
+ // A cache entry key should end with |kDBKeyDelimeter + kCacheEntryKeySuffix|.
+ const leveldb::Slice expected_suffix(kCacheEntryKeySuffix,
+ arraysize(kCacheEntryKeySuffix) - 1);
+ if (key.size() < 1 + expected_suffix.size() ||
+ key[key.size() - expected_suffix.size() - 1] != kDBKeyDelimeter)
+ return false;
+
+ const leveldb::Slice key_substring(
+ key.data() + key.size() - expected_suffix.size(), expected_suffix.size());
+ return key_substring.compare(expected_suffix) == 0;
+}
+
+// Returns ID extracted from a cache entry key.
+std::string GetIdFromCacheEntryKey(const leveldb::Slice& key) {
+ DCHECK(IsCacheEntryKey(key));
+ // Drop the suffix |kDBKeyDelimeter + kCacheEntryKeySuffix| from the key.
+ const size_t kSuffixLength = arraysize(kCacheEntryKeySuffix) - 1;
+ const int id_length = key.size() - 1 - kSuffixLength;
+ return std::string(key.data(), id_length);
+}
+
+// Returns a string to be used as a key for a resource-ID-to-local-ID entry.
+std::string GetIdEntryKey(const std::string& resource_id) {
+ std::string key;
+ key.push_back(kDBKeyDelimeter);
+ key.append(kIdEntryKeyPrefix);
+ key.push_back(kDBKeyDelimeter);
+ key.append(resource_id);
+ return key;
+}
+
+// Returns true if |key| is a key for a resource-ID-to-local-ID entry.
+bool IsIdEntryKey(const leveldb::Slice& key) {
+ // A resource-ID-to-local-ID entry key should start with
+ // |kDBKeyDelimeter + kIdEntryKeyPrefix + kDBKeyDelimeter|.
+ const leveldb::Slice expected_prefix(kIdEntryKeyPrefix,
+ arraysize(kIdEntryKeyPrefix) - 1);
+ if (key.size() < 2 + expected_prefix.size())
+ return false;
+ const leveldb::Slice key_substring(key.data() + 1, expected_prefix.size());
+ return key[0] == kDBKeyDelimeter &&
+ key_substring.compare(expected_prefix) == 0 &&
+ key[expected_prefix.size() + 1] == kDBKeyDelimeter;
+}
+
+// Returns the resource ID extracted from a resource-ID-to-local-ID entry key.
+std::string GetResourceIdFromIdEntryKey(const leveldb::Slice& key) {
+ DCHECK(IsIdEntryKey(key));
+ // Drop the prefix |kDBKeyDelimeter + kIdEntryKeyPrefix + kDBKeyDelimeter|
+ // from the key.
+ const size_t kPrefixLength = arraysize(kIdEntryKeyPrefix) - 1;
+ const int offset = kPrefixLength + 2;
+ return std::string(key.data() + offset, key.size() - offset);
+}
+
+// Converts leveldb::Status to DBInitStatus.
+DBInitStatus LevelDBStatusToDBInitStatus(const leveldb::Status& status) {
+ if (status.ok())
+ return DB_INIT_SUCCESS;
+ if (status.IsNotFound())
+ return DB_INIT_NOT_FOUND;
+ if (status.IsCorruption())
+ return DB_INIT_CORRUPTION;
+ if (status.IsIOError())
+ return DB_INIT_IO_ERROR;
+ return DB_INIT_FAILED;
+}
+
+// Converts leveldb::Status to FileError.
+FileError LevelDBStatusToFileError(const leveldb::Status& status) {
+ if (status.ok())
+ return FILE_ERROR_OK;
+ if (status.IsNotFound())
+ return FILE_ERROR_NOT_FOUND;
+ if (leveldb_env::IndicatesDiskFull(status))
+ return FILE_ERROR_NO_LOCAL_SPACE;
+ return FILE_ERROR_FAILED;
+}
+
+ResourceMetadataHeader GetDefaultHeaderEntry() {
+ ResourceMetadataHeader header;
+ header.set_version(ResourceMetadataStorage::kDBVersion);
+ return header;
+}
+
+bool MoveIfPossible(const base::FilePath& from, const base::FilePath& to) {
+ return !base::PathExists(from) || base::Move(from, to);
+}
+
+void RecordCheckValidityFailure(CheckValidityFailureReason reason) {
+ UMA_HISTOGRAM_ENUMERATION("Drive.MetadataDBValidityCheckFailureReason",
+ reason,
+ CHECK_VALIDITY_FAILURE_MAX_VALUE);
+}
+
+} // namespace
+
+ResourceMetadataStorage::Iterator::Iterator(scoped_ptr<leveldb::Iterator> it)
+ : it_(it.Pass()) {
+ base::ThreadRestrictions::AssertIOAllowed();
+ DCHECK(it_);
+
+ // Skip the header entry.
+ // Note: The header entry comes before all other entries because its key
+ // starts with kDBKeyDelimeter. (i.e. '\0')
+ it_->Seek(leveldb::Slice(GetHeaderDBKey()));
+
+ Advance();
+}
+
+ResourceMetadataStorage::Iterator::~Iterator() {
+ base::ThreadRestrictions::AssertIOAllowed();
+}
+
+bool ResourceMetadataStorage::Iterator::IsAtEnd() const {
+ base::ThreadRestrictions::AssertIOAllowed();
+ return !it_->Valid();
+}
+
+std::string ResourceMetadataStorage::Iterator::GetID() const {
+ return it_->key().ToString();
+}
+
+const ResourceEntry& ResourceMetadataStorage::Iterator::GetValue() const {
+ base::ThreadRestrictions::AssertIOAllowed();
+ DCHECK(!IsAtEnd());
+ return entry_;
+}
+
+void ResourceMetadataStorage::Iterator::Advance() {
+ base::ThreadRestrictions::AssertIOAllowed();
+ DCHECK(!IsAtEnd());
+
+ for (it_->Next() ; it_->Valid(); it_->Next()) {
+ if (!IsChildEntryKey(it_->key()) &&
+ !IsIdEntryKey(it_->key()) &&
+ entry_.ParseFromArray(it_->value().data(), it_->value().size())) {
+ break;
+ }
+ }
+}
+
+bool ResourceMetadataStorage::Iterator::HasError() const {
+ base::ThreadRestrictions::AssertIOAllowed();
+ return !it_->status().ok();
+}
+
+// static
+bool ResourceMetadataStorage::UpgradeOldDB(
+ const base::FilePath& directory_path) {
+ base::ThreadRestrictions::AssertIOAllowed();
+ static_assert(
+ kDBVersion == 13,
+ "database version and this function must be updated at the same time");
+
+ const base::FilePath resource_map_path =
+ directory_path.Append(kResourceMapDBName);
+ const base::FilePath preserved_resource_map_path =
+ directory_path.Append(kPreservedResourceMapDBName);
+
+ if (base::PathExists(preserved_resource_map_path)) {
+ // Preserved DB is found. The previous attempt to create a new DB should not
+ // be successful. Discard the imperfect new DB and restore the old DB.
+ if (!base::DeleteFile(resource_map_path, false /* recursive */) ||
+ !base::Move(preserved_resource_map_path, resource_map_path))
+ return false;
+ }
+
+ if (!base::PathExists(resource_map_path))
+ return false;
+
+ // Open DB.
+ leveldb::DB* db = NULL;
+ leveldb::Options options;
+ options.max_open_files = 0; // Use minimum.
+ options.create_if_missing = false;
+ options.reuse_logs = leveldb_env::kDefaultLogReuseOptionValue;
+ if (!leveldb::DB::Open(options, resource_map_path.AsUTF8Unsafe(), &db).ok())
+ return false;
+ scoped_ptr<leveldb::DB> resource_map(db);
+
+ // Check DB version.
+ std::string serialized_header;
+ ResourceMetadataHeader header;
+ if (!resource_map->Get(leveldb::ReadOptions(),
+ leveldb::Slice(GetHeaderDBKey()),
+ &serialized_header).ok() ||
+ !header.ParseFromString(serialized_header))
+ return false;
+ UMA_HISTOGRAM_SPARSE_SLOWLY("Drive.MetadataDBVersionBeforeUpgradeCheck",
+ header.version());
+
+ if (header.version() == kDBVersion) {
+ // Before r272134, UpgradeOldDB() was not deleting unused ID entries.
+ // Delete unused ID entries to fix crbug.com/374648.
+ std::set<std::string> used_ids;
+
+ scoped_ptr<leveldb::Iterator> it(
+ resource_map->NewIterator(leveldb::ReadOptions()));
+ it->Seek(leveldb::Slice(GetHeaderDBKey()));
+ it->Next();
+ for (; it->Valid(); it->Next()) {
+ if (IsCacheEntryKey(it->key())) {
+ used_ids.insert(GetIdFromCacheEntryKey(it->key()));
+ } else if (!IsChildEntryKey(it->key()) && !IsIdEntryKey(it->key())) {
+ used_ids.insert(it->key().ToString());
+ }
+ }
+ if (!it->status().ok())
+ return false;
+
+ leveldb::WriteBatch batch;
+ for (it->SeekToFirst(); it->Valid(); it->Next()) {
+ if (IsIdEntryKey(it->key()) && !used_ids.count(it->value().ToString()))
+ batch.Delete(it->key());
+ }
+ if (!it->status().ok())
+ return false;
+
+ return resource_map->Write(leveldb::WriteOptions(), &batch).ok();
+ } else if (header.version() < 6) { // Too old, nothing can be done.
+ return false;
+ } else if (header.version() < 11) { // Cache entries can be reused.
+ leveldb::ReadOptions options;
+ options.verify_checksums = true;
+ scoped_ptr<leveldb::Iterator> it(resource_map->NewIterator(options));
+
+ leveldb::WriteBatch batch;
+ // First, remove all entries.
+ for (it->SeekToFirst(); it->Valid(); it->Next())
+ batch.Delete(it->key());
+
+ // Put ID entries and cache entries.
+ for (it->SeekToFirst(); it->Valid(); it->Next()) {
+ if (IsCacheEntryKey(it->key())) {
+ FileCacheEntry cache_entry;
+ if (!cache_entry.ParseFromArray(it->value().data(), it->value().size()))
+ return false;
+
+ // The resource ID might be in old WAPI format. We need to canonicalize
+ // to the format of API service currently in use.
+ const std::string& id = GetIdFromCacheEntryKey(it->key());
+ const std::string& id_new = util::CanonicalizeResourceId(id);
+
+ // Before v11, resource ID was directly used as local ID. Such entries
+ // can be migrated by adding an identity ID mapping.
+ batch.Put(GetIdEntryKey(id_new), id_new);
+
+ // Put cache state into a ResourceEntry.
+ ResourceEntry entry;
+ entry.set_local_id(id_new);
+ entry.set_resource_id(id_new);
+ *entry.mutable_file_specific_info()->mutable_cache_state() =
+ cache_entry;
+
+ std::string serialized_entry;
+ if (!entry.SerializeToString(&serialized_entry)) {
+ DLOG(ERROR) << "Failed to serialize the entry: " << id;
+ return false;
+ }
+ batch.Put(id_new, serialized_entry);
+ }
+ }
+ if (!it->status().ok())
+ return false;
+
+ // Put header with the latest version number.
+ std::string serialized_header;
+ if (!GetDefaultHeaderEntry().SerializeToString(&serialized_header))
+ return false;
+ batch.Put(GetHeaderDBKey(), serialized_header);
+
+ return resource_map->Write(leveldb::WriteOptions(), &batch).ok();
+ } else if (header.version() < 12) { // Cache and ID map entries are reusable.
+ leveldb::ReadOptions options;
+ options.verify_checksums = true;
+ scoped_ptr<leveldb::Iterator> it(resource_map->NewIterator(options));
+
+ // First, get the set of local IDs associated with cache entries.
+ std::set<std::string> cached_entry_ids;
+ for (it->SeekToFirst(); it->Valid(); it->Next()) {
+ if (IsCacheEntryKey(it->key()))
+ cached_entry_ids.insert(GetIdFromCacheEntryKey(it->key()));
+ }
+ if (!it->status().ok())
+ return false;
+
+ // Remove all entries except used ID entries.
+ leveldb::WriteBatch batch;
+ std::map<std::string, std::string> local_id_to_resource_id;
+ for (it->SeekToFirst(); it->Valid(); it->Next()) {
+ const bool is_used_id = IsIdEntryKey(it->key()) &&
+ cached_entry_ids.count(it->value().ToString());
+ if (is_used_id) {
+ local_id_to_resource_id[it->value().ToString()] =
+ GetResourceIdFromIdEntryKey(it->key());
+ } else {
+ batch.Delete(it->key());
+ }
+ }
+ if (!it->status().ok())
+ return false;
+
+ // Put cache entries.
+ for (it->SeekToFirst(); it->Valid(); it->Next()) {
+ if (IsCacheEntryKey(it->key())) {
+ const std::string& id = GetIdFromCacheEntryKey(it->key());
+
+ std::map<std::string, std::string>::const_iterator iter_resource_id =
+ local_id_to_resource_id.find(id);
+ if (iter_resource_id == local_id_to_resource_id.end())
+ continue;
+
+ FileCacheEntry cache_entry;
+ if (!cache_entry.ParseFromArray(it->value().data(), it->value().size()))
+ return false;
+
+ // Put cache state into a ResourceEntry.
+ ResourceEntry entry;
+ entry.set_local_id(id);
+ entry.set_resource_id(iter_resource_id->second);
+ *entry.mutable_file_specific_info()->mutable_cache_state() =
+ cache_entry;
+
+ std::string serialized_entry;
+ if (!entry.SerializeToString(&serialized_entry)) {
+ DLOG(ERROR) << "Failed to serialize the entry: " << id;
+ return false;
+ }
+ batch.Put(id, serialized_entry);
+ }
+ }
+ if (!it->status().ok())
+ return false;
+
+ // Put header with the latest version number.
+ std::string serialized_header;
+ if (!GetDefaultHeaderEntry().SerializeToString(&serialized_header))
+ return false;
+ batch.Put(GetHeaderDBKey(), serialized_header);
+
+ return resource_map->Write(leveldb::WriteOptions(), &batch).ok();
+ } else if (header.version() < 13) { // Reuse all entries.
+ leveldb::ReadOptions options;
+ options.verify_checksums = true;
+ scoped_ptr<leveldb::Iterator> it(resource_map->NewIterator(options));
+
+ // First, get local ID to resource ID map.
+ std::map<std::string, std::string> local_id_to_resource_id;
+ for (it->SeekToFirst(); it->Valid(); it->Next()) {
+ if (IsIdEntryKey(it->key())) {
+ local_id_to_resource_id[it->value().ToString()] =
+ GetResourceIdFromIdEntryKey(it->key());
+ }
+ }
+ if (!it->status().ok())
+ return false;
+
+ leveldb::WriteBatch batch;
+ // Merge cache entries to ResourceEntry.
+ for (it->SeekToFirst(); it->Valid(); it->Next()) {
+ if (IsCacheEntryKey(it->key())) {
+ const std::string& id = GetIdFromCacheEntryKey(it->key());
+
+ FileCacheEntry cache_entry;
+ if (!cache_entry.ParseFromArray(it->value().data(), it->value().size()))
+ return false;
+
+ std::string serialized_entry;
+ leveldb::Status status = resource_map->Get(options,
+ leveldb::Slice(id),
+ &serialized_entry);
+
+ std::map<std::string, std::string>::const_iterator iter_resource_id =
+ local_id_to_resource_id.find(id);
+
+ // No need to keep cache-only entries without resource ID.
+ if (status.IsNotFound() &&
+ iter_resource_id == local_id_to_resource_id.end())
+ continue;
+
+ ResourceEntry entry;
+ if (status.ok()) {
+ if (!entry.ParseFromString(serialized_entry))
+ return false;
+ } else if (status.IsNotFound()) {
+ entry.set_local_id(id);
+ entry.set_resource_id(iter_resource_id->second);
+ } else {
+ DLOG(ERROR) << "Failed to get the entry: " << id;
+ return false;
+ }
+ *entry.mutable_file_specific_info()->mutable_cache_state() =
+ cache_entry;
+
+ if (!entry.SerializeToString(&serialized_entry)) {
+ DLOG(ERROR) << "Failed to serialize the entry: " << id;
+ return false;
+ }
+ batch.Delete(it->key());
+ batch.Put(id, serialized_entry);
+ }
+ }
+ if (!it->status().ok())
+ return false;
+
+ // Put header with the latest version number.
+ header.set_version(ResourceMetadataStorage::kDBVersion);
+ std::string serialized_header;
+ if (!header.SerializeToString(&serialized_header))
+ return false;
+ batch.Put(GetHeaderDBKey(), serialized_header);
+
+ return resource_map->Write(leveldb::WriteOptions(), &batch).ok();
+ }
+
+ LOG(WARNING) << "Unexpected DB version: " << header.version();
+ return false;
+}
+
+ResourceMetadataStorage::ResourceMetadataStorage(
+ const base::FilePath& directory_path,
+ base::SequencedTaskRunner* blocking_task_runner)
+ : directory_path_(directory_path),
+ cache_file_scan_is_needed_(true),
+ blocking_task_runner_(blocking_task_runner) {
+}
+
+void ResourceMetadataStorage::Destroy() {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&ResourceMetadataStorage::DestroyOnBlockingPool,
+ base::Unretained(this)));
+}
+
+bool ResourceMetadataStorage::Initialize() {
+ base::ThreadRestrictions::AssertIOAllowed();
+
+ resource_map_.reset();
+
+ const base::FilePath resource_map_path =
+ directory_path_.Append(kResourceMapDBName);
+ const base::FilePath preserved_resource_map_path =
+ directory_path_.Append(kPreservedResourceMapDBName);
+ const base::FilePath trashed_resource_map_path =
+ directory_path_.Append(kTrashedResourceMapDBName);
+
+ // Discard unneeded DBs.
+ if (!base::DeleteFile(preserved_resource_map_path, true /* recursive */) ||
+ !base::DeleteFile(trashed_resource_map_path, true /* recursive */)) {
+ LOG(ERROR) << "Failed to remove unneeded DBs.";
+ return false;
+ }
+
+ // Try to open the existing DB.
+ leveldb::DB* db = NULL;
+ leveldb::Options options;
+ options.max_open_files = 0; // Use minimum.
+ options.create_if_missing = false;
+ options.reuse_logs = leveldb_env::kDefaultLogReuseOptionValue;
+
+ DBInitStatus open_existing_result = DB_INIT_NOT_FOUND;
+ leveldb::Status status;
+ if (base::PathExists(resource_map_path)) {
+ status = leveldb::DB::Open(options, resource_map_path.AsUTF8Unsafe(), &db);
+ open_existing_result = LevelDBStatusToDBInitStatus(status);
+ }
+
+ if (open_existing_result == DB_INIT_SUCCESS) {
+ resource_map_.reset(db);
+
+ // Check the validity of existing DB.
+ int db_version = -1;
+ ResourceMetadataHeader header;
+ if (GetHeader(&header) == FILE_ERROR_OK)
+ db_version = header.version();
+
+ bool should_discard_db = true;
+ if (db_version != kDBVersion) {
+ open_existing_result = DB_INIT_INCOMPATIBLE;
+ DVLOG(1) << "Reject incompatible DB.";
+ } else if (!CheckValidity()) {
+ open_existing_result = DB_INIT_BROKEN;
+ LOG(ERROR) << "Reject invalid DB.";
+ } else {
+ should_discard_db = false;
+ }
+
+ if (should_discard_db)
+ resource_map_.reset();
+ else
+ cache_file_scan_is_needed_ = false;
+ }
+
+ UMA_HISTOGRAM_ENUMERATION("Drive.MetadataDBOpenExistingResult",
+ open_existing_result,
+ DB_INIT_MAX_VALUE);
+
+ DBInitStatus init_result = DB_INIT_OPENED_EXISTING_DB;
+
+ // Failed to open the existing DB, create new DB.
+ if (!resource_map_) {
+ // Move the existing DB to the preservation path. The moved old DB is
+ // deleted once the new DB creation succeeds, or is restored later in
+ // UpgradeOldDB() when the creation fails.
+ MoveIfPossible(resource_map_path, preserved_resource_map_path);
+
+ // Create DB.
+ options.max_open_files = 0; // Use minimum.
+ options.create_if_missing = true;
+ options.error_if_exists = true;
+ options.reuse_logs = leveldb_env::kDefaultLogReuseOptionValue;
+
+ status = leveldb::DB::Open(options, resource_map_path.AsUTF8Unsafe(), &db);
+ if (status.ok()) {
+ resource_map_.reset(db);
+
+ // Set up header and trash the old DB.
+ if (PutHeader(GetDefaultHeaderEntry()) == FILE_ERROR_OK &&
+ MoveIfPossible(preserved_resource_map_path,
+ trashed_resource_map_path)) {
+ init_result = open_existing_result == DB_INIT_NOT_FOUND ?
+ DB_INIT_CREATED_NEW_DB : DB_INIT_REPLACED_EXISTING_DB_WITH_NEW_DB;
+ } else {
+ init_result = DB_INIT_FAILED;
+ resource_map_.reset();
+ }
+ } else {
+ LOG(ERROR) << "Failed to create resource map DB: " << status.ToString();
+ init_result = LevelDBStatusToDBInitStatus(status);
+ }
+ }
+
+ UMA_HISTOGRAM_ENUMERATION("Drive.MetadataDBInitResult",
+ init_result,
+ DB_INIT_MAX_VALUE);
+ return resource_map_;
+}
+
+void ResourceMetadataStorage::RecoverCacheInfoFromTrashedResourceMap(
+ RecoveredCacheInfoMap* out_info) {
+ const base::FilePath trashed_resource_map_path =
+ directory_path_.Append(kTrashedResourceMapDBName);
+
+ if (!base::PathExists(trashed_resource_map_path))
+ return;
+
+ leveldb::Options options;
+ options.max_open_files = 0; // Use minimum.
+ options.create_if_missing = false;
+ options.reuse_logs = leveldb_env::kDefaultLogReuseOptionValue;
+
+ // Trashed DB may be broken, repair it first.
+ leveldb::Status status;
+ status = leveldb::RepairDB(trashed_resource_map_path.AsUTF8Unsafe(), options);
+ if (!status.ok()) {
+ LOG(ERROR) << "Failed to repair trashed DB: " << status.ToString();
+ return;
+ }
+
+ // Open it.
+ leveldb::DB* db = NULL;
+ status = leveldb::DB::Open(options, trashed_resource_map_path.AsUTF8Unsafe(),
+ &db);
+ if (!status.ok()) {
+ LOG(ERROR) << "Failed to open trashed DB: " << status.ToString();
+ return;
+ }
+ scoped_ptr<leveldb::DB> resource_map(db);
+
+ // Check DB version.
+ std::string serialized_header;
+ ResourceMetadataHeader header;
+ if (!resource_map->Get(leveldb::ReadOptions(),
+ leveldb::Slice(GetHeaderDBKey()),
+ &serialized_header).ok() ||
+ !header.ParseFromString(serialized_header) ||
+ header.version() != kDBVersion) {
+ LOG(ERROR) << "Incompatible DB version: " << header.version();
+ return;
+ }
+
+ // Collect cache entries.
+ scoped_ptr<leveldb::Iterator> it(
+ resource_map->NewIterator(leveldb::ReadOptions()));
+ for (it->SeekToFirst(); it->Valid(); it->Next()) {
+ if (!IsChildEntryKey(it->key()) &&
+ !IsIdEntryKey(it->key())) {
+ const std::string id = it->key().ToString();
+ ResourceEntry entry;
+ if (entry.ParseFromArray(it->value().data(), it->value().size()) &&
+ entry.file_specific_info().has_cache_state()) {
+ RecoveredCacheInfo* info = &(*out_info)[id];
+ info->is_dirty = entry.file_specific_info().cache_state().is_dirty();
+ info->md5 = entry.file_specific_info().cache_state().md5();
+ info->title = entry.title();
+ }
+ }
+ }
+}
+
+FileError ResourceMetadataStorage::SetLargestChangestamp(
+ int64 largest_changestamp) {
+ base::ThreadRestrictions::AssertIOAllowed();
+
+ ResourceMetadataHeader header;
+ FileError error = GetHeader(&header);
+ if (error != FILE_ERROR_OK) {
+ DLOG(ERROR) << "Failed to get the header.";
+ return error;
+ }
+ header.set_largest_changestamp(largest_changestamp);
+ return PutHeader(header);
+}
+
+FileError ResourceMetadataStorage::GetLargestChangestamp(
+ int64* largest_changestamp) {
+ base::ThreadRestrictions::AssertIOAllowed();
+ ResourceMetadataHeader header;
+ FileError error = GetHeader(&header);
+ if (error != FILE_ERROR_OK) {
+ DLOG(ERROR) << "Failed to get the header.";
+ return error;
+ }
+ *largest_changestamp = header.largest_changestamp();
+ return FILE_ERROR_OK;
+}
+
+FileError ResourceMetadataStorage::PutEntry(const ResourceEntry& entry) {
+ base::ThreadRestrictions::AssertIOAllowed();
+
+ const std::string& id = entry.local_id();
+ DCHECK(!id.empty());
+
+ // Try to get existing entry.
+ std::string serialized_entry;
+ leveldb::Status status = resource_map_->Get(leveldb::ReadOptions(),
+ leveldb::Slice(id),
+ &serialized_entry);
+ if (!status.ok() && !status.IsNotFound()) // Unexpected errors.
+ return LevelDBStatusToFileError(status);
+
+ ResourceEntry old_entry;
+ if (status.ok() && !old_entry.ParseFromString(serialized_entry))
+ return FILE_ERROR_FAILED;
+
+ // Construct write batch.
+ leveldb::WriteBatch batch;
+
+ // Remove from the old parent.
+ if (!old_entry.parent_local_id().empty()) {
+ batch.Delete(GetChildEntryKey(old_entry.parent_local_id(),
+ old_entry.base_name()));
+ }
+ // Add to the new parent.
+ if (!entry.parent_local_id().empty())
+ batch.Put(GetChildEntryKey(entry.parent_local_id(), entry.base_name()), id);
+
+ // Refresh resource-ID-to-local-ID mapping entry.
+ if (old_entry.resource_id() != entry.resource_id()) {
+ // Resource ID should not change.
+ DCHECK(old_entry.resource_id().empty() || entry.resource_id().empty());
+
+ if (!old_entry.resource_id().empty())
+ batch.Delete(GetIdEntryKey(old_entry.resource_id()));
+ if (!entry.resource_id().empty())
+ batch.Put(GetIdEntryKey(entry.resource_id()), id);
+ }
+
+ // Put the entry itself.
+ if (!entry.SerializeToString(&serialized_entry)) {
+ DLOG(ERROR) << "Failed to serialize the entry: " << id;
+ return FILE_ERROR_FAILED;
+ }
+ batch.Put(id, serialized_entry);
+
+ status = resource_map_->Write(leveldb::WriteOptions(), &batch);
+ return LevelDBStatusToFileError(status);
+}
+
+FileError ResourceMetadataStorage::GetEntry(const std::string& id,
+ ResourceEntry* out_entry) {
+ base::ThreadRestrictions::AssertIOAllowed();
+ DCHECK(!id.empty());
+
+ std::string serialized_entry;
+ const leveldb::Status status = resource_map_->Get(leveldb::ReadOptions(),
+ leveldb::Slice(id),
+ &serialized_entry);
+ if (!status.ok())
+ return LevelDBStatusToFileError(status);
+ if (!out_entry->ParseFromString(serialized_entry))
+ return FILE_ERROR_FAILED;
+ return FILE_ERROR_OK;
+}
+
+FileError ResourceMetadataStorage::RemoveEntry(const std::string& id) {
+ base::ThreadRestrictions::AssertIOAllowed();
+ DCHECK(!id.empty());
+
+ ResourceEntry entry;
+ FileError error = GetEntry(id, &entry);
+ if (error != FILE_ERROR_OK)
+ return error;
+
+ leveldb::WriteBatch batch;
+
+ // Remove from the parent.
+ if (!entry.parent_local_id().empty())
+ batch.Delete(GetChildEntryKey(entry.parent_local_id(), entry.base_name()));
+
+ // Remove resource ID-local ID mapping entry.
+ if (!entry.resource_id().empty())
+ batch.Delete(GetIdEntryKey(entry.resource_id()));
+
+ // Remove the entry itself.
+ batch.Delete(id);
+
+ const leveldb::Status status = resource_map_->Write(leveldb::WriteOptions(),
+ &batch);
+ return LevelDBStatusToFileError(status);
+}
+
+scoped_ptr<ResourceMetadataStorage::Iterator>
+ResourceMetadataStorage::GetIterator() {
+ base::ThreadRestrictions::AssertIOAllowed();
+
+ scoped_ptr<leveldb::Iterator> it(
+ resource_map_->NewIterator(leveldb::ReadOptions()));
+ return make_scoped_ptr(new Iterator(it.Pass()));
+}
+
+FileError ResourceMetadataStorage::GetChild(const std::string& parent_id,
+ const std::string& child_name,
+ std::string* child_id) {
+ base::ThreadRestrictions::AssertIOAllowed();
+ DCHECK(!parent_id.empty());
+ DCHECK(!child_name.empty());
+
+ const leveldb::Status status =
+ resource_map_->Get(
+ leveldb::ReadOptions(),
+ leveldb::Slice(GetChildEntryKey(parent_id, child_name)),
+ child_id);
+ return LevelDBStatusToFileError(status);
+}
+
+FileError ResourceMetadataStorage::GetChildren(
+ const std::string& parent_id,
+ std::vector<std::string>* children) {
+ base::ThreadRestrictions::AssertIOAllowed();
+ DCHECK(!parent_id.empty());
+
+ // Iterate over all entries with keys starting with |parent_id|.
+ scoped_ptr<leveldb::Iterator> it(
+ resource_map_->NewIterator(leveldb::ReadOptions()));
+ for (it->Seek(parent_id);
+ it->Valid() && it->key().starts_with(leveldb::Slice(parent_id));
+ it->Next()) {
+ if (IsChildEntryKey(it->key()))
+ children->push_back(it->value().ToString());
+ }
+ return LevelDBStatusToFileError(it->status());
+}
+
+ResourceMetadataStorage::RecoveredCacheInfo::RecoveredCacheInfo()
+ : is_dirty(false) {}
+
+ResourceMetadataStorage::RecoveredCacheInfo::~RecoveredCacheInfo() {}
+
+FileError ResourceMetadataStorage::GetIdByResourceId(
+ const std::string& resource_id,
+ std::string* out_id) {
+ base::ThreadRestrictions::AssertIOAllowed();
+ DCHECK(!resource_id.empty());
+
+ const leveldb::Status status = resource_map_->Get(
+ leveldb::ReadOptions(),
+ leveldb::Slice(GetIdEntryKey(resource_id)),
+ out_id);
+ return LevelDBStatusToFileError(status);
+}
+
+ResourceMetadataStorage::~ResourceMetadataStorage() {
+ base::ThreadRestrictions::AssertIOAllowed();
+}
+
+void ResourceMetadataStorage::DestroyOnBlockingPool() {
+ delete this;
+}
+
+// static
+std::string ResourceMetadataStorage::GetChildEntryKey(
+ const std::string& parent_id,
+ const std::string& child_name) {
+ DCHECK(!parent_id.empty());
+ DCHECK(!child_name.empty());
+
+ std::string key = parent_id;
+ key.push_back(kDBKeyDelimeter);
+ key.append(child_name);
+ key.push_back(kDBKeyDelimeter);
+ return key;
+}
+
+FileError ResourceMetadataStorage::PutHeader(
+ const ResourceMetadataHeader& header) {
+ base::ThreadRestrictions::AssertIOAllowed();
+
+ std::string serialized_header;
+ if (!header.SerializeToString(&serialized_header)) {
+ DLOG(ERROR) << "Failed to serialize the header";
+ return FILE_ERROR_FAILED;
+ }
+
+ const leveldb::Status status = resource_map_->Put(
+ leveldb::WriteOptions(),
+ leveldb::Slice(GetHeaderDBKey()),
+ leveldb::Slice(serialized_header));
+ return LevelDBStatusToFileError(status);
+}
+
+FileError ResourceMetadataStorage::GetHeader(ResourceMetadataHeader* header) {
+ base::ThreadRestrictions::AssertIOAllowed();
+
+ std::string serialized_header;
+ const leveldb::Status status = resource_map_->Get(
+ leveldb::ReadOptions(),
+ leveldb::Slice(GetHeaderDBKey()),
+ &serialized_header);
+ if (!status.ok())
+ return LevelDBStatusToFileError(status);
+ return header->ParseFromString(serialized_header) ?
+ FILE_ERROR_OK : FILE_ERROR_FAILED;
+}
+
+bool ResourceMetadataStorage::CheckValidity() {
+ base::ThreadRestrictions::AssertIOAllowed();
+
+ // Perform read with checksums verification enabled.
+ leveldb::ReadOptions options;
+ options.verify_checksums = true;
+
+ scoped_ptr<leveldb::Iterator> it(resource_map_->NewIterator(options));
+ it->SeekToFirst();
+
+ // DB is organized like this:
+ //
+ // <key> : <value>
+ // "\0HEADER" : ResourceMetadataHeader
+ // "\0ID\0|resource ID 1|" : Local ID associated to resource ID 1.
+ // "\0ID\0|resource ID 2|" : Local ID associated to resource ID 2.
+ // ...
+ // "|ID of A|" : ResourceEntry for entry A.
+ // "|ID of A|\0|child name 1|\0" : ID of the 1st child entry of entry A.
+ // "|ID of A|\0|child name 2|\0" : ID of the 2nd child entry of entry A.
+ // ...
+ // "|ID of A|\0|child name n|\0" : ID of the nth child entry of entry A.
+ // "|ID of B|" : ResourceEntry for entry B.
+ // ...
+
+ // Check the header.
+ ResourceMetadataHeader header;
+ if (!it->Valid() ||
+ it->key() != GetHeaderDBKey() || // Header entry must come first.
+ !header.ParseFromArray(it->value().data(), it->value().size()) ||
+ header.version() != kDBVersion) {
+ DLOG(ERROR) << "Invalid header detected. version = " << header.version();
+ RecordCheckValidityFailure(CHECK_VALIDITY_FAILURE_INVALID_HEADER);
+ return false;
+ }
+
+ // First scan. Remember relationships between IDs.
+ typedef base::hash_map<std::string, std::string> KeyToIdMapping;
+ KeyToIdMapping local_id_to_resource_id_map;
+ KeyToIdMapping child_key_to_local_id_map;
+ std::set<std::string> resource_entries;
+ std::string first_resource_entry_key;
+ for (it->Next(); it->Valid(); it->Next()) {
+ if (IsChildEntryKey(it->key())) {
+ child_key_to_local_id_map[it->key().ToString()] = it->value().ToString();
+ continue;
+ }
+
+ if (IsIdEntryKey(it->key())) {
+ const auto result = local_id_to_resource_id_map.insert(std::make_pair(
+ it->value().ToString(),
+ GetResourceIdFromIdEntryKey(it->key().ToString())));
+ // Check that no local ID is associated with more than one resource ID.
+ if (!result.second) {
+ DLOG(ERROR) << "Broken ID entry.";
+ RecordCheckValidityFailure(CHECK_VALIDITY_FAILURE_BROKEN_ID_ENTRY);
+ return false;
+ }
+ continue;
+ }
+
+ // Remember the key of the first resource entry record, so the second scan
+ // can start from this point.
+ if (first_resource_entry_key.empty())
+ first_resource_entry_key = it->key().ToString();
+
+ resource_entries.insert(it->key().ToString());
+ }
+
+ // Second scan. Verify relationships and resource entry correctness.
+ size_t num_entries_with_parent = 0;
+ ResourceEntry entry;
+ for (it->Seek(first_resource_entry_key); it->Valid(); it->Next()) {
+ if (IsChildEntryKey(it->key()))
+ continue;
+
+ if (!entry.ParseFromArray(it->value().data(), it->value().size())) {
+ DLOG(ERROR) << "Broken entry detected.";
+ RecordCheckValidityFailure(CHECK_VALIDITY_FAILURE_BROKEN_ENTRY);
+ return false;
+ }
+
+ // Resource-ID-to-local-ID mapping without entry for the local ID is OK,
+ // but if it exists, then the resource ID must be consistent.
+ const auto mapping_it =
+ local_id_to_resource_id_map.find(it->key().ToString());
+ if (mapping_it != local_id_to_resource_id_map.end() &&
+ entry.resource_id() != mapping_it->second) {
+ DLOG(ERROR) << "Broken ID entry.";
+ RecordCheckValidityFailure(CHECK_VALIDITY_FAILURE_BROKEN_ID_ENTRY);
+ return false;
+ }
+
+ // If the parent is referenced, then confirm that it exists and check the
+ // parent-child relationships.
+ if (!entry.parent_local_id().empty()) {
+ const auto mapping_it = resource_entries.find(entry.parent_local_id());
+ if (mapping_it == resource_entries.end()) {
+ DLOG(ERROR) << "Parent entry not found.";
+ RecordCheckValidityFailure(CHECK_VALIDITY_FAILURE_INVALID_PARENT_ID);
+ return false;
+ }
+
+ // Check if parent-child relationship is stored correctly.
+ const auto child_mapping_it = child_key_to_local_id_map.find(
+ GetChildEntryKey(entry.parent_local_id(), entry.base_name()));
+ if (child_mapping_it == child_key_to_local_id_map.end() ||
+ leveldb::Slice(child_mapping_it->second) != it->key()) {
+ DLOG(ERROR) << "Child map is broken.";
+ RecordCheckValidityFailure(CHECK_VALIDITY_FAILURE_BROKEN_CHILD_MAP);
+ return false;
+ }
+ ++num_entries_with_parent;
+ }
+ }
+
+ if (!it->status().ok()) {
+ DLOG(ERROR) << "Error during checking resource map. status = "
+ << it->status().ToString();
+ RecordCheckValidityFailure(CHECK_VALIDITY_FAILURE_ITERATOR_ERROR);
+ return false;
+ }
+
+ if (child_key_to_local_id_map.size() != num_entries_with_parent) {
+ DLOG(ERROR) << "Child entry count mismatch.";
+ RecordCheckValidityFailure(
+ CHECK_VALIDITY_FAILURE_CHILD_ENTRY_COUNT_MISMATCH);
+ return false;
+ }
+
+ return true;
+}
+
+} // namespace internal
+} // namespace drive
diff --git a/components/drive/resource_metadata_storage.h b/components/drive/resource_metadata_storage.h
new file mode 100644
index 0000000..8f8c695c
--- /dev/null
+++ b/components/drive/resource_metadata_storage.h
@@ -0,0 +1,172 @@
+// 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_RESOURCE_METADATA_STORAGE_H_
+#define COMPONENTS_DRIVE_RESOURCE_METADATA_STORAGE_H_
+
+#include <string>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/files/file_path.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "components/drive/drive.pb.h"
+#include "components/drive/file_errors.h"
+
+namespace base {
+class SequencedTaskRunner;
+}
+
+namespace leveldb {
+class DB;
+class Iterator;
+}
+
+namespace drive {
+
+class ResourceEntry;
+class ResourceMetadataHeader;
+
+namespace internal {
+
+// Storage for ResourceMetadata which is responsible to manage resource
+// entries and child-parent relationships between entries.
+class ResourceMetadataStorage {
+ public:
+ // This should be incremented when incompatibility change is made to DB
+ // format.
+ static const int kDBVersion = 13;
+
+ // Object to iterate over entries stored in this storage.
+ class Iterator {
+ public:
+ explicit Iterator(scoped_ptr<leveldb::Iterator> it);
+ ~Iterator();
+
+ // Returns true if this iterator cannot advance any more and does not point
+ // to a valid entry. Get() and Advance() should not be called in such cases.
+ bool IsAtEnd() const;
+
+ // Returns the ID of the entry currently pointed by this object.
+ std::string GetID() const;
+
+ // Returns the entry currently pointed by this object.
+ const ResourceEntry& GetValue() const;
+
+ // Advances to the next entry.
+ void Advance();
+
+ // Returns true if this object has encountered any error.
+ bool HasError() const;
+
+ private:
+ ResourceEntry entry_;
+ scoped_ptr<leveldb::Iterator> it_;
+
+ DISALLOW_COPY_AND_ASSIGN(Iterator);
+ };
+
+ // Cache information recovered from trashed DB.
+ struct RecoveredCacheInfo {
+ RecoveredCacheInfo();
+ ~RecoveredCacheInfo();
+
+ bool is_dirty;
+ std::string md5;
+ std::string title;
+ };
+ typedef std::map<std::string, RecoveredCacheInfo> RecoveredCacheInfoMap;
+
+ // Returns true if the DB was successfully upgraded to the newest version.
+ static bool UpgradeOldDB(const base::FilePath& directory_path);
+
+ ResourceMetadataStorage(const base::FilePath& directory_path,
+ base::SequencedTaskRunner* blocking_task_runner);
+
+ const base::FilePath& directory_path() const { return directory_path_; }
+
+ // Returns true when cache entries were not loaded to the DB during
+ // initialization.
+ bool cache_file_scan_is_needed() const { return cache_file_scan_is_needed_; }
+
+ // Destroys this object.
+ void Destroy();
+
+ // Initializes this object.
+ bool Initialize();
+
+ // Collects cache info from trashed resource map DB.
+ void RecoverCacheInfoFromTrashedResourceMap(RecoveredCacheInfoMap* out_info);
+
+ // Sets the largest changestamp.
+ FileError SetLargestChangestamp(int64 largest_changestamp);
+
+ // Gets the largest changestamp.
+ FileError GetLargestChangestamp(int64* largest_changestamp);
+
+ // Puts the entry to this storage.
+ FileError PutEntry(const ResourceEntry& entry);
+
+ // Gets an entry stored in this storage.
+ FileError GetEntry(const std::string& id, ResourceEntry* out_entry);
+
+ // Removes an entry from this storage.
+ FileError RemoveEntry(const std::string& id);
+
+ // Returns an object to iterate over entries stored in this storage.
+ scoped_ptr<Iterator> GetIterator();
+
+ // Returns the ID of the parent's child.
+ FileError GetChild(const std::string& parent_id,
+ const std::string& child_name,
+ std::string* child_id);
+
+ // Returns the IDs of the parent's children.
+ FileError GetChildren(const std::string& parent_id,
+ std::vector<std::string>* children);
+
+ // Returns the local ID associated with the given resource ID.
+ FileError GetIdByResourceId(const std::string& resource_id,
+ std::string* out_id);
+
+ private:
+ friend class ResourceMetadataStorageTest;
+
+ // To destruct this object, use Destroy().
+ ~ResourceMetadataStorage();
+
+ // Used to implement Destroy().
+ void DestroyOnBlockingPool();
+
+ // Returns a string to be used as a key for child entry.
+ static std::string GetChildEntryKey(const std::string& parent_id,
+ const std::string& child_name);
+
+ // Puts header.
+ FileError PutHeader(const ResourceMetadataHeader& header);
+
+ // Gets header.
+ FileError GetHeader(ResourceMetadataHeader* out_header);
+
+ // Checks validity of the data.
+ bool CheckValidity();
+
+ // Path to the directory where the data is stored.
+ base::FilePath directory_path_;
+
+ bool cache_file_scan_is_needed_;
+
+ // Entries stored in this storage.
+ scoped_ptr<leveldb::DB> resource_map_;
+
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_;
+
+ DISALLOW_COPY_AND_ASSIGN(ResourceMetadataStorage);
+};
+
+} // namespace internal
+} // namespace drive
+
+#endif // COMPONENTS_DRIVE_RESOURCE_METADATA_STORAGE_H_
diff --git a/components/drive/resource_metadata_storage_unittest.cc b/components/drive/resource_metadata_storage_unittest.cc
new file mode 100644
index 0000000..5596938
--- /dev/null
+++ b/components/drive/resource_metadata_storage_unittest.cc
@@ -0,0 +1,633 @@
+// 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/resource_metadata_storage.h"
+
+#include <algorithm>
+
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/single_thread_task_runner.h"
+#include "base/strings/string_split.h"
+#include "base/thread_task_runner_handle.h"
+#include "components/drive/drive.pb.h"
+#include "components/drive/drive_test_util.h"
+#include "content/public/test/test_browser_thread_bundle.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/leveldatabase/src/include/leveldb/db.h"
+#include "third_party/leveldatabase/src/include/leveldb/write_batch.h"
+
+namespace drive {
+namespace internal {
+
+class ResourceMetadataStorageTest : public testing::Test {
+ protected:
+ void SetUp() override {
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+
+ storage_.reset(new ResourceMetadataStorage(
+ temp_dir_.path(), base::ThreadTaskRunnerHandle::Get().get()));
+ ASSERT_TRUE(storage_->Initialize());
+ }
+
+ // Overwrites |storage_|'s version.
+ void SetDBVersion(int version) {
+ ResourceMetadataHeader header;
+ ASSERT_EQ(FILE_ERROR_OK, storage_->GetHeader(&header));
+ header.set_version(version);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutHeader(header));
+ }
+
+ bool CheckValidity() {
+ return storage_->CheckValidity();
+ }
+
+ leveldb::DB* resource_map() { return storage_->resource_map_.get(); }
+
+ // Puts a child entry.
+ void PutChild(const std::string& parent_id,
+ const std::string& child_base_name,
+ const std::string& child_id) {
+ storage_->resource_map_->Put(
+ leveldb::WriteOptions(),
+ ResourceMetadataStorage::GetChildEntryKey(parent_id, child_base_name),
+ child_id);
+ }
+
+ // Removes a child entry.
+ void RemoveChild(const std::string& parent_id,
+ const std::string& child_base_name) {
+ storage_->resource_map_->Delete(
+ leveldb::WriteOptions(),
+ ResourceMetadataStorage::GetChildEntryKey(parent_id, child_base_name));
+ }
+
+ content::TestBrowserThreadBundle thread_bundle_;
+ base::ScopedTempDir temp_dir_;
+ scoped_ptr<ResourceMetadataStorage,
+ test_util::DestroyHelperForTests> storage_;
+};
+
+TEST_F(ResourceMetadataStorageTest, LargestChangestamp) {
+ const int64 kLargestChangestamp = 1234567890;
+ EXPECT_EQ(FILE_ERROR_OK,
+ storage_->SetLargestChangestamp(kLargestChangestamp));
+ int64 value = 0;
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetLargestChangestamp(&value));
+ EXPECT_EQ(kLargestChangestamp, value);
+}
+
+TEST_F(ResourceMetadataStorageTest, PutEntry) {
+ const std::string key1 = "abcdefg";
+ const std::string key2 = "abcd";
+ const std::string key3 = "efgh";
+ const std::string name2 = "ABCD";
+ const std::string name3 = "EFGH";
+
+ // key1 not found.
+ ResourceEntry result;
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, storage_->GetEntry(key1, &result));
+
+ // Put entry1.
+ ResourceEntry entry1;
+ entry1.set_local_id(key1);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry1));
+
+ // key1 found.
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetEntry(key1, &result));
+
+ // key2 not found.
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, storage_->GetEntry(key2, &result));
+
+ // Put entry2 as a child of entry1.
+ ResourceEntry entry2;
+ entry2.set_local_id(key2);
+ entry2.set_parent_local_id(key1);
+ entry2.set_base_name(name2);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry2));
+
+ // key2 found.
+ std::string child_id;
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetEntry(key2, &result));
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetChild(key1, name2, &child_id));
+ EXPECT_EQ(key2, child_id);
+
+ // Put entry3 as a child of entry2.
+ ResourceEntry entry3;
+ entry3.set_local_id(key3);
+ entry3.set_parent_local_id(key2);
+ entry3.set_base_name(name3);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry3));
+
+ // key3 found.
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetEntry(key3, &result));
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetChild(key2, name3, &child_id));
+ EXPECT_EQ(key3, child_id);
+
+ // Change entry3's parent to entry1.
+ entry3.set_parent_local_id(key1);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry3));
+
+ // entry3 is a child of entry1 now.
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, storage_->GetChild(key2, name3, &child_id));
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetChild(key1, name3, &child_id));
+ EXPECT_EQ(key3, child_id);
+
+ // Remove entries.
+ EXPECT_EQ(FILE_ERROR_OK, storage_->RemoveEntry(key3));
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, storage_->GetEntry(key3, &result));
+ EXPECT_EQ(FILE_ERROR_OK, storage_->RemoveEntry(key2));
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, storage_->GetEntry(key2, &result));
+ EXPECT_EQ(FILE_ERROR_OK, storage_->RemoveEntry(key1));
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, storage_->GetEntry(key1, &result));
+}
+
+TEST_F(ResourceMetadataStorageTest, Iterator) {
+ // Prepare data.
+ std::vector<std::string> keys;
+
+ keys.push_back("entry1");
+ keys.push_back("entry2");
+ keys.push_back("entry3");
+ keys.push_back("entry4");
+
+ for (size_t i = 0; i < keys.size(); ++i) {
+ ResourceEntry entry;
+ entry.set_local_id(keys[i]);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry));
+ }
+
+ // Iterate and check the result.
+ std::map<std::string, ResourceEntry> found_entries;
+ scoped_ptr<ResourceMetadataStorage::Iterator> it = storage_->GetIterator();
+ ASSERT_TRUE(it);
+ for (; !it->IsAtEnd(); it->Advance()) {
+ const ResourceEntry& entry = it->GetValue();
+ found_entries[it->GetID()] = entry;
+ }
+ EXPECT_FALSE(it->HasError());
+
+ EXPECT_EQ(keys.size(), found_entries.size());
+ for (size_t i = 0; i < keys.size(); ++i)
+ EXPECT_EQ(1U, found_entries.count(keys[i]));
+}
+
+TEST_F(ResourceMetadataStorageTest, GetIdByResourceId) {
+ const std::string local_id = "local_id";
+ const std::string resource_id = "resource_id";
+
+ // Resource ID to local ID mapping is not stored yet.
+ std::string id;
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND,
+ storage_->GetIdByResourceId(resource_id, &id));
+
+ // Put an entry with the resource ID.
+ ResourceEntry entry;
+ entry.set_local_id(local_id);
+ entry.set_resource_id(resource_id);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry));
+
+ // Can get local ID by resource ID.
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetIdByResourceId(resource_id, &id));
+ EXPECT_EQ(local_id, id);
+
+ // Resource ID to local ID mapping is removed.
+ EXPECT_EQ(FILE_ERROR_OK, storage_->RemoveEntry(local_id));
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND,
+ storage_->GetIdByResourceId(resource_id, &id));
+}
+
+TEST_F(ResourceMetadataStorageTest, GetChildren) {
+ const std::string parents_id[] = { "mercury", "venus", "mars", "jupiter",
+ "saturn" };
+ std::vector<base::StringPairs> children_name_id(arraysize(parents_id));
+ // Skip children_name_id[0/1] here because Mercury and Venus have no moon.
+ children_name_id[2].push_back(std::make_pair("phobos", "mars_i"));
+ children_name_id[2].push_back(std::make_pair("deimos", "mars_ii"));
+ children_name_id[3].push_back(std::make_pair("io", "jupiter_i"));
+ children_name_id[3].push_back(std::make_pair("europa", "jupiter_ii"));
+ children_name_id[3].push_back(std::make_pair("ganymede", "jupiter_iii"));
+ children_name_id[3].push_back(std::make_pair("calisto", "jupiter_iv"));
+ children_name_id[4].push_back(std::make_pair("mimas", "saturn_i"));
+ children_name_id[4].push_back(std::make_pair("enceladus", "saturn_ii"));
+ children_name_id[4].push_back(std::make_pair("tethys", "saturn_iii"));
+ children_name_id[4].push_back(std::make_pair("dione", "saturn_iv"));
+ children_name_id[4].push_back(std::make_pair("rhea", "saturn_v"));
+ children_name_id[4].push_back(std::make_pair("titan", "saturn_vi"));
+ children_name_id[4].push_back(std::make_pair("iapetus", "saturn_vii"));
+
+ // Put parents.
+ for (size_t i = 0; i < arraysize(parents_id); ++i) {
+ ResourceEntry entry;
+ entry.set_local_id(parents_id[i]);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry));
+ }
+
+ // Put children.
+ for (size_t i = 0; i < children_name_id.size(); ++i) {
+ for (size_t j = 0; j < children_name_id[i].size(); ++j) {
+ ResourceEntry entry;
+ entry.set_local_id(children_name_id[i][j].second);
+ entry.set_parent_local_id(parents_id[i]);
+ entry.set_base_name(children_name_id[i][j].first);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry));
+ }
+ }
+
+ // Try to get children.
+ for (size_t i = 0; i < children_name_id.size(); ++i) {
+ std::vector<std::string> children;
+ storage_->GetChildren(parents_id[i], &children);
+ EXPECT_EQ(children_name_id[i].size(), children.size());
+ for (size_t j = 0; j < children_name_id[i].size(); ++j) {
+ EXPECT_EQ(1, std::count(children.begin(),
+ children.end(),
+ children_name_id[i][j].second));
+ }
+ }
+}
+
+TEST_F(ResourceMetadataStorageTest, OpenExistingDB) {
+ const std::string parent_id1 = "abcdefg";
+ const std::string child_name1 = "WXYZABC";
+ const std::string child_id1 = "qwerty";
+
+ ResourceEntry entry1;
+ entry1.set_local_id(parent_id1);
+ ResourceEntry entry2;
+ entry2.set_local_id(child_id1);
+ entry2.set_parent_local_id(parent_id1);
+ entry2.set_base_name(child_name1);
+
+ // Put some data.
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry1));
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry2));
+
+ // Close DB and reopen.
+ storage_.reset(new ResourceMetadataStorage(
+ temp_dir_.path(), base::ThreadTaskRunnerHandle::Get().get()));
+ ASSERT_TRUE(storage_->Initialize());
+
+ // Can read data.
+ ResourceEntry result;
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetEntry(parent_id1, &result));
+
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetEntry(child_id1, &result));
+ EXPECT_EQ(parent_id1, result.parent_local_id());
+ EXPECT_EQ(child_name1, result.base_name());
+
+ std::string child_id;
+ EXPECT_EQ(FILE_ERROR_OK,
+ storage_->GetChild(parent_id1, child_name1, &child_id));
+ EXPECT_EQ(child_id1, child_id);
+}
+
+TEST_F(ResourceMetadataStorageTest, IncompatibleDB_M29) {
+ const int64 kLargestChangestamp = 1234567890;
+ const std::string title = "title";
+
+ // Construct M29 version DB.
+ SetDBVersion(6);
+ EXPECT_EQ(FILE_ERROR_OK,
+ storage_->SetLargestChangestamp(kLargestChangestamp));
+
+ leveldb::WriteBatch batch;
+
+ // Put a file entry and its cache entry.
+ ResourceEntry entry;
+ std::string serialized_entry;
+ entry.set_title(title);
+ entry.set_resource_id("file:abcd");
+ EXPECT_TRUE(entry.SerializeToString(&serialized_entry));
+ batch.Put("file:abcd", serialized_entry);
+
+ FileCacheEntry cache_entry;
+ EXPECT_TRUE(cache_entry.SerializeToString(&serialized_entry));
+ batch.Put(std::string("file:abcd") + '\0' + "CACHE", serialized_entry);
+
+ EXPECT_TRUE(resource_map()->Write(leveldb::WriteOptions(), &batch).ok());
+
+ // Upgrade and reopen.
+ storage_.reset();
+ EXPECT_TRUE(ResourceMetadataStorage::UpgradeOldDB(temp_dir_.path()));
+ storage_.reset(new ResourceMetadataStorage(
+ temp_dir_.path(), base::ThreadTaskRunnerHandle::Get().get()));
+ ASSERT_TRUE(storage_->Initialize());
+
+ // Resource-ID-to-local-ID mapping is added.
+ std::string id;
+ EXPECT_EQ(FILE_ERROR_OK,
+ storage_->GetIdByResourceId("abcd", &id)); // "file:" is dropped.
+
+ // Data is erased, except cache entries.
+ int64 largest_changestamp = 0;
+ EXPECT_EQ(FILE_ERROR_OK,
+ storage_->GetLargestChangestamp(&largest_changestamp));
+ EXPECT_EQ(0, largest_changestamp);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetEntry(id, &entry));
+ EXPECT_TRUE(entry.title().empty());
+ EXPECT_TRUE(entry.file_specific_info().has_cache_state());
+}
+
+TEST_F(ResourceMetadataStorageTest, IncompatibleDB_M32) {
+ const int64 kLargestChangestamp = 1234567890;
+ const std::string title = "title";
+ const std::string resource_id = "abcd";
+ const std::string local_id = "local-abcd";
+
+ // Construct M32 version DB.
+ SetDBVersion(11);
+ EXPECT_EQ(FILE_ERROR_OK,
+ storage_->SetLargestChangestamp(kLargestChangestamp));
+
+ leveldb::WriteBatch batch;
+
+ // Put a file entry and its cache and id entry.
+ ResourceEntry entry;
+ std::string serialized_entry;
+ entry.set_title(title);
+ entry.set_local_id(local_id);
+ entry.set_resource_id(resource_id);
+ EXPECT_TRUE(entry.SerializeToString(&serialized_entry));
+ batch.Put(local_id, serialized_entry);
+
+ FileCacheEntry cache_entry;
+ EXPECT_TRUE(cache_entry.SerializeToString(&serialized_entry));
+ batch.Put(local_id + '\0' + "CACHE", serialized_entry);
+
+ batch.Put('\0' + std::string("ID") + '\0' + resource_id, local_id);
+
+ EXPECT_TRUE(resource_map()->Write(leveldb::WriteOptions(), &batch).ok());
+
+ // Upgrade and reopen.
+ storage_.reset();
+ EXPECT_TRUE(ResourceMetadataStorage::UpgradeOldDB(temp_dir_.path()));
+ storage_.reset(new ResourceMetadataStorage(
+ temp_dir_.path(), base::ThreadTaskRunnerHandle::Get().get()));
+ ASSERT_TRUE(storage_->Initialize());
+
+ // Data is erased, except cache and id mapping entries.
+ std::string id;
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetIdByResourceId(resource_id, &id));
+ EXPECT_EQ(local_id, id);
+ int64 largest_changestamp = 0;
+ EXPECT_EQ(FILE_ERROR_OK,
+ storage_->GetLargestChangestamp(&largest_changestamp));
+ EXPECT_EQ(0, largest_changestamp);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetEntry(id, &entry));
+ EXPECT_TRUE(entry.title().empty());
+ EXPECT_TRUE(entry.file_specific_info().has_cache_state());
+}
+
+TEST_F(ResourceMetadataStorageTest, IncompatibleDB_M33) {
+ const int64 kLargestChangestamp = 1234567890;
+ const std::string title = "title";
+ const std::string resource_id = "abcd";
+ const std::string local_id = "local-abcd";
+ const std::string md5 = "md5";
+ const std::string resource_id2 = "efgh";
+ const std::string local_id2 = "local-efgh";
+ const std::string md5_2 = "md5_2";
+
+ // Construct M33 version DB.
+ SetDBVersion(12);
+ EXPECT_EQ(FILE_ERROR_OK,
+ storage_->SetLargestChangestamp(kLargestChangestamp));
+
+ leveldb::WriteBatch batch;
+
+ // Put a file entry and its cache and id entry.
+ ResourceEntry entry;
+ std::string serialized_entry;
+ entry.set_title(title);
+ entry.set_local_id(local_id);
+ entry.set_resource_id(resource_id);
+ EXPECT_TRUE(entry.SerializeToString(&serialized_entry));
+ batch.Put(local_id, serialized_entry);
+
+ FileCacheEntry cache_entry;
+ cache_entry.set_md5(md5);
+ EXPECT_TRUE(cache_entry.SerializeToString(&serialized_entry));
+ batch.Put(local_id + '\0' + "CACHE", serialized_entry);
+
+ batch.Put('\0' + std::string("ID") + '\0' + resource_id, local_id);
+
+ // Put another cache entry which is not accompanied by a ResourceEntry.
+ cache_entry.set_md5(md5_2);
+ EXPECT_TRUE(cache_entry.SerializeToString(&serialized_entry));
+ batch.Put(local_id2 + '\0' + "CACHE", serialized_entry);
+ batch.Put('\0' + std::string("ID") + '\0' + resource_id2, local_id2);
+
+ EXPECT_TRUE(resource_map()->Write(leveldb::WriteOptions(), &batch).ok());
+
+ // Upgrade and reopen.
+ storage_.reset();
+ EXPECT_TRUE(ResourceMetadataStorage::UpgradeOldDB(temp_dir_.path()));
+ storage_.reset(new ResourceMetadataStorage(
+ temp_dir_.path(), base::ThreadTaskRunnerHandle::Get().get()));
+ ASSERT_TRUE(storage_->Initialize());
+
+ // No data is lost.
+ int64 largest_changestamp = 0;
+ EXPECT_EQ(FILE_ERROR_OK,
+ storage_->GetLargestChangestamp(&largest_changestamp));
+ EXPECT_EQ(kLargestChangestamp, largest_changestamp);
+
+ std::string id;
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetIdByResourceId(resource_id, &id));
+ EXPECT_EQ(local_id, id);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetEntry(id, &entry));
+ EXPECT_EQ(title, entry.title());
+ EXPECT_EQ(md5, entry.file_specific_info().cache_state().md5());
+
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetIdByResourceId(resource_id2, &id));
+ EXPECT_EQ(local_id2, id);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetEntry(id, &entry));
+ EXPECT_EQ(md5_2, entry.file_specific_info().cache_state().md5());
+}
+
+TEST_F(ResourceMetadataStorageTest, IncompatibleDB_Unknown) {
+ const int64 kLargestChangestamp = 1234567890;
+ const std::string key1 = "abcd";
+
+ // Put some data.
+ EXPECT_EQ(FILE_ERROR_OK,
+ storage_->SetLargestChangestamp(kLargestChangestamp));
+ ResourceEntry entry;
+ entry.set_local_id(key1);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry));
+
+ // Set newer version, upgrade and reopen DB.
+ SetDBVersion(ResourceMetadataStorage::kDBVersion + 1);
+ storage_.reset();
+ EXPECT_FALSE(ResourceMetadataStorage::UpgradeOldDB(temp_dir_.path()));
+ storage_.reset(new ResourceMetadataStorage(
+ temp_dir_.path(), base::ThreadTaskRunnerHandle::Get().get()));
+ ASSERT_TRUE(storage_->Initialize());
+
+ // Data is erased because of the incompatible version.
+ int64 largest_changestamp = 0;
+ EXPECT_EQ(FILE_ERROR_OK,
+ storage_->GetLargestChangestamp(&largest_changestamp));
+ EXPECT_EQ(0, largest_changestamp);
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, storage_->GetEntry(key1, &entry));
+}
+
+TEST_F(ResourceMetadataStorageTest, DeleteUnusedIDEntries) {
+ leveldb::WriteBatch batch;
+
+ // Put an ID entry with a corresponding ResourceEntry.
+ ResourceEntry entry;
+ entry.set_local_id("id1");
+ entry.set_resource_id("resource_id1");
+
+ std::string serialized_entry;
+ EXPECT_TRUE(entry.SerializeToString(&serialized_entry));
+ batch.Put("id1", serialized_entry);
+ batch.Put('\0' + std::string("ID") + '\0' + "resource_id1", "id1");
+
+ // Put an ID entry without any corresponding entries.
+ batch.Put('\0' + std::string("ID") + '\0' + "resource_id2", "id3");
+
+ EXPECT_TRUE(resource_map()->Write(leveldb::WriteOptions(), &batch).ok());
+
+ // Upgrade and reopen.
+ storage_.reset();
+ EXPECT_TRUE(ResourceMetadataStorage::UpgradeOldDB(temp_dir_.path()));
+ storage_.reset(new ResourceMetadataStorage(
+ temp_dir_.path(), base::ThreadTaskRunnerHandle::Get().get()));
+ ASSERT_TRUE(storage_->Initialize());
+
+ // Only the unused entry is deleted.
+ std::string id;
+ EXPECT_EQ(FILE_ERROR_OK, storage_->GetIdByResourceId("resource_id1", &id));
+ EXPECT_EQ("id1", id);
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND,
+ storage_->GetIdByResourceId("resource_id2", &id));
+}
+
+TEST_F(ResourceMetadataStorageTest, WrongPath) {
+ // Create a file.
+ base::FilePath path;
+ ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir_.path(), &path));
+
+ storage_.reset(new ResourceMetadataStorage(
+ path, base::ThreadTaskRunnerHandle::Get().get()));
+ // Cannot initialize DB beacause the path does not point a directory.
+ ASSERT_FALSE(storage_->Initialize());
+}
+
+TEST_F(ResourceMetadataStorageTest, RecoverCacheEntriesFromTrashedResourceMap) {
+ // Put entry with id_foo.
+ ResourceEntry entry;
+ entry.set_local_id("id_foo");
+ entry.set_base_name("foo");
+ entry.set_title("foo");
+ entry.mutable_file_specific_info()->mutable_cache_state()->set_md5("md5_foo");
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry));
+
+ // Put entry with id_bar as a id_foo's child.
+ entry.set_local_id("id_bar");
+ entry.set_parent_local_id("id_foo");
+ entry.set_base_name("bar");
+ entry.set_title("bar");
+ entry.mutable_file_specific_info()->mutable_cache_state()->set_md5("md5_bar");
+ entry.mutable_file_specific_info()->mutable_cache_state()->set_is_dirty(true);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry));
+
+ // Remove parent-child relationship to make the DB invalid.
+ RemoveChild("id_foo", "bar");
+ EXPECT_FALSE(CheckValidity());
+
+ // Reopen. This should result in trashing the DB.
+ storage_.reset(new ResourceMetadataStorage(
+ temp_dir_.path(), base::ThreadTaskRunnerHandle::Get().get()));
+ ASSERT_TRUE(storage_->Initialize());
+
+ // Recover cache entries from the trashed DB.
+ ResourceMetadataStorage::RecoveredCacheInfoMap recovered_cache_info;
+ storage_->RecoverCacheInfoFromTrashedResourceMap(&recovered_cache_info);
+ EXPECT_EQ(2U, recovered_cache_info.size());
+ EXPECT_FALSE(recovered_cache_info["id_foo"].is_dirty);
+ EXPECT_EQ("md5_foo", recovered_cache_info["id_foo"].md5);
+ EXPECT_EQ("foo", recovered_cache_info["id_foo"].title);
+ EXPECT_TRUE(recovered_cache_info["id_bar"].is_dirty);
+ EXPECT_EQ("md5_bar", recovered_cache_info["id_bar"].md5);
+ EXPECT_EQ("bar", recovered_cache_info["id_bar"].title);
+}
+
+TEST_F(ResourceMetadataStorageTest, CheckValidity) {
+ const std::string key1 = "foo";
+ const std::string name1 = "hoge";
+ const std::string key2 = "bar";
+ const std::string name2 = "fuga";
+ const std::string key3 = "boo";
+ const std::string name3 = "piyo";
+
+ // Empty storage is valid.
+ EXPECT_TRUE(CheckValidity());
+
+ // Put entry with key1.
+ ResourceEntry entry;
+ entry.set_local_id(key1);
+ entry.set_base_name(name1);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry));
+ EXPECT_TRUE(CheckValidity());
+
+ // Put entry with key2 under key1.
+ entry.set_local_id(key2);
+ entry.set_parent_local_id(key1);
+ entry.set_base_name(name2);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry));
+ EXPECT_TRUE(CheckValidity());
+
+ RemoveChild(key1, name2);
+ EXPECT_FALSE(CheckValidity()); // Missing parent-child relationship.
+
+ // Add back parent-child relationship between key1 and key2.
+ PutChild(key1, name2, key2);
+ EXPECT_TRUE(CheckValidity());
+
+ // Add parent-child relationship between key2 and key3.
+ PutChild(key2, name3, key3);
+ EXPECT_FALSE(CheckValidity()); // key3 is not stored in the storage.
+
+ // Put entry with key3 under key2.
+ entry.set_local_id(key3);
+ entry.set_parent_local_id(key2);
+ entry.set_base_name(name3);
+ EXPECT_EQ(FILE_ERROR_OK, storage_->PutEntry(entry));
+ EXPECT_TRUE(CheckValidity());
+
+ // Parent-child relationship with wrong name.
+ RemoveChild(key2, name3);
+ EXPECT_FALSE(CheckValidity());
+ PutChild(key2, name2, key3);
+ EXPECT_FALSE(CheckValidity());
+
+ // Fix up the relationship between key2 and key3.
+ RemoveChild(key2, name2);
+ EXPECT_FALSE(CheckValidity());
+ PutChild(key2, name3, key3);
+ EXPECT_TRUE(CheckValidity());
+
+ // Remove key2.
+ RemoveChild(key1, name2);
+ EXPECT_FALSE(CheckValidity());
+ EXPECT_EQ(FILE_ERROR_OK, storage_->RemoveEntry(key2));
+ EXPECT_FALSE(CheckValidity());
+
+ // Remove key3.
+ RemoveChild(key2, name3);
+ EXPECT_FALSE(CheckValidity());
+ EXPECT_EQ(FILE_ERROR_OK, storage_->RemoveEntry(key3));
+ EXPECT_TRUE(CheckValidity());
+
+ // Remove key1.
+ EXPECT_EQ(FILE_ERROR_OK, storage_->RemoveEntry(key1));
+ EXPECT_TRUE(CheckValidity());
+}
+
+} // namespace internal
+} // namespace drive
diff --git a/components/drive/resource_metadata_unittest.cc b/components/drive/resource_metadata_unittest.cc
new file mode 100644
index 0000000..15beb4d
--- /dev/null
+++ b/components/drive/resource_metadata_unittest.cc
@@ -0,0 +1,709 @@
+// 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/resource_metadata.h"
+
+#include <algorithm>
+#include <string>
+#include <vector>
+
+#include "base/files/scoped_temp_dir.h"
+#include "base/single_thread_task_runner.h"
+#include "base/strings/stringprintf.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_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 {
+
+// The changestamp of the resource metadata used in
+// ResourceMetadataTest.
+const int64 kTestChangestamp = 100;
+
+// Returns the sorted base names from |entries|.
+std::vector<std::string> GetSortedBaseNames(
+ const ResourceEntryVector& entries) {
+ std::vector<std::string> base_names;
+ for (size_t i = 0; i < entries.size(); ++i)
+ base_names.push_back(entries[i].base_name());
+ std::sort(base_names.begin(), base_names.end());
+
+ return base_names;
+}
+
+// Creates a ResourceEntry for a directory with explicitly set resource_id.
+ResourceEntry CreateDirectoryEntryWithResourceId(
+ const std::string& title,
+ const std::string& resource_id,
+ const std::string& parent_local_id) {
+ ResourceEntry entry;
+ entry.set_title(title);
+ entry.set_resource_id(resource_id);
+ entry.set_parent_local_id(parent_local_id);
+ entry.mutable_file_info()->set_is_directory(true);
+ entry.mutable_directory_specific_info()->set_changestamp(kTestChangestamp);
+ return entry;
+}
+
+// Creates a ResourceEntry for a directory.
+ResourceEntry CreateDirectoryEntry(const std::string& title,
+ const std::string& parent_local_id) {
+ return CreateDirectoryEntryWithResourceId(
+ title, "id:" + title, parent_local_id);
+}
+
+// Creates a ResourceEntry for a file with explicitly set resource_id.
+ResourceEntry CreateFileEntryWithResourceId(
+ const std::string& title,
+ const std::string& resource_id,
+ const std::string& parent_local_id) {
+ ResourceEntry entry;
+ entry.set_title(title);
+ entry.set_resource_id(resource_id);
+ entry.set_parent_local_id(parent_local_id);
+ entry.mutable_file_info()->set_is_directory(false);
+ entry.mutable_file_info()->set_size(1024);
+ entry.mutable_file_specific_info()->set_md5("md5:" + title);
+ return entry;
+}
+
+// Creates a ResourceEntry for a file.
+ResourceEntry CreateFileEntry(const std::string& title,
+ const std::string& parent_local_id) {
+ return CreateFileEntryWithResourceId(title, "id:" + title, parent_local_id);
+}
+
+// Creates the following files/directories
+// drive/root/dir1/
+// drive/root/dir2/
+// drive/root/dir1/dir3/
+// drive/root/dir1/file4
+// drive/root/dir1/file5
+// drive/root/dir2/file6
+// drive/root/dir2/file7
+// drive/root/dir2/file8
+// drive/root/dir1/dir3/file9
+// drive/root/dir1/dir3/file10
+void SetUpEntries(ResourceMetadata* resource_metadata) {
+ std::string local_id;
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata->GetIdByPath(
+ util::GetDriveMyDriveRootPath(), &local_id));
+ const std::string root_local_id = local_id;
+
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata->AddEntry(
+ CreateDirectoryEntry("dir1", root_local_id), &local_id));
+ const std::string local_id_dir1 = local_id;
+
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata->AddEntry(
+ CreateDirectoryEntry("dir2", root_local_id), &local_id));
+ const std::string local_id_dir2 = local_id;
+
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata->AddEntry(
+ CreateDirectoryEntry("dir3", local_id_dir1), &local_id));
+ const std::string local_id_dir3 = local_id;
+
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata->AddEntry(
+ CreateFileEntry("file4", local_id_dir1), &local_id));
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata->AddEntry(
+ CreateFileEntry("file5", local_id_dir1), &local_id));
+
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata->AddEntry(
+ CreateFileEntry("file6", local_id_dir2), &local_id));
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata->AddEntry(
+ CreateFileEntry("file7", local_id_dir2), &local_id));
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata->AddEntry(
+ CreateFileEntry("file8", local_id_dir2), &local_id));
+
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata->AddEntry(
+ CreateFileEntry("file9", local_id_dir3), &local_id));
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata->AddEntry(
+ CreateFileEntry("file10", local_id_dir3), &local_id));
+
+ ASSERT_EQ(FILE_ERROR_OK,
+ resource_metadata->SetLargestChangestamp(kTestChangestamp));
+}
+
+} // namespace
+
+// Tests for methods running on the blocking task runner.
+class ResourceMetadataTest : public testing::Test {
+ protected:
+ void SetUp() override {
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+
+ metadata_storage_.reset(new ResourceMetadataStorage(
+ temp_dir_.path(), base::ThreadTaskRunnerHandle::Get().get()));
+ ASSERT_TRUE(metadata_storage_->Initialize());
+
+ fake_free_disk_space_getter_.reset(new FakeFreeDiskSpaceGetter);
+ 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()));
+
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->Initialize());
+
+ SetUpEntries(resource_metadata_.get());
+ }
+
+ base::ScopedTempDir temp_dir_;
+ content::TestBrowserThreadBundle thread_bundle_;
+ scoped_ptr<ResourceMetadataStorage, test_util::DestroyHelperForTests>
+ metadata_storage_;
+ scoped_ptr<FakeFreeDiskSpaceGetter> fake_free_disk_space_getter_;
+ scoped_ptr<FileCache, test_util::DestroyHelperForTests> cache_;
+ scoped_ptr<ResourceMetadata, test_util::DestroyHelperForTests>
+ resource_metadata_;
+};
+
+TEST_F(ResourceMetadataTest, LargestChangestamp) {
+ const int64 kChangestamp = 123456;
+ EXPECT_EQ(FILE_ERROR_OK,
+ resource_metadata_->SetLargestChangestamp(kChangestamp));
+ int64 changestamp = 0;
+ EXPECT_EQ(FILE_ERROR_OK,
+ resource_metadata_->GetLargestChangestamp(&changestamp));
+ EXPECT_EQ(kChangestamp, changestamp);
+}
+
+TEST_F(ResourceMetadataTest, GetResourceEntryByPath) {
+ // Confirm that an existing file is found.
+ ResourceEntry entry;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/file4"), &entry));
+ EXPECT_EQ("file4", entry.base_name());
+
+ // Confirm that a non existing file is not found.
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/non_existing"), &entry));
+
+ // Confirm that the root is found.
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("drive"), &entry));
+
+ // Confirm that a non existing file is not found at the root level.
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("non_existing"), &entry));
+
+ // Confirm that an entry is not found with a wrong root.
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("non_existing/root"), &entry));
+}
+
+TEST_F(ResourceMetadataTest, ReadDirectoryByPath) {
+ // Confirm that an existing directory is found.
+ ResourceEntryVector entries;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->ReadDirectoryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1"), &entries));
+ ASSERT_EQ(3U, entries.size());
+ // The order is not guaranteed so we should sort the base names.
+ std::vector<std::string> base_names = GetSortedBaseNames(entries);
+ EXPECT_EQ("dir3", base_names[0]);
+ EXPECT_EQ("file4", base_names[1]);
+ EXPECT_EQ("file5", base_names[2]);
+
+ // Confirm that a non existing directory is not found.
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, resource_metadata_->ReadDirectoryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/non_existing"), &entries));
+
+ // Confirm that reading a file results in FILE_ERROR_NOT_A_DIRECTORY.
+ EXPECT_EQ(FILE_ERROR_NOT_A_DIRECTORY, resource_metadata_->ReadDirectoryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/file4"), &entries));
+}
+
+TEST_F(ResourceMetadataTest, RefreshEntry) {
+ base::FilePath drive_file_path;
+ ResourceEntry entry;
+
+ // Get file9.
+ std::string file_id;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetIdByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/dir3/file9"), &file_id));
+ EXPECT_EQ(FILE_ERROR_OK,
+ resource_metadata_->GetResourceEntryById(file_id, &entry));
+ EXPECT_EQ("file9", entry.base_name());
+ EXPECT_TRUE(!entry.file_info().is_directory());
+ EXPECT_EQ("md5:file9", entry.file_specific_info().md5());
+
+ // Rename it.
+ ResourceEntry file_entry(entry);
+ file_entry.set_title("file100");
+ EXPECT_EQ(FILE_ERROR_OK,
+ resource_metadata_->RefreshEntry(file_entry));
+
+ base::FilePath path;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetFilePath(file_id, &path));
+ EXPECT_EQ("drive/root/dir1/dir3/file100", path.AsUTF8Unsafe());
+ entry.Clear();
+ EXPECT_EQ(FILE_ERROR_OK,
+ resource_metadata_->GetResourceEntryById(file_id, &entry));
+ EXPECT_EQ("file100", entry.base_name());
+ EXPECT_TRUE(!entry.file_info().is_directory());
+ EXPECT_EQ("md5:file9", entry.file_specific_info().md5());
+
+ // Update the file md5.
+ const std::string updated_md5("md5:updated");
+ file_entry = entry;
+ file_entry.mutable_file_specific_info()->set_md5(updated_md5);
+ EXPECT_EQ(FILE_ERROR_OK,
+ resource_metadata_->RefreshEntry(file_entry));
+
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetFilePath(file_id, &path));
+ EXPECT_EQ("drive/root/dir1/dir3/file100", path.AsUTF8Unsafe());
+ entry.Clear();
+ EXPECT_EQ(FILE_ERROR_OK,
+ resource_metadata_->GetResourceEntryById(file_id, &entry));
+ EXPECT_EQ("file100", entry.base_name());
+ EXPECT_TRUE(!entry.file_info().is_directory());
+ EXPECT_EQ(updated_md5, entry.file_specific_info().md5());
+
+ // Make sure we get the same thing from GetResourceEntryByPath.
+ entry.Clear();
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/dir3/file100"), &entry));
+ EXPECT_EQ("file100", entry.base_name());
+ ASSERT_TRUE(!entry.file_info().is_directory());
+ EXPECT_EQ(updated_md5, entry.file_specific_info().md5());
+
+ // Get dir2.
+ entry.Clear();
+ std::string dir_id;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetIdByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir2"), &dir_id));
+ EXPECT_EQ(FILE_ERROR_OK,
+ resource_metadata_->GetResourceEntryById(dir_id, &entry));
+ EXPECT_EQ("dir2", entry.base_name());
+ ASSERT_TRUE(entry.file_info().is_directory());
+
+ // Get dir3's ID.
+ std::string dir3_id;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetIdByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/dir3"), &dir3_id));
+
+ // Change the name to dir100 and change the parent to drive/dir1/dir3.
+ ResourceEntry dir_entry(entry);
+ dir_entry.set_title("dir100");
+ dir_entry.set_parent_local_id(dir3_id);
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->RefreshEntry(dir_entry));
+
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetFilePath(dir_id, &path));
+ EXPECT_EQ("drive/root/dir1/dir3/dir100", path.AsUTF8Unsafe());
+ entry.Clear();
+ EXPECT_EQ(FILE_ERROR_OK,
+ resource_metadata_->GetResourceEntryById(dir_id, &entry));
+ EXPECT_EQ("dir100", entry.base_name());
+ EXPECT_TRUE(entry.file_info().is_directory());
+ EXPECT_EQ("id:dir2", entry.resource_id());
+
+ // Make sure the children have moved over. Test file6.
+ entry.Clear();
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/dir3/dir100/file6"),
+ &entry));
+ EXPECT_EQ("file6", entry.base_name());
+
+ // Make sure dir2 no longer exists.
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir2"), &entry));
+
+ // Make sure that directory cannot move under a file.
+ dir_entry.set_parent_local_id(file_id);
+ EXPECT_EQ(FILE_ERROR_NOT_A_DIRECTORY,
+ resource_metadata_->RefreshEntry(dir_entry));
+
+ // Cannot refresh root.
+ dir_entry.Clear();
+ dir_entry.set_local_id(util::kDriveGrandRootLocalId);
+ dir_entry.set_title("new-root-name");
+ dir_entry.set_parent_local_id(dir3_id);
+ EXPECT_EQ(FILE_ERROR_INVALID_OPERATION,
+ resource_metadata_->RefreshEntry(dir_entry));
+}
+
+TEST_F(ResourceMetadataTest, RefreshEntry_ResourceIDCheck) {
+ // Get an entry with a non-empty resource ID.
+ ResourceEntry entry;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1"), &entry));
+ EXPECT_FALSE(entry.resource_id().empty());
+
+ // Add a new entry with an empty resource ID.
+ ResourceEntry new_entry;
+ new_entry.set_parent_local_id(entry.local_id());
+ new_entry.set_title("new entry");
+ std::string local_id;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(new_entry, &local_id));
+
+ // Try to refresh the new entry with a used resource ID.
+ new_entry.set_local_id(local_id);
+ new_entry.set_resource_id(entry.resource_id());
+ EXPECT_EQ(FILE_ERROR_INVALID_OPERATION,
+ resource_metadata_->RefreshEntry(new_entry));
+}
+
+TEST_F(ResourceMetadataTest, RefreshEntry_DoNotOverwriteCacheState) {
+ ResourceEntry entry;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/file4"), &entry));
+
+ // Try to set MD5 with RefreshEntry.
+ entry.mutable_file_specific_info()->mutable_cache_state()->set_md5("md5");
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->RefreshEntry(entry));
+
+ // Cache state is unchanged.
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/file4"), &entry));
+ EXPECT_TRUE(entry.file_specific_info().cache_state().md5().empty());
+
+ // Pin the file.
+ EXPECT_EQ(FILE_ERROR_OK, cache_->Pin(entry.local_id()));
+
+ // Try to clear the cache state with RefreshEntry.
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/file4"), &entry));
+ entry.mutable_file_specific_info()->clear_cache_state();
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->RefreshEntry(entry));
+
+ // Cache state is not cleared.
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/file4"), &entry));
+ EXPECT_TRUE(entry.file_specific_info().cache_state().is_pinned());
+}
+
+TEST_F(ResourceMetadataTest, GetSubDirectoriesRecursively) {
+ std::set<base::FilePath> sub_directories;
+
+ // file9: not a directory, so no children.
+ std::string local_id;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetIdByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/dir3/file9"), &local_id));
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetSubDirectoriesRecursively(
+ local_id, &sub_directories));
+ EXPECT_TRUE(sub_directories.empty());
+
+ // dir2: no child directories.
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetIdByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir2"), &local_id));
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetSubDirectoriesRecursively(
+ local_id, &sub_directories));
+ EXPECT_TRUE(sub_directories.empty());
+ const std::string dir2_id = local_id;
+
+ // dir1: dir3 is the only child
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetIdByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1"), &local_id));
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetSubDirectoriesRecursively(
+ local_id, &sub_directories));
+ EXPECT_EQ(1u, sub_directories.size());
+ EXPECT_EQ(1u, sub_directories.count(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/dir3")));
+ sub_directories.clear();
+
+ // Add a few more directories to make sure deeper nesting works.
+ // dir2/dir100
+ // dir2/dir101
+ // dir2/dir101/dir102
+ // dir2/dir101/dir103
+ // dir2/dir101/dir104
+ // dir2/dir101/dir104/dir105
+ // dir2/dir101/dir104/dir105/dir106
+ // dir2/dir101/dir104/dir105/dir106/dir107
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(
+ CreateDirectoryEntry("dir100", dir2_id), &local_id));
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(
+ CreateDirectoryEntry("dir101", dir2_id), &local_id));
+ const std::string dir101_id = local_id;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(
+ CreateDirectoryEntry("dir102", dir101_id), &local_id));
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(
+ CreateDirectoryEntry("dir103", dir101_id), &local_id));
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(
+ CreateDirectoryEntry("dir104", dir101_id), &local_id));
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(
+ CreateDirectoryEntry("dir105", local_id), &local_id));
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(
+ CreateDirectoryEntry("dir106", local_id), &local_id));
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(
+ CreateDirectoryEntry("dir107", local_id), &local_id));
+
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetSubDirectoriesRecursively(
+ dir2_id, &sub_directories));
+ EXPECT_EQ(8u, sub_directories.size());
+ EXPECT_EQ(1u, sub_directories.count(base::FilePath::FromUTF8Unsafe(
+ "drive/root/dir2/dir101")));
+ EXPECT_EQ(1u, sub_directories.count(base::FilePath::FromUTF8Unsafe(
+ "drive/root/dir2/dir101/dir104")));
+ EXPECT_EQ(1u, sub_directories.count(base::FilePath::FromUTF8Unsafe(
+ "drive/root/dir2/dir101/dir104/dir105/dir106/dir107")));
+}
+
+TEST_F(ResourceMetadataTest, AddEntry) {
+ // Add a file to dir3.
+ std::string local_id;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetIdByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/dir3"), &local_id));
+ ResourceEntry file_entry = CreateFileEntry("file100", local_id);
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(file_entry, &local_id));
+ base::FilePath path;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetFilePath(local_id, &path));
+ EXPECT_EQ("drive/root/dir1/dir3/file100", path.AsUTF8Unsafe());
+
+ // Add a directory.
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetIdByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1"), &local_id));
+ ResourceEntry dir_entry = CreateDirectoryEntry("dir101", local_id);
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(dir_entry, &local_id));
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetFilePath(local_id, &path));
+ EXPECT_EQ("drive/root/dir1/dir101", path.AsUTF8Unsafe());
+
+ // Add to an invalid parent.
+ ResourceEntry file_entry3 = CreateFileEntry("file103", "id:invalid");
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND,
+ resource_metadata_->AddEntry(file_entry3, &local_id));
+
+ // Add an existing file.
+ EXPECT_EQ(FILE_ERROR_EXISTS,
+ resource_metadata_->AddEntry(file_entry, &local_id));
+}
+
+TEST_F(ResourceMetadataTest, RemoveEntry) {
+ // Make sure file9 is found.
+ std::string file9_local_id;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetIdByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/dir3/file9"),
+ &file9_local_id));
+ ResourceEntry entry;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryById(
+ file9_local_id, &entry));
+ EXPECT_EQ("file9", entry.base_name());
+
+ // Remove file9.
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->RemoveEntry(file9_local_id));
+
+ // file9 should no longer exist.
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, resource_metadata_->GetResourceEntryById(
+ file9_local_id, &entry));
+
+ // Look for dir3.
+ std::string dir3_local_id;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetIdByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/dir3"), &dir3_local_id));
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryById(
+ dir3_local_id, &entry));
+ EXPECT_EQ("dir3", entry.base_name());
+
+ // Remove dir3.
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->RemoveEntry(dir3_local_id));
+
+ // dir3 should no longer exist.
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, resource_metadata_->GetResourceEntryById(
+ dir3_local_id, &entry));
+
+ // Remove unknown local_id using RemoveEntry.
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, resource_metadata_->RemoveEntry("foo"));
+
+ // Try removing root. This should fail.
+ EXPECT_EQ(FILE_ERROR_ACCESS_DENIED, resource_metadata_->RemoveEntry(
+ util::kDriveGrandRootLocalId));
+}
+
+TEST_F(ResourceMetadataTest, GetResourceEntryById_RootDirectory) {
+ // Look up the root directory by its ID.
+ ResourceEntry entry;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryById(
+ util::kDriveGrandRootLocalId, &entry));
+ EXPECT_EQ("drive", entry.base_name());
+}
+
+TEST_F(ResourceMetadataTest, GetResourceEntryById) {
+ // Get file4 by path.
+ std::string local_id;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetIdByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/dir1/file4"), &local_id));
+
+ // Confirm that an existing file is found.
+ ResourceEntry entry;
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryById(
+ local_id, &entry));
+ EXPECT_EQ("file4", entry.base_name());
+
+ // Confirm that a non existing file is not found.
+ EXPECT_EQ(FILE_ERROR_NOT_FOUND, resource_metadata_->GetResourceEntryById(
+ "non_existing", &entry));
+}
+
+TEST_F(ResourceMetadataTest, Iterate) {
+ scoped_ptr<ResourceMetadata::Iterator> it = resource_metadata_->GetIterator();
+ ASSERT_TRUE(it);
+
+ int file_count = 0, directory_count = 0;
+ for (; !it->IsAtEnd(); it->Advance()) {
+ if (!it->GetValue().file_info().is_directory())
+ ++file_count;
+ else
+ ++directory_count;
+ }
+
+ EXPECT_EQ(7, file_count);
+ EXPECT_EQ(7, directory_count);
+}
+
+TEST_F(ResourceMetadataTest, DuplicatedNames) {
+ std::string root_local_id;
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->GetIdByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root"), &root_local_id));
+
+ ResourceEntry entry;
+
+ // When multiple entries with the same title are added in a single directory,
+ // their base_names are de-duped.
+ // - drive/root/foo
+ // - drive/root/foo (1)
+ std::string dir_id_0;
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(
+ CreateDirectoryEntryWithResourceId(
+ "foo", "foo0", root_local_id), &dir_id_0));
+ std::string dir_id_1;
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(
+ CreateDirectoryEntryWithResourceId(
+ "foo", "foo1", root_local_id), &dir_id_1));
+
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryById(
+ dir_id_0, &entry));
+ EXPECT_EQ("foo", entry.base_name());
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryById(
+ dir_id_1, &entry));
+ EXPECT_EQ("foo (1)", entry.base_name());
+
+ // - drive/root/foo/bar.txt
+ // - drive/root/foo/bar (1).txt
+ // - drive/root/foo/bar (2).txt
+ // ...
+ // - drive/root/foo/bar (99).txt
+ std::vector<std::string> file_ids(100);
+ for (size_t i = 0; i < file_ids.size(); ++i) {
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(
+ CreateFileEntryWithResourceId(
+ "bar.txt", base::StringPrintf("bar%d", static_cast<int>(i)),
+ dir_id_0), &file_ids[i]));
+ }
+
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryById(
+ file_ids[0], &entry));
+ EXPECT_EQ("bar.txt", entry.base_name());
+ for (size_t i = 1; i < file_ids.size(); ++i) {
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryById(
+ file_ids[i], &entry)) << i;
+ EXPECT_EQ(base::StringPrintf("bar (%d).txt", static_cast<int>(i)),
+ entry.base_name());
+ }
+
+ // Same name but different parent. No renaming.
+ // - drive/root/foo (1)/bar.txt
+ std::string file_id_3;
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(
+ CreateFileEntryWithResourceId(
+ "bar.txt", "bar_different_parent", dir_id_1), &file_id_3));
+
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryById(
+ file_id_3, &entry));
+ EXPECT_EQ("bar.txt", entry.base_name());
+
+ // Checks that the entries can be looked up by the de-duped paths.
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/foo/bar (2).txt"), &entry));
+ EXPECT_EQ("bar2", entry.resource_id());
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root/foo (1)/bar.txt"), &entry));
+ EXPECT_EQ("bar_different_parent", entry.resource_id());
+}
+
+TEST_F(ResourceMetadataTest, EncodedNames) {
+ std::string root_local_id;
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->GetIdByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root"), &root_local_id));
+
+ ResourceEntry entry;
+
+ std::string dir_id;
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(
+ CreateDirectoryEntry("\\(^o^)/", root_local_id), &dir_id));
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryById(
+ dir_id, &entry));
+ EXPECT_EQ("\\(^o^)_", entry.base_name());
+
+ std::string file_id;
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(
+ CreateFileEntryWithResourceId("Slash /.txt", "myfile", dir_id),
+ &file_id));
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryById(
+ file_id, &entry));
+ EXPECT_EQ("Slash _.txt", entry.base_name());
+
+ ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe(
+ "drive/root/\\(^o^)_/Slash _.txt"),
+ &entry));
+ EXPECT_EQ("myfile", entry.resource_id());
+}
+
+TEST_F(ResourceMetadataTest, Reset) {
+ // The grand root has "root" which is not empty.
+ std::vector<ResourceEntry> entries;
+ ASSERT_EQ(FILE_ERROR_OK,
+ resource_metadata_->ReadDirectoryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/root"), &entries));
+ ASSERT_FALSE(entries.empty());
+
+ // Reset.
+ EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->Reset());
+
+ // change stamp should be reset.
+ int64 changestamp = 0;
+ EXPECT_EQ(FILE_ERROR_OK,
+ resource_metadata_->GetLargestChangestamp(&changestamp));
+ EXPECT_EQ(0, changestamp);
+
+ // root should continue to exist.
+ ResourceEntry entry;
+ ASSERT_EQ(FILE_ERROR_OK,
+ resource_metadata_->GetResourceEntryByPath(
+ base::FilePath::FromUTF8Unsafe("drive"), &entry));
+ EXPECT_EQ("drive", entry.base_name());
+ ASSERT_TRUE(entry.file_info().is_directory());
+ EXPECT_EQ(util::kDriveGrandRootLocalId, entry.local_id());
+
+ // There are "other", "trash" and "root" under "drive".
+ ASSERT_EQ(FILE_ERROR_OK,
+ resource_metadata_->ReadDirectoryByPath(
+ base::FilePath::FromUTF8Unsafe("drive"), &entries));
+ EXPECT_EQ(3U, entries.size());
+
+ // The "other" directory should be empty.
+ ASSERT_EQ(FILE_ERROR_OK,
+ resource_metadata_->ReadDirectoryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/other"), &entries));
+ EXPECT_TRUE(entries.empty());
+
+ // The "trash" directory should be empty.
+ ASSERT_EQ(FILE_ERROR_OK,
+ resource_metadata_->ReadDirectoryByPath(
+ base::FilePath::FromUTF8Unsafe("drive/trash"), &entries));
+ EXPECT_TRUE(entries.empty());
+}
+
+} // namespace internal
+} // namespace drive