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/chromeos/drive/file_system/copy_operation.h" 6 7 #include <string> 8 9 #include "base/file_util.h" 10 #include "base/task_runner_util.h" 11 #include "chrome/browser/chromeos/drive/drive.pb.h" 12 #include "chrome/browser/chromeos/drive/file_cache.h" 13 #include "chrome/browser/chromeos/drive/file_system/create_file_operation.h" 14 #include "chrome/browser/chromeos/drive/file_system/operation_observer.h" 15 #include "chrome/browser/chromeos/drive/file_system_util.h" 16 #include "chrome/browser/chromeos/drive/job_scheduler.h" 17 #include "chrome/browser/chromeos/drive/resource_entry_conversion.h" 18 #include "chrome/browser/chromeos/drive/resource_metadata.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 23 using content::BrowserThread; 24 25 namespace drive { 26 namespace file_system { 27 28 struct CopyOperation::CopyParams { 29 base::FilePath src_file_path; 30 base::FilePath dest_file_path; 31 bool preserve_last_modified; 32 FileOperationCallback callback; 33 ResourceEntry src_entry; 34 ResourceEntry parent_entry; 35 }; 36 37 // Enum for categorizing where a gdoc represented by a JSON file exists. 38 enum JsonGdocLocationType { 39 NOT_IN_METADATA, 40 IS_ORPHAN, 41 HAS_PARENT, 42 }; 43 44 struct CopyOperation::TransferJsonGdocParams { 45 TransferJsonGdocParams(const FileOperationCallback& callback, 46 const std::string& resource_id, 47 const ResourceEntry& parent_entry, 48 const std::string& new_title) 49 : callback(callback), 50 resource_id(resource_id), 51 parent_resource_id(parent_entry.resource_id()), 52 parent_local_id(parent_entry.local_id()), 53 new_title(new_title), 54 location_type(NOT_IN_METADATA) { 55 } 56 // Parameters supplied or calculated from operation arguments. 57 const FileOperationCallback callback; 58 const std::string resource_id; 59 const std::string parent_resource_id; 60 const std::string parent_local_id; 61 const std::string new_title; 62 63 // Values computed during operation. 64 JsonGdocLocationType location_type; // types where the gdoc file is located. 65 std::string local_id; // the local_id of the file (if exists in metadata.) 66 base::FilePath changed_path; 67 }; 68 69 namespace { 70 71 FileError TryToCopyLocally(internal::ResourceMetadata* metadata, 72 internal::FileCache* cache, 73 CopyOperation::CopyParams* params, 74 std::vector<std::string>* updated_local_ids, 75 bool* directory_changed, 76 bool* should_copy_on_server) { 77 FileError error = metadata->GetResourceEntryByPath(params->src_file_path, 78 ¶ms->src_entry); 79 if (error != FILE_ERROR_OK) 80 return error; 81 82 error = metadata->GetResourceEntryByPath(params->dest_file_path.DirName(), 83 ¶ms->parent_entry); 84 if (error != FILE_ERROR_OK) 85 return error; 86 87 if (!params->parent_entry.file_info().is_directory()) 88 return FILE_ERROR_NOT_A_DIRECTORY; 89 90 // Drive File System doesn't support recursive copy. 91 if (params->src_entry.file_info().is_directory()) 92 return FILE_ERROR_NOT_A_FILE; 93 94 // Check destination. 95 ResourceEntry dest_entry; 96 error = metadata->GetResourceEntryByPath(params->dest_file_path, &dest_entry); 97 switch (error) { 98 case FILE_ERROR_OK: 99 // File API spec says it is an error to try to "copy a file to a path 100 // occupied by a directory". 101 if (dest_entry.file_info().is_directory()) 102 return FILE_ERROR_INVALID_OPERATION; 103 104 // Move the existing entry to the trash. 105 dest_entry.set_parent_local_id(util::kDriveTrashDirLocalId); 106 error = metadata->RefreshEntry(dest_entry); 107 if (error != FILE_ERROR_OK) 108 return error; 109 updated_local_ids->push_back(dest_entry.local_id()); 110 *directory_changed = true; 111 break; 112 case FILE_ERROR_NOT_FOUND: 113 break; 114 default: 115 return error; 116 } 117 118 // If the cache file is not present and the entry exists on the server, 119 // server side copy should be used. 120 if (!params->src_entry.file_specific_info().cache_state().is_present() && 121 !params->src_entry.resource_id().empty()) { 122 *should_copy_on_server = true; 123 return FILE_ERROR_OK; 124 } 125 126 // Copy locally. 127 ResourceEntry entry; 128 const int64 now = base::Time::Now().ToInternalValue(); 129 entry.set_title(params->dest_file_path.BaseName().AsUTF8Unsafe()); 130 entry.set_parent_local_id(params->parent_entry.local_id()); 131 entry.mutable_file_specific_info()->set_content_mime_type( 132 params->src_entry.file_specific_info().content_mime_type()); 133 entry.set_metadata_edit_state(ResourceEntry::DIRTY); 134 entry.set_modification_date(base::Time::Now().ToInternalValue()); 135 entry.mutable_file_info()->set_last_modified( 136 params->preserve_last_modified ? 137 params->src_entry.file_info().last_modified() : now); 138 entry.mutable_file_info()->set_last_accessed(now); 139 140 std::string local_id; 141 error = metadata->AddEntry(entry, &local_id); 142 if (error != FILE_ERROR_OK) 143 return error; 144 updated_local_ids->push_back(local_id); 145 *directory_changed = true; 146 147 if (!params->src_entry.file_specific_info().cache_state().is_present()) { 148 DCHECK(params->src_entry.resource_id().empty()); 149 // Locally created empty file may have no cache file. 150 return FILE_ERROR_OK; 151 } 152 153 base::FilePath cache_file_path; 154 error = cache->GetFile(params->src_entry.local_id(), &cache_file_path); 155 if (error != FILE_ERROR_OK) 156 return error; 157 158 return cache->Store(local_id, std::string(), cache_file_path, 159 internal::FileCache::FILE_OPERATION_COPY); 160 } 161 162 // Stores the entry returned from the server and returns its path. 163 FileError UpdateLocalStateForServerSideOperation( 164 internal::ResourceMetadata* metadata, 165 scoped_ptr<google_apis::FileResource> file_resource, 166 base::FilePath* file_path) { 167 DCHECK(file_resource); 168 169 ResourceEntry entry; 170 std::string parent_resource_id; 171 if (!ConvertFileResourceToResourceEntry(*file_resource, &entry, 172 &parent_resource_id) || 173 parent_resource_id.empty()) 174 return FILE_ERROR_NOT_A_FILE; 175 176 std::string parent_local_id; 177 FileError error = metadata->GetIdByResourceId(parent_resource_id, 178 &parent_local_id); 179 if (error != FILE_ERROR_OK) 180 return error; 181 entry.set_parent_local_id(parent_local_id); 182 183 std::string local_id; 184 error = metadata->AddEntry(entry, &local_id); 185 // Depending on timing, the metadata may have inserted via change list 186 // already. So, FILE_ERROR_EXISTS is not an error. 187 if (error == FILE_ERROR_EXISTS) 188 error = metadata->GetIdByResourceId(entry.resource_id(), &local_id); 189 190 if (error != FILE_ERROR_OK) 191 return error; 192 193 return metadata->GetFilePath(local_id, file_path); 194 } 195 196 // Stores the file at |local_file_path| to the cache as a content of entry at 197 // |remote_dest_path|, and marks it dirty. 198 FileError UpdateLocalStateForScheduleTransfer( 199 internal::ResourceMetadata* metadata, 200 internal::FileCache* cache, 201 const base::FilePath& local_src_path, 202 const base::FilePath& remote_dest_path, 203 std::string* local_id) { 204 FileError error = metadata->GetIdByPath(remote_dest_path, local_id); 205 if (error != FILE_ERROR_OK) 206 return error; 207 208 ResourceEntry entry; 209 error = metadata->GetResourceEntryById(*local_id, &entry); 210 if (error != FILE_ERROR_OK) 211 return error; 212 213 return cache->Store(*local_id, std::string(), local_src_path, 214 internal::FileCache::FILE_OPERATION_COPY); 215 } 216 217 // Gets the file size of the |local_path|, and the ResourceEntry for the parent 218 // of |remote_path| to prepare the necessary information for transfer. 219 FileError PrepareTransferFileFromLocalToRemote( 220 internal::ResourceMetadata* metadata, 221 const base::FilePath& local_src_path, 222 const base::FilePath& remote_dest_path, 223 std::string* gdoc_resource_id, 224 ResourceEntry* parent_entry) { 225 FileError error = metadata->GetResourceEntryByPath( 226 remote_dest_path.DirName(), parent_entry); 227 if (error != FILE_ERROR_OK) 228 return error; 229 230 // The destination's parent must be a directory. 231 if (!parent_entry->file_info().is_directory()) 232 return FILE_ERROR_NOT_A_DIRECTORY; 233 234 // Try to parse GDoc File and extract the resource id, if necessary. 235 // Failing isn't problem. It'd be handled as a regular file, then. 236 if (util::HasGDocFileExtension(local_src_path)) 237 *gdoc_resource_id = util::ReadResourceIdFromGDocFile(local_src_path); 238 return FILE_ERROR_OK; 239 } 240 241 // Performs local work before server-side work for transferring JSON-represented 242 // gdoc files. 243 FileError LocalWorkForTransferJsonGdocFile( 244 internal::ResourceMetadata* metadata, 245 CopyOperation::TransferJsonGdocParams* params) { 246 std::string local_id; 247 FileError error = metadata->GetIdByResourceId(params->resource_id, &local_id); 248 if (error != FILE_ERROR_OK) { 249 params->location_type = NOT_IN_METADATA; 250 return error == FILE_ERROR_NOT_FOUND ? FILE_ERROR_OK : error; 251 } 252 253 ResourceEntry entry; 254 error = metadata->GetResourceEntryById(local_id, &entry); 255 if (error != FILE_ERROR_OK) 256 return error; 257 params->local_id = entry.local_id(); 258 259 if (entry.parent_local_id() == util::kDriveOtherDirLocalId) { 260 params->location_type = IS_ORPHAN; 261 entry.set_title(params->new_title); 262 entry.set_parent_local_id(params->parent_local_id); 263 entry.set_metadata_edit_state(ResourceEntry::DIRTY); 264 entry.set_modification_date(base::Time::Now().ToInternalValue()); 265 error = metadata->RefreshEntry(entry); 266 if (error != FILE_ERROR_OK) 267 return error; 268 return metadata->GetFilePath(local_id, ¶ms->changed_path); 269 } 270 271 params->location_type = HAS_PARENT; 272 return FILE_ERROR_OK; 273 } 274 275 } // namespace 276 277 CopyOperation::CopyOperation(base::SequencedTaskRunner* blocking_task_runner, 278 OperationObserver* observer, 279 JobScheduler* scheduler, 280 internal::ResourceMetadata* metadata, 281 internal::FileCache* cache, 282 const ResourceIdCanonicalizer& id_canonicalizer) 283 : blocking_task_runner_(blocking_task_runner), 284 observer_(observer), 285 scheduler_(scheduler), 286 metadata_(metadata), 287 cache_(cache), 288 id_canonicalizer_(id_canonicalizer), 289 create_file_operation_(new CreateFileOperation(blocking_task_runner, 290 observer, 291 metadata)), 292 weak_ptr_factory_(this) { 293 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 294 } 295 296 CopyOperation::~CopyOperation() { 297 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 298 } 299 300 void CopyOperation::Copy(const base::FilePath& src_file_path, 301 const base::FilePath& dest_file_path, 302 bool preserve_last_modified, 303 const FileOperationCallback& callback) { 304 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 305 DCHECK(!callback.is_null()); 306 307 CopyParams* params = new CopyParams; 308 params->src_file_path = src_file_path; 309 params->dest_file_path = dest_file_path; 310 params->preserve_last_modified = preserve_last_modified; 311 params->callback = callback; 312 313 std::vector<std::string>* updated_local_ids = new std::vector<std::string>; 314 bool* directory_changed = new bool(false); 315 bool* should_copy_on_server = new bool(false); 316 base::PostTaskAndReplyWithResult( 317 blocking_task_runner_.get(), 318 FROM_HERE, 319 base::Bind(&TryToCopyLocally, metadata_, cache_, params, 320 updated_local_ids, directory_changed, should_copy_on_server), 321 base::Bind(&CopyOperation::CopyAfterTryToCopyLocally, 322 weak_ptr_factory_.GetWeakPtr(), base::Owned(params), 323 base::Owned(updated_local_ids), base::Owned(directory_changed), 324 base::Owned(should_copy_on_server))); 325 } 326 327 void CopyOperation::CopyAfterTryToCopyLocally( 328 const CopyParams* params, 329 const std::vector<std::string>* updated_local_ids, 330 const bool* directory_changed, 331 const bool* should_copy_on_server, 332 FileError error) { 333 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 334 DCHECK(!params->callback.is_null()); 335 336 for (size_t i = 0; i < updated_local_ids->size(); ++i) 337 observer_->OnEntryUpdatedByOperation((*updated_local_ids)[i]); 338 339 if (*directory_changed) 340 observer_->OnDirectoryChangedByOperation(params->dest_file_path.DirName()); 341 342 if (error != FILE_ERROR_OK || !*should_copy_on_server) { 343 params->callback.Run(error); 344 return; 345 } 346 347 base::FilePath new_title = params->dest_file_path.BaseName(); 348 if (params->src_entry.file_specific_info().is_hosted_document()) { 349 // Drop the document extension, which should not be in the title. 350 // TODO(yoshiki): Remove this code with crbug.com/223304. 351 new_title = new_title.RemoveExtension(); 352 } 353 354 base::Time last_modified = 355 params->preserve_last_modified ? 356 base::Time::FromInternalValue( 357 params->src_entry.file_info().last_modified()) : base::Time(); 358 359 CopyResourceOnServer( 360 params->src_entry.resource_id(), params->parent_entry.resource_id(), 361 new_title.AsUTF8Unsafe(), last_modified, params->callback); 362 } 363 364 void CopyOperation::TransferFileFromLocalToRemote( 365 const base::FilePath& local_src_path, 366 const base::FilePath& remote_dest_path, 367 const FileOperationCallback& callback) { 368 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 369 DCHECK(!callback.is_null()); 370 371 std::string* gdoc_resource_id = new std::string; 372 ResourceEntry* parent_entry = new ResourceEntry; 373 base::PostTaskAndReplyWithResult( 374 blocking_task_runner_.get(), 375 FROM_HERE, 376 base::Bind( 377 &PrepareTransferFileFromLocalToRemote, 378 metadata_, local_src_path, remote_dest_path, 379 gdoc_resource_id, parent_entry), 380 base::Bind( 381 &CopyOperation::TransferFileFromLocalToRemoteAfterPrepare, 382 weak_ptr_factory_.GetWeakPtr(), 383 local_src_path, remote_dest_path, callback, 384 base::Owned(gdoc_resource_id), base::Owned(parent_entry))); 385 } 386 387 void CopyOperation::TransferFileFromLocalToRemoteAfterPrepare( 388 const base::FilePath& local_src_path, 389 const base::FilePath& remote_dest_path, 390 const FileOperationCallback& callback, 391 std::string* gdoc_resource_id, 392 ResourceEntry* parent_entry, 393 FileError error) { 394 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 395 DCHECK(!callback.is_null()); 396 397 if (error != FILE_ERROR_OK) { 398 callback.Run(error); 399 return; 400 } 401 402 // For regular files, schedule the transfer. 403 if (gdoc_resource_id->empty()) { 404 ScheduleTransferRegularFile(local_src_path, remote_dest_path, callback); 405 return; 406 } 407 408 // GDoc file may contain a resource ID in the old format. 409 const std::string canonicalized_resource_id = 410 id_canonicalizer_.Run(*gdoc_resource_id); 411 412 // Drop the document extension, which should not be in the title. 413 // TODO(yoshiki): Remove this code with crbug.com/223304. 414 const std::string new_title = 415 remote_dest_path.BaseName().RemoveExtension().AsUTF8Unsafe(); 416 417 // This is uploading a JSON file representing a hosted document. 418 TransferJsonGdocParams* params = new TransferJsonGdocParams( 419 callback, canonicalized_resource_id, *parent_entry, new_title); 420 base::PostTaskAndReplyWithResult( 421 blocking_task_runner_.get(), 422 FROM_HERE, 423 base::Bind(&LocalWorkForTransferJsonGdocFile, metadata_, params), 424 base::Bind(&CopyOperation::TransferJsonGdocFileAfterLocalWork, 425 weak_ptr_factory_.GetWeakPtr(), base::Owned(params))); 426 } 427 428 void CopyOperation::TransferJsonGdocFileAfterLocalWork( 429 TransferJsonGdocParams* params, 430 FileError error) { 431 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 432 433 if (error != FILE_ERROR_OK) { 434 params->callback.Run(error); 435 return; 436 } 437 438 switch (params->location_type) { 439 // When |resource_id| is found in the local metadata and it has a specific 440 // parent folder, we assume the user's intention is to copy the document and 441 // thus perform the server-side copy operation. 442 case HAS_PARENT: 443 CopyResourceOnServer(params->resource_id, 444 params->parent_resource_id, 445 params->new_title, 446 base::Time(), 447 params->callback); 448 break; 449 // When |resource_id| has no parent, we just set the new destination folder 450 // as the parent, for sharing the document between the original source. 451 // This reparenting is already done in LocalWorkForTransferJsonGdocFile(). 452 case IS_ORPHAN: 453 DCHECK(!params->changed_path.empty()); 454 observer_->OnEntryUpdatedByOperation(params->local_id); 455 observer_->OnDirectoryChangedByOperation(params->changed_path.DirName()); 456 params->callback.Run(error); 457 break; 458 // When the |resource_id| is not in the local metadata, assume it to be a 459 // document just now shared on the server but not synced locally. 460 // Same as the IS_ORPHAN case, we want to deal the case by setting parent, 461 // but this time we need to resort to server side operation. 462 case NOT_IN_METADATA: 463 scheduler_->UpdateResource( 464 params->resource_id, 465 params->parent_resource_id, 466 params->new_title, 467 base::Time(), 468 base::Time(), 469 ClientContext(USER_INITIATED), 470 base::Bind(&CopyOperation::UpdateAfterServerSideOperation, 471 weak_ptr_factory_.GetWeakPtr(), 472 params->callback)); 473 break; 474 } 475 } 476 477 void CopyOperation::CopyResourceOnServer( 478 const std::string& resource_id, 479 const std::string& parent_resource_id, 480 const std::string& new_title, 481 const base::Time& last_modified, 482 const FileOperationCallback& callback) { 483 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 484 DCHECK(!callback.is_null()); 485 486 scheduler_->CopyResource( 487 resource_id, parent_resource_id, new_title, last_modified, 488 base::Bind(&CopyOperation::UpdateAfterServerSideOperation, 489 weak_ptr_factory_.GetWeakPtr(), 490 callback)); 491 } 492 493 void CopyOperation::UpdateAfterServerSideOperation( 494 const FileOperationCallback& callback, 495 google_apis::GDataErrorCode status, 496 scoped_ptr<google_apis::FileResource> entry) { 497 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 498 DCHECK(!callback.is_null()); 499 500 FileError error = GDataToFileError(status); 501 if (error != FILE_ERROR_OK) { 502 callback.Run(error); 503 return; 504 } 505 506 // The copy on the server side is completed successfully. Update the local 507 // metadata. 508 base::FilePath* file_path = new base::FilePath; 509 base::PostTaskAndReplyWithResult( 510 blocking_task_runner_.get(), 511 FROM_HERE, 512 base::Bind(&UpdateLocalStateForServerSideOperation, 513 metadata_, base::Passed(&entry), file_path), 514 base::Bind(&CopyOperation::UpdateAfterLocalStateUpdate, 515 weak_ptr_factory_.GetWeakPtr(), 516 callback, base::Owned(file_path))); 517 } 518 519 void CopyOperation::UpdateAfterLocalStateUpdate( 520 const FileOperationCallback& callback, 521 base::FilePath* file_path, 522 FileError error) { 523 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 524 DCHECK(!callback.is_null()); 525 526 if (error == FILE_ERROR_OK) 527 observer_->OnDirectoryChangedByOperation(file_path->DirName()); 528 callback.Run(error); 529 } 530 531 void CopyOperation::ScheduleTransferRegularFile( 532 const base::FilePath& local_src_path, 533 const base::FilePath& remote_dest_path, 534 const FileOperationCallback& callback) { 535 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 536 DCHECK(!callback.is_null()); 537 538 create_file_operation_->CreateFile( 539 remote_dest_path, 540 false, // Not exclusive (OK even if a file already exists). 541 std::string(), // no specific mime type; CreateFile should guess it. 542 base::Bind(&CopyOperation::ScheduleTransferRegularFileAfterCreate, 543 weak_ptr_factory_.GetWeakPtr(), 544 local_src_path, remote_dest_path, callback)); 545 } 546 547 void CopyOperation::ScheduleTransferRegularFileAfterCreate( 548 const base::FilePath& local_src_path, 549 const base::FilePath& remote_dest_path, 550 const FileOperationCallback& callback, 551 FileError error) { 552 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 553 DCHECK(!callback.is_null()); 554 555 if (error != FILE_ERROR_OK) { 556 callback.Run(error); 557 return; 558 } 559 560 std::string* local_id = new std::string; 561 base::PostTaskAndReplyWithResult( 562 blocking_task_runner_.get(), 563 FROM_HERE, 564 base::Bind( 565 &UpdateLocalStateForScheduleTransfer, 566 metadata_, cache_, local_src_path, remote_dest_path, local_id), 567 base::Bind( 568 &CopyOperation::ScheduleTransferRegularFileAfterUpdateLocalState, 569 weak_ptr_factory_.GetWeakPtr(), callback, remote_dest_path, 570 base::Owned(local_id))); 571 } 572 573 void CopyOperation::ScheduleTransferRegularFileAfterUpdateLocalState( 574 const FileOperationCallback& callback, 575 const base::FilePath& remote_dest_path, 576 std::string* local_id, 577 FileError error) { 578 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 579 DCHECK(!callback.is_null()); 580 581 if (error == FILE_ERROR_OK) { 582 observer_->OnDirectoryChangedByOperation(remote_dest_path.DirName()); 583 observer_->OnEntryUpdatedByOperation(*local_id); 584 } 585 callback.Run(error); 586 } 587 588 } // namespace file_system 589 } // namespace drive 590