diff options
author | lukasza <lukasza@chromium.org> | 2015-07-20 13:57:20 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-07-20 20:58:23 +0000 |
commit | 8acc4eb18b6919f36413a10718a607f2a34fb8f6 (patch) | |
tree | 1e53fa500119384a12b38075ebcc97a469750dd7 /components/drive | |
parent | 1f37aaf390288b2864efe98a07427dbf91771489 (diff) | |
download | chromium_src-8acc4eb18b6919f36413a10718a607f2a34fb8f6.zip chromium_src-8acc4eb18b6919f36413a10718a607f2a34fb8f6.tar.gz chromium_src-8acc4eb18b6919f36413a10718a607f2a34fb8f6.tar.bz2 |
Move (most of) chrome/browser/drive into components/drive.
Note that drive_notification_manager_factory is left behind in
chrome/browser/drive. This is because it needs to stay dependent on the
browser. This is okay, because drive_notification_manager_factory is
not needed by drive::FileSystem and other parts of drive libraries that
we want to componentize.
Also note that some things moved to components/drive continue to
have slightly undesirable dependencies:
- Tests are still built and executed as part of the browser test suites.
- drive_uploader.cc depends on content/public/browser/power_save_blocker.h
This means that to use components/drive outside of the browser,
users of components/drive have to provide a no-op implementation
of power_save_blocker.h
The most desirable approach in the long-term would be to componentize
power_save_blocker.h. An alternative would be to continue with
the state introduced by the current changelist (or to introduce
drive-specific abstraction similar to power_save_blocker.h, but
outside of the browser).
- drive_api_util.cc depends on storage/browser/fileapi/file_stream_reader.h
To get rid of this dependency, it is probably best to move
FileStreamMd5Digester class closer to the only point of usage
(c/b/chromeos/extensions/file_manager/private_api_file_system.cc).
Landing via CQ with NOPRESUBMIT=true, because "git cl presubmit" incorrectly
identifies target directory of a dependency (we are depending on
third_party/cacheinvalidation/src/google/cacheinvalidation/types.proto
and including the generated header via google/cacheinvalidation/types.pb.h).
I believe this is a case of crbug.com/448570. All other presubmit checks
are passing AFAIK:
$ git cl presubmit
Running presubmit commit checks ...
** Presubmit ERRORS **
Missing LGTM from OWNERS of dependencies added to DEPS:
'+google/cacheinvalidation/types.pb.h',
Presubmit checks took 2.9s to calculate.
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 builds 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
NOPRESUBMIT=true
Review URL: https://codereview.chromium.org/1190203002
Cr-Commit-Position: refs/heads/master@{#339512}
Diffstat (limited to 'components/drive')
32 files changed, 10098 insertions, 0 deletions
diff --git a/components/drive/BUILD.gn b/components/drive/BUILD.gn new file mode 100644 index 0000000..64f3141 --- /dev/null +++ b/components/drive/BUILD.gn @@ -0,0 +1,57 @@ +# Copyright (c) 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. + +source_set("drive") { + sources = [ + "drive_api_util.cc", + "drive_api_util.h", + "drive_app_registry.cc", + "drive_app_registry.h", + "drive_app_registry_observer.h", + "drive_notification_manager.cc", + "drive_notification_manager.h", + "drive_notification_observer.h", + "drive_uploader.cc", + "drive_uploader.h", + "event_logger.cc", + "event_logger.h", + "service/drive_api_service.cc", + "service/drive_api_service.h", + "service/drive_service_interface.cc", + "service/drive_service_interface.h", + ] + deps = [ + "//base:base", + "//components/invalidation/public", + + # TODO(lukasza): Remove this dependency (see DEPS file for more info). + "//content/public/browser:browser", + + "//google_apis:google_apis", + "//net:net", + + # TODO(lukasza): Remove this dependency (see DEPS file for more info). + "//storage/browser:browser", + + "//third_party/re2:re2", + ] +} + +source_set("test_support") { + testonly = true + sources = [ + "service/dummy_drive_service.cc", + "service/dummy_drive_service.h", + "service/fake_drive_service.cc", + "service/fake_drive_service.h", + "service/test_util.cc", + "service/test_util.h", + ] + deps = [ + ":drive", + "//base:base", + "//google_apis:google_apis", + "//net:net", + ] +} diff --git a/components/drive/DEPS b/components/drive/DEPS new file mode 100644 index 0000000..818ed7c --- /dev/null +++ b/components/drive/DEPS @@ -0,0 +1,24 @@ +include_rules = [ + "+components/invalidation", + "+components/keyed_service", + "+google_apis", + "+google/cacheinvalidation/types.pb.h", + "+net", + "+third_party/re2", +] + +specific_include_rules = { + # The following dependency should be removed to fully make this + # directory chrome/ and content/ and storage/ independent. + # crbug.com/257943 + "drive_api_util\.cc": [ + "+storage/browser/fileapi/file_stream_reader.h" + ], + + # The following dependency should be removed to fully make this + # directory chrome/ and content/ and storage/ independent. + # crbug.com/257943 + "drive_uploader\.cc": [ + "+content/public/browser/power_save_blocker.h", + ], +} diff --git a/components/drive/OWNERS b/components/drive/OWNERS new file mode 100644 index 0000000..aec41e6 --- /dev/null +++ b/components/drive/OWNERS @@ -0,0 +1,7 @@ +fukino@chromium.org +hashimoto@chromium.org +hidehiko@chromium.org +hirono@chromium.org +kinaba@chromium.org +mtomasz@chromium.org +yoshiki@chromium.org diff --git a/components/drive/README b/components/drive/README new file mode 100644 index 0000000..beaedae --- /dev/null +++ b/components/drive/README @@ -0,0 +1,3 @@ +This directory contains utility code for accessing Google Drive, shared by +SyncFileSystem (chrome/browser/sync_file_system) and Google Drive integration +on Chrome OS (chrome/browser/chromeos/drive). diff --git a/components/drive/drive_api_util.cc b/components/drive/drive_api_util.cc new file mode 100644 index 0000000..57c3919 --- /dev/null +++ b/components/drive/drive_api_util.cc @@ -0,0 +1,249 @@ +// 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/drive_api_util.h" + +#include <string> + +#include "base/files/file.h" +#include "base/logging.h" +#include "base/md5.h" +#include "base/strings/string16.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/synchronization/cancellation_flag.h" +#include "base/values.h" +#include "google_apis/drive/drive_api_parser.h" +#include "net/base/escape.h" +#include "net/base/io_buffer.h" +#include "net/base/net_errors.h" +#include "storage/browser/fileapi/file_stream_reader.h" +#include "third_party/re2/re2/re2.h" +#include "url/gurl.h" + +namespace drive { +namespace util { +namespace { + +struct HostedDocumentKind { + const char* mime_type; + const char* extension; +}; + +const HostedDocumentKind kHostedDocumentKinds[] = { + {kGoogleDocumentMimeType, ".gdoc"}, + {kGoogleSpreadsheetMimeType, ".gsheet"}, + {kGooglePresentationMimeType, ".gslides"}, + {kGoogleDrawingMimeType, ".gdraw"}, + {kGoogleTableMimeType, ".gtable"}, + {kGoogleFormMimeType, ".gform"}, + {kGoogleMapMimeType, ".gmaps"}, +}; + +const char kUnknownHostedDocumentExtension[] = ".glink"; + +const int kMd5DigestBufferSize = 512 * 1024; // 512 kB. + +} // namespace + +std::string EscapeQueryStringValue(const std::string& str) { + std::string result; + result.reserve(str.size()); + for (size_t i = 0; i < str.size(); ++i) { + if (str[i] == '\\' || str[i] == '\'') { + result.push_back('\\'); + } + result.push_back(str[i]); + } + return result; +} + +std::string TranslateQuery(const std::string& original_query) { + // In order to handle non-ascii white spaces correctly, convert to UTF16. + base::string16 query = base::UTF8ToUTF16(original_query); + const base::string16 kDelimiter( + base::kWhitespaceUTF16 + base::ASCIIToUTF16("\"")); + + std::string result; + for (size_t index = query.find_first_not_of(base::kWhitespaceUTF16); + index != base::string16::npos; + index = query.find_first_not_of(base::kWhitespaceUTF16, index)) { + bool is_exclusion = (query[index] == '-'); + if (is_exclusion) + ++index; + if (index == query.length()) { + // Here, the token is '-' and it should be ignored. + continue; + } + + size_t begin_token = index; + base::string16 token; + if (query[begin_token] == '"') { + // Quoted query. + ++begin_token; + size_t end_token = query.find('"', begin_token); + if (end_token == base::string16::npos) { + // This is kind of syntax error, since quoted string isn't finished. + // However, the query is built by user manually, so here we treat + // whole remaining string as a token as a fallback, by appending + // a missing double-quote character. + end_token = query.length(); + query.push_back('"'); + } + + token = query.substr(begin_token, end_token - begin_token); + index = end_token + 1; // Consume last '"', too. + } else { + size_t end_token = query.find_first_of(kDelimiter, begin_token); + if (end_token == base::string16::npos) { + end_token = query.length(); + } + + token = query.substr(begin_token, end_token - begin_token); + index = end_token; + } + + if (token.empty()) { + // Just ignore an empty token. + continue; + } + + if (!result.empty()) { + // If there are two or more tokens, need to connect with "and". + result.append(" and "); + } + + // The meaning of "fullText" should include title, description and content. + base::StringAppendF( + &result, + "%sfullText contains \'%s\'", + is_exclusion ? "not " : "", + EscapeQueryStringValue(base::UTF16ToUTF8(token)).c_str()); + } + + return result; +} + +std::string CanonicalizeResourceId(const std::string& resource_id) { + // If resource ID is in the old WAPI format starting with a prefix like + // "document:", strip it and return the remaining part. + std::string stripped_resource_id; + if (RE2::FullMatch(resource_id, "^[a-z-]+(?::|%3A)([\\w-]+)$", + &stripped_resource_id)) + return stripped_resource_id; + return resource_id; +} + +std::string GetMd5Digest(const base::FilePath& file_path, + const base::CancellationFlag* cancellation_flag) { + base::File file(file_path, base::File::FLAG_OPEN | base::File::FLAG_READ); + if (!file.IsValid()) + return std::string(); + + base::MD5Context context; + base::MD5Init(&context); + + int64 offset = 0; + scoped_ptr<char[]> buffer(new char[kMd5DigestBufferSize]); + while (true) { + if (cancellation_flag && cancellation_flag->IsSet()) { // Cancelled. + return std::string(); + } + int result = file.Read(offset, buffer.get(), kMd5DigestBufferSize); + if (result < 0) { + // Found an error. + return std::string(); + } + + if (result == 0) { + // End of file. + break; + } + + offset += result; + base::MD5Update(&context, base::StringPiece(buffer.get(), result)); + } + + base::MD5Digest digest; + base::MD5Final(&digest, &context); + return base::MD5DigestToBase16(digest); +} + +FileStreamMd5Digester::FileStreamMd5Digester() + : buffer_(new net::IOBuffer(kMd5DigestBufferSize)) { +} + +FileStreamMd5Digester::~FileStreamMd5Digester() { +} + +void FileStreamMd5Digester::GetMd5Digest( + scoped_ptr<storage::FileStreamReader> stream_reader, + const ResultCallback& callback) { + reader_ = stream_reader.Pass(); + base::MD5Init(&md5_context_); + + // Start the read/hash. + ReadNextChunk(callback); +} + +void FileStreamMd5Digester::ReadNextChunk(const ResultCallback& callback) { + const int result = + reader_->Read(buffer_.get(), kMd5DigestBufferSize, + base::Bind(&FileStreamMd5Digester::OnChunkRead, + base::Unretained(this), callback)); + if (result != net::ERR_IO_PENDING) + OnChunkRead(callback, result); +} + +void FileStreamMd5Digester::OnChunkRead(const ResultCallback& callback, + int bytes_read) { + if (bytes_read < 0) { + // Error - just return empty string. + callback.Run(""); + return; + } else if (bytes_read == 0) { + // EOF. + base::MD5Digest digest; + base::MD5Final(&digest, &md5_context_); + std::string result = base::MD5DigestToBase16(digest); + callback.Run(result); + return; + } + + // Read data and digest it. + base::MD5Update(&md5_context_, + base::StringPiece(buffer_->data(), bytes_read)); + + // Kick off the next read. + ReadNextChunk(callback); +} + +std::string GetHostedDocumentExtension(const std::string& mime_type) { + for (size_t i = 0; i < arraysize(kHostedDocumentKinds); ++i) { + if (mime_type == kHostedDocumentKinds[i].mime_type) + return kHostedDocumentKinds[i].extension; + } + return kUnknownHostedDocumentExtension; +} + +bool IsKnownHostedDocumentMimeType(const std::string& mime_type) { + for (size_t i = 0; i < arraysize(kHostedDocumentKinds); ++i) { + if (mime_type == kHostedDocumentKinds[i].mime_type) + return true; + } + return false; +} + +bool HasHostedDocumentExtension(const base::FilePath& path) { + const std::string extension = base::FilePath(path.Extension()).AsUTF8Unsafe(); + for (size_t i = 0; i < arraysize(kHostedDocumentKinds); ++i) { + if (extension == kHostedDocumentKinds[i].extension) + return true; + } + return extension == kUnknownHostedDocumentExtension; +} + +} // namespace util +} // namespace drive diff --git a/components/drive/drive_api_util.h b/components/drive/drive_api_util.h new file mode 100644 index 0000000..f34f21e --- /dev/null +++ b/components/drive/drive_api_util.h @@ -0,0 +1,124 @@ +// 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_DRIVE_API_UTIL_H_ +#define COMPONENTS_DRIVE_DRIVE_API_UTIL_H_ + +#include <string> + +#include "base/md5.h" +#include "base/memory/scoped_ptr.h" +#include "google_apis/drive/drive_api_error_codes.h" +#include "google_apis/drive/drive_common_callbacks.h" + +class GURL; + +namespace base { +class CancellationFlag; +class FilePath; +class Value; +} // namespace base + +namespace google_apis { +class ChangeList; +class ChangeResource; +class FileList; +class FileResource; +class ResourceEntry; +} // namespace google_apis + +namespace net { +class IOBuffer; +} // namespace net + +namespace storage { +class FileStreamReader; +} // namespace storage + +namespace drive { +namespace util { + +// Google Apps MIME types: +const char kGoogleDocumentMimeType[] = "application/vnd.google-apps.document"; +const char kGoogleDrawingMimeType[] = "application/vnd.google-apps.drawing"; +const char kGooglePresentationMimeType[] = + "application/vnd.google-apps.presentation"; +const char kGoogleSpreadsheetMimeType[] = + "application/vnd.google-apps.spreadsheet"; +const char kGoogleTableMimeType[] = "application/vnd.google-apps.table"; +const char kGoogleFormMimeType[] = "application/vnd.google-apps.form"; +const char kGoogleMapMimeType[] = "application/vnd.google-apps.map"; +const char kDriveFolderMimeType[] = "application/vnd.google-apps.folder"; + +// Escapes ' to \' in the |str|. This is designed to use for string value of +// search parameter on Drive API v2. +// See also: https://developers.google.com/drive/search-parameters +std::string EscapeQueryStringValue(const std::string& str); + +// Parses the query, and builds a search query for Drive API v2. +// This only supports: +// Regular query (e.g. dog => fullText contains 'dog') +// Conjunctions +// (e.g. dog cat => fullText contains 'dog' and fullText contains 'cat') +// Exclusion query (e.g. -cat => not fullText contains 'cat'). +// Quoted query (e.g. "dog cat" => fullText contains 'dog cat'). +// See also: https://developers.google.com/drive/search-parameters +std::string TranslateQuery(const std::string& original_query); + +// If |resource_id| is in the old resource ID format used by WAPI, converts it +// into the new format. +std::string CanonicalizeResourceId(const std::string& resource_id); + +// Returns the (base-16 encoded) MD5 digest of the file content at |file_path|, +// or an empty string if an error is found. +std::string GetMd5Digest(const base::FilePath& file_path, + const base::CancellationFlag* cancellation_flag); + +// Computes the (base-16 encoded) MD5 digest of data extracted from a file +// stream. +class FileStreamMd5Digester { + public: + typedef base::Callback<void(const std::string&)> ResultCallback; + + FileStreamMd5Digester(); + ~FileStreamMd5Digester(); + + // Computes an MD5 digest of data read from the given |streamReader|. The + // work occurs asynchronously, and the resulting hash is returned via the + // |callback|. If an error occurs, |callback| is called with an empty string. + // Only one stream can be processed at a time by each digester. Do not call + // GetMd5Digest before the results of a previous call have been returned. + void GetMd5Digest(scoped_ptr<storage::FileStreamReader> stream_reader, + const ResultCallback& callback); + + private: + // Kicks off a read of the next chunk from the stream. + void ReadNextChunk(const ResultCallback& callback); + // Handles the incoming chunk of data from a stream read. + void OnChunkRead(const ResultCallback& callback, int bytes_read); + + // Maximum chunk size for read operations. + scoped_ptr<storage::FileStreamReader> reader_; + scoped_refptr<net::IOBuffer> buffer_; + base::MD5Context md5_context_; + + DISALLOW_COPY_AND_ASSIGN(FileStreamMd5Digester); +}; + +// Returns preferred file extension for hosted documents which have given mime +// type. +std::string GetHostedDocumentExtension(const std::string& mime_type); + +// Returns true if the given mime type is corresponding to one of known hosted +// document types. +bool IsKnownHostedDocumentMimeType(const std::string& mime_type); + +// Returns true if the given file path has an extension corresponding to one of +// hosted document types. +bool HasHostedDocumentExtension(const base::FilePath& path); + +} // namespace util +} // namespace drive + +#endif // COMPONENTS_DRIVE_DRIVE_API_UTIL_H_ diff --git a/components/drive/drive_api_util_unittest.cc b/components/drive/drive_api_util_unittest.cc new file mode 100644 index 0000000..20fe8a5 --- /dev/null +++ b/components/drive/drive_api_util_unittest.cc @@ -0,0 +1,110 @@ +// 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/drive_api_util.h" + +#include "base/files/scoped_temp_dir.h" +#include "base/md5.h" +#include "google_apis/drive/drive_api_parser.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace drive { +namespace util { + +TEST(DriveApiUtilTest, EscapeQueryStringValue) { + EXPECT_EQ("abcde", EscapeQueryStringValue("abcde")); + EXPECT_EQ("\\'", EscapeQueryStringValue("'")); + EXPECT_EQ("\\'abcde\\'", EscapeQueryStringValue("'abcde'")); + EXPECT_EQ("\\\\", EscapeQueryStringValue("\\")); + EXPECT_EQ("\\\\\\'", EscapeQueryStringValue("\\'")); +} + +TEST(DriveApiUtilTest, TranslateQuery) { + EXPECT_EQ("", TranslateQuery("")); + EXPECT_EQ("fullText contains 'dog'", TranslateQuery("dog")); + EXPECT_EQ("fullText contains 'dog' and fullText contains 'cat'", + TranslateQuery("dog cat")); + EXPECT_EQ("not fullText contains 'cat'", TranslateQuery("-cat")); + EXPECT_EQ("fullText contains 'dog cat'", TranslateQuery("\"dog cat\"")); + + // Should handles full-width white space correctly. + // Note: \xE3\x80\x80 (\u3000) is Ideographic Space (a.k.a. Japanese + // full-width whitespace). + EXPECT_EQ("fullText contains 'dog' and fullText contains 'cat'", + TranslateQuery("dog" "\xE3\x80\x80" "cat")); + + // If the quoted token is not closed (i.e. the last '"' is missing), + // we handle the remaining string is one token, as a fallback. + EXPECT_EQ("fullText contains 'dog cat'", TranslateQuery("\"dog cat")); + + // For quoted text with leading '-'. + EXPECT_EQ("not fullText contains 'dog cat'", TranslateQuery("-\"dog cat\"")); + + // Empty tokens should be simply ignored. + EXPECT_EQ("", TranslateQuery("-")); + EXPECT_EQ("", TranslateQuery("\"\"")); + EXPECT_EQ("", TranslateQuery("-\"\"")); + EXPECT_EQ("", TranslateQuery("\"\"\"\"")); + EXPECT_EQ("", TranslateQuery("\"\" \"\"")); + EXPECT_EQ("fullText contains 'dog'", TranslateQuery("\"\" dog \"\"")); +} + +TEST(DriveAPIUtilTest, CanonicalizeResourceId) { + std::string resource_id("1YsCnrMxxgp7LDdtlFDt-WdtEIth89vA9inrILtvK-Ug"); + + // New style ID is unchanged. + EXPECT_EQ(resource_id, CanonicalizeResourceId(resource_id)); + + // Drop prefixes from old style IDs. + EXPECT_EQ(resource_id, CanonicalizeResourceId("document:" + resource_id)); + EXPECT_EQ(resource_id, CanonicalizeResourceId("spreadsheet:" + resource_id)); + EXPECT_EQ(resource_id, CanonicalizeResourceId("presentation:" + resource_id)); + EXPECT_EQ(resource_id, CanonicalizeResourceId("drawing:" + resource_id)); + EXPECT_EQ(resource_id, CanonicalizeResourceId("table:" + resource_id)); + EXPECT_EQ(resource_id, CanonicalizeResourceId("externalapp:" + resource_id)); +} + +TEST(DriveAPIUtilTest, GetMd5Digest) { + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + + base::FilePath path = temp_dir.path().AppendASCII("test.txt"); + const char kTestData[] = "abcdefghijklmnopqrstuvwxyz0123456789"; + ASSERT_TRUE(google_apis::test_util::WriteStringToFile(path, kTestData)); + + EXPECT_EQ(base::MD5String(kTestData), GetMd5Digest(path, nullptr)); +} + +TEST(DriveAPIUtilTest, HasHostedDocumentExtension) { + EXPECT_TRUE( + HasHostedDocumentExtension(base::FilePath::FromUTF8Unsafe("xx.gdoc"))); + EXPECT_TRUE( + HasHostedDocumentExtension(base::FilePath::FromUTF8Unsafe("xx.gsheet"))); + EXPECT_TRUE( + HasHostedDocumentExtension(base::FilePath::FromUTF8Unsafe("xx.gslides"))); + EXPECT_TRUE( + HasHostedDocumentExtension(base::FilePath::FromUTF8Unsafe("xx.gdraw"))); + EXPECT_TRUE( + HasHostedDocumentExtension(base::FilePath::FromUTF8Unsafe("xx.gtable"))); + EXPECT_TRUE( + HasHostedDocumentExtension(base::FilePath::FromUTF8Unsafe("xx.gform"))); + EXPECT_TRUE( + HasHostedDocumentExtension(base::FilePath::FromUTF8Unsafe("xx.gmaps"))); + EXPECT_TRUE( + HasHostedDocumentExtension(base::FilePath::FromUTF8Unsafe("xx.glink"))); + + EXPECT_FALSE( + HasHostedDocumentExtension(base::FilePath::FromUTF8Unsafe("xx.gdocs"))); + EXPECT_FALSE( + HasHostedDocumentExtension(base::FilePath::FromUTF8Unsafe("xx.docx"))); + EXPECT_FALSE( + HasHostedDocumentExtension(base::FilePath::FromUTF8Unsafe("xx.jpg"))); + EXPECT_FALSE( + HasHostedDocumentExtension(base::FilePath::FromUTF8Unsafe("xx.gmap"))); +} + +} // namespace util +} // namespace drive diff --git a/components/drive/drive_app_registry.cc b/components/drive/drive_app_registry.cc new file mode 100644 index 0000000..90598e7 --- /dev/null +++ b/components/drive/drive_app_registry.cc @@ -0,0 +1,246 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/drive/drive_app_registry.h" + +#include <algorithm> +#include <set> +#include <utility> + +#include "base/callback.h" +#include "base/files/file_path.h" +#include "components/drive/drive_app_registry_observer.h" +#include "components/drive/service/drive_service_interface.h" +#include "google_apis/drive/drive_api_parser.h" +#include "google_apis/google_api_keys.h" + +namespace { + +// Add {selector -> app_id} mapping to |map|. +void AddAppSelectorList(const ScopedVector<std::string>& selectors, + const std::string& app_id, + std::multimap<std::string, std::string>* map) { + for (size_t i = 0; i < selectors.size(); ++i) + map->insert(std::make_pair(*selectors[i], app_id)); +} + +// Append list of app ids in |map| looked up by |selector| to |matched_apps|. +void FindAppsForSelector(const std::string& selector, + const std::multimap<std::string, std::string>& map, + std::vector<std::string>* matched_apps) { + typedef std::multimap<std::string, std::string>::const_iterator iterator; + std::pair<iterator, iterator> range = map.equal_range(selector); + for (iterator it = range.first; it != range.second; ++it) + matched_apps->push_back(it->second); +} + +void RemoveAppFromSelector(const std::string& app_id, + std::multimap<std::string, std::string>* map) { + typedef std::multimap<std::string, std::string>::iterator iterator; + for (iterator it = map->begin(); it != map->end(); ) { + iterator now = it++; + if (now->second == app_id) + map->erase(now); + } +} + +} // namespace + +namespace drive { + +DriveAppInfo::DriveAppInfo() { +} + +DriveAppInfo::DriveAppInfo( + const std::string& app_id, + const std::string& product_id, + const IconList& app_icons, + const IconList& document_icons, + const std::string& app_name, + const GURL& create_url, + bool is_removable) + : app_id(app_id), + product_id(product_id), + app_icons(app_icons), + document_icons(document_icons), + app_name(app_name), + create_url(create_url), + is_removable(is_removable) { +} + +DriveAppInfo::~DriveAppInfo() { +} + +DriveAppRegistry::DriveAppRegistry(DriveServiceInterface* drive_service) + : drive_service_(drive_service), + is_updating_(false), + weak_ptr_factory_(this) { +} + +DriveAppRegistry::~DriveAppRegistry() { +} + +void DriveAppRegistry::GetAppsForFile( + const base::FilePath::StringType& file_extension, + const std::string& mime_type, + std::vector<DriveAppInfo>* apps) const { + DCHECK(thread_checker_.CalledOnValidThread()); + + std::vector<std::string> matched_apps; + if (!file_extension.empty()) { + const std::string without_dot = + base::FilePath(file_extension.substr(1)).AsUTF8Unsafe(); + FindAppsForSelector(without_dot, extension_map_, &matched_apps); + } + if (!mime_type.empty()) + FindAppsForSelector(mime_type, mimetype_map_, &matched_apps); + + // Insert found Drive apps into |apps|, but skip duplicate results. + std::set<std::string> inserted_app_ids; + for (size_t i = 0; i < matched_apps.size(); ++i) { + if (inserted_app_ids.count(matched_apps[i]) == 0) { + inserted_app_ids.insert(matched_apps[i]); + std::map<std::string, DriveAppInfo>::const_iterator it = + all_apps_.find(matched_apps[i]); + DCHECK(it != all_apps_.end()); + apps->push_back(it->second); + } + } +} + +void DriveAppRegistry::GetAppList(std::vector<DriveAppInfo>* apps) const { + DCHECK(thread_checker_.CalledOnValidThread()); + + apps->clear(); + for (std::map<std::string, DriveAppInfo>::const_iterator + it = all_apps_.begin(); it != all_apps_.end(); ++it) { + apps->push_back(it->second); + } +} + +void DriveAppRegistry::Update() { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (is_updating_) // There is already an update in progress. + return; + is_updating_ = true; + + drive_service_->GetAppList( + base::Bind(&DriveAppRegistry::UpdateAfterGetAppList, + weak_ptr_factory_.GetWeakPtr())); +} + +void DriveAppRegistry::UpdateAfterGetAppList( + google_apis::DriveApiErrorCode gdata_error, + scoped_ptr<google_apis::AppList> app_list) { + DCHECK(thread_checker_.CalledOnValidThread()); + + DCHECK(is_updating_); + is_updating_ = false; + + // Failed to fetch the data from the server. We can do nothing here. + if (gdata_error != google_apis::HTTP_SUCCESS) + return; + + DCHECK(app_list); + UpdateFromAppList(*app_list); +} + +void DriveAppRegistry::UpdateFromAppList(const google_apis::AppList& app_list) { + all_apps_.clear(); + extension_map_.clear(); + mimetype_map_.clear(); + + for (size_t i = 0; i < app_list.items().size(); ++i) { + const google_apis::AppResource& app = *app_list.items()[i]; + const std::string id = app.application_id(); + + DriveAppInfo::IconList app_icons; + DriveAppInfo::IconList document_icons; + for (size_t j = 0; j < app.icons().size(); ++j) { + const google_apis::DriveAppIcon& icon = *app.icons()[j]; + if (icon.icon_url().is_empty()) + continue; + if (icon.category() == google_apis::DriveAppIcon::APPLICATION) + app_icons.push_back(std::make_pair(icon.icon_side_length(), + icon.icon_url())); + if (icon.category() == google_apis::DriveAppIcon::DOCUMENT) + document_icons.push_back(std::make_pair(icon.icon_side_length(), + icon.icon_url())); + } + + all_apps_[id] = DriveAppInfo(app.application_id(), + app.product_id(), + app_icons, + document_icons, + app.name(), + app.create_url(), + app.is_removable()); + + // TODO(kinaba): consider taking primary/secondary distinction into account. + AddAppSelectorList(app.primary_mimetypes(), id, &mimetype_map_); + AddAppSelectorList(app.secondary_mimetypes(), id, &mimetype_map_); + AddAppSelectorList(app.primary_file_extensions(), id, &extension_map_); + AddAppSelectorList(app.secondary_file_extensions(), id, &extension_map_); + } + + FOR_EACH_OBSERVER(DriveAppRegistryObserver, + observers_, + OnDriveAppRegistryUpdated()); +} + +void DriveAppRegistry::AddObserver(DriveAppRegistryObserver* observer) { + observers_.AddObserver(observer); +} + +void DriveAppRegistry::RemoveObserver(DriveAppRegistryObserver* observer) { + observers_.RemoveObserver(observer); +} + +void DriveAppRegistry::UninstallApp(const std::string& app_id, + const UninstallCallback& callback) { + DCHECK(!callback.is_null()); + + drive_service_->UninstallApp(app_id, + base::Bind(&DriveAppRegistry::OnAppUninstalled, + weak_ptr_factory_.GetWeakPtr(), + app_id, + callback)); +} + +void DriveAppRegistry::OnAppUninstalled(const std::string& app_id, + const UninstallCallback& callback, + google_apis::DriveApiErrorCode error) { + if (error == google_apis::HTTP_NO_CONTENT) { + all_apps_.erase(app_id); + RemoveAppFromSelector(app_id, &mimetype_map_); + RemoveAppFromSelector(app_id, &extension_map_); + } + callback.Run(error); +} + +// static +bool DriveAppRegistry::IsAppUninstallSupported() { + return google_apis::IsGoogleChromeAPIKeyUsed(); +} + +namespace util { + +GURL FindPreferredIcon(const DriveAppInfo::IconList& icons, + int preferred_size) { + if (icons.empty()) + return GURL(); + + DriveAppInfo::IconList sorted_icons = icons; + std::sort(sorted_icons.rbegin(), sorted_icons.rend()); + + // Go forward while the size is larger or equal to preferred_size. + size_t i = 1; + while (i < sorted_icons.size() && sorted_icons[i].first >= preferred_size) + ++i; + return sorted_icons[i - 1].second; +} + +} // namespace util +} // namespace drive diff --git a/components/drive/drive_app_registry.h b/components/drive/drive_app_registry.h new file mode 100644 index 0000000..817543f --- /dev/null +++ b/components/drive/drive_app_registry.h @@ -0,0 +1,160 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_DRIVE_DRIVE_APP_REGISTRY_H_ +#define COMPONENTS_DRIVE_DRIVE_APP_REGISTRY_H_ + +#include <map> +#include <string> +#include <vector> + +#include "base/callback_forward.h" +#include "base/files/file_path.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "base/threading/thread_checker.h" +#include "google_apis/drive/drive_api_error_codes.h" +#include "url/gurl.h" + +namespace google_apis { +class AppList; +} // namespace google_apis + +namespace drive { + +class DriveAppRegistryObserver; +class DriveServiceInterface; + +// Data structure that defines Drive app. See +// https://chrome.google.com/webstore/category/collection/drive_apps for +// Drive apps available on the webstore. +struct DriveAppInfo { + typedef std::vector<std::pair<int, GURL> > IconList; + + DriveAppInfo(); + DriveAppInfo(const std::string& app_id, + const std::string& product_id, + const IconList& app_icons, + const IconList& document_icons, + const std::string& app_name, + const GURL& create_url, + bool is_removable); + ~DriveAppInfo(); + + // Drive app id. + std::string app_id; + // Drive app's product id. This is different from app id that is used inside + // Drive. Product id is an id for the app in webstore; hence, it can be used + // for identifying the same app install as Chrome extension and as Drive app + // at the same time. + std::string product_id; + // Drive application icon URLs for this app, paired with their size (length of + // a side in pixels). + IconList app_icons; + // Drive document icon URLs for this app, paired with their size (length of + // a side in pixels). + IconList document_icons; + // App name. + std::string app_name; + // URL for opening a new file in the app. Empty if the app does not support + // new file creation. + GURL create_url; + // Returns if UninstallApp() is allowed for the app. Built-in apps have this + // field set false. + bool is_removable; +}; + +// Callback type for UninstallApp(). +typedef base::Callback<void(google_apis::DriveApiErrorCode)> UninstallCallback; + +// Keeps the track of installed drive applications in-memory. +class DriveAppRegistry { + public: + explicit DriveAppRegistry(DriveServiceInterface* scheduler); + ~DriveAppRegistry(); + + // Returns a list of Drive app information for the |file_extension| with + // |mime_type|. + void GetAppsForFile(const base::FilePath::StringType& file_extension, + const std::string& mime_type, + std::vector<DriveAppInfo>* apps) const; + + // Returns the list of all Drive apps installed. + void GetAppList(std::vector<DriveAppInfo>* apps) const; + + // Uninstalls the app specified by |app_id|. This method sends requests to the + // remote server, and returns the result to |callback| asynchronously. When + // succeeded, the callback receives HTTP_NO_CONTENT, and error code otherwise. + // |callback| must not be null. + void UninstallApp(const std::string& app_id, + const UninstallCallback& callback); + + // Checks whether UinstallApp is supported. The feature is available only for + // clients with whitelisted API keys (like Official Google Chrome build). + static bool IsAppUninstallSupported(); + + // Updates this registry by fetching the data from the server. + void Update(); + + // Updates this registry from the |app_list|. + void UpdateFromAppList(const google_apis::AppList& app_list); + + void AddObserver(DriveAppRegistryObserver* observer); + void RemoveObserver(DriveAppRegistryObserver* observer); + + private: + // Part of Update(). Runs upon the completion of fetching the Drive apps + // data from the server. + void UpdateAfterGetAppList(google_apis::DriveApiErrorCode gdata_error, + scoped_ptr<google_apis::AppList> app_list); + + // Part of UninstallApp(). Receives the response from the server. + void OnAppUninstalled(const std::string& app_id, + const UninstallCallback& callback, + google_apis::DriveApiErrorCode error); + + // The class is expected to run on UI thread. + base::ThreadChecker thread_checker_; + + // Map of application id to each app's info. + std::map<std::string, DriveAppInfo> all_apps_; + + // Defines mapping between file content type selectors (extensions, MIME + // types) and corresponding app. + typedef std::multimap<std::string, std::string> DriveAppFileSelectorMap; + DriveAppFileSelectorMap extension_map_; + DriveAppFileSelectorMap mimetype_map_; + + DriveServiceInterface* drive_service_; + + bool is_updating_; + + base::ObserverList<DriveAppRegistryObserver> observers_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate the weak pointers before any other members are destroyed. + base::WeakPtrFactory<DriveAppRegistry> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(DriveAppRegistry); +}; + +namespace util { + +// The preferred icon size, which should usually be used for FindPreferredIcon; +const int kPreferredIconSize = 16; + +// Finds an icon in the list of icons. If unable to find an icon of the exact +// size requested, returns one with the next larger size. If all icons are +// smaller than the preferred size, we'll return the largest one available. +// Icons do not have to be sorted by the icon size. If there are no icons in +// the list, returns an empty URL. +GURL FindPreferredIcon(const DriveAppInfo::IconList& icons, + int preferred_size); + +} // namespace util + +} // namespace drive + +#endif // COMPONENTS_DRIVE_DRIVE_APP_REGISTRY_H_ diff --git a/components/drive/drive_app_registry_observer.h b/components/drive/drive_app_registry_observer.h new file mode 100644 index 0000000..d22781f --- /dev/null +++ b/components/drive/drive_app_registry_observer.h @@ -0,0 +1,21 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_DRIVE_DRIVE_APP_REGISTRY_OBSERVER_H_ +#define COMPONENTS_DRIVE_DRIVE_APP_REGISTRY_OBSERVER_H_ + +namespace drive { + +class DriveAppRegistryObserver { + public: + // Invoked when DriveAppRegistry has updated its list of Drive apps. + virtual void OnDriveAppRegistryUpdated() = 0; + + protected: + virtual ~DriveAppRegistryObserver() {} +}; + +} // namespace drive + +#endif // COMPONENTS_DRIVE_DRIVE_APP_REGISTRY_OBSERVER_H_ diff --git a/components/drive/drive_app_registry_unittest.cc b/components/drive/drive_app_registry_unittest.cc new file mode 100644 index 0000000..d5dfd6c --- /dev/null +++ b/components/drive/drive_app_registry_unittest.cc @@ -0,0 +1,224 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/drive/drive_app_registry.h" + +#include "base/files/file_path.h" +#include "base/macros.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/values.h" +#include "components/drive/drive_app_registry_observer.h" +#include "components/drive/service/fake_drive_service.h" +#include "google_apis/drive/drive_api_parser.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { + +class TestDriveAppRegistryObserver : public DriveAppRegistryObserver { + public: + explicit TestDriveAppRegistryObserver(DriveAppRegistry* registry) + : registry_(registry), + update_count_(0) { + registry_->AddObserver(this); + } + ~TestDriveAppRegistryObserver() override { registry_->RemoveObserver(this); } + + int update_count() const { return update_count_; } + + private: + // DriveAppRegistryObserver overrides: + void OnDriveAppRegistryUpdated() override { ++update_count_; } + + DriveAppRegistry* registry_; + int update_count_; + DISALLOW_COPY_AND_ASSIGN(TestDriveAppRegistryObserver); +}; + +class DriveAppRegistryTest : public testing::Test { + protected: + void SetUp() override { + fake_drive_service_.reset(new FakeDriveService); + fake_drive_service_->LoadAppListForDriveApi("drive/applist.json"); + + apps_registry_.reset(new DriveAppRegistry(fake_drive_service_.get())); + } + + bool VerifyApp(const std::vector<DriveAppInfo>& list, + const std::string& app_id, + const std::string& app_name) { + bool found = false; + for (size_t i = 0; i < list.size(); ++i) { + const DriveAppInfo& app = list[i]; + if (app_id == app.app_id) { + EXPECT_EQ(app_name, app.app_name); + found = true; + break; + } + } + EXPECT_TRUE(found) << "Unable to find app with app_id " << app_id; + return found; + } + + base::MessageLoop message_loop_; + scoped_ptr<FakeDriveService> fake_drive_service_; + scoped_ptr<DriveAppRegistry> apps_registry_; +}; + +TEST_F(DriveAppRegistryTest, BasicParse) { + TestDriveAppRegistryObserver observer(apps_registry_.get()); + + apps_registry_->Update(); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(1, observer.update_count()); + + std::vector<DriveAppInfo> apps; + apps_registry_->GetAppList(&apps); + + ASSERT_EQ(2u, apps.size()); + EXPECT_EQ("123456788192", apps[0].app_id); + EXPECT_EQ("Drive app 1", apps[0].app_name); + EXPECT_EQ("https://www.example.com/createForApp1", + apps[0].create_url.spec()); + EXPECT_EQ("abcdefghabcdefghabcdefghabcdefgh", apps[0].product_id); + EXPECT_TRUE(apps[0].is_removable); +} + +TEST_F(DriveAppRegistryTest, LoadAndFindDriveApps) { + TestDriveAppRegistryObserver observer(apps_registry_.get()); + + apps_registry_->Update(); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(1, observer.update_count()); + + // Find by primary extension 'exe'. + std::vector<DriveAppInfo> ext_results; + base::FilePath ext_file(FILE_PATH_LITERAL("drive/file.exe")); + apps_registry_->GetAppsForFile(ext_file.Extension(), "", &ext_results); + ASSERT_EQ(1U, ext_results.size()); + VerifyApp(ext_results, "123456788192", "Drive app 1"); + + // Find by primary MIME type. + std::vector<DriveAppInfo> primary_app; + apps_registry_->GetAppsForFile(base::FilePath::StringType(), + "application/vnd.google-apps.drive-sdk.123456788192", &primary_app); + ASSERT_EQ(1U, primary_app.size()); + VerifyApp(primary_app, "123456788192", "Drive app 1"); + + // Find by secondary MIME type. + std::vector<DriveAppInfo> secondary_app; + apps_registry_->GetAppsForFile( + base::FilePath::StringType(), "text/html", &secondary_app); + ASSERT_EQ(1U, secondary_app.size()); + VerifyApp(secondary_app, "123456788192", "Drive app 1"); +} + +TEST_F(DriveAppRegistryTest, UpdateFromAppList) { + scoped_ptr<base::Value> app_info_value = + google_apis::test_util::LoadJSONFile("drive/applist.json"); + scoped_ptr<google_apis::AppList> app_list( + google_apis::AppList::CreateFrom(*app_info_value)); + + TestDriveAppRegistryObserver observer(apps_registry_.get()); + apps_registry_->UpdateFromAppList(*app_list); + EXPECT_EQ(1, observer.update_count()); + + // Confirm that something was loaded from applist.json. + std::vector<DriveAppInfo> ext_results; + base::FilePath ext_file(FILE_PATH_LITERAL("drive/file.exe")); + apps_registry_->GetAppsForFile(ext_file.Extension(), "", &ext_results); + ASSERT_EQ(1U, ext_results.size()); +} + +TEST_F(DriveAppRegistryTest, MultipleUpdate) { + TestDriveAppRegistryObserver observer(apps_registry_.get()); + + // Call Update(). + apps_registry_->Update(); + EXPECT_EQ(0, observer.update_count()); + + // Call Update() again. + // This call should be ignored because there is already an ongoing update. + apps_registry_->Update(); + EXPECT_EQ(0, observer.update_count()); + + // The app list should be loaded only once. + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(1, fake_drive_service_->app_list_load_count()); + EXPECT_EQ(1, observer.update_count()); +} + +TEST(DriveAppRegistryUtilTest, FindPreferredIcon_Empty) { + DriveAppInfo::IconList icons; + EXPECT_EQ("", + util::FindPreferredIcon(icons, util::kPreferredIconSize).spec()); +} + +TEST(DriveAppRegistryUtilTest, FindPreferredIcon_) { + const char kSmallerIconUrl[] = "http://example.com/smaller.png"; + const char kMediumIconUrl[] = "http://example.com/medium.png"; + const char kBiggerIconUrl[] = "http://example.com/bigger.png"; + const int kMediumSize = 16; + + DriveAppInfo::IconList icons; + // The icons are not sorted by the size. + icons.push_back(std::make_pair(kMediumSize, + GURL(kMediumIconUrl))); + icons.push_back(std::make_pair(kMediumSize + 2, + GURL(kBiggerIconUrl))); + icons.push_back(std::make_pair(kMediumSize - 2, + GURL(kSmallerIconUrl))); + + // Exact match. + EXPECT_EQ(kMediumIconUrl, + util::FindPreferredIcon(icons, kMediumSize).spec()); + // The requested size is in-between of smaller.png and + // medium.png. medium.png should be returned. + EXPECT_EQ(kMediumIconUrl, + util::FindPreferredIcon(icons, kMediumSize - 1).spec()); + // The requested size is smaller than the smallest icon. The smallest icon + // should be returned. + EXPECT_EQ(kSmallerIconUrl, + util::FindPreferredIcon(icons, kMediumSize - 3).spec()); + // The requested size is larger than the largest icon. The largest icon + // should be returned. + EXPECT_EQ(kBiggerIconUrl, + util::FindPreferredIcon(icons, kMediumSize + 3).spec()); +} + +TEST_F(DriveAppRegistryTest, UninstallDriveApp) { + apps_registry_->Update(); + base::RunLoop().RunUntilIdle(); + + std::vector<DriveAppInfo> apps; + apps_registry_->GetAppList(&apps); + size_t original_count = apps.size(); + + // Uninstall an existing app. + google_apis::DriveApiErrorCode error = google_apis::DRIVE_OTHER_ERROR; + apps_registry_->UninstallApp( + "123456788192", + google_apis::test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(error, google_apis::HTTP_NO_CONTENT); + + // Check that the number of apps is decreased by one. + apps_registry_->GetAppList(&apps); + EXPECT_EQ(original_count - 1, apps.size()); + + // Try to uninstall a non-existing app. + error = google_apis::DRIVE_OTHER_ERROR; + apps_registry_->UninstallApp( + "non-existing-app-id", + google_apis::test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(error, google_apis::HTTP_NOT_FOUND); + + // Check that the number is not changed this time. + apps_registry_->GetAppList(&apps); + EXPECT_EQ(original_count - 1, apps.size()); +} + +} // namespace drive diff --git a/components/drive/drive_notification_manager.cc b/components/drive/drive_notification_manager.cc new file mode 100644 index 0000000..14e0768 --- /dev/null +++ b/components/drive/drive_notification_manager.cc @@ -0,0 +1,160 @@ +// 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/drive_notification_manager.h" + +#include "base/metrics/histogram.h" +#include "components/drive/drive_notification_observer.h" +#include "components/invalidation/public/invalidation_service.h" +#include "components/invalidation/public/object_id_invalidation_map.h" +#include "google/cacheinvalidation/types.pb.h" + +namespace drive { + +namespace { + +// The polling interval time is used when XMPP is disabled. +const int kFastPollingIntervalInSecs = 60; + +// The polling interval time is used when XMPP is enabled. Theoretically +// polling should be unnecessary if XMPP is enabled, but just in case. +const int kSlowPollingIntervalInSecs = 300; + +// The sync invalidation object ID for Google Drive. +const char kDriveInvalidationObjectId[] = "CHANGELOG"; + +} // namespace + +DriveNotificationManager::DriveNotificationManager( + invalidation::InvalidationService* invalidation_service) + : invalidation_service_(invalidation_service), + push_notification_registered_(false), + push_notification_enabled_(false), + observers_notified_(false), + polling_timer_(true /* retain_user_task */, false /* is_repeating */), + weak_ptr_factory_(this) { + DCHECK(invalidation_service_); + RegisterDriveNotifications(); + RestartPollingTimer(); +} + +DriveNotificationManager::~DriveNotificationManager() {} + +void DriveNotificationManager::Shutdown() { + // Unregister for Drive notifications. + if (!invalidation_service_ || !push_notification_registered_) + return; + + // We unregister the handler without updating unregistering our IDs on + // purpose. See the class comment on the InvalidationService interface for + // more information. + invalidation_service_->UnregisterInvalidationHandler(this); + invalidation_service_ = NULL; +} + +void DriveNotificationManager::OnInvalidatorStateChange( + syncer::InvalidatorState state) { + push_notification_enabled_ = (state == syncer::INVALIDATIONS_ENABLED); + if (push_notification_enabled_) { + DVLOG(1) << "XMPP Notifications enabled"; + } else { + DVLOG(1) << "XMPP Notifications disabled (state=" << state << ")"; + } + FOR_EACH_OBSERVER(DriveNotificationObserver, observers_, + OnPushNotificationEnabled(push_notification_enabled_)); +} + +void DriveNotificationManager::OnIncomingInvalidation( + const syncer::ObjectIdInvalidationMap& invalidation_map) { + DVLOG(2) << "XMPP Drive Notification Received"; + syncer::ObjectIdSet ids = invalidation_map.GetObjectIds(); + DCHECK_EQ(1U, ids.size()); + const invalidation::ObjectId object_id( + ipc::invalidation::ObjectSource::COSMO_CHANGELOG, + kDriveInvalidationObjectId); + DCHECK_EQ(1U, ids.count(object_id)); + + // This effectively disables 'local acks'. It tells the invalidations system + // to not bother saving invalidations across restarts for us. + // See crbug.com/320878. + invalidation_map.AcknowledgeAll(); + NotifyObserversToUpdate(NOTIFICATION_XMPP); +} + +std::string DriveNotificationManager::GetOwnerName() const { return "Drive"; } + +void DriveNotificationManager::AddObserver( + DriveNotificationObserver* observer) { + observers_.AddObserver(observer); +} + +void DriveNotificationManager::RemoveObserver( + DriveNotificationObserver* observer) { + observers_.RemoveObserver(observer); +} + +void DriveNotificationManager::RestartPollingTimer() { + const int interval_secs = (push_notification_enabled_ ? + kSlowPollingIntervalInSecs : + kFastPollingIntervalInSecs); + polling_timer_.Stop(); + polling_timer_.Start( + FROM_HERE, + base::TimeDelta::FromSeconds(interval_secs), + base::Bind(&DriveNotificationManager::NotifyObserversToUpdate, + weak_ptr_factory_.GetWeakPtr(), + NOTIFICATION_POLLING)); +} + +void DriveNotificationManager::NotifyObserversToUpdate( + NotificationSource source) { + DVLOG(1) << "Notifying observers: " << NotificationSourceToString(source); + FOR_EACH_OBSERVER(DriveNotificationObserver, observers_, + OnNotificationReceived()); + if (!observers_notified_) { + UMA_HISTOGRAM_BOOLEAN("Drive.PushNotificationInitiallyEnabled", + push_notification_enabled_); + } + observers_notified_ = true; + + // Note that polling_timer_ is not a repeating timer. Restarting manually + // here is better as XMPP may be received right before the polling timer is + // fired (i.e. we don't notify observers twice in a row). + RestartPollingTimer(); +} + +void DriveNotificationManager::RegisterDriveNotifications() { + DCHECK(!push_notification_enabled_); + + if (!invalidation_service_) + return; + + invalidation_service_->RegisterInvalidationHandler(this); + syncer::ObjectIdSet ids; + ids.insert(invalidation::ObjectId( + ipc::invalidation::ObjectSource::COSMO_CHANGELOG, + kDriveInvalidationObjectId)); + CHECK(invalidation_service_->UpdateRegisteredInvalidationIds(this, ids)); + push_notification_registered_ = true; + OnInvalidatorStateChange(invalidation_service_->GetInvalidatorState()); + + UMA_HISTOGRAM_BOOLEAN("Drive.PushNotificationRegistered", + push_notification_registered_); +} + +// static +std::string DriveNotificationManager::NotificationSourceToString( + NotificationSource source) { + switch (source) { + case NOTIFICATION_XMPP: + return "NOTIFICATION_XMPP"; + case NOTIFICATION_POLLING: + return "NOTIFICATION_POLLING"; + } + + NOTREACHED(); + return ""; +} + +} // namespace drive diff --git a/components/drive/drive_notification_manager.h b/components/drive/drive_notification_manager.h new file mode 100644 index 0000000..5cbb8a7 --- /dev/null +++ b/components/drive/drive_notification_manager.h @@ -0,0 +1,98 @@ +// 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_DRIVE_NOTIFICATION_MANAGER_H_ +#define COMPONENTS_DRIVE_DRIVE_NOTIFICATION_MANAGER_H_ + +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "base/timer/timer.h" +#include "components/drive/drive_notification_observer.h" +#include "components/invalidation/public/invalidation_handler.h" +#include "components/keyed_service/core/keyed_service.h" + +class ProfileSyncService; + +namespace invalidation { +class InvalidationService; +} + +namespace drive { + +// Informs observers when they should check Google Drive for updates. +// Conditions under which updates should be searched: +// 1. XMPP invalidation is received from Google Drive. +// 2. Polling timer counts down. +class DriveNotificationManager : public KeyedService, + public syncer::InvalidationHandler { + public: + explicit DriveNotificationManager( + invalidation::InvalidationService* invalidation_service); + ~DriveNotificationManager() override; + + // KeyedService override. + void Shutdown() override; + + // syncer::InvalidationHandler implementation. + void OnInvalidatorStateChange(syncer::InvalidatorState state) override; + void OnIncomingInvalidation( + const syncer::ObjectIdInvalidationMap& invalidation_map) override; + std::string GetOwnerName() const override; + + void AddObserver(DriveNotificationObserver* observer); + void RemoveObserver(DriveNotificationObserver* observer); + + // True when XMPP notification is currently enabled. + bool push_notification_enabled() const { + return push_notification_enabled_; + } + + // True when XMPP notification has been registered. + bool push_notification_registered() const { + return push_notification_registered_; + } + + private: + enum NotificationSource { + NOTIFICATION_XMPP, + NOTIFICATION_POLLING, + }; + + // Restarts the polling timer. Used for polling-based notification. + void RestartPollingTimer(); + + // Notifies the observers that it's time to check for updates. + // |source| indicates where the notification comes from. + void NotifyObserversToUpdate(NotificationSource source); + + // Registers for Google Drive invalidation notifications through XMPP. + void RegisterDriveNotifications(); + + // Returns a string representation of NotificationSource. + static std::string NotificationSourceToString(NotificationSource source); + + invalidation::InvalidationService* invalidation_service_; + base::ObserverList<DriveNotificationObserver> observers_; + + // True when Drive File Sync Service is registered for Drive notifications. + bool push_notification_registered_; + // True if the XMPP-based push notification is currently enabled. + bool push_notification_enabled_; + // True once observers are notified for the first time. + bool observers_notified_; + + // The timer is used for polling based notification. XMPP should usually be + // used but notification is done per polling when XMPP is not working. + base::Timer polling_timer_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate its weak pointers before any other members are destroyed. + base::WeakPtrFactory<DriveNotificationManager> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(DriveNotificationManager); +}; + +} // namespace drive + +#endif // COMPONENTS_DRIVE_DRIVE_NOTIFICATION_MANAGER_H_ diff --git a/components/drive/drive_notification_observer.h b/components/drive/drive_notification_observer.h new file mode 100644 index 0000000..b04a06b --- /dev/null +++ b/components/drive/drive_notification_observer.h @@ -0,0 +1,26 @@ +// Copyright (c) 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_DRIVE_NOTIFICATION_OBSERVER_H_ +#define COMPONENTS_DRIVE_DRIVE_NOTIFICATION_OBSERVER_H_ + +namespace drive { + +// Interface for classes which need to know when to check Google Drive for +// updates. +class DriveNotificationObserver { + public: + // Called when a notification from Google Drive is received. + virtual void OnNotificationReceived() = 0; + + // Called when XMPP-based push notification is enabled or disabled. + virtual void OnPushNotificationEnabled(bool enabled) {} + + protected: + virtual ~DriveNotificationObserver() {} +}; + +} // namespace drive + +#endif // COMPONENTS_DRIVE_DRIVE_NOTIFICATION_OBSERVER_H_ diff --git a/components/drive/drive_uploader.cc b/components/drive/drive_uploader.cc new file mode 100644 index 0000000..210d1b1 --- /dev/null +++ b/components/drive/drive_uploader.cc @@ -0,0 +1,533 @@ +// 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/drive_uploader.h" + +#include <algorithm> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/files/file_util.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/string_number_conversions.h" +#include "base/task_runner_util.h" +#include "components/drive/service/drive_service_interface.h" +#include "content/public/browser/power_save_blocker.h" +#include "google_apis/drive/drive_api_parser.h" + +using google_apis::CancelCallback; +using google_apis::FileResource; +using google_apis::DRIVE_CANCELLED; +using google_apis::DriveApiErrorCode; +using google_apis::DRIVE_NO_SPACE; +using google_apis::HTTP_CONFLICT; +using google_apis::HTTP_CREATED; +using google_apis::HTTP_FORBIDDEN; +using google_apis::HTTP_NOT_FOUND; +using google_apis::HTTP_PRECONDITION; +using google_apis::HTTP_RESUME_INCOMPLETE; +using google_apis::HTTP_SUCCESS; +using google_apis::ProgressCallback; +using google_apis::UploadRangeResponse; + +namespace drive { + +namespace { +// Upload data is split to multiple HTTP request each conveying kUploadChunkSize +// bytes (except the request for uploading the last chunk of data). +// The value must be a multiple of 512KB according to the spec of GData WAPI and +// Drive API v2. It is set to a smaller value than 2^31 for working around +// server side error (crbug.com/264089). +const int64 kUploadChunkSize = (1LL << 30); // 1GB +// Maximum file size to be uploaded by multipart requests. The file that is +// larger than the size is processed by resumable upload. +const int64 kMaxMultipartUploadSize = (1LL << 20); // 1MB + +// Drive upload protocol. This is used to back a histogram. Sync this with UMA +// enum "DriveUploadProtocol" and treat this as append-only. +enum DriveUploadProtocol { + UPLOAD_METHOD_RESUMABLE, + UPLOAD_METHOD_MULTIPART, + UPLOAD_METHOD_BATCH, + UPLOAD_METHOD_MAX_VALUE +}; + +void RecordDriveUploadProtocol(DriveUploadProtocol protocol) { + UMA_HISTOGRAM_ENUMERATION( + "Drive.UploadProtocol", protocol, UPLOAD_METHOD_MAX_VALUE); +} +} // namespace + +// Refcounted helper class to manage batch request. DriveUploader uses the class +// for keeping the BatchRequestConfigurator instance while it prepares upload +// file information asynchronously. DriveUploader discard the reference after +// getting file information and the instance will be destroyed after all +// preparations complete. At that time, the helper instance commits owned batch +// request at the destrutor. +class DriveUploader::RefCountedBatchRequest + : public base::RefCounted<RefCountedBatchRequest> { + public: + RefCountedBatchRequest( + scoped_ptr<BatchRequestConfiguratorInterface> configurator) + : configurator_(configurator.Pass()) {} + + // Gets pointer of BatchRequestConfiguratorInterface owned by the instance. + BatchRequestConfiguratorInterface* configurator() const { + return configurator_.get(); + } + + private: + friend class base::RefCounted<RefCountedBatchRequest>; + ~RefCountedBatchRequest() { configurator_->Commit(); } + scoped_ptr<BatchRequestConfiguratorInterface> configurator_; +}; + +// Structure containing current upload information of file, passed between +// DriveServiceInterface methods and callbacks. +struct DriveUploader::UploadFileInfo { + UploadFileInfo(const base::FilePath& local_path, + const std::string& content_type, + const UploadCompletionCallback& callback, + const ProgressCallback& progress_callback) + : file_path(local_path), + content_type(content_type), + completion_callback(callback), + progress_callback(progress_callback), + content_length(0), + next_start_position(-1), + power_save_blocker(content::PowerSaveBlocker::Create( + content::PowerSaveBlocker::kPowerSaveBlockPreventAppSuspension, + content::PowerSaveBlocker::kReasonOther, + "Upload in progress")), + cancelled(false), + weak_ptr_factory_(this) {} + + ~UploadFileInfo() { + } + + // Useful for printf debugging. + std::string DebugString() const { + return "file_path=[" + file_path.AsUTF8Unsafe() + + "], content_type=[" + content_type + + "], content_length=[" + base::UintToString(content_length) + + "]"; + } + + // Returns the callback to cancel the upload represented by this struct. + CancelCallback GetCancelCallback() { + return base::Bind(&UploadFileInfo::Cancel, weak_ptr_factory_.GetWeakPtr()); + } + + // The local file path of the file to be uploaded. + const base::FilePath file_path; + + // Content-Type of file. + const std::string content_type; + + // Callback to be invoked once the upload has finished. + const UploadCompletionCallback completion_callback; + + // Callback to periodically notify the upload progress. + const ProgressCallback progress_callback; + + // Location URL where file is to be uploaded to, returned from + // InitiateUpload. Used for the subsequent ResumeUpload requests. + GURL upload_location; + + // Header content-Length. + int64 content_length; + + int64 next_start_position; + + // Blocks system suspend while upload is in progress. + scoped_ptr<content::PowerSaveBlocker> power_save_blocker; + + // Fields for implementing cancellation. |cancel_callback| is non-null if + // there is an in-flight HTTP request. In that case, |cancell_callback| will + // cancel the operation. |cancelled| is initially false and turns to true + // once Cancel() is called. DriveUploader will check this field before after + // an async task other than HTTP requests and cancels the subsequent requests + // if this is flagged to true. + CancelCallback cancel_callback; + bool cancelled; + + private: + // Cancels the upload represented by this struct. + void Cancel() { + cancelled = true; + if (!cancel_callback.is_null()) + cancel_callback.Run(); + } + + base::WeakPtrFactory<UploadFileInfo> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(UploadFileInfo); +}; + +DriveUploader::DriveUploader( + DriveServiceInterface* drive_service, + const scoped_refptr<base::TaskRunner>& blocking_task_runner) + : drive_service_(drive_service), + blocking_task_runner_(blocking_task_runner), + weak_ptr_factory_(this) { +} + +DriveUploader::~DriveUploader() {} + +CancelCallback DriveUploader::UploadNewFile( + const std::string& parent_resource_id, + const base::FilePath& local_file_path, + const std::string& title, + const std::string& content_type, + const UploadNewFileOptions& options, + const UploadCompletionCallback& callback, + const ProgressCallback& progress_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!parent_resource_id.empty()); + DCHECK(!local_file_path.empty()); + DCHECK(!title.empty()); + DCHECK(!content_type.empty()); + DCHECK(!callback.is_null()); + + return StartUploadFile( + scoped_ptr<UploadFileInfo>(new UploadFileInfo( + local_file_path, content_type, callback, progress_callback)), + base::Bind(&DriveUploader::CallUploadServiceAPINewFile, + weak_ptr_factory_.GetWeakPtr(), parent_resource_id, title, + options, current_batch_request_)); +} + +void DriveUploader::StartBatchProcessing() { + DCHECK(current_batch_request_ == nullptr); + current_batch_request_ = + new RefCountedBatchRequest(drive_service_->StartBatchRequest().Pass()); +} + +void DriveUploader::StopBatchProcessing() { + current_batch_request_ = nullptr; +} + +CancelCallback DriveUploader::UploadExistingFile( + const std::string& resource_id, + const base::FilePath& local_file_path, + const std::string& content_type, + const UploadExistingFileOptions& options, + const UploadCompletionCallback& callback, + const ProgressCallback& progress_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!resource_id.empty()); + DCHECK(!local_file_path.empty()); + DCHECK(!content_type.empty()); + DCHECK(!callback.is_null()); + + return StartUploadFile( + scoped_ptr<UploadFileInfo>(new UploadFileInfo( + local_file_path, content_type, callback, progress_callback)), + base::Bind(&DriveUploader::CallUploadServiceAPIExistingFile, + weak_ptr_factory_.GetWeakPtr(), resource_id, options, + current_batch_request_)); +} + +CancelCallback DriveUploader::ResumeUploadFile( + const GURL& upload_location, + const base::FilePath& local_file_path, + const std::string& content_type, + const UploadCompletionCallback& callback, + const ProgressCallback& progress_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!local_file_path.empty()); + DCHECK(!content_type.empty()); + DCHECK(!callback.is_null()); + + scoped_ptr<UploadFileInfo> upload_file_info(new UploadFileInfo( + local_file_path, content_type, callback, progress_callback)); + upload_file_info->upload_location = upload_location; + + return StartUploadFile( + upload_file_info.Pass(), + base::Bind(&DriveUploader::StartGetUploadStatus, + weak_ptr_factory_.GetWeakPtr())); +} + +CancelCallback DriveUploader::StartUploadFile( + scoped_ptr<UploadFileInfo> upload_file_info, + const StartInitiateUploadCallback& start_initiate_upload_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DVLOG(1) << "Uploading file: " << upload_file_info->DebugString(); + + UploadFileInfo* info_ptr = upload_file_info.get(); + base::PostTaskAndReplyWithResult( + blocking_task_runner_.get(), + FROM_HERE, + base::Bind(&base::GetFileSize, + info_ptr->file_path, + &info_ptr->content_length), + base::Bind(&DriveUploader::StartUploadFileAfterGetFileSize, + weak_ptr_factory_.GetWeakPtr(), + base::Passed(&upload_file_info), + start_initiate_upload_callback)); + return info_ptr->GetCancelCallback(); +} + +void DriveUploader::StartUploadFileAfterGetFileSize( + scoped_ptr<UploadFileInfo> upload_file_info, + const StartInitiateUploadCallback& start_initiate_upload_callback, + bool get_file_size_result) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (!get_file_size_result) { + UploadFailed(upload_file_info.Pass(), HTTP_NOT_FOUND); + return; + } + DCHECK_GE(upload_file_info->content_length, 0); + + if (upload_file_info->cancelled) { + UploadFailed(upload_file_info.Pass(), DRIVE_CANCELLED); + return; + } + start_initiate_upload_callback.Run(upload_file_info.Pass()); +} + +void DriveUploader::CallUploadServiceAPINewFile( + const std::string& parent_resource_id, + const std::string& title, + const UploadNewFileOptions& options, + const scoped_refptr<RefCountedBatchRequest>& batch_request, + scoped_ptr<UploadFileInfo> upload_file_info) { + DCHECK(thread_checker_.CalledOnValidThread()); + + UploadFileInfo* const info_ptr = upload_file_info.get(); + if (info_ptr->content_length <= kMaxMultipartUploadSize) { + DriveServiceBatchOperationsInterface* service; + // If this is a batched request, calls the API on the request instead. + if (batch_request.get()) { + service = batch_request->configurator(); + RecordDriveUploadProtocol(UPLOAD_METHOD_BATCH); + } else { + service = drive_service_; + RecordDriveUploadProtocol(UPLOAD_METHOD_MULTIPART); + } + info_ptr->cancel_callback = service->MultipartUploadNewFile( + info_ptr->content_type, info_ptr->content_length, parent_resource_id, + title, info_ptr->file_path, options, + base::Bind(&DriveUploader::OnMultipartUploadComplete, + weak_ptr_factory_.GetWeakPtr(), + base::Passed(&upload_file_info)), + info_ptr->progress_callback); + } else { + RecordDriveUploadProtocol(UPLOAD_METHOD_RESUMABLE); + info_ptr->cancel_callback = drive_service_->InitiateUploadNewFile( + info_ptr->content_type, info_ptr->content_length, parent_resource_id, + title, options, base::Bind(&DriveUploader::OnUploadLocationReceived, + weak_ptr_factory_.GetWeakPtr(), + base::Passed(&upload_file_info))); + } +} + +void DriveUploader::CallUploadServiceAPIExistingFile( + const std::string& resource_id, + const UploadExistingFileOptions& options, + const scoped_refptr<RefCountedBatchRequest>& batch_request, + scoped_ptr<UploadFileInfo> upload_file_info) { + DCHECK(thread_checker_.CalledOnValidThread()); + + UploadFileInfo* const info_ptr = upload_file_info.get(); + if (info_ptr->content_length <= kMaxMultipartUploadSize) { + DriveServiceBatchOperationsInterface* service; + // If this is a batched request, calls the API on the request instead. + if (batch_request.get()) { + service = batch_request->configurator(); + RecordDriveUploadProtocol(UPLOAD_METHOD_BATCH); + } else { + service = drive_service_; + RecordDriveUploadProtocol(UPLOAD_METHOD_MULTIPART); + } + info_ptr->cancel_callback = service->MultipartUploadExistingFile( + info_ptr->content_type, info_ptr->content_length, resource_id, + info_ptr->file_path, options, + base::Bind(&DriveUploader::OnMultipartUploadComplete, + weak_ptr_factory_.GetWeakPtr(), + base::Passed(&upload_file_info)), + info_ptr->progress_callback); + } else { + RecordDriveUploadProtocol(UPLOAD_METHOD_RESUMABLE); + info_ptr->cancel_callback = drive_service_->InitiateUploadExistingFile( + info_ptr->content_type, info_ptr->content_length, resource_id, options, + base::Bind(&DriveUploader::OnUploadLocationReceived, + weak_ptr_factory_.GetWeakPtr(), + base::Passed(&upload_file_info))); + } +} + +void DriveUploader::OnUploadLocationReceived( + scoped_ptr<UploadFileInfo> upload_file_info, + DriveApiErrorCode code, + const GURL& upload_location) { + DCHECK(thread_checker_.CalledOnValidThread()); + + DVLOG(1) << "Got upload location [" << upload_location.spec() + << "] for [" << upload_file_info->file_path.value() << "]"; + + if (code != HTTP_SUCCESS) { + if (code == HTTP_PRECONDITION) + code = HTTP_CONFLICT; // ETag mismatch. + UploadFailed(upload_file_info.Pass(), code); + return; + } + + upload_file_info->upload_location = upload_location; + upload_file_info->next_start_position = 0; + UploadNextChunk(upload_file_info.Pass()); +} + +void DriveUploader::StartGetUploadStatus( + scoped_ptr<UploadFileInfo> upload_file_info) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(upload_file_info); + + UploadFileInfo* info_ptr = upload_file_info.get(); + info_ptr->cancel_callback = drive_service_->GetUploadStatus( + info_ptr->upload_location, + info_ptr->content_length, + base::Bind(&DriveUploader::OnUploadRangeResponseReceived, + weak_ptr_factory_.GetWeakPtr(), + base::Passed(&upload_file_info))); +} + +void DriveUploader::UploadNextChunk( + scoped_ptr<UploadFileInfo> upload_file_info) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(upload_file_info); + DCHECK_GE(upload_file_info->next_start_position, 0); + DCHECK_LE(upload_file_info->next_start_position, + upload_file_info->content_length); + + if (upload_file_info->cancelled) { + UploadFailed(upload_file_info.Pass(), DRIVE_CANCELLED); + return; + } + + // Limit the size of data uploaded per each request by kUploadChunkSize. + const int64 end_position = std::min( + upload_file_info->content_length, + upload_file_info->next_start_position + kUploadChunkSize); + + UploadFileInfo* info_ptr = upload_file_info.get(); + info_ptr->cancel_callback = drive_service_->ResumeUpload( + info_ptr->upload_location, + info_ptr->next_start_position, + end_position, + info_ptr->content_length, + info_ptr->content_type, + info_ptr->file_path, + base::Bind(&DriveUploader::OnUploadRangeResponseReceived, + weak_ptr_factory_.GetWeakPtr(), + base::Passed(&upload_file_info)), + base::Bind(&DriveUploader::OnUploadProgress, + weak_ptr_factory_.GetWeakPtr(), + info_ptr->progress_callback, + info_ptr->next_start_position, + info_ptr->content_length)); +} + +void DriveUploader::OnUploadRangeResponseReceived( + scoped_ptr<UploadFileInfo> upload_file_info, + const UploadRangeResponse& response, + scoped_ptr<FileResource> entry) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (response.code == HTTP_CREATED || response.code == HTTP_SUCCESS) { + // When uploading a new file, we expect HTTP_CREATED, and when uploading + // an existing file (to overwrite), we expect HTTP_SUCCESS. + // There is an exception: if we uploading an empty file, uploading a new + // file also returns HTTP_SUCCESS on Drive API v2. The correct way of the + // fix should be uploading the metadata only. However, to keep the + // compatibility with GData WAPI during the migration period, we just + // relax the condition here. + // TODO(hidehiko): Upload metadata only for empty files, after GData WAPI + // code is gone. + DVLOG(1) << "Successfully created uploaded file=[" + << upload_file_info->file_path.value() << "]"; + + // Done uploading. + upload_file_info->completion_callback.Run( + HTTP_SUCCESS, GURL(), entry.Pass()); + return; + } + + // ETag mismatch. + if (response.code == HTTP_PRECONDITION) { + UploadFailed(upload_file_info.Pass(), HTTP_CONFLICT); + return; + } + + // If code is 308 (RESUME_INCOMPLETE) and |range_received| starts with 0 + // (meaning that the data is uploaded from the beginning of the file), + // proceed to upload the next chunk. + if (response.code != HTTP_RESUME_INCOMPLETE || + response.start_position_received != 0) { + DVLOG(1) + << "UploadNextChunk http code=" << response.code + << ", start_position_received=" << response.start_position_received + << ", end_position_received=" << response.end_position_received; + UploadFailed( + upload_file_info.Pass(), + response.code == HTTP_FORBIDDEN ? DRIVE_NO_SPACE : response.code); + return; + } + + DVLOG(1) << "Received range " << response.start_position_received + << "-" << response.end_position_received + << " for [" << upload_file_info->file_path.value() << "]"; + + upload_file_info->next_start_position = response.end_position_received; + UploadNextChunk(upload_file_info.Pass()); +} + +void DriveUploader::OnUploadProgress(const ProgressCallback& callback, + int64 start_position, + int64 total_size, + int64 progress_of_chunk, + int64 total_of_chunk) { + if (!callback.is_null()) + callback.Run(start_position + progress_of_chunk, total_size); +} + +void DriveUploader::UploadFailed(scoped_ptr<UploadFileInfo> upload_file_info, + DriveApiErrorCode error) { + DCHECK(thread_checker_.CalledOnValidThread()); + + DVLOG(1) << "Upload failed " << upload_file_info->DebugString(); + + if (upload_file_info->next_start_position < 0) { + // Discard the upload location because no request could succeed with it. + // Maybe it's obsolete. + upload_file_info->upload_location = GURL(); + } + + upload_file_info->completion_callback.Run( + error, upload_file_info->upload_location, scoped_ptr<FileResource>()); +} + +void DriveUploader::OnMultipartUploadComplete( + scoped_ptr<UploadFileInfo> upload_file_info, + google_apis::DriveApiErrorCode error, + scoped_ptr<FileResource> entry) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (error == HTTP_CREATED || error == HTTP_SUCCESS) { + DVLOG(1) << "Successfully created uploaded file=[" + << upload_file_info->file_path.value() << "]"; + // Done uploading. + upload_file_info->completion_callback.Run( + HTTP_SUCCESS, upload_file_info->upload_location, entry.Pass()); + } else { + DVLOG(1) << "Upload failed " << upload_file_info->DebugString(); + if (error == HTTP_PRECONDITION) + error = HTTP_CONFLICT; // ETag mismatch. + upload_file_info->completion_callback.Run( + error, upload_file_info->upload_location, scoped_ptr<FileResource>()); + } +} + +} // namespace drive diff --git a/components/drive/drive_uploader.h b/components/drive/drive_uploader.h new file mode 100644 index 0000000..e383614 --- /dev/null +++ b/components/drive/drive_uploader.h @@ -0,0 +1,240 @@ +// 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_DRIVE_UPLOADER_H_ +#define COMPONENTS_DRIVE_DRIVE_UPLOADER_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "components/drive/service/drive_service_interface.h" +#include "google_apis/drive/drive_api_error_codes.h" + +class GURL; + +namespace base { +class FilePath; +class TaskRunner; +} + +namespace google_apis { +struct UploadRangeResponse; +} + +namespace drive { +class DriveServiceInterface; + +// Callback to be invoked once the upload has completed. +// |upload_location| will be returned when the uploading process is started but +// terminated before the completion due to some errors. It can be used to +// resume it. +typedef base::Callback<void( + google_apis::DriveApiErrorCode error, + const GURL& upload_location, + scoped_ptr<google_apis::FileResource> resource_entry)> + UploadCompletionCallback; + +class DriveUploaderInterface { + public: + virtual ~DriveUploaderInterface() {} + + // Starts batch processing for upload requests. All requests which upload + // small files (less than kMaxMultipartUploadSize) between + // |StartBatchProcessing| and |StopBatchProcessing| are sent as a single batch + // request. + virtual void StartBatchProcessing() = 0; + + // Stops batch processing. Must be called after calling |StartBatchProcessing| + // to commit requests. + virtual void StopBatchProcessing() = 0; + + // Uploads a new file to a directory specified by |upload_location|. + // Returns a callback for cancelling the uploading job. + // + // parent_resource_id: + // resource id of the destination directory. + // + // local_file_path: + // The path to the local file to be uploaded. + // + // title: + // The title (file name) of the file to be uploaded. + // + // content_type: + // The content type of the file to be uploaded. + // + // callback: + // Called when an upload is done regardless of it was successful or not. + // Must not be null. + // + // progress_callback: + // Periodically called back with the total number of bytes sent so far. + // May be null if the information is not needed. + virtual google_apis::CancelCallback UploadNewFile( + const std::string& parent_resource_id, + const base::FilePath& local_file_path, + const std::string& title, + const std::string& content_type, + const UploadNewFileOptions& options, + const UploadCompletionCallback& callback, + const google_apis::ProgressCallback& progress_callback) = 0; + + // Uploads an existing file (a file that already exists on Drive). + // + // See comments at UploadNewFile about common parameters and the return value. + // + // resource_id: + // resource id of the existing file to be overwritten. + // + // etag: + // Expected ETag for the destination file. If it does not match, the upload + // fails with UPLOAD_ERROR_CONFLICT. + // If |etag| is empty, the test is skipped. + virtual google_apis::CancelCallback UploadExistingFile( + const std::string& resource_id, + const base::FilePath& local_file_path, + const std::string& content_type, + const UploadExistingFileOptions& options, + const UploadCompletionCallback& callback, + const google_apis::ProgressCallback& progress_callback) = 0; + + // Resumes the uploading process terminated before the completion. + // |upload_location| should be the one returned via UploadCompletionCallback + // for previous invocation. |drive_file_path|, |local_file_path| and + // |content_type| must be set to the same ones for previous invocation. + // + // See comments at UploadNewFile about common parameters and the return value. + virtual google_apis::CancelCallback ResumeUploadFile( + const GURL& upload_location, + const base::FilePath& local_file_path, + const std::string& content_type, + const UploadCompletionCallback& callback, + const google_apis::ProgressCallback& progress_callback) = 0; +}; + +class DriveUploader : public DriveUploaderInterface { + public: + DriveUploader(DriveServiceInterface* drive_service, + const scoped_refptr<base::TaskRunner>& blocking_task_runner); + ~DriveUploader() override; + + // DriveUploaderInterface overrides. + void StartBatchProcessing() override; + void StopBatchProcessing() override; + google_apis::CancelCallback UploadNewFile( + const std::string& parent_resource_id, + const base::FilePath& local_file_path, + const std::string& title, + const std::string& content_type, + const UploadNewFileOptions& options, + const UploadCompletionCallback& callback, + const google_apis::ProgressCallback& progress_callback) override; + google_apis::CancelCallback UploadExistingFile( + const std::string& resource_id, + const base::FilePath& local_file_path, + const std::string& content_type, + const UploadExistingFileOptions& options, + const UploadCompletionCallback& callback, + const google_apis::ProgressCallback& progress_callback) override; + google_apis::CancelCallback ResumeUploadFile( + const GURL& upload_location, + const base::FilePath& local_file_path, + const std::string& content_type, + const UploadCompletionCallback& callback, + const google_apis::ProgressCallback& progress_callback) override; + + private: + class RefCountedBatchRequest; + struct UploadFileInfo; + typedef base::Callback<void(scoped_ptr<UploadFileInfo> upload_file_info)> + StartInitiateUploadCallback; + + // Starts uploading a file with |upload_file_info|. + google_apis::CancelCallback StartUploadFile( + scoped_ptr<UploadFileInfo> upload_file_info, + const StartInitiateUploadCallback& start_initiate_upload_callback); + void StartUploadFileAfterGetFileSize( + scoped_ptr<UploadFileInfo> upload_file_info, + const StartInitiateUploadCallback& start_initiate_upload_callback, + bool get_file_size_result); + + // Checks file size and call InitiateUploadNewFile or MultipartUploadNewFile + // API. Upon completion, OnUploadLocationReceived (for InitiateUploadNewFile) + // or OnMultipartUploadComplete (for MultipartUploadNewFile) should be called. + // If |batch_request| is non-null, it calls the API function on the batch + // request. + void CallUploadServiceAPINewFile( + const std::string& parent_resource_id, + const std::string& title, + const UploadNewFileOptions& options, + const scoped_refptr<RefCountedBatchRequest>& batch_request, + scoped_ptr<UploadFileInfo> upload_file_info); + + // Checks file size and call InitiateUploadExistingFile or + // MultipartUploadExistingFile API. Upon completion, OnUploadLocationReceived + // (for InitiateUploadExistingFile) or OnMultipartUploadComplete (for + // MultipartUploadExistingFile) should be called. + // If |batch_request| is non-null, it calls the API function on the batch + // request. + void CallUploadServiceAPIExistingFile( + const std::string& resource_id, + const UploadExistingFileOptions& options, + const scoped_refptr<RefCountedBatchRequest>& batch_request, + scoped_ptr<UploadFileInfo> upload_file_info); + + // DriveService callback for InitiateUpload. + void OnUploadLocationReceived(scoped_ptr<UploadFileInfo> upload_file_info, + google_apis::DriveApiErrorCode code, + const GURL& upload_location); + + // Starts to get the current upload status for the file uploading. + // Upon completion, OnUploadRangeResponseReceived should be called. + void StartGetUploadStatus(scoped_ptr<UploadFileInfo> upload_file_info); + + // Uploads the next chunk of data from the file. + void UploadNextChunk(scoped_ptr<UploadFileInfo> upload_file_info); + + // DriveService callback for ResumeUpload. + void OnUploadRangeResponseReceived( + scoped_ptr<UploadFileInfo> upload_file_info, + const google_apis::UploadRangeResponse& response, + scoped_ptr<google_apis::FileResource> entry); + void OnUploadProgress(const google_apis::ProgressCallback& callback, + int64 start_position, + int64 total_size, + int64 progress_of_chunk, + int64 total_of_chunk); + + // Handles failed uploads. + void UploadFailed(scoped_ptr<UploadFileInfo> upload_file_info, + google_apis::DriveApiErrorCode error); + + // Handles completion/error of multipart uploading. + void OnMultipartUploadComplete(scoped_ptr<UploadFileInfo> upload_file_info, + google_apis::DriveApiErrorCode error, + scoped_ptr<google_apis::FileResource> entry); + + // The class is expected to run on UI thread. + base::ThreadChecker thread_checker_; + + // The lifetime of this object should be guaranteed to exceed that of the + // DriveUploader instance. + DriveServiceInterface* drive_service_; // Not owned by this class. + + scoped_refptr<base::TaskRunner> blocking_task_runner_; + scoped_refptr<RefCountedBatchRequest> current_batch_request_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate its weak pointers before any other members are destroyed. + base::WeakPtrFactory<DriveUploader> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(DriveUploader); +}; + +} // namespace drive + +#endif // COMPONENTS_DRIVE_DRIVE_UPLOADER_H_ diff --git a/components/drive/drive_uploader_unittest.cc b/components/drive/drive_uploader_unittest.cc new file mode 100644 index 0000000..29fbf26ac --- /dev/null +++ b/components/drive/drive_uploader_unittest.cc @@ -0,0 +1,955 @@ +// 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/drive_uploader.h" + +#include <algorithm> +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/thread_task_runner_handle.h" +#include "base/values.h" +#include "components/drive/service/dummy_drive_service.h" +#include "google_apis/drive/drive_api_parser.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +using google_apis::CancelCallback; +using google_apis::FileResource; +using google_apis::DriveApiErrorCode; +using google_apis::DRIVE_NO_CONNECTION; +using google_apis::DRIVE_OTHER_ERROR; +using google_apis::HTTP_CONFLICT; +using google_apis::HTTP_CREATED; +using google_apis::HTTP_NOT_FOUND; +using google_apis::HTTP_PRECONDITION; +using google_apis::HTTP_RESUME_INCOMPLETE; +using google_apis::HTTP_SUCCESS; +using google_apis::InitiateUploadCallback; +using google_apis::ProgressCallback; +using google_apis::UploadRangeResponse; +using google_apis::drive::UploadRangeCallback; +namespace test_util = google_apis::test_util; + +namespace drive { + +namespace { + +const char kTestDummyMd5[] = "dummy_md5"; +const char kTestDocumentTitle[] = "Hello world"; +const char kTestInitiateUploadParentResourceId[] = "parent_resource_id"; +const char kTestInitiateUploadResourceId[] = "resource_id"; +const char kTestMimeType[] = "text/plain"; +const char kTestUploadNewFileURL[] = "http://test/upload_location/new_file"; +const char kTestUploadExistingFileURL[] = + "http://test/upload_location/existing_file"; +const int64 kUploadChunkSize = 1024 * 1024 * 1024; +const char kTestETag[] = "test_etag"; + +CancelCallback SendMultipartUploadResult( + DriveApiErrorCode response_code, + int64 content_length, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) { + // Callback progress + if (!progress_callback.is_null()) { + // For the testing purpose, it always notifies the progress at the end of + // whole file uploading. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(progress_callback, content_length, content_length)); + } + + // MultipartUploadXXXFile is an asynchronous function, so don't callback + // directly. + scoped_ptr<FileResource> entry; + entry.reset(new FileResource); + entry->set_md5_checksum(kTestDummyMd5); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, response_code, base::Passed(&entry))); + return CancelCallback(); +} + +// Mock DriveService that verifies if the uploaded content matches the preset +// expectation. +class MockDriveServiceWithUploadExpectation : public DummyDriveService { + public: + // Sets up an expected upload content. InitiateUpload and ResumeUpload will + // verify that the specified data is correctly uploaded. + MockDriveServiceWithUploadExpectation( + const base::FilePath& expected_upload_file, + int64 expected_content_length) + : expected_upload_file_(expected_upload_file), + expected_content_length_(expected_content_length), + received_bytes_(0), + resume_upload_call_count_(0), + multipart_upload_call_count_(0) {} + + int64 received_bytes() const { return received_bytes_; } + void set_received_bytes(int64 received_bytes) { + received_bytes_ = received_bytes; + } + + int64 resume_upload_call_count() const { return resume_upload_call_count_; } + int64 multipart_upload_call_count() const { + return multipart_upload_call_count_; + } + + private: + // DriveServiceInterface overrides. + // Handles a request for obtaining an upload location URL. + CancelCallback InitiateUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const UploadNewFileOptions& options, + const InitiateUploadCallback& callback) override { + EXPECT_EQ(kTestDocumentTitle, title); + EXPECT_EQ(kTestMimeType, content_type); + EXPECT_EQ(expected_content_length_, content_length); + EXPECT_EQ(kTestInitiateUploadParentResourceId, parent_resource_id); + + // Calls back the upload URL for subsequent ResumeUpload requests. + // InitiateUpload is an asynchronous function, so don't callback directly. + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + base::Bind(callback, HTTP_SUCCESS, GURL(kTestUploadNewFileURL))); + return CancelCallback(); + } + + CancelCallback InitiateUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const UploadExistingFileOptions& options, + const InitiateUploadCallback& callback) override { + EXPECT_EQ(kTestMimeType, content_type); + EXPECT_EQ(expected_content_length_, content_length); + EXPECT_EQ(kTestInitiateUploadResourceId, resource_id); + + if (!options.etag.empty() && options.etag != kTestETag) { + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + base::Bind(callback, HTTP_PRECONDITION, GURL())); + return CancelCallback(); + } + + // Calls back the upload URL for subsequent ResumeUpload requests. + // InitiateUpload is an asynchronous function, so don't callback directly. + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + base::Bind(callback, HTTP_SUCCESS, GURL(kTestUploadExistingFileURL))); + return CancelCallback(); + } + + // Handles a request for uploading a chunk of bytes. + CancelCallback ResumeUpload( + const GURL& upload_location, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path, + const UploadRangeCallback& callback, + const ProgressCallback& progress_callback) override { + // The upload range should start from the current first unreceived byte. + EXPECT_EQ(received_bytes_, start_position); + EXPECT_EQ(expected_upload_file_, local_file_path); + + // The upload data must be split into 512KB chunks. + const int64 expected_chunk_end = + std::min(received_bytes_ + kUploadChunkSize, expected_content_length_); + EXPECT_EQ(expected_chunk_end, end_position); + + // The upload URL returned by InitiateUpload() must be used. + EXPECT_TRUE(GURL(kTestUploadNewFileURL) == upload_location || + GURL(kTestUploadExistingFileURL) == upload_location); + + // Other parameters should be the exact values passed to DriveUploader. + EXPECT_EQ(expected_content_length_, content_length); + EXPECT_EQ(kTestMimeType, content_type); + + // Update the internal status of the current upload session. + resume_upload_call_count_++; + received_bytes_ = end_position; + + // Callback progress + if (!progress_callback.is_null()) { + // For the testing purpose, it always notifies the progress at the end of + // each chunk uploading. + int64 chunk_size = end_position - start_position; + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + base::Bind(progress_callback, chunk_size, chunk_size)); + } + + SendUploadRangeResponse(upload_location, callback); + return CancelCallback(); + } + + // Handles a request to fetch the current upload status. + CancelCallback GetUploadStatus(const GURL& upload_location, + int64 content_length, + const UploadRangeCallback& callback) override { + EXPECT_EQ(expected_content_length_, content_length); + // The upload URL returned by InitiateUpload() must be used. + EXPECT_TRUE(GURL(kTestUploadNewFileURL) == upload_location || + GURL(kTestUploadExistingFileURL) == upload_location); + + SendUploadRangeResponse(upload_location, callback); + return CancelCallback(); + } + + // Runs |callback| with the current upload status. + void SendUploadRangeResponse(const GURL& upload_location, + const UploadRangeCallback& callback) { + // Callback with response. + UploadRangeResponse response; + scoped_ptr<FileResource> entry; + if (received_bytes_ == expected_content_length_) { + DriveApiErrorCode response_code = + upload_location == GURL(kTestUploadNewFileURL) ? + HTTP_CREATED : HTTP_SUCCESS; + response = UploadRangeResponse(response_code, -1, -1); + + entry.reset(new FileResource); + entry->set_md5_checksum(kTestDummyMd5); + } else { + response = UploadRangeResponse( + HTTP_RESUME_INCOMPLETE, 0, received_bytes_); + } + // ResumeUpload is an asynchronous function, so don't callback directly. + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + base::Bind(callback, response, base::Passed(&entry))); + } + + CancelCallback MultipartUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const base::FilePath& local_file_path, + const UploadNewFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) override { + EXPECT_EQ(kTestMimeType, content_type); + EXPECT_EQ(expected_content_length_, content_length); + EXPECT_EQ(kTestInitiateUploadParentResourceId, parent_resource_id); + EXPECT_EQ(kTestDocumentTitle, title); + EXPECT_EQ(expected_upload_file_, local_file_path); + + received_bytes_ = content_length; + multipart_upload_call_count_++; + return SendMultipartUploadResult(HTTP_CREATED, content_length, callback, + progress_callback); + } + + CancelCallback MultipartUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const base::FilePath& local_file_path, + const UploadExistingFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) override { + EXPECT_EQ(kTestMimeType, content_type); + EXPECT_EQ(expected_content_length_, content_length); + EXPECT_EQ(kTestInitiateUploadResourceId, resource_id); + EXPECT_EQ(expected_upload_file_, local_file_path); + + if (!options.etag.empty() && options.etag != kTestETag) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_PRECONDITION, + base::Passed(make_scoped_ptr<FileResource>(NULL)))); + return CancelCallback(); + } + + received_bytes_ = content_length; + multipart_upload_call_count_++; + return SendMultipartUploadResult(HTTP_SUCCESS, content_length, callback, + progress_callback); + } + + const base::FilePath expected_upload_file_; + const int64 expected_content_length_; + int64 received_bytes_; + int64 resume_upload_call_count_; + int64 multipart_upload_call_count_; +}; + +// Mock DriveService that returns a failure at InitiateUpload(). +class MockDriveServiceNoConnectionAtInitiate : public DummyDriveService { + // Returns error. + CancelCallback InitiateUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const UploadNewFileOptions& options, + const InitiateUploadCallback& callback) override { + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + base::Bind(callback, DRIVE_NO_CONNECTION, GURL())); + return CancelCallback(); + } + + CancelCallback InitiateUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const UploadExistingFileOptions& options, + const InitiateUploadCallback& callback) override { + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + base::Bind(callback, DRIVE_NO_CONNECTION, GURL())); + return CancelCallback(); + } + + // Should not be used. + CancelCallback ResumeUpload( + const GURL& upload_url, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path, + const UploadRangeCallback& callback, + const ProgressCallback& progress_callback) override { + NOTREACHED(); + return CancelCallback(); + } + + CancelCallback MultipartUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const base::FilePath& local_file_path, + const UploadNewFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) override { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, DRIVE_NO_CONNECTION, + base::Passed(make_scoped_ptr<FileResource>(NULL)))); + return CancelCallback(); + } + + CancelCallback MultipartUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const base::FilePath& local_file_path, + const UploadExistingFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) override { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, DRIVE_NO_CONNECTION, + base::Passed(make_scoped_ptr<FileResource>(NULL)))); + return CancelCallback(); + } +}; + +// Mock DriveService that returns a failure at ResumeUpload(). +class MockDriveServiceNoConnectionAtResume : public DummyDriveService { + // Succeeds and returns an upload location URL. + CancelCallback InitiateUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const UploadNewFileOptions& options, + const InitiateUploadCallback& callback) override { + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + base::Bind(callback, HTTP_SUCCESS, GURL(kTestUploadNewFileURL))); + return CancelCallback(); + } + + CancelCallback InitiateUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const UploadExistingFileOptions& options, + const InitiateUploadCallback& callback) override { + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + base::Bind(callback, HTTP_SUCCESS, GURL(kTestUploadExistingFileURL))); + return CancelCallback(); + } + + // Returns error. + CancelCallback ResumeUpload( + const GURL& upload_url, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path, + const UploadRangeCallback& callback, + const ProgressCallback& progress_callback) override { + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + base::Bind(callback, + UploadRangeResponse(DRIVE_NO_CONNECTION, -1, -1), + base::Passed(scoped_ptr<FileResource>()))); + return CancelCallback(); + } +}; + +// Mock DriveService that returns a failure at GetUploadStatus(). +class MockDriveServiceNoConnectionAtGetUploadStatus : public DummyDriveService { + // Returns error. + CancelCallback GetUploadStatus(const GURL& upload_url, + int64 content_length, + const UploadRangeCallback& callback) override { + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + base::Bind(callback, + UploadRangeResponse(DRIVE_NO_CONNECTION, -1, -1), + base::Passed(scoped_ptr<FileResource>()))); + return CancelCallback(); + } +}; + +class DriveUploaderTest : public testing::Test { + public: + void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); } + + protected: + base::MessageLoop message_loop_; + base::ScopedTempDir temp_dir_; +}; + +} // namespace + +TEST_F(DriveUploaderTest, UploadExisting0KB) { + base::FilePath local_path; + std::string data; + ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize( + temp_dir_.path(), 0, &local_path, &data)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + scoped_ptr<FileResource> entry; + + MockDriveServiceWithUploadExpectation mock_service(local_path, data.size()); + DriveUploader uploader(&mock_service, + base::ThreadTaskRunnerHandle::Get().get()); + std::vector<test_util::ProgressInfo> upload_progress_values; + uploader.UploadExistingFile( + kTestInitiateUploadResourceId, local_path, kTestMimeType, + UploadExistingFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location, &entry), + base::Bind(&test_util::AppendProgressCallbackResult, + &upload_progress_values)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(0, mock_service.resume_upload_call_count()); + EXPECT_EQ(1, mock_service.multipart_upload_call_count()); + EXPECT_EQ(0, mock_service.received_bytes()); + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_TRUE(upload_location.is_empty()); + ASSERT_TRUE(entry); + EXPECT_EQ(kTestDummyMd5, entry->md5_checksum()); + ASSERT_EQ(1U, upload_progress_values.size()); + EXPECT_EQ(test_util::ProgressInfo(0, 0), upload_progress_values[0]); +} + +TEST_F(DriveUploaderTest, UploadExisting512KB) { + base::FilePath local_path; + std::string data; + ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize( + temp_dir_.path(), 512 * 1024, &local_path, &data)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + scoped_ptr<FileResource> entry; + + MockDriveServiceWithUploadExpectation mock_service(local_path, data.size()); + DriveUploader uploader(&mock_service, + base::ThreadTaskRunnerHandle::Get().get()); + std::vector<test_util::ProgressInfo> upload_progress_values; + uploader.UploadExistingFile( + kTestInitiateUploadResourceId, local_path, kTestMimeType, + UploadExistingFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location, &entry), + base::Bind(&test_util::AppendProgressCallbackResult, + &upload_progress_values)); + base::RunLoop().RunUntilIdle(); + + // 512KB upload should be uploaded as multipart body. + EXPECT_EQ(0, mock_service.resume_upload_call_count()); + EXPECT_EQ(1, mock_service.multipart_upload_call_count()); + EXPECT_EQ(512 * 1024, mock_service.received_bytes()); + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_TRUE(upload_location.is_empty()); + ASSERT_TRUE(entry); + EXPECT_EQ(kTestDummyMd5, entry->md5_checksum()); + ASSERT_EQ(1U, upload_progress_values.size()); + EXPECT_EQ(test_util::ProgressInfo(512 * 1024, 512 * 1024), + upload_progress_values[0]); +} + +TEST_F(DriveUploaderTest, UploadExisting2MB) { + base::FilePath local_path; + std::string data; + ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize( + temp_dir_.path(), 2 * 1024 * 1024, &local_path, &data)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + scoped_ptr<FileResource> entry; + + MockDriveServiceWithUploadExpectation mock_service(local_path, data.size()); + DriveUploader uploader(&mock_service, + base::ThreadTaskRunnerHandle::Get().get()); + std::vector<test_util::ProgressInfo> upload_progress_values; + uploader.UploadExistingFile( + kTestInitiateUploadResourceId, local_path, kTestMimeType, + UploadExistingFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location, &entry), + base::Bind(&test_util::AppendProgressCallbackResult, + &upload_progress_values)); + base::RunLoop().RunUntilIdle(); + + // 2MB upload should not be split into multiple chunks. + EXPECT_EQ(1, mock_service.resume_upload_call_count()); + EXPECT_EQ(0, mock_service.multipart_upload_call_count()); + EXPECT_EQ(2 * 1024 * 1024, mock_service.received_bytes()); + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_TRUE(upload_location.is_empty()); + ASSERT_TRUE(entry); + EXPECT_EQ(kTestDummyMd5, entry->md5_checksum()); + ASSERT_EQ(1U, upload_progress_values.size()); + EXPECT_EQ(test_util::ProgressInfo(2 * 1024 * 1024, 2 * 1024 * 1024), + upload_progress_values[0]); +} + +TEST_F(DriveUploaderTest, InitiateUploadFail) { + base::FilePath local_path; + std::string data; + ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize( + temp_dir_.path(), 2 * 1024 * 1024, &local_path, &data)); + + DriveApiErrorCode error = HTTP_SUCCESS; + GURL upload_location; + scoped_ptr<FileResource> entry; + + MockDriveServiceNoConnectionAtInitiate mock_service; + DriveUploader uploader(&mock_service, + base::ThreadTaskRunnerHandle::Get().get()); + uploader.UploadExistingFile( + kTestInitiateUploadResourceId, local_path, kTestMimeType, + UploadExistingFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location, &entry), + google_apis::ProgressCallback()); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_TRUE(upload_location.is_empty()); + EXPECT_FALSE(entry); +} + +TEST_F(DriveUploaderTest, MultipartUploadFail) { + base::FilePath local_path; + std::string data; + ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize(temp_dir_.path(), 512 * 1024, + &local_path, &data)); + + DriveApiErrorCode error = HTTP_SUCCESS; + GURL upload_location; + scoped_ptr<FileResource> entry; + + MockDriveServiceNoConnectionAtInitiate mock_service; + DriveUploader uploader(&mock_service, + base::ThreadTaskRunnerHandle::Get().get()); + uploader.UploadExistingFile( + kTestInitiateUploadResourceId, local_path, kTestMimeType, + UploadExistingFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location, &entry), + google_apis::ProgressCallback()); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_TRUE(upload_location.is_empty()); + EXPECT_FALSE(entry); +} + +TEST_F(DriveUploaderTest, InitiateUploadNoConflict) { + base::FilePath local_path; + std::string data; + ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize( + temp_dir_.path(), 512 * 1024, &local_path, &data)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + scoped_ptr<FileResource> entry; + + MockDriveServiceWithUploadExpectation mock_service(local_path, data.size()); + DriveUploader uploader(&mock_service, + base::ThreadTaskRunnerHandle::Get().get()); + UploadExistingFileOptions options; + options.etag = kTestETag; + uploader.UploadExistingFile(kTestInitiateUploadResourceId, + local_path, + kTestMimeType, + options, + test_util::CreateCopyResultCallback( + &error, &upload_location, &entry), + google_apis::ProgressCallback()); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_TRUE(upload_location.is_empty()); +} + +TEST_F(DriveUploaderTest, MultipartUploadConflict) { + base::FilePath local_path; + std::string data; + ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize( + temp_dir_.path(), 512 * 1024, &local_path, &data)); + const std::string kDestinationETag("destination_etag"); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + scoped_ptr<FileResource> entry; + + MockDriveServiceWithUploadExpectation mock_service(local_path, data.size()); + DriveUploader uploader(&mock_service, + base::ThreadTaskRunnerHandle::Get().get()); + UploadExistingFileOptions options; + options.etag = kDestinationETag; + uploader.UploadExistingFile(kTestInitiateUploadResourceId, + local_path, + kTestMimeType, + options, + test_util::CreateCopyResultCallback( + &error, &upload_location, &entry), + google_apis::ProgressCallback()); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_CONFLICT, error); + EXPECT_TRUE(upload_location.is_empty()); +} + +TEST_F(DriveUploaderTest, InitiateUploadConflict) { + base::FilePath local_path; + std::string data; + ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize( + temp_dir_.path(), 2 * 1024 * 1024, &local_path, &data)); + const std::string kDestinationETag("destination_etag"); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + scoped_ptr<FileResource> entry; + + MockDriveServiceWithUploadExpectation mock_service(local_path, data.size()); + DriveUploader uploader(&mock_service, + base::ThreadTaskRunnerHandle::Get().get()); + UploadExistingFileOptions options; + options.etag = kDestinationETag; + uploader.UploadExistingFile( + kTestInitiateUploadResourceId, local_path, kTestMimeType, options, + test_util::CreateCopyResultCallback(&error, &upload_location, &entry), + google_apis::ProgressCallback()); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_CONFLICT, error); + EXPECT_TRUE(upload_location.is_empty()); +} + +TEST_F(DriveUploaderTest, ResumeUploadFail) { + base::FilePath local_path; + std::string data; + ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize( + temp_dir_.path(), 2 * 1024 * 1024, &local_path, &data)); + + DriveApiErrorCode error = HTTP_SUCCESS; + GURL upload_location; + scoped_ptr<FileResource> entry; + + MockDriveServiceNoConnectionAtResume mock_service; + DriveUploader uploader(&mock_service, + base::ThreadTaskRunnerHandle::Get().get()); + uploader.UploadExistingFile( + kTestInitiateUploadResourceId, local_path, kTestMimeType, + UploadExistingFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location, &entry), + google_apis::ProgressCallback()); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_EQ(GURL(kTestUploadExistingFileURL), upload_location); +} + +TEST_F(DriveUploaderTest, GetUploadStatusFail) { + base::FilePath local_path; + std::string data; + ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize( + temp_dir_.path(), 2 * 1024 * 1024, &local_path, &data)); + + DriveApiErrorCode error = HTTP_SUCCESS; + GURL upload_location; + scoped_ptr<FileResource> entry; + + MockDriveServiceNoConnectionAtGetUploadStatus mock_service; + DriveUploader uploader(&mock_service, + base::ThreadTaskRunnerHandle::Get().get()); + uploader.ResumeUploadFile(GURL(kTestUploadExistingFileURL), + local_path, + kTestMimeType, + test_util::CreateCopyResultCallback( + &error, &upload_location, &entry), + google_apis::ProgressCallback()); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_TRUE(upload_location.is_empty()); +} + +TEST_F(DriveUploaderTest, NonExistingSourceFile) { + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + scoped_ptr<FileResource> entry; + + DriveUploader uploader(NULL, // NULL, the service won't be used. + base::ThreadTaskRunnerHandle::Get().get()); + uploader.UploadExistingFile( + kTestInitiateUploadResourceId, + temp_dir_.path().AppendASCII("_this_path_should_not_exist_"), + kTestMimeType, UploadExistingFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location, &entry), + google_apis::ProgressCallback()); + base::RunLoop().RunUntilIdle(); + + // Should return failure without doing any attempt to connect to the server. + EXPECT_EQ(HTTP_NOT_FOUND, error); + EXPECT_TRUE(upload_location.is_empty()); +} + +TEST_F(DriveUploaderTest, ResumeUpload) { + base::FilePath local_path; + std::string data; + ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize( + temp_dir_.path(), 1024 * 1024, &local_path, &data)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + scoped_ptr<FileResource> entry; + + MockDriveServiceWithUploadExpectation mock_service(local_path, data.size()); + DriveUploader uploader(&mock_service, + base::ThreadTaskRunnerHandle::Get().get()); + // Emulate the situation that the only first part is successfully uploaded, + // but not the latter half. + mock_service.set_received_bytes(512 * 1024); + + std::vector<test_util::ProgressInfo> upload_progress_values; + uploader.ResumeUploadFile( + GURL(kTestUploadExistingFileURL), + local_path, + kTestMimeType, + test_util::CreateCopyResultCallback( + &error, &upload_location, &entry), + base::Bind(&test_util::AppendProgressCallbackResult, + &upload_progress_values)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(1, mock_service.resume_upload_call_count()); + EXPECT_EQ(1024 * 1024, mock_service.received_bytes()); + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_TRUE(upload_location.is_empty()); + ASSERT_TRUE(entry); + EXPECT_EQ(kTestDummyMd5, entry->md5_checksum()); + ASSERT_EQ(1U, upload_progress_values.size()); + EXPECT_EQ(test_util::ProgressInfo(1024 * 1024, 1024 * 1024), + upload_progress_values[0]); +} + +class MockDriveServiceForBatchProcessing : public DummyDriveService { + public: + struct UploadFileInfo { + enum { NEW_FILE, EXISTING_FILE } type; + std::string content_type; + uint64 content_length; + std::string parent_resource_id; + std::string resource_id; + std::string title; + base::FilePath local_file_path; + google_apis::FileResourceCallback callback; + google_apis::ProgressCallback progress_callback; + }; + + class BatchRequestConfigurator : public BatchRequestConfiguratorInterface { + public: + explicit BatchRequestConfigurator( + MockDriveServiceForBatchProcessing* service) + : service(service) {} + + CancelCallback MultipartUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const base::FilePath& local_file_path, + const UploadNewFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) override { + UploadFileInfo info; + info.type = UploadFileInfo::NEW_FILE; + info.content_type = content_type; + info.content_length = content_length; + info.parent_resource_id = parent_resource_id; + info.title = title; + info.local_file_path = local_file_path; + info.callback = callback; + info.progress_callback = progress_callback; + service->files.push_back(info); + return CancelCallback(); + } + + CancelCallback MultipartUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const base::FilePath& local_file_path, + const UploadExistingFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) override { + UploadFileInfo info; + info.type = UploadFileInfo::EXISTING_FILE; + info.content_type = content_type; + info.content_length = content_length; + info.resource_id = resource_id; + info.local_file_path = local_file_path; + info.callback = callback; + info.progress_callback = progress_callback; + service->files.push_back(info); + return CancelCallback(); + } + + void Commit() override { + ASSERT_FALSE(service->committed); + service->committed = true; + for (const auto& file : service->files) { + SendMultipartUploadResult(HTTP_SUCCESS, file.content_length, + file.callback, file.progress_callback); + } + } + + private: + MockDriveServiceForBatchProcessing* service; + }; + + public: + scoped_ptr<BatchRequestConfiguratorInterface> StartBatchRequest() override { + committed = false; + return scoped_ptr<BatchRequestConfiguratorInterface>( + new BatchRequestConfigurator(this)); + } + + std::vector<UploadFileInfo> files; + bool committed; +}; + +TEST_F(DriveUploaderTest, BatchProcessing) { + // Preapre test file. + const size_t kTestFileSize = 1024 * 512; + base::FilePath local_path; + std::string data; + ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize( + temp_dir_.path(), kTestFileSize, &local_path, &data)); + + // Prepare test target. + MockDriveServiceForBatchProcessing service; + DriveUploader uploader(&service, base::ThreadTaskRunnerHandle::Get().get()); + + struct { + DriveApiErrorCode error; + GURL resume_url; + scoped_ptr<FileResource> file; + UploadCompletionCallback callback() { + return test_util::CreateCopyResultCallback(&error, &resume_url, &file); + } + } results[2]; + + uploader.StartBatchProcessing(); + uploader.UploadNewFile("parent_resource_id", local_path, "title", + kTestMimeType, UploadNewFileOptions(), + results[0].callback(), + google_apis::ProgressCallback()); + uploader.UploadExistingFile( + "resource_id", local_path, kTestMimeType, UploadExistingFileOptions(), + results[1].callback(), google_apis::ProgressCallback()); + uploader.StopBatchProcessing(); + base::RunLoop().RunUntilIdle(); + + ASSERT_EQ(2u, service.files.size()); + EXPECT_TRUE(service.committed); + + EXPECT_EQ(MockDriveServiceForBatchProcessing::UploadFileInfo::NEW_FILE, + service.files[0].type); + EXPECT_EQ(kTestMimeType, service.files[0].content_type); + EXPECT_EQ(kTestFileSize, service.files[0].content_length); + EXPECT_EQ("parent_resource_id", service.files[0].parent_resource_id); + EXPECT_EQ("", service.files[0].resource_id); + EXPECT_EQ("title", service.files[0].title); + EXPECT_EQ(local_path.value(), service.files[0].local_file_path.value()); + + EXPECT_EQ(MockDriveServiceForBatchProcessing::UploadFileInfo::EXISTING_FILE, + service.files[1].type); + EXPECT_EQ(kTestMimeType, service.files[1].content_type); + EXPECT_EQ(kTestFileSize, service.files[1].content_length); + EXPECT_EQ("", service.files[1].parent_resource_id); + EXPECT_EQ("resource_id", service.files[1].resource_id); + EXPECT_EQ("", service.files[1].title); + EXPECT_EQ(local_path.value(), service.files[1].local_file_path.value()); + + EXPECT_EQ(HTTP_SUCCESS, results[0].error); + EXPECT_TRUE(results[0].resume_url.is_empty()); + EXPECT_TRUE(results[0].file); + + EXPECT_EQ(HTTP_SUCCESS, results[1].error); + EXPECT_TRUE(results[1].resume_url.is_empty()); + EXPECT_TRUE(results[1].file); +} + +TEST_F(DriveUploaderTest, BatchProcessingWithError) { + // Prepare test target. + MockDriveServiceForBatchProcessing service; + DriveUploader uploader(&service, base::ThreadTaskRunnerHandle::Get().get()); + + struct { + DriveApiErrorCode error; + GURL resume_url; + scoped_ptr<FileResource> file; + UploadCompletionCallback callback() { + return test_util::CreateCopyResultCallback(&error, &resume_url, &file); + } + } results[2]; + + uploader.StartBatchProcessing(); + uploader.UploadNewFile("parent_resource_id", + base::FilePath(FILE_PATH_LITERAL("/path/non_exists")), + "title", kTestMimeType, UploadNewFileOptions(), + results[0].callback(), + google_apis::ProgressCallback()); + uploader.UploadExistingFile( + "resource_id", base::FilePath(FILE_PATH_LITERAL("/path/non_exists")), + kTestMimeType, UploadExistingFileOptions(), results[1].callback(), + google_apis::ProgressCallback()); + uploader.StopBatchProcessing(); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(0u, service.files.size()); + EXPECT_TRUE(service.committed); + + EXPECT_EQ(HTTP_NOT_FOUND, results[0].error); + EXPECT_TRUE(results[0].resume_url.is_empty()); + EXPECT_FALSE(results[0].file); + + EXPECT_EQ(HTTP_NOT_FOUND, results[1].error); + EXPECT_TRUE(results[1].resume_url.is_empty()); + EXPECT_FALSE(results[1].file); +} +} // namespace drive diff --git a/components/drive/event_logger.cc b/components/drive/event_logger.cc new file mode 100644 index 0000000..9815c11 --- /dev/null +++ b/components/drive/event_logger.cc @@ -0,0 +1,63 @@ +// 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/event_logger.h" + +#include "base/logging.h" +#include "base/strings/stringprintf.h" + +namespace drive { + +EventLogger::Event::Event( + int id, logging::LogSeverity severity, const std::string& what) + : id(id), + severity(severity), + when(base::Time::Now()), + what(what) { +} + +EventLogger::EventLogger() + : history_size_(kDefaultHistorySize), + next_event_id_(0) { +} + +EventLogger::~EventLogger() { +} + +void EventLogger::LogRawString(logging::LogSeverity severity, + const std::string& what) { + base::AutoLock auto_lock(lock_); + history_.push_back(Event(next_event_id_, severity, what)); + ++next_event_id_; + if (history_.size() > history_size_) + history_.pop_front(); +} + +void EventLogger::Log(logging::LogSeverity severity, const char* format, ...) { + std::string what; + + va_list args; + va_start(args, format); + base::StringAppendV(&what, format, args); + va_end(args); + + DVLOG(1) << what; + LogRawString(severity, what); +} + +void EventLogger::SetHistorySize(size_t history_size) { + base::AutoLock auto_lock(lock_); + history_.clear(); + history_size_ = history_size; +} + +std::vector<EventLogger::Event> EventLogger::GetHistory() { + base::AutoLock auto_lock(lock_); + std::vector<Event> output; + output.assign(history_.begin(), history_.end()); + return output; +} + + +} // namespace drive diff --git a/components/drive/event_logger.h b/components/drive/event_logger.h new file mode 100644 index 0000000..6b82a4c --- /dev/null +++ b/components/drive/event_logger.h @@ -0,0 +1,69 @@ +// 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_EVENT_LOGGER_H_ +#define COMPONENTS_DRIVE_EVENT_LOGGER_H_ + +#include <deque> +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/logging.h" +#include "base/synchronization/lock.h" +#include "base/time/time.h" + +namespace drive { + +// The default history size used by EventLogger. +const int kDefaultHistorySize = 1000; + +// EventLogger is used to collect and expose text messages for diagnosing +// behaviors of Google APIs stuff. For instance, the collected messages are +// exposed to chrome:drive-internals. +class EventLogger { + public: + // Represents a single event log. + struct Event { + Event(int id, logging::LogSeverity severity, const std::string& what); + int id; // Monotonically increasing ID starting from 0. + logging::LogSeverity severity; // Severity of the event. + base::Time when; // When the event occurred. + std::string what; // What happened. + }; + + // Creates an event logger that keeps the latest kDefaultHistorySize events. + EventLogger(); + ~EventLogger(); + + // Logs a message and its severity. + // Can be called from any thread as long as the object is alive. + void LogRawString(logging::LogSeverity severity, const std::string& what); + + // Logs a message with formatting. + // Can be called from any thread as long as the object is alive. + void Log(logging::LogSeverity severity, const char* format, ...) + PRINTF_FORMAT(3, 4); + + // Sets the history size. The existing history is cleared. + // Can be called from any thread as long as the object is alive. + void SetHistorySize(size_t history_size); + + // Gets the list of latest events (the oldest event comes first). + // Can be called from any thread as long as the object is alive. + std::vector<Event> GetHistory(); + + private: + std::deque<Event> history_; // guarded by lock_. + size_t history_size_; // guarded by lock_. + int next_event_id_; // guarded by lock_. + base::Lock lock_; + + DISALLOW_COPY_AND_ASSIGN(EventLogger); +}; + +} // namespace drive + +#endif // COMPONENTS_DRIVE_EVENT_LOGGER_H_ diff --git a/components/drive/event_logger_unittest.cc b/components/drive/event_logger_unittest.cc new file mode 100644 index 0000000..9d3b1fd --- /dev/null +++ b/components/drive/event_logger_unittest.cc @@ -0,0 +1,43 @@ +// 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/event_logger.h" + +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { + +TEST(EventLoggerTest, BasicLogging) { + EventLogger logger; + logger.SetHistorySize(3); // At most 3 events are kept. + EXPECT_EQ(0U, logger.GetHistory().size()); + + logger.Log(logging::LOG_INFO, "first"); + logger.Log(logging::LOG_INFO, "%dnd", 2); + logger.Log(logging::LOG_INFO, "third"); + + // Events are recorded in the chronological order with sequential IDs. + std::vector<EventLogger::Event> history = logger.GetHistory(); + ASSERT_EQ(3U, history.size()); + EXPECT_EQ(0, history[0].id); + EXPECT_EQ("first", history[0].what); + EXPECT_EQ(1, history[1].id); + EXPECT_EQ("2nd", history[1].what); + EXPECT_EQ(2, history[2].id); + EXPECT_EQ("third", history[2].what); + + logger.Log(logging::LOG_INFO, "fourth"); + // It does not log events beyond the specified. + history = logger.GetHistory(); + ASSERT_EQ(3U, history.size()); + // The oldest events is pushed out. + EXPECT_EQ(1, history[0].id); + EXPECT_EQ("2nd", history[0].what); + EXPECT_EQ(2, history[1].id); + EXPECT_EQ("third", history[1].what); + EXPECT_EQ(3, history[2].id); + EXPECT_EQ("fourth", history[2].what); +} + +} // namespace drive diff --git a/components/drive/service/drive_api_service.cc b/components/drive/service/drive_api_service.cc new file mode 100644 index 0000000..644079a --- /dev/null +++ b/components/drive/service/drive_api_service.cc @@ -0,0 +1,872 @@ +// 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/service/drive_api_service.h" + +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/strings/stringprintf.h" +#include "components/drive/drive_api_util.h" +#include "google_apis/drive/auth_service.h" +#include "google_apis/drive/base_requests.h" +#include "google_apis/drive/drive_api_parser.h" +#include "google_apis/drive/drive_api_requests.h" +#include "google_apis/drive/files_list_request_runner.h" +#include "google_apis/drive/request_sender.h" +#include "google_apis/google_api_keys.h" +#include "net/url_request/url_request_context_getter.h" + +using google_apis::AboutResourceCallback; +using google_apis::AppList; +using google_apis::AppListCallback; +using google_apis::AuthStatusCallback; +using google_apis::AuthorizeAppCallback; +using google_apis::CancelCallback; +using google_apis::ChangeList; +using google_apis::ChangeListCallback; +using google_apis::DownloadActionCallback; +using google_apis::EntryActionCallback; +using google_apis::FileList; +using google_apis::FileListCallback; +using google_apis::FileResource; +using google_apis::FileResourceCallback; +using google_apis::DRIVE_OTHER_ERROR; +using google_apis::DRIVE_PARSE_ERROR; +using google_apis::DriveApiErrorCode; +using google_apis::GetContentCallback; +using google_apis::GetShareUrlCallback; +using google_apis::HTTP_NOT_IMPLEMENTED; +using google_apis::HTTP_SUCCESS; +using google_apis::InitiateUploadCallback; +using google_apis::ProgressCallback; +using google_apis::RequestSender; +using google_apis::FilesListRequestRunner; +using google_apis::UploadRangeResponse; +using google_apis::drive::AboutGetRequest; +using google_apis::drive::AppsListRequest; +using google_apis::drive::ChangesListRequest; +using google_apis::drive::ChangesListNextPageRequest; +using google_apis::drive::ChildrenDeleteRequest; +using google_apis::drive::ChildrenInsertRequest; +using google_apis::drive::DownloadFileRequest; +using google_apis::drive::FilesCopyRequest; +using google_apis::drive::FilesGetRequest; +using google_apis::drive::FilesInsertRequest; +using google_apis::drive::FilesPatchRequest; +using google_apis::drive::FilesListRequest; +using google_apis::drive::FilesListNextPageRequest; +using google_apis::drive::FilesDeleteRequest; +using google_apis::drive::FilesTrashRequest; +using google_apis::drive::GetUploadStatusRequest; +using google_apis::drive::InitiateUploadExistingFileRequest; +using google_apis::drive::InitiateUploadNewFileRequest; +using google_apis::drive::ResumeUploadRequest; +using google_apis::drive::UploadRangeCallback; + +namespace drive { + +namespace { + +// OAuth2 scopes for Drive API. +const char kDriveScope[] = "https://www.googleapis.com/auth/drive"; +const char kDriveAppsReadonlyScope[] = + "https://www.googleapis.com/auth/drive.apps.readonly"; +const char kDriveAppsScope[] = "https://www.googleapis.com/auth/drive.apps"; +const char kDocsListScope[] = "https://docs.google.com/feeds/"; + +// Mime type to create a directory. +const char kFolderMimeType[] = "application/vnd.google-apps.folder"; + +// Max number of file entries to be fetched in a single http request. +// +// The larger the number is, +// - The total running time to fetch the whole file list will become shorter. +// - The running time for a single request tends to become longer. +// Since the file list fetching is a completely background task, for our side, +// only the total time matters. However, the server seems to have a time limit +// per single request, which disables us to set the largest value (1000). +// TODO(kinaba): make it larger when the server gets faster. +const int kMaxNumFilesResourcePerRequest = 300; +const int kMaxNumFilesResourcePerRequestForSearch = 100; + +// For performance, we declare all fields we use. +const char kAboutResourceFields[] = + "kind,quotaBytesTotal,quotaBytesUsedAggregate,largestChangeId,rootFolderId"; +const char kFileResourceFields[] = + "kind,id,title,createdDate,sharedWithMeDate,mimeType," + "md5Checksum,fileSize,labels/trashed,imageMediaMetadata/width," + "imageMediaMetadata/height,imageMediaMetadata/rotation,etag," + "parents(id,parentLink),alternateLink," + "modifiedDate,lastViewedByMeDate,shared"; +const char kFileResourceOpenWithLinksFields[] = + "kind,id,openWithLinks/*"; +const char kFileResourceShareLinkFields[] = + "kind,id,shareLink"; +const char kFileListFields[] = + "kind,items(kind,id,title,createdDate,sharedWithMeDate," + "mimeType,md5Checksum,fileSize,labels/trashed,imageMediaMetadata/width," + "imageMediaMetadata/height,imageMediaMetadata/rotation,etag," + "parents(id,parentLink),alternateLink," + "modifiedDate,lastViewedByMeDate,shared),nextLink"; +const char kChangeListFields[] = + "kind,items(file(kind,id,title,createdDate,sharedWithMeDate," + "mimeType,md5Checksum,fileSize,labels/trashed,imageMediaMetadata/width," + "imageMediaMetadata/height,imageMediaMetadata/rotation,etag," + "parents(id,parentLink),alternateLink,modifiedDate," + "lastViewedByMeDate,shared),deleted,id,fileId,modificationDate),nextLink," + "largestChangeId"; + +void ExtractOpenUrlAndRun(const std::string& app_id, + const AuthorizeAppCallback& callback, + DriveApiErrorCode error, + scoped_ptr<FileResource> value) { + DCHECK(!callback.is_null()); + + if (!value) { + callback.Run(error, GURL()); + return; + } + + const std::vector<FileResource::OpenWithLink>& open_with_links = + value->open_with_links(); + for (size_t i = 0; i < open_with_links.size(); ++i) { + if (open_with_links[i].app_id == app_id) { + callback.Run(HTTP_SUCCESS, open_with_links[i].open_url); + return; + } + } + + // Not found. + callback.Run(DRIVE_OTHER_ERROR, GURL()); +} + +void ExtractShareUrlAndRun(const GetShareUrlCallback& callback, + DriveApiErrorCode error, + scoped_ptr<FileResource> value) { + callback.Run(error, value ? value->share_link() : GURL()); +} + +// Ignores the |entry|, and runs the |callback|. +void EntryActionCallbackAdapter( + const EntryActionCallback& callback, + DriveApiErrorCode error, scoped_ptr<FileResource> entry) { + callback.Run(error); +} + +// The resource ID for the root directory for Drive API is defined in the spec: +// https://developers.google.com/drive/folder +const char kDriveApiRootDirectoryResourceId[] = "root"; + +} // namespace + +BatchRequestConfigurator::BatchRequestConfigurator( + const base::WeakPtr<google_apis::drive::BatchUploadRequest>& batch_request, + base::SequencedTaskRunner* task_runner, + const google_apis::DriveApiUrlGenerator& url_generator, + const google_apis::CancelCallback& cancel_callback) + : batch_request_(batch_request), + task_runner_(task_runner), + url_generator_(url_generator), + cancel_callback_(cancel_callback) { +} + +BatchRequestConfigurator::~BatchRequestConfigurator() { + // The batch requst has not been committed. + if (batch_request_) + cancel_callback_.Run(); +} + +google_apis::CancelCallback BatchRequestConfigurator::MultipartUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const base::FilePath& local_file_path, + const UploadNewFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) { + DCHECK(CalledOnValidThread()); + DCHECK(!callback.is_null()); + + scoped_ptr<google_apis::BatchableDelegate> delegate( + new google_apis::drive::MultipartUploadNewFileDelegate( + task_runner_.get(), title, parent_resource_id, content_type, + content_length, options.modified_date, options.last_viewed_by_me_date, + local_file_path, options.properties, url_generator_, callback, + progress_callback)); + // Batch request can be null when pre-authorization for the requst is failed + // in request sender. + if (batch_request_) + batch_request_->AddRequest(delegate.release()); + else + delegate->NotifyError(DRIVE_OTHER_ERROR); + return cancel_callback_; +} + +google_apis::CancelCallback +BatchRequestConfigurator::MultipartUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const base::FilePath& local_file_path, + const UploadExistingFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) { + DCHECK(CalledOnValidThread()); + DCHECK(!callback.is_null()); + + scoped_ptr<google_apis::BatchableDelegate> delegate( + new google_apis::drive::MultipartUploadExistingFileDelegate( + task_runner_.get(), options.title, resource_id, + options.parent_resource_id, content_type, content_length, + options.modified_date, options.last_viewed_by_me_date, + local_file_path, options.etag, options.properties, url_generator_, + callback, progress_callback)); + // Batch request can be null when pre-authorization for the requst is failed + // in request sender. + if (batch_request_) + batch_request_->AddRequest(delegate.release()); + else + delegate->NotifyError(DRIVE_OTHER_ERROR); + return cancel_callback_; +} + +void BatchRequestConfigurator::Commit() { + DCHECK(CalledOnValidThread()); + if (!batch_request_) + return; + batch_request_->Commit(); + batch_request_.reset(); +} + +DriveAPIService::DriveAPIService( + OAuth2TokenService* oauth2_token_service, + net::URLRequestContextGetter* url_request_context_getter, + base::SequencedTaskRunner* blocking_task_runner, + const GURL& base_url, + const GURL& base_download_url, + const std::string& custom_user_agent) + : oauth2_token_service_(oauth2_token_service), + url_request_context_getter_(url_request_context_getter), + blocking_task_runner_(blocking_task_runner), + url_generator_(base_url, base_download_url), + custom_user_agent_(custom_user_agent) { +} + +DriveAPIService::~DriveAPIService() { + DCHECK(thread_checker_.CalledOnValidThread()); + if (sender_.get()) + sender_->auth_service()->RemoveObserver(this); +} + +void DriveAPIService::Initialize(const std::string& account_id) { + DCHECK(thread_checker_.CalledOnValidThread()); + + std::vector<std::string> scopes; + scopes.push_back(kDriveScope); + scopes.push_back(kDriveAppsReadonlyScope); + scopes.push_back(kDriveAppsScope); + + // Note: The following scope is used to support GetShareUrl on Drive API v2. + // Unfortunately, there is no support on Drive API v2, so we need to fall back + // to GData WAPI for the GetShareUrl. + scopes.push_back(kDocsListScope); + + sender_.reset(new RequestSender( + new google_apis::AuthService(oauth2_token_service_, + account_id, + url_request_context_getter_.get(), + scopes), + url_request_context_getter_.get(), + blocking_task_runner_.get(), + custom_user_agent_)); + sender_->auth_service()->AddObserver(this); + + files_list_request_runner_.reset( + new FilesListRequestRunner(sender_.get(), url_generator_)); +} + +void DriveAPIService::AddObserver(DriveServiceObserver* observer) { + observers_.AddObserver(observer); +} + +void DriveAPIService::RemoveObserver(DriveServiceObserver* observer) { + observers_.RemoveObserver(observer); +} + +bool DriveAPIService::CanSendRequest() const { + DCHECK(thread_checker_.CalledOnValidThread()); + + return HasRefreshToken(); +} + +std::string DriveAPIService::GetRootResourceId() const { + return kDriveApiRootDirectoryResourceId; +} + +CancelCallback DriveAPIService::GetAllFileList( + const FileListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FilesListRequest* request = new FilesListRequest( + sender_.get(), url_generator_, callback); + request->set_max_results(kMaxNumFilesResourcePerRequest); + request->set_q("trashed = false"); // Exclude trashed files. + request->set_fields(kFileListFields); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::GetFileListInDirectory( + const std::string& directory_resource_id, + const FileListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!directory_resource_id.empty()); + DCHECK(!callback.is_null()); + + // Because children.list method on Drive API v2 returns only the list of + // children's references, but we need all file resource list. + // So, here we use files.list method instead, with setting parents query. + // After the migration from GData WAPI to Drive API v2, we should clean the + // code up by moving the responsibility to include "parents" in the query + // to client side. + // We aren't interested in files in trash in this context, neither. + return files_list_request_runner_->CreateAndStartWithSizeBackoff( + kMaxNumFilesResourcePerRequest, + base::StringPrintf( + "'%s' in parents and trashed = false", + util::EscapeQueryStringValue(directory_resource_id).c_str()), + kFileListFields, callback); +} + +CancelCallback DriveAPIService::Search( + const std::string& search_query, + const FileListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!search_query.empty()); + DCHECK(!callback.is_null()); + + return files_list_request_runner_->CreateAndStartWithSizeBackoff( + kMaxNumFilesResourcePerRequestForSearch, + util::TranslateQuery(search_query), kFileListFields, callback); +} + +CancelCallback DriveAPIService::SearchByTitle( + const std::string& title, + const std::string& directory_resource_id, + const FileListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!title.empty()); + DCHECK(!callback.is_null()); + + std::string query; + base::StringAppendF(&query, "title = '%s'", + util::EscapeQueryStringValue(title).c_str()); + if (!directory_resource_id.empty()) { + base::StringAppendF( + &query, " and '%s' in parents", + util::EscapeQueryStringValue(directory_resource_id).c_str()); + } + query += " and trashed = false"; + + FilesListRequest* request = new FilesListRequest( + sender_.get(), url_generator_, callback); + request->set_max_results(kMaxNumFilesResourcePerRequest); + request->set_q(query); + request->set_fields(kFileListFields); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::GetChangeList( + int64 start_changestamp, + const ChangeListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + ChangesListRequest* request = new ChangesListRequest( + sender_.get(), url_generator_, callback); + request->set_max_results(kMaxNumFilesResourcePerRequest); + request->set_start_change_id(start_changestamp); + request->set_fields(kChangeListFields); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::GetRemainingChangeList( + const GURL& next_link, + const ChangeListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!next_link.is_empty()); + DCHECK(!callback.is_null()); + + ChangesListNextPageRequest* request = new ChangesListNextPageRequest( + sender_.get(), callback); + request->set_next_link(next_link); + request->set_fields(kChangeListFields); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::GetRemainingFileList( + const GURL& next_link, + const FileListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!next_link.is_empty()); + DCHECK(!callback.is_null()); + + FilesListNextPageRequest* request = new FilesListNextPageRequest( + sender_.get(), callback); + request->set_next_link(next_link); + request->set_fields(kFileListFields); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::GetFileResource( + const std::string& resource_id, + const FileResourceCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FilesGetRequest* request = new FilesGetRequest( + sender_.get(), url_generator_, google_apis::IsGoogleChromeAPIKeyUsed(), + callback); + request->set_file_id(resource_id); + request->set_fields(kFileResourceFields); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::GetShareUrl( + const std::string& resource_id, + const GURL& embed_origin, + const GetShareUrlCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (!google_apis::IsGoogleChromeAPIKeyUsed()) { + LOG(ERROR) << "Only the official build of Chrome OS can open share dialogs " + << "from the file manager."; + } + + FilesGetRequest* request = new FilesGetRequest( + sender_.get(), url_generator_, google_apis::IsGoogleChromeAPIKeyUsed(), + base::Bind(&ExtractShareUrlAndRun, callback)); + request->set_file_id(resource_id); + request->set_fields(kFileResourceShareLinkFields); + request->set_embed_origin(embed_origin); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::GetAboutResource( + const AboutResourceCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + AboutGetRequest* request = + new AboutGetRequest(sender_.get(), url_generator_, callback); + request->set_fields(kAboutResourceFields); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::GetAppList(const AppListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + return sender_->StartRequestWithAuthRetry( + new AppsListRequest(sender_.get(), url_generator_, + google_apis::IsGoogleChromeAPIKeyUsed(), callback)); +} + +CancelCallback DriveAPIService::DownloadFile( + const base::FilePath& local_cache_path, + const std::string& resource_id, + const DownloadActionCallback& download_action_callback, + const GetContentCallback& get_content_callback, + const ProgressCallback& progress_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!download_action_callback.is_null()); + // get_content_callback may be null. + + return sender_->StartRequestWithAuthRetry(new DownloadFileRequest( + sender_.get(), url_generator_, resource_id, local_cache_path, + download_action_callback, get_content_callback, progress_callback)); +} + +CancelCallback DriveAPIService::DeleteResource( + const std::string& resource_id, + const std::string& etag, + const EntryActionCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FilesDeleteRequest* request = new FilesDeleteRequest( + sender_.get(), url_generator_, callback); + request->set_file_id(resource_id); + request->set_etag(etag); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::TrashResource( + const std::string& resource_id, + const EntryActionCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FilesTrashRequest* request = new FilesTrashRequest( + sender_.get(), url_generator_, + base::Bind(&EntryActionCallbackAdapter, callback)); + request->set_file_id(resource_id); + request->set_fields(kFileResourceFields); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::AddNewDirectory( + const std::string& parent_resource_id, + const std::string& directory_title, + const AddNewDirectoryOptions& options, + const FileResourceCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FilesInsertRequest* request = new FilesInsertRequest( + sender_.get(), url_generator_, callback); + request->set_last_viewed_by_me_date(options.last_viewed_by_me_date); + request->set_mime_type(kFolderMimeType); + request->set_modified_date(options.modified_date); + request->add_parent(parent_resource_id); + request->set_title(directory_title); + request->set_properties(options.properties); + request->set_fields(kFileResourceFields); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::CopyResource( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const FileResourceCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FilesCopyRequest* request = new FilesCopyRequest( + sender_.get(), url_generator_, callback); + request->set_file_id(resource_id); + request->add_parent(parent_resource_id); + request->set_title(new_title); + request->set_modified_date(last_modified); + request->set_fields(kFileResourceFields); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::UpdateResource( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const base::Time& last_viewed_by_me, + const google_apis::drive::Properties& properties, + const FileResourceCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FilesPatchRequest* request = new FilesPatchRequest( + sender_.get(), url_generator_, callback); + request->set_file_id(resource_id); + request->set_title(new_title); + if (!parent_resource_id.empty()) + request->add_parent(parent_resource_id); + if (!last_modified.is_null()) { + // Need to set setModifiedDate to true to overwrite modifiedDate. + request->set_set_modified_date(true); + request->set_modified_date(last_modified); + } + if (!last_viewed_by_me.is_null()) { + // Need to set updateViewedDate to false, otherwise the lastViewedByMeDate + // will be set to the request time (not the specified time via request). + request->set_update_viewed_date(false); + request->set_last_viewed_by_me_date(last_viewed_by_me); + } + request->set_fields(kFileResourceFields); + request->set_properties(properties); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::AddResourceToDirectory( + const std::string& parent_resource_id, + const std::string& resource_id, + const EntryActionCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + ChildrenInsertRequest* request = + new ChildrenInsertRequest(sender_.get(), url_generator_, callback); + request->set_folder_id(parent_resource_id); + request->set_id(resource_id); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::RemoveResourceFromDirectory( + const std::string& parent_resource_id, + const std::string& resource_id, + const EntryActionCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + ChildrenDeleteRequest* request = + new ChildrenDeleteRequest(sender_.get(), url_generator_, callback); + request->set_child_id(resource_id); + request->set_folder_id(parent_resource_id); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::InitiateUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const UploadNewFileOptions& options, + const InitiateUploadCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + InitiateUploadNewFileRequest* request = + new InitiateUploadNewFileRequest(sender_.get(), + url_generator_, + content_type, + content_length, + parent_resource_id, + title, + callback); + request->set_modified_date(options.modified_date); + request->set_last_viewed_by_me_date(options.last_viewed_by_me_date); + request->set_properties(options.properties); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::InitiateUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const UploadExistingFileOptions& options, + const InitiateUploadCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + InitiateUploadExistingFileRequest* request = + new InitiateUploadExistingFileRequest(sender_.get(), + url_generator_, + content_type, + content_length, + resource_id, + options.etag, + callback); + request->set_parent_resource_id(options.parent_resource_id); + request->set_title(options.title); + request->set_modified_date(options.modified_date); + request->set_last_viewed_by_me_date(options.last_viewed_by_me_date); + request->set_properties(options.properties); + return sender_->StartRequestWithAuthRetry(request); +} + +CancelCallback DriveAPIService::ResumeUpload( + const GURL& upload_url, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path, + const UploadRangeCallback& callback, + const ProgressCallback& progress_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + return sender_->StartRequestWithAuthRetry(new ResumeUploadRequest( + sender_.get(), upload_url, start_position, end_position, content_length, + content_type, local_file_path, callback, progress_callback)); +} + +CancelCallback DriveAPIService::GetUploadStatus( + const GURL& upload_url, + int64 content_length, + const UploadRangeCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + return sender_->StartRequestWithAuthRetry(new GetUploadStatusRequest( + sender_.get(), upload_url, content_length, callback)); +} + +CancelCallback DriveAPIService::MultipartUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const base::FilePath& local_file_path, + const drive::UploadNewFileOptions& options, + const FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + return sender_->StartRequestWithAuthRetry( + new google_apis::drive::SingleBatchableDelegateRequest( + sender_.get(), + new google_apis::drive::MultipartUploadNewFileDelegate( + sender_->blocking_task_runner(), title, parent_resource_id, + content_type, content_length, options.modified_date, + options.last_viewed_by_me_date, local_file_path, + options.properties, url_generator_, callback, + progress_callback))); +} + +CancelCallback DriveAPIService::MultipartUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const base::FilePath& local_file_path, + const drive::UploadExistingFileOptions& options, + const FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + return sender_->StartRequestWithAuthRetry( + new google_apis::drive::SingleBatchableDelegateRequest( + sender_.get(), + new google_apis::drive::MultipartUploadExistingFileDelegate( + sender_->blocking_task_runner(), options.title, resource_id, + options.parent_resource_id, content_type, content_length, + options.modified_date, options.last_viewed_by_me_date, + local_file_path, options.etag, options.properties, url_generator_, + callback, progress_callback))); +} + +CancelCallback DriveAPIService::AuthorizeApp( + const std::string& resource_id, + const std::string& app_id, + const AuthorizeAppCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + // Files.Authorize is only available for whitelisted clients like official + // Google Chrome. In other cases, we fall back to Files.Get that returns the + // same value as Files.Authorize without doing authorization. In that case, + // the app can open if it was authorized by other means (from whitelisted + // clients or drive.google.com web UI.) + if (google_apis::IsGoogleChromeAPIKeyUsed()) { + google_apis::drive::FilesAuthorizeRequest* request = + new google_apis::drive::FilesAuthorizeRequest( + sender_.get(), url_generator_, + base::Bind(&ExtractOpenUrlAndRun, app_id, callback)); + request->set_app_id(app_id); + request->set_file_id(resource_id); + request->set_fields(kFileResourceOpenWithLinksFields); + return sender_->StartRequestWithAuthRetry(request); + } else { + FilesGetRequest* request = new FilesGetRequest( + sender_.get(), url_generator_, google_apis::IsGoogleChromeAPIKeyUsed(), + base::Bind(&ExtractOpenUrlAndRun, app_id, callback)); + request->set_file_id(resource_id); + request->set_fields(kFileResourceOpenWithLinksFields); + return sender_->StartRequestWithAuthRetry(request); + } +} + +CancelCallback DriveAPIService::UninstallApp( + const std::string& app_id, + const google_apis::EntryActionCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + google_apis::drive::AppsDeleteRequest* request = + new google_apis::drive::AppsDeleteRequest(sender_.get(), url_generator_, + callback); + request->set_app_id(app_id); + return sender_->StartRequestWithAuthRetry(request); +} + +google_apis::CancelCallback DriveAPIService::AddPermission( + const std::string& resource_id, + const std::string& email, + google_apis::drive::PermissionRole role, + const google_apis::EntryActionCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + google_apis::drive::PermissionsInsertRequest* request = + new google_apis::drive::PermissionsInsertRequest(sender_.get(), + url_generator_, + callback); + request->set_id(resource_id); + request->set_role(role); + request->set_type(google_apis::drive::PERMISSION_TYPE_USER); + request->set_value(email); + return sender_->StartRequestWithAuthRetry(request); +} + +bool DriveAPIService::HasAccessToken() const { + DCHECK(thread_checker_.CalledOnValidThread()); + return sender_->auth_service()->HasAccessToken(); +} + +void DriveAPIService::RequestAccessToken(const AuthStatusCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + const std::string access_token = sender_->auth_service()->access_token(); + if (!access_token.empty()) { + callback.Run(google_apis::HTTP_NOT_MODIFIED, access_token); + return; + } + + // Retrieve the new auth token. + sender_->auth_service()->StartAuthentication(callback); +} + +bool DriveAPIService::HasRefreshToken() const { + DCHECK(thread_checker_.CalledOnValidThread()); + return sender_->auth_service()->HasRefreshToken(); +} + +void DriveAPIService::ClearAccessToken() { + DCHECK(thread_checker_.CalledOnValidThread()); + sender_->auth_service()->ClearAccessToken(); +} + +void DriveAPIService::ClearRefreshToken() { + DCHECK(thread_checker_.CalledOnValidThread()); + sender_->auth_service()->ClearRefreshToken(); +} + +void DriveAPIService::OnOAuth2RefreshTokenChanged() { + DCHECK(thread_checker_.CalledOnValidThread()); + if (CanSendRequest()) { + FOR_EACH_OBSERVER( + DriveServiceObserver, observers_, OnReadyToSendRequests()); + } else if (!HasRefreshToken()) { + FOR_EACH_OBSERVER( + DriveServiceObserver, observers_, OnRefreshTokenInvalid()); + } +} + +scoped_ptr<BatchRequestConfiguratorInterface> +DriveAPIService::StartBatchRequest() { + scoped_ptr<google_apis::drive::BatchUploadRequest> request( + new google_apis::drive::BatchUploadRequest(sender_.get(), + url_generator_)); + const base::WeakPtr<google_apis::drive::BatchUploadRequest> weak_ref = + request->GetWeakPtrAsBatchUploadRequest(); + // Have sender_ manage the lifetime of the request. + // TODO(hirono): Currently we need to pass the ownership of the request to + // RequestSender before the request is committed because the request has a + // reference to RequestSender and we should ensure to delete the request when + // the sender is deleted. Resolve the circulating dependency and fix it. + const google_apis::CancelCallback callback = + sender_->StartRequestWithAuthRetry(request.release()); + return make_scoped_ptr<BatchRequestConfiguratorInterface>( + new BatchRequestConfigurator(weak_ref, sender_->blocking_task_runner(), + url_generator_, callback)); +} + +} // namespace drive diff --git a/components/drive/service/drive_api_service.h b/components/drive/service/drive_api_service.h new file mode 100644 index 0000000..22bb7d4 --- /dev/null +++ b/components/drive/service/drive_api_service.h @@ -0,0 +1,269 @@ +// 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_SERVICE_DRIVE_API_SERVICE_H_ +#define COMPONENTS_DRIVE_SERVICE_DRIVE_API_SERVICE_H_ + +#include <string> + +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "base/threading/thread_checker.h" +#include "components/drive/service/drive_service_interface.h" +#include "google_apis/drive/auth_service_interface.h" +#include "google_apis/drive/auth_service_observer.h" +#include "google_apis/drive/drive_api_url_generator.h" + +class GURL; +class OAuth2TokenService; + +namespace base { +class FilePath; +class SequencedTaskRunner; +} + +namespace google_apis { +class FilesListRequestRunner; +class RequestSender; +namespace drive { +class BatchUploadRequest; +} // namespace drive +} // namespace google_apis + +namespace net { +class URLRequestContextGetter; +} // namespace net + +namespace drive { + +// Builder for batch request returned by |DriveAPIService|. +class BatchRequestConfigurator : public BatchRequestConfiguratorInterface, + public base::NonThreadSafe { + public: + BatchRequestConfigurator( + const base::WeakPtr<google_apis::drive::BatchUploadRequest>& + batch_request, + base::SequencedTaskRunner* task_runner, + const google_apis::DriveApiUrlGenerator& url_generator, + const google_apis::CancelCallback& cancel_callback); + ~BatchRequestConfigurator() override; + + // BatchRequestConfiguratorInterface overrides. + google_apis::CancelCallback MultipartUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const base::FilePath& local_file_path, + const UploadNewFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) override; + google_apis::CancelCallback MultipartUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const base::FilePath& local_file_path, + const UploadExistingFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) override; + void Commit() override; + + private: + // Reference to batch request. It turns to null after committing. + base::WeakPtr<google_apis::drive::BatchUploadRequest> batch_request_; + scoped_refptr<base::SequencedTaskRunner> task_runner_; + google_apis::DriveApiUrlGenerator url_generator_; + google_apis::CancelCallback cancel_callback_; + + DISALLOW_COPY_AND_ASSIGN(BatchRequestConfigurator); +}; + +// This class provides Drive request calls using Drive V2 API. +// Details of API call are abstracted in each request class and this class +// works as a thin wrapper for the API. +class DriveAPIService : public DriveServiceInterface, + public google_apis::AuthServiceObserver { + public: + // |oauth2_token_service| is used for obtaining OAuth2 access tokens. + // |url_request_context_getter| is used to initialize URLFetcher. + // |blocking_task_runner| is used to run blocking tasks (like parsing JSON). + // |base_url| is used to generate URLs for communication with the drive API. + // |base_download_url| is used to generate URLs for downloading file from the + // drive API. + // |custom_user_agent| will be used for the User-Agent header in HTTP + // requests issues through the service if the value is not empty. + DriveAPIService( + OAuth2TokenService* oauth2_token_service, + net::URLRequestContextGetter* url_request_context_getter, + base::SequencedTaskRunner* blocking_task_runner, + const GURL& base_url, + const GURL& base_download_url, + const std::string& custom_user_agent); + ~DriveAPIService() override; + + // DriveServiceInterface Overrides + void Initialize(const std::string& account_id) override; + void AddObserver(DriveServiceObserver* observer) override; + void RemoveObserver(DriveServiceObserver* observer) override; + bool CanSendRequest() const override; + bool HasAccessToken() const override; + void RequestAccessToken( + const google_apis::AuthStatusCallback& callback) override; + bool HasRefreshToken() const override; + void ClearAccessToken() override; + void ClearRefreshToken() override; + std::string GetRootResourceId() const override; + google_apis::CancelCallback GetAllFileList( + const google_apis::FileListCallback& callback) override; + google_apis::CancelCallback GetFileListInDirectory( + const std::string& directory_resource_id, + const google_apis::FileListCallback& callback) override; + google_apis::CancelCallback Search( + const std::string& search_query, + const google_apis::FileListCallback& callback) override; + google_apis::CancelCallback SearchByTitle( + const std::string& title, + const std::string& directory_resource_id, + const google_apis::FileListCallback& callback) override; + google_apis::CancelCallback GetChangeList( + int64 start_changestamp, + const google_apis::ChangeListCallback& callback) override; + google_apis::CancelCallback GetRemainingChangeList( + const GURL& next_link, + const google_apis::ChangeListCallback& callback) override; + google_apis::CancelCallback GetRemainingFileList( + const GURL& next_link, + const google_apis::FileListCallback& callback) override; + google_apis::CancelCallback GetFileResource( + const std::string& resource_id, + const google_apis::FileResourceCallback& callback) override; + google_apis::CancelCallback GetShareUrl( + const std::string& resource_id, + const GURL& embed_origin, + const google_apis::GetShareUrlCallback& callback) override; + google_apis::CancelCallback GetAboutResource( + const google_apis::AboutResourceCallback& callback) override; + google_apis::CancelCallback GetAppList( + const google_apis::AppListCallback& callback) override; + google_apis::CancelCallback DeleteResource( + const std::string& resource_id, + const std::string& etag, + const google_apis::EntryActionCallback& callback) override; + google_apis::CancelCallback TrashResource( + const std::string& resource_id, + const google_apis::EntryActionCallback& callback) override; + google_apis::CancelCallback DownloadFile( + const base::FilePath& local_cache_path, + const std::string& resource_id, + const google_apis::DownloadActionCallback& download_action_callback, + const google_apis::GetContentCallback& get_content_callback, + const google_apis::ProgressCallback& progress_callback) override; + google_apis::CancelCallback CopyResource( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const google_apis::FileResourceCallback& callback) override; + google_apis::CancelCallback UpdateResource( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const base::Time& last_viewed_by_me, + const google_apis::drive::Properties& properties, + const google_apis::FileResourceCallback& callback) override; + google_apis::CancelCallback AddResourceToDirectory( + const std::string& parent_resource_id, + const std::string& resource_id, + const google_apis::EntryActionCallback& callback) override; + google_apis::CancelCallback RemoveResourceFromDirectory( + const std::string& parent_resource_id, + const std::string& resource_id, + const google_apis::EntryActionCallback& callback) override; + google_apis::CancelCallback AddNewDirectory( + const std::string& parent_resource_id, + const std::string& directory_title, + const AddNewDirectoryOptions& options, + const google_apis::FileResourceCallback& callback) override; + google_apis::CancelCallback InitiateUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const UploadNewFileOptions& options, + const google_apis::InitiateUploadCallback& callback) override; + google_apis::CancelCallback InitiateUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const UploadExistingFileOptions& options, + const google_apis::InitiateUploadCallback& callback) override; + google_apis::CancelCallback ResumeUpload( + const GURL& upload_url, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path, + const google_apis::drive::UploadRangeCallback& callback, + const google_apis::ProgressCallback& progress_callback) override; + google_apis::CancelCallback GetUploadStatus( + const GURL& upload_url, + int64 content_length, + const google_apis::drive::UploadRangeCallback& callback) override; + google_apis::CancelCallback MultipartUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const base::FilePath& local_file_path, + const drive::UploadNewFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) override; + google_apis::CancelCallback MultipartUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const base::FilePath& local_file_path, + const drive::UploadExistingFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) override; + google_apis::CancelCallback AuthorizeApp( + const std::string& resource_id, + const std::string& app_id, + const google_apis::AuthorizeAppCallback& callback) override; + google_apis::CancelCallback UninstallApp( + const std::string& app_id, + const google_apis::EntryActionCallback& callback) override; + google_apis::CancelCallback AddPermission( + const std::string& resource_id, + const std::string& email, + google_apis::drive::PermissionRole role, + const google_apis::EntryActionCallback& callback) override; + scoped_ptr<BatchRequestConfiguratorInterface> StartBatchRequest() override; + + private: + // AuthServiceObserver override. + void OnOAuth2RefreshTokenChanged() override; + + // The class is expected to run on UI thread. + base::ThreadChecker thread_checker_; + + OAuth2TokenService* oauth2_token_service_; + scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_; + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; + scoped_ptr<google_apis::RequestSender> sender_; + scoped_ptr<google_apis::FilesListRequestRunner> files_list_request_runner_; + base::ObserverList<DriveServiceObserver> observers_; + google_apis::DriveApiUrlGenerator url_generator_; + const std::string custom_user_agent_; + + DISALLOW_COPY_AND_ASSIGN(DriveAPIService); +}; + +} // namespace drive + +#endif // COMPONENTS_DRIVE_SERVICE_DRIVE_API_SERVICE_H_ diff --git a/components/drive/service/drive_api_service_unittest.cc b/components/drive/service/drive_api_service_unittest.cc new file mode 100644 index 0000000..6655e68 --- /dev/null +++ b/components/drive/service/drive_api_service_unittest.cc @@ -0,0 +1,79 @@ +// 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 "base/message_loop/message_loop.h" +#include "base/test/test_simple_task_runner.h" +#include "components/drive/service/drive_api_service.h" +#include "google_apis/drive/dummy_auth_service.h" +#include "google_apis/drive/request_sender.h" +#include "google_apis/drive/test_util.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace drive { +namespace { +const char kTestUserAgent[] = "test-user-agent"; +} + +class TestAuthService : public google_apis::DummyAuthService { + public: + void StartAuthentication( + const google_apis::AuthStatusCallback& callback) override { + callback_ = callback; + } + + bool HasAccessToken() const override { return false; } + + void SendHttpError() { + ASSERT_FALSE(callback_.is_null()); + callback_.Run(google_apis::HTTP_UNAUTHORIZED, ""); + } + + private: + google_apis::AuthStatusCallback callback_; +}; + +TEST(DriveAPIServiceTest, BatchRequestConfiguratorWithAuthFailure) { + const GURL test_base_url("http://localhost/"); + google_apis::DriveApiUrlGenerator url_generator(test_base_url, test_base_url); + scoped_refptr<base::TestSimpleTaskRunner> task_runner = + new base::TestSimpleTaskRunner(); + scoped_refptr<net::TestURLRequestContextGetter> request_context_getter = + new net::TestURLRequestContextGetter(task_runner.get()); + google_apis::RequestSender sender(new TestAuthService, + request_context_getter.get(), + task_runner.get(), kTestUserAgent); + google_apis::drive::BatchUploadRequest* const request = + new google_apis::drive::BatchUploadRequest(&sender, url_generator); + sender.StartRequestWithAuthRetry(request); + BatchRequestConfigurator configurator( + request->GetWeakPtrAsBatchUploadRequest(), task_runner.get(), + url_generator, google_apis::CancelCallback()); + static_cast<TestAuthService*>(sender.auth_service())->SendHttpError(); + + { + google_apis::DriveApiErrorCode error = google_apis::HTTP_SUCCESS; + scoped_ptr<google_apis::FileResource> file_resource; + configurator.MultipartUploadNewFile( + "text/plain", 10, "", "title", + base::FilePath(FILE_PATH_LITERAL("/file")), UploadNewFileOptions(), + google_apis::test_util::CreateCopyResultCallback(&error, + &file_resource), + google_apis::ProgressCallback()); + EXPECT_EQ(google_apis::DRIVE_OTHER_ERROR, error); + } + { + google_apis::DriveApiErrorCode error = google_apis::HTTP_SUCCESS; + scoped_ptr<google_apis::FileResource> file_resource; + configurator.MultipartUploadExistingFile( + "text/plain", 10, "resource_id", + base::FilePath(FILE_PATH_LITERAL("/file")), UploadExistingFileOptions(), + google_apis::test_util::CreateCopyResultCallback(&error, + &file_resource), + google_apis::ProgressCallback()); + EXPECT_EQ(google_apis::DRIVE_OTHER_ERROR, error); + } +} + +} // namespace drive diff --git a/components/drive/service/drive_service_interface.cc b/components/drive/service/drive_service_interface.cc new file mode 100644 index 0000000..76c1f20 --- /dev/null +++ b/components/drive/service/drive_service_interface.cc @@ -0,0 +1,27 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/drive/service/drive_service_interface.h" + +namespace drive { + +AddNewDirectoryOptions::AddNewDirectoryOptions() { +} + +AddNewDirectoryOptions::~AddNewDirectoryOptions() { +} + +UploadNewFileOptions::UploadNewFileOptions() { +} + +UploadNewFileOptions::~UploadNewFileOptions() { +} + +UploadExistingFileOptions::UploadExistingFileOptions() { +} + +UploadExistingFileOptions::~UploadExistingFileOptions() { +} + +} // namespace drive diff --git a/components/drive/service/drive_service_interface.h b/components/drive/service/drive_service_interface.h new file mode 100644 index 0000000..1c0dadd --- /dev/null +++ b/components/drive/service/drive_service_interface.h @@ -0,0 +1,467 @@ +// 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_SERVICE_DRIVE_SERVICE_INTERFACE_H_ +#define COMPONENTS_DRIVE_SERVICE_DRIVE_SERVICE_INTERFACE_H_ + +#include <string> + +#include "base/time/time.h" +#include "google_apis/drive/auth_service_interface.h" +#include "google_apis/drive/base_requests.h" +#include "google_apis/drive/drive_api_requests.h" +#include "google_apis/drive/drive_common_callbacks.h" + +namespace base { +class Time; +} + +namespace drive { + +// Observer interface for DriveServiceInterface. +class DriveServiceObserver { + public: + // Triggered when the service gets ready to send requests. + virtual void OnReadyToSendRequests() {} + + // Called when the refresh token was found to be invalid. + virtual void OnRefreshTokenInvalid() {} + + protected: + virtual ~DriveServiceObserver() {} +}; + +// Optional parameters for AddNewDirectory(). +struct AddNewDirectoryOptions { + AddNewDirectoryOptions(); + ~AddNewDirectoryOptions(); + + // modified_date of the directory. + // Pass the null Time if you are not interested in setting this property. + base::Time modified_date; + + // last_viewed_by_me_date of the directory. + // Pass the null Time if you are not interested in setting this property. + base::Time last_viewed_by_me_date; + + // List of properties for a new directory. + google_apis::drive::Properties properties; +}; + +// Optional parameters for InitiateUploadNewFile() and +// MultipartUploadNewFile(). +struct UploadNewFileOptions { + UploadNewFileOptions(); + ~UploadNewFileOptions(); + + // modified_date of the file. + // Pass the null Time if you are not interested in setting this property. + base::Time modified_date; + + // last_viewed_by_me_date of the file. + // Pass the null Time if you are not interested in setting this property. + base::Time last_viewed_by_me_date; + + // List of properties for a new file. + google_apis::drive::Properties properties; +}; + +// Optional parameters for InitiateUploadExistingFile() and +// MultipartUploadExistingFile(). +struct UploadExistingFileOptions { + UploadExistingFileOptions(); + ~UploadExistingFileOptions(); + + // Expected ETag of the file. UPLOAD_ERROR_CONFLICT error is generated when + // matching fails. + // Pass the empty string to disable this behavior. + std::string etag; + + // New parent of the file. + // Pass the empty string to keep the property unchanged. + std::string parent_resource_id; + + // New title of the file. + // Pass the empty string to keep the property unchanged. + std::string title; + + // New modified_date of the file. + // Pass the null Time if you are not interested in setting this property. + base::Time modified_date; + + // New last_viewed_by_me_date of the file. + // Pass the null Time if you are not interested in setting this property. + base::Time last_viewed_by_me_date; + + // List of new properties for an existing file (it will be merged with + // existing properties). + google_apis::drive::Properties properties; +}; + +// Interface where we define operations that can be sent in batch requests. +class DriveServiceBatchOperationsInterface { + public: + virtual ~DriveServiceBatchOperationsInterface() {} + + // Uploads a file by a single request with multipart body. It's more efficient + // for small files than using |InitiateUploadNewFile| and |ResumeUpload|. + // |content_type| and |content_length| should be the ones of the file to be + // uploaded. |callback| must not be null. |progress_callback| may be null. + virtual google_apis::CancelCallback MultipartUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const base::FilePath& local_file_path, + const UploadNewFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) = 0; + + // Uploads a file by a single request with multipart body. It's more efficient + // for small files than using |InitiateUploadExistingFile| and |ResumeUpload|. + // |content_type| and |content_length| should be the ones of the file to be + // uploaded. |callback| must not be null. |progress_callback| may be null. + virtual google_apis::CancelCallback MultipartUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const base::FilePath& local_file_path, + const UploadExistingFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) = 0; +}; + +// Builder returned by DriveServiceInterface to build batch request. +class BatchRequestConfiguratorInterface + : public DriveServiceBatchOperationsInterface { + public: + ~BatchRequestConfiguratorInterface() override {} + + // Commits and sends the batch request. + virtual void Commit() = 0; +}; + +// This defines an interface for sharing by DriveService and MockDriveService +// so that we can do testing of clients of DriveService. +// +// All functions must be called on UI thread. DriveService is built on top of +// URLFetcher that runs on UI thread. +class DriveServiceInterface : public DriveServiceBatchOperationsInterface { + public: + ~DriveServiceInterface() override {} + + // Common service: + + // Initializes the documents service with |account_id|. + virtual void Initialize(const std::string& account_id) = 0; + + // Adds an observer. + virtual void AddObserver(DriveServiceObserver* observer) = 0; + + // Removes an observer. + virtual void RemoveObserver(DriveServiceObserver* observer) = 0; + + // True if ready to send requests. + virtual bool CanSendRequest() const = 0; + + // Authentication service: + + // True if OAuth2 access token is retrieved and believed to be fresh. + virtual bool HasAccessToken() const = 0; + + // Gets the cached OAuth2 access token or if empty, then fetches a new one. + virtual void RequestAccessToken( + const google_apis::AuthStatusCallback& callback) = 0; + + // True if OAuth2 refresh token is present. + virtual bool HasRefreshToken() const = 0; + + // Clears OAuth2 access token. + virtual void ClearAccessToken() = 0; + + // Clears OAuth2 refresh token. + virtual void ClearRefreshToken() = 0; + + // Document access: + + // Returns the resource id for the root directory. + virtual std::string GetRootResourceId() const = 0; + + // Fetches a file list of the account. |callback| will be called upon + // completion. + // If the list is too long, it may be paged. In such a case, a URL to fetch + // remaining results will be included in the returned result. See also + // GetRemainingFileList. + // + // |callback| must not be null. + virtual google_apis::CancelCallback GetAllFileList( + const google_apis::FileListCallback& callback) = 0; + + // Fetches a file list in the directory with |directory_resource_id|. + // |callback| will be called upon completion. + // If the list is too long, it may be paged. In such a case, a URL to fetch + // remaining results will be included in the returned result. See also + // GetRemainingFileList. + // + // |directory_resource_id| must not be empty. + // |callback| must not be null. + virtual google_apis::CancelCallback GetFileListInDirectory( + const std::string& directory_resource_id, + const google_apis::FileListCallback& callback) = 0; + + // Searches the resources for the |search_query| from all the user's + // resources. |callback| will be called upon completion. + // If the list is too long, it may be paged. In such a case, a URL to fetch + // remaining results will be included in the returned result. See also + // GetRemainingFileList. + // + // |search_query| must not be empty. + // |callback| must not be null. + virtual google_apis::CancelCallback Search( + const std::string& search_query, + const google_apis::FileListCallback& callback) = 0; + + // Searches the resources with the |title|. + // |directory_resource_id| is an optional parameter. If it is empty, + // the search target is all the existing resources. Otherwise, it is + // the resources directly under the directory with |directory_resource_id|. + // If the list is too long, it may be paged. In such a case, a URL to fetch + // remaining results will be included in the returned result. See also + // GetRemainingFileList. + // + // |title| must not be empty, and |callback| must not be null. + virtual google_apis::CancelCallback SearchByTitle( + const std::string& title, + const std::string& directory_resource_id, + const google_apis::FileListCallback& callback) = 0; + + // Fetches change list since |start_changestamp|. |callback| will be + // called upon completion. + // If the list is too long, it may be paged. In such a case, a URL to fetch + // remaining results will be included in the returned result. See also + // GetRemainingChangeList. + // + // |callback| must not be null. + virtual google_apis::CancelCallback GetChangeList( + int64 start_changestamp, + const google_apis::ChangeListCallback& callback) = 0; + + // The result of GetChangeList() may be paged. + // In such a case, a next link to fetch remaining result is returned. + // The page token can be used for this method. |callback| will be called upon + // completion. + // + // |next_link| must not be empty. |callback| must not be null. + virtual google_apis::CancelCallback GetRemainingChangeList( + const GURL& next_link, + const google_apis::ChangeListCallback& callback) = 0; + + // The result of GetAllFileList(), GetFileListInDirectory(), Search() + // and SearchByTitle() may be paged. In such a case, a next link to fetch + // remaining result is returned. The page token can be used for this method. + // |callback| will be called upon completion. + // + // |next_link| must not be empty. |callback| must not be null. + virtual google_apis::CancelCallback GetRemainingFileList( + const GURL& next_link, + const google_apis::FileListCallback& callback) = 0; + + // Fetches single entry metadata from server. The entry's file id equals + // |resource_id|. + // Upon completion, invokes |callback| with results on the calling thread. + // |callback| must not be null. + virtual google_apis::CancelCallback GetFileResource( + const std::string& resource_id, + const google_apis::FileResourceCallback& callback) = 0; + + // Fetches an url for the sharing dialog for a single entry with id + // |resource_id|, to be embedded in a webview or an iframe with origin + // |embed_origin|. The url is returned via |callback| with results on the + // calling thread. |callback| must not be null. + virtual google_apis::CancelCallback GetShareUrl( + const std::string& resource_id, + const GURL& embed_origin, + const google_apis::GetShareUrlCallback& callback) = 0; + + // Gets the about resource information from the server. + // Upon completion, invokes |callback| with results on the calling thread. + // |callback| must not be null. + virtual google_apis::CancelCallback GetAboutResource( + const google_apis::AboutResourceCallback& callback) = 0; + + // Gets the application information from the server. + // Upon completion, invokes |callback| with results on the calling thread. + // |callback| must not be null. + virtual google_apis::CancelCallback GetAppList( + const google_apis::AppListCallback& callback) = 0; + + // Permanently deletes a resource identified by its |resource_id|. + // If |etag| is not empty and did not match, the deletion fails with + // HTTP_PRECONDITION error. + // Upon completion, invokes |callback| with results on the calling thread. + // |callback| must not be null. + virtual google_apis::CancelCallback DeleteResource( + const std::string& resource_id, + const std::string& etag, + const google_apis::EntryActionCallback& callback) = 0; + + // Trashes a resource identified by its |resource_id|. + // Upon completion, invokes |callback| with results on the calling thread. + // |callback| must not be null. + virtual google_apis::CancelCallback TrashResource( + const std::string& resource_id, + const google_apis::EntryActionCallback& callback) = 0; + + // Makes a copy of a resource with |resource_id|. + // The new resource will be put under a directory with |parent_resource_id|, + // and it'll be named |new_title|. + // If |last_modified| is not null, the modified date of the resource on the + // server will be set to the date. + // Upon completion, invokes |callback| with results on the calling thread. + // |callback| must not be null. + virtual google_apis::CancelCallback CopyResource( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const google_apis::FileResourceCallback& callback) = 0; + + // Updates a resource with |resource_id| to the directory of + // |parent_resource_id| with renaming to |new_title|. + // If |last_modified| or |last_accessed| is not null, the modified/accessed + // date of the resource on the server will be set to the date. + // If |properties| are specified, then they will be set on |resource_id|. + // Upon completion, invokes |callback| with results on the calling thread. + // |callback| must not be null. + virtual google_apis::CancelCallback UpdateResource( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const base::Time& last_viewed_by_me, + const google_apis::drive::Properties& properties, + const google_apis::FileResourceCallback& callback) = 0; + + // Adds a resource (document, file, or collection) identified by its + // |resource_id| to a collection represented by the |parent_resource_id|. + // Upon completion, invokes |callback| with results on the calling thread. + // |callback| must not be null. + virtual google_apis::CancelCallback AddResourceToDirectory( + const std::string& parent_resource_id, + const std::string& resource_id, + const google_apis::EntryActionCallback& callback) = 0; + + // Removes a resource (document, file, collection) identified by its + // |resource_id| from a collection represented by the |parent_resource_id|. + // Upon completion, invokes |callback| with results on the calling thread. + // |callback| must not be null. + virtual google_apis::CancelCallback RemoveResourceFromDirectory( + const std::string& parent_resource_id, + const std::string& resource_id, + const google_apis::EntryActionCallback& callback) = 0; + + // Adds new collection with |directory_title| under parent directory + // identified with |parent_resource_id|. |parent_resource_id| can be the + // value returned by GetRootResourceId to represent the root directory. + // Upon completion, invokes |callback| and passes newly created entry on + // the calling thread. + // This function cannot be named as "CreateDirectory" as it conflicts with + // a macro on Windows. + // |callback| must not be null. + virtual google_apis::CancelCallback AddNewDirectory( + const std::string& parent_resource_id, + const std::string& directory_title, + const AddNewDirectoryOptions& options, + const google_apis::FileResourceCallback& callback) = 0; + + // Downloads a file with |resourced_id|. The downloaded file will + // be stored at |local_cache_path| location. Upon completion, invokes + // |download_action_callback| with results on the calling thread. + // If |get_content_callback| is not empty, + // URLFetcherDelegate::OnURLFetchDownloadData will be called, which will in + // turn invoke |get_content_callback| on the calling thread. + // If |progress_callback| is not empty, it is invoked periodically when + // the download made some progress. + // + // |download_action_callback| must not be null. + // |get_content_callback| and |progress_callback| may be null. + virtual google_apis::CancelCallback DownloadFile( + const base::FilePath& local_cache_path, + const std::string& resource_id, + const google_apis::DownloadActionCallback& download_action_callback, + const google_apis::GetContentCallback& get_content_callback, + const google_apis::ProgressCallback& progress_callback) = 0; + + // Initiates uploading of a new document/file. + // |content_type| and |content_length| should be the ones of the file to be + // uploaded. + // |callback| must not be null. + virtual google_apis::CancelCallback InitiateUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const UploadNewFileOptions& options, + const google_apis::InitiateUploadCallback& callback) = 0; + + // Initiates uploading of an existing document/file. + // |content_type| and |content_length| should be the ones of the file to be + // uploaded. + // |callback| must not be null. + virtual google_apis::CancelCallback InitiateUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const UploadExistingFileOptions& options, + const google_apis::InitiateUploadCallback& callback) = 0; + + // Resumes uploading of a document/file on the calling thread. + // |callback| must not be null. |progress_callback| may be null. + virtual google_apis::CancelCallback ResumeUpload( + const GURL& upload_url, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path, + const google_apis::drive::UploadRangeCallback& callback, + const google_apis::ProgressCallback& progress_callback) = 0; + + // Gets the current status of the uploading to |upload_url| from the server. + // |drive_file_path| and |content_length| should be set to the same value + // which is used for ResumeUpload. + // |callback| must not be null. + virtual google_apis::CancelCallback GetUploadStatus( + const GURL& upload_url, + int64 content_length, + const google_apis::drive::UploadRangeCallback& callback) = 0; + + // Authorizes a Drive app with the id |app_id| to open the given file. + // Upon completion, invokes |callback| with the link to open the file with + // the provided app. |callback| must not be null. + virtual google_apis::CancelCallback AuthorizeApp( + const std::string& resource_id, + const std::string& app_id, + const google_apis::AuthorizeAppCallback& callback) = 0; + + // Uninstalls a Drive app with the id |app_id|. |callback| must not be null. + virtual google_apis::CancelCallback UninstallApp( + const std::string& app_id, + const google_apis::EntryActionCallback& callback) = 0; + + // Authorizes the account |email| to access |resource_id| as a |role|. + // |callback| must not be null. + virtual google_apis::CancelCallback AddPermission( + const std::string& resource_id, + const std::string& email, + google_apis::drive::PermissionRole role, + const google_apis::EntryActionCallback& callback) = 0; + + // Starts batch request and returns |BatchRequestConfigurator|. + virtual scoped_ptr<BatchRequestConfiguratorInterface> StartBatchRequest() = 0; +}; + +} // namespace drive + +#endif // COMPONENTS_DRIVE_SERVICE_DRIVE_SERVICE_INTERFACE_H_ diff --git a/components/drive/service/dummy_drive_service.cc b/components/drive/service/dummy_drive_service.cc new file mode 100644 index 0000000..8700fce --- /dev/null +++ b/components/drive/service/dummy_drive_service.cc @@ -0,0 +1,224 @@ +// 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/service/dummy_drive_service.h" + +#include "base/bind.h" + +using google_apis::AboutResourceCallback; +using google_apis::AppListCallback; +using google_apis::AuthStatusCallback; +using google_apis::AuthorizeAppCallback; +using google_apis::CancelCallback; +using google_apis::ChangeListCallback; +using google_apis::DownloadActionCallback; +using google_apis::EntryActionCallback; +using google_apis::FileListCallback; +using google_apis::FileResourceCallback; +using google_apis::GetContentCallback; +using google_apis::GetShareUrlCallback; +using google_apis::InitiateUploadCallback; +using google_apis::ProgressCallback; +using google_apis::drive::UploadRangeCallback; + +namespace drive { + +DummyDriveService::DummyDriveService() {} + +DummyDriveService::~DummyDriveService() {} + +void DummyDriveService::Initialize(const std::string& account_id) {} + +void DummyDriveService::AddObserver(DriveServiceObserver* observer) {} + +void DummyDriveService::RemoveObserver(DriveServiceObserver* observer) {} + +bool DummyDriveService::CanSendRequest() const { return true; } + +bool DummyDriveService::HasAccessToken() const { return true; } + +void DummyDriveService::RequestAccessToken(const AuthStatusCallback& callback) { + callback.Run(google_apis::HTTP_NOT_MODIFIED, "fake_access_token"); +} + +bool DummyDriveService::HasRefreshToken() const { return true; } + +void DummyDriveService::ClearAccessToken() { } + +void DummyDriveService::ClearRefreshToken() { } + +std::string DummyDriveService::GetRootResourceId() const { + return "dummy_root"; +} + +CancelCallback DummyDriveService::GetAllFileList( + const FileListCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::GetFileListInDirectory( + const std::string& directory_resource_id, + const FileListCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::Search( + const std::string& search_query, + const FileListCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::SearchByTitle( + const std::string& title, + const std::string& directory_resource_id, + const FileListCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::GetChangeList( + int64 start_changestamp, + const ChangeListCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::GetRemainingChangeList( + const GURL& next_link, + const ChangeListCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::GetRemainingFileList( + const GURL& next_link, + const FileListCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::GetFileResource( + const std::string& resource_id, + const FileResourceCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::GetShareUrl( + const std::string& resource_id, + const GURL& embed_origin, + const GetShareUrlCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::GetAboutResource( + const AboutResourceCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::GetAppList( + const AppListCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::DeleteResource( + const std::string& resource_id, + const std::string& etag, + const EntryActionCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::TrashResource( + const std::string& resource_id, + const EntryActionCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::DownloadFile( + const base::FilePath& local_cache_path, + const std::string& resource_id, + const DownloadActionCallback& download_action_callback, + const GetContentCallback& get_content_callback, + const ProgressCallback& progress_callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::CopyResource( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const FileResourceCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::UpdateResource( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const base::Time& last_viewed_by_me, + const google_apis::drive::Properties& properties, + const google_apis::FileResourceCallback& callback) { + return CancelCallback(); +} + +CancelCallback DummyDriveService::AddResourceToDirectory( + const std::string& parent_resource_id, + const std::string& resource_id, + const EntryActionCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::RemoveResourceFromDirectory( + const std::string& parent_resource_id, + const std::string& resource_id, + const EntryActionCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::AddNewDirectory( + const std::string& parent_resource_id, + const std::string& directory_title, + const AddNewDirectoryOptions& options, + const FileResourceCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::InitiateUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const UploadNewFileOptions& options, + const InitiateUploadCallback& callback) { + return CancelCallback(); +} + +CancelCallback DummyDriveService::InitiateUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const UploadExistingFileOptions& options, + const InitiateUploadCallback& callback) { + return CancelCallback(); +} + +CancelCallback DummyDriveService::ResumeUpload( + const GURL& upload_url, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path, + const UploadRangeCallback& callback, + const ProgressCallback& progress_callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::GetUploadStatus( + const GURL& upload_url, + int64 content_length, + const UploadRangeCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::MultipartUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const base::FilePath& local_file_path, + const UploadNewFileOptions& options, + const FileResourceCallback& callback, + const ProgressCallback& progress_callback) { + return CancelCallback(); +} + +CancelCallback DummyDriveService::MultipartUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const base::FilePath& local_file_path, + const UploadExistingFileOptions& options, + const FileResourceCallback& callback, + const ProgressCallback& progress_callback) { + return CancelCallback(); +} + +CancelCallback DummyDriveService::AuthorizeApp( + const std::string& resource_id, + const std::string& app_id, + const AuthorizeAppCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::UninstallApp( + const std::string& app_id, + const EntryActionCallback& callback) { return CancelCallback(); } + +CancelCallback DummyDriveService::AddPermission( + const std::string& resource_id, + const std::string& email, + google_apis::drive::PermissionRole role, + const EntryActionCallback& callback) { return CancelCallback(); } +scoped_ptr<BatchRequestConfiguratorInterface> +DummyDriveService::StartBatchRequest() { + return scoped_ptr<BatchRequestConfiguratorInterface>(); +} + +} // namespace drive diff --git a/components/drive/service/dummy_drive_service.h b/components/drive/service/dummy_drive_service.h new file mode 100644 index 0000000..3317c45 --- /dev/null +++ b/components/drive/service/dummy_drive_service.h @@ -0,0 +1,166 @@ +// 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_SERVICE_DUMMY_DRIVE_SERVICE_H_ +#define COMPONENTS_DRIVE_SERVICE_DUMMY_DRIVE_SERVICE_H_ + +#include <string> + +#include "components/drive/service/drive_service_interface.h" +#include "google_apis/drive/auth_service_interface.h" + +namespace drive { + +// Dummy implementation of DriveServiceInterface. +// All functions do nothing, or return place holder values like 'true'. +class DummyDriveService : public DriveServiceInterface { + public: + DummyDriveService(); + ~DummyDriveService() override; + + // DriveServiceInterface Overrides + void Initialize(const std::string& account_id) override; + void AddObserver(DriveServiceObserver* observer) override; + void RemoveObserver(DriveServiceObserver* observer) override; + bool CanSendRequest() const override; + bool HasAccessToken() const override; + void RequestAccessToken( + const google_apis::AuthStatusCallback& callback) override; + bool HasRefreshToken() const override; + void ClearAccessToken() override; + void ClearRefreshToken() override; + std::string GetRootResourceId() const override; + google_apis::CancelCallback GetAllFileList( + const google_apis::FileListCallback& callback) override; + google_apis::CancelCallback GetFileListInDirectory( + const std::string& directory_resource_id, + const google_apis::FileListCallback& callback) override; + google_apis::CancelCallback Search( + const std::string& search_query, + const google_apis::FileListCallback& callback) override; + google_apis::CancelCallback SearchByTitle( + const std::string& title, + const std::string& directory_resource_id, + const google_apis::FileListCallback& callback) override; + google_apis::CancelCallback GetChangeList( + int64 start_changestamp, + const google_apis::ChangeListCallback& callback) override; + google_apis::CancelCallback GetRemainingChangeList( + const GURL& next_link, + const google_apis::ChangeListCallback& callback) override; + google_apis::CancelCallback GetRemainingFileList( + const GURL& next_link, + const google_apis::FileListCallback& callback) override; + google_apis::CancelCallback GetFileResource( + const std::string& resource_id, + const google_apis::FileResourceCallback& callback) override; + google_apis::CancelCallback GetShareUrl( + const std::string& resource_id, + const GURL& embed_origin, + const google_apis::GetShareUrlCallback& callback) override; + google_apis::CancelCallback GetAboutResource( + const google_apis::AboutResourceCallback& callback) override; + google_apis::CancelCallback GetAppList( + const google_apis::AppListCallback& callback) override; + google_apis::CancelCallback DeleteResource( + const std::string& resource_id, + const std::string& etag, + const google_apis::EntryActionCallback& callback) override; + google_apis::CancelCallback TrashResource( + const std::string& resource_id, + const google_apis::EntryActionCallback& callback) override; + google_apis::CancelCallback DownloadFile( + const base::FilePath& local_cache_path, + const std::string& resource_id, + const google_apis::DownloadActionCallback& download_action_callback, + const google_apis::GetContentCallback& get_content_callback, + const google_apis::ProgressCallback& progress_callback) override; + google_apis::CancelCallback CopyResource( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const google_apis::FileResourceCallback& callback) override; + google_apis::CancelCallback UpdateResource( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const base::Time& last_viewed_by_me, + const google_apis::drive::Properties& properties, + const google_apis::FileResourceCallback& callback) override; + google_apis::CancelCallback AddResourceToDirectory( + const std::string& parent_resource_id, + const std::string& resource_id, + const google_apis::EntryActionCallback& callback) override; + google_apis::CancelCallback RemoveResourceFromDirectory( + const std::string& parent_resource_id, + const std::string& resource_id, + const google_apis::EntryActionCallback& callback) override; + google_apis::CancelCallback AddNewDirectory( + const std::string& parent_resource_id, + const std::string& directory_title, + const AddNewDirectoryOptions& options, + const google_apis::FileResourceCallback& callback) override; + google_apis::CancelCallback InitiateUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const UploadNewFileOptions& options, + const google_apis::InitiateUploadCallback& callback) override; + google_apis::CancelCallback InitiateUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const UploadExistingFileOptions& options, + const google_apis::InitiateUploadCallback& callback) override; + google_apis::CancelCallback ResumeUpload( + const GURL& upload_url, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path, + const google_apis::drive::UploadRangeCallback& callback, + const google_apis::ProgressCallback& progress_callback) override; + google_apis::CancelCallback GetUploadStatus( + const GURL& upload_url, + int64 content_length, + const google_apis::drive::UploadRangeCallback& callback) override; + google_apis::CancelCallback MultipartUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const base::FilePath& local_file_path, + const UploadNewFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) override; + google_apis::CancelCallback MultipartUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const base::FilePath& local_file_path, + const UploadExistingFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) override; + google_apis::CancelCallback AuthorizeApp( + const std::string& resource_id, + const std::string& app_id, + const google_apis::AuthorizeAppCallback& callback) override; + google_apis::CancelCallback UninstallApp( + const std::string& app_id, + const google_apis::EntryActionCallback& callback) override; + google_apis::CancelCallback AddPermission( + const std::string& resource_id, + const std::string& email, + google_apis::drive::PermissionRole role, + const google_apis::EntryActionCallback& callback) override; + scoped_ptr<BatchRequestConfiguratorInterface> StartBatchRequest() override; +}; + +} // namespace drive + +#endif // COMPONENTS_DRIVE_SERVICE_DUMMY_DRIVE_SERVICE_H_ diff --git a/components/drive/service/fake_drive_service.cc b/components/drive/service/fake_drive_service.cc new file mode 100644 index 0000000..81b55d1 --- /dev/null +++ b/components/drive/service/fake_drive_service.cc @@ -0,0 +1,1787 @@ +// 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/service/fake_drive_service.h" + +#include <string> + +#include "base/files/file_util.h" +#include "base/json/json_string_value_serializer.h" +#include "base/logging.h" +#include "base/md5.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/strings/string_tokenizer.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/thread_task_runner_handle.h" +#include "base/values.h" +#include "components/drive/drive_api_util.h" +#include "google_apis/drive/drive_api_parser.h" +#include "google_apis/drive/test_util.h" +#include "net/base/escape.h" +#include "net/base/url_util.h" + +using google_apis::AboutResource; +using google_apis::AboutResourceCallback; +using google_apis::AppList; +using google_apis::AppListCallback; +using google_apis::AuthStatusCallback; +using google_apis::AuthorizeAppCallback; +using google_apis::CancelCallback; +using google_apis::ChangeList; +using google_apis::ChangeListCallback; +using google_apis::ChangeResource; +using google_apis::DownloadActionCallback; +using google_apis::EntryActionCallback; +using google_apis::FileList; +using google_apis::FileListCallback; +using google_apis::FileResource; +using google_apis::FileResourceCallback; +using google_apis::DRIVE_FILE_ERROR; +using google_apis::DRIVE_NO_CONNECTION; +using google_apis::DRIVE_OTHER_ERROR; +using google_apis::DriveApiErrorCode; +using google_apis::GetContentCallback; +using google_apis::GetShareUrlCallback; +using google_apis::HTTP_BAD_REQUEST; +using google_apis::HTTP_CREATED; +using google_apis::HTTP_FORBIDDEN; +using google_apis::HTTP_NOT_FOUND; +using google_apis::HTTP_NO_CONTENT; +using google_apis::HTTP_PRECONDITION; +using google_apis::HTTP_RESUME_INCOMPLETE; +using google_apis::HTTP_SUCCESS; +using google_apis::InitiateUploadCallback; +using google_apis::ParentReference; +using google_apis::ProgressCallback; +using google_apis::UploadRangeResponse; +using google_apis::drive::UploadRangeCallback; +namespace test_util = google_apis::test_util; + +namespace drive { +namespace { + +// Returns true if the entry matches with the search query. +// Supports queries consist of following format. +// - Phrases quoted by double/single quotes +// - AND search for multiple words/phrases segmented by space +// - Limited attribute search. Only "title:" is supported. +bool EntryMatchWithQuery(const ChangeResource& entry, + const std::string& query) { + base::StringTokenizer tokenizer(query, " "); + tokenizer.set_quote_chars("\"'"); + while (tokenizer.GetNext()) { + std::string key, value; + const std::string& token = tokenizer.token(); + if (token.find(':') == std::string::npos) { + base::TrimString(token, "\"'", &value); + } else { + base::StringTokenizer key_value(token, ":"); + key_value.set_quote_chars("\"'"); + if (!key_value.GetNext()) + return false; + key = key_value.token(); + if (!key_value.GetNext()) + return false; + base::TrimString(key_value.token(), "\"'", &value); + } + + // TODO(peria): Deal with other attributes than title. + if (!key.empty() && key != "title") + return false; + // Search query in the title. + if (!entry.file() || + entry.file()->title().find(value) == std::string::npos) + return false; + } + return true; +} + +void ScheduleUploadRangeCallback(const UploadRangeCallback& callback, + int64 start_position, + int64 end_position, + DriveApiErrorCode error, + scoped_ptr<FileResource> entry) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, + UploadRangeResponse(error, + start_position, + end_position), + base::Passed(&entry))); +} + +void FileListCallbackAdapter(const FileListCallback& callback, + DriveApiErrorCode error, + scoped_ptr<ChangeList> change_list) { + scoped_ptr<FileList> file_list; + if (!change_list) { + callback.Run(error, file_list.Pass()); + return; + } + + file_list.reset(new FileList); + file_list->set_next_link(change_list->next_link()); + for (size_t i = 0; i < change_list->items().size(); ++i) { + const ChangeResource& entry = *change_list->items()[i]; + if (entry.file()) + file_list->mutable_items()->push_back(new FileResource(*entry.file())); + } + callback.Run(error, file_list.Pass()); +} + +bool UserHasWriteAccess(google_apis::drive::PermissionRole user_permission) { + switch (user_permission) { + case google_apis::drive::PERMISSION_ROLE_OWNER: + case google_apis::drive::PERMISSION_ROLE_WRITER: + return true; + case google_apis::drive::PERMISSION_ROLE_READER: + case google_apis::drive::PERMISSION_ROLE_COMMENTER: + break; + } + return false; +} + +void CallFileResouceCallback(const FileResourceCallback& callback, + const UploadRangeResponse& response, + scoped_ptr<FileResource> entry) { + callback.Run(response.code, entry.Pass()); +} + +struct CallResumeUpload { + CallResumeUpload() {} + ~CallResumeUpload() {} + + void Run(DriveApiErrorCode code, const GURL& upload_url) { + if (service) { + service->ResumeUpload( + upload_url, + /* start position */ 0, + /* end position */ content_length, + content_length, + content_type, + local_file_path, + base::Bind(&CallFileResouceCallback, callback), + progress_callback); + } + } + + base::WeakPtr<FakeDriveService> service; + int64 content_length; + std::string content_type; + base::FilePath local_file_path; + FileResourceCallback callback; + ProgressCallback progress_callback; +}; + +} // namespace + +struct FakeDriveService::EntryInfo { + EntryInfo() : user_permission(google_apis::drive::PERMISSION_ROLE_OWNER) {} + + google_apis::ChangeResource change_resource; + GURL share_url; + std::string content_data; + + // Behaves in the same way as "userPermission" described in + // https://developers.google.com/drive/v2/reference/files + google_apis::drive::PermissionRole user_permission; +}; + +struct FakeDriveService::UploadSession { + std::string content_type; + int64 content_length; + std::string parent_resource_id; + std::string resource_id; + std::string etag; + std::string title; + + int64 uploaded_size; + + UploadSession() + : content_length(0), + uploaded_size(0) {} + + UploadSession( + std::string content_type, + int64 content_length, + std::string parent_resource_id, + std::string resource_id, + std::string etag, + std::string title) + : content_type(content_type), + content_length(content_length), + parent_resource_id(parent_resource_id), + resource_id(resource_id), + etag(etag), + title(title), + uploaded_size(0) { + } +}; + +FakeDriveService::FakeDriveService() + : about_resource_(new AboutResource), + published_date_seq_(0), + next_upload_sequence_number_(0), + default_max_results_(0), + resource_id_count_(0), + file_list_load_count_(0), + change_list_load_count_(0), + directory_load_count_(0), + about_resource_load_count_(0), + app_list_load_count_(0), + blocked_file_list_load_count_(0), + offline_(false), + never_return_all_file_list_(false), + share_url_base_("https://share_url/"), + weak_ptr_factory_(this) { + about_resource_->set_largest_change_id(654321); + about_resource_->set_quota_bytes_total(9876543210); + about_resource_->set_quota_bytes_used_aggregate(6789012345); + about_resource_->set_root_folder_id(GetRootResourceId()); +} + +FakeDriveService::~FakeDriveService() { + DCHECK(thread_checker_.CalledOnValidThread()); + STLDeleteValues(&entries_); +} + +bool FakeDriveService::LoadAppListForDriveApi( + const std::string& relative_path) { + DCHECK(thread_checker_.CalledOnValidThread()); + + // Load JSON data, which must be a dictionary. + scoped_ptr<base::Value> value = test_util::LoadJSONFile(relative_path); + CHECK_EQ(base::Value::TYPE_DICTIONARY, value->GetType()); + app_info_value_.reset( + static_cast<base::DictionaryValue*>(value.release())); + return app_info_value_; +} + +void FakeDriveService::AddApp(const std::string& app_id, + const std::string& app_name, + const std::string& product_id, + const std::string& create_url, + bool is_removable) { + if (app_json_template_.empty()) { + base::FilePath path = + test_util::GetTestFilePath("drive/applist_app_template.json"); + CHECK(base::ReadFileToString(path, &app_json_template_)); + } + + std::string app_json = app_json_template_; + base::ReplaceSubstringsAfterOffset(&app_json, 0, "$AppId", app_id); + base::ReplaceSubstringsAfterOffset(&app_json, 0, "$AppName", app_name); + base::ReplaceSubstringsAfterOffset(&app_json, 0, "$ProductId", product_id); + base::ReplaceSubstringsAfterOffset(&app_json, 0, "$CreateUrl", create_url); + base::ReplaceSubstringsAfterOffset( + &app_json, 0, "$Removable", is_removable ? "true" : "false"); + + JSONStringValueDeserializer json(app_json); + std::string error_message; + scoped_ptr<base::Value> value(json.Deserialize(NULL, &error_message)); + CHECK_EQ(base::Value::TYPE_DICTIONARY, value->GetType()); + + base::ListValue* item_list; + CHECK(app_info_value_->GetListWithoutPathExpansion("items", &item_list)); + item_list->Append(value.release()); +} + +void FakeDriveService::RemoveAppByProductId(const std::string& product_id) { + base::ListValue* item_list; + CHECK(app_info_value_->GetListWithoutPathExpansion("items", &item_list)); + for (size_t i = 0; i < item_list->GetSize(); ++i) { + base::DictionaryValue* item; + CHECK(item_list->GetDictionary(i, &item)); + const char kKeyProductId[] = "productId"; + std::string item_product_id; + if (item->GetStringWithoutPathExpansion(kKeyProductId, &item_product_id) && + product_id == item_product_id) { + item_list->Remove(i, NULL); + return; + } + } +} + +bool FakeDriveService::HasApp(const std::string& app_id) const { + base::ListValue* item_list; + CHECK(app_info_value_->GetListWithoutPathExpansion("items", &item_list)); + for (size_t i = 0; i < item_list->GetSize(); ++i) { + base::DictionaryValue* item; + CHECK(item_list->GetDictionary(i, &item)); + const char kKeyId[] = "id"; + std::string item_id; + if (item->GetStringWithoutPathExpansion(kKeyId, &item_id) && + item_id == app_id) { + return true; + } + } + + return false; +} + +void FakeDriveService::SetQuotaValue(int64 used, int64 total) { + DCHECK(thread_checker_.CalledOnValidThread()); + + about_resource_->set_quota_bytes_used_aggregate(used); + about_resource_->set_quota_bytes_total(total); +} + +GURL FakeDriveService::GetFakeLinkUrl(const std::string& resource_id) { + return GURL("https://fake_server/" + net::EscapePath(resource_id)); +} + +void FakeDriveService::Initialize(const std::string& account_id) { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +void FakeDriveService::AddObserver(DriveServiceObserver* observer) { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +void FakeDriveService::RemoveObserver(DriveServiceObserver* observer) { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +bool FakeDriveService::CanSendRequest() const { + DCHECK(thread_checker_.CalledOnValidThread()); + return true; +} + +bool FakeDriveService::HasAccessToken() const { + DCHECK(thread_checker_.CalledOnValidThread()); + return true; +} + +void FakeDriveService::RequestAccessToken(const AuthStatusCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + callback.Run(google_apis::HTTP_NOT_MODIFIED, "fake_access_token"); +} + +bool FakeDriveService::HasRefreshToken() const { + DCHECK(thread_checker_.CalledOnValidThread()); + return true; +} + +void FakeDriveService::ClearAccessToken() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +void FakeDriveService::ClearRefreshToken() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +std::string FakeDriveService::GetRootResourceId() const { + return "fake_root"; +} + +CancelCallback FakeDriveService::GetAllFileList( + const FileListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (never_return_all_file_list_) { + ++blocked_file_list_load_count_; + return CancelCallback(); + } + + GetChangeListInternal(0, // start changestamp + std::string(), // empty search query + std::string(), // no directory resource id, + 0, // start offset + default_max_results_, + &file_list_load_count_, + base::Bind(&FileListCallbackAdapter, callback)); + return CancelCallback(); +} + +CancelCallback FakeDriveService::GetFileListInDirectory( + const std::string& directory_resource_id, + const FileListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!directory_resource_id.empty()); + DCHECK(!callback.is_null()); + + GetChangeListInternal(0, // start changestamp + std::string(), // empty search query + directory_resource_id, + 0, // start offset + default_max_results_, + &directory_load_count_, + base::Bind(&FileListCallbackAdapter, callback)); + return CancelCallback(); +} + +CancelCallback FakeDriveService::Search( + const std::string& search_query, + const FileListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!search_query.empty()); + DCHECK(!callback.is_null()); + + GetChangeListInternal(0, // start changestamp + search_query, + std::string(), // no directory resource id, + 0, // start offset + default_max_results_, + NULL, + base::Bind(&FileListCallbackAdapter, callback)); + return CancelCallback(); +} + +CancelCallback FakeDriveService::SearchByTitle( + const std::string& title, + const std::string& directory_resource_id, + const FileListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!title.empty()); + DCHECK(!callback.is_null()); + + // Note: the search implementation here doesn't support quotation unescape, + // so don't escape here. + GetChangeListInternal(0, // start changestamp + base::StringPrintf("title:'%s'", title.c_str()), + directory_resource_id, + 0, // start offset + default_max_results_, + NULL, + base::Bind(&FileListCallbackAdapter, callback)); + return CancelCallback(); +} + +CancelCallback FakeDriveService::GetChangeList( + int64 start_changestamp, + const ChangeListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + GetChangeListInternal(start_changestamp, + std::string(), // empty search query + std::string(), // no directory resource id, + 0, // start offset + default_max_results_, + &change_list_load_count_, + callback); + return CancelCallback(); +} + +CancelCallback FakeDriveService::GetRemainingChangeList( + const GURL& next_link, + const ChangeListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!next_link.is_empty()); + DCHECK(!callback.is_null()); + + // "changestamp", "q", "parent" and "start-offset" are parameters to + // implement "paging" of the result on FakeDriveService. + // The URL should be the one filled in GetChangeListInternal of the + // previous method invocation, so it should start with "http://localhost/?". + // See also GetChangeListInternal. + DCHECK_EQ(next_link.host(), "localhost"); + DCHECK_EQ(next_link.path(), "/"); + + int64 start_changestamp = 0; + std::string search_query; + std::string directory_resource_id; + int start_offset = 0; + int max_results = default_max_results_; + base::StringPairs parameters; + if (base::SplitStringIntoKeyValuePairs( + next_link.query(), '=', '&', ¶meters)) { + for (size_t i = 0; i < parameters.size(); ++i) { + if (parameters[i].first == "changestamp") { + base::StringToInt64(parameters[i].second, &start_changestamp); + } else if (parameters[i].first == "q") { + search_query = + net::UnescapeURLComponent(parameters[i].second, + net::UnescapeRule::URL_SPECIAL_CHARS); + } else if (parameters[i].first == "parent") { + directory_resource_id = + net::UnescapeURLComponent(parameters[i].second, + net::UnescapeRule::URL_SPECIAL_CHARS); + } else if (parameters[i].first == "start-offset") { + base::StringToInt(parameters[i].second, &start_offset); + } else if (parameters[i].first == "max-results") { + base::StringToInt(parameters[i].second, &max_results); + } + } + } + + GetChangeListInternal(start_changestamp, search_query, directory_resource_id, + start_offset, max_results, NULL, callback); + return CancelCallback(); +} + +CancelCallback FakeDriveService::GetRemainingFileList( + const GURL& next_link, + const FileListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!next_link.is_empty()); + DCHECK(!callback.is_null()); + + return GetRemainingChangeList( + next_link, base::Bind(&FileListCallbackAdapter, callback)); +} + +CancelCallback FakeDriveService::GetFileResource( + const std::string& resource_id, + const FileResourceCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, + DRIVE_NO_CONNECTION, + base::Passed(scoped_ptr<FileResource>()))); + return CancelCallback(); + } + + EntryInfo* entry = FindEntryByResourceId(resource_id); + if (entry && entry->change_resource.file()) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_SUCCESS, base::Passed(make_scoped_ptr( + new FileResource(*entry->change_resource.file()))))); + return CancelCallback(); + } + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_NOT_FOUND, + base::Passed(scoped_ptr<FileResource>()))); + return CancelCallback(); +} + +CancelCallback FakeDriveService::GetShareUrl( + const std::string& resource_id, + const GURL& /* embed_origin */, + const GetShareUrlCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, + DRIVE_NO_CONNECTION, + GURL())); + return CancelCallback(); + } + + EntryInfo* entry = FindEntryByResourceId(resource_id); + if (entry) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_SUCCESS, entry->share_url)); + return CancelCallback(); + } + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_NOT_FOUND, GURL())); + return CancelCallback(); +} + +CancelCallback FakeDriveService::GetAboutResource( + const AboutResourceCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (offline_) { + scoped_ptr<AboutResource> null; + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, + DRIVE_NO_CONNECTION, base::Passed(&null))); + return CancelCallback(); + } + + ++about_resource_load_count_; + scoped_ptr<AboutResource> about_resource(new AboutResource(*about_resource_)); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, + HTTP_SUCCESS, base::Passed(&about_resource))); + return CancelCallback(); +} + +CancelCallback FakeDriveService::GetAppList(const AppListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + DCHECK(app_info_value_); + + if (offline_) { + scoped_ptr<AppList> null; + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, + DRIVE_NO_CONNECTION, + base::Passed(&null))); + return CancelCallback(); + } + + ++app_list_load_count_; + scoped_ptr<AppList> app_list(AppList::CreateFrom(*app_info_value_)); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_SUCCESS, base::Passed(&app_list))); + return CancelCallback(); +} + +CancelCallback FakeDriveService::DeleteResource( + const std::string& resource_id, + const std::string& etag, + const EntryActionCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, DRIVE_NO_CONNECTION)); + return CancelCallback(); + } + + EntryInfo* entry = FindEntryByResourceId(resource_id); + if (!entry) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND)); + return CancelCallback(); + } + + ChangeResource* change = &entry->change_resource; + const FileResource* file = change->file(); + if (change->is_deleted()) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND)); + return CancelCallback(); + } + + if (!etag.empty() && etag != file->etag()) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, HTTP_PRECONDITION)); + return CancelCallback(); + } + + if (entry->user_permission != google_apis::drive::PERMISSION_ROLE_OWNER) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, HTTP_FORBIDDEN)); + return CancelCallback(); + } + + change->set_deleted(true); + AddNewChangestamp(change); + change->set_file(scoped_ptr<FileResource>()); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, HTTP_NO_CONTENT)); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(&FakeDriveService::NotifyObservers, + weak_ptr_factory_.GetWeakPtr())); + return CancelCallback(); +} + +CancelCallback FakeDriveService::TrashResource( + const std::string& resource_id, + const EntryActionCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, DRIVE_NO_CONNECTION)); + return CancelCallback(); + } + + EntryInfo* entry = FindEntryByResourceId(resource_id); + if (!entry) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND)); + return CancelCallback(); + } + + ChangeResource* change = &entry->change_resource; + FileResource* file = change->mutable_file(); + if (change->is_deleted() || file->labels().is_trashed()) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND)); + return CancelCallback(); + } + + if (entry->user_permission != google_apis::drive::PERMISSION_ROLE_OWNER) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, HTTP_FORBIDDEN)); + return CancelCallback(); + } + + file->mutable_labels()->set_trashed(true); + AddNewChangestamp(change); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, HTTP_SUCCESS)); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(&FakeDriveService::NotifyObservers, + weak_ptr_factory_.GetWeakPtr())); + return CancelCallback(); +} + +CancelCallback FakeDriveService::DownloadFile( + const base::FilePath& local_cache_path, + const std::string& resource_id, + const DownloadActionCallback& download_action_callback, + const GetContentCallback& get_content_callback, + const ProgressCallback& progress_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!download_action_callback.is_null()); + + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(download_action_callback, + DRIVE_NO_CONNECTION, + base::FilePath())); + return CancelCallback(); + } + + EntryInfo* entry = FindEntryByResourceId(resource_id); + if (!entry || entry->change_resource.file()->IsHostedDocument()) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(download_action_callback, HTTP_NOT_FOUND, base::FilePath())); + return CancelCallback(); + } + + const FileResource* file = entry->change_resource.file(); + const std::string& content_data = entry->content_data; + int64 file_size = file->file_size(); + DCHECK_EQ(static_cast<size_t>(file_size), content_data.size()); + + if (!get_content_callback.is_null()) { + const int64 kBlockSize = 5; + for (int64 i = 0; i < file_size; i += kBlockSize) { + const int64 size = std::min(kBlockSize, file_size - i); + scoped_ptr<std::string> content_for_callback( + new std::string(content_data.substr(i, size))); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(get_content_callback, HTTP_SUCCESS, + base::Passed(&content_for_callback))); + } + } + + if (!test_util::WriteStringToFile(local_cache_path, content_data)) { + // Failed to write the content. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(download_action_callback, + DRIVE_FILE_ERROR, base::FilePath())); + return CancelCallback(); + } + + if (!progress_callback.is_null()) { + // See also the comment in ResumeUpload(). For testing that clients + // can handle the case progress_callback is called multiple times, + // here we invoke the callback twice. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(progress_callback, file_size / 2, file_size)); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(progress_callback, file_size, file_size)); + } + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(download_action_callback, + HTTP_SUCCESS, + local_cache_path)); + return CancelCallback(); +} + +CancelCallback FakeDriveService::CopyResource( + const std::string& resource_id, + const std::string& in_parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const FileResourceCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, + DRIVE_NO_CONNECTION, + base::Passed(scoped_ptr<FileResource>()))); + return CancelCallback(); + } + + const std::string& parent_resource_id = in_parent_resource_id.empty() ? + GetRootResourceId() : in_parent_resource_id; + + EntryInfo* entry = FindEntryByResourceId(resource_id); + if (!entry) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_NOT_FOUND, + base::Passed(scoped_ptr<FileResource>()))); + return CancelCallback(); + } + + // Make a copy and set the new resource ID and the new title. + scoped_ptr<EntryInfo> copied_entry(new EntryInfo); + copied_entry->content_data = entry->content_data; + copied_entry->share_url = entry->share_url; + copied_entry->change_resource.set_file( + make_scoped_ptr(new FileResource(*entry->change_resource.file()))); + + ChangeResource* new_change = &copied_entry->change_resource; + FileResource* new_file = new_change->mutable_file(); + const std::string new_resource_id = GetNewResourceId(); + new_change->set_file_id(new_resource_id); + new_file->set_file_id(new_resource_id); + new_file->set_title(new_title); + + ParentReference parent; + parent.set_file_id(parent_resource_id); + parent.set_parent_link(GetFakeLinkUrl(parent_resource_id)); + std::vector<ParentReference> parents; + parents.push_back(parent); + *new_file->mutable_parents() = parents; + + if (!last_modified.is_null()) + new_file->set_modified_date(last_modified); + + AddNewChangestamp(new_change); + UpdateETag(new_file); + + // Add the new entry to the map. + entries_[new_resource_id] = copied_entry.release(); + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, + HTTP_SUCCESS, + base::Passed(make_scoped_ptr(new FileResource(*new_file))))); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(&FakeDriveService::NotifyObservers, + weak_ptr_factory_.GetWeakPtr())); + return CancelCallback(); +} + +CancelCallback FakeDriveService::UpdateResource( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const base::Time& last_viewed_by_me, + const google_apis::drive::Properties& properties, + const google_apis::FileResourceCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, DRIVE_NO_CONNECTION, + base::Passed(scoped_ptr<FileResource>()))); + return CancelCallback(); + } + + EntryInfo* entry = FindEntryByResourceId(resource_id); + if (!entry) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_NOT_FOUND, + base::Passed(scoped_ptr<FileResource>()))); + return CancelCallback(); + } + + if (!UserHasWriteAccess(entry->user_permission)) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_FORBIDDEN, + base::Passed(scoped_ptr<FileResource>()))); + return CancelCallback(); + } + + ChangeResource* change = &entry->change_resource; + FileResource* file = change->mutable_file(); + + if (!new_title.empty()) + file->set_title(new_title); + + // Set parent if necessary. + if (!parent_resource_id.empty()) { + ParentReference parent; + parent.set_file_id(parent_resource_id); + parent.set_parent_link(GetFakeLinkUrl(parent_resource_id)); + + std::vector<ParentReference> parents; + parents.push_back(parent); + *file->mutable_parents() = parents; + } + + if (!last_modified.is_null()) + file->set_modified_date(last_modified); + + if (!last_viewed_by_me.is_null()) + file->set_last_viewed_by_me_date(last_viewed_by_me); + + AddNewChangestamp(change); + UpdateETag(file); + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_SUCCESS, + base::Passed(make_scoped_ptr(new FileResource(*file))))); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(&FakeDriveService::NotifyObservers, + weak_ptr_factory_.GetWeakPtr())); + return CancelCallback(); +} + +CancelCallback FakeDriveService::AddResourceToDirectory( + const std::string& parent_resource_id, + const std::string& resource_id, + const EntryActionCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, DRIVE_NO_CONNECTION)); + return CancelCallback(); + } + + EntryInfo* entry = FindEntryByResourceId(resource_id); + if (!entry) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND)); + return CancelCallback(); + } + + ChangeResource* change = &entry->change_resource; + // On the real Drive server, resources do not necessary shape a tree + // structure. That is, each resource can have multiple parent. + // We mimic the behavior here; AddResourceToDirectoy just adds + // one more parent, not overwriting old ones. + ParentReference parent; + parent.set_file_id(parent_resource_id); + parent.set_parent_link(GetFakeLinkUrl(parent_resource_id)); + change->mutable_file()->mutable_parents()->push_back(parent); + + AddNewChangestamp(change); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, HTTP_SUCCESS)); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(&FakeDriveService::NotifyObservers, + weak_ptr_factory_.GetWeakPtr())); + return CancelCallback(); +} + +CancelCallback FakeDriveService::RemoveResourceFromDirectory( + const std::string& parent_resource_id, + const std::string& resource_id, + const EntryActionCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, DRIVE_NO_CONNECTION)); + return CancelCallback(); + } + + EntryInfo* entry = FindEntryByResourceId(resource_id); + if (!entry) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND)); + return CancelCallback(); + } + + ChangeResource* change = &entry->change_resource; + FileResource* file = change->mutable_file(); + std::vector<ParentReference>* parents = file->mutable_parents(); + for (size_t i = 0; i < parents->size(); ++i) { + if ((*parents)[i].file_id() == parent_resource_id) { + parents->erase(parents->begin() + i); + AddNewChangestamp(change); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, HTTP_NO_CONTENT)); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(&FakeDriveService::NotifyObservers, + weak_ptr_factory_.GetWeakPtr())); + return CancelCallback(); + } + } + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND)); + return CancelCallback(); +} + +CancelCallback FakeDriveService::AddNewDirectory( + const std::string& parent_resource_id, + const std::string& directory_title, + const AddNewDirectoryOptions& options, + const FileResourceCallback& callback) { + return AddNewDirectoryWithResourceId( + "", + parent_resource_id.empty() ? GetRootResourceId() : parent_resource_id, + directory_title, + options, + callback); +} + +CancelCallback FakeDriveService::InitiateUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const UploadNewFileOptions& options, + const InitiateUploadCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, DRIVE_NO_CONNECTION, GURL())); + return CancelCallback(); + } + + if (parent_resource_id != GetRootResourceId() && + !entries_.count(parent_resource_id)) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_NOT_FOUND, GURL())); + return CancelCallback(); + } + + GURL session_url = GetNewUploadSessionUrl(); + upload_sessions_[session_url] = + UploadSession(content_type, content_length, + parent_resource_id, + "", // resource_id + "", // etag + title); + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_SUCCESS, session_url)); + return CancelCallback(); +} + +CancelCallback FakeDriveService::InitiateUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const UploadExistingFileOptions& options, + const InitiateUploadCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, DRIVE_NO_CONNECTION, GURL())); + return CancelCallback(); + } + + EntryInfo* entry = FindEntryByResourceId(resource_id); + if (!entry) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_NOT_FOUND, GURL())); + return CancelCallback(); + } + + if (!UserHasWriteAccess(entry->user_permission)) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_FORBIDDEN, GURL())); + return CancelCallback(); + } + + FileResource* file = entry->change_resource.mutable_file(); + if (!options.etag.empty() && options.etag != file->etag()) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_PRECONDITION, GURL())); + return CancelCallback(); + } + // TODO(hashimoto): Update |file|'s metadata with |options|. + + GURL session_url = GetNewUploadSessionUrl(); + upload_sessions_[session_url] = + UploadSession(content_type, content_length, + "", // parent_resource_id + resource_id, + file->etag(), + "" /* title */); + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_SUCCESS, session_url)); + return CancelCallback(); +} + +CancelCallback FakeDriveService::GetUploadStatus( + const GURL& upload_url, + int64 content_length, + const UploadRangeCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + return CancelCallback(); +} + +CancelCallback FakeDriveService::ResumeUpload( + const GURL& upload_url, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path, + const UploadRangeCallback& callback, + const ProgressCallback& progress_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + FileResourceCallback completion_callback + = base::Bind(&ScheduleUploadRangeCallback, + callback, start_position, end_position); + + if (offline_) { + completion_callback.Run(DRIVE_NO_CONNECTION, scoped_ptr<FileResource>()); + return CancelCallback(); + } + + if (!upload_sessions_.count(upload_url)) { + completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<FileResource>()); + return CancelCallback(); + } + + UploadSession* session = &upload_sessions_[upload_url]; + + // Chunks are required to be sent in such a ways that they fill from the start + // of the not-yet-uploaded part with no gaps nor overlaps. + if (session->uploaded_size != start_position) { + completion_callback.Run(HTTP_BAD_REQUEST, scoped_ptr<FileResource>()); + return CancelCallback(); + } + + if (!progress_callback.is_null()) { + // In the real GDataWapi/Drive DriveService, progress is reported in + // nondeterministic timing. In this fake implementation, we choose to call + // it twice per one ResumeUpload. This is for making sure that client code + // works fine even if the callback is invoked more than once; it is the + // crucial difference of the progress callback from others. + // Note that progress is notified in the relative offset in each chunk. + const int64 chunk_size = end_position - start_position; + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(progress_callback, chunk_size / 2, chunk_size)); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(progress_callback, chunk_size, chunk_size)); + } + + if (content_length != end_position) { + session->uploaded_size = end_position; + completion_callback.Run(HTTP_RESUME_INCOMPLETE, scoped_ptr<FileResource>()); + return CancelCallback(); + } + + std::string content_data; + if (!base::ReadFileToString(local_file_path, &content_data)) { + session->uploaded_size = end_position; + completion_callback.Run(DRIVE_FILE_ERROR, scoped_ptr<FileResource>()); + return CancelCallback(); + } + session->uploaded_size = end_position; + + // |resource_id| is empty if the upload is for new file. + if (session->resource_id.empty()) { + DCHECK(!session->parent_resource_id.empty()); + DCHECK(!session->title.empty()); + const EntryInfo* new_entry = AddNewEntry( + "", // auto generate resource id. + session->content_type, + content_data, + session->parent_resource_id, + session->title, + false); // shared_with_me + if (!new_entry) { + completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<FileResource>()); + return CancelCallback(); + } + + completion_callback.Run(HTTP_CREATED, make_scoped_ptr( + new FileResource(*new_entry->change_resource.file()))); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(&FakeDriveService::NotifyObservers, + weak_ptr_factory_.GetWeakPtr())); + return CancelCallback(); + } + + EntryInfo* entry = FindEntryByResourceId(session->resource_id); + if (!entry) { + completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<FileResource>()); + return CancelCallback(); + } + + ChangeResource* change = &entry->change_resource; + FileResource* file = change->mutable_file(); + if (file->etag().empty() || session->etag != file->etag()) { + completion_callback.Run(HTTP_PRECONDITION, scoped_ptr<FileResource>()); + return CancelCallback(); + } + + file->set_md5_checksum(base::MD5String(content_data)); + entry->content_data = content_data; + file->set_file_size(end_position); + AddNewChangestamp(change); + UpdateETag(file); + + completion_callback.Run(HTTP_SUCCESS, make_scoped_ptr( + new FileResource(*file))); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(&FakeDriveService::NotifyObservers, + weak_ptr_factory_.GetWeakPtr())); + return CancelCallback(); +} + +CancelCallback FakeDriveService::MultipartUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const base::FilePath& local_file_path, + const UploadNewFileOptions& options, + const FileResourceCallback& callback, + const ProgressCallback& progress_callback) { + CallResumeUpload* const call_resume_upload = new CallResumeUpload(); + call_resume_upload->service = weak_ptr_factory_.GetWeakPtr(); + call_resume_upload->content_type = content_type; + call_resume_upload->content_length = content_length; + call_resume_upload->local_file_path = local_file_path; + call_resume_upload->callback = callback; + call_resume_upload->progress_callback = progress_callback; + InitiateUploadNewFile( + content_type, + content_length, + parent_resource_id, + title, + options, + base::Bind(&CallResumeUpload::Run, base::Owned(call_resume_upload))); + return CancelCallback(); +} + +CancelCallback FakeDriveService::MultipartUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const base::FilePath& local_file_path, + const UploadExistingFileOptions& options, + const FileResourceCallback& callback, + const ProgressCallback& progress_callback) { + CallResumeUpload* const call_resume_upload = new CallResumeUpload(); + call_resume_upload->service = weak_ptr_factory_.GetWeakPtr(); + call_resume_upload->content_type = content_type; + call_resume_upload->content_length = content_length; + call_resume_upload->local_file_path = local_file_path; + call_resume_upload->callback = callback; + call_resume_upload->progress_callback = progress_callback; + InitiateUploadExistingFile( + content_type, + content_length, + resource_id, + options, + base::Bind(&CallResumeUpload::Run, base::Owned(call_resume_upload))); + return CancelCallback(); +} + +CancelCallback FakeDriveService::AuthorizeApp( + const std::string& resource_id, + const std::string& app_id, + const AuthorizeAppCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (entries_.count(resource_id) == 0) { + callback.Run(google_apis::HTTP_NOT_FOUND, GURL()); + return CancelCallback(); + } + + callback.Run(HTTP_SUCCESS, + GURL(base::StringPrintf(open_url_format_.c_str(), + resource_id.c_str(), + app_id.c_str()))); + return CancelCallback(); +} + +CancelCallback FakeDriveService::UninstallApp( + const std::string& app_id, + const google_apis::EntryActionCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, google_apis::DRIVE_NO_CONNECTION)); + return CancelCallback(); + } + + // Find app_id from app_info_value_ and delete. + base::ListValue* items = NULL; + if (!app_info_value_->GetList("items", &items)) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, google_apis::HTTP_NOT_FOUND)); + return CancelCallback(); + } + + for (size_t i = 0; i < items->GetSize(); ++i) { + base::DictionaryValue* item = NULL; + std::string id; + if (items->GetDictionary(i, &item) && item->GetString("id", &id) && + id == app_id) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, + items->Remove(i, NULL) ? google_apis::HTTP_NO_CONTENT + : google_apis::HTTP_NOT_FOUND)); + return CancelCallback(); + } + } + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, google_apis::HTTP_NOT_FOUND)); + return CancelCallback(); +} + +void FakeDriveService::AddNewFile(const std::string& content_type, + const std::string& content_data, + const std::string& parent_resource_id, + const std::string& title, + bool shared_with_me, + const FileResourceCallback& callback) { + AddNewFileWithResourceId("", content_type, content_data, parent_resource_id, + title, shared_with_me, callback); +} + +void FakeDriveService::AddNewFileWithResourceId( + const std::string& resource_id, + const std::string& content_type, + const std::string& content_data, + const std::string& parent_resource_id, + const std::string& title, + bool shared_with_me, + const FileResourceCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, + DRIVE_NO_CONNECTION, + base::Passed(scoped_ptr<FileResource>()))); + return; + } + + const EntryInfo* new_entry = AddNewEntry(resource_id, + content_type, + content_data, + parent_resource_id, + title, + shared_with_me); + if (!new_entry) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_NOT_FOUND, + base::Passed(scoped_ptr<FileResource>()))); + return; + } + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_CREATED, + base::Passed(make_scoped_ptr( + new FileResource(*new_entry->change_resource.file()))))); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(&FakeDriveService::NotifyObservers, + weak_ptr_factory_.GetWeakPtr())); +} + +CancelCallback FakeDriveService::AddNewDirectoryWithResourceId( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& directory_title, + const AddNewDirectoryOptions& options, + const FileResourceCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, + DRIVE_NO_CONNECTION, + base::Passed(scoped_ptr<FileResource>()))); + return CancelCallback(); + } + + const EntryInfo* new_entry = AddNewEntry(resource_id, + util::kDriveFolderMimeType, + "", // content_data + parent_resource_id, + directory_title, + false); // shared_with_me + if (!new_entry) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_NOT_FOUND, + base::Passed(scoped_ptr<FileResource>()))); + return CancelCallback(); + } + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_CREATED, + base::Passed(make_scoped_ptr( + new FileResource(*new_entry->change_resource.file()))))); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(&FakeDriveService::NotifyObservers, + weak_ptr_factory_.GetWeakPtr())); + return CancelCallback(); +} + +void FakeDriveService::SetLastModifiedTime( + const std::string& resource_id, + const base::Time& last_modified_time, + const FileResourceCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, + DRIVE_NO_CONNECTION, + base::Passed(scoped_ptr<FileResource>()))); + return; + } + + EntryInfo* entry = FindEntryByResourceId(resource_id); + if (!entry) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_NOT_FOUND, + base::Passed(scoped_ptr<FileResource>()))); + return; + } + + ChangeResource* change = &entry->change_resource; + FileResource* file = change->mutable_file(); + file->set_modified_date(last_modified_time); + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_SUCCESS, + base::Passed(make_scoped_ptr(new FileResource(*file))))); +} + +google_apis::DriveApiErrorCode FakeDriveService::SetUserPermission( + const std::string& resource_id, + google_apis::drive::PermissionRole user_permission) { + DCHECK(thread_checker_.CalledOnValidThread()); + + EntryInfo* entry = FindEntryByResourceId(resource_id); + if (!entry) + return HTTP_NOT_FOUND; + + entry->user_permission = user_permission; + return HTTP_SUCCESS; +} + +void FakeDriveService::AddChangeObserver(ChangeObserver* change_observer) { + change_observers_.AddObserver(change_observer); +} + +void FakeDriveService::RemoveChangeObserver(ChangeObserver* change_observer) { + change_observers_.RemoveObserver(change_observer); +} + +FakeDriveService::EntryInfo* FakeDriveService::FindEntryByResourceId( + const std::string& resource_id) { + DCHECK(thread_checker_.CalledOnValidThread()); + + EntryInfoMap::iterator it = entries_.find(resource_id); + // Deleted entries don't have FileResource. + return it != entries_.end() && it->second->change_resource.file() ? + it->second : NULL; +} + +std::string FakeDriveService::GetNewResourceId() { + DCHECK(thread_checker_.CalledOnValidThread()); + + ++resource_id_count_; + return base::StringPrintf("resource_id_%d", resource_id_count_); +} + +void FakeDriveService::UpdateETag(google_apis::FileResource* file) { + file->set_etag( + "etag_" + base::Int64ToString(about_resource_->largest_change_id())); +} + +void FakeDriveService::AddNewChangestamp(google_apis::ChangeResource* change) { + about_resource_->set_largest_change_id( + about_resource_->largest_change_id() + 1); + change->set_change_id(about_resource_->largest_change_id()); +} + +const FakeDriveService::EntryInfo* FakeDriveService::AddNewEntry( + const std::string& given_resource_id, + const std::string& content_type, + const std::string& content_data, + const std::string& parent_resource_id, + const std::string& title, + bool shared_with_me) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (!parent_resource_id.empty() && + parent_resource_id != GetRootResourceId() && + !entries_.count(parent_resource_id)) { + return NULL; + } + + const std::string resource_id = + given_resource_id.empty() ? GetNewResourceId() : given_resource_id; + if (entries_.count(resource_id)) + return NULL; + GURL upload_url = GURL("https://xxx/upload/" + resource_id); + + scoped_ptr<EntryInfo> new_entry(new EntryInfo); + ChangeResource* new_change = &new_entry->change_resource; + FileResource* new_file = new FileResource; + new_change->set_file(make_scoped_ptr(new_file)); + + // Set the resource ID and the title + new_change->set_file_id(resource_id); + new_file->set_file_id(resource_id); + new_file->set_title(title); + // Set the contents, size and MD5 for a file. + if (content_type != util::kDriveFolderMimeType && + !util::IsKnownHostedDocumentMimeType(content_type)) { + new_entry->content_data = content_data; + new_file->set_file_size(content_data.size()); + new_file->set_md5_checksum(base::MD5String(content_data)); + } + + if (shared_with_me) { + // Set current time to mark the file as shared_with_me. + new_file->set_shared_with_me_date(base::Time::Now()); + } + + std::string escaped_resource_id = net::EscapePath(resource_id); + + // Set mime type. + new_file->set_mime_type(content_type); + + // Set alternate link if needed. + if (content_type == util::kGoogleDocumentMimeType) + new_file->set_alternate_link(GURL("https://document_alternate_link")); + + // Set parents. + if (!parent_resource_id.empty()) { + ParentReference parent; + parent.set_file_id(parent_resource_id); + parent.set_parent_link(GetFakeLinkUrl(parent.file_id())); + std::vector<ParentReference> parents; + parents.push_back(parent); + *new_file->mutable_parents() = parents; + } + + new_entry->share_url = net::AppendOrReplaceQueryParameter( + share_url_base_, "name", title); + + AddNewChangestamp(new_change); + UpdateETag(new_file); + + base::Time published_date = + base::Time() + base::TimeDelta::FromMilliseconds(++published_date_seq_); + new_file->set_created_date(published_date); + + EntryInfo* raw_new_entry = new_entry.release(); + entries_[resource_id] = raw_new_entry; + return raw_new_entry; +} + +void FakeDriveService::GetChangeListInternal( + int64 start_changestamp, + const std::string& search_query, + const std::string& directory_resource_id, + int start_offset, + int max_results, + int* load_counter, + const ChangeListCallback& callback) { + if (offline_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, + DRIVE_NO_CONNECTION, + base::Passed(scoped_ptr<ChangeList>()))); + return; + } + + // Filter out entries per parameters like |directory_resource_id| and + // |search_query|. + ScopedVector<ChangeResource> entries; + int num_entries_matched = 0; + for (EntryInfoMap::iterator it = entries_.begin(); it != entries_.end(); + ++it) { + const ChangeResource& entry = it->second->change_resource; + bool should_exclude = false; + + // If |directory_resource_id| is set, exclude the entry if it's not in + // the target directory. + if (!directory_resource_id.empty()) { + // Get the parent resource ID of the entry. + std::string parent_resource_id; + if (entry.file() && !entry.file()->parents().empty()) + parent_resource_id = entry.file()->parents()[0].file_id(); + + if (directory_resource_id != parent_resource_id) + should_exclude = true; + } + + // If |search_query| is set, exclude the entry if it does not contain the + // search query in the title. + if (!should_exclude && !search_query.empty() && + !EntryMatchWithQuery(entry, search_query)) { + should_exclude = true; + } + + // If |start_changestamp| is set, exclude the entry if the + // changestamp is older than |largest_changestamp|. + // See https://developers.google.com/google-apps/documents-list/ + // #retrieving_all_changes_since_a_given_changestamp + if (start_changestamp > 0 && entry.change_id() < start_changestamp) + should_exclude = true; + + // If the caller requests other list than change list by specifying + // zero-|start_changestamp|, exclude deleted entry from the result. + const bool deleted = entry.is_deleted() || + (entry.file() && entry.file()->labels().is_trashed()); + if (!start_changestamp && deleted) + should_exclude = true; + + // The entry matched the criteria for inclusion. + if (!should_exclude) + ++num_entries_matched; + + // If |start_offset| is set, exclude the entry if the entry is before the + // start index. <= instead of < as |num_entries_matched| was + // already incremented. + if (start_offset > 0 && num_entries_matched <= start_offset) + should_exclude = true; + + if (!should_exclude) { + scoped_ptr<ChangeResource> entry_copied(new ChangeResource); + entry_copied->set_change_id(entry.change_id()); + entry_copied->set_file_id(entry.file_id()); + entry_copied->set_deleted(entry.is_deleted()); + if (entry.file()) { + entry_copied->set_file( + make_scoped_ptr(new FileResource(*entry.file()))); + } + entry_copied->set_modification_date(entry.modification_date()); + entries.push_back(entry_copied.release()); + } + } + + scoped_ptr<ChangeList> change_list(new ChangeList); + if (start_changestamp > 0 && start_offset == 0) { + change_list->set_largest_change_id(about_resource_->largest_change_id()); + } + + // If |max_results| is set, trim the entries if the number exceeded the max + // results. + if (max_results > 0 && entries.size() > static_cast<size_t>(max_results)) { + entries.erase(entries.begin() + max_results, entries.end()); + // Adds the next URL. + // Here, we embed information which is needed for continuing the + // GetChangeList request in the next invocation into url query + // parameters. + GURL next_url(base::StringPrintf( + "http://localhost/?start-offset=%d&max-results=%d", + start_offset + max_results, + max_results)); + if (start_changestamp > 0) { + next_url = net::AppendOrReplaceQueryParameter( + next_url, "changestamp", + base::Int64ToString(start_changestamp).c_str()); + } + if (!search_query.empty()) { + next_url = net::AppendOrReplaceQueryParameter( + next_url, "q", search_query); + } + if (!directory_resource_id.empty()) { + next_url = net::AppendOrReplaceQueryParameter( + next_url, "parent", directory_resource_id); + } + + change_list->set_next_link(next_url); + } + *change_list->mutable_items() = entries.Pass(); + + if (load_counter) + *load_counter += 1; + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, HTTP_SUCCESS, base::Passed(&change_list))); +} + +GURL FakeDriveService::GetNewUploadSessionUrl() { + return GURL("https://upload_session_url/" + + base::Int64ToString(next_upload_sequence_number_++)); +} + +google_apis::CancelCallback FakeDriveService::AddPermission( + const std::string& resource_id, + const std::string& email, + google_apis::drive::PermissionRole role, + const google_apis::EntryActionCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!callback.is_null()); + + NOTREACHED(); + return CancelCallback(); +} + +scoped_ptr<BatchRequestConfiguratorInterface> +FakeDriveService::StartBatchRequest() { + DCHECK(thread_checker_.CalledOnValidThread()); + + NOTREACHED(); + return scoped_ptr<BatchRequestConfiguratorInterface>(); +} + +void FakeDriveService::NotifyObservers() { + FOR_EACH_OBSERVER(ChangeObserver, change_observers_, OnNewChangeAvailable()); +} + +} // namespace drive diff --git a/components/drive/service/fake_drive_service.h b/components/drive/service/fake_drive_service.h new file mode 100644 index 0000000..09ae14a --- /dev/null +++ b/components/drive/service/fake_drive_service.h @@ -0,0 +1,406 @@ +// 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_SERVICE_FAKE_DRIVE_SERVICE_H_ +#define COMPONENTS_DRIVE_SERVICE_FAKE_DRIVE_SERVICE_H_ + +#include <string> + +#include "base/files/file_path.h" +#include "base/threading/thread_checker.h" +#include "components/drive/service/drive_service_interface.h" + +namespace base { +class DictionaryValue; +} + +namespace google_apis { +class AboutResource; +class ChangeResource; +class FileResource; +} + +namespace drive { + +// This class implements a fake DriveService which acts like a real Drive +// service. The fake service works as follows: +// +// 1) Load JSON files and construct the in-memory resource list. +// 2) Return valid responses based on the the in-memory resource list. +// 3) Update the in-memory resource list by requests like DeleteResource(). +class FakeDriveService : public DriveServiceInterface { + public: + class ChangeObserver { + public: + virtual ~ChangeObserver() {} + virtual void OnNewChangeAvailable() = 0; + }; + + FakeDriveService(); + ~FakeDriveService() override; + + // Loads the app list for Drive API. Returns true on success. + bool LoadAppListForDriveApi(const std::string& relative_path); + + // Adds an app to app list. + void AddApp(const std::string& app_id, + const std::string& app_name, + const std::string& product_id, + const std::string& create_url, + bool is_removable); + + // Removes an app by product id. + void RemoveAppByProductId(const std::string& product_id); + + // Returns true if the service knows the given drive app id. + bool HasApp(const std::string& app_id) const; + + // Changes the offline state. All functions fail with DRIVE_NO_CONNECTION + // when offline. By default the offline state is false. + void set_offline(bool offline) { offline_ = offline; } + + // GetAllFileList never returns result when this is set to true. + // Used to emulate the real server's slowness. + void set_never_return_all_file_list(bool value) { + never_return_all_file_list_ = value; + } + + // Changes the default max results returned from GetAllFileList(). + // By default, it's set to 0, which is unlimited. + void set_default_max_results(int default_max_results) { + default_max_results_ = default_max_results; + } + + // Sets the url to the test server to be used as a base for generated share + // urls to the share dialog. + void set_share_url_base(const GURL& share_url_base) { + share_url_base_ = share_url_base; + } + + // Changes the quota fields returned from GetAboutResource(). + void SetQuotaValue(int64 used, int64 total); + + // Returns the AboutResource. + const google_apis::AboutResource& about_resource() const { + return *about_resource_; + } + + // Returns the number of times the file list is successfully loaded by + // GetAllFileList(). + int file_list_load_count() const { return file_list_load_count_; } + + // Returns the number of times the resource list is successfully loaded by + // GetChangeList(). + int change_list_load_count() const { return change_list_load_count_; } + + // Returns the number of times the resource list is successfully loaded by + // GetFileListInDirectory(). + int directory_load_count() const { return directory_load_count_; } + + // Returns the number of times the about resource is successfully loaded + // by GetAboutResource(). + int about_resource_load_count() const { + return about_resource_load_count_; + } + + // Returns the number of times the app list is successfully loaded by + // GetAppList(). + int app_list_load_count() const { return app_list_load_count_; } + + // Returns the number of times GetAllFileList are blocked due to + // set_never_return_all_file_list(). + int blocked_file_list_load_count() const { + return blocked_file_list_load_count_; + } + + // Returns the file path whose request is cancelled just before this method + // invocation. + const base::FilePath& last_cancelled_file() const { + return last_cancelled_file_; + } + + // Returns the (fake) URL for the link. + static GURL GetFakeLinkUrl(const std::string& resource_id); + + // Sets the printf format for constructing the response of AuthorizeApp(). + // The format string must include two %s that are to be filled with + // resource_id and app_id. + void set_open_url_format(const std::string& url_format) { + open_url_format_ = url_format; + } + + // DriveServiceInterface Overrides + void Initialize(const std::string& account_id) override; + void AddObserver(DriveServiceObserver* observer) override; + void RemoveObserver(DriveServiceObserver* observer) override; + bool CanSendRequest() const override; + std::string GetRootResourceId() const override; + bool HasAccessToken() const override; + void RequestAccessToken( + const google_apis::AuthStatusCallback& callback) override; + bool HasRefreshToken() const override; + void ClearAccessToken() override; + void ClearRefreshToken() override; + google_apis::CancelCallback GetAllFileList( + const google_apis::FileListCallback& callback) override; + google_apis::CancelCallback GetFileListInDirectory( + const std::string& directory_resource_id, + const google_apis::FileListCallback& callback) override; + // See the comment for EntryMatchWidthQuery() in .cc file for details about + // the supported search query types. + google_apis::CancelCallback Search( + const std::string& search_query, + const google_apis::FileListCallback& callback) override; + google_apis::CancelCallback SearchByTitle( + const std::string& title, + const std::string& directory_resource_id, + const google_apis::FileListCallback& callback) override; + google_apis::CancelCallback GetChangeList( + int64 start_changestamp, + const google_apis::ChangeListCallback& callback) override; + google_apis::CancelCallback GetRemainingChangeList( + const GURL& next_link, + const google_apis::ChangeListCallback& callback) override; + google_apis::CancelCallback GetRemainingFileList( + const GURL& next_link, + const google_apis::FileListCallback& callback) override; + google_apis::CancelCallback GetFileResource( + const std::string& resource_id, + const google_apis::FileResourceCallback& callback) override; + google_apis::CancelCallback GetShareUrl( + const std::string& resource_id, + const GURL& embed_origin, + const google_apis::GetShareUrlCallback& callback) override; + google_apis::CancelCallback GetAboutResource( + const google_apis::AboutResourceCallback& callback) override; + google_apis::CancelCallback GetAppList( + const google_apis::AppListCallback& callback) override; + google_apis::CancelCallback DeleteResource( + const std::string& resource_id, + const std::string& etag, + const google_apis::EntryActionCallback& callback) override; + google_apis::CancelCallback TrashResource( + const std::string& resource_id, + const google_apis::EntryActionCallback& callback) override; + google_apis::CancelCallback DownloadFile( + const base::FilePath& local_cache_path, + const std::string& resource_id, + const google_apis::DownloadActionCallback& download_action_callback, + const google_apis::GetContentCallback& get_content_callback, + const google_apis::ProgressCallback& progress_callback) override; + google_apis::CancelCallback CopyResource( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const google_apis::FileResourceCallback& callback) override; + google_apis::CancelCallback UpdateResource( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& new_title, + const base::Time& last_modified, + const base::Time& last_viewed_by_me, + const google_apis::drive::Properties& properties, + const google_apis::FileResourceCallback& callback) override; + google_apis::CancelCallback AddResourceToDirectory( + const std::string& parent_resource_id, + const std::string& resource_id, + const google_apis::EntryActionCallback& callback) override; + google_apis::CancelCallback RemoveResourceFromDirectory( + const std::string& parent_resource_id, + const std::string& resource_id, + const google_apis::EntryActionCallback& callback) override; + google_apis::CancelCallback AddNewDirectory( + const std::string& parent_resource_id, + const std::string& directory_title, + const AddNewDirectoryOptions& options, + const google_apis::FileResourceCallback& callback) override; + google_apis::CancelCallback InitiateUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const UploadNewFileOptions& options, + const google_apis::InitiateUploadCallback& callback) override; + google_apis::CancelCallback InitiateUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const UploadExistingFileOptions& options, + const google_apis::InitiateUploadCallback& callback) override; + google_apis::CancelCallback ResumeUpload( + const GURL& upload_url, + int64 start_position, + int64 end_position, + int64 content_length, + const std::string& content_type, + const base::FilePath& local_file_path, + const google_apis::drive::UploadRangeCallback& callback, + const google_apis::ProgressCallback& progress_callback) override; + google_apis::CancelCallback GetUploadStatus( + const GURL& upload_url, + int64 content_length, + const google_apis::drive::UploadRangeCallback& callback) override; + google_apis::CancelCallback MultipartUploadNewFile( + const std::string& content_type, + int64 content_length, + const std::string& parent_resource_id, + const std::string& title, + const base::FilePath& local_file_path, + const UploadNewFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) override; + google_apis::CancelCallback MultipartUploadExistingFile( + const std::string& content_type, + int64 content_length, + const std::string& resource_id, + const base::FilePath& local_file_path, + const UploadExistingFileOptions& options, + const google_apis::FileResourceCallback& callback, + const google_apis::ProgressCallback& progress_callback) override; + google_apis::CancelCallback AuthorizeApp( + const std::string& resource_id, + const std::string& app_id, + const google_apis::AuthorizeAppCallback& callback) override; + google_apis::CancelCallback UninstallApp( + const std::string& app_id, + const google_apis::EntryActionCallback& callback) override; + google_apis::CancelCallback AddPermission( + const std::string& resource_id, + const std::string& email, + google_apis::drive::PermissionRole role, + const google_apis::EntryActionCallback& callback) override; + scoped_ptr<BatchRequestConfiguratorInterface> StartBatchRequest() override; + + // Adds a new file with the given parameters. On success, returns + // HTTP_CREATED with the parsed entry. + // |callback| must not be null. + void AddNewFile(const std::string& content_type, + const std::string& content_data, + const std::string& parent_resource_id, + const std::string& title, + bool shared_with_me, + const google_apis::FileResourceCallback& callback); + + // Adds a new file with the given |resource_id|. If the id already exists, + // it's an error. This is used for testing cross profile file sharing that + // needs to have matching resource IDs in different fake service instances. + // |callback| must not be null. + void AddNewFileWithResourceId( + const std::string& resource_id, + const std::string& content_type, + const std::string& content_data, + const std::string& parent_resource_id, + const std::string& title, + bool shared_with_me, + const google_apis::FileResourceCallback& callback); + + // Adds a new directory with the given |resource_id|. + // |callback| must not be null. + google_apis::CancelCallback AddNewDirectoryWithResourceId( + const std::string& resource_id, + const std::string& parent_resource_id, + const std::string& directory_title, + const AddNewDirectoryOptions& options, + const google_apis::FileResourceCallback& callback); + + // Sets the last modified time for an entry specified by |resource_id|. + // On success, returns HTTP_SUCCESS with the parsed entry. + // |callback| must not be null. + void SetLastModifiedTime( + const std::string& resource_id, + const base::Time& last_modified_time, + const google_apis::FileResourceCallback& callback); + + // Sets the user's permission for an entry specified by |resource_id|. + google_apis::DriveApiErrorCode SetUserPermission( + const std::string& resource_id, + google_apis::drive::PermissionRole user_permission); + + void AddChangeObserver(ChangeObserver* observer); + void RemoveChangeObserver(ChangeObserver* observer); + + private: + struct EntryInfo; + struct UploadSession; + + // Returns a pointer to the entry that matches |resource_id|, or NULL if + // not found. + EntryInfo* FindEntryByResourceId(const std::string& resource_id); + + // Returns a new resource ID, which looks like "resource_id_<num>" where + // <num> is a monotonically increasing number starting from 1. + std::string GetNewResourceId(); + + // Increments |largest_changestamp_| and adds the new changestamp. + void AddNewChangestamp(google_apis::ChangeResource* change); + + // Update ETag of |file| based on |largest_changestamp_|. + void UpdateETag(google_apis::FileResource* file); + + // Adds a new entry based on the given parameters. + // |resource_id| can be empty, in the case, the id is automatically generated. + // Returns a pointer to the newly added entry, or NULL if failed. + const EntryInfo* AddNewEntry( + const std::string& resource_id, + const std::string& content_type, + const std::string& content_data, + const std::string& parent_resource_id, + const std::string& title, + bool shared_with_me); + + // Core implementation of GetChangeList. + // This method returns the slice of the all matched entries, and its range + // is between |start_offset| (inclusive) and |start_offset| + |max_results| + // (exclusive). + // Increments *load_counter by 1 before it returns successfully. + void GetChangeListInternal( + int64 start_changestamp, + const std::string& search_query, + const std::string& directory_resource_id, + int start_offset, + int max_results, + int* load_counter, + const google_apis::ChangeListCallback& callback); + + // Returns new upload session URL. + GURL GetNewUploadSessionUrl(); + + void NotifyObservers(); + + // The class is expected to run on UI thread. + base::ThreadChecker thread_checker_; + + typedef std::map<std::string, EntryInfo*> EntryInfoMap; + EntryInfoMap entries_; + scoped_ptr<google_apis::AboutResource> about_resource_; + scoped_ptr<base::DictionaryValue> app_info_value_; + std::map<GURL, UploadSession> upload_sessions_; + int64 published_date_seq_; + int64 next_upload_sequence_number_; + int default_max_results_; + int resource_id_count_; + int file_list_load_count_; + int change_list_load_count_; + int directory_load_count_; + int about_resource_load_count_; + int app_list_load_count_; + int blocked_file_list_load_count_; + bool offline_; + bool never_return_all_file_list_; + base::FilePath last_cancelled_file_; + GURL share_url_base_; + std::string app_json_template_; + std::string open_url_format_; + + base::ObserverList<ChangeObserver> change_observers_; + + base::WeakPtrFactory<FakeDriveService> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(FakeDriveService); +}; + +} // namespace drive + +#endif // COMPONENTS_DRIVE_SERVICE_FAKE_DRIVE_SERVICE_H_ diff --git a/components/drive/service/fake_drive_service_unittest.cc b/components/drive/service/fake_drive_service_unittest.cc new file mode 100644 index 0000000..009f8ca --- /dev/null +++ b/components/drive/service/fake_drive_service_unittest.cc @@ -0,0 +1,2177 @@ +// 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/service/fake_drive_service.h" + +#include <string> +#include <vector> + +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/md5.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/stl_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "components/drive/service/test_util.h" +#include "google_apis/drive/drive_api_parser.h" +#include "google_apis/drive/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +using google_apis::AboutResource; +using google_apis::AppList; +using google_apis::ChangeList; +using google_apis::ChangeResource; +using google_apis::FileList; +using google_apis::FileResource; +using google_apis::DRIVE_NO_CONNECTION; +using google_apis::DRIVE_OTHER_ERROR; +using google_apis::DriveApiErrorCode; +using google_apis::GetContentCallback; +using google_apis::HTTP_CREATED; +using google_apis::HTTP_FORBIDDEN; +using google_apis::HTTP_NOT_FOUND; +using google_apis::HTTP_NO_CONTENT; +using google_apis::HTTP_PRECONDITION; +using google_apis::HTTP_RESUME_INCOMPLETE; +using google_apis::HTTP_SUCCESS; +using google_apis::ProgressCallback; +using google_apis::UploadRangeResponse; + +namespace drive { + +namespace test_util { + +using google_apis::test_util::AppendProgressCallbackResult; +using google_apis::test_util::CreateCopyResultCallback; +using google_apis::test_util::ProgressInfo; +using google_apis::test_util::TestGetContentCallback; +using google_apis::test_util::WriteStringToFile; + +} // namespace test_util + +namespace { + +class FakeDriveServiceTest : public testing::Test { + protected: + // Returns the resource entry that matches |resource_id|. + scoped_ptr<FileResource> FindEntry(const std::string& resource_id) { + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.GetFileResource( + resource_id, test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + return entry.Pass(); + } + + // Returns true if the resource identified by |resource_id| exists. + bool Exists(const std::string& resource_id) { + scoped_ptr<FileResource> entry = FindEntry(resource_id); + return entry && !entry->labels().is_trashed(); + } + + // Adds a new directory at |parent_resource_id| with the given name. + // Returns true on success. + bool AddNewDirectory(const std::string& parent_resource_id, + const std::string& directory_title) { + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.AddNewDirectory( + parent_resource_id, directory_title, AddNewDirectoryOptions(), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + return error == HTTP_CREATED; + } + + // Returns true if the resource identified by |resource_id| has a parent + // identified by |parent_id|. + bool HasParent(const std::string& resource_id, const std::string& parent_id) { + scoped_ptr<FileResource> entry = FindEntry(resource_id); + if (entry) { + for (size_t i = 0; i < entry->parents().size(); ++i) { + if (entry->parents()[i].file_id() == parent_id) + return true; + } + } + return false; + } + + int64 GetLargestChangeByAboutResource() { + DriveApiErrorCode error; + scoped_ptr<AboutResource> about_resource; + fake_service_.GetAboutResource( + test_util::CreateCopyResultCallback(&error, &about_resource)); + base::RunLoop().RunUntilIdle(); + return about_resource->largest_change_id(); + } + + base::MessageLoop message_loop_; + FakeDriveService fake_service_; +}; + +TEST_F(FakeDriveServiceTest, GetAllFileList) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.GetAllFileList( + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + // Do some sanity check. + EXPECT_EQ(15U, file_list->items().size()); + EXPECT_EQ(1, fake_service_.file_list_load_count()); +} + +TEST_F(FakeDriveServiceTest, GetAllFileList_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.GetAllFileList( + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_FALSE(file_list); +} + +TEST_F(FakeDriveServiceTest, GetFileListInDirectory_InRootDirectory) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.GetFileListInDirectory( + fake_service_.GetRootResourceId(), + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + // Do some sanity check. There are 8 entries in the root directory. + EXPECT_EQ(8U, file_list->items().size()); + EXPECT_EQ(1, fake_service_.directory_load_count()); +} + +TEST_F(FakeDriveServiceTest, GetFileListInDirectory_InNonRootDirectory) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.GetFileListInDirectory( + "1_folder_resource_id", + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + // Do some sanity check. There is three entries in 1_folder_resource_id + // directory. + EXPECT_EQ(3U, file_list->items().size()); + EXPECT_EQ(1, fake_service_.directory_load_count()); +} + +TEST_F(FakeDriveServiceTest, GetFileListInDirectory_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.GetFileListInDirectory( + fake_service_.GetRootResourceId(), + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_FALSE(file_list); +} + +TEST_F(FakeDriveServiceTest, Search) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.Search( + "File", // search_query + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + // Do some sanity check. There are 4 entries that contain "File" in their + // titles. + EXPECT_EQ(4U, file_list->items().size()); +} + +TEST_F(FakeDriveServiceTest, Search_WithAttribute) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.Search( + "title:1.txt", // search_query + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + // Do some sanity check. There are 4 entries that contain "1.txt" in their + // titles. + EXPECT_EQ(4U, file_list->items().size()); +} + +TEST_F(FakeDriveServiceTest, Search_MultipleQueries) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.Search( + "Directory 1", // search_query + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + // There are 2 entries that contain both "Directory" and "1" in their titles. + EXPECT_EQ(2U, file_list->items().size()); + + fake_service_.Search( + "\"Directory 1\"", // search_query + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + // There is 1 entry that contain "Directory 1" in its title. + EXPECT_EQ(1U, file_list->items().size()); +} + +TEST_F(FakeDriveServiceTest, Search_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.Search( + "Directory 1", // search_query + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_FALSE(file_list); +} + +TEST_F(FakeDriveServiceTest, Search_Deleted) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.DeleteResource("2_file_resource_id", + std::string(), // etag + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(HTTP_NO_CONTENT, error); + + error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.Search( + "File", // search_query + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + // Do some sanity check. There are 4 entries that contain "File" in their + // titles and one of them is deleted. + EXPECT_EQ(3U, file_list->items().size()); +} + +TEST_F(FakeDriveServiceTest, Search_Trashed) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.TrashResource("2_file_resource_id", + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(HTTP_SUCCESS, error); + + error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.Search( + "File", // search_query + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + // Do some sanity check. There are 4 entries that contain "File" in their + // titles and one of them is deleted. + EXPECT_EQ(3U, file_list->items().size()); +} + +TEST_F(FakeDriveServiceTest, SearchByTitle) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.SearchByTitle( + "1.txt", // title + fake_service_.GetRootResourceId(), // directory_resource_id + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + // Do some sanity check. There are 2 entries that contain "1.txt" in their + // titles directly under the root directory. + EXPECT_EQ(2U, file_list->items().size()); +} + +TEST_F(FakeDriveServiceTest, SearchByTitle_EmptyDirectoryResourceId) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.SearchByTitle( + "1.txt", // title + "", // directory resource id + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + // Do some sanity check. There are 4 entries that contain "1.txt" in their + // titles. + EXPECT_EQ(4U, file_list->items().size()); +} + +TEST_F(FakeDriveServiceTest, SearchByTitle_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.SearchByTitle( + "Directory 1", // title + fake_service_.GetRootResourceId(), // directory_resource_id + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_FALSE(file_list); +} + +TEST_F(FakeDriveServiceTest, GetChangeList_NoNewEntries) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<ChangeList> change_list; + fake_service_.GetChangeList( + fake_service_.about_resource().largest_change_id() + 1, + test_util::CreateCopyResultCallback(&error, &change_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(change_list); + EXPECT_EQ(fake_service_.about_resource().largest_change_id(), + change_list->largest_change_id()); + // This should be empty as the latest changestamp was passed to + // GetChangeList(), hence there should be no new entries. + EXPECT_EQ(0U, change_list->items().size()); + // It's considered loaded even if the result is empty. + EXPECT_EQ(1, fake_service_.change_list_load_count()); +} + +TEST_F(FakeDriveServiceTest, GetChangeList_WithNewEntry) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + const int64 old_largest_change_id = + fake_service_.about_resource().largest_change_id(); + + // Add a new directory in the root directory. + ASSERT_TRUE(AddNewDirectory( + fake_service_.GetRootResourceId(), "new directory")); + + // Get the resource list newer than old_largest_change_id. + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<ChangeList> change_list; + fake_service_.GetChangeList( + old_largest_change_id + 1, + test_util::CreateCopyResultCallback(&error, &change_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(change_list); + EXPECT_EQ(fake_service_.about_resource().largest_change_id(), + change_list->largest_change_id()); + // The result should only contain the newly created directory. + ASSERT_EQ(1U, change_list->items().size()); + ASSERT_TRUE(change_list->items()[0]->file()); + EXPECT_EQ("new directory", change_list->items()[0]->file()->title()); + EXPECT_EQ(1, fake_service_.change_list_load_count()); +} + +TEST_F(FakeDriveServiceTest, GetChangeList_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<ChangeList> change_list; + fake_service_.GetChangeList( + 654321, // start_changestamp + test_util::CreateCopyResultCallback(&error, &change_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_FALSE(change_list); +} + +TEST_F(FakeDriveServiceTest, GetChangeList_DeletedEntry) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + ASSERT_TRUE(Exists("2_file_resource_id")); + const int64 old_largest_change_id = + fake_service_.about_resource().largest_change_id(); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.DeleteResource("2_file_resource_id", + std::string(), // etag + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + ASSERT_EQ(HTTP_NO_CONTENT, error); + ASSERT_FALSE(Exists("2_file_resource_id")); + + // Get the resource list newer than old_largest_change_id. + error = DRIVE_OTHER_ERROR; + scoped_ptr<ChangeList> change_list; + fake_service_.GetChangeList( + old_largest_change_id + 1, + test_util::CreateCopyResultCallback(&error, &change_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(change_list); + EXPECT_EQ(fake_service_.about_resource().largest_change_id(), + change_list->largest_change_id()); + // The result should only contain the deleted file. + ASSERT_EQ(1U, change_list->items().size()); + const ChangeResource& item = *change_list->items()[0]; + EXPECT_EQ("2_file_resource_id", item.file_id()); + EXPECT_FALSE(item.file()); + EXPECT_TRUE(item.is_deleted()); + EXPECT_EQ(1, fake_service_.change_list_load_count()); +} + +TEST_F(FakeDriveServiceTest, GetChangeList_TrashedEntry) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + ASSERT_TRUE(Exists("2_file_resource_id")); + const int64 old_largest_change_id = + fake_service_.about_resource().largest_change_id(); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.TrashResource("2_file_resource_id", + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + ASSERT_EQ(HTTP_SUCCESS, error); + ASSERT_FALSE(Exists("2_file_resource_id")); + + // Get the resource list newer than old_largest_change_id. + error = DRIVE_OTHER_ERROR; + scoped_ptr<ChangeList> change_list; + fake_service_.GetChangeList( + old_largest_change_id + 1, + test_util::CreateCopyResultCallback(&error, &change_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(change_list); + EXPECT_EQ(fake_service_.about_resource().largest_change_id(), + change_list->largest_change_id()); + // The result should only contain the trashed file. + ASSERT_EQ(1U, change_list->items().size()); + const ChangeResource& item = *change_list->items()[0]; + EXPECT_EQ("2_file_resource_id", item.file_id()); + ASSERT_TRUE(item.file()); + EXPECT_TRUE(item.file()->labels().is_trashed()); + EXPECT_EQ(1, fake_service_.change_list_load_count()); +} + +TEST_F(FakeDriveServiceTest, GetRemainingFileList_GetAllFileList) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_default_max_results(6); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.GetAllFileList( + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + + // Do some sanity check. + // The number of results is 14 entries. Thus, it should split into three + // chunks: 6, 6, and then 2. + EXPECT_EQ(6U, file_list->items().size()); + EXPECT_EQ(1, fake_service_.file_list_load_count()); + + // Second page loading. + // Keep the next url before releasing the |file_list|. + GURL next_url(file_list->next_link()); + + error = DRIVE_OTHER_ERROR; + file_list.reset(); + fake_service_.GetRemainingFileList( + next_url, + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + + EXPECT_EQ(6U, file_list->items().size()); + EXPECT_EQ(1, fake_service_.file_list_load_count()); + + // Third page loading. + next_url = file_list->next_link(); + + error = DRIVE_OTHER_ERROR; + file_list.reset(); + fake_service_.GetRemainingFileList( + next_url, + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + + EXPECT_EQ(3U, file_list->items().size()); + EXPECT_EQ(1, fake_service_.file_list_load_count()); +} + +TEST_F(FakeDriveServiceTest, GetRemainingFileList_GetFileListInDirectory) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_default_max_results(3); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.GetFileListInDirectory( + fake_service_.GetRootResourceId(), + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + + // Do some sanity check. + // The number of results is 8 entries. Thus, it should split into three + // chunks: 3, 3, and then 2. + EXPECT_EQ(3U, file_list->items().size()); + EXPECT_EQ(1, fake_service_.directory_load_count()); + + // Second page loading. + // Keep the next url before releasing the |file_list|. + GURL next_url = file_list->next_link(); + + error = DRIVE_OTHER_ERROR; + file_list.reset(); + fake_service_.GetRemainingFileList( + next_url, + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + + EXPECT_EQ(3U, file_list->items().size()); + EXPECT_EQ(1, fake_service_.directory_load_count()); + + // Third page loading. + next_url = file_list->next_link(); + + error = DRIVE_OTHER_ERROR; + file_list.reset(); + fake_service_.GetRemainingFileList( + next_url, + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + + EXPECT_EQ(2U, file_list->items().size()); + EXPECT_EQ(1, fake_service_.directory_load_count()); +} + +TEST_F(FakeDriveServiceTest, GetRemainingFileList_Search) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_default_max_results(2); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileList> file_list; + fake_service_.Search( + "File", // search_query + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + + // Do some sanity check. + // The number of results is 4 entries. Thus, it should split into two + // chunks: 2, and then 2 + EXPECT_EQ(2U, file_list->items().size()); + + // Second page loading. + // Keep the next url before releasing the |file_list|. + GURL next_url = file_list->next_link(); + + error = DRIVE_OTHER_ERROR; + file_list.reset(); + fake_service_.GetRemainingFileList( + next_url, + test_util::CreateCopyResultCallback(&error, &file_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(file_list); + + EXPECT_EQ(2U, file_list->items().size()); +} + +TEST_F(FakeDriveServiceTest, GetRemainingChangeList_GetChangeList) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_default_max_results(2); + const int64 old_largest_change_id = + fake_service_.about_resource().largest_change_id(); + + // Add 5 new directory in the root directory. + for (int i = 0; i < 5; ++i) { + ASSERT_TRUE(AddNewDirectory( + fake_service_.GetRootResourceId(), + base::StringPrintf("new directory %d", i))); + } + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<ChangeList> change_list; + fake_service_.GetChangeList( + old_largest_change_id + 1, // start_changestamp + test_util::CreateCopyResultCallback(&error, &change_list)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(change_list); + + // Do some sanity check. + // The number of results is 5 entries. Thus, it should split into three + // chunks: 2, 2 and then 1. + EXPECT_EQ(2U, change_list->items().size()); + EXPECT_EQ(1, fake_service_.change_list_load_count()); + + // Second page loading. + // Keep the next url before releasing the |change_list|. + GURL next_url = change_list->next_link(); + + error = DRIVE_OTHER_ERROR; + change_list.reset(); + fake_service_.GetRemainingChangeList( + next_url, + test_util::CreateCopyResultCallback(&error, &change_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(change_list); + + EXPECT_EQ(2U, change_list->items().size()); + EXPECT_EQ(1, fake_service_.change_list_load_count()); + + // Third page loading. + next_url = change_list->next_link(); + + error = DRIVE_OTHER_ERROR; + change_list.reset(); + fake_service_.GetRemainingChangeList( + next_url, + test_util::CreateCopyResultCallback(&error, &change_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(change_list); + + EXPECT_EQ(1U, change_list->items().size()); + EXPECT_EQ(1, fake_service_.change_list_load_count()); +} + +TEST_F(FakeDriveServiceTest, GetAboutResource) { + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<AboutResource> about_resource; + fake_service_.GetAboutResource( + test_util::CreateCopyResultCallback(&error, &about_resource)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + + ASSERT_TRUE(about_resource); + // Do some sanity check. + EXPECT_EQ(fake_service_.GetRootResourceId(), + about_resource->root_folder_id()); + EXPECT_EQ(1, fake_service_.about_resource_load_count()); +} + +TEST_F(FakeDriveServiceTest, GetAboutResource_Offline) { + fake_service_.set_offline(true); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<AboutResource> about_resource; + fake_service_.GetAboutResource( + test_util::CreateCopyResultCallback(&error, &about_resource)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_FALSE(about_resource); +} + +TEST_F(FakeDriveServiceTest, GetAppList) { + ASSERT_TRUE(fake_service_.LoadAppListForDriveApi( + "drive/applist.json")); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<AppList> app_list; + fake_service_.GetAppList( + test_util::CreateCopyResultCallback(&error, &app_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + + ASSERT_TRUE(app_list); + EXPECT_EQ(1, fake_service_.app_list_load_count()); +} + +TEST_F(FakeDriveServiceTest, GetAppList_Offline) { + ASSERT_TRUE(fake_service_.LoadAppListForDriveApi( + "drive/applist.json")); + fake_service_.set_offline(true); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<AppList> app_list; + fake_service_.GetAppList( + test_util::CreateCopyResultCallback(&error, &app_list)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_FALSE(app_list); +} + +TEST_F(FakeDriveServiceTest, GetFileResource_ExistingFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + const std::string kResourceId = "2_file_resource_id"; + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.GetFileResource( + kResourceId, test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(entry); + // Do some sanity check. + EXPECT_EQ(kResourceId, entry->file_id()); +} + +TEST_F(FakeDriveServiceTest, GetFileResource_NonexistingFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + const std::string kResourceId = "nonexisting_resource_id"; + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.GetFileResource( + kResourceId, test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NOT_FOUND, error); + ASSERT_FALSE(entry); +} + +TEST_F(FakeDriveServiceTest, GetFileResource_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + const std::string kResourceId = "2_file_resource_id"; + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.GetFileResource( + kResourceId, test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_FALSE(entry); +} + +TEST_F(FakeDriveServiceTest, GetShareUrl) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + const std::string kResourceId = "2_file_resource_id"; + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL share_url; + fake_service_.GetShareUrl( + kResourceId, + GURL(), // embed origin + test_util::CreateCopyResultCallback(&error, &share_url)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_FALSE(share_url.is_empty()); +} + +TEST_F(FakeDriveServiceTest, DeleteResource_ExistingFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + // Resource "2_file_resource_id" should now exist. + ASSERT_TRUE(Exists("2_file_resource_id")); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.DeleteResource("2_file_resource_id", + std::string(), // etag + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NO_CONTENT, error); + // Resource "2_file_resource_id" should be gone now. + EXPECT_FALSE(Exists("2_file_resource_id")); + + error = DRIVE_OTHER_ERROR; + fake_service_.DeleteResource("2_file_resource_id", + std::string(), // etag + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(HTTP_NOT_FOUND, error); + EXPECT_FALSE(Exists("2_file_resource_id")); +} + +TEST_F(FakeDriveServiceTest, DeleteResource_NonexistingFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.DeleteResource("nonexisting_resource_id", + std::string(), // etag + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NOT_FOUND, error); +} + +TEST_F(FakeDriveServiceTest, DeleteResource_ETagMatch) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + // Resource "2_file_resource_id" should now exist. + scoped_ptr<FileResource> entry = FindEntry("2_file_resource_id"); + ASSERT_TRUE(entry); + ASSERT_FALSE(entry->labels().is_trashed()); + ASSERT_FALSE(entry->etag().empty()); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.DeleteResource("2_file_resource_id", + entry->etag() + "_mismatch", + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_PRECONDITION, error); + // Resource "2_file_resource_id" should still exist. + EXPECT_TRUE(Exists("2_file_resource_id")); + + error = DRIVE_OTHER_ERROR; + fake_service_.DeleteResource("2_file_resource_id", + entry->etag(), + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(HTTP_NO_CONTENT, error); + // Resource "2_file_resource_id" should be gone now. + EXPECT_FALSE(Exists("2_file_resource_id")); +} + +TEST_F(FakeDriveServiceTest, DeleteResource_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.DeleteResource("2_file_resource_id", + std::string(), // etag + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); +} + +TEST_F(FakeDriveServiceTest, DeleteResource_Forbidden) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + EXPECT_EQ(HTTP_SUCCESS, fake_service_.SetUserPermission( + "2_file_resource_id", google_apis::drive::PERMISSION_ROLE_READER)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.DeleteResource("2_file_resource_id", + std::string(), // etag + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_FORBIDDEN, error); +} + +TEST_F(FakeDriveServiceTest, TrashResource_ExistingFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + // Resource "2_file_resource_id" should now exist. + ASSERT_TRUE(Exists("2_file_resource_id")); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.TrashResource("2_file_resource_id", + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + // Resource "2_file_resource_id" should be gone now. + EXPECT_FALSE(Exists("2_file_resource_id")); + + error = DRIVE_OTHER_ERROR; + fake_service_.TrashResource("2_file_resource_id", + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(HTTP_NOT_FOUND, error); + EXPECT_FALSE(Exists("2_file_resource_id")); +} + +TEST_F(FakeDriveServiceTest, TrashResource_NonexistingFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.TrashResource("nonexisting_resource_id", + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NOT_FOUND, error); +} + +TEST_F(FakeDriveServiceTest, TrashResource_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.TrashResource("2_file_resource_id", + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); +} + +TEST_F(FakeDriveServiceTest, TrashResource_Forbidden) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + EXPECT_EQ(HTTP_SUCCESS, fake_service_.SetUserPermission( + "2_file_resource_id", google_apis::drive::PERMISSION_ROLE_READER)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.TrashResource("2_file_resource_id", + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_FORBIDDEN, error); +} + +TEST_F(FakeDriveServiceTest, DownloadFile_ExistingFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + + std::vector<test_util::ProgressInfo> download_progress_values; + + const base::FilePath kOutputFilePath = + temp_dir.path().AppendASCII("whatever.txt"); + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + base::FilePath output_file_path; + test_util::TestGetContentCallback get_content_callback; + fake_service_.DownloadFile( + kOutputFilePath, + "2_file_resource_id", + test_util::CreateCopyResultCallback(&error, &output_file_path), + get_content_callback.callback(), + base::Bind(&test_util::AppendProgressCallbackResult, + &download_progress_values)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_EQ(output_file_path, kOutputFilePath); + std::string content; + ASSERT_TRUE(base::ReadFileToString(output_file_path, &content)); + EXPECT_EQ("This is some test content.", content); + ASSERT_TRUE(!download_progress_values.empty()); + EXPECT_TRUE(base::STLIsSorted(download_progress_values)); + EXPECT_LE(0, download_progress_values.front().first); + EXPECT_GE(26, download_progress_values.back().first); + EXPECT_EQ(content, get_content_callback.GetConcatenatedData()); +} + +TEST_F(FakeDriveServiceTest, DownloadFile_NonexistingFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + + const base::FilePath kOutputFilePath = + temp_dir.path().AppendASCII("whatever.txt"); + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + base::FilePath output_file_path; + fake_service_.DownloadFile( + kOutputFilePath, + "non_existent_file_resource_id", + test_util::CreateCopyResultCallback(&error, &output_file_path), + GetContentCallback(), + ProgressCallback()); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NOT_FOUND, error); +} + +TEST_F(FakeDriveServiceTest, DownloadFile_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + + const base::FilePath kOutputFilePath = + temp_dir.path().AppendASCII("whatever.txt"); + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + base::FilePath output_file_path; + fake_service_.DownloadFile( + kOutputFilePath, + "2_file_resource_id", + test_util::CreateCopyResultCallback(&error, &output_file_path), + GetContentCallback(), + ProgressCallback()); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); +} + +TEST_F(FakeDriveServiceTest, CopyResource) { + const base::Time::Exploded kModifiedDate = {2012, 7, 0, 19, 15, 59, 13, 123}; + + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + const std::string kResourceId = "2_file_resource_id"; + const std::string kParentResourceId = "2_folder_resource_id"; + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.CopyResource( + kResourceId, + kParentResourceId, + "new title", + base::Time::FromUTCExploded(kModifiedDate), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(entry); + // The copied entry should have the new resource ID and the title. + EXPECT_NE(kResourceId, entry->file_id()); + EXPECT_EQ("new title", entry->title()); + EXPECT_EQ(base::Time::FromUTCExploded(kModifiedDate), entry->modified_date()); + EXPECT_TRUE(HasParent(entry->file_id(), kParentResourceId)); + // Should be incremented as a new hosted document was created. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); +} + +TEST_F(FakeDriveServiceTest, CopyResource_NonExisting) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + const std::string kResourceId = "nonexisting_resource_id"; + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.CopyResource( + kResourceId, + "1_folder_resource_id", + "new title", + base::Time(), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NOT_FOUND, error); +} + +TEST_F(FakeDriveServiceTest, CopyResource_EmptyParentResourceId) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + const std::string kResourceId = "2_file_resource_id"; + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.CopyResource( + kResourceId, + std::string(), + "new title", + base::Time(), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(entry); + // The copied entry should have the new resource ID and the title. + EXPECT_NE(kResourceId, entry->file_id()); + EXPECT_EQ("new title", entry->title()); + EXPECT_TRUE(HasParent(kResourceId, fake_service_.GetRootResourceId())); + // Should be incremented as a new hosted document was created. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); +} + +TEST_F(FakeDriveServiceTest, CopyResource_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + const std::string kResourceId = "2_file_resource_id"; + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.CopyResource( + kResourceId, + "1_folder_resource_id", + "new title", + base::Time(), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_FALSE(entry); +} + +TEST_F(FakeDriveServiceTest, UpdateResource) { + const base::Time::Exploded kModifiedDate = {2012, 7, 0, 19, 15, 59, 13, 123}; + const base::Time::Exploded kViewedDate = {2013, 8, 1, 20, 16, 00, 14, 234}; + + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + const std::string kResourceId = "2_file_resource_id"; + const std::string kParentResourceId = "2_folder_resource_id"; + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.UpdateResource( + kResourceId, kParentResourceId, "new title", + base::Time::FromUTCExploded(kModifiedDate), + base::Time::FromUTCExploded(kViewedDate), + google_apis::drive::Properties(), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(entry); + // The updated entry should have the new title. + EXPECT_EQ(kResourceId, entry->file_id()); + EXPECT_EQ("new title", entry->title()); + EXPECT_EQ(base::Time::FromUTCExploded(kModifiedDate), + entry->modified_date()); + EXPECT_EQ(base::Time::FromUTCExploded(kViewedDate), + entry->last_viewed_by_me_date()); + EXPECT_TRUE(HasParent(kResourceId, kParentResourceId)); + // Should be incremented as a new hosted document was created. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); +} + +TEST_F(FakeDriveServiceTest, UpdateResource_NonExisting) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + const std::string kResourceId = "nonexisting_resource_id"; + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.UpdateResource( + kResourceId, "1_folder_resource_id", "new title", base::Time(), + base::Time(), google_apis::drive::Properties(), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NOT_FOUND, error); +} + +TEST_F(FakeDriveServiceTest, UpdateResource_EmptyParentResourceId) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + const std::string kResourceId = "2_file_resource_id"; + + // Just make sure that the resource is under root. + ASSERT_TRUE(HasParent(kResourceId, "fake_root")); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.UpdateResource( + kResourceId, std::string(), "new title", base::Time(), base::Time(), + google_apis::drive::Properties(), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(entry); + // The updated entry should have the new title. + EXPECT_EQ(kResourceId, entry->file_id()); + EXPECT_EQ("new title", entry->title()); + EXPECT_TRUE(HasParent(kResourceId, "fake_root")); + // Should be incremented as a new hosted document was created. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); +} + +TEST_F(FakeDriveServiceTest, UpdateResource_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + const std::string kResourceId = "2_file_resource_id"; + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.UpdateResource( + kResourceId, std::string(), "new title", base::Time(), base::Time(), + google_apis::drive::Properties(), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_FALSE(entry); +} + +TEST_F(FakeDriveServiceTest, UpdateResource_Forbidden) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + const std::string kResourceId = "2_file_resource_id"; + EXPECT_EQ(HTTP_SUCCESS, fake_service_.SetUserPermission( + kResourceId, google_apis::drive::PERMISSION_ROLE_READER)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.UpdateResource( + kResourceId, std::string(), "new title", base::Time(), base::Time(), + google_apis::drive::Properties(), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_FORBIDDEN, error); + EXPECT_FALSE(entry); +} + +TEST_F(FakeDriveServiceTest, AddResourceToDirectory_FileInRootDirectory) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + const std::string kResourceId = "2_file_resource_id"; + const std::string kOldParentResourceId = fake_service_.GetRootResourceId(); + const std::string kNewParentResourceId = "1_folder_resource_id"; + + // Here's the original parent link. + EXPECT_TRUE(HasParent(kResourceId, kOldParentResourceId)); + EXPECT_FALSE(HasParent(kResourceId, kNewParentResourceId)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.AddResourceToDirectory( + kNewParentResourceId, + kResourceId, + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + + // The parent link should now be changed. + EXPECT_TRUE(HasParent(kResourceId, kOldParentResourceId)); + EXPECT_TRUE(HasParent(kResourceId, kNewParentResourceId)); + // Should be incremented as a file was moved. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); +} + +TEST_F(FakeDriveServiceTest, AddResourceToDirectory_FileInNonRootDirectory) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + const std::string kResourceId = "subdirectory_file_1_id"; + const std::string kOldParentResourceId = "1_folder_resource_id"; + const std::string kNewParentResourceId = "2_folder_resource_id"; + + // Here's the original parent link. + EXPECT_TRUE(HasParent(kResourceId, kOldParentResourceId)); + EXPECT_FALSE(HasParent(kResourceId, kNewParentResourceId)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.AddResourceToDirectory( + kNewParentResourceId, + kResourceId, + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + + // The parent link should now be changed. + EXPECT_TRUE(HasParent(kResourceId, kOldParentResourceId)); + EXPECT_TRUE(HasParent(kResourceId, kNewParentResourceId)); + // Should be incremented as a file was moved. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); +} + +TEST_F(FakeDriveServiceTest, AddResourceToDirectory_NonexistingFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + const std::string kResourceId = "nonexisting_file"; + const std::string kNewParentResourceId = "1_folder_resource_id"; + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.AddResourceToDirectory( + kNewParentResourceId, + kResourceId, + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NOT_FOUND, error); +} + +TEST_F(FakeDriveServiceTest, AddResourceToDirectory_OrphanFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + const std::string kResourceId = "1_orphanfile_resource_id"; + const std::string kNewParentResourceId = "1_folder_resource_id"; + + // The file does not belong to any directory, even to the root. + EXPECT_FALSE(HasParent(kResourceId, kNewParentResourceId)); + EXPECT_FALSE(HasParent(kResourceId, fake_service_.GetRootResourceId())); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.AddResourceToDirectory( + kNewParentResourceId, + kResourceId, + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + + // The parent link should now be changed. + EXPECT_TRUE(HasParent(kResourceId, kNewParentResourceId)); + EXPECT_FALSE(HasParent(kResourceId, fake_service_.GetRootResourceId())); + // Should be incremented as a file was moved. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); +} + +TEST_F(FakeDriveServiceTest, AddResourceToDirectory_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + const std::string kResourceId = "2_file_resource_id"; + const std::string kNewParentResourceId = "1_folder_resource_id"; + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.AddResourceToDirectory( + kNewParentResourceId, + kResourceId, + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); +} + +TEST_F(FakeDriveServiceTest, RemoveResourceFromDirectory_ExistingFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + const std::string kResourceId = "subdirectory_file_1_id"; + const std::string kParentResourceId = "1_folder_resource_id"; + + scoped_ptr<FileResource> entry = FindEntry(kResourceId); + ASSERT_TRUE(entry); + // The entry should have a parent now. + ASSERT_FALSE(entry->parents().empty()); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.RemoveResourceFromDirectory( + kParentResourceId, + kResourceId, + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NO_CONTENT, error); + + entry = FindEntry(kResourceId); + ASSERT_TRUE(entry); + // The entry should have no parent now. + ASSERT_TRUE(entry->parents().empty()); + // Should be incremented as a file was moved to the root directory. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); +} + +TEST_F(FakeDriveServiceTest, RemoveResourceFromDirectory_NonexistingFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + const std::string kResourceId = "nonexisting_file"; + const std::string kParentResourceId = "1_folder_resource_id"; + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.RemoveResourceFromDirectory( + kParentResourceId, + kResourceId, + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NOT_FOUND, error); +} + +TEST_F(FakeDriveServiceTest, RemoveResourceFromDirectory_OrphanFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + const std::string kResourceId = "1_orphanfile_resource_id"; + const std::string kParentResourceId = fake_service_.GetRootResourceId(); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.RemoveResourceFromDirectory( + kParentResourceId, + kResourceId, + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NOT_FOUND, error); +} + +TEST_F(FakeDriveServiceTest, RemoveResourceFromDirectory_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + const std::string kResourceId = "subdirectory_file_1_id"; + const std::string kParentResourceId = "1_folder_resource_id"; + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + fake_service_.RemoveResourceFromDirectory( + kParentResourceId, + kResourceId, + test_util::CreateCopyResultCallback(&error)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); +} + +TEST_F(FakeDriveServiceTest, AddNewDirectory_EmptyParent) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.AddNewDirectory( + std::string(), "new directory", AddNewDirectoryOptions(), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_CREATED, error); + ASSERT_TRUE(entry); + EXPECT_TRUE(entry->IsDirectory()); + EXPECT_EQ("resource_id_1", entry->file_id()); + EXPECT_EQ("new directory", entry->title()); + EXPECT_TRUE(HasParent(entry->file_id(), fake_service_.GetRootResourceId())); + // Should be incremented as a new directory was created. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); +} + +TEST_F(FakeDriveServiceTest, AddNewDirectory_ToRootDirectory) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.AddNewDirectory( + fake_service_.GetRootResourceId(), "new directory", + AddNewDirectoryOptions(), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_CREATED, error); + ASSERT_TRUE(entry); + EXPECT_TRUE(entry->IsDirectory()); + EXPECT_EQ("resource_id_1", entry->file_id()); + EXPECT_EQ("new directory", entry->title()); + EXPECT_TRUE(HasParent(entry->file_id(), fake_service_.GetRootResourceId())); + // Should be incremented as a new directory was created. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); +} + +TEST_F(FakeDriveServiceTest, AddNewDirectory_ToRootDirectoryOnEmptyFileSystem) { + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.AddNewDirectory( + fake_service_.GetRootResourceId(), "new directory", + AddNewDirectoryOptions(), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_CREATED, error); + ASSERT_TRUE(entry); + EXPECT_TRUE(entry->IsDirectory()); + EXPECT_EQ("resource_id_1", entry->file_id()); + EXPECT_EQ("new directory", entry->title()); + EXPECT_TRUE(HasParent(entry->file_id(), fake_service_.GetRootResourceId())); + // Should be incremented as a new directory was created. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); +} + +TEST_F(FakeDriveServiceTest, AddNewDirectory_ToNonRootDirectory) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + const std::string kParentResourceId = "1_folder_resource_id"; + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.AddNewDirectory( + kParentResourceId, "new directory", AddNewDirectoryOptions(), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_CREATED, error); + ASSERT_TRUE(entry); + EXPECT_TRUE(entry->IsDirectory()); + EXPECT_EQ("resource_id_1", entry->file_id()); + EXPECT_EQ("new directory", entry->title()); + EXPECT_TRUE(HasParent(entry->file_id(), kParentResourceId)); + // Should be incremented as a new directory was created. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); +} + +TEST_F(FakeDriveServiceTest, AddNewDirectory_ToNonexistingDirectory) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + const std::string kParentResourceId = "nonexisting_resource_id"; + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.AddNewDirectory( + kParentResourceId, "new directory", AddNewDirectoryOptions(), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NOT_FOUND, error); + EXPECT_FALSE(entry); +} + +TEST_F(FakeDriveServiceTest, AddNewDirectory_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.AddNewDirectory( + fake_service_.GetRootResourceId(), "new directory", + AddNewDirectoryOptions(), + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_FALSE(entry); +} + +TEST_F(FakeDriveServiceTest, InitiateUploadNewFile_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + fake_service_.InitiateUploadNewFile( + "test/foo", 13, "1_folder_resource_id", "new file.foo", + UploadNewFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_TRUE(upload_location.is_empty()); +} + +TEST_F(FakeDriveServiceTest, InitiateUploadNewFile_NotFound) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + fake_service_.InitiateUploadNewFile( + "test/foo", 13, "non_existent", "new file.foo", UploadNewFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NOT_FOUND, error); + EXPECT_TRUE(upload_location.is_empty()); +} + +TEST_F(FakeDriveServiceTest, InitiateUploadNewFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + fake_service_.InitiateUploadNewFile( + "test/foo", 13, "1_folder_resource_id", "new file.foo", + UploadNewFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_FALSE(upload_location.is_empty()); + EXPECT_NE(GURL("https://1_folder_resumable_create_media_link?mode=newfile"), + upload_location); +} + +TEST_F(FakeDriveServiceTest, InitiateUploadExistingFile_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + fake_service_.InitiateUploadExistingFile( + "test/foo", 13, "2_file_resource_id", UploadExistingFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_TRUE(upload_location.is_empty()); +} + +TEST_F(FakeDriveServiceTest, InitiateUploadExistingFile_Forbidden) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + EXPECT_EQ(HTTP_SUCCESS, fake_service_.SetUserPermission( + "2_file_resource_id", google_apis::drive::PERMISSION_ROLE_READER)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + fake_service_.InitiateUploadExistingFile( + "test/foo", 13, "2_file_resource_id", UploadExistingFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_FORBIDDEN, error); + EXPECT_TRUE(upload_location.is_empty()); +} + +TEST_F(FakeDriveServiceTest, InitiateUploadExistingFile_NotFound) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + fake_service_.InitiateUploadExistingFile( + "test/foo", 13, "non_existent", UploadExistingFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NOT_FOUND, error); + EXPECT_TRUE(upload_location.is_empty()); +} + +TEST_F(FakeDriveServiceTest, InitiateUploadExistingFile_WrongETag) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + UploadExistingFileOptions options; + options.etag = "invalid_etag"; + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + fake_service_.InitiateUploadExistingFile( + "text/plain", + 13, + "2_file_resource_id", + options, + test_util::CreateCopyResultCallback(&error, &upload_location)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_PRECONDITION, error); + EXPECT_TRUE(upload_location.is_empty()); +} + +TEST_F(FakeDriveServiceTest, InitiateUpload_ExistingFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + scoped_ptr<FileResource> entry = FindEntry("2_file_resource_id"); + ASSERT_TRUE(entry); + + UploadExistingFileOptions options; + options.etag = entry->etag(); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + fake_service_.InitiateUploadExistingFile( + "text/plain", + 13, + "2_file_resource_id", + options, + test_util::CreateCopyResultCallback(&error, &upload_location)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_TRUE(upload_location.is_valid()); +} + +TEST_F(FakeDriveServiceTest, ResumeUpload_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + fake_service_.InitiateUploadNewFile( + "test/foo", 15, "1_folder_resource_id", "new file.foo", + UploadNewFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_FALSE(upload_location.is_empty()); + EXPECT_NE(GURL("https://1_folder_resumable_create_media_link"), + upload_location); + + fake_service_.set_offline(true); + + UploadRangeResponse response; + scoped_ptr<FileResource> entry; + fake_service_.ResumeUpload( + upload_location, + 0, 13, 15, "test/foo", + base::FilePath(), + test_util::CreateCopyResultCallback(&response, &entry), + ProgressCallback()); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, response.code); + EXPECT_FALSE(entry.get()); +} + +TEST_F(FakeDriveServiceTest, ResumeUpload_NotFound) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + fake_service_.InitiateUploadNewFile( + "test/foo", 15, "1_folder_resource_id", "new file.foo", + UploadNewFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location)); + base::RunLoop().RunUntilIdle(); + + ASSERT_EQ(HTTP_SUCCESS, error); + + UploadRangeResponse response; + scoped_ptr<FileResource> entry; + fake_service_.ResumeUpload( + GURL("https://foo.com/"), + 0, 13, 15, "test/foo", + base::FilePath(), + test_util::CreateCopyResultCallback(&response, &entry), + ProgressCallback()); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NOT_FOUND, response.code); + EXPECT_FALSE(entry.get()); +} + +TEST_F(FakeDriveServiceTest, ResumeUpload_ExistingFile) { + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + base::FilePath local_file_path = + temp_dir.path().Append(FILE_PATH_LITERAL("File 1.txt")); + std::string contents("hogefugapiyo"); + ASSERT_TRUE(test_util::WriteStringToFile(local_file_path, contents)); + + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + scoped_ptr<FileResource> entry = FindEntry("2_file_resource_id"); + ASSERT_TRUE(entry); + + UploadExistingFileOptions options; + options.etag = entry->etag(); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + fake_service_.InitiateUploadExistingFile( + "text/plain", + contents.size(), + "2_file_resource_id", + options, + test_util::CreateCopyResultCallback(&error, &upload_location)); + base::RunLoop().RunUntilIdle(); + + ASSERT_EQ(HTTP_SUCCESS, error); + + UploadRangeResponse response; + entry.reset(); + std::vector<test_util::ProgressInfo> upload_progress_values; + fake_service_.ResumeUpload( + upload_location, + 0, contents.size() / 2, contents.size(), "text/plain", + local_file_path, + test_util::CreateCopyResultCallback(&response, &entry), + base::Bind(&test_util::AppendProgressCallbackResult, + &upload_progress_values)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code); + EXPECT_FALSE(entry.get()); + ASSERT_TRUE(!upload_progress_values.empty()); + EXPECT_TRUE(base::STLIsSorted(upload_progress_values)); + EXPECT_LE(0, upload_progress_values.front().first); + EXPECT_GE(static_cast<int64>(contents.size() / 2), + upload_progress_values.back().first); + + upload_progress_values.clear(); + fake_service_.ResumeUpload( + upload_location, + contents.size() / 2, contents.size(), contents.size(), "text/plain", + local_file_path, + test_util::CreateCopyResultCallback(&response, &entry), + base::Bind(&test_util::AppendProgressCallbackResult, + &upload_progress_values)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, response.code); + EXPECT_TRUE(entry.get()); + EXPECT_EQ(static_cast<int64>(contents.size()), entry->file_size()); + EXPECT_TRUE(Exists(entry->file_id())); + ASSERT_TRUE(!upload_progress_values.empty()); + EXPECT_TRUE(base::STLIsSorted(upload_progress_values)); + EXPECT_LE(0, upload_progress_values.front().first); + EXPECT_GE(static_cast<int64>(contents.size() - contents.size() / 2), + upload_progress_values.back().first); + EXPECT_EQ(base::MD5String(contents), entry->md5_checksum()); +} + +TEST_F(FakeDriveServiceTest, ResumeUpload_NewFile) { + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + base::FilePath local_file_path = + temp_dir.path().Append(FILE_PATH_LITERAL("new file.foo")); + std::string contents("hogefugapiyo"); + ASSERT_TRUE(test_util::WriteStringToFile(local_file_path, contents)); + + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + GURL upload_location; + fake_service_.InitiateUploadNewFile( + "test/foo", contents.size(), "1_folder_resource_id", "new file.foo", + UploadNewFileOptions(), + test_util::CreateCopyResultCallback(&error, &upload_location)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + EXPECT_FALSE(upload_location.is_empty()); + EXPECT_NE(GURL("https://1_folder_resumable_create_media_link"), + upload_location); + + UploadRangeResponse response; + scoped_ptr<FileResource> entry; + std::vector<test_util::ProgressInfo> upload_progress_values; + fake_service_.ResumeUpload( + upload_location, + 0, contents.size() / 2, contents.size(), "test/foo", + local_file_path, + test_util::CreateCopyResultCallback(&response, &entry), + base::Bind(&test_util::AppendProgressCallbackResult, + &upload_progress_values)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_RESUME_INCOMPLETE, response.code); + EXPECT_FALSE(entry.get()); + ASSERT_TRUE(!upload_progress_values.empty()); + EXPECT_TRUE(base::STLIsSorted(upload_progress_values)); + EXPECT_LE(0, upload_progress_values.front().first); + EXPECT_GE(static_cast<int64>(contents.size() / 2), + upload_progress_values.back().first); + + upload_progress_values.clear(); + fake_service_.ResumeUpload( + upload_location, + contents.size() / 2, contents.size(), contents.size(), "test/foo", + local_file_path, + test_util::CreateCopyResultCallback(&response, &entry), + base::Bind(&test_util::AppendProgressCallbackResult, + &upload_progress_values)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_CREATED, response.code); + EXPECT_TRUE(entry.get()); + EXPECT_EQ(static_cast<int64>(contents.size()), entry->file_size()); + EXPECT_TRUE(Exists(entry->file_id())); + ASSERT_TRUE(!upload_progress_values.empty()); + EXPECT_TRUE(base::STLIsSorted(upload_progress_values)); + EXPECT_LE(0, upload_progress_values.front().first); + EXPECT_GE(static_cast<int64>(contents.size() - contents.size() / 2), + upload_progress_values.back().first); + EXPECT_EQ(base::MD5String(contents), entry->md5_checksum()); +} + +TEST_F(FakeDriveServiceTest, AddNewFile_ToRootDirectory) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + const std::string kContentType = "text/plain"; + const std::string kContentData = "This is some test content."; + const std::string kTitle = "new file"; + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.AddNewFile( + kContentType, + kContentData, + fake_service_.GetRootResourceId(), + kTitle, + false, // shared_with_me + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_CREATED, error); + ASSERT_TRUE(entry); + EXPECT_EQ(kContentType, entry->mime_type()); + EXPECT_EQ(static_cast<int64>(kContentData.size()), entry->file_size()); + EXPECT_EQ("resource_id_1", entry->file_id()); + EXPECT_EQ(kTitle, entry->title()); + EXPECT_TRUE(HasParent(entry->file_id(), fake_service_.GetRootResourceId())); + // Should be incremented as a new directory was created. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); + EXPECT_EQ(base::MD5String(kContentData), entry->md5_checksum()); +} + +TEST_F(FakeDriveServiceTest, AddNewFile_ToRootDirectoryOnEmptyFileSystem) { + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + const std::string kContentType = "text/plain"; + const std::string kContentData = "This is some test content."; + const std::string kTitle = "new file"; + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.AddNewFile( + kContentType, + kContentData, + fake_service_.GetRootResourceId(), + kTitle, + false, // shared_with_me + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_CREATED, error); + ASSERT_TRUE(entry); + EXPECT_EQ(kContentType, entry->mime_type()); + EXPECT_EQ(static_cast<int64>(kContentData.size()), entry->file_size()); + EXPECT_EQ("resource_id_1", entry->file_id()); + EXPECT_EQ(kTitle, entry->title()); + EXPECT_TRUE(HasParent(entry->file_id(), fake_service_.GetRootResourceId())); + // Should be incremented as a new directory was created. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); + EXPECT_EQ(base::MD5String(kContentData), entry->md5_checksum()); +} + +TEST_F(FakeDriveServiceTest, AddNewFile_ToNonRootDirectory) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + const std::string kContentType = "text/plain"; + const std::string kContentData = "This is some test content."; + const std::string kTitle = "new file"; + const std::string kParentResourceId = "1_folder_resource_id"; + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.AddNewFile( + kContentType, + kContentData, + kParentResourceId, + kTitle, + false, // shared_with_me + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_CREATED, error); + ASSERT_TRUE(entry); + EXPECT_EQ(kContentType, entry->mime_type()); + EXPECT_EQ(static_cast<int64>(kContentData.size()), entry->file_size()); + EXPECT_EQ("resource_id_1", entry->file_id()); + EXPECT_EQ(kTitle, entry->title()); + EXPECT_TRUE(HasParent(entry->file_id(), kParentResourceId)); + // Should be incremented as a new directory was created. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); + EXPECT_EQ(base::MD5String(kContentData), entry->md5_checksum()); +} + +TEST_F(FakeDriveServiceTest, AddNewFile_ToNonexistingDirectory) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + const std::string kContentType = "text/plain"; + const std::string kContentData = "This is some test content."; + const std::string kTitle = "new file"; + const std::string kParentResourceId = "nonexisting_resource_id"; + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.AddNewFile( + kContentType, + kContentData, + kParentResourceId, + kTitle, + false, // shared_with_me + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NOT_FOUND, error); + EXPECT_FALSE(entry); +} + +TEST_F(FakeDriveServiceTest, AddNewFile_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + const std::string kContentType = "text/plain"; + const std::string kContentData = "This is some test content."; + const std::string kTitle = "new file"; + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.AddNewFile( + kContentType, + kContentData, + fake_service_.GetRootResourceId(), + kTitle, + false, // shared_with_me + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_FALSE(entry); +} + +TEST_F(FakeDriveServiceTest, AddNewFile_SharedWithMeLabel) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + const std::string kContentType = "text/plain"; + const std::string kContentData = "This is some test content."; + const std::string kTitle = "new file"; + + int64 old_largest_change_id = GetLargestChangeByAboutResource(); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.AddNewFile( + kContentType, + kContentData, + fake_service_.GetRootResourceId(), + kTitle, + true, // shared_with_me + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_CREATED, error); + ASSERT_TRUE(entry); + EXPECT_EQ(kContentType, entry->mime_type()); + EXPECT_EQ(static_cast<int64>(kContentData.size()), entry->file_size()); + EXPECT_EQ("resource_id_1", entry->file_id()); + EXPECT_EQ(kTitle, entry->title()); + EXPECT_TRUE(HasParent(entry->file_id(), fake_service_.GetRootResourceId())); + EXPECT_FALSE(entry->shared_with_me_date().is_null()); + // Should be incremented as a new directory was created. + EXPECT_EQ(old_largest_change_id + 1, + fake_service_.about_resource().largest_change_id()); + EXPECT_EQ(old_largest_change_id + 1, GetLargestChangeByAboutResource()); + EXPECT_EQ(base::MD5String(kContentData), entry->md5_checksum()); +} + +TEST_F(FakeDriveServiceTest, SetLastModifiedTime_ExistingFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + const std::string kResourceId = "2_file_resource_id"; + base::Time time; + ASSERT_TRUE(base::Time::FromString("1 April 2013 12:34:56", &time)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.SetLastModifiedTime( + kResourceId, + time, + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_TRUE(entry); + EXPECT_EQ(time, entry->modified_date()); +} + +TEST_F(FakeDriveServiceTest, SetLastModifiedTime_NonexistingFile) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + + const std::string kResourceId = "nonexisting_resource_id"; + base::Time time; + ASSERT_TRUE(base::Time::FromString("1 April 2013 12:34:56", &time)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.SetLastModifiedTime( + kResourceId, + time, + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(HTTP_NOT_FOUND, error); + EXPECT_FALSE(entry); +} + +TEST_F(FakeDriveServiceTest, SetLastModifiedTime_Offline) { + ASSERT_TRUE(test_util::SetUpTestEntries(&fake_service_)); + fake_service_.set_offline(true); + + const std::string kResourceId = "2_file_resource_id"; + base::Time time; + ASSERT_TRUE(base::Time::FromString("1 April 2013 12:34:56", &time)); + + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + fake_service_.SetLastModifiedTime( + kResourceId, + time, + test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(DRIVE_NO_CONNECTION, error); + EXPECT_FALSE(entry); +} + +} // namespace + +} // namespace drive diff --git a/components/drive/service/test_util.cc b/components/drive/service/test_util.cc new file mode 100644 index 0000000..42c54f1 --- /dev/null +++ b/components/drive/service/test_util.cc @@ -0,0 +1,193 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/drive/service/test_util.h" + +#include "base/run_loop.h" +#include "components/drive/drive_api_util.h" +#include "components/drive/service/fake_drive_service.h" +#include "google_apis/drive/drive_api_parser.h" +#include "google_apis/drive/test_util.h" + +using google_apis::FileResource; +using google_apis::DRIVE_OTHER_ERROR; +using google_apis::DriveApiErrorCode; +using google_apis::HTTP_CREATED; + +namespace drive { +namespace test_util { + +bool SetUpTestEntries(FakeDriveService* drive_service) { + DriveApiErrorCode error = DRIVE_OTHER_ERROR; + scoped_ptr<FileResource> entry; + + drive_service->AddNewFileWithResourceId( + "2_file_resource_id", + "audio/mpeg", + "This is some test content.", + drive_service->GetRootResourceId(), + "File 1.txt", + false, // shared_with_me + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + if (error != HTTP_CREATED) + return false; + + drive_service->AddNewFileWithResourceId( + "slash_file_resource_id", + "audio/mpeg", + "This is some test content.", + drive_service->GetRootResourceId(), + "Slash / in file 1.txt", + false, // shared_with_me + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + if (error != HTTP_CREATED) + return false; + + drive_service->AddNewFileWithResourceId( + "3_file_resource_id", + "audio/mpeg", + "This is some test content.", + drive_service->GetRootResourceId(), + "Duplicate Name.txt", + false, // shared_with_me + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + if (error != HTTP_CREATED) + return false; + + drive_service->AddNewFileWithResourceId( + "4_file_resource_id", + "audio/mpeg", + "This is some test content.", + drive_service->GetRootResourceId(), + "Duplicate Name.txt", + false, // shared_with_me + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + if (error != HTTP_CREATED) + return false; + + drive_service->AddNewFileWithResourceId( + "5_document_resource_id", + util::kGoogleDocumentMimeType, + std::string(), + drive_service->GetRootResourceId(), + "Document 1 excludeDir-test", + false, // shared_with_me + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + if (error != HTTP_CREATED) + return false; + + drive_service->AddNewFileWithResourceId( + "1_folder_resource_id", + util::kDriveFolderMimeType, + std::string(), + drive_service->GetRootResourceId(), + "Directory 1", + false, // shared_with_me + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + if (error != HTTP_CREATED) + return false; + + drive_service->AddNewFileWithResourceId( + "subdirectory_file_1_id", + "audio/mpeg", + "This is some test content.", + "1_folder_resource_id", + "SubDirectory File 1.txt", + false, // shared_with_me + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + if (error != HTTP_CREATED) + return false; + + drive_service->AddNewFileWithResourceId( + "subdirectory_unowned_file_1_id", + "audio/mpeg", + "This is some test content.", + "1_folder_resource_id", + "Shared to The Account Owner.txt", + true, // shared_with_me + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + if (error != HTTP_CREATED) + return false; + + drive_service->AddNewDirectoryWithResourceId( + "sub_dir_folder_resource_id", "1_folder_resource_id", + "Sub Directory Folder", AddNewDirectoryOptions(), + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + if (error != HTTP_CREATED) + return false; + + drive_service->AddNewDirectoryWithResourceId( + "sub_sub_directory_folder_id", "sub_dir_folder_resource_id", + "Sub Sub Directory Folder", AddNewDirectoryOptions(), + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + if (error != HTTP_CREATED) + return false; + + drive_service->AddNewDirectoryWithResourceId( + "slash_dir_folder_resource_id", drive_service->GetRootResourceId(), + "Slash / in directory", AddNewDirectoryOptions(), + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + if (error != HTTP_CREATED) + return false; + + drive_service->AddNewFileWithResourceId( + "slash_subdir_file", + "audio/mpeg", + "This is some test content.", + "slash_dir_folder_resource_id", + "Slash SubDir File.txt", + false, // shared_with_me + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + if (error != HTTP_CREATED) + return false; + + drive_service->AddNewDirectoryWithResourceId( + "sub_dir_folder_2_self_link", drive_service->GetRootResourceId(), + "Directory 2 excludeDir-test", AddNewDirectoryOptions(), + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + if (error != HTTP_CREATED) + return false; + + drive_service->AddNewFileWithResourceId( + "1_orphanfile_resource_id", + "text/plain", + "This is some test content.", + std::string(), + "Orphan File 1.txt", + true, // shared_with_me + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + if (error != HTTP_CREATED) + return false; + + drive_service->AddNewFileWithResourceId( + "orphan_doc_1", + util::kGoogleDocumentMimeType, + std::string(), + std::string(), + "Orphan Document", + true, // shared_with_me + google_apis::test_util::CreateCopyResultCallback(&error, &entry)); + base::RunLoop().RunUntilIdle(); + if (error != HTTP_CREATED) + return false; + + return true; +} + +} // namespace test_util +} // namespace drive diff --git a/components/drive/service/test_util.h b/components/drive/service/test_util.h new file mode 100644 index 0000000..9cd25ae --- /dev/null +++ b/components/drive/service/test_util.h @@ -0,0 +1,19 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_DRIVE_SERVICE_TEST_UTIL_H_ +#define COMPONENTS_DRIVE_SERVICE_TEST_UTIL_H_ + +namespace drive { + +class FakeDriveService; + +namespace test_util { + +bool SetUpTestEntries(FakeDriveService* drive_service); + +} // namespace test_util +} // namespace drive + +#endif // COMPONENTS_DRIVE_SERVICE_TEST_UTIL_H_ |