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