// 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 "chrome/browser/drive/fake_drive_service.h" #include <string> #include "base/file_util.h" #include "base/logging.h" #include "base/md5.h" #include "base/message_loop/message_loop.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 "chrome/browser/drive/drive_api_util.h" #include "content/public/browser/browser_thread.h" #include "google_apis/drive/drive_api_parser.h" #include "google_apis/drive/gdata_wapi_parser.h" #include "google_apis/drive/test_util.h" #include "google_apis/drive/time_util.h" #include "net/base/escape.h" #include "net/base/url_util.h" using content::BrowserThread; using google_apis::AboutResource; using google_apis::AboutResourceCallback; using google_apis::AccountMetadata; using google_apis::AppList; using google_apis::AppListCallback; using google_apis::AuthStatusCallback; using google_apis::AuthorizeAppCallback; using google_apis::CancelCallback; using google_apis::DownloadActionCallback; using google_apis::EntryActionCallback; using google_apis::GDATA_FILE_ERROR; using google_apis::GDATA_NO_CONNECTION; using google_apis::GDATA_OTHER_ERROR; using google_apis::GDataErrorCode; using google_apis::GetContentCallback; using google_apis::GetResourceEntryCallback; using google_apis::GetResourceListCallback; using google_apis::GetShareUrlCallback; using google_apis::HTTP_BAD_REQUEST; using google_apis::HTTP_CREATED; 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::Link; using google_apis::ProgressCallback; using google_apis::ResourceEntry; using google_apis::ResourceList; using google_apis::UploadRangeCallback; using google_apis::UploadRangeResponse; namespace test_util = google_apis::test_util; namespace drive { namespace { // Rel property of an upload link in the entries dictionary value. const char kUploadUrlRel[] = "http://schemas.google.com/g/2005#resumable-create-media"; // Rel property of a share link in the entries dictionary value. const char kShareUrlRel[] = "http://schemas.google.com/docs/2007#share"; // Returns true if a resource 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 ResourceEntry& 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.title().find(value) == std::string::npos) return false; } return true; } void ScheduleUploadRangeCallback(const UploadRangeCallback& callback, int64 start_position, int64 end_position, GDataErrorCode error, scoped_ptr<ResourceEntry> entry) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, UploadRangeResponse(error, start_position, end_position), base::Passed(&entry))); } void EntryActionCallbackAdapter( const EntryActionCallback& callback, GDataErrorCode error, scoped_ptr<ResourceEntry> resource_entry) { callback.Run(error); } void ClearPropatiesForPermanentDelete(DictionaryValue* entry) { scoped_ptr<DictionaryValue> new_entry(new DictionaryValue); const char* kPreservedProperties[] = { "docs$removed", "docs$changestamp", "gd$resourceId", "id", "updated" }; for (size_t i = 0; i < arraysize(kPreservedProperties); ++i) { const char* key = kPreservedProperties[i]; scoped_ptr<Value> value; if (entry->Remove(key, &value)) new_entry->Set(key, value.release()); } entry->Swap(new_entry.get()); } } // namespace 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() : largest_changestamp_(0), published_date_seq_(0), next_upload_sequence_number_(0), default_max_results_(0), resource_id_count_(0), resource_list_load_count_(0), change_list_load_count_(0), directory_load_count_(0), about_resource_load_count_(0), app_list_load_count_(0), blocked_resource_list_load_count_(0), offline_(false), never_return_all_resource_list_(false) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); } FakeDriveService::~FakeDriveService() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); } bool FakeDriveService::LoadResourceListForWapi( const std::string& relative_path) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); scoped_ptr<Value> raw_value = test_util::LoadJSONFile(relative_path); base::DictionaryValue* as_dict = NULL; scoped_ptr<base::Value> feed; base::DictionaryValue* feed_as_dict = NULL; // Extract the "feed" from the raw value and take the ownership. // Note that Remove() transfers the ownership to |feed|. if (raw_value->GetAsDictionary(&as_dict) && as_dict->Remove("feed", &feed) && feed->GetAsDictionary(&feed_as_dict)) { ignore_result(feed.release()); resource_list_value_.reset(feed_as_dict); // Go through entries and convert test$data from a string to a binary blob. base::ListValue* entries = NULL; if (feed_as_dict->GetList("entry", &entries)) { for (size_t i = 0; i < entries->GetSize(); ++i) { base::DictionaryValue* entry = NULL; std::string content_data; if (entries->GetDictionary(i, &entry) && entry->GetString("test$data", &content_data)) { entry->Set("test$data", base::BinaryValue::CreateWithCopiedBuffer( content_data.c_str(), content_data.size())); } } } } return resource_list_value_; } bool FakeDriveService::LoadAccountMetadataForWapi( const std::string& relative_path) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); // 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()); account_metadata_value_.reset( static_cast<base::DictionaryValue*>(value.release())); // Update the largest_changestamp_. scoped_ptr<AccountMetadata> account_metadata = AccountMetadata::CreateFrom(*account_metadata_value_); largest_changestamp_ = account_metadata->largest_changestamp(); // Add the largest changestamp to the existing entries. // This will be used to generate change lists in GetResourceList(). if (resource_list_value_) { base::ListValue* entries = NULL; if (resource_list_value_->GetList("entry", &entries)) { for (size_t i = 0; i < entries->GetSize(); ++i) { base::DictionaryValue* entry = NULL; if (entries->GetDictionary(i, &entry)) { entry->SetString("docs$changestamp.value", base::Int64ToString(largest_changestamp_)); } } } } return account_metadata_value_; } bool FakeDriveService::LoadAppListForDriveApi( const std::string& relative_path) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); app_info_value_ = test_util::LoadJSONFile(relative_path); return app_info_value_; } void FakeDriveService::SetQuotaValue(int64 used, int64 total) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(account_metadata_value_); account_metadata_value_->SetString("entry.gd$quotaBytesUsed.$t", base::Int64ToString16(used)); account_metadata_value_->SetString("entry.gd$quotaBytesTotal.$t", base::Int64ToString16(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(BrowserThread::CurrentlyOn(BrowserThread::UI)); } void FakeDriveService::AddObserver(DriveServiceObserver* observer) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); } void FakeDriveService::RemoveObserver(DriveServiceObserver* observer) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); } bool FakeDriveService::CanSendRequest() const { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); return true; } ResourceIdCanonicalizer FakeDriveService::GetResourceIdCanonicalizer() const { return util::GetIdentityResourceIdCanonicalizer(); } bool FakeDriveService::HasAccessToken() const { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); return true; } void FakeDriveService::RequestAccessToken(const AuthStatusCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); callback.Run(google_apis::HTTP_NOT_MODIFIED, "fake_access_token"); } bool FakeDriveService::HasRefreshToken() const { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); return true; } void FakeDriveService::ClearAccessToken() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); } void FakeDriveService::ClearRefreshToken() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); } std::string FakeDriveService::GetRootResourceId() const { return "fake_root"; } CancelCallback FakeDriveService::GetAllResourceList( const GetResourceListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (never_return_all_resource_list_) { ++blocked_resource_list_load_count_; return CancelCallback(); } GetResourceListInternal(0, // start changestamp std::string(), // empty search query std::string(), // no directory resource id, 0, // start offset default_max_results_, &resource_list_load_count_, callback); return CancelCallback(); } CancelCallback FakeDriveService::GetResourceListInDirectory( const std::string& directory_resource_id, const GetResourceListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!directory_resource_id.empty()); DCHECK(!callback.is_null()); GetResourceListInternal(0, // start changestamp std::string(), // empty search query directory_resource_id, 0, // start offset default_max_results_, &directory_load_count_, callback); return CancelCallback(); } CancelCallback FakeDriveService::Search( const std::string& search_query, const GetResourceListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!search_query.empty()); DCHECK(!callback.is_null()); GetResourceListInternal(0, // start changestamp search_query, std::string(), // no directory resource id, 0, // start offset default_max_results_, NULL, callback); return CancelCallback(); } CancelCallback FakeDriveService::SearchByTitle( const std::string& title, const std::string& directory_resource_id, const GetResourceListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!title.empty()); DCHECK(!callback.is_null()); // Note: the search implementation here doesn't support quotation unescape, // so don't escape here. GetResourceListInternal(0, // start changestamp base::StringPrintf("title:'%s'", title.c_str()), directory_resource_id, 0, // start offset default_max_results_, NULL, callback); return CancelCallback(); } CancelCallback FakeDriveService::GetChangeList( int64 start_changestamp, const GetResourceListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); GetResourceListInternal(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 GetResourceListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!next_link.is_empty()); DCHECK(!callback.is_null()); return GetRemainingResourceList(next_link, callback); } CancelCallback FakeDriveService::GetRemainingFileList( const GURL& next_link, const GetResourceListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!next_link.is_empty()); DCHECK(!callback.is_null()); return GetRemainingResourceList(next_link, callback); } CancelCallback FakeDriveService::GetResourceEntry( const std::string& resource_id, const GetResourceEntryCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (offline_) { scoped_ptr<ResourceEntry> null; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION, base::Passed(&null))); return CancelCallback(); } base::DictionaryValue* entry = FindEntryByResourceId(resource_id); if (entry) { scoped_ptr<ResourceEntry> resource_entry = ResourceEntry::CreateFrom(*entry); base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_SUCCESS, base::Passed(&resource_entry))); return CancelCallback(); } scoped_ptr<ResourceEntry> null; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null))); return CancelCallback(); } CancelCallback FakeDriveService::GetShareUrl( const std::string& resource_id, const GURL& /* embed_origin */, const GetShareUrlCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (offline_) { scoped_ptr<ResourceEntry> null; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION, GURL())); return CancelCallback(); } base::DictionaryValue* entry = FindEntryByResourceId(resource_id); if (entry) { // Share urls are stored in the resource entry, and they do not rely on the // embedding origin. scoped_ptr<ResourceEntry> resource_entry = ResourceEntry::CreateFrom(*entry); const Link* share_url = resource_entry->GetLinkByType(Link::LINK_SHARE); if (share_url) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_SUCCESS, share_url->href())); } else { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_SUCCESS, GURL())); } return CancelCallback(); } scoped_ptr<ResourceEntry> null; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND, GURL())); return CancelCallback(); } CancelCallback FakeDriveService::GetAboutResource( const AboutResourceCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (offline_) { scoped_ptr<AboutResource> null; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION, base::Passed(&null))); return CancelCallback(); } ++about_resource_load_count_; scoped_ptr<AboutResource> about_resource( util::ConvertAccountMetadataToAboutResource( *AccountMetadata::CreateFrom(*account_metadata_value_), GetRootResourceId())); // Overwrite the change id. about_resource->set_largest_change_id(largest_changestamp_); base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_SUCCESS, base::Passed(&about_resource))); return CancelCallback(); } CancelCallback FakeDriveService::GetAppList(const AppListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); DCHECK(app_info_value_); if (offline_) { scoped_ptr<AppList> null; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION, base::Passed(&null))); return CancelCallback(); } ++app_list_load_count_; scoped_ptr<AppList> app_list(AppList::CreateFrom(*app_info_value_)); base::MessageLoop::current()->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(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (offline_) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION)); return CancelCallback(); } base::ListValue* entries = NULL; if (resource_list_value_->GetList("entry", &entries)) { for (size_t i = 0; i < entries->GetSize(); ++i) { base::DictionaryValue* entry = NULL; std::string current_resource_id; if (entries->GetDictionary(i, &entry) && entry->GetString("gd$resourceId.$t", ¤t_resource_id) && resource_id == current_resource_id) { if (entry->HasKey("docs$removed")) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND)); return CancelCallback(); } std::string entry_etag; entry->GetString("gd$etag", &entry_etag); if (!etag.empty() && entry_etag != etag) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_PRECONDITION)); return CancelCallback(); } entry->Set("docs$removed", new DictionaryValue); AddNewChangestamp(entry); ClearPropatiesForPermanentDelete(entry); base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_NO_CONTENT)); return CancelCallback(); } } } base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND)); return CancelCallback(); } CancelCallback FakeDriveService::TrashResource( const std::string& resource_id, const EntryActionCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (offline_) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION)); return CancelCallback(); } base::ListValue* entries = NULL; // Go through entries and remove the one that matches |resource_id|. if (resource_list_value_->GetList("entry", &entries)) { for (size_t i = 0; i < entries->GetSize(); ++i) { base::DictionaryValue* entry = NULL; std::string current_resource_id; if (entries->GetDictionary(i, &entry) && entry->GetString("gd$resourceId.$t", ¤t_resource_id) && resource_id == current_resource_id) { GDataErrorCode error = google_apis::GDATA_OTHER_ERROR; if (entry->HasKey("gd$deleted") || entry->HasKey("docs$removed")) { error = HTTP_NOT_FOUND; } else { entry->Set("gd$deleted", new DictionaryValue); AddNewChangestamp(entry); error = HTTP_SUCCESS; } base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, error)); return CancelCallback(); } } } base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND)); 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(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!download_action_callback.is_null()); if (offline_) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(download_action_callback, GDATA_NO_CONNECTION, base::FilePath())); return CancelCallback(); } // The field content.src is the URL to download the file. base::DictionaryValue* entry = FindEntryByResourceId(resource_id); if (!entry) { base::MessageLoopProxy::current()->PostTask( FROM_HERE, base::Bind(download_action_callback, HTTP_NOT_FOUND, base::FilePath())); return CancelCallback(); } // Write "x"s of the file size specified in the entry. std::string file_size_string; entry->GetString("docs$size.$t", &file_size_string); int64 file_size = 0; if (base::StringToInt64(file_size_string, &file_size)) { base::BinaryValue* content_binary_data; std::string content_data; if (entry->GetBinary("test$data", &content_binary_data)) { content_data = std::string(content_binary_data->GetBuffer(), content_binary_data->GetSize()); } 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::MessageLoopProxy::current()->PostTask( FROM_HERE, base::Bind(get_content_callback, HTTP_SUCCESS, base::Passed(&content_for_callback))); } } if (test_util::WriteStringToFile(local_cache_path, content_data)) { 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::MessageLoopProxy::current()->PostTask( FROM_HERE, base::Bind(progress_callback, file_size / 2, file_size)); base::MessageLoopProxy::current()->PostTask( FROM_HERE, base::Bind(progress_callback, file_size, file_size)); } base::MessageLoopProxy::current()->PostTask( FROM_HERE, base::Bind(download_action_callback, HTTP_SUCCESS, local_cache_path)); return CancelCallback(); } } // Failed to write the content. base::MessageLoopProxy::current()->PostTask( FROM_HERE, base::Bind(download_action_callback, GDATA_FILE_ERROR, base::FilePath())); 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 GetResourceEntryCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (offline_) { scoped_ptr<ResourceEntry> null; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION, base::Passed(&null))); return CancelCallback(); } const std::string& parent_resource_id = in_parent_resource_id.empty() ? GetRootResourceId() : in_parent_resource_id; base::ListValue* entries = NULL; // Go through entries and copy the one that matches |resource_id|. if (resource_list_value_->GetList("entry", &entries)) { for (size_t i = 0; i < entries->GetSize(); ++i) { base::DictionaryValue* entry = NULL; std::string current_resource_id; if (entries->GetDictionary(i, &entry) && entry->GetString("gd$resourceId.$t", ¤t_resource_id) && resource_id == current_resource_id) { // Make a copy and set the new resource ID and the new title. scoped_ptr<DictionaryValue> copied_entry(entry->DeepCopy()); copied_entry->SetString("gd$resourceId.$t", resource_id + "_copied"); copied_entry->SetString("title.$t", new_title); // Reset parent directory. base::ListValue* links = NULL; if (!copied_entry->GetList("link", &links)) { links = new base::ListValue; copied_entry->Set("link", links); } links->Clear(); base::DictionaryValue* link = new base::DictionaryValue; link->SetString( "rel", "http://schemas.google.com/docs/2007#parent"); link->SetString("href", GetFakeLinkUrl(parent_resource_id).spec()); links->Append(link); if (!last_modified.is_null()) { copied_entry->SetString( "updated.$t", google_apis::util::FormatTimeAsString(last_modified)); } AddNewChangestamp(copied_entry.get()); UpdateETag(copied_entry.get()); // Parse the new entry. scoped_ptr<ResourceEntry> resource_entry = ResourceEntry::CreateFrom(*copied_entry); // Add it to the resource list. entries->Append(copied_entry.release()); base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_SUCCESS, base::Passed(&resource_entry))); return CancelCallback(); } } } scoped_ptr<ResourceEntry> null; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null))); 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::GetResourceEntryCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (offline_) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION, base::Passed(scoped_ptr<ResourceEntry>()))); return CancelCallback(); } base::DictionaryValue* entry = FindEntryByResourceId(resource_id); if (entry) { entry->SetString("title.$t", new_title); // Set parent if necessary. if (!parent_resource_id.empty()) { base::ListValue* links = NULL; if (!entry->GetList("link", &links)) { links = new base::ListValue; entry->Set("link", links); } // Remove old parent(s). for (size_t i = 0; i < links->GetSize(); ) { base::DictionaryValue* link = NULL; std::string rel; std::string href; if (links->GetDictionary(i, &link) && link->GetString("rel", &rel) && link->GetString("href", &href) && rel == "http://schemas.google.com/docs/2007#parent") { links->Remove(i, NULL); } else { ++i; } } base::DictionaryValue* link = new base::DictionaryValue; link->SetString("rel", "http://schemas.google.com/docs/2007#parent"); link->SetString( "href", GetFakeLinkUrl(parent_resource_id).spec()); links->Append(link); } if (!last_modified.is_null()) { entry->SetString( "updated.$t", google_apis::util::FormatTimeAsString(last_modified)); } if (!last_viewed_by_me.is_null()) { entry->SetString( "gd$lastViewed.$t", google_apis::util::FormatTimeAsString(last_viewed_by_me)); } AddNewChangestamp(entry); UpdateETag(entry); // Parse the new entry. scoped_ptr<ResourceEntry> resource_entry = ResourceEntry::CreateFrom(*entry); base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_SUCCESS, base::Passed(&resource_entry))); return CancelCallback(); } base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND, base::Passed(scoped_ptr<ResourceEntry>()))); return CancelCallback(); } CancelCallback FakeDriveService::RenameResource( const std::string& resource_id, const std::string& new_title, const EntryActionCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return UpdateResource( resource_id, std::string(), new_title, base::Time(), base::Time(), base::Bind(&EntryActionCallbackAdapter, callback)); } CancelCallback FakeDriveService::AddResourceToDirectory( const std::string& parent_resource_id, const std::string& resource_id, const EntryActionCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (offline_) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION)); return CancelCallback(); } base::DictionaryValue* entry = FindEntryByResourceId(resource_id); if (entry) { base::ListValue* links = NULL; if (!entry->GetList("link", &links)) { links = new base::ListValue; entry->Set("link", links); } // 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 link, not overwriting old links. base::DictionaryValue* link = new base::DictionaryValue; link->SetString("rel", "http://schemas.google.com/docs/2007#parent"); link->SetString( "href", GetFakeLinkUrl(parent_resource_id).spec()); links->Append(link); AddNewChangestamp(entry); base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_SUCCESS)); return CancelCallback(); } base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND)); return CancelCallback(); } CancelCallback FakeDriveService::RemoveResourceFromDirectory( const std::string& parent_resource_id, const std::string& resource_id, const EntryActionCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (offline_) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION)); return CancelCallback(); } base::DictionaryValue* entry = FindEntryByResourceId(resource_id); if (entry) { base::ListValue* links = NULL; if (entry->GetList("link", &links)) { GURL parent_content_url = GetFakeLinkUrl(parent_resource_id); for (size_t i = 0; i < links->GetSize(); ++i) { base::DictionaryValue* link = NULL; std::string rel; std::string href; if (links->GetDictionary(i, &link) && link->GetString("rel", &rel) && link->GetString("href", &href) && rel == "http://schemas.google.com/docs/2007#parent" && GURL(href) == parent_content_url) { links->Remove(i, NULL); AddNewChangestamp(entry); base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_NO_CONTENT)); return CancelCallback(); } } } } base::MessageLoop::current()->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 GetResourceEntryCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (offline_) { scoped_ptr<ResourceEntry> null; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION, base::Passed(&null))); return CancelCallback(); } const char kContentType[] = "application/atom+xml;type=feed"; const base::DictionaryValue* new_entry = AddNewEntry(kContentType, "", // content_data parent_resource_id, directory_title, false, // shared_with_me "folder"); if (!new_entry) { scoped_ptr<ResourceEntry> null; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null))); return CancelCallback(); } scoped_ptr<ResourceEntry> parsed_entry(ResourceEntry::CreateFrom(*new_entry)); base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_CREATED, base::Passed(&parsed_entry))); return CancelCallback(); } CancelCallback FakeDriveService::InitiateUploadNewFile( const std::string& content_type, int64 content_length, const std::string& parent_resource_id, const std::string& title, const InitiateUploadCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (offline_) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION, GURL())); return CancelCallback(); } if (parent_resource_id != GetRootResourceId() && !FindEntryByResourceId(parent_resource_id)) { base::MessageLoop::current()->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::MessageLoop::current()->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 std::string& etag, const InitiateUploadCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (offline_) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION, GURL())); return CancelCallback(); } DictionaryValue* entry = FindEntryByResourceId(resource_id); if (!entry) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND, GURL())); return CancelCallback(); } std::string entry_etag; entry->GetString("gd$etag", &entry_etag); if (!etag.empty() && etag != entry_etag) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_PRECONDITION, GURL())); return CancelCallback(); } GURL session_url = GetNewUploadSessionUrl(); upload_sessions_[session_url] = UploadSession(content_type, content_length, "", // parent_resource_id resource_id, entry_etag, "" /* title */); base::MessageLoop::current()->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(BrowserThread::CurrentlyOn(BrowserThread::UI)); 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(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); GetResourceEntryCallback completion_callback = base::Bind(&ScheduleUploadRangeCallback, callback, start_position, end_position); if (offline_) { completion_callback.Run(GDATA_NO_CONNECTION, scoped_ptr<ResourceEntry>()); return CancelCallback(); } if (!upload_sessions_.count(upload_url)) { completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<ResourceEntry>()); 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<ResourceEntry>()); 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::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(progress_callback, chunk_size / 2, chunk_size)); base::MessageLoop::current()->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<ResourceEntry>()); return CancelCallback(); } std::string content_data; if (!base::ReadFileToString(local_file_path, &content_data)) { session->uploaded_size = end_position; completion_callback.Run(GDATA_FILE_ERROR, scoped_ptr<ResourceEntry>()); 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 DictionaryValue* new_entry = AddNewEntry( session->content_type, content_data, session->parent_resource_id, session->title, false, // shared_with_me "file"); if (!new_entry) { completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<ResourceEntry>()); return CancelCallback(); } completion_callback.Run(HTTP_CREATED, ResourceEntry::CreateFrom(*new_entry)); return CancelCallback(); } DictionaryValue* entry = FindEntryByResourceId(session->resource_id); if (!entry) { completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<ResourceEntry>()); return CancelCallback(); } std::string entry_etag; entry->GetString("gd$etag", &entry_etag); if (entry_etag.empty() || session->etag != entry_etag) { completion_callback.Run(HTTP_PRECONDITION, scoped_ptr<ResourceEntry>()); return CancelCallback(); } entry->SetString("docs$md5Checksum.$t", base::MD5String(content_data)); entry->Set("test$data", base::BinaryValue::CreateWithCopiedBuffer( content_data.data(), content_data.size())); entry->SetString("docs$size.$t", base::Int64ToString(end_position)); AddNewChangestamp(entry); UpdateETag(entry); completion_callback.Run(HTTP_SUCCESS, ResourceEntry::CreateFrom(*entry)); return CancelCallback(); } CancelCallback FakeDriveService::AuthorizeApp( const std::string& resource_id, const std::string& app_id, const AuthorizeAppCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return CancelCallback(); } CancelCallback FakeDriveService::GetResourceListInDirectoryByWapi( const std::string& directory_resource_id, const google_apis::GetResourceListCallback& callback) { return GetResourceListInDirectory( directory_resource_id == util::kWapiRootDirectoryResourceId ? GetRootResourceId() : directory_resource_id, callback); } CancelCallback FakeDriveService::GetRemainingResourceList( const GURL& next_link, const google_apis::GetResourceListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 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 GetResourceListInternal of the // previous method invocation, so it should start with "http://localhost/?". // See also GetResourceListInternal. 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_; std::vector<std::pair<std::string, std::string> > 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); } } } GetResourceListInternal( start_changestamp, search_query, directory_resource_id, start_offset, max_results, NULL, callback); 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 GetResourceEntryCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (offline_) { scoped_ptr<ResourceEntry> null; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION, base::Passed(&null))); return; } // Prepare "kind" for hosted documents. This only supports Google Document. std::string entry_kind; if (content_type == "application/vnd.google-apps.document") entry_kind = "document"; else entry_kind = "file"; const base::DictionaryValue* new_entry = AddNewEntry(content_type, content_data, parent_resource_id, title, shared_with_me, entry_kind); if (!new_entry) { scoped_ptr<ResourceEntry> null; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null))); return; } scoped_ptr<ResourceEntry> parsed_entry( ResourceEntry::CreateFrom(*new_entry)); base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_CREATED, base::Passed(&parsed_entry))); } void FakeDriveService::SetLastModifiedTime( const std::string& resource_id, const base::Time& last_modified_time, const GetResourceEntryCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (offline_) { scoped_ptr<ResourceEntry> null; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION, base::Passed(&null))); return; } base::DictionaryValue* entry = FindEntryByResourceId(resource_id); if (!entry) { scoped_ptr<ResourceEntry> null; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null))); return; } if (last_modified_time.is_null()) { entry->Remove("updated.$t", NULL); } else { entry->SetString( "updated.$t", google_apis::util::FormatTimeAsString(last_modified_time)); } scoped_ptr<ResourceEntry> parsed_entry( ResourceEntry::CreateFrom(*entry)); base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_SUCCESS, base::Passed(&parsed_entry))); } base::DictionaryValue* FakeDriveService::FindEntryByResourceId( const std::string& resource_id) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); base::ListValue* entries = NULL; // Go through entries and return the one that matches |resource_id|. if (resource_list_value_->GetList("entry", &entries)) { for (size_t i = 0; i < entries->GetSize(); ++i) { base::DictionaryValue* entry = NULL; std::string current_resource_id; if (entries->GetDictionary(i, &entry) && entry->GetString("gd$resourceId.$t", ¤t_resource_id) && resource_id == current_resource_id) { return entry; } } } return NULL; } base::DictionaryValue* FakeDriveService::FindEntryByContentUrl( const GURL& content_url) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); base::ListValue* entries = NULL; // Go through entries and return the one that matches |content_url|. if (resource_list_value_->GetList("entry", &entries)) { for (size_t i = 0; i < entries->GetSize(); ++i) { base::DictionaryValue* entry = NULL; std::string current_content_url; if (entries->GetDictionary(i, &entry) && entry->GetString("content.src", ¤t_content_url) && content_url == GURL(current_content_url)) { return entry; } } } return NULL; } std::string FakeDriveService::GetNewResourceId() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); ++resource_id_count_; return base::StringPrintf("resource_id_%d", resource_id_count_); } void FakeDriveService::UpdateETag(base::DictionaryValue* entry) { entry->SetString("gd$etag", "etag_" + base::Int64ToString(largest_changestamp_)); } void FakeDriveService::AddNewChangestamp(base::DictionaryValue* entry) { ++largest_changestamp_; entry->SetString("docs$changestamp.value", base::Int64ToString(largest_changestamp_)); } const base::DictionaryValue* FakeDriveService::AddNewEntry( 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 std::string& entry_kind) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); if (!parent_resource_id.empty() && parent_resource_id != GetRootResourceId() && !FindEntryByResourceId(parent_resource_id)) { return NULL; } std::string resource_id = GetNewResourceId(); GURL upload_url = GURL("https://xxx/upload/" + resource_id); scoped_ptr<base::DictionaryValue> new_entry(new base::DictionaryValue); // Set the resource ID and the title new_entry->SetString("gd$resourceId.$t", resource_id); new_entry->SetString("title.$t", title); new_entry->SetString("docs$filename", title); // Set the contents, size and MD5 for a file. if (entry_kind == "file") { new_entry->Set("test$data", base::BinaryValue::CreateWithCopiedBuffer( content_data.c_str(), content_data.size())); new_entry->SetString("docs$size.$t", base::Int64ToString(content_data.size())); new_entry->SetString("docs$md5Checksum.$t", base::MD5String(content_data)); } // Add "category" which sets the resource type to |entry_kind|. base::ListValue* categories = new base::ListValue; base::DictionaryValue* category = new base::DictionaryValue; category->SetString("scheme", "http://schemas.google.com/g/2005#kind"); category->SetString("term", "http://schemas.google.com/docs/2007#" + entry_kind); categories->Append(category); new_entry->Set("category", categories); if (shared_with_me) { base::DictionaryValue* shared_with_me_label = new base::DictionaryValue; shared_with_me_label->SetString("label", "shared-with-me"); shared_with_me_label->SetString("scheme", "http://schemas.google.com/g/2005/labels"); shared_with_me_label->SetString( "term", "http://schemas.google.com/g/2005/labels#shared"); categories->Append(shared_with_me_label); } std::string escaped_resource_id = net::EscapePath(resource_id); // Add "content" which sets the content URL. base::DictionaryValue* content = new base::DictionaryValue; content->SetString("src", "https://xxx/content/" + escaped_resource_id); content->SetString("type", content_type); new_entry->Set("content", content); // Add "link" which sets the parent URL, the edit URL and the upload URL. base::ListValue* links = new base::ListValue; base::DictionaryValue* parent_link = new base::DictionaryValue; if (parent_resource_id.empty()) parent_link->SetString("href", GetFakeLinkUrl(GetRootResourceId()).spec()); else parent_link->SetString("href", GetFakeLinkUrl(parent_resource_id).spec()); parent_link->SetString("rel", "http://schemas.google.com/docs/2007#parent"); links->Append(parent_link); base::DictionaryValue* edit_link = new base::DictionaryValue; edit_link->SetString("href", "https://xxx/edit/" + escaped_resource_id); edit_link->SetString("rel", "edit"); links->Append(edit_link); base::DictionaryValue* upload_link = new base::DictionaryValue; upload_link->SetString("href", upload_url.spec()); upload_link->SetString("rel", kUploadUrlRel); links->Append(upload_link); const GURL share_url = net::AppendOrReplaceQueryParameter( share_url_base_, "name", title); base::DictionaryValue* share_link = new base::DictionaryValue; upload_link->SetString("href", share_url.spec()); upload_link->SetString("rel", kShareUrlRel); links->Append(share_link); new_entry->Set("link", links); AddNewChangestamp(new_entry.get()); UpdateETag(new_entry.get()); base::Time published_date = base::Time() + base::TimeDelta::FromMilliseconds(++published_date_seq_); new_entry->SetString( "published.$t", google_apis::util::FormatTimeAsString(published_date)); // If there are no entries, prepare an empty entry to add. if (!resource_list_value_->HasKey("entry")) resource_list_value_->Set("entry", new ListValue); base::DictionaryValue* raw_new_entry = new_entry.release(); base::ListValue* entries = NULL; if (resource_list_value_->GetList("entry", &entries)) entries->Append(raw_new_entry); return raw_new_entry; } void FakeDriveService::GetResourceListInternal( int64 start_changestamp, const std::string& search_query, const std::string& directory_resource_id, int start_offset, int max_results, int* load_counter, const GetResourceListCallback& callback) { if (offline_) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION, base::Passed(scoped_ptr<ResourceList>()))); return; } scoped_ptr<ResourceList> resource_list = ResourceList::CreateFrom(*resource_list_value_); // TODO(hashimoto): Drive API always provides largest changestamp. Remove this // if-statement after API switch. if (start_changestamp > 0 && start_offset == 0) resource_list->set_largest_changestamp(largest_changestamp_); // Filter out entries per parameters like |directory_resource_id| and // |search_query|. ScopedVector<ResourceEntry>* entries = resource_list->mutable_entries(); int num_entries_matched = 0; for (size_t i = 0; i < entries->size();) { ResourceEntry* entry = (*entries)[i]; 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; const google_apis::Link* parent_link = entry->GetLinkByType(Link::LINK_PARENT); if (parent_link) { parent_resource_id = net::UnescapeURLComponent(parent_link->href().ExtractFileName(), net::UnescapeRule::URL_SPECIAL_CHARS); } 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->changestamp() < 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. if (!start_changestamp && entry->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) entries->erase(entries->begin() + i); else ++i; } // 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 // GetResourceList 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); } Link* link = new Link; link->set_type(Link::LINK_NEXT); link->set_href(next_url); resource_list->mutable_links()->push_back(link); } if (load_counter) *load_counter += 1; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(callback, HTTP_SUCCESS, base::Passed(&resource_list))); } GURL FakeDriveService::GetNewUploadSessionUrl() { return GURL("https://upload_session_url/" + base::Int64ToString(next_upload_sequence_number_++)); } } // namespace drive