diff options
author | lukasza <lukasza@chromium.org> | 2015-08-20 18:13:24 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-08-21 01:13:59 +0000 |
commit | 6364a0299330a1ac060f0794f4638f82c0dc5cc2 (patch) | |
tree | 38c2645500cb3be95f73187aaa211f210dd3ce1a /components/drive | |
parent | 85cc8532e314967a8ec308bc7fed379f3ad9d80a (diff) | |
download | chromium_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')
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 |