blob: 595e8e1b1f6a238c0d202514b5a8f1af4abc1dc8 [file] [log] [blame]
// 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 "components/drive/file_system/copy_operation.h"
#include <string>
#include "base/task_runner_util.h"
#include "components/drive/drive.pb.h"
#include "components/drive/drive_api_util.h"
#include "components/drive/file_cache.h"
#include "components/drive/file_change.h"
#include "components/drive/file_system/create_file_operation.h"
#include "components/drive/file_system/operation_delegate.h"
#include "components/drive/file_system_core_util.h"
#include "components/drive/job_scheduler.h"
#include "components/drive/resource_entry_conversion.h"
#include "components/drive/resource_metadata.h"
#include "google_apis/drive/drive_api_parser.h"
namespace drive {
namespace file_system {
struct CopyOperation::CopyParams {
base::FilePath src_file_path;
base::FilePath dest_file_path;
bool preserve_last_modified;
FileOperationCallback callback;
ResourceEntry src_entry;
ResourceEntry parent_entry;
};
// Enum for categorizing where a gdoc represented by a JSON file exists.
enum JsonGdocLocationType {
NOT_IN_METADATA,
IS_ORPHAN,
HAS_PARENT,
};
struct CopyOperation::TransferJsonGdocParams {
TransferJsonGdocParams(const FileOperationCallback& callback,
const std::string& resource_id,
const ResourceEntry& parent_entry,
const std::string& new_title)
: callback(callback),
resource_id(resource_id),
parent_resource_id(parent_entry.resource_id()),
parent_local_id(parent_entry.local_id()),
new_title(new_title),
location_type(NOT_IN_METADATA) {
}
// Parameters supplied or calculated from operation arguments.
const FileOperationCallback callback;
const std::string resource_id;
const std::string parent_resource_id;
const std::string parent_local_id;
const std::string new_title;
// Values computed during operation.
JsonGdocLocationType location_type; // types where the gdoc file is located.
std::string local_id; // the local_id of the file (if exists in metadata.)
base::FilePath changed_path;
};
namespace {
FileError TryToCopyLocally(internal::ResourceMetadata* metadata,
internal::FileCache* cache,
CopyOperation::CopyParams* params,
std::vector<std::string>* updated_local_ids,
bool* directory_changed,
bool* should_copy_on_server) {
FileError error = metadata->GetResourceEntryByPath(params->src_file_path,
&params->src_entry);
if (error != FILE_ERROR_OK)
return error;
error = metadata->GetResourceEntryByPath(params->dest_file_path.DirName(),
&params->parent_entry);
if (error != FILE_ERROR_OK)
return error;
if (!params->parent_entry.file_info().is_directory())
return FILE_ERROR_NOT_A_DIRECTORY;
// Drive File System doesn't support recursive copy.
if (params->src_entry.file_info().is_directory())
return FILE_ERROR_NOT_A_FILE;
// Check destination.
ResourceEntry dest_entry;
error = metadata->GetResourceEntryByPath(params->dest_file_path, &dest_entry);
switch (error) {
case FILE_ERROR_OK:
// File API spec says it is an error to try to "copy a file to a path
// occupied by a directory".
if (dest_entry.file_info().is_directory())
return FILE_ERROR_INVALID_OPERATION;
// Move the existing entry to the trash.
dest_entry.set_parent_local_id(util::kDriveTrashDirLocalId);
error = metadata->RefreshEntry(dest_entry);
if (error != FILE_ERROR_OK)
return error;
updated_local_ids->push_back(dest_entry.local_id());
*directory_changed = true;
break;
case FILE_ERROR_NOT_FOUND:
break;
default:
return error;
}
// If the cache file is not present and the entry exists on the server,
// server side copy should be used.
if (!params->src_entry.file_specific_info().cache_state().is_present() &&
!params->src_entry.resource_id().empty()) {
*should_copy_on_server = true;
return FILE_ERROR_OK;
}
// Copy locally.
ResourceEntry entry;
const int64 now = base::Time::Now().ToInternalValue();
entry.set_title(params->dest_file_path.BaseName().AsUTF8Unsafe());
entry.set_parent_local_id(params->parent_entry.local_id());
entry.mutable_file_specific_info()->set_content_mime_type(
params->src_entry.file_specific_info().content_mime_type());
entry.set_metadata_edit_state(ResourceEntry::DIRTY);
entry.set_modification_date(base::Time::Now().ToInternalValue());
entry.mutable_file_info()->set_last_modified(
params->preserve_last_modified ?
params->src_entry.file_info().last_modified() : now);
entry.mutable_file_info()->set_last_accessed(now);
std::string local_id;
error = metadata->AddEntry(entry, &local_id);
if (error != FILE_ERROR_OK)
return error;
updated_local_ids->push_back(local_id);
*directory_changed = true;
if (!params->src_entry.file_specific_info().cache_state().is_present()) {
DCHECK(params->src_entry.resource_id().empty());
// Locally created empty file may have no cache file.
return FILE_ERROR_OK;
}
base::FilePath cache_file_path;
error = cache->GetFile(params->src_entry.local_id(), &cache_file_path);
if (error != FILE_ERROR_OK)
return error;
return cache->Store(local_id, std::string(), cache_file_path,
internal::FileCache::FILE_OPERATION_COPY);
}
// Stores the entry returned from the server and returns its path.
FileError UpdateLocalStateForServerSideOperation(
internal::ResourceMetadata* metadata,
scoped_ptr<google_apis::FileResource> file_resource,
ResourceEntry* entry,
base::FilePath* file_path) {
DCHECK(file_resource);
std::string parent_resource_id;
if (!ConvertFileResourceToResourceEntry(
*file_resource, entry, &parent_resource_id) ||
parent_resource_id.empty())
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)
return error;
return metadata->GetFilePath(local_id, file_path);
}
// 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,
ResourceEntry* entry,
std::string* local_id) {
FileError error = metadata->GetIdByPath(remote_dest_path, local_id);
if (error != FILE_ERROR_OK)
return error;
error = metadata->GetResourceEntryById(*local_id, entry);
if (error != FILE_ERROR_OK)
return error;
return cache->Store(*local_id, std::string(), local_src_path,
internal::FileCache::FILE_OPERATION_COPY);
}
// 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,
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::HasHostedDocumentExtension(local_src_path))
*gdoc_resource_id = util::ReadResourceIdFromGDocFile(local_src_path);
return FILE_ERROR_OK;
}
// Performs local work before server-side work for transferring JSON-represented
// gdoc files.
FileError LocalWorkForTransferJsonGdocFile(
internal::ResourceMetadata* metadata,
CopyOperation::TransferJsonGdocParams* params) {
std::string local_id;
FileError error = metadata->GetIdByResourceId(params->resource_id, &local_id);
if (error != FILE_ERROR_OK) {
params->location_type = NOT_IN_METADATA;
return error == FILE_ERROR_NOT_FOUND ? FILE_ERROR_OK : error;
}
ResourceEntry entry;
error = metadata->GetResourceEntryById(local_id, &entry);
if (error != FILE_ERROR_OK)
return error;
params->local_id = entry.local_id();
if (entry.parent_local_id() == util::kDriveOtherDirLocalId) {
params->location_type = IS_ORPHAN;
entry.set_title(params->new_title);
entry.set_parent_local_id(params->parent_local_id);
entry.set_metadata_edit_state(ResourceEntry::DIRTY);
entry.set_modification_date(base::Time::Now().ToInternalValue());
error = metadata->RefreshEntry(entry);
if (error != FILE_ERROR_OK)
return error;
return metadata->GetFilePath(local_id, &params->changed_path);
}
params->location_type = HAS_PARENT;
return FILE_ERROR_OK;
}
} // namespace
CopyOperation::CopyOperation(base::SequencedTaskRunner* blocking_task_runner,
OperationDelegate* delegate,
JobScheduler* scheduler,
internal::ResourceMetadata* metadata,
internal::FileCache* cache)
: blocking_task_runner_(blocking_task_runner),
delegate_(delegate),
scheduler_(scheduler),
metadata_(metadata),
cache_(cache),
create_file_operation_(new CreateFileOperation(blocking_task_runner,
delegate,
metadata)),
weak_ptr_factory_(this) {
}
CopyOperation::~CopyOperation() {
DCHECK(thread_checker_.CalledOnValidThread());
}
void CopyOperation::Copy(const base::FilePath& src_file_path,
const base::FilePath& dest_file_path,
bool preserve_last_modified,
const FileOperationCallback& callback) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!callback.is_null());
CopyParams* params = new CopyParams;
params->src_file_path = src_file_path;
params->dest_file_path = dest_file_path;
params->preserve_last_modified = preserve_last_modified;
params->callback = callback;
std::vector<std::string>* updated_local_ids = new std::vector<std::string>;
bool* directory_changed = new bool(false);
bool* should_copy_on_server = new bool(false);
base::PostTaskAndReplyWithResult(
blocking_task_runner_.get(),
FROM_HERE,
base::Bind(&TryToCopyLocally, metadata_, cache_, params,
updated_local_ids, directory_changed, should_copy_on_server),
base::Bind(&CopyOperation::CopyAfterTryToCopyLocally,
weak_ptr_factory_.GetWeakPtr(), base::Owned(params),
base::Owned(updated_local_ids), base::Owned(directory_changed),
base::Owned(should_copy_on_server)));
}
void CopyOperation::CopyAfterTryToCopyLocally(
const CopyParams* params,
const std::vector<std::string>* updated_local_ids,
const bool* directory_changed,
const bool* should_copy_on_server,
FileError error) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!params->callback.is_null());
for (const auto& id : *updated_local_ids) {
// Syncing for copy should be done in background, so pass the BACKGROUND
// context. See: crbug.com/420278.
delegate_->OnEntryUpdatedByOperation(ClientContext(BACKGROUND), id);
}
if (*directory_changed) {
FileChange changed_file;
DCHECK(!params->src_entry.file_info().is_directory());
changed_file.Update(params->dest_file_path, FileChange::FILE_TYPE_FILE,
FileChange::CHANGE_TYPE_ADD_OR_UPDATE);
delegate_->OnFileChangedByOperation(changed_file);
}
if (error != FILE_ERROR_OK || !*should_copy_on_server) {
params->callback.Run(error);
return;
}
if (params->parent_entry.resource_id().empty()) {
// Parent entry may be being synced.
const bool waiting = delegate_->WaitForSyncComplete(
params->parent_entry.local_id(),
base::Bind(&CopyOperation::CopyAfterParentSync,
weak_ptr_factory_.GetWeakPtr(), *params));
if (!waiting)
params->callback.Run(FILE_ERROR_NOT_FOUND);
} else {
CopyAfterGetParentResourceId(*params, &params->parent_entry, FILE_ERROR_OK);
}
}
void CopyOperation::CopyAfterParentSync(const CopyParams& params,
FileError error) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!params.callback.is_null());
if (error != FILE_ERROR_OK) {
params.callback.Run(error);
return;
}
ResourceEntry* parent = new ResourceEntry;
base::PostTaskAndReplyWithResult(
blocking_task_runner_.get(),
FROM_HERE,
base::Bind(&internal::ResourceMetadata::GetResourceEntryById,
base::Unretained(metadata_),
params.parent_entry.local_id(),
parent),
base::Bind(&CopyOperation::CopyAfterGetParentResourceId,
weak_ptr_factory_.GetWeakPtr(),
params,
base::Owned(parent)));
}
void CopyOperation::CopyAfterGetParentResourceId(const CopyParams& params,
const ResourceEntry* parent,
FileError error) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!params.callback.is_null());
if (error != FILE_ERROR_OK) {
params.callback.Run(error);
return;
}
base::FilePath new_title = params.dest_file_path.BaseName();
if (params.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(
params.src_entry.file_info().last_modified()) : base::Time();
CopyResourceOnServer(
params.src_entry.resource_id(), parent->resource_id(),
new_title.AsUTF8Unsafe(), last_modified, params.callback);
}
void CopyOperation::TransferFileFromLocalToRemote(
const base::FilePath& local_src_path,
const base::FilePath& remote_dest_path,
const FileOperationCallback& callback) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!callback.is_null());
std::string* gdoc_resource_id = new std::string;
ResourceEntry* parent_entry = new ResourceEntry;
base::PostTaskAndReplyWithResult(
blocking_task_runner_.get(),
FROM_HERE,
base::Bind(
&PrepareTransferFileFromLocalToRemote,
metadata_, local_src_path, remote_dest_path,
gdoc_resource_id, parent_entry),
base::Bind(
&CopyOperation::TransferFileFromLocalToRemoteAfterPrepare,
weak_ptr_factory_.GetWeakPtr(),
local_src_path, remote_dest_path, callback,
base::Owned(gdoc_resource_id), base::Owned(parent_entry)));
}
void CopyOperation::TransferFileFromLocalToRemoteAfterPrepare(
const base::FilePath& local_src_path,
const base::FilePath& remote_dest_path,
const FileOperationCallback& callback,
std::string* gdoc_resource_id,
ResourceEntry* parent_entry,
FileError error) {
DCHECK(thread_checker_.CalledOnValidThread());
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;
}
// GDoc file may contain a resource ID in the old format.
const std::string canonicalized_resource_id =
util::CanonicalizeResourceId(*gdoc_resource_id);
// Drop the document extension, which should not be in the title.
// TODO(yoshiki): Remove this code with crbug.com/223304.
const std::string new_title =
remote_dest_path.BaseName().RemoveExtension().AsUTF8Unsafe();
// This is uploading a JSON file representing a hosted document.
TransferJsonGdocParams* params = new TransferJsonGdocParams(
callback, canonicalized_resource_id, *parent_entry, new_title);
base::PostTaskAndReplyWithResult(
blocking_task_runner_.get(),
FROM_HERE,
base::Bind(&LocalWorkForTransferJsonGdocFile, metadata_, params),
base::Bind(&CopyOperation::TransferJsonGdocFileAfterLocalWork,
weak_ptr_factory_.GetWeakPtr(), base::Owned(params)));
}
void CopyOperation::TransferJsonGdocFileAfterLocalWork(
TransferJsonGdocParams* params,
FileError error) {
DCHECK(thread_checker_.CalledOnValidThread());
if (error != FILE_ERROR_OK) {
params->callback.Run(error);
return;
}
switch (params->location_type) {
// When |resource_id| is found in the local metadata and it has a specific
// parent folder, we assume the user's intention is to copy the document and
// thus perform the server-side copy operation.
case HAS_PARENT:
CopyResourceOnServer(params->resource_id,
params->parent_resource_id,
params->new_title,
base::Time(),
params->callback);
break;
// When |resource_id| has no parent, we just set the new destination folder
// as the parent, for sharing the document between the original source.
// This reparenting is already done in LocalWorkForTransferJsonGdocFile().
case IS_ORPHAN: {
DCHECK(!params->changed_path.empty());
// Syncing for copy should be done in background, so pass the BACKGROUND
// context. See: crbug.com/420278.
delegate_->OnEntryUpdatedByOperation(ClientContext(BACKGROUND),
params->local_id);
FileChange changed_file;
changed_file.Update(
params->changed_path,
FileChange::FILE_TYPE_FILE, // This must be a hosted document.
FileChange::CHANGE_TYPE_ADD_OR_UPDATE);
delegate_->OnFileChangedByOperation(changed_file);
params->callback.Run(error);
break;
}
// When the |resource_id| is not in the local metadata, assume it to be a
// document just now shared on the server but not synced locally.
// Same as the IS_ORPHAN case, we want to deal the case by setting parent,
// but this time we need to resort to server side operation.
case NOT_IN_METADATA:
scheduler_->UpdateResource(
params->resource_id, params->parent_resource_id, params->new_title,
base::Time(), base::Time(), google_apis::drive::Properties(),
ClientContext(USER_INITIATED),
base::Bind(&CopyOperation::UpdateAfterServerSideOperation,
weak_ptr_factory_.GetWeakPtr(), params->callback));
break;
}
}
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(thread_checker_.CalledOnValidThread());
DCHECK(!callback.is_null());
scheduler_->CopyResource(
resource_id, parent_resource_id, new_title, last_modified,
base::Bind(&CopyOperation::UpdateAfterServerSideOperation,
weak_ptr_factory_.GetWeakPtr(),
callback));
}
void CopyOperation::UpdateAfterServerSideOperation(
const FileOperationCallback& callback,
google_apis::DriveApiErrorCode status,
scoped_ptr<google_apis::FileResource> entry) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!callback.is_null());
FileError error = GDataToFileError(status);
if (error != FILE_ERROR_OK) {
callback.Run(error);
return;
}
ResourceEntry* resource_entry = new ResourceEntry;
// 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(&UpdateLocalStateForServerSideOperation,
metadata_,
base::Passed(&entry),
resource_entry,
file_path),
base::Bind(&CopyOperation::UpdateAfterLocalStateUpdate,
weak_ptr_factory_.GetWeakPtr(),
callback,
base::Owned(file_path),
base::Owned(resource_entry)));
}
void CopyOperation::UpdateAfterLocalStateUpdate(
const FileOperationCallback& callback,
base::FilePath* file_path,
const ResourceEntry* entry,
FileError error) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!callback.is_null());
if (error == FILE_ERROR_OK) {
FileChange changed_file;
changed_file.Update(*file_path, *entry,
FileChange::CHANGE_TYPE_ADD_OR_UPDATE);
delegate_->OnFileChangedByOperation(changed_file);
}
callback.Run(error);
}
void CopyOperation::ScheduleTransferRegularFile(
const base::FilePath& local_src_path,
const base::FilePath& remote_dest_path,
const FileOperationCallback& callback) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!callback.is_null());
create_file_operation_->CreateFile(
remote_dest_path,
false, // Not exclusive (OK even 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(thread_checker_.CalledOnValidThread());
DCHECK(!callback.is_null());
if (error != FILE_ERROR_OK) {
callback.Run(error);
return;
}
std::string* local_id = new std::string;
ResourceEntry* entry = new ResourceEntry;
base::PostTaskAndReplyWithResult(
blocking_task_runner_.get(),
FROM_HERE,
base::Bind(&UpdateLocalStateForScheduleTransfer,
metadata_,
cache_,
local_src_path,
remote_dest_path,
entry,
local_id),
base::Bind(
&CopyOperation::ScheduleTransferRegularFileAfterUpdateLocalState,
weak_ptr_factory_.GetWeakPtr(),
callback,
remote_dest_path,
base::Owned(entry),
base::Owned(local_id)));
}
void CopyOperation::ScheduleTransferRegularFileAfterUpdateLocalState(
const FileOperationCallback& callback,
const base::FilePath& remote_dest_path,
const ResourceEntry* entry,
std::string* local_id,
FileError error) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!callback.is_null());
if (error == FILE_ERROR_OK) {
FileChange changed_file;
changed_file.Update(remote_dest_path, *entry,
FileChange::CHANGE_TYPE_ADD_OR_UPDATE);
delegate_->OnFileChangedByOperation(changed_file);
// Syncing for copy should be done in background, so pass the BACKGROUND
// context. See: crbug.com/420278.
delegate_->OnEntryUpdatedByOperation(ClientContext(BACKGROUND), *local_id);
}
callback.Run(error);
}
} // namespace file_system
} // namespace drive