| OLD | NEW |
| (Empty) |
| 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 "sync/internal_api/public/write_node.h" | |
| 6 | |
| 7 #include <stdint.h> | |
| 8 | |
| 9 #include "base/strings/string_util.h" | |
| 10 #include "base/strings/utf_string_conversions.h" | |
| 11 #include "base/values.h" | |
| 12 #include "sync/internal_api/public/base_transaction.h" | |
| 13 #include "sync/internal_api/public/write_transaction.h" | |
| 14 #include "sync/internal_api/syncapi_internal.h" | |
| 15 #include "sync/protocol/bookmark_specifics.pb.h" | |
| 16 #include "sync/protocol/typed_url_specifics.pb.h" | |
| 17 #include "sync/syncable/mutable_entry.h" | |
| 18 #include "sync/syncable/nigori_util.h" | |
| 19 #include "sync/syncable/syncable_util.h" | |
| 20 #include "sync/util/cryptographer.h" | |
| 21 | |
| 22 using std::string; | |
| 23 using std::vector; | |
| 24 | |
| 25 namespace syncer { | |
| 26 | |
| 27 using syncable::kEncryptedString; | |
| 28 using syncable::SPECIFICS; | |
| 29 | |
| 30 static const char kDefaultNameForNewNodes[] = " "; | |
| 31 | |
| 32 void WriteNode::SetIsFolder(bool folder) { | |
| 33 if (entry_->GetIsDir() == folder) | |
| 34 return; // Skip redundant changes. | |
| 35 | |
| 36 entry_->PutIsDir(folder); | |
| 37 MarkForSyncing(); | |
| 38 } | |
| 39 | |
| 40 void WriteNode::SetTitle(const std::string& title) { | |
| 41 DCHECK_NE(GetModelType(), UNSPECIFIED); | |
| 42 ModelType type = GetModelType(); | |
| 43 // It's possible the nigori lost the set of encrypted types. If the current | |
| 44 // specifics are already encrypted, we want to ensure we continue encrypting. | |
| 45 bool needs_encryption = GetTransaction()->GetEncryptedTypes().Has(type) || | |
| 46 entry_->GetSpecifics().has_encrypted(); | |
| 47 | |
| 48 // If this datatype is encrypted and is not a bookmark, we disregard the | |
| 49 // specified title in favor of kEncryptedString. For encrypted bookmarks the | |
| 50 // NON_UNIQUE_NAME will still be kEncryptedString, but we store the real title | |
| 51 // into the specifics. All strings compared are server legal strings. | |
| 52 std::string new_legal_title; | |
| 53 if (type != BOOKMARKS && needs_encryption) { | |
| 54 new_legal_title = kEncryptedString; | |
| 55 } else { | |
| 56 DCHECK(base::IsStringUTF8(title)); | |
| 57 SyncAPINameToServerName(title, &new_legal_title); | |
| 58 base::TruncateUTF8ToByteSize(new_legal_title, 255, &new_legal_title); | |
| 59 } | |
| 60 | |
| 61 std::string current_legal_title; | |
| 62 if (BOOKMARKS == type && | |
| 63 entry_->GetSpecifics().has_encrypted()) { | |
| 64 // Encrypted bookmarks only have their title in the unencrypted specifics. | |
| 65 current_legal_title = GetBookmarkSpecifics().title(); | |
| 66 } else { | |
| 67 // Non-bookmarks and legacy bookmarks (those with no title in their | |
| 68 // specifics) store their title in NON_UNIQUE_NAME. Non-legacy bookmarks | |
| 69 // store their title in specifics as well as NON_UNIQUE_NAME. | |
| 70 current_legal_title = entry_->GetNonUniqueName(); | |
| 71 } | |
| 72 | |
| 73 bool title_matches = (current_legal_title == new_legal_title); | |
| 74 bool encrypted_without_overwriting_name = (needs_encryption && | |
| 75 entry_->GetNonUniqueName() != kEncryptedString); | |
| 76 | |
| 77 // For bookmarks, we also set the title field in the specifics. | |
| 78 // TODO(zea): refactor bookmarks to not need this functionality. | |
| 79 sync_pb::EntitySpecifics specifics = GetEntitySpecifics(); | |
| 80 if (GetModelType() == BOOKMARKS && | |
| 81 specifics.bookmark().title() != new_legal_title) { | |
| 82 specifics.mutable_bookmark()->set_title(new_legal_title); | |
| 83 SetEntitySpecifics(specifics); // Does it's own encryption checking. | |
| 84 title_matches = false; | |
| 85 } | |
| 86 | |
| 87 // If the title matches and the NON_UNIQUE_NAME is properly overwritten as | |
| 88 // necessary, nothing needs to change. | |
| 89 if (title_matches && !encrypted_without_overwriting_name) { | |
| 90 DVLOG(2) << "Title matches, dropping change."; | |
| 91 return; | |
| 92 } | |
| 93 | |
| 94 // For bookmarks, this has to happen after we set the title in the specifics, | |
| 95 // because the presence of a title in the NON_UNIQUE_NAME is what controls | |
| 96 // the logic deciding whether this is an empty node or a legacy bookmark. | |
| 97 // See BaseNode::GetUnencryptedSpecific(..). | |
| 98 if (needs_encryption) | |
| 99 entry_->PutNonUniqueName(kEncryptedString); | |
| 100 else | |
| 101 entry_->PutNonUniqueName(new_legal_title); | |
| 102 | |
| 103 DVLOG(1) << "Overwriting title of type " | |
| 104 << ModelTypeToString(type) | |
| 105 << " and marking for syncing."; | |
| 106 MarkForSyncing(); | |
| 107 } | |
| 108 | |
| 109 void WriteNode::SetBookmarkSpecifics( | |
| 110 const sync_pb::BookmarkSpecifics& new_value) { | |
| 111 sync_pb::EntitySpecifics entity_specifics; | |
| 112 entity_specifics.mutable_bookmark()->CopyFrom(new_value); | |
| 113 SetEntitySpecifics(entity_specifics); | |
| 114 } | |
| 115 | |
| 116 void WriteNode::SetNigoriSpecifics( | |
| 117 const sync_pb::NigoriSpecifics& new_value) { | |
| 118 sync_pb::EntitySpecifics entity_specifics; | |
| 119 entity_specifics.mutable_nigori()->CopyFrom(new_value); | |
| 120 SetEntitySpecifics(entity_specifics); | |
| 121 } | |
| 122 | |
| 123 void WriteNode::SetPasswordSpecifics( | |
| 124 const sync_pb::PasswordSpecificsData& data) { | |
| 125 DCHECK_EQ(GetModelType(), PASSWORDS); | |
| 126 | |
| 127 Cryptographer* cryptographer = GetTransaction()->GetCryptographer(); | |
| 128 | |
| 129 // We have to do the idempotency check here (vs in UpdateEntryWithEncryption) | |
| 130 // because Passwords have their encrypted data within the PasswordSpecifics, | |
| 131 // vs within the EntitySpecifics like all the other types. | |
| 132 const sync_pb::EntitySpecifics& old_specifics = GetEntitySpecifics(); | |
| 133 sync_pb::EntitySpecifics entity_specifics; | |
| 134 // Copy over the old specifics if they exist. | |
| 135 if (GetModelTypeFromSpecifics(old_specifics) == PASSWORDS) { | |
| 136 entity_specifics.CopyFrom(old_specifics); | |
| 137 } else { | |
| 138 AddDefaultFieldValue(PASSWORDS, &entity_specifics); | |
| 139 } | |
| 140 sync_pb::PasswordSpecifics* password_specifics = | |
| 141 entity_specifics.mutable_password(); | |
| 142 // This will only update password_specifics if the underlying unencrypted blob | |
| 143 // was different from |data| or was not encrypted with the proper passphrase. | |
| 144 if (!cryptographer->Encrypt(data, password_specifics->mutable_encrypted())) { | |
| 145 LOG(ERROR) << "Failed to encrypt password, possibly due to sync node " | |
| 146 << "corruption"; | |
| 147 return; | |
| 148 } | |
| 149 SetEntitySpecifics(entity_specifics); | |
| 150 } | |
| 151 | |
| 152 void WriteNode::SetEntitySpecifics( | |
| 153 const sync_pb::EntitySpecifics& new_value) { | |
| 154 ModelType new_specifics_type = | |
| 155 GetModelTypeFromSpecifics(new_value); | |
| 156 CHECK(!new_value.password().has_client_only_encrypted_data()); | |
| 157 DCHECK_NE(new_specifics_type, UNSPECIFIED); | |
| 158 DVLOG(1) << "Writing entity specifics of type " | |
| 159 << ModelTypeToString(new_specifics_type); | |
| 160 DCHECK_EQ(new_specifics_type, GetModelType()); | |
| 161 | |
| 162 // Preserve unknown fields. | |
| 163 const sync_pb::EntitySpecifics& old_specifics = entry_->GetSpecifics(); | |
| 164 sync_pb::EntitySpecifics new_specifics; | |
| 165 new_specifics.CopyFrom(new_value); | |
| 166 new_specifics.mutable_unknown_fields() | |
| 167 ->append(old_specifics.unknown_fields()); | |
| 168 | |
| 169 // Will update the entry if encryption was necessary. | |
| 170 if (!UpdateEntryWithEncryption(GetTransaction()->GetWrappedTrans(), | |
| 171 new_specifics, | |
| 172 entry_)) { | |
| 173 return; | |
| 174 } | |
| 175 if (entry_->GetSpecifics().has_encrypted()) { | |
| 176 // EncryptIfNecessary already updated the entry for us and marked for | |
| 177 // syncing if it was needed. Now we just make a copy of the unencrypted | |
| 178 // specifics so that if this node is updated, we do not have to decrypt the | |
| 179 // old data. Note that this only modifies the node's local data, not the | |
| 180 // entry itself. | |
| 181 SetUnencryptedSpecifics(new_value); | |
| 182 } | |
| 183 | |
| 184 DCHECK_EQ(new_specifics_type, GetModelType()); | |
| 185 } | |
| 186 | |
| 187 void WriteNode::ResetFromSpecifics() { | |
| 188 SetEntitySpecifics(GetEntitySpecifics()); | |
| 189 } | |
| 190 | |
| 191 void WriteNode::SetTypedUrlSpecifics( | |
| 192 const sync_pb::TypedUrlSpecifics& new_value) { | |
| 193 sync_pb::EntitySpecifics entity_specifics; | |
| 194 entity_specifics.mutable_typed_url()->CopyFrom(new_value); | |
| 195 SetEntitySpecifics(entity_specifics); | |
| 196 } | |
| 197 | |
| 198 void WriteNode::SetExternalId(int64_t id) { | |
| 199 if (GetExternalId() != id) | |
| 200 entry_->PutLocalExternalId(id); | |
| 201 } | |
| 202 | |
| 203 WriteNode::WriteNode(WriteTransaction* transaction) | |
| 204 : entry_(NULL), transaction_(transaction) { | |
| 205 DCHECK(transaction); | |
| 206 } | |
| 207 | |
| 208 WriteNode::~WriteNode() { | |
| 209 delete entry_; | |
| 210 } | |
| 211 | |
| 212 // Find an existing node matching the ID |id|, and bind this WriteNode to it. | |
| 213 // Return true on success. | |
| 214 BaseNode::InitByLookupResult WriteNode::InitByIdLookup(int64_t id) { | |
| 215 DCHECK(!entry_) << "Init called twice"; | |
| 216 DCHECK_NE(id, kInvalidId); | |
| 217 entry_ = new syncable::MutableEntry(transaction_->GetWrappedWriteTrans(), | |
| 218 syncable::GET_BY_HANDLE, id); | |
| 219 if (!entry_->good()) | |
| 220 return INIT_FAILED_ENTRY_NOT_GOOD; | |
| 221 if (entry_->GetIsDel()) | |
| 222 return INIT_FAILED_ENTRY_IS_DEL; | |
| 223 return DecryptIfNecessary() ? INIT_OK : INIT_FAILED_DECRYPT_IF_NECESSARY; | |
| 224 } | |
| 225 | |
| 226 // Find a node by client tag, and bind this WriteNode to it. | |
| 227 // Return true if the write node was found, and was not deleted. | |
| 228 // Undeleting a deleted node is possible by ClientTag. | |
| 229 BaseNode::InitByLookupResult WriteNode::InitByClientTagLookup( | |
| 230 ModelType model_type, | |
| 231 const std::string& tag) { | |
| 232 DCHECK(!entry_) << "Init called twice"; | |
| 233 if (tag.empty()) | |
| 234 return INIT_FAILED_PRECONDITION; | |
| 235 | |
| 236 const std::string hash = syncable::GenerateSyncableHash(model_type, tag); | |
| 237 | |
| 238 entry_ = new syncable::MutableEntry(transaction_->GetWrappedWriteTrans(), | |
| 239 syncable::GET_BY_CLIENT_TAG, hash); | |
| 240 if (!entry_->good()) | |
| 241 return INIT_FAILED_ENTRY_NOT_GOOD; | |
| 242 if (entry_->GetIsDel()) | |
| 243 return INIT_FAILED_ENTRY_IS_DEL; | |
| 244 return DecryptIfNecessary() ? INIT_OK : INIT_FAILED_DECRYPT_IF_NECESSARY; | |
| 245 } | |
| 246 | |
| 247 BaseNode::InitByLookupResult WriteNode::InitTypeRoot(ModelType type) { | |
| 248 DCHECK(!entry_) << "Init called twice"; | |
| 249 if (!IsRealDataType(type)) | |
| 250 return INIT_FAILED_PRECONDITION; | |
| 251 entry_ = new syncable::MutableEntry(transaction_->GetWrappedWriteTrans(), | |
| 252 syncable::GET_TYPE_ROOT, type); | |
| 253 if (!entry_->good()) | |
| 254 return INIT_FAILED_ENTRY_NOT_GOOD; | |
| 255 if (entry_->GetIsDel()) | |
| 256 return INIT_FAILED_ENTRY_IS_DEL; | |
| 257 ModelType model_type = GetModelType(); | |
| 258 DCHECK_EQ(model_type, NIGORI); | |
| 259 return INIT_OK; | |
| 260 } | |
| 261 | |
| 262 // Create a new node with default properties, and bind this WriteNode to it. | |
| 263 // Return true on success. | |
| 264 bool WriteNode::InitBookmarkByCreation(const BaseNode& parent, | |
| 265 const BaseNode* predecessor) { | |
| 266 DCHECK(!entry_) << "Init called twice"; | |
| 267 // |predecessor| must be a child of |parent| or NULL. | |
| 268 if (predecessor && predecessor->GetParentId() != parent.GetId()) { | |
| 269 DCHECK(false); | |
| 270 return false; | |
| 271 } | |
| 272 | |
| 273 syncable::Id parent_id = parent.GetSyncId(); | |
| 274 DCHECK(!parent_id.IsNull()); | |
| 275 | |
| 276 // Start out with a dummy name. We expect | |
| 277 // the caller to set a meaningful name after creation. | |
| 278 string dummy(kDefaultNameForNewNodes); | |
| 279 | |
| 280 entry_ = new syncable::MutableEntry(transaction_->GetWrappedWriteTrans(), | |
| 281 syncable::CREATE, BOOKMARKS, | |
| 282 parent_id, dummy); | |
| 283 | |
| 284 if (!entry_->good()) | |
| 285 return false; | |
| 286 | |
| 287 // Entries are untitled folders by default. | |
| 288 entry_->PutIsDir(true); | |
| 289 | |
| 290 if (!PutPredecessor(predecessor)) { | |
| 291 return false; | |
| 292 } | |
| 293 | |
| 294 // Mark this entry as unsynced, to wake up the syncer. | |
| 295 MarkForSyncing(); | |
| 296 return true; | |
| 297 } | |
| 298 | |
| 299 WriteNode::InitUniqueByCreationResult WriteNode::InitUniqueByCreation( | |
| 300 ModelType model_type, | |
| 301 const BaseNode& parent, | |
| 302 const std::string& tag) { | |
| 303 return InitUniqueByCreationImpl(model_type, parent.GetSyncId(), tag); | |
| 304 } | |
| 305 | |
| 306 WriteNode::InitUniqueByCreationResult WriteNode::InitUniqueByCreation( | |
| 307 ModelType model_type, | |
| 308 const std::string& tag) { | |
| 309 return InitUniqueByCreationImpl(model_type, syncable::Id(), tag); | |
| 310 } | |
| 311 | |
| 312 // Create a new node with default properties and a client defined unique tag, | |
| 313 // and bind this WriteNode to it. | |
| 314 // Return true on success. If the tag exists in the database, then | |
| 315 // we will attempt to undelete the node. | |
| 316 WriteNode::InitUniqueByCreationResult WriteNode::InitUniqueByCreationImpl( | |
| 317 ModelType model_type, | |
| 318 const syncable::Id& parent_id, | |
| 319 const std::string& tag) { | |
| 320 // This DCHECK will only fail if init is called twice. | |
| 321 DCHECK(!entry_); | |
| 322 if (tag.empty()) { | |
| 323 LOG(WARNING) << "InitUniqueByCreation failed due to empty tag."; | |
| 324 return INIT_FAILED_EMPTY_TAG; | |
| 325 } | |
| 326 | |
| 327 const std::string hash = syncable::GenerateSyncableHash(model_type, tag); | |
| 328 | |
| 329 // Start out with a dummy name. We expect | |
| 330 // the caller to set a meaningful name after creation. | |
| 331 string dummy(kDefaultNameForNewNodes); | |
| 332 | |
| 333 // Check if we have this locally and need to undelete it. | |
| 334 std::unique_ptr<syncable::MutableEntry> existing_entry( | |
| 335 new syncable::MutableEntry(transaction_->GetWrappedWriteTrans(), | |
| 336 syncable::GET_BY_CLIENT_TAG, hash)); | |
| 337 | |
| 338 if (existing_entry->good()) { | |
| 339 bool entry_undeleted = false; | |
| 340 if (existing_entry->GetIsDel()) { | |
| 341 // Rules for undelete: | |
| 342 // BASE_VERSION: Must keep the same. | |
| 343 // ID: Essential to keep the same. | |
| 344 // META_HANDLE: Must be the same, so we can't "split" the entry. | |
| 345 // IS_DEL: Must be set to false, will cause reindexing. | |
| 346 // This one is weird because IS_DEL is true for "update only" | |
| 347 // items. It should be OK to undelete an update only. | |
| 348 // MTIME/CTIME: Seems reasonable to just leave them alone. | |
| 349 // IS_UNSYNCED: Must set this to true or face database insurrection. | |
| 350 // We do this below this block. | |
| 351 // IS_UNAPPLIED_UPDATE: Either keep it the same or also set BASE_VERSION | |
| 352 // to SERVER_VERSION. We keep it the same here. | |
| 353 // IS_DIR: We'll leave it the same. | |
| 354 // SPECIFICS: Reset it. | |
| 355 | |
| 356 // Put specifics to define the entry's model type to handle the case | |
| 357 // where this is not actually an undeletion, but instead a collision | |
| 358 // with a newly downloaded, processed, and unapplied server update. | |
| 359 // This should be done first before inserting the entry into the | |
| 360 // directory's ParentChildIndex by clearing its "deleted" flag below. | |
| 361 // This is a fix for http://crbug.com/505761. | |
| 362 sync_pb::EntitySpecifics specifics; | |
| 363 AddDefaultFieldValue(model_type, &specifics); | |
| 364 existing_entry->PutSpecifics(specifics); | |
| 365 | |
| 366 existing_entry->PutIsDel(false); | |
| 367 | |
| 368 // Client tags are immutable and must be paired with the ID. | |
| 369 // If a server update comes down with an ID and client tag combo, | |
| 370 // and it already exists, always overwrite it and store only one copy. | |
| 371 // We have to undelete entries because we can't disassociate IDs from | |
| 372 // tags and updates. | |
| 373 | |
| 374 existing_entry->PutNonUniqueName(dummy); | |
| 375 existing_entry->PutParentId(parent_id); | |
| 376 entry_undeleted = true; | |
| 377 } // Else just reuse the existing entry. | |
| 378 entry_ = existing_entry.release(); | |
| 379 // If entry is undeleted, its specifics are reset to default, unencrypted | |
| 380 // value, and therefore no decryption is necessary. Moreover trying to | |
| 381 // decrypt the password entry will fail because passwords are expected to be | |
| 382 // encrypted. | |
| 383 if (!entry_undeleted && !DecryptIfNecessary()) | |
| 384 return INIT_FAILED_DECRYPT_EXISTING_ENTRY; | |
| 385 } else { | |
| 386 entry_ = new syncable::MutableEntry(transaction_->GetWrappedWriteTrans(), | |
| 387 syncable::CREATE, | |
| 388 model_type, parent_id, dummy); | |
| 389 } | |
| 390 | |
| 391 if (!entry_->good()) | |
| 392 return INIT_FAILED_COULD_NOT_CREATE_ENTRY; | |
| 393 | |
| 394 // Has no impact if the client tag is already set. | |
| 395 entry_->PutUniqueClientTag(hash); | |
| 396 | |
| 397 // We don't support directory and tag combinations. | |
| 398 entry_->PutIsDir(false); | |
| 399 | |
| 400 if (entry_->ShouldMaintainPosition()) { | |
| 401 if (!entry_->PutPredecessor(syncable::Id())) | |
| 402 return INIT_FAILED_SET_PREDECESSOR; | |
| 403 } | |
| 404 | |
| 405 // Mark this entry as unsynced, to wake up the syncer. | |
| 406 MarkForSyncing(); | |
| 407 | |
| 408 return INIT_SUCCESS; | |
| 409 } | |
| 410 | |
| 411 bool WriteNode::SetPosition(const BaseNode& new_parent, | |
| 412 const BaseNode* predecessor) { | |
| 413 // |predecessor| must be a child of |new_parent| or NULL. | |
| 414 if (predecessor && predecessor->GetParentId() != new_parent.GetId()) { | |
| 415 DCHECK(false); | |
| 416 return false; | |
| 417 } | |
| 418 | |
| 419 syncable::Id new_parent_id = new_parent.GetSyncId(); | |
| 420 DCHECK(!new_parent_id.IsNull()); | |
| 421 | |
| 422 // Filter out redundant changes if both the parent and the predecessor match. | |
| 423 if (new_parent_id == entry_->GetParentId()) { | |
| 424 const syncable::Id& old = entry_->GetPredecessorId(); | |
| 425 if ((!predecessor && old.IsNull()) || | |
| 426 (predecessor && (old == predecessor->GetSyncId()))) { | |
| 427 return true; | |
| 428 } | |
| 429 } | |
| 430 | |
| 431 entry_->PutParentId(new_parent_id); | |
| 432 | |
| 433 if (!PutPredecessor(predecessor)) { | |
| 434 return false; | |
| 435 } | |
| 436 | |
| 437 // Mark this entry as unsynced, to wake up the syncer. | |
| 438 MarkForSyncing(); | |
| 439 return true; | |
| 440 } | |
| 441 | |
| 442 void WriteNode::SetAttachmentMetadata( | |
| 443 const sync_pb::AttachmentMetadata& attachment_metadata) { | |
| 444 entry_->PutAttachmentMetadata(attachment_metadata); | |
| 445 } | |
| 446 | |
| 447 const syncable::Entry* WriteNode::GetEntry() const { | |
| 448 return entry_; | |
| 449 } | |
| 450 | |
| 451 const BaseTransaction* WriteNode::GetTransaction() const { | |
| 452 return transaction_; | |
| 453 } | |
| 454 | |
| 455 syncable::MutableEntry* WriteNode::GetMutableEntryForTest() { | |
| 456 return entry_; | |
| 457 } | |
| 458 | |
| 459 void WriteNode::Tombstone() { | |
| 460 // These lines must be in this order. The call to Put(IS_DEL) might choose to | |
| 461 // unset the IS_UNSYNCED bit if the item was not known to the server at the | |
| 462 // time of deletion. It's important that the bit not be reset in that case. | |
| 463 MarkForSyncing(); | |
| 464 entry_->PutIsDel(true); | |
| 465 } | |
| 466 | |
| 467 void WriteNode::Drop() { | |
| 468 if (entry_->GetId().ServerKnows()) { | |
| 469 entry_->PutIsDel(true); | |
| 470 } | |
| 471 } | |
| 472 | |
| 473 bool WriteNode::PutPredecessor(const BaseNode* predecessor) { | |
| 474 DCHECK(!entry_->GetParentId().IsNull()); | |
| 475 syncable::Id predecessor_id = | |
| 476 predecessor ? predecessor->GetSyncId() : syncable::Id(); | |
| 477 return entry_->PutPredecessor(predecessor_id); | |
| 478 } | |
| 479 | |
| 480 void WriteNode::MarkForSyncing() { | |
| 481 syncable::MarkForSyncing(entry_); | |
| 482 } | |
| 483 | |
| 484 } // namespace syncer | |
| OLD | NEW |