From a6caa357c8df77a77ceedfa74904dd18c0d95e96 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Mon, 3 May 2021 12:55:38 +0200 Subject: [PATCH] Desktop: Add synchronization tools to clear local sync state or data --- .eslintignore | 6 ++ .gitignore | 8 ++- packages/app-cli/tests/Synchronizer.tools.ts | 53 +++++++++++++++ packages/app-cli/tests/test-utils.ts | 15 +++-- .../gui/ConfigScreen/ConfigScreen.tsx | 64 ++++++------------- .../gui/StatusScreen/StatusScreen.tsx | 25 ++++++-- packages/lib/BaseApplication.ts | 16 ++++- packages/lib/Logger.ts | 2 +- packages/lib/SyncTargetRegistry.js | 4 ++ packages/lib/Synchronizer.ts | 2 +- packages/lib/database.ts | 2 +- packages/lib/models/BaseItem.ts | 2 +- packages/lib/models/Setting.ts | 47 +++++++++++++- packages/lib/services/synchronizer/tools.ts | 48 ++++++++++++++ 14 files changed, 229 insertions(+), 65 deletions(-) create mode 100644 packages/app-cli/tests/Synchronizer.tools.ts create mode 100644 packages/lib/services/synchronizer/tools.ts diff --git a/.eslintignore b/.eslintignore index 4c0d38ba02..d7cee13718 100644 --- a/.eslintignore +++ b/.eslintignore @@ -106,6 +106,9 @@ packages/app-cli/tests/Synchronizer.sharing.js.map packages/app-cli/tests/Synchronizer.tags.d.ts packages/app-cli/tests/Synchronizer.tags.js packages/app-cli/tests/Synchronizer.tags.js.map +packages/app-cli/tests/Synchronizer.tools.d.ts +packages/app-cli/tests/Synchronizer.tools.js +packages/app-cli/tests/Synchronizer.tools.js.map packages/app-cli/tests/fsDriver.d.ts packages/app-cli/tests/fsDriver.js packages/app-cli/tests/fsDriver.js.map @@ -1348,6 +1351,9 @@ packages/lib/services/synchronizer/migrations/1.js.map packages/lib/services/synchronizer/migrations/2.d.ts packages/lib/services/synchronizer/migrations/2.js packages/lib/services/synchronizer/migrations/2.js.map +packages/lib/services/synchronizer/tools.d.ts +packages/lib/services/synchronizer/tools.js +packages/lib/services/synchronizer/tools.js.map packages/lib/services/synchronizer/utils/types.d.ts packages/lib/services/synchronizer/utils/types.js packages/lib/services/synchronizer/utils/types.js.map diff --git a/.gitignore b/.gitignore index 82768c69a8..ece6821aa3 100644 --- a/.gitignore +++ b/.gitignore @@ -93,6 +93,9 @@ packages/app-cli/tests/Synchronizer.sharing.js.map packages/app-cli/tests/Synchronizer.tags.d.ts packages/app-cli/tests/Synchronizer.tags.js packages/app-cli/tests/Synchronizer.tags.js.map +packages/app-cli/tests/Synchronizer.tools.d.ts +packages/app-cli/tests/Synchronizer.tools.js +packages/app-cli/tests/Synchronizer.tools.js.map packages/app-cli/tests/fsDriver.d.ts packages/app-cli/tests/fsDriver.js packages/app-cli/tests/fsDriver.js.map @@ -1335,6 +1338,9 @@ packages/lib/services/synchronizer/migrations/1.js.map packages/lib/services/synchronizer/migrations/2.d.ts packages/lib/services/synchronizer/migrations/2.js packages/lib/services/synchronizer/migrations/2.js.map +packages/lib/services/synchronizer/tools.d.ts +packages/lib/services/synchronizer/tools.js +packages/lib/services/synchronizer/tools.js.map packages/lib/services/synchronizer/utils/types.d.ts packages/lib/services/synchronizer/utils/types.js packages/lib/services/synchronizer/utils/types.js.map @@ -1516,5 +1522,3 @@ packages/tools/tool-utils.d.ts packages/tools/tool-utils.js packages/tools/tool-utils.js.map # AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD -packages/app-desktop/gui/getWindowTitle.js -packages/app-desktop/gui/getWindowTitle.test.js diff --git a/packages/app-cli/tests/Synchronizer.tools.ts b/packages/app-cli/tests/Synchronizer.tools.ts new file mode 100644 index 0000000000..0c7ab14a34 --- /dev/null +++ b/packages/app-cli/tests/Synchronizer.tools.ts @@ -0,0 +1,53 @@ +import { allNotesFolders, remoteNotesAndFolders } from './test-utils-synchronizer'; +import { afterAllCleanUp, synchronizerStart, setupDatabaseAndSynchronizer, switchClient, fileApi, db } from './test-utils'; +import Folder from '@joplin/lib/models/Folder'; +import Note from '@joplin/lib/models/Note'; +import { clearLocalDataForRedownload, clearLocalSyncStateForReupload } from '@joplin/lib/services/synchronizer/tools'; + +describe('Synchronizer.tools', function() { + + beforeEach(async (done) => { + await setupDatabaseAndSynchronizer(1); + await setupDatabaseAndSynchronizer(2); + await switchClient(1); + done(); + }); + + afterAll(async () => { + await afterAllCleanUp(); + }); + + it('should clear local sync data, and re-upload everything to sync target', (async () => { + await Folder.save({ title: 'test' }); + + await synchronizerStart(); + + await fileApi().clearRoot(); + + await clearLocalSyncStateForReupload(db()); + + // Now that the local sync state has been cleared, it should re-upload + // the items as if it was a new sync target. It should also not delete* + // any local data. + await synchronizerStart(); + expect((await remoteNotesAndFolders()).length).toBe(1); + expect((await Folder.all()).length).toBe(1); + })); + + it('should clear local data, and re-downlaod everything from sync target', (async () => { + const folder = await Folder.save({ title: 'test' }); + await Note.save({ title: 'test note', parent_id: folder.id }); + + await synchronizerStart(); + + await clearLocalDataForRedownload(db()); + + expect((await allNotesFolders()).length).toBe(0); + + await synchronizerStart(); + + expect((await allNotesFolders()).length).toBe(2); + expect((await remoteNotesAndFolders()).length).toBe(2); + })); + +}); diff --git a/packages/app-cli/tests/test-utils.ts b/packages/app-cli/tests/test-utils.ts index b8eef41cc9..e48a76d35a 100644 --- a/packages/app-cli/tests/test-utils.ts +++ b/packages/app-cli/tests/test-utils.ts @@ -1,7 +1,7 @@ /* eslint-disable require-atomic-updates */ import BaseApplication from '@joplin/lib/BaseApplication'; import BaseModel from '@joplin/lib/BaseModel'; -import Logger, { TargetType, LoggerWrapper } from '@joplin/lib/Logger'; +import Logger, { TargetType, LoggerWrapper, LogLevel } from '@joplin/lib/Logger'; import Setting from '@joplin/lib/models/Setting'; import BaseService from '@joplin/lib/services/BaseService'; import FsDriverNode from '@joplin/lib/fs-driver-node'; @@ -69,7 +69,6 @@ const suiteName_ = uuid.createNano(); const databases_: any[] = []; let synchronizers_: any[] = []; -const synchronizerContexts_: any = {}; const fileApis_: any = {}; const encryptionServices_: any[] = []; const revisionServices_: any[] = []; @@ -168,7 +167,7 @@ dbLogger.setLevel(Logger.LEVEL_WARN); const logger = new Logger(); logger.addTarget(TargetType.Console); -logger.setLevel(Logger.LEVEL_WARN); // Set to DEBUG to display sync process in console +logger.setLevel(LogLevel.Warn); // Set to DEBUG to display sync process in console Logger.initializeGlobalLogger(logger); @@ -390,7 +389,6 @@ async function setupDatabaseAndSynchronizer(id: number, options: any = null) { syncTarget.setFileApi(fileApi()); syncTarget.setLogger(logger); synchronizers_[id] = await syncTarget.synchronizer(); - synchronizerContexts_[id] = null; } encryptionServices_[id] = new EncryptionService(); @@ -420,11 +418,16 @@ function synchronizer(id: number = null) { // the client. async function synchronizerStart(id: number = null, extraOptions: any = null) { if (id === null) id = currentClient_; - const context = synchronizerContexts_[id]; + + const contextKey = `sync.${syncTargetId()}.context`; + const context = Setting.value(contextKey); + const options = Object.assign({}, extraOptions); if (context) options.context = context; const newContext = await synchronizer(id).start(options); - synchronizerContexts_[id] = newContext; + + Setting.setValue(contextKey, JSON.stringify(newContext)); + return newContext; } diff --git a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx index 6bf703405a..31fc132c08 100644 --- a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx @@ -4,7 +4,7 @@ import ButtonBar from './ButtonBar'; import Button, { ButtonLevel } from '../Button/Button'; import { _ } from '@joplin/lib/locale'; import bridge from '../../services/bridge'; -import Setting from '@joplin/lib/models/Setting'; +import Setting, { SyncStartupOperation } from '@joplin/lib/models/Setting'; import control_PluginsStates from './controls/plugins/PluginsStates'; const { connect } = require('react-redux'); @@ -51,6 +51,7 @@ class ConfigScreenComponent extends React.Component { this.renderLabel = this.renderLabel.bind(this); this.renderDescription = this.renderDescription.bind(this); this.renderHeader = this.renderHeader.bind(this); + this.handleSettingButton = this.handleSettingButton.bind(this); } async checkSyncConfig_() { @@ -82,6 +83,22 @@ class ConfigScreenComponent extends React.Component { } } + private async handleSettingButton(key: string) { + if (key === 'sync.clearLocalSyncStateButton') { + if (!confirm('This cannot be undone. Do you want to continue?')) return; + Setting.setValue('sync.startupOperation', SyncStartupOperation.ClearLocalSyncState); + await Setting.saveAll(); + bridge().restart(); + } else if (key === 'sync.clearLocalDataButton') { + if (!confirm('This cannot be undone. Do you want to continue?')) return; + Setting.setValue('sync.startupOperation', SyncStartupOperation.ClearLocalData); + await Setting.saveAll(); + bridge().restart(); + } else { + throw new Error(`Unhandled key: ${key}`); + } + } + sectionByName(name: string) { const sections = shared.settingsSections({ device: 'desktop', settings: this.state.settings }); for (const section of sections) { @@ -202,49 +219,6 @@ class ConfigScreenComponent extends React.Component { ); } - - // if (syncTargetMd.name === 'nextcloud') { - // const syncTarget = settings['sync.5.syncTargets'][settings['sync.5.path']]; - - // let status = _('Unknown'); - // let errorMessage = null; - - // if (this.state.checkNextcloudAppResult === 'checking') { - // status = _('Checking...'); - // } else if (syncTarget) { - // if (syncTarget.uuid) status = _('OK'); - // if (syncTarget.error) { - // status = _('Error'); - // errorMessage = syncTarget.error; - // } - // } - - // const statusComp = !errorMessage || this.state.checkNextcloudAppResult === 'checking' || !this.state.showNextcloudAppLog ? null : ( - //
- //

{_('The Joplin Nextcloud App is either not installed or misconfigured. Please see the full error message below:')}

- //
{errorMessage}
- //
- // ); - - // const showLogButton = !errorMessage || this.state.showNextcloudAppLog ? null : ( - // [{_('Show Log')}] - // ); - - // const appStatusStyle = Object.assign({}, theme.textStyle, { fontWeight: 'bold' }); - - // settingComps.push( - //
- // Beta: {_('Joplin Nextcloud App status:')} {status} - //    - // {showLogButton} - //    - //
- // ); - // } } let advancedSettingsButton = null; @@ -637,7 +611,7 @@ class ConfigScreenComponent extends React.Component {
-