1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

All: Fixed sync issue when target does not have reliable timestamps

This commit is contained in:
Laurent Cozic 2017-12-04 18:39:40 +00:00
parent ab8c66a361
commit 73e81a54b4

View File

@ -208,6 +208,7 @@ class Synchronizer {
let action = null; let action = null;
let updateSyncTimeOnly = true; let updateSyncTimeOnly = true;
let reason = ''; let reason = '';
let remoteContent = null;
if (!remote) { if (!remote) {
if (!local.sync_time) { if (!local.sync_time) {
@ -220,9 +221,25 @@ class Synchronizer {
reason = 'remote has been deleted, but local has changes'; reason = 'remote has been deleted, but local has changes';
} }
} else { } else {
if (remote.updated_time > local.sync_time) { // Note: in order to know the real updated_time value, we need to load the content. In theory we could
console.info('CONFLICT', local.sync_time, remote.updated_time); // rely on the file timestamp (in remote.updated_time) but in practice it's not accurate enough and
// can lead to conflicts (for example when the file timestamp is slightly ahead of it's real
// updated_time). updated_time is set and managed by clients so it's always accurate.
// Same situation below for updateLocal.
//
// This is a bit inefficient because if the resulting action is "updateRemote" we don't need the whole
// content, but for now that will do since being reliable is the priority.
//
// TODO: assuming a particular sync target is guaranteed to have accurate timestamps, the driver maybe
// could expose this with a accurateTimestamps() method that returns "true". In that case, the test
// could be done using the file timestamp and the potentially unecessary content loading could be skipped.
// OneDrive does not appear to have accurate timestamps as lastModifiedDateTime would occasionally be
// a few seconds ahead of what it was set with setTimestamp()
remoteContent = await this.api().get(path);
if (!remoteContent) throw new Error('Got metadata for path but could not fetch content: ' + path);
remoteContent = await BaseItem.unserialize(remoteContent);
if (remoteContent.updated_time > local.sync_time) {
// Since, in this loop, we are only dealing with items that require sync, if the // Since, in this loop, we are only dealing with items that require sync, if the
// remote has been modified after the sync time, it means both items have been // remote has been modified after the sync time, it means both items have been
// modified and so there's a conflict. // modified and so there's a conflict.
@ -268,8 +285,6 @@ class Synchronizer {
// await this.api().setTimestamp(tempPath, local.updated_time); // await this.api().setTimestamp(tempPath, local.updated_time);
// await this.api().move(tempPath, path); // await this.api().move(tempPath, path);
console.info('Update remote: ', local);
await this.api().put(path, content); await this.api().put(path, content);
await this.api().setTimestamp(path, local.updated_time); await this.api().setTimestamp(path, local.updated_time);
await ItemClass.saveSyncTime(syncTargetId, local, time.unixMs()); await ItemClass.saveSyncTime(syncTargetId, local, time.unixMs());
@ -277,8 +292,7 @@ class Synchronizer {
} else if (action == 'itemConflict') { } else if (action == 'itemConflict') {
if (remote) { if (remote) {
let remoteContent = await this.api().get(path); local = remoteContent;
local = await BaseItem.unserialize(remoteContent);
const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs()); const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs());
await ItemClass.save(local, { autoTimestamp: false, nextQueries: syncTimeQueries }); await ItemClass.save(local, { autoTimestamp: false, nextQueries: syncTimeQueries });
@ -294,12 +308,9 @@ class Synchronizer {
// so in this case we just take the remote content. // so in this case we just take the remote content.
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
let loadedRemote = null;
let mustHandleConflict = true; let mustHandleConflict = true;
if (remote) { if (remoteContent) {
const remoteContent = await this.api().get(path); mustHandleConflict = Note.mustHandleConflict(local, remoteContent);
loadedRemote = await BaseItem.unserialize(remoteContent);
mustHandleConflict = Note.mustHandleConflict(local, loadedRemote);
} }
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
@ -320,7 +331,7 @@ class Synchronizer {
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
if (remote) { if (remote) {
local = loadedRemote; local = remoteContent;
const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs()); const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs());
await ItemClass.save(local, { autoTimestamp: false, nextQueries: syncTimeQueries }); await ItemClass.save(local, { autoTimestamp: false, nextQueries: syncTimeQueries });
} else { } else {
@ -389,21 +400,30 @@ class Synchronizer {
let remote = remotes[i]; let remote = remotes[i];
if (!BaseItem.isSystemPath(remote.path)) continue; // The delta API might return things like the .sync, .resource or the root folder if (!BaseItem.isSystemPath(remote.path)) continue; // The delta API might return things like the .sync, .resource or the root folder
const loadContent = async () => {
content = await this.api().get(path);
if (!content) return null;
return await BaseItem.unserialize(content);
}
let path = remote.path; let path = remote.path;
let action = null; let action = null;
let reason = ''; let reason = '';
let local = await BaseItem.loadItemByPath(path); let local = await BaseItem.loadItemByPath(path);
let content = null;
if (!local) { if (!local) {
if (!remote.isDeleted) { if (remote.isDeleted !== true) {
action = 'createLocal'; action = 'createLocal';
reason = 'remote exists but local does not'; reason = 'remote exists but local does not';
content = await loadContent();
} }
} else { } else {
if (remote.isDeleted) { if (remote.isDeleted) {
action = 'deleteLocal'; action = 'deleteLocal';
reason = 'remote has been deleted'; reason = 'remote has been deleted';
} else { } else {
if (remote.updated_time > local.updated_time) { content = await loadContent();
if (content && content.updated_time > local.updated_time) {
action = 'updateLocal'; action = 'updateLocal';
reason = 'remote is more recent than local'; reason = 'remote is more recent than local';
} }
@ -416,12 +436,10 @@ class Synchronizer {
if (action == 'createLocal' || action == 'updateLocal') { if (action == 'createLocal' || action == 'updateLocal') {
let content = await this.api().get(path);
if (content === null) { if (content === null) {
this.logger().warn('Remote has been deleted between now and the list() call? In that case it will be handled during the next sync: ' + path); this.logger().warn('Remote has been deleted between now and the list() call? In that case it will be handled during the next sync: ' + path);
continue; continue;
} }
content = await BaseItem.unserialize(content);
let ItemClass = BaseItem.itemClass(content); let ItemClass = BaseItem.itemClass(content);
content = ItemClass.filter(content); content = ItemClass.filter(content);