| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/chromeos/drive/file_system/copy_operation.h" |
| |
| #include <string> |
| |
| #include "base/file_util.h" |
| #include "base/task_runner_util.h" |
| #include "chrome/browser/chromeos/drive/drive.pb.h" |
| #include "chrome/browser/chromeos/drive/file_cache.h" |
| #include "chrome/browser/chromeos/drive/file_system/create_file_operation.h" |
| #include "chrome/browser/chromeos/drive/file_system/download_operation.h" |
| #include "chrome/browser/chromeos/drive/file_system/move_operation.h" |
| #include "chrome/browser/chromeos/drive/file_system/operation_observer.h" |
| #include "chrome/browser/chromeos/drive/file_system_util.h" |
| #include "chrome/browser/chromeos/drive/job_scheduler.h" |
| #include "chrome/browser/chromeos/drive/resource_entry_conversion.h" |
| #include "chrome/browser/drive/drive_api_util.h" |
| #include "content/public/browser/browser_thread.h" |
| |
| using content::BrowserThread; |
| |
| namespace drive { |
| namespace file_system { |
| |
| namespace { |
| |
| FileError PrepareCopy(internal::ResourceMetadata* metadata, |
| const base::FilePath& src_path, |
| const base::FilePath& dest_path, |
| ResourceEntry* src_entry, |
| std::string* parent_resource_id) { |
| FileError error = metadata->GetResourceEntryByPath(src_path, src_entry); |
| if (error != FILE_ERROR_OK) |
| return error; |
| |
| ResourceEntry parent_entry; |
| error = metadata->GetResourceEntryByPath(dest_path.DirName(), &parent_entry); |
| if (error != FILE_ERROR_OK) |
| return error; |
| |
| if (!parent_entry.file_info().is_directory()) |
| return FILE_ERROR_NOT_A_DIRECTORY; |
| |
| // Drive File System doesn't support recursive copy. |
| if (src_entry->file_info().is_directory()) |
| return FILE_ERROR_NOT_A_FILE; |
| |
| *parent_resource_id = parent_entry.resource_id(); |
| return FILE_ERROR_OK; |
| } |
| |
| int64 GetFileSize(const base::FilePath& file_path) { |
| int64 file_size; |
| if (!file_util::GetFileSize(file_path, &file_size)) |
| return -1; |
| return file_size; |
| } |
| |
| // Stores the copied entry and returns its path. |
| FileError UpdateLocalStateForServerSideCopy( |
| internal::ResourceMetadata* metadata, |
| scoped_ptr<google_apis::ResourceEntry> resource_entry, |
| base::FilePath* file_path) { |
| DCHECK(resource_entry); |
| |
| ResourceEntry entry; |
| std::string parent_resource_id; |
| if (!ConvertToResourceEntry(*resource_entry, &entry, &parent_resource_id)) |
| return FILE_ERROR_NOT_A_FILE; |
| |
| std::string parent_local_id; |
| FileError error = metadata->GetIdByResourceId(parent_resource_id, |
| &parent_local_id); |
| if (error != FILE_ERROR_OK) |
| return error; |
| entry.set_parent_local_id(parent_local_id); |
| |
| std::string local_id; |
| error = metadata->AddEntry(entry, &local_id); |
| // Depending on timing, the metadata may have inserted via change list |
| // already. So, FILE_ERROR_EXISTS is not an error. |
| if (error == FILE_ERROR_EXISTS) |
| error = metadata->GetIdByResourceId(entry.resource_id(), &local_id); |
| |
| if (error == FILE_ERROR_OK) |
| *file_path = metadata->GetFilePath(local_id); |
| |
| return error; |
| } |
| |
| // Stores the file at |local_file_path| to the cache as a content of entry at |
| // |remote_dest_path|, and marks it dirty. |
| FileError UpdateLocalStateForScheduleTransfer( |
| internal::ResourceMetadata* metadata, |
| internal::FileCache* cache, |
| const base::FilePath& local_src_path, |
| const base::FilePath& remote_dest_path, |
| std::string* local_id) { |
| FileError error = metadata->GetIdByPath(remote_dest_path, local_id); |
| if (error != FILE_ERROR_OK) |
| return error; |
| |
| ResourceEntry entry; |
| error = metadata->GetResourceEntryById(*local_id, &entry); |
| if (error != FILE_ERROR_OK) |
| return error; |
| |
| error = cache->Store( |
| *local_id, entry.file_specific_info().md5(), local_src_path, |
| internal::FileCache::FILE_OPERATION_COPY); |
| if (error != FILE_ERROR_OK) |
| return error; |
| |
| error = cache->MarkDirty(*local_id); |
| if (error != FILE_ERROR_OK) |
| return error; |
| |
| return FILE_ERROR_OK; |
| } |
| |
| // Gets the file size of the |local_path|, and the ResourceEntry for the parent |
| // of |remote_path| to prepare the necessary information for transfer. |
| FileError PrepareTransferFileFromLocalToRemote( |
| internal::ResourceMetadata* metadata, |
| const base::FilePath& local_src_path, |
| const base::FilePath& remote_dest_path, |
| std::string* gdoc_resource_id, |
| std::string* parent_resource_id) { |
| ResourceEntry parent_entry; |
| FileError error = metadata->GetResourceEntryByPath( |
| remote_dest_path.DirName(), &parent_entry); |
| if (error != FILE_ERROR_OK) |
| return error; |
| |
| // The destination's parent must be a directory. |
| if (!parent_entry.file_info().is_directory()) |
| return FILE_ERROR_NOT_A_DIRECTORY; |
| |
| // Try to parse GDoc File and extract the resource id, if necessary. |
| // Failing isn't problem. It'd be handled as a regular file, then. |
| if (util::HasGDocFileExtension(local_src_path)) { |
| *gdoc_resource_id = util::ReadResourceIdFromGDocFile(local_src_path); |
| *parent_resource_id = parent_entry.resource_id(); |
| } |
| |
| return FILE_ERROR_OK; |
| } |
| |
| } // namespace |
| |
| struct CopyOperation::CopyParams { |
| base::FilePath src_file_path; |
| base::FilePath dest_file_path; |
| bool preserve_last_modified; |
| FileOperationCallback callback; |
| }; |
| |
| CopyOperation::CopyOperation(base::SequencedTaskRunner* blocking_task_runner, |
| OperationObserver* observer, |
| JobScheduler* scheduler, |
| internal::ResourceMetadata* metadata, |
| internal::FileCache* cache, |
| DriveServiceInterface* drive_service, |
| const base::FilePath& temporary_file_directory) |
| : blocking_task_runner_(blocking_task_runner), |
| observer_(observer), |
| scheduler_(scheduler), |
| metadata_(metadata), |
| cache_(cache), |
| drive_service_(drive_service), |
| create_file_operation_(new CreateFileOperation(blocking_task_runner, |
| observer, |
| scheduler, |
| metadata, |
| cache)), |
| download_operation_(new DownloadOperation(blocking_task_runner, |
| observer, |
| scheduler, |
| metadata, |
| cache, |
| temporary_file_directory)), |
| move_operation_(new MoveOperation(blocking_task_runner, |
| observer, |
| scheduler, |
| metadata)), |
| weak_ptr_factory_(this) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| } |
| |
| CopyOperation::~CopyOperation() { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| } |
| |
| void CopyOperation::Copy(const base::FilePath& src_file_path, |
| const base::FilePath& dest_file_path, |
| bool preserve_last_modified, |
| const FileOperationCallback& callback) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!callback.is_null()); |
| |
| CopyParams params; |
| params.src_file_path = src_file_path; |
| params.dest_file_path = dest_file_path; |
| params.preserve_last_modified = preserve_last_modified; |
| params.callback = callback; |
| |
| ResourceEntry* src_entry = new ResourceEntry; |
| std::string* parent_resource_id = new std::string; |
| base::PostTaskAndReplyWithResult( |
| blocking_task_runner_.get(), |
| FROM_HERE, |
| base::Bind(&PrepareCopy, |
| metadata_, src_file_path, dest_file_path, |
| src_entry, parent_resource_id), |
| base::Bind(&CopyOperation::CopyAfterPrepare, |
| weak_ptr_factory_.GetWeakPtr(), params, |
| base::Owned(src_entry), base::Owned(parent_resource_id))); |
| } |
| |
| void CopyOperation::CopyAfterPrepare( |
| const CopyParams& params, |
| ResourceEntry* src_entry, |
| std::string* parent_resource_id, |
| FileError error) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!params.callback.is_null()); |
| |
| if (error != FILE_ERROR_OK) { |
| params.callback.Run(error); |
| return; |
| } |
| |
| // If Drive API v2 is enabled, we can copy resources on server side. |
| if (util::IsDriveV2ApiEnabled()) { |
| base::FilePath new_title = params.dest_file_path.BaseName(); |
| if (src_entry->file_specific_info().is_hosted_document()) { |
| // Drop the document extension, which should not be in the title. |
| // TODO(yoshiki): Remove this code with crbug.com/223304. |
| new_title = new_title.RemoveExtension(); |
| } |
| |
| base::Time last_modified = |
| params.preserve_last_modified ? |
| base::Time::FromInternalValue(src_entry->file_info().last_modified()) : |
| base::Time(); |
| |
| CopyResourceOnServer( |
| src_entry->resource_id(), *parent_resource_id, |
| new_title.AsUTF8Unsafe(), last_modified, params.callback); |
| return; |
| } |
| |
| // Here after GData WAPI's code. |
| if (src_entry->file_specific_info().is_hosted_document()) { |
| // For hosted documents, GData WAPI copies it on server. |
| CopyHostedDocument( |
| src_entry->resource_id(), |
| // Drop the document extension, which should not be in the title. |
| // TODO(yoshiki): Remove this code with crbug.com/223304. |
| params.dest_file_path.RemoveExtension(), |
| params.callback); |
| return; |
| } |
| |
| // For regular files, download the content, and then re-upload. |
| // Note that upload is done later by SyncClient. |
| download_operation_->EnsureFileDownloadedByPath( |
| params.src_file_path, |
| ClientContext(USER_INITIATED), |
| GetFileContentInitializedCallback(), |
| google_apis::GetContentCallback(), |
| base::Bind(&CopyOperation::CopyAfterDownload, |
| weak_ptr_factory_.GetWeakPtr(), |
| params.dest_file_path, params.callback)); |
| } |
| |
| void CopyOperation::CopyAfterDownload( |
| const base::FilePath& dest_file_path, |
| const FileOperationCallback& callback, |
| FileError error, |
| const base::FilePath& local_file_path, |
| scoped_ptr<ResourceEntry> entry) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!callback.is_null()); |
| |
| if (error != FILE_ERROR_OK) { |
| callback.Run(error); |
| return; |
| } |
| |
| // This callback is only triggered for a regular file via Copy(). |
| DCHECK(entry && !entry->file_specific_info().is_hosted_document()); |
| ScheduleTransferRegularFile(local_file_path, dest_file_path, callback); |
| } |
| |
| void CopyOperation::TransferFileFromLocalToRemote( |
| const base::FilePath& local_src_path, |
| const base::FilePath& remote_dest_path, |
| const FileOperationCallback& callback) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!callback.is_null()); |
| |
| std::string* gdoc_resource_id = new std::string; |
| std::string* parent_resource_id = new std::string; |
| base::PostTaskAndReplyWithResult( |
| blocking_task_runner_.get(), |
| FROM_HERE, |
| base::Bind( |
| &PrepareTransferFileFromLocalToRemote, |
| metadata_, local_src_path, remote_dest_path, |
| gdoc_resource_id, parent_resource_id), |
| base::Bind( |
| &CopyOperation::TransferFileFromLocalToRemoteAfterPrepare, |
| weak_ptr_factory_.GetWeakPtr(), |
| local_src_path, remote_dest_path, callback, |
| base::Owned(gdoc_resource_id), base::Owned(parent_resource_id))); |
| } |
| |
| void CopyOperation::TransferFileFromLocalToRemoteAfterPrepare( |
| const base::FilePath& local_src_path, |
| const base::FilePath& remote_dest_path, |
| const FileOperationCallback& callback, |
| std::string* gdoc_resource_id, |
| std::string* parent_resource_id, |
| FileError error) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!callback.is_null()); |
| |
| if (error != FILE_ERROR_OK) { |
| callback.Run(error); |
| return; |
| } |
| |
| // For regular files, schedule the transfer. |
| if (gdoc_resource_id->empty()) { |
| ScheduleTransferRegularFile(local_src_path, remote_dest_path, callback); |
| return; |
| } |
| |
| // This is uploading a JSON file representing a hosted document. |
| // Copy the document on the Drive server. |
| |
| // GDoc file may contain a resource ID in the old format. |
| const std::string canonicalized_resource_id = |
| drive_service_->GetResourceIdCanonicalizer().Run(*gdoc_resource_id); |
| |
| // If Drive API v2 is enabled, we can copy resources on server side. |
| if (util::IsDriveV2ApiEnabled()) { |
| CopyResourceOnServer( |
| *gdoc_resource_id, *parent_resource_id, |
| // Drop the document extension, which should not be in the title. |
| // TODO(yoshiki): Remove this code with crbug.com/223304. |
| remote_dest_path.BaseName().RemoveExtension().AsUTF8Unsafe(), |
| base::Time(), |
| callback); |
| return; |
| } |
| |
| CopyHostedDocument( |
| canonicalized_resource_id, |
| // Drop the document extension, which should not be |
| // in the document title. |
| // TODO(yoshiki): Remove this code with crbug.com/223304. |
| remote_dest_path.RemoveExtension(), |
| callback); |
| } |
| |
| void CopyOperation::CopyResourceOnServer( |
| const std::string& resource_id, |
| const std::string& parent_resource_id, |
| const std::string& new_title, |
| const base::Time& last_modified, |
| const FileOperationCallback& callback) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!callback.is_null()); |
| |
| scheduler_->CopyResource( |
| resource_id, parent_resource_id, new_title, last_modified, |
| base::Bind(&CopyOperation::CopyResourceOnServerAfterServerSideCopy, |
| weak_ptr_factory_.GetWeakPtr(), |
| callback)); |
| } |
| |
| void CopyOperation::CopyResourceOnServerAfterServerSideCopy( |
| const FileOperationCallback& callback, |
| google_apis::GDataErrorCode status, |
| scoped_ptr<google_apis::ResourceEntry> resource_entry) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!callback.is_null()); |
| |
| FileError error = GDataToFileError(status); |
| if (error != FILE_ERROR_OK) { |
| callback.Run(error); |
| return; |
| } |
| |
| // The copy on the server side is completed successfully. Update the local |
| // metadata. |
| base::FilePath* file_path = new base::FilePath; |
| base::PostTaskAndReplyWithResult( |
| blocking_task_runner_.get(), |
| FROM_HERE, |
| base::Bind(&UpdateLocalStateForServerSideCopy, |
| metadata_, base::Passed(&resource_entry), file_path), |
| base::Bind(&CopyOperation::CopyResourceOnServerAfterUpdateLocalState, |
| weak_ptr_factory_.GetWeakPtr(), |
| callback, base::Owned(file_path))); |
| } |
| |
| void CopyOperation::CopyResourceOnServerAfterUpdateLocalState( |
| const FileOperationCallback& callback, |
| base::FilePath* file_path, |
| FileError error) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!callback.is_null()); |
| |
| if (error == FILE_ERROR_OK) |
| observer_->OnDirectoryChangedByOperation(file_path->DirName()); |
| callback.Run(error); |
| } |
| |
| void CopyOperation::ScheduleTransferRegularFile( |
| const base::FilePath& local_src_path, |
| const base::FilePath& remote_dest_path, |
| const FileOperationCallback& callback) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!callback.is_null()); |
| |
| base::PostTaskAndReplyWithResult( |
| blocking_task_runner_.get(), |
| FROM_HERE, |
| base::Bind(&GetFileSize, local_src_path), |
| base::Bind(&CopyOperation::ScheduleTransferRegularFileAfterGetFileSize, |
| weak_ptr_factory_.GetWeakPtr(), |
| local_src_path, remote_dest_path, callback)); |
| } |
| |
| void CopyOperation::ScheduleTransferRegularFileAfterGetFileSize( |
| const base::FilePath& local_src_path, |
| const base::FilePath& remote_dest_path, |
| const FileOperationCallback& callback, |
| int64 local_file_size) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!callback.is_null()); |
| |
| if (local_file_size < 0) { |
| callback.Run(FILE_ERROR_NOT_FOUND); |
| return; |
| } |
| |
| // For regular files, check the server-side quota whether sufficient space |
| // is available for the file to be uploaded. |
| scheduler_->GetAboutResource( |
| base::Bind( |
| &CopyOperation::ScheduleTransferRegularFileAfterGetAboutResource, |
| weak_ptr_factory_.GetWeakPtr(), |
| local_src_path, remote_dest_path, callback, local_file_size)); |
| } |
| |
| void CopyOperation::ScheduleTransferRegularFileAfterGetAboutResource( |
| const base::FilePath& local_src_path, |
| const base::FilePath& remote_dest_path, |
| const FileOperationCallback& callback, |
| int64 local_file_size, |
| google_apis::GDataErrorCode status, |
| scoped_ptr<google_apis::AboutResource> about_resource) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!callback.is_null()); |
| |
| FileError error = GDataToFileError(status); |
| if (error != FILE_ERROR_OK) { |
| callback.Run(error); |
| return; |
| } |
| |
| DCHECK(about_resource); |
| const int64 space = |
| about_resource->quota_bytes_total() - about_resource->quota_bytes_used(); |
| if (space < local_file_size) { |
| callback.Run(FILE_ERROR_NO_SERVER_SPACE); |
| return; |
| } |
| |
| create_file_operation_->CreateFile( |
| remote_dest_path, |
| true, // Exclusive (i.e. fail if a file already exists). |
| std::string(), // no specific mime type; CreateFile should guess it. |
| base::Bind(&CopyOperation::ScheduleTransferRegularFileAfterCreate, |
| weak_ptr_factory_.GetWeakPtr(), |
| local_src_path, remote_dest_path, callback)); |
| } |
| |
| void CopyOperation::ScheduleTransferRegularFileAfterCreate( |
| const base::FilePath& local_src_path, |
| const base::FilePath& remote_dest_path, |
| const FileOperationCallback& callback, |
| FileError error) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!callback.is_null()); |
| |
| if (error != FILE_ERROR_OK) { |
| callback.Run(error); |
| return; |
| } |
| |
| std::string* local_id = new std::string; |
| base::PostTaskAndReplyWithResult( |
| blocking_task_runner_.get(), |
| FROM_HERE, |
| base::Bind( |
| &UpdateLocalStateForScheduleTransfer, |
| metadata_, cache_, local_src_path, remote_dest_path, local_id), |
| base::Bind( |
| &CopyOperation::ScheduleTransferRegularFileAfterUpdateLocalState, |
| weak_ptr_factory_.GetWeakPtr(), callback, base::Owned(local_id))); |
| } |
| |
| void CopyOperation::ScheduleTransferRegularFileAfterUpdateLocalState( |
| const FileOperationCallback& callback, |
| std::string* local_id, |
| FileError error) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!callback.is_null()); |
| |
| if (error == FILE_ERROR_OK) |
| observer_->OnCacheFileUploadNeededByOperation(*local_id); |
| callback.Run(error); |
| } |
| |
| void CopyOperation::CopyHostedDocument( |
| const std::string& resource_id, |
| const base::FilePath& dest_path, |
| const FileOperationCallback& callback) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!callback.is_null()); |
| |
| scheduler_->CopyHostedDocument( |
| resource_id, |
| dest_path.BaseName().AsUTF8Unsafe(), |
| base::Bind(&CopyOperation::CopyHostedDocumentAfterServerSideCopy, |
| weak_ptr_factory_.GetWeakPtr(), dest_path, callback)); |
| } |
| |
| void CopyOperation::CopyHostedDocumentAfterServerSideCopy( |
| const base::FilePath& dest_path, |
| const FileOperationCallback& callback, |
| google_apis::GDataErrorCode status, |
| scoped_ptr<google_apis::ResourceEntry> resource_entry) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!callback.is_null()); |
| |
| FileError error = GDataToFileError(status); |
| if (error != FILE_ERROR_OK) { |
| callback.Run(error); |
| return; |
| } |
| |
| // The document is copied into the root directory (temporarily) on the server, |
| // so we should first update the local metadata, then move it to the |
| // destination directory. |
| base::FilePath* file_path = new base::FilePath; |
| base::PostTaskAndReplyWithResult( |
| blocking_task_runner_.get(), |
| FROM_HERE, |
| base::Bind(&UpdateLocalStateForServerSideCopy, |
| metadata_, base::Passed(&resource_entry), file_path), |
| base::Bind(&CopyOperation::CopyHostedDocumentAfterUpdateLocalState, |
| weak_ptr_factory_.GetWeakPtr(), |
| dest_path, callback, base::Owned(file_path))); |
| } |
| |
| void CopyOperation::CopyHostedDocumentAfterUpdateLocalState( |
| const base::FilePath& dest_path, |
| const FileOperationCallback& callback, |
| base::FilePath* file_path, |
| FileError error) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!callback.is_null()); |
| |
| // If an error is found, or the destination directory is mydrive root |
| // (i.e. it is unnecessary to move the copied document), return the status. |
| if (error != FILE_ERROR_OK || |
| dest_path.DirName() == util::GetDriveMyDriveRootPath()) { |
| callback.Run(error); |
| return; |
| } |
| |
| DCHECK_EQ(util::GetDriveMyDriveRootPath().value(), |
| file_path->DirName().value()) << file_path->value(); |
| |
| move_operation_->Move(*file_path, dest_path, false, callback); |
| } |
| |
| } // namespace file_system |
| } // namespace drive |