1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/drive/fake_drive_service.h" 6 7 #include <string> 8 9 #include "base/file_util.h" 10 #include "base/logging.h" 11 #include "base/md5.h" 12 #include "base/message_loop/message_loop.h" 13 #include "base/strings/string_number_conversions.h" 14 #include "base/strings/string_split.h" 15 #include "base/strings/string_tokenizer.h" 16 #include "base/strings/string_util.h" 17 #include "base/strings/stringprintf.h" 18 #include "base/strings/utf_string_conversions.h" 19 #include "chrome/browser/google_apis/drive_api_parser.h" 20 #include "chrome/browser/google_apis/gdata_wapi_parser.h" 21 #include "chrome/browser/google_apis/test_util.h" 22 #include "chrome/browser/google_apis/time_util.h" 23 #include "content/public/browser/browser_thread.h" 24 #include "net/base/escape.h" 25 #include "net/base/url_util.h" 26 27 using content::BrowserThread; 28 using google_apis::AboutResource; 29 using google_apis::AccountMetadata; 30 using google_apis::AppList; 31 using google_apis::AuthStatusCallback; 32 using google_apis::AuthorizeAppCallback; 33 using google_apis::CancelCallback; 34 using google_apis::DownloadActionCallback; 35 using google_apis::EntryActionCallback; 36 using google_apis::GDataErrorCode; 37 using google_apis::GDATA_FILE_ERROR; 38 using google_apis::GDATA_NO_CONNECTION; 39 using google_apis::GDATA_OTHER_ERROR; 40 using google_apis::GetAboutResourceCallback; 41 using google_apis::GetAppListCallback; 42 using google_apis::GetContentCallback; 43 using google_apis::GetResourceEntryCallback; 44 using google_apis::GetResourceListCallback; 45 using google_apis::GetShareUrlCallback; 46 using google_apis::HTTP_BAD_REQUEST; 47 using google_apis::HTTP_CREATED; 48 using google_apis::HTTP_NOT_FOUND; 49 using google_apis::HTTP_PRECONDITION; 50 using google_apis::HTTP_RESUME_INCOMPLETE; 51 using google_apis::HTTP_SUCCESS; 52 using google_apis::InitiateUploadCallback; 53 using google_apis::Link; 54 using google_apis::ProgressCallback; 55 using google_apis::ResourceEntry; 56 using google_apis::ResourceList; 57 using google_apis::UploadRangeCallback; 58 using google_apis::UploadRangeResponse; 59 namespace test_util = google_apis::test_util; 60 namespace util = google_apis::util; 61 62 namespace drive { 63 namespace { 64 65 // Rel property of an upload link in the entries dictionary value. 66 const char kUploadUrlRel[] = 67 "http://schemas.google.com/g/2005#resumable-create-media"; 68 69 // Rel property of a share link in the entries dictionary value. 70 const char kShareUrlRel[] = 71 "http://schemas.google.com/docs/2007#share"; 72 73 // Returns true if a resource entry matches with the search query. 74 // Supports queries consist of following format. 75 // - Phrases quoted by double/single quotes 76 // - AND search for multiple words/phrases segmented by space 77 // - Limited attribute search. Only "title:" is supported. 78 bool EntryMatchWithQuery(const ResourceEntry& entry, 79 const std::string& query) { 80 base::StringTokenizer tokenizer(query, " "); 81 tokenizer.set_quote_chars("\"'"); 82 while (tokenizer.GetNext()) { 83 std::string key, value; 84 const std::string& token = tokenizer.token(); 85 if (token.find(':') == std::string::npos) { 86 TrimString(token, "\"'", &value); 87 } else { 88 base::StringTokenizer key_value(token, ":"); 89 key_value.set_quote_chars("\"'"); 90 if (!key_value.GetNext()) 91 return false; 92 key = key_value.token(); 93 if (!key_value.GetNext()) 94 return false; 95 TrimString(key_value.token(), "\"'", &value); 96 } 97 98 // TODO(peria): Deal with other attributes than title. 99 if (!key.empty() && key != "title") 100 return false; 101 // Search query in the title. 102 if (entry.title().find(value) == std::string::npos) 103 return false; 104 } 105 return true; 106 } 107 108 // Returns |url| without query parameter. 109 GURL RemoveQueryParameter(const GURL& url) { 110 GURL::Replacements replacements; 111 replacements.ClearQuery(); 112 return url.ReplaceComponents(replacements); 113 } 114 115 void ScheduleUploadRangeCallback(const UploadRangeCallback& callback, 116 int64 start_position, 117 int64 end_position, 118 GDataErrorCode error, 119 scoped_ptr<ResourceEntry> entry) { 120 base::MessageLoop::current()->PostTask( 121 FROM_HERE, 122 base::Bind(callback, 123 UploadRangeResponse(error, 124 start_position, 125 end_position), 126 base::Passed(&entry))); 127 } 128 129 } // namespace 130 131 struct FakeDriveService::UploadSession { 132 std::string content_type; 133 int64 content_length; 134 std::string parent_resource_id; 135 std::string resource_id; 136 std::string etag; 137 std::string title; 138 139 int64 uploaded_size; 140 141 UploadSession() 142 : content_length(0), 143 uploaded_size(0) {} 144 145 UploadSession( 146 std::string content_type, 147 int64 content_length, 148 std::string parent_resource_id, 149 std::string resource_id, 150 std::string etag, 151 std::string title) 152 : content_type(content_type), 153 content_length(content_length), 154 parent_resource_id(parent_resource_id), 155 resource_id(resource_id), 156 etag(etag), 157 title(title), 158 uploaded_size(0) { 159 } 160 }; 161 162 FakeDriveService::FakeDriveService() 163 : largest_changestamp_(0), 164 published_date_seq_(0), 165 next_upload_sequence_number_(0), 166 default_max_results_(0), 167 resource_id_count_(0), 168 resource_list_load_count_(0), 169 change_list_load_count_(0), 170 directory_load_count_(0), 171 about_resource_load_count_(0), 172 app_list_load_count_(0), 173 offline_(false) { 174 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 175 } 176 177 FakeDriveService::~FakeDriveService() { 178 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 179 } 180 181 bool FakeDriveService::LoadResourceListForWapi( 182 const std::string& relative_path) { 183 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 184 scoped_ptr<Value> raw_value = test_util::LoadJSONFile(relative_path); 185 base::DictionaryValue* as_dict = NULL; 186 scoped_ptr<base::Value> feed; 187 base::DictionaryValue* feed_as_dict = NULL; 188 189 // Extract the "feed" from the raw value and take the ownership. 190 // Note that Remove() transfers the ownership to |feed|. 191 if (raw_value->GetAsDictionary(&as_dict) && 192 as_dict->Remove("feed", &feed) && 193 feed->GetAsDictionary(&feed_as_dict)) { 194 ignore_result(feed.release()); 195 resource_list_value_.reset(feed_as_dict); 196 197 // Go through entries and convert test$data from a string to a binary blob. 198 base::ListValue* entries = NULL; 199 if (feed_as_dict->GetList("entry", &entries)) { 200 for (size_t i = 0; i < entries->GetSize(); ++i) { 201 base::DictionaryValue* entry = NULL; 202 std::string content_data; 203 if (entries->GetDictionary(i, &entry) && 204 entry->GetString("test$data", &content_data)) { 205 entry->Set("test$data", 206 base::BinaryValue::CreateWithCopiedBuffer( 207 content_data.c_str(), content_data.size())); 208 } 209 } 210 } 211 } 212 213 return resource_list_value_; 214 } 215 216 bool FakeDriveService::LoadAccountMetadataForWapi( 217 const std::string& relative_path) { 218 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 219 220 // Load JSON data, which must be a dictionary. 221 scoped_ptr<base::Value> value = test_util::LoadJSONFile(relative_path); 222 CHECK_EQ(base::Value::TYPE_DICTIONARY, value->GetType()); 223 account_metadata_value_.reset( 224 static_cast<base::DictionaryValue*>(value.release())); 225 226 // Update the largest_changestamp_. 227 scoped_ptr<AccountMetadata> account_metadata = 228 AccountMetadata::CreateFrom(*account_metadata_value_); 229 largest_changestamp_ = account_metadata->largest_changestamp(); 230 231 // Add the largest changestamp to the existing entries. 232 // This will be used to generate change lists in GetResourceList(). 233 if (resource_list_value_) { 234 base::ListValue* entries = NULL; 235 if (resource_list_value_->GetList("entry", &entries)) { 236 for (size_t i = 0; i < entries->GetSize(); ++i) { 237 base::DictionaryValue* entry = NULL; 238 if (entries->GetDictionary(i, &entry)) { 239 entry->SetString("docs$changestamp.value", 240 base::Int64ToString(largest_changestamp_)); 241 } 242 } 243 } 244 } 245 246 return account_metadata_value_; 247 } 248 249 bool FakeDriveService::LoadAppListForDriveApi( 250 const std::string& relative_path) { 251 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 252 app_info_value_ = test_util::LoadJSONFile(relative_path); 253 return app_info_value_; 254 } 255 256 void FakeDriveService::SetQuotaValue(int64 used, int64 total) { 257 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 258 DCHECK(account_metadata_value_); 259 260 account_metadata_value_->SetString("entry.gd$quotaBytesUsed.$t", 261 base::Int64ToString16(used)); 262 account_metadata_value_->SetString("entry.gd$quotaBytesTotal.$t", 263 base::Int64ToString16(total)); 264 } 265 266 GURL FakeDriveService::GetFakeLinkUrl(const std::string& resource_id) { 267 return GURL("https://fake_server/" + net::EscapePath(resource_id)); 268 } 269 270 void FakeDriveService::Initialize() { 271 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 272 } 273 274 void FakeDriveService::AddObserver(DriveServiceObserver* observer) { 275 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 276 } 277 278 void FakeDriveService::RemoveObserver(DriveServiceObserver* observer) { 279 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 280 } 281 282 bool FakeDriveService::CanSendRequest() const { 283 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 284 return true; 285 } 286 287 std::string FakeDriveService::CanonicalizeResourceId( 288 const std::string& resource_id) const { 289 return resource_id; 290 } 291 292 bool FakeDriveService::HasAccessToken() const { 293 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 294 return true; 295 } 296 297 void FakeDriveService::RequestAccessToken(const AuthStatusCallback& callback) { 298 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 299 DCHECK(!callback.is_null()); 300 callback.Run(google_apis::HTTP_NOT_MODIFIED, "fake_access_token"); 301 } 302 303 bool FakeDriveService::HasRefreshToken() const { 304 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 305 return true; 306 } 307 308 void FakeDriveService::ClearAccessToken() { 309 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 310 } 311 312 void FakeDriveService::ClearRefreshToken() { 313 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 314 } 315 316 std::string FakeDriveService::GetRootResourceId() const { 317 return "fake_root"; 318 } 319 320 CancelCallback FakeDriveService::GetAllResourceList( 321 const GetResourceListCallback& callback) { 322 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 323 DCHECK(!callback.is_null()); 324 325 GetResourceListInternal(0, // start changestamp 326 std::string(), // empty search query 327 std::string(), // no directory resource id, 328 0, // start offset 329 default_max_results_, 330 &resource_list_load_count_, 331 callback); 332 return CancelCallback(); 333 } 334 335 CancelCallback FakeDriveService::GetResourceListInDirectory( 336 const std::string& directory_resource_id, 337 const GetResourceListCallback& callback) { 338 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 339 DCHECK(!directory_resource_id.empty()); 340 DCHECK(!callback.is_null()); 341 342 GetResourceListInternal(0, // start changestamp 343 std::string(), // empty search query 344 directory_resource_id, 345 0, // start offset 346 default_max_results_, 347 &directory_load_count_, 348 callback); 349 return CancelCallback(); 350 } 351 352 CancelCallback FakeDriveService::Search( 353 const std::string& search_query, 354 const GetResourceListCallback& callback) { 355 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 356 DCHECK(!search_query.empty()); 357 DCHECK(!callback.is_null()); 358 359 GetResourceListInternal(0, // start changestamp 360 search_query, 361 std::string(), // no directory resource id, 362 0, // start offset 363 default_max_results_, 364 NULL, 365 callback); 366 return CancelCallback(); 367 } 368 369 CancelCallback FakeDriveService::SearchByTitle( 370 const std::string& title, 371 const std::string& directory_resource_id, 372 const GetResourceListCallback& callback) { 373 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 374 DCHECK(!title.empty()); 375 DCHECK(!callback.is_null()); 376 377 // Note: the search implementation here doesn't support quotation unescape, 378 // so don't escape here. 379 GetResourceListInternal(0, // start changestamp 380 base::StringPrintf("title:'%s'", title.c_str()), 381 directory_resource_id, 382 0, // start offset 383 default_max_results_, 384 NULL, 385 callback); 386 return CancelCallback(); 387 } 388 389 CancelCallback FakeDriveService::GetChangeList( 390 int64 start_changestamp, 391 const GetResourceListCallback& callback) { 392 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 393 DCHECK(!callback.is_null()); 394 395 GetResourceListInternal(start_changestamp, 396 std::string(), // empty search query 397 std::string(), // no directory resource id, 398 0, // start offset 399 default_max_results_, 400 &change_list_load_count_, 401 callback); 402 return CancelCallback(); 403 } 404 405 CancelCallback FakeDriveService::ContinueGetResourceList( 406 const GURL& override_url, 407 const GetResourceListCallback& callback) { 408 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 409 DCHECK(!override_url.is_empty()); 410 DCHECK(!callback.is_null()); 411 412 // "changestamp", "q", "parent" and "start-offset" are parameters to 413 // implement "paging" of the result on FakeDriveService. 414 // The URL should be the one filled in GetResourceListInternal of the 415 // previous method invocation, so it should start with "http://localhost/?". 416 // See also GetResourceListInternal. 417 DCHECK_EQ(override_url.host(), "localhost"); 418 DCHECK_EQ(override_url.path(), "/"); 419 420 int64 start_changestamp = 0; 421 std::string search_query; 422 std::string directory_resource_id; 423 int start_offset = 0; 424 int max_results = default_max_results_; 425 std::vector<std::pair<std::string, std::string> > parameters; 426 if (base::SplitStringIntoKeyValuePairs( 427 override_url.query(), '=', '&', ¶meters)) { 428 for (size_t i = 0; i < parameters.size(); ++i) { 429 if (parameters[i].first == "changestamp") { 430 base::StringToInt64(parameters[i].second, &start_changestamp); 431 } else if (parameters[i].first == "q") { 432 search_query = 433 net::UnescapeURLComponent(parameters[i].second, 434 net::UnescapeRule::URL_SPECIAL_CHARS); 435 } else if (parameters[i].first == "parent") { 436 directory_resource_id = 437 net::UnescapeURLComponent(parameters[i].second, 438 net::UnescapeRule::URL_SPECIAL_CHARS); 439 } else if (parameters[i].first == "start-offset") { 440 base::StringToInt(parameters[i].second, &start_offset); 441 } else if (parameters[i].first == "max-results") { 442 base::StringToInt(parameters[i].second, &max_results); 443 } 444 } 445 } 446 447 GetResourceListInternal( 448 start_changestamp, search_query, directory_resource_id, 449 start_offset, max_results, NULL, callback); 450 return CancelCallback(); 451 } 452 453 CancelCallback FakeDriveService::GetResourceEntry( 454 const std::string& resource_id, 455 const GetResourceEntryCallback& callback) { 456 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 457 DCHECK(!callback.is_null()); 458 459 if (offline_) { 460 scoped_ptr<ResourceEntry> null; 461 base::MessageLoop::current()->PostTask( 462 FROM_HERE, 463 base::Bind(callback, 464 GDATA_NO_CONNECTION, 465 base::Passed(&null))); 466 return CancelCallback(); 467 } 468 469 base::DictionaryValue* entry = FindEntryByResourceId(resource_id); 470 if (entry) { 471 scoped_ptr<ResourceEntry> resource_entry = 472 ResourceEntry::CreateFrom(*entry); 473 base::MessageLoop::current()->PostTask( 474 FROM_HERE, 475 base::Bind(callback, HTTP_SUCCESS, base::Passed(&resource_entry))); 476 return CancelCallback(); 477 } 478 479 scoped_ptr<ResourceEntry> null; 480 base::MessageLoop::current()->PostTask( 481 FROM_HERE, 482 base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null))); 483 return CancelCallback(); 484 } 485 486 CancelCallback FakeDriveService::GetShareUrl( 487 const std::string& resource_id, 488 const GURL& /* embed_origin */, 489 const GetShareUrlCallback& callback) { 490 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 491 DCHECK(!callback.is_null()); 492 493 if (offline_) { 494 scoped_ptr<ResourceEntry> null; 495 base::MessageLoop::current()->PostTask( 496 FROM_HERE, 497 base::Bind(callback, 498 GDATA_NO_CONNECTION, 499 GURL())); 500 return CancelCallback(); 501 } 502 503 base::DictionaryValue* entry = FindEntryByResourceId(resource_id); 504 if (entry) { 505 // Share urls are stored in the resource entry, and they do not rely on the 506 // embedding origin. 507 scoped_ptr<ResourceEntry> resource_entry = 508 ResourceEntry::CreateFrom(*entry); 509 const Link* share_url = resource_entry->GetLinkByType(Link::LINK_SHARE); 510 if (share_url) { 511 base::MessageLoop::current()->PostTask( 512 FROM_HERE, 513 base::Bind(callback, HTTP_SUCCESS, share_url->href())); 514 } else { 515 base::MessageLoop::current()->PostTask( 516 FROM_HERE, 517 base::Bind(callback, HTTP_SUCCESS, GURL())); 518 } 519 return CancelCallback(); 520 } 521 522 scoped_ptr<ResourceEntry> null; 523 base::MessageLoop::current()->PostTask( 524 FROM_HERE, 525 base::Bind(callback, HTTP_NOT_FOUND, GURL())); 526 return CancelCallback(); 527 } 528 529 CancelCallback FakeDriveService::GetAboutResource( 530 const GetAboutResourceCallback& callback) { 531 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 532 DCHECK(!callback.is_null()); 533 534 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 535 DCHECK(!callback.is_null()); 536 537 if (offline_) { 538 scoped_ptr<AboutResource> null; 539 base::MessageLoop::current()->PostTask( 540 FROM_HERE, 541 base::Bind(callback, 542 GDATA_NO_CONNECTION, base::Passed(&null))); 543 return CancelCallback(); 544 } 545 546 ++about_resource_load_count_; 547 scoped_ptr<AboutResource> about_resource( 548 AboutResource::CreateFromAccountMetadata( 549 *AccountMetadata::CreateFrom(*account_metadata_value_), 550 GetRootResourceId())); 551 // Overwrite the change id. 552 about_resource->set_largest_change_id(largest_changestamp_); 553 base::MessageLoop::current()->PostTask( 554 FROM_HERE, 555 base::Bind(callback, 556 HTTP_SUCCESS, base::Passed(&about_resource))); 557 return CancelCallback(); 558 } 559 560 CancelCallback FakeDriveService::GetAppList( 561 const GetAppListCallback& callback) { 562 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 563 DCHECK(!callback.is_null()); 564 DCHECK(app_info_value_); 565 566 if (offline_) { 567 scoped_ptr<AppList> null; 568 base::MessageLoop::current()->PostTask( 569 FROM_HERE, 570 base::Bind(callback, 571 GDATA_NO_CONNECTION, 572 base::Passed(&null))); 573 return CancelCallback(); 574 } 575 576 ++app_list_load_count_; 577 scoped_ptr<AppList> app_list(AppList::CreateFrom(*app_info_value_)); 578 base::MessageLoop::current()->PostTask( 579 FROM_HERE, 580 base::Bind(callback, HTTP_SUCCESS, base::Passed(&app_list))); 581 return CancelCallback(); 582 } 583 584 CancelCallback FakeDriveService::DeleteResource( 585 const std::string& resource_id, 586 const std::string& etag, 587 const EntryActionCallback& callback) { 588 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 589 DCHECK(!callback.is_null()); 590 591 if (offline_) { 592 base::MessageLoop::current()->PostTask( 593 FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION)); 594 return CancelCallback(); 595 } 596 597 base::ListValue* entries = NULL; 598 // Go through entries and remove the one that matches |resource_id|. 599 if (resource_list_value_->GetList("entry", &entries)) { 600 for (size_t i = 0; i < entries->GetSize(); ++i) { 601 base::DictionaryValue* entry = NULL; 602 std::string current_resource_id; 603 if (entries->GetDictionary(i, &entry) && 604 entry->GetString("gd$resourceId.$t", ¤t_resource_id) && 605 resource_id == current_resource_id) { 606 GDataErrorCode error = google_apis::GDATA_OTHER_ERROR; 607 if (entry->HasKey("gd$deleted")) { 608 error = HTTP_NOT_FOUND; 609 } else { 610 entry->Set("gd$deleted", new DictionaryValue); 611 AddNewChangestampAndETag(entry); 612 error = HTTP_SUCCESS; 613 } 614 base::MessageLoop::current()->PostTask( 615 FROM_HERE, base::Bind(callback, error)); 616 return CancelCallback(); 617 } 618 } 619 } 620 621 base::MessageLoop::current()->PostTask( 622 FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND)); 623 return CancelCallback(); 624 } 625 626 CancelCallback FakeDriveService::DownloadFile( 627 const base::FilePath& local_cache_path, 628 const std::string& resource_id, 629 const DownloadActionCallback& download_action_callback, 630 const GetContentCallback& get_content_callback, 631 const ProgressCallback& progress_callback) { 632 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 633 DCHECK(!download_action_callback.is_null()); 634 635 if (offline_) { 636 base::MessageLoop::current()->PostTask( 637 FROM_HERE, 638 base::Bind(download_action_callback, 639 GDATA_NO_CONNECTION, 640 base::FilePath())); 641 return CancelCallback(); 642 } 643 644 // The field content.src is the URL to download the file. 645 base::DictionaryValue* entry = FindEntryByResourceId(resource_id); 646 if (!entry) { 647 base::MessageLoopProxy::current()->PostTask( 648 FROM_HERE, 649 base::Bind(download_action_callback, HTTP_NOT_FOUND, base::FilePath())); 650 return CancelCallback(); 651 } 652 653 // Write "x"s of the file size specified in the entry. 654 std::string file_size_string; 655 entry->GetString("docs$size.$t", &file_size_string); 656 int64 file_size = 0; 657 if (base::StringToInt64(file_size_string, &file_size)) { 658 base::BinaryValue* content_binary_data; 659 std::string content_data; 660 if (entry->GetBinary("test$data", &content_binary_data)) { 661 content_data = std::string(content_binary_data->GetBuffer(), 662 content_binary_data->GetSize()); 663 } 664 DCHECK_EQ(static_cast<size_t>(file_size), content_data.size()); 665 666 if (!get_content_callback.is_null()) { 667 const int64 kBlockSize = 5; 668 for (int64 i = 0; i < file_size; i += kBlockSize) { 669 const int64 size = std::min(kBlockSize, file_size - i); 670 scoped_ptr<std::string> content_for_callback( 671 new std::string(content_data.substr(i, size))); 672 base::MessageLoopProxy::current()->PostTask( 673 FROM_HERE, 674 base::Bind(get_content_callback, HTTP_SUCCESS, 675 base::Passed(&content_for_callback))); 676 } 677 } 678 679 if (test_util::WriteStringToFile(local_cache_path, content_data)) { 680 if (!progress_callback.is_null()) { 681 // See also the comment in ResumeUpload(). For testing that clients 682 // can handle the case progress_callback is called multiple times, 683 // here we invoke the callback twice. 684 base::MessageLoopProxy::current()->PostTask( 685 FROM_HERE, 686 base::Bind(progress_callback, file_size / 2, file_size)); 687 base::MessageLoopProxy::current()->PostTask( 688 FROM_HERE, 689 base::Bind(progress_callback, file_size, file_size)); 690 } 691 base::MessageLoopProxy::current()->PostTask( 692 FROM_HERE, 693 base::Bind(download_action_callback, 694 HTTP_SUCCESS, 695 local_cache_path)); 696 return CancelCallback(); 697 } 698 } 699 700 // Failed to write the content. 701 base::MessageLoopProxy::current()->PostTask( 702 FROM_HERE, 703 base::Bind(download_action_callback, GDATA_FILE_ERROR, base::FilePath())); 704 return CancelCallback(); 705 } 706 707 CancelCallback FakeDriveService::CopyResource( 708 const std::string& resource_id, 709 const std::string& in_parent_resource_id, 710 const std::string& new_title, 711 const GetResourceEntryCallback& callback) { 712 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 713 DCHECK(!callback.is_null()); 714 715 if (offline_) { 716 scoped_ptr<ResourceEntry> null; 717 base::MessageLoop::current()->PostTask( 718 FROM_HERE, 719 base::Bind(callback, 720 GDATA_NO_CONNECTION, 721 base::Passed(&null))); 722 return CancelCallback(); 723 } 724 725 const std::string& parent_resource_id = in_parent_resource_id.empty() ? 726 GetRootResourceId() : in_parent_resource_id; 727 728 base::ListValue* entries = NULL; 729 // Go through entries and copy the one that matches |resource_id|. 730 if (resource_list_value_->GetList("entry", &entries)) { 731 for (size_t i = 0; i < entries->GetSize(); ++i) { 732 base::DictionaryValue* entry = NULL; 733 std::string current_resource_id; 734 if (entries->GetDictionary(i, &entry) && 735 entry->GetString("gd$resourceId.$t", ¤t_resource_id) && 736 resource_id == current_resource_id) { 737 // Make a copy and set the new resource ID and the new title. 738 scoped_ptr<DictionaryValue> copied_entry(entry->DeepCopy()); 739 copied_entry->SetString("gd$resourceId.$t", 740 resource_id + "_copied"); 741 copied_entry->SetString("title.$t", new_title); 742 743 // Reset parent directory. 744 base::ListValue* links = NULL; 745 if (!entry->GetList("link", &links)) { 746 links = new base::ListValue; 747 entry->Set("link", links); 748 } 749 links->Clear(); 750 751 base::DictionaryValue* link = new base::DictionaryValue; 752 link->SetString( 753 "rel", "http://schemas.google.com/docs/2007#parent"); 754 link->SetString("href", GetFakeLinkUrl(parent_resource_id).spec()); 755 links->Append(link); 756 757 AddNewChangestampAndETag(copied_entry.get()); 758 759 // Parse the new entry. 760 scoped_ptr<ResourceEntry> resource_entry = 761 ResourceEntry::CreateFrom(*copied_entry); 762 // Add it to the resource list. 763 entries->Append(copied_entry.release()); 764 765 base::MessageLoop::current()->PostTask( 766 FROM_HERE, 767 base::Bind(callback, 768 HTTP_SUCCESS, 769 base::Passed(&resource_entry))); 770 return CancelCallback(); 771 } 772 } 773 } 774 775 scoped_ptr<ResourceEntry> null; 776 base::MessageLoop::current()->PostTask( 777 FROM_HERE, 778 base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null))); 779 return CancelCallback(); 780 } 781 782 CancelCallback FakeDriveService::CopyHostedDocument( 783 const std::string& resource_id, 784 const std::string& new_title, 785 const GetResourceEntryCallback& callback) { 786 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 787 DCHECK(!callback.is_null()); 788 789 return CopyResource(resource_id, std::string(), new_title, callback); 790 } 791 792 CancelCallback FakeDriveService::RenameResource( 793 const std::string& resource_id, 794 const std::string& new_title, 795 const EntryActionCallback& callback) { 796 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 797 DCHECK(!callback.is_null()); 798 799 if (offline_) { 800 base::MessageLoop::current()->PostTask( 801 FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION)); 802 return CancelCallback(); 803 } 804 805 base::DictionaryValue* entry = FindEntryByResourceId(resource_id); 806 if (entry) { 807 entry->SetString("title.$t", new_title); 808 AddNewChangestampAndETag(entry); 809 base::MessageLoop::current()->PostTask( 810 FROM_HERE, base::Bind(callback, HTTP_SUCCESS)); 811 return CancelCallback(); 812 } 813 814 base::MessageLoop::current()->PostTask( 815 FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND)); 816 return CancelCallback(); 817 } 818 819 CancelCallback FakeDriveService::TouchResource( 820 const std::string& resource_id, 821 const base::Time& modified_date, 822 const base::Time& last_viewed_by_me_date, 823 const GetResourceEntryCallback& callback) { 824 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 825 DCHECK(!modified_date.is_null()); 826 DCHECK(!last_viewed_by_me_date.is_null()); 827 DCHECK(!callback.is_null()); 828 829 if (offline_) { 830 base::MessageLoop::current()->PostTask( 831 FROM_HERE, 832 base::Bind(callback, GDATA_NO_CONNECTION, 833 base::Passed(scoped_ptr<ResourceEntry>()))); 834 return CancelCallback(); 835 } 836 837 base::DictionaryValue* entry = FindEntryByResourceId(resource_id); 838 if (!entry) { 839 base::MessageLoop::current()->PostTask( 840 FROM_HERE, 841 base::Bind(callback, HTTP_NOT_FOUND, 842 base::Passed(scoped_ptr<ResourceEntry>()))); 843 return CancelCallback(); 844 } 845 846 entry->SetString("updated.$t", 847 util::FormatTimeAsString(modified_date)); 848 entry->SetString("gd$lastViewed.$t", 849 util::FormatTimeAsString(last_viewed_by_me_date)); 850 AddNewChangestampAndETag(entry); 851 852 scoped_ptr<ResourceEntry> parsed_entry(ResourceEntry::CreateFrom(*entry)); 853 base::MessageLoop::current()->PostTask( 854 FROM_HERE, 855 base::Bind(callback, HTTP_SUCCESS, base::Passed(&parsed_entry))); 856 return CancelCallback(); 857 } 858 859 CancelCallback FakeDriveService::AddResourceToDirectory( 860 const std::string& parent_resource_id, 861 const std::string& resource_id, 862 const EntryActionCallback& callback) { 863 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 864 DCHECK(!callback.is_null()); 865 866 if (offline_) { 867 base::MessageLoop::current()->PostTask( 868 FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION)); 869 return CancelCallback(); 870 } 871 872 base::DictionaryValue* entry = FindEntryByResourceId(resource_id); 873 if (entry) { 874 base::ListValue* links = NULL; 875 if (!entry->GetList("link", &links)) { 876 links = new base::ListValue; 877 entry->Set("link", links); 878 } 879 880 // On the real Drive server, resources do not necessary shape a tree 881 // structure. That is, each resource can have multiple parent. 882 // We mimic the behavior here; AddResourceToDirectoy just adds 883 // one more parent link, not overwriting old links. 884 base::DictionaryValue* link = new base::DictionaryValue; 885 link->SetString("rel", "http://schemas.google.com/docs/2007#parent"); 886 link->SetString( 887 "href", GetFakeLinkUrl(parent_resource_id).spec()); 888 links->Append(link); 889 890 AddNewChangestampAndETag(entry); 891 base::MessageLoop::current()->PostTask( 892 FROM_HERE, base::Bind(callback, HTTP_SUCCESS)); 893 return CancelCallback(); 894 } 895 896 base::MessageLoop::current()->PostTask( 897 FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND)); 898 return CancelCallback(); 899 } 900 901 CancelCallback FakeDriveService::RemoveResourceFromDirectory( 902 const std::string& parent_resource_id, 903 const std::string& resource_id, 904 const EntryActionCallback& callback) { 905 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 906 DCHECK(!callback.is_null()); 907 908 if (offline_) { 909 base::MessageLoop::current()->PostTask( 910 FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION)); 911 return CancelCallback(); 912 } 913 914 base::DictionaryValue* entry = FindEntryByResourceId(resource_id); 915 if (entry) { 916 base::ListValue* links = NULL; 917 if (entry->GetList("link", &links)) { 918 GURL parent_content_url = GetFakeLinkUrl(parent_resource_id); 919 for (size_t i = 0; i < links->GetSize(); ++i) { 920 base::DictionaryValue* link = NULL; 921 std::string rel; 922 std::string href; 923 if (links->GetDictionary(i, &link) && 924 link->GetString("rel", &rel) && 925 link->GetString("href", &href) && 926 rel == "http://schemas.google.com/docs/2007#parent" && 927 GURL(href) == parent_content_url) { 928 links->Remove(i, NULL); 929 AddNewChangestampAndETag(entry); 930 base::MessageLoop::current()->PostTask( 931 FROM_HERE, base::Bind(callback, HTTP_SUCCESS)); 932 return CancelCallback(); 933 } 934 } 935 } 936 } 937 938 base::MessageLoop::current()->PostTask( 939 FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND)); 940 return CancelCallback(); 941 } 942 943 CancelCallback FakeDriveService::AddNewDirectory( 944 const std::string& parent_resource_id, 945 const std::string& directory_title, 946 const GetResourceEntryCallback& callback) { 947 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 948 DCHECK(!callback.is_null()); 949 950 if (offline_) { 951 scoped_ptr<ResourceEntry> null; 952 base::MessageLoop::current()->PostTask( 953 FROM_HERE, 954 base::Bind(callback, 955 GDATA_NO_CONNECTION, 956 base::Passed(&null))); 957 return CancelCallback(); 958 } 959 960 const char kContentType[] = "application/atom+xml;type=feed"; 961 const base::DictionaryValue* new_entry = AddNewEntry(kContentType, 962 "", // content_data 963 parent_resource_id, 964 directory_title, 965 false, // shared_with_me 966 "folder"); 967 if (!new_entry) { 968 scoped_ptr<ResourceEntry> null; 969 base::MessageLoop::current()->PostTask( 970 FROM_HERE, 971 base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null))); 972 return CancelCallback(); 973 } 974 975 scoped_ptr<ResourceEntry> parsed_entry(ResourceEntry::CreateFrom(*new_entry)); 976 base::MessageLoop::current()->PostTask( 977 FROM_HERE, 978 base::Bind(callback, HTTP_CREATED, base::Passed(&parsed_entry))); 979 return CancelCallback(); 980 } 981 982 CancelCallback FakeDriveService::InitiateUploadNewFile( 983 const std::string& content_type, 984 int64 content_length, 985 const std::string& parent_resource_id, 986 const std::string& title, 987 const InitiateUploadCallback& callback) { 988 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 989 DCHECK(!callback.is_null()); 990 991 if (offline_) { 992 base::MessageLoop::current()->PostTask( 993 FROM_HERE, 994 base::Bind(callback, GDATA_NO_CONNECTION, GURL())); 995 return CancelCallback(); 996 } 997 998 if (parent_resource_id != GetRootResourceId() && 999 !FindEntryByResourceId(parent_resource_id)) { 1000 base::MessageLoop::current()->PostTask( 1001 FROM_HERE, 1002 base::Bind(callback, HTTP_NOT_FOUND, GURL())); 1003 return CancelCallback(); 1004 } 1005 1006 GURL session_url = GetNewUploadSessionUrl(); 1007 upload_sessions_[session_url] = 1008 UploadSession(content_type, content_length, 1009 parent_resource_id, 1010 "", // resource_id 1011 "", // etag 1012 title); 1013 1014 base::MessageLoop::current()->PostTask( 1015 FROM_HERE, 1016 base::Bind(callback, HTTP_SUCCESS, session_url)); 1017 return CancelCallback(); 1018 } 1019 1020 CancelCallback FakeDriveService::InitiateUploadExistingFile( 1021 const std::string& content_type, 1022 int64 content_length, 1023 const std::string& resource_id, 1024 const std::string& etag, 1025 const InitiateUploadCallback& callback) { 1026 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 1027 DCHECK(!callback.is_null()); 1028 1029 if (offline_) { 1030 base::MessageLoop::current()->PostTask( 1031 FROM_HERE, 1032 base::Bind(callback, GDATA_NO_CONNECTION, GURL())); 1033 return CancelCallback(); 1034 } 1035 1036 DictionaryValue* entry = FindEntryByResourceId(resource_id); 1037 if (!entry) { 1038 base::MessageLoop::current()->PostTask( 1039 FROM_HERE, 1040 base::Bind(callback, HTTP_NOT_FOUND, GURL())); 1041 return CancelCallback(); 1042 } 1043 1044 std::string entry_etag; 1045 entry->GetString("gd$etag", &entry_etag); 1046 if (!etag.empty() && etag != entry_etag) { 1047 base::MessageLoop::current()->PostTask( 1048 FROM_HERE, 1049 base::Bind(callback, HTTP_PRECONDITION, GURL())); 1050 return CancelCallback(); 1051 } 1052 1053 GURL session_url = GetNewUploadSessionUrl(); 1054 upload_sessions_[session_url] = 1055 UploadSession(content_type, content_length, 1056 "", // parent_resource_id 1057 resource_id, 1058 entry_etag, 1059 "" /* title */); 1060 1061 base::MessageLoop::current()->PostTask( 1062 FROM_HERE, 1063 base::Bind(callback, HTTP_SUCCESS, session_url)); 1064 return CancelCallback(); 1065 } 1066 1067 CancelCallback FakeDriveService::GetUploadStatus( 1068 const GURL& upload_url, 1069 int64 content_length, 1070 const UploadRangeCallback& callback) { 1071 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 1072 DCHECK(!callback.is_null()); 1073 return CancelCallback(); 1074 } 1075 1076 CancelCallback FakeDriveService::ResumeUpload( 1077 const GURL& upload_url, 1078 int64 start_position, 1079 int64 end_position, 1080 int64 content_length, 1081 const std::string& content_type, 1082 const base::FilePath& local_file_path, 1083 const UploadRangeCallback& callback, 1084 const ProgressCallback& progress_callback) { 1085 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 1086 DCHECK(!callback.is_null()); 1087 1088 GetResourceEntryCallback completion_callback 1089 = base::Bind(&ScheduleUploadRangeCallback, 1090 callback, start_position, end_position); 1091 1092 if (offline_) { 1093 completion_callback.Run(GDATA_NO_CONNECTION, scoped_ptr<ResourceEntry>()); 1094 return CancelCallback(); 1095 } 1096 1097 if (!upload_sessions_.count(upload_url)) { 1098 completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<ResourceEntry>()); 1099 return CancelCallback(); 1100 } 1101 1102 UploadSession* session = &upload_sessions_[upload_url]; 1103 1104 // Chunks are required to be sent in such a ways that they fill from the start 1105 // of the not-yet-uploaded part with no gaps nor overlaps. 1106 if (session->uploaded_size != start_position) { 1107 completion_callback.Run(HTTP_BAD_REQUEST, scoped_ptr<ResourceEntry>()); 1108 return CancelCallback(); 1109 } 1110 1111 if (!progress_callback.is_null()) { 1112 // In the real GDataWapi/Drive DriveService, progress is reported in 1113 // nondeterministic timing. In this fake implementation, we choose to call 1114 // it twice per one ResumeUpload. This is for making sure that client code 1115 // works fine even if the callback is invoked more than once; it is the 1116 // crucial difference of the progress callback from others. 1117 // Note that progress is notified in the relative offset in each chunk. 1118 const int64 chunk_size = end_position - start_position; 1119 base::MessageLoop::current()->PostTask( 1120 FROM_HERE, base::Bind(progress_callback, chunk_size / 2, chunk_size)); 1121 base::MessageLoop::current()->PostTask( 1122 FROM_HERE, base::Bind(progress_callback, chunk_size, chunk_size)); 1123 } 1124 1125 if (content_length != end_position) { 1126 session->uploaded_size = end_position; 1127 completion_callback.Run(HTTP_RESUME_INCOMPLETE, 1128 scoped_ptr<ResourceEntry>()); 1129 return CancelCallback(); 1130 } 1131 1132 std::string content_data; 1133 if (!file_util::ReadFileToString(local_file_path, &content_data)) { 1134 session->uploaded_size = end_position; 1135 completion_callback.Run(GDATA_FILE_ERROR, scoped_ptr<ResourceEntry>()); 1136 return CancelCallback(); 1137 } 1138 session->uploaded_size = end_position; 1139 1140 // |resource_id| is empty if the upload is for new file. 1141 if (session->resource_id.empty()) { 1142 DCHECK(!session->parent_resource_id.empty()); 1143 DCHECK(!session->title.empty()); 1144 const DictionaryValue* new_entry = AddNewEntry( 1145 session->content_type, 1146 content_data, 1147 session->parent_resource_id, 1148 session->title, 1149 false, // shared_with_me 1150 "file"); 1151 if (!new_entry) { 1152 completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<ResourceEntry>()); 1153 return CancelCallback(); 1154 } 1155 1156 completion_callback.Run(HTTP_CREATED, 1157 ResourceEntry::CreateFrom(*new_entry)); 1158 return CancelCallback(); 1159 } 1160 1161 DictionaryValue* entry = FindEntryByResourceId(session->resource_id); 1162 if (!entry) { 1163 completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<ResourceEntry>()); 1164 return CancelCallback(); 1165 } 1166 1167 std::string entry_etag; 1168 entry->GetString("gd$etag", &entry_etag); 1169 if (entry_etag.empty() || session->etag != entry_etag) { 1170 completion_callback.Run(HTTP_PRECONDITION, scoped_ptr<ResourceEntry>()); 1171 return CancelCallback(); 1172 } 1173 1174 entry->SetString("docs$md5Checksum.$t", base::MD5String(content_data)); 1175 entry->Set("test$data", 1176 base::BinaryValue::CreateWithCopiedBuffer( 1177 content_data.data(), content_data.size())); 1178 entry->SetString("docs$size.$t", base::Int64ToString(end_position)); 1179 AddNewChangestampAndETag(entry); 1180 1181 completion_callback.Run(HTTP_SUCCESS, ResourceEntry::CreateFrom(*entry)); 1182 return CancelCallback(); 1183 } 1184 1185 CancelCallback FakeDriveService::AuthorizeApp( 1186 const std::string& resource_id, 1187 const std::string& app_id, 1188 const AuthorizeAppCallback& callback) { 1189 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 1190 DCHECK(!callback.is_null()); 1191 return CancelCallback(); 1192 } 1193 1194 void FakeDriveService::AddNewFile(const std::string& content_type, 1195 const std::string& content_data, 1196 const std::string& parent_resource_id, 1197 const std::string& title, 1198 bool shared_with_me, 1199 const GetResourceEntryCallback& callback) { 1200 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 1201 DCHECK(!callback.is_null()); 1202 1203 if (offline_) { 1204 scoped_ptr<ResourceEntry> null; 1205 base::MessageLoop::current()->PostTask( 1206 FROM_HERE, 1207 base::Bind(callback, 1208 GDATA_NO_CONNECTION, 1209 base::Passed(&null))); 1210 return; 1211 } 1212 1213 // Prepare "kind" for hosted documents. This only supports Google Document. 1214 std::string entry_kind; 1215 if (content_type == "application/vnd.google-apps.document") 1216 entry_kind = "document"; 1217 else 1218 entry_kind = "file"; 1219 1220 const base::DictionaryValue* new_entry = AddNewEntry(content_type, 1221 content_data, 1222 parent_resource_id, 1223 title, 1224 shared_with_me, 1225 entry_kind); 1226 if (!new_entry) { 1227 scoped_ptr<ResourceEntry> null; 1228 base::MessageLoop::current()->PostTask( 1229 FROM_HERE, 1230 base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null))); 1231 return; 1232 } 1233 1234 scoped_ptr<ResourceEntry> parsed_entry( 1235 ResourceEntry::CreateFrom(*new_entry)); 1236 base::MessageLoop::current()->PostTask( 1237 FROM_HERE, 1238 base::Bind(callback, HTTP_CREATED, base::Passed(&parsed_entry))); 1239 } 1240 1241 void FakeDriveService::SetLastModifiedTime( 1242 const std::string& resource_id, 1243 const base::Time& last_modified_time, 1244 const GetResourceEntryCallback& callback) { 1245 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 1246 DCHECK(!callback.is_null()); 1247 1248 if (offline_) { 1249 scoped_ptr<ResourceEntry> null; 1250 base::MessageLoop::current()->PostTask( 1251 FROM_HERE, 1252 base::Bind(callback, 1253 GDATA_NO_CONNECTION, 1254 base::Passed(&null))); 1255 return; 1256 } 1257 1258 base::DictionaryValue* entry = FindEntryByResourceId(resource_id); 1259 if (!entry) { 1260 scoped_ptr<ResourceEntry> null; 1261 base::MessageLoop::current()->PostTask( 1262 FROM_HERE, 1263 base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null))); 1264 return; 1265 } 1266 1267 if (last_modified_time.is_null()) { 1268 entry->Remove("updated.$t", NULL); 1269 } else { 1270 entry->SetString("updated.$t", 1271 util::FormatTimeAsString(last_modified_time)); 1272 } 1273 1274 scoped_ptr<ResourceEntry> parsed_entry( 1275 ResourceEntry::CreateFrom(*entry)); 1276 base::MessageLoop::current()->PostTask( 1277 FROM_HERE, 1278 base::Bind(callback, HTTP_SUCCESS, base::Passed(&parsed_entry))); 1279 } 1280 1281 base::DictionaryValue* FakeDriveService::FindEntryByResourceId( 1282 const std::string& resource_id) { 1283 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 1284 1285 base::ListValue* entries = NULL; 1286 // Go through entries and return the one that matches |resource_id|. 1287 if (resource_list_value_->GetList("entry", &entries)) { 1288 for (size_t i = 0; i < entries->GetSize(); ++i) { 1289 base::DictionaryValue* entry = NULL; 1290 std::string current_resource_id; 1291 if (entries->GetDictionary(i, &entry) && 1292 entry->GetString("gd$resourceId.$t", ¤t_resource_id) && 1293 resource_id == current_resource_id) { 1294 return entry; 1295 } 1296 } 1297 } 1298 1299 return NULL; 1300 } 1301 1302 base::DictionaryValue* FakeDriveService::FindEntryByContentUrl( 1303 const GURL& content_url) { 1304 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 1305 1306 base::ListValue* entries = NULL; 1307 // Go through entries and return the one that matches |content_url|. 1308 if (resource_list_value_->GetList("entry", &entries)) { 1309 for (size_t i = 0; i < entries->GetSize(); ++i) { 1310 base::DictionaryValue* entry = NULL; 1311 std::string current_content_url; 1312 if (entries->GetDictionary(i, &entry) && 1313 entry->GetString("content.src", ¤t_content_url) && 1314 content_url == GURL(current_content_url)) { 1315 return entry; 1316 } 1317 } 1318 } 1319 1320 return NULL; 1321 } 1322 1323 std::string FakeDriveService::GetNewResourceId() { 1324 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 1325 1326 ++resource_id_count_; 1327 return base::StringPrintf("resource_id_%d", resource_id_count_); 1328 } 1329 1330 void FakeDriveService::AddNewChangestampAndETag(base::DictionaryValue* entry) { 1331 ++largest_changestamp_; 1332 entry->SetString("docs$changestamp.value", 1333 base::Int64ToString(largest_changestamp_)); 1334 entry->SetString("gd$etag", 1335 "etag_" + base::Int64ToString(largest_changestamp_)); 1336 } 1337 1338 const base::DictionaryValue* FakeDriveService::AddNewEntry( 1339 const std::string& content_type, 1340 const std::string& content_data, 1341 const std::string& parent_resource_id, 1342 const std::string& title, 1343 bool shared_with_me, 1344 const std::string& entry_kind) { 1345 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 1346 1347 if (parent_resource_id != GetRootResourceId() && 1348 !FindEntryByResourceId(parent_resource_id)) { 1349 return NULL; 1350 } 1351 1352 std::string resource_id = GetNewResourceId(); 1353 GURL upload_url = GURL("https://xxx/upload/" + resource_id); 1354 1355 scoped_ptr<base::DictionaryValue> new_entry(new base::DictionaryValue); 1356 // Set the resource ID and the title 1357 new_entry->SetString("gd$resourceId.$t", resource_id); 1358 new_entry->SetString("title.$t", title); 1359 new_entry->SetString("docs$filename", title); 1360 // Set the contents, size and MD5 for a file. 1361 if (entry_kind == "file") { 1362 new_entry->Set("test$data", 1363 base::BinaryValue::CreateWithCopiedBuffer( 1364 content_data.c_str(), content_data.size())); 1365 new_entry->SetString("docs$size.$t", 1366 base::Int64ToString(content_data.size())); 1367 new_entry->SetString("docs$md5Checksum.$t", 1368 base::MD5String(content_data)); 1369 } 1370 1371 // Add "category" which sets the resource type to |entry_kind|. 1372 base::ListValue* categories = new base::ListValue; 1373 base::DictionaryValue* category = new base::DictionaryValue; 1374 category->SetString("scheme", "http://schemas.google.com/g/2005#kind"); 1375 category->SetString("term", "http://schemas.google.com/docs/2007#" + 1376 entry_kind); 1377 categories->Append(category); 1378 new_entry->Set("category", categories); 1379 if (shared_with_me) { 1380 base::DictionaryValue* shared_with_me_label = new base::DictionaryValue; 1381 shared_with_me_label->SetString("label", "shared-with-me"); 1382 shared_with_me_label->SetString("scheme", 1383 "http://schemas.google.com/g/2005/labels"); 1384 shared_with_me_label->SetString( 1385 "term", "http://schemas.google.com/g/2005/labels#shared"); 1386 categories->Append(shared_with_me_label); 1387 } 1388 1389 std::string escaped_resource_id = net::EscapePath(resource_id); 1390 1391 // Add "content" which sets the content URL. 1392 base::DictionaryValue* content = new base::DictionaryValue; 1393 content->SetString("src", "https://xxx/content/" + escaped_resource_id); 1394 content->SetString("type", content_type); 1395 new_entry->Set("content", content); 1396 1397 // Add "link" which sets the parent URL, the edit URL and the upload URL. 1398 base::ListValue* links = new base::ListValue; 1399 1400 base::DictionaryValue* parent_link = new base::DictionaryValue; 1401 parent_link->SetString("href", GetFakeLinkUrl(parent_resource_id).spec()); 1402 parent_link->SetString("rel", 1403 "http://schemas.google.com/docs/2007#parent"); 1404 links->Append(parent_link); 1405 1406 base::DictionaryValue* edit_link = new base::DictionaryValue; 1407 edit_link->SetString("href", "https://xxx/edit/" + escaped_resource_id); 1408 edit_link->SetString("rel", "edit"); 1409 links->Append(edit_link); 1410 1411 base::DictionaryValue* upload_link = new base::DictionaryValue; 1412 upload_link->SetString("href", upload_url.spec()); 1413 upload_link->SetString("rel", kUploadUrlRel); 1414 links->Append(upload_link); 1415 1416 const GURL share_url = net::AppendOrReplaceQueryParameter( 1417 share_url_base_, "name", title); 1418 base::DictionaryValue* share_link = new base::DictionaryValue; 1419 upload_link->SetString("href", share_url.spec()); 1420 upload_link->SetString("rel", kShareUrlRel); 1421 links->Append(share_link); 1422 new_entry->Set("link", links); 1423 1424 AddNewChangestampAndETag(new_entry.get()); 1425 1426 base::Time published_date = 1427 base::Time() + base::TimeDelta::FromMilliseconds(++published_date_seq_); 1428 new_entry->SetString("published.$t", 1429 util::FormatTimeAsString(published_date)); 1430 1431 // If there are no entries, prepare an empty entry to add. 1432 if (!resource_list_value_->HasKey("entry")) 1433 resource_list_value_->Set("entry", new ListValue); 1434 1435 base::DictionaryValue* raw_new_entry = new_entry.release(); 1436 base::ListValue* entries = NULL; 1437 if (resource_list_value_->GetList("entry", &entries)) 1438 entries->Append(raw_new_entry); 1439 1440 return raw_new_entry; 1441 } 1442 1443 void FakeDriveService::GetResourceListInternal( 1444 int64 start_changestamp, 1445 const std::string& search_query, 1446 const std::string& directory_resource_id, 1447 int start_offset, 1448 int max_results, 1449 int* load_counter, 1450 const GetResourceListCallback& callback) { 1451 if (offline_) { 1452 base::MessageLoop::current()->PostTask( 1453 FROM_HERE, 1454 base::Bind(callback, 1455 GDATA_NO_CONNECTION, 1456 base::Passed(scoped_ptr<ResourceList>()))); 1457 return; 1458 } 1459 1460 scoped_ptr<ResourceList> resource_list = 1461 ResourceList::CreateFrom(*resource_list_value_); 1462 1463 // TODO(hashimoto): Drive API always provides largest changestamp. Remove this 1464 // if-statement after API switch. 1465 if (start_changestamp > 0 && start_offset == 0) 1466 resource_list->set_largest_changestamp(largest_changestamp_); 1467 1468 // Filter out entries per parameters like |directory_resource_id| and 1469 // |search_query|. 1470 ScopedVector<ResourceEntry>* entries = resource_list->mutable_entries(); 1471 1472 int num_entries_matched = 0; 1473 for (size_t i = 0; i < entries->size();) { 1474 ResourceEntry* entry = (*entries)[i]; 1475 bool should_exclude = false; 1476 1477 // If |directory_resource_id| is set, exclude the entry if it's not in 1478 // the target directory. 1479 if (!directory_resource_id.empty()) { 1480 // Get the parent resource ID of the entry. 1481 std::string parent_resource_id; 1482 const google_apis::Link* parent_link = 1483 entry->GetLinkByType(Link::LINK_PARENT); 1484 if (parent_link) { 1485 parent_resource_id = 1486 net::UnescapeURLComponent(parent_link->href().ExtractFileName(), 1487 net::UnescapeRule::URL_SPECIAL_CHARS); 1488 } 1489 if (directory_resource_id != parent_resource_id) 1490 should_exclude = true; 1491 } 1492 1493 // If |search_query| is set, exclude the entry if it does not contain the 1494 // search query in the title. 1495 if (!should_exclude && !search_query.empty() && 1496 !EntryMatchWithQuery(*entry, search_query)) { 1497 should_exclude = true; 1498 } 1499 1500 // If |start_changestamp| is set, exclude the entry if the 1501 // changestamp is older than |largest_changestamp|. 1502 // See https://developers.google.com/google-apps/documents-list/ 1503 // #retrieving_all_changes_since_a_given_changestamp 1504 if (start_changestamp > 0 && entry->changestamp() < start_changestamp) 1505 should_exclude = true; 1506 1507 // If the caller requests other list than change list by specifying 1508 // zero-|start_changestamp|, exclude deleted entry from the result. 1509 if (!start_changestamp && entry->deleted()) 1510 should_exclude = true; 1511 1512 // The entry matched the criteria for inclusion. 1513 if (!should_exclude) 1514 ++num_entries_matched; 1515 1516 // If |start_offset| is set, exclude the entry if the entry is before the 1517 // start index. <= instead of < as |num_entries_matched| was 1518 // already incremented. 1519 if (start_offset > 0 && num_entries_matched <= start_offset) 1520 should_exclude = true; 1521 1522 if (should_exclude) 1523 entries->erase(entries->begin() + i); 1524 else 1525 ++i; 1526 } 1527 1528 // If |max_results| is set, trim the entries if the number exceeded the max 1529 // results. 1530 if (max_results > 0 && entries->size() > static_cast<size_t>(max_results)) { 1531 entries->erase(entries->begin() + max_results, entries->end()); 1532 // Adds the next URL. 1533 // Here, we embed information which is needed for continuing the 1534 // GetResourceList request in the next invocation into url query 1535 // parameters. 1536 GURL next_url(base::StringPrintf( 1537 "http://localhost/?start-offset=%d&max-results=%d", 1538 start_offset + max_results, 1539 max_results)); 1540 if (start_changestamp > 0) { 1541 next_url = net::AppendOrReplaceQueryParameter( 1542 next_url, "changestamp", 1543 base::Int64ToString(start_changestamp).c_str()); 1544 } 1545 if (!search_query.empty()) { 1546 next_url = net::AppendOrReplaceQueryParameter( 1547 next_url, "q", search_query); 1548 } 1549 if (!directory_resource_id.empty()) { 1550 next_url = net::AppendOrReplaceQueryParameter( 1551 next_url, "parent", directory_resource_id); 1552 } 1553 1554 Link* link = new Link; 1555 link->set_type(Link::LINK_NEXT); 1556 link->set_href(next_url); 1557 resource_list->mutable_links()->push_back(link); 1558 } 1559 1560 if (load_counter) 1561 *load_counter += 1; 1562 base::MessageLoop::current()->PostTask( 1563 FROM_HERE, 1564 base::Bind(callback, HTTP_SUCCESS, base::Passed(&resource_list))); 1565 } 1566 1567 GURL FakeDriveService::GetNewUploadSessionUrl() { 1568 return GURL("https://upload_session_url/" + 1569 base::Int64ToString(next_upload_sequence_number_++)); 1570 } 1571 1572 } // namespace drive 1573