1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-17 18:44:45 +02:00

Chore: Add more types to synchronizer and improved sync conflict log

This commit is contained in:
Laurent Cozic 2023-12-30 19:05:41 +00:00
parent 2512adebd2
commit 37dbea1613
4 changed files with 46 additions and 29 deletions

View File

@ -26,10 +26,11 @@ import { fetchSyncInfo, getActiveMasterKey, localSyncInfo, mergeSyncInfos, saveL
import { getMasterPassword, setupAndDisableEncryption, setupAndEnableEncryption } from './services/e2ee/utils';
import { generateKeyPair } from './services/e2ee/ppk';
import syncDebugLog from './services/synchronizer/syncDebugLog';
import handleConflictAction, { ConflictAction } from './services/synchronizer/utils/handleConflictAction';
import handleConflictAction from './services/synchronizer/utils/handleConflictAction';
import resourceRemotePath from './services/synchronizer/utils/resourceRemotePath';
import syncDeleteStep from './services/synchronizer/utils/syncDeleteStep';
import { ErrorCode } from './errors';
import { SyncAction } from './services/synchronizer/utils/types';
const { sprintf } = require('sprintf-js');
const { Dirnames } = require('./services/synchronizer/utils/types');
@ -199,7 +200,7 @@ export default class Synchronizer {
return lines;
}
public logSyncOperation(action: string, local: any = null, remote: RemoteItem = null, message: string = null, actionCount = 1) {
public logSyncOperation(action: SyncAction | 'cancelling' | 'starting' | 'fetchingTotal' | 'fetchingProcessed' | 'finished', local: any = null, remote: RemoteItem = null, message: string = null, actionCount = 1) {
const line = ['Sync'];
line.push(action);
if (message) line.push(message);
@ -577,20 +578,20 @@ export default class Synchronizer {
if (donePaths.indexOf(path) >= 0) throw new JoplinError(sprintf('Processing a path that has already been done: %s. sync_time was not updated? Remote item has an updated_time in the future?', path), 'processingPathTwice');
const remote: RemoteItem = result.neverSyncedItemIds.includes(local.id) ? null : await this.apiCall('stat', path);
let action = null;
let action: SyncAction = null;
let itemIsReadOnly = false;
let reason = '';
let remoteContent = null;
const getConflictType = (conflictedItem: any) => {
if (conflictedItem.type_ === BaseModel.TYPE_NOTE) return 'noteConflict';
if (conflictedItem.type_ === BaseModel.TYPE_RESOURCE) return 'resourceConflict';
return 'itemConflict';
if (conflictedItem.type_ === BaseModel.TYPE_NOTE) return SyncAction.NoteConflict;
if (conflictedItem.type_ === BaseModel.TYPE_RESOURCE) return SyncAction.ResourceConflict;
return SyncAction.ItemConflict;
};
if (!remote) {
if (!local.sync_time) {
action = 'createRemote';
action = SyncAction.CreateRemote;
reason = 'remote does not exist, and local is new and has never been synced';
} else {
// Note or item was modified after having been deleted remotely
@ -635,7 +636,7 @@ export default class Synchronizer {
action = getConflictType(local);
reason = 'both remote and local have changes';
} else {
action = 'updateRemote';
action = SyncAction.UpdateRemote;
reason = 'local has changes';
}
}
@ -648,7 +649,7 @@ export default class Synchronizer {
this.logSyncOperation(action, local, remote, reason);
if (local.type_ === BaseModel.TYPE_RESOURCE && (action === 'createRemote' || action === 'updateRemote')) {
if (local.type_ === BaseModel.TYPE_RESOURCE && (action === SyncAction.CreateRemote || action === SyncAction.UpdateRemote)) {
const localState = await Resource.localState(local.id);
if (localState.fetch_status !== Resource.FETCH_STATUS_DONE) {
// This condition normally shouldn't happen
@ -722,7 +723,7 @@ export default class Synchronizer {
}
}
if (action === 'createRemote' || action === 'updateRemote') {
if (action === SyncAction.CreateRemote || action === SyncAction.UpdateRemote) {
let canSync = true;
try {
if (this.testingHooks_.indexOf('notesRejectedByTarget') >= 0 && local.type_ === BaseModel.TYPE_NOTE) throw new JoplinError('Testing rejectedByTarget', 'rejectedByTarget');
@ -766,7 +767,7 @@ export default class Synchronizer {
}
await handleConflictAction(
action as ConflictAction,
action,
ItemClass,
!!remote,
remoteContent,
@ -875,7 +876,7 @@ export default class Synchronizer {
const path = remote.path;
const remoteId = BaseItem.pathToId(path);
let action = null;
let action: SyncAction = null;
let reason = '';
let local = locals.find(l => l.id === remoteId);
let ItemClass = null;
@ -884,7 +885,7 @@ export default class Synchronizer {
try {
if (!local) {
if (remote.isDeleted !== true) {
action = 'createLocal';
action = SyncAction.CreateLocal;
reason = 'remote exists but local does not';
content = await loadContent();
ItemClass = content ? BaseItem.itemClass(content) : null;
@ -893,7 +894,7 @@ export default class Synchronizer {
ItemClass = BaseItem.itemClass(local);
local = ItemClass.filter(local);
if (remote.isDeleted) {
action = 'deleteLocal';
action = SyncAction.DeleteLocal;
reason = 'remote has been deleted';
} else {
if (this.api().supportsAccurateTimestamp && remote.jop_updated_time === local.updated_time) {
@ -901,7 +902,7 @@ export default class Synchronizer {
} else {
content = await loadContent();
if (content && content.updated_time > local.updated_time) {
action = 'updateLocal';
action = SyncAction.UpdateLocal;
reason = 'remote is more recent than local';
}
}
@ -924,7 +925,7 @@ export default class Synchronizer {
this.logSyncOperation(action, local, remote, reason);
if (action === 'createLocal' || action === 'updateLocal') {
if (action === SyncAction.CreateLocal || action === SyncAction.UpdateLocal) {
if (content === null) {
logger.warn(`Remote has been deleted between now and the delta() call? In that case it will be handled during the next sync: ${path}`);
continue;
@ -944,10 +945,10 @@ export default class Synchronizer {
nextQueries: BaseItem.updateSyncTimeQueries(syncTargetId, content, time.unixMs()),
changeSource: ItemChange.SOURCE_SYNC,
};
if (action === 'createLocal') options.isNew = true;
if (action === 'updateLocal') options.oldItem = local;
if (action === SyncAction.CreateLocal) options.isNew = true;
if (action === SyncAction.UpdateLocal) options.oldItem = local;
const creatingOrUpdatingResource = content.type_ === BaseModel.TYPE_RESOURCE && (action === 'createLocal' || action === 'updateLocal');
const creatingOrUpdatingResource = content.type_ === BaseModel.TYPE_RESOURCE && (action === SyncAction.CreateLocal || action === SyncAction.UpdateLocal);
if (creatingOrUpdatingResource) {
if (content.size >= this.maxResourceSize()) {
@ -991,7 +992,7 @@ export default class Synchronizer {
// }
if (content.encryption_applied) this.dispatch({ type: 'SYNC_GOT_ENCRYPTED_ITEM' });
} else if (action === 'deleteLocal') {
} else if (action === SyncAction.DeleteLocal) {
if (local.type_ === BaseModel.TYPE_FOLDER) {
localFoldersToDelete.push(local);
continue;

View File

@ -5,16 +5,18 @@ import ItemChange from '../../../models/ItemChange';
import Note from '../../../models/Note';
import Resource from '../../../models/Resource';
import time from '../../../time';
import { SyncAction, conflictActions } from './types';
const logger = Logger.create('handleConflictAction');
export type ConflictAction = 'itemConflict' | 'noteConflict' | 'resourceConflict';
export default async (action: SyncAction, ItemClass: any, remoteExists: boolean, remoteContent: any, local: any, syncTargetId: number, itemIsReadOnly: boolean, dispatch: Dispatch) => {
if (!conflictActions.includes(action)) return;
export default async (action: ConflictAction, ItemClass: any, remoteExists: boolean, remoteContent: any, local: any, syncTargetId: number, itemIsReadOnly: boolean, dispatch: Dispatch) => {
logger.debug(`Handling conflict: ${action}`);
logger.debug('local:', local, 'remoteContent', remoteContent);
logger.debug('remoteExists:', remoteExists);
if (action === 'itemConflict') {
if (action === SyncAction.ItemConflict) {
// ------------------------------------------------------------------------------
// For non-note conflicts, we take the remote version (i.e. the version that was
// synced first) and overwrite the local content.
@ -31,7 +33,7 @@ export default async (action: ConflictAction, ItemClass: any, remoteExists: bool
trackDeleted: false,
});
}
} else if (action === 'noteConflict') {
} else if (action === SyncAction.NoteConflict) {
// ------------------------------------------------------------------------------
// First find out if the conflict matters. For example, if the conflict is on the title or body
// we want to preserve all the changes. If it's on todo_completed it doesn't really matter
@ -51,7 +53,7 @@ export default async (action: ConflictAction, ItemClass: any, remoteExists: bool
if (mustHandleConflict) {
await Note.createConflictNote(local, ItemChange.SOURCE_SYNC);
}
} else if (action === 'resourceConflict') {
} else if (action === SyncAction.ResourceConflict) {
if (!remoteContent || Resource.mustHandleConflict(local, remoteContent)) {
await Resource.createConflictResourceNote(local);
@ -66,7 +68,7 @@ export default async (action: ConflictAction, ItemClass: any, remoteExists: bool
}
}
if (['noteConflict', 'resourceConflict'].includes(action)) {
if ([SyncAction.NoteConflict, SyncAction.ResourceConflict].includes(action)) {
// ------------------------------------------------------------------------------
// For note and resource conflicts, the creation of the conflict item is done
// differently. However the way the local content is handled is the same.

View File

@ -5,7 +5,7 @@ import ItemChange from '../../../models/ItemChange';
import Resource from '../../../models/Resource';
import time from '../../../time';
import resourceRemotePath from './resourceRemotePath';
import { ApiCallFunction, LogSyncOperationFunction } from './types';
import { ApiCallFunction, LogSyncOperationFunction, SyncAction } from './types';
export default async (syncTargetId: number, cancelling: boolean, logSyncOperation: LogSyncOperationFunction, apiCall: ApiCallFunction, dispatch: Dispatch) => {
const deletedItems = await BaseItem.deletedItems(syncTargetId);
@ -24,7 +24,7 @@ export default async (syncTargetId: number, cancelling: boolean, logSyncOperatio
await apiCall('delete', remoteContentPath);
}
logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
logSyncOperation(SyncAction.DeleteRemote, null, { id: item.item_id }, 'local has been deleted');
} catch (error) {
if (error.code === 'isReadOnly') {
let remoteContent = await apiCall('get', path);

View File

@ -6,6 +6,20 @@ export enum Dirnames {
Temp = 'temp',
}
export type LogSyncOperationFunction = (action: string, local?: any, remote?: RemoteItem, message?: string, actionCount?: number)=> void;
export enum SyncAction {
ItemConflict = 'itemConflict',
NoteConflict = 'noteConflict',
ResourceConflict = 'resourceConflict',
CreateRemote = 'createRemote',
UpdateRemote = 'updateRemote',
DeleteRemote = 'deleteRemote',
CreateLocal = 'createLocal',
UpdateLocal = 'updateLocal',
DeleteLocal = 'deleteLocal',
}
export type LogSyncOperationFunction = (action: SyncAction, local?: any, remote?: RemoteItem, message?: string, actionCount?: number)=> void;
export type ApiCallFunction = (fnName: string, ...args: any[])=> Promise<any>;
export const conflictActions: SyncAction[] = [SyncAction.ItemConflict, SyncAction.NoteConflict, SyncAction.ResourceConflict];