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.
5 #include "components/drive/file_system/copy_operation.h"
9 #include "base/task_runner_util.h"
10 #include "components/drive/drive.pb.h"
11 #include "components/drive/drive_api_util.h"
12 #include "components/drive/file_cache.h"
13 #include "components/drive/file_change.h"
14 #include "components/drive/file_system/create_file_operation.h"
15 #include "components/drive/file_system/operation_delegate.h"
16 #include "components/drive/file_system_core_util.h"
17 #include "components/drive/job_scheduler.h"
18 #include "components/drive/resource_entry_conversion.h"
19 #include "components/drive/resource_metadata.h"
20 #include "google_apis/drive/drive_api_parser.h"
23 namespace file_system
{
25 struct CopyOperation::CopyParams
{
26 base::FilePath src_file_path
;
27 base::FilePath dest_file_path
;
28 bool preserve_last_modified
;
29 FileOperationCallback callback
;
30 ResourceEntry src_entry
;
31 ResourceEntry parent_entry
;
34 // Enum for categorizing where a gdoc represented by a JSON file exists.
35 enum JsonGdocLocationType
{
41 struct CopyOperation::TransferJsonGdocParams
{
42 TransferJsonGdocParams(const FileOperationCallback
& callback
,
43 const std::string
& resource_id
,
44 const ResourceEntry
& parent_entry
,
45 const std::string
& new_title
)
47 resource_id(resource_id
),
48 parent_resource_id(parent_entry
.resource_id()),
49 parent_local_id(parent_entry
.local_id()),
51 location_type(NOT_IN_METADATA
) {
53 // Parameters supplied or calculated from operation arguments.
54 const FileOperationCallback callback
;
55 const std::string resource_id
;
56 const std::string parent_resource_id
;
57 const std::string parent_local_id
;
58 const std::string new_title
;
60 // Values computed during operation.
61 JsonGdocLocationType location_type
; // types where the gdoc file is located.
62 std::string local_id
; // the local_id of the file (if exists in metadata.)
63 base::FilePath changed_path
;
68 FileError
TryToCopyLocally(internal::ResourceMetadata
* metadata
,
69 internal::FileCache
* cache
,
70 CopyOperation::CopyParams
* params
,
71 std::vector
<std::string
>* updated_local_ids
,
72 bool* directory_changed
,
73 bool* should_copy_on_server
) {
74 FileError error
= metadata
->GetResourceEntryByPath(params
->src_file_path
,
76 if (error
!= FILE_ERROR_OK
)
79 error
= metadata
->GetResourceEntryByPath(params
->dest_file_path
.DirName(),
80 ¶ms
->parent_entry
);
81 if (error
!= FILE_ERROR_OK
)
84 if (!params
->parent_entry
.file_info().is_directory())
85 return FILE_ERROR_NOT_A_DIRECTORY
;
87 // Drive File System doesn't support recursive copy.
88 if (params
->src_entry
.file_info().is_directory())
89 return FILE_ERROR_NOT_A_FILE
;
92 ResourceEntry dest_entry
;
93 error
= metadata
->GetResourceEntryByPath(params
->dest_file_path
, &dest_entry
);
96 // File API spec says it is an error to try to "copy a file to a path
97 // occupied by a directory".
98 if (dest_entry
.file_info().is_directory())
99 return FILE_ERROR_INVALID_OPERATION
;
101 // Move the existing entry to the trash.
102 dest_entry
.set_parent_local_id(util::kDriveTrashDirLocalId
);
103 error
= metadata
->RefreshEntry(dest_entry
);
104 if (error
!= FILE_ERROR_OK
)
106 updated_local_ids
->push_back(dest_entry
.local_id());
107 *directory_changed
= true;
109 case FILE_ERROR_NOT_FOUND
:
115 // If the cache file is not present and the entry exists on the server,
116 // server side copy should be used.
117 if (!params
->src_entry
.file_specific_info().cache_state().is_present() &&
118 !params
->src_entry
.resource_id().empty()) {
119 *should_copy_on_server
= true;
120 return FILE_ERROR_OK
;
125 const int64 now
= base::Time::Now().ToInternalValue();
126 entry
.set_title(params
->dest_file_path
.BaseName().AsUTF8Unsafe());
127 entry
.set_parent_local_id(params
->parent_entry
.local_id());
128 entry
.mutable_file_specific_info()->set_content_mime_type(
129 params
->src_entry
.file_specific_info().content_mime_type());
130 entry
.set_metadata_edit_state(ResourceEntry::DIRTY
);
131 entry
.set_modification_date(base::Time::Now().ToInternalValue());
132 entry
.mutable_file_info()->set_last_modified(
133 params
->preserve_last_modified
?
134 params
->src_entry
.file_info().last_modified() : now
);
135 entry
.mutable_file_info()->set_last_accessed(now
);
137 std::string local_id
;
138 error
= metadata
->AddEntry(entry
, &local_id
);
139 if (error
!= FILE_ERROR_OK
)
141 updated_local_ids
->push_back(local_id
);
142 *directory_changed
= true;
144 if (!params
->src_entry
.file_specific_info().cache_state().is_present()) {
145 DCHECK(params
->src_entry
.resource_id().empty());
146 // Locally created empty file may have no cache file.
147 return FILE_ERROR_OK
;
150 base::FilePath cache_file_path
;
151 error
= cache
->GetFile(params
->src_entry
.local_id(), &cache_file_path
);
152 if (error
!= FILE_ERROR_OK
)
155 return cache
->Store(local_id
, std::string(), cache_file_path
,
156 internal::FileCache::FILE_OPERATION_COPY
);
159 // Stores the entry returned from the server and returns its path.
160 FileError
UpdateLocalStateForServerSideOperation(
161 internal::ResourceMetadata
* metadata
,
162 scoped_ptr
<google_apis::FileResource
> file_resource
,
163 ResourceEntry
* entry
,
164 base::FilePath
* file_path
) {
165 DCHECK(file_resource
);
167 std::string parent_resource_id
;
168 if (!ConvertFileResourceToResourceEntry(
169 *file_resource
, entry
, &parent_resource_id
) ||
170 parent_resource_id
.empty())
171 return FILE_ERROR_NOT_A_FILE
;
173 std::string parent_local_id
;
174 FileError error
= metadata
->GetIdByResourceId(parent_resource_id
,
176 if (error
!= FILE_ERROR_OK
)
178 entry
->set_parent_local_id(parent_local_id
);
180 std::string local_id
;
181 error
= metadata
->AddEntry(*entry
, &local_id
);
182 // Depending on timing, the metadata may have inserted via change list
183 // already. So, FILE_ERROR_EXISTS is not an error.
184 if (error
== FILE_ERROR_EXISTS
)
185 error
= metadata
->GetIdByResourceId(entry
->resource_id(), &local_id
);
187 if (error
!= FILE_ERROR_OK
)
190 return metadata
->GetFilePath(local_id
, file_path
);
193 // Stores the file at |local_file_path| to the cache as a content of entry at
194 // |remote_dest_path|, and marks it dirty.
195 FileError
UpdateLocalStateForScheduleTransfer(
196 internal::ResourceMetadata
* metadata
,
197 internal::FileCache
* cache
,
198 const base::FilePath
& local_src_path
,
199 const base::FilePath
& remote_dest_path
,
200 ResourceEntry
* entry
,
201 std::string
* local_id
) {
202 FileError error
= metadata
->GetIdByPath(remote_dest_path
, local_id
);
203 if (error
!= FILE_ERROR_OK
)
206 error
= metadata
->GetResourceEntryById(*local_id
, entry
);
207 if (error
!= FILE_ERROR_OK
)
210 return cache
->Store(*local_id
, std::string(), local_src_path
,
211 internal::FileCache::FILE_OPERATION_COPY
);
214 // Gets the file size of the |local_path|, and the ResourceEntry for the parent
215 // of |remote_path| to prepare the necessary information for transfer.
216 FileError
PrepareTransferFileFromLocalToRemote(
217 internal::ResourceMetadata
* metadata
,
218 const base::FilePath
& local_src_path
,
219 const base::FilePath
& remote_dest_path
,
220 std::string
* gdoc_resource_id
,
221 ResourceEntry
* parent_entry
) {
222 FileError error
= metadata
->GetResourceEntryByPath(
223 remote_dest_path
.DirName(), parent_entry
);
224 if (error
!= FILE_ERROR_OK
)
227 // The destination's parent must be a directory.
228 if (!parent_entry
->file_info().is_directory())
229 return FILE_ERROR_NOT_A_DIRECTORY
;
231 // Try to parse GDoc File and extract the resource id, if necessary.
232 // Failing isn't problem. It'd be handled as a regular file, then.
233 if (util::HasHostedDocumentExtension(local_src_path
))
234 *gdoc_resource_id
= util::ReadResourceIdFromGDocFile(local_src_path
);
235 return FILE_ERROR_OK
;
238 // Performs local work before server-side work for transferring JSON-represented
240 FileError
LocalWorkForTransferJsonGdocFile(
241 internal::ResourceMetadata
* metadata
,
242 CopyOperation::TransferJsonGdocParams
* params
) {
243 std::string local_id
;
244 FileError error
= metadata
->GetIdByResourceId(params
->resource_id
, &local_id
);
245 if (error
!= FILE_ERROR_OK
) {
246 params
->location_type
= NOT_IN_METADATA
;
247 return error
== FILE_ERROR_NOT_FOUND
? FILE_ERROR_OK
: error
;
251 error
= metadata
->GetResourceEntryById(local_id
, &entry
);
252 if (error
!= FILE_ERROR_OK
)
254 params
->local_id
= entry
.local_id();
256 if (entry
.parent_local_id() == util::kDriveOtherDirLocalId
) {
257 params
->location_type
= IS_ORPHAN
;
258 entry
.set_title(params
->new_title
);
259 entry
.set_parent_local_id(params
->parent_local_id
);
260 entry
.set_metadata_edit_state(ResourceEntry::DIRTY
);
261 entry
.set_modification_date(base::Time::Now().ToInternalValue());
262 error
= metadata
->RefreshEntry(entry
);
263 if (error
!= FILE_ERROR_OK
)
265 return metadata
->GetFilePath(local_id
, ¶ms
->changed_path
);
268 params
->location_type
= HAS_PARENT
;
269 return FILE_ERROR_OK
;
274 CopyOperation::CopyOperation(base::SequencedTaskRunner
* blocking_task_runner
,
275 OperationDelegate
* delegate
,
276 JobScheduler
* scheduler
,
277 internal::ResourceMetadata
* metadata
,
278 internal::FileCache
* cache
)
279 : blocking_task_runner_(blocking_task_runner
),
281 scheduler_(scheduler
),
284 create_file_operation_(new CreateFileOperation(blocking_task_runner
,
287 weak_ptr_factory_(this) {
290 CopyOperation::~CopyOperation() {
291 DCHECK(thread_checker_
.CalledOnValidThread());
294 void CopyOperation::Copy(const base::FilePath
& src_file_path
,
295 const base::FilePath
& dest_file_path
,
296 bool preserve_last_modified
,
297 const FileOperationCallback
& callback
) {
298 DCHECK(thread_checker_
.CalledOnValidThread());
299 DCHECK(!callback
.is_null());
301 CopyParams
* params
= new CopyParams
;
302 params
->src_file_path
= src_file_path
;
303 params
->dest_file_path
= dest_file_path
;
304 params
->preserve_last_modified
= preserve_last_modified
;
305 params
->callback
= callback
;
307 std::vector
<std::string
>* updated_local_ids
= new std::vector
<std::string
>;
308 bool* directory_changed
= new bool(false);
309 bool* should_copy_on_server
= new bool(false);
310 base::PostTaskAndReplyWithResult(
311 blocking_task_runner_
.get(),
313 base::Bind(&TryToCopyLocally
, metadata_
, cache_
, params
,
314 updated_local_ids
, directory_changed
, should_copy_on_server
),
315 base::Bind(&CopyOperation::CopyAfterTryToCopyLocally
,
316 weak_ptr_factory_
.GetWeakPtr(), base::Owned(params
),
317 base::Owned(updated_local_ids
), base::Owned(directory_changed
),
318 base::Owned(should_copy_on_server
)));
321 void CopyOperation::CopyAfterTryToCopyLocally(
322 const CopyParams
* params
,
323 const std::vector
<std::string
>* updated_local_ids
,
324 const bool* directory_changed
,
325 const bool* should_copy_on_server
,
327 DCHECK(thread_checker_
.CalledOnValidThread());
328 DCHECK(!params
->callback
.is_null());
330 for (const auto& id
: *updated_local_ids
) {
331 // Syncing for copy should be done in background, so pass the BACKGROUND
332 // context. See: crbug.com/420278.
333 delegate_
->OnEntryUpdatedByOperation(ClientContext(BACKGROUND
), id
);
336 if (*directory_changed
) {
337 FileChange changed_file
;
338 DCHECK(!params
->src_entry
.file_info().is_directory());
339 changed_file
.Update(params
->dest_file_path
, FileChange::FILE_TYPE_FILE
,
340 FileChange::CHANGE_TYPE_ADD_OR_UPDATE
);
341 delegate_
->OnFileChangedByOperation(changed_file
);
344 if (error
!= FILE_ERROR_OK
|| !*should_copy_on_server
) {
345 params
->callback
.Run(error
);
349 if (params
->parent_entry
.resource_id().empty()) {
350 // Parent entry may be being synced.
351 const bool waiting
= delegate_
->WaitForSyncComplete(
352 params
->parent_entry
.local_id(),
353 base::Bind(&CopyOperation::CopyAfterParentSync
,
354 weak_ptr_factory_
.GetWeakPtr(), *params
));
356 params
->callback
.Run(FILE_ERROR_NOT_FOUND
);
358 CopyAfterGetParentResourceId(*params
, ¶ms
->parent_entry
, FILE_ERROR_OK
);
362 void CopyOperation::CopyAfterParentSync(const CopyParams
& params
,
364 DCHECK(thread_checker_
.CalledOnValidThread());
365 DCHECK(!params
.callback
.is_null());
367 if (error
!= FILE_ERROR_OK
) {
368 params
.callback
.Run(error
);
372 ResourceEntry
* parent
= new ResourceEntry
;
373 base::PostTaskAndReplyWithResult(
374 blocking_task_runner_
.get(),
376 base::Bind(&internal::ResourceMetadata::GetResourceEntryById
,
377 base::Unretained(metadata_
),
378 params
.parent_entry
.local_id(),
380 base::Bind(&CopyOperation::CopyAfterGetParentResourceId
,
381 weak_ptr_factory_
.GetWeakPtr(),
383 base::Owned(parent
)));
386 void CopyOperation::CopyAfterGetParentResourceId(const CopyParams
& params
,
387 const ResourceEntry
* parent
,
389 DCHECK(thread_checker_
.CalledOnValidThread());
390 DCHECK(!params
.callback
.is_null());
392 if (error
!= FILE_ERROR_OK
) {
393 params
.callback
.Run(error
);
397 base::FilePath new_title
= params
.dest_file_path
.BaseName();
398 if (params
.src_entry
.file_specific_info().is_hosted_document()) {
399 // Drop the document extension, which should not be in the title.
400 // TODO(yoshiki): Remove this code with crbug.com/223304.
401 new_title
= new_title
.RemoveExtension();
404 base::Time last_modified
=
405 params
.preserve_last_modified
?
406 base::Time::FromInternalValue(
407 params
.src_entry
.file_info().last_modified()) : base::Time();
409 CopyResourceOnServer(
410 params
.src_entry
.resource_id(), parent
->resource_id(),
411 new_title
.AsUTF8Unsafe(), last_modified
, params
.callback
);
414 void CopyOperation::TransferFileFromLocalToRemote(
415 const base::FilePath
& local_src_path
,
416 const base::FilePath
& remote_dest_path
,
417 const FileOperationCallback
& callback
) {
418 DCHECK(thread_checker_
.CalledOnValidThread());
419 DCHECK(!callback
.is_null());
421 std::string
* gdoc_resource_id
= new std::string
;
422 ResourceEntry
* parent_entry
= new ResourceEntry
;
423 base::PostTaskAndReplyWithResult(
424 blocking_task_runner_
.get(),
427 &PrepareTransferFileFromLocalToRemote
,
428 metadata_
, local_src_path
, remote_dest_path
,
429 gdoc_resource_id
, parent_entry
),
431 &CopyOperation::TransferFileFromLocalToRemoteAfterPrepare
,
432 weak_ptr_factory_
.GetWeakPtr(),
433 local_src_path
, remote_dest_path
, callback
,
434 base::Owned(gdoc_resource_id
), base::Owned(parent_entry
)));
437 void CopyOperation::TransferFileFromLocalToRemoteAfterPrepare(
438 const base::FilePath
& local_src_path
,
439 const base::FilePath
& remote_dest_path
,
440 const FileOperationCallback
& callback
,
441 std::string
* gdoc_resource_id
,
442 ResourceEntry
* parent_entry
,
444 DCHECK(thread_checker_
.CalledOnValidThread());
445 DCHECK(!callback
.is_null());
447 if (error
!= FILE_ERROR_OK
) {
452 // For regular files, schedule the transfer.
453 if (gdoc_resource_id
->empty()) {
454 ScheduleTransferRegularFile(local_src_path
, remote_dest_path
, callback
);
458 // GDoc file may contain a resource ID in the old format.
459 const std::string canonicalized_resource_id
=
460 util::CanonicalizeResourceId(*gdoc_resource_id
);
462 // Drop the document extension, which should not be in the title.
463 // TODO(yoshiki): Remove this code with crbug.com/223304.
464 const std::string new_title
=
465 remote_dest_path
.BaseName().RemoveExtension().AsUTF8Unsafe();
467 // This is uploading a JSON file representing a hosted document.
468 TransferJsonGdocParams
* params
= new TransferJsonGdocParams(
469 callback
, canonicalized_resource_id
, *parent_entry
, new_title
);
470 base::PostTaskAndReplyWithResult(
471 blocking_task_runner_
.get(),
473 base::Bind(&LocalWorkForTransferJsonGdocFile
, metadata_
, params
),
474 base::Bind(&CopyOperation::TransferJsonGdocFileAfterLocalWork
,
475 weak_ptr_factory_
.GetWeakPtr(), base::Owned(params
)));
478 void CopyOperation::TransferJsonGdocFileAfterLocalWork(
479 TransferJsonGdocParams
* params
,
481 DCHECK(thread_checker_
.CalledOnValidThread());
483 if (error
!= FILE_ERROR_OK
) {
484 params
->callback
.Run(error
);
488 switch (params
->location_type
) {
489 // When |resource_id| is found in the local metadata and it has a specific
490 // parent folder, we assume the user's intention is to copy the document and
491 // thus perform the server-side copy operation.
493 CopyResourceOnServer(params
->resource_id
,
494 params
->parent_resource_id
,
499 // When |resource_id| has no parent, we just set the new destination folder
500 // as the parent, for sharing the document between the original source.
501 // This reparenting is already done in LocalWorkForTransferJsonGdocFile().
503 DCHECK(!params
->changed_path
.empty());
504 // Syncing for copy should be done in background, so pass the BACKGROUND
505 // context. See: crbug.com/420278.
506 delegate_
->OnEntryUpdatedByOperation(ClientContext(BACKGROUND
),
509 FileChange changed_file
;
511 params
->changed_path
,
512 FileChange::FILE_TYPE_FILE
, // This must be a hosted document.
513 FileChange::CHANGE_TYPE_ADD_OR_UPDATE
);
514 delegate_
->OnFileChangedByOperation(changed_file
);
515 params
->callback
.Run(error
);
518 // When the |resource_id| is not in the local metadata, assume it to be a
519 // document just now shared on the server but not synced locally.
520 // Same as the IS_ORPHAN case, we want to deal the case by setting parent,
521 // but this time we need to resort to server side operation.
522 case NOT_IN_METADATA
:
523 scheduler_
->UpdateResource(
524 params
->resource_id
, params
->parent_resource_id
, params
->new_title
,
525 base::Time(), base::Time(), google_apis::drive::Properties(),
526 ClientContext(USER_INITIATED
),
527 base::Bind(&CopyOperation::UpdateAfterServerSideOperation
,
528 weak_ptr_factory_
.GetWeakPtr(), params
->callback
));
533 void CopyOperation::CopyResourceOnServer(
534 const std::string
& resource_id
,
535 const std::string
& parent_resource_id
,
536 const std::string
& new_title
,
537 const base::Time
& last_modified
,
538 const FileOperationCallback
& callback
) {
539 DCHECK(thread_checker_
.CalledOnValidThread());
540 DCHECK(!callback
.is_null());
542 scheduler_
->CopyResource(
543 resource_id
, parent_resource_id
, new_title
, last_modified
,
544 base::Bind(&CopyOperation::UpdateAfterServerSideOperation
,
545 weak_ptr_factory_
.GetWeakPtr(),
549 void CopyOperation::UpdateAfterServerSideOperation(
550 const FileOperationCallback
& callback
,
551 google_apis::DriveApiErrorCode status
,
552 scoped_ptr
<google_apis::FileResource
> entry
) {
553 DCHECK(thread_checker_
.CalledOnValidThread());
554 DCHECK(!callback
.is_null());
556 FileError error
= GDataToFileError(status
);
557 if (error
!= FILE_ERROR_OK
) {
562 ResourceEntry
* resource_entry
= new ResourceEntry
;
564 // The copy on the server side is completed successfully. Update the local
566 base::FilePath
* file_path
= new base::FilePath
;
567 base::PostTaskAndReplyWithResult(
568 blocking_task_runner_
.get(),
570 base::Bind(&UpdateLocalStateForServerSideOperation
,
572 base::Passed(&entry
),
575 base::Bind(&CopyOperation::UpdateAfterLocalStateUpdate
,
576 weak_ptr_factory_
.GetWeakPtr(),
578 base::Owned(file_path
),
579 base::Owned(resource_entry
)));
582 void CopyOperation::UpdateAfterLocalStateUpdate(
583 const FileOperationCallback
& callback
,
584 base::FilePath
* file_path
,
585 const ResourceEntry
* entry
,
587 DCHECK(thread_checker_
.CalledOnValidThread());
588 DCHECK(!callback
.is_null());
590 if (error
== FILE_ERROR_OK
) {
591 FileChange changed_file
;
592 changed_file
.Update(*file_path
, *entry
,
593 FileChange::CHANGE_TYPE_ADD_OR_UPDATE
);
594 delegate_
->OnFileChangedByOperation(changed_file
);
599 void CopyOperation::ScheduleTransferRegularFile(
600 const base::FilePath
& local_src_path
,
601 const base::FilePath
& remote_dest_path
,
602 const FileOperationCallback
& callback
) {
603 DCHECK(thread_checker_
.CalledOnValidThread());
604 DCHECK(!callback
.is_null());
606 create_file_operation_
->CreateFile(
608 false, // Not exclusive (OK even if a file already exists).
609 std::string(), // no specific mime type; CreateFile should guess it.
610 base::Bind(&CopyOperation::ScheduleTransferRegularFileAfterCreate
,
611 weak_ptr_factory_
.GetWeakPtr(),
612 local_src_path
, remote_dest_path
, callback
));
615 void CopyOperation::ScheduleTransferRegularFileAfterCreate(
616 const base::FilePath
& local_src_path
,
617 const base::FilePath
& remote_dest_path
,
618 const FileOperationCallback
& callback
,
620 DCHECK(thread_checker_
.CalledOnValidThread());
621 DCHECK(!callback
.is_null());
623 if (error
!= FILE_ERROR_OK
) {
628 std::string
* local_id
= new std::string
;
629 ResourceEntry
* entry
= new ResourceEntry
;
630 base::PostTaskAndReplyWithResult(
631 blocking_task_runner_
.get(),
633 base::Bind(&UpdateLocalStateForScheduleTransfer
,
641 &CopyOperation::ScheduleTransferRegularFileAfterUpdateLocalState
,
642 weak_ptr_factory_
.GetWeakPtr(),
646 base::Owned(local_id
)));
649 void CopyOperation::ScheduleTransferRegularFileAfterUpdateLocalState(
650 const FileOperationCallback
& callback
,
651 const base::FilePath
& remote_dest_path
,
652 const ResourceEntry
* entry
,
653 std::string
* local_id
,
655 DCHECK(thread_checker_
.CalledOnValidThread());
656 DCHECK(!callback
.is_null());
658 if (error
== FILE_ERROR_OK
) {
659 FileChange changed_file
;
660 changed_file
.Update(remote_dest_path
, *entry
,
661 FileChange::CHANGE_TYPE_ADD_OR_UPDATE
);
662 delegate_
->OnFileChangedByOperation(changed_file
);
663 // Syncing for copy should be done in background, so pass the BACKGROUND
664 // context. See: crbug.com/420278.
665 delegate_
->OnEntryUpdatedByOperation(ClientContext(BACKGROUND
), *local_id
);
670 } // namespace file_system