From 0c147236a30c02792c200c1afc3da831609ded9f Mon Sep 17 00:00:00 2001 From: Laurent Date: Sun, 2 Aug 2020 12:28:50 +0100 Subject: [PATCH] All: Add mechanism to lock and upgrade sync targets (#3524) --- .eslintignore | 10 + .gitignore | 10 + .travis.yml | 2 +- CliClient/.gitignore | 2 + CliClient/app/cli-utils.js | 14 + CliClient/app/command-sync.js | 48 +- CliClient/gulpfile.js | 2 + CliClient/package.json | 3 +- CliClient/tests/EnexToHtml.js | 2 - CliClient/tests/EnexToMd.js | 2 - CliClient/tests/HtmlToHtml.js | 2 - CliClient/tests/HtmlToMd.js | 2 - CliClient/tests/MdToHtml.js | 2 - CliClient/tests/file_api_driver.js | 182 ++++---- CliClient/tests/models_Resource.js | 2 - CliClient/tests/services_EncryptionService.js | 2 - CliClient/tests/services_InteropService.js | 2 - .../services_InteropService_Exporter_Md.js | 2 - CliClient/tests/services_KvStore.js | 2 - CliClient/tests/services_ResourceService.js | 2 - CliClient/tests/services_UndoRedoService.js | 2 - CliClient/tests/services_rest_Api.js | 2 - CliClient/tests/support/jasmine.json | 2 +- .../5fcd3813d8ec4fb29d2a9d08da81a0a0 | 1 + .../bda6d120223140afbe7f03ef1d876400 | 1 + .../2/e2ee/.sync/readme.txt | 1 + .../2/e2ee/.sync/version.txt | 1 + .../e2ee/04c4e932fe3c4c4a9450c09208bd6c21.md | 24 + .../e2ee/26b9c0dc3ff146ed99031e259bc1240b.md | 24 + .../e2ee/3a8eaf72f62847689176a952a0b321a0.md | 24 + .../e2ee/3d675395b5cd4d1e9d7ca4f045f41493.md | 10 + .../e2ee/49e1777d9c17439fb612cce85700d16d.md | 24 + .../e2ee/51799b7f7fda4bff84954305f707ca72.md | 10 + .../e2ee/5fcd3813d8ec4fb29d2a9d08da81a0a0.md | 14 + .../e2ee/673415563b2f4db2aae8665ddf9fbc67.md | 10 + .../e2ee/6d05a0cf086043129720b5da35210efd.md | 11 + .../e2ee/7ac37541e8404239babdaf1d3fa39c90.md | 24 + .../e2ee/8a17074d4ec24de7b5a1aa666f7d8b38.md | 10 + .../e2ee/8d4de93d82e2468baa8e539d10d67510.md | 10 + .../e2ee/a1a0987e82cc400c90582492f814c23c.md | 8 + .../e2ee/a2fc1b9ae0d04c8bbeab6d299d0193cc.md | 10 + .../e2ee/a807b6e7d6594567934938dbfcd2bcdf.md | 11 + .../e2ee/aa7dca873bdc47beaa9465e04610619d.md | 10 + .../e2ee/bda6d120223140afbe7f03ef1d876400.md | 14 + .../e2ee/ce59e12313e84ae299b6166068755253.md | 11 + .../e2ee/ed20d91ef4e64fc0910088112c077188.md | 11 + .../e2ee/f58c1af04627410da55d9c771b28bece.md | 11 + .../syncTargetSnapshots/2/e2ee/info.json | 1 + .../006a89df4de64a22b4b1fa71f87fd258 | Bin 0 -> 2720 bytes .../6f60ca35b0e4423fb49f9e097449fd99 | Bin 0 -> 2720 bytes .../2/normal/.sync/readme.txt | 1 + .../2/normal/.sync/version.txt | 1 + .../006a89df4de64a22b4b1fa71f87fd258.md | 16 + .../106ec766eba54715b19dc899e1de6906.md | 11 + .../23d35df6c34848ec86c42c3194051ecc.md | 11 + .../2a914b3fb8fb43819b976eb4e5be80e3.md | 28 ++ .../2fa39884ba3b47a489dae93dc20021f2.md | 12 + .../352dcd65cd6e4b09a93669378b8e2b50.md | 12 + .../38341af5a8764d4d9318f58f778e3240.md | 11 + .../40f117103de1405586b4289a55e0ea22.md | 12 + .../45867f53ece54da38eed83288882e374.md | 26 ++ .../50fdc4447c334b00a4dde44344aceb25.md | 11 + .../567486477f4249d38feadf6c5ec6e03d.md | 11 + .../6cb91bb296ee458589eea0256ada06fa.md | 12 + .../6f60ca35b0e4423fb49f9e097449fd99.md | 16 + .../7ae4083db8e64328a4d3ccab6279c6bf.md | 12 + .../a91bf5ddf3a749d2be010e9a04e5a1cc.md | 26 ++ .../b684a65012c74c508b891935ecf2f5b1.md | 12 + .../bf551517ef7c40be9477168677d0b77a.md | 28 ++ .../c4e45cadb2e84beb801980155a707e21.md | 12 + .../edd3cb394ada4d389c01c2bdca09b3ef.md | 26 ++ .../syncTargetSnapshots/2/normal/info.json | 1 + CliClient/tests/support/syncTargetUtils.js | 13 +- CliClient/tests/synchronizer.js | 388 ++++++++-------- CliClient/tests/synchronizer_LockHandler.ts | 201 +++++++++ .../tests/synchronizer_MigrationHandler.ts | 159 +++++++ CliClient/tests/test-utils.js | 173 +++++-- ElectronClient/app.js | 41 +- ElectronClient/bridge.js | 9 + ElectronClient/gui/MainScreen/MainScreen.jsx | 22 +- ElectronClient/gui/Root_UpgradeSyncTarget.tsx | 102 +++++ ElectronClient/main-html.js | 8 +- ElectronClient/package-lock.json | 6 + ElectronClient/package.json | 1 + README.md | 7 +- ReactNativeClient/lib/BaseSyncTarget.js | 4 - ReactNativeClient/lib/SyncTargetOneDrive.js | 13 +- .../lib/components/screen-header.js | 2 + .../screens/UpgradeSyncTargetScreen.tsx | 72 +++ .../lib/file-api-driver-memory.js | 2 +- .../lib/file-api-driver-onedrive.js | 23 +- .../lib/file-api-driver-webdav.js | 24 +- ReactNativeClient/lib/file-api.js | 10 + ReactNativeClient/lib/models/Setting.js | 12 +- ReactNativeClient/lib/onedrive-api.js | 5 +- ReactNativeClient/lib/parameters.js | 15 +- ReactNativeClient/lib/services/BaseService.js | 5 + .../lib/services/ResourceFetcher.js | 5 +- .../lib/services/synchronizer/LockHandler.ts | 348 +++++++++++++++ .../services/synchronizer/MigrationHandler.ts | 141 ++++++ .../synchronizer/gui/useSyncTargetUpgrade.ts | 50 +++ .../lib/services/synchronizer/migrations/1.ts | 9 + .../lib/services/synchronizer/migrations/2.ts | 10 + .../lib/services/synchronizer/utils/types.ts | 6 + ReactNativeClient/lib/synchronizer.js | 177 ++++---- ReactNativeClient/root.js | 2 + Tools/build-website.js | 19 +- docs/api/index.html | 12 +- docs/changelog/index.html | 12 +- docs/changelog_cli/index.html | 12 +- docs/clipper/index.html | 12 +- docs/conflict/index.html | 12 +- docs/debugging/index.html | 12 +- docs/desktop/index.html | 12 +- docs/donate/index.html | 12 +- docs/e2ee/index.html | 14 +- docs/faq/index.html | 12 +- docs/gsoc2020/ideas/index.html | 12 +- docs/gsoc2020/index.html | 12 +- docs/gsod2020/ideas/index.html | 12 +- docs/gsod2020/index.html | 12 +- docs/index.html | 12 +- docs/markdown/index.html | 12 +- docs/mobile/index.html | 12 +- docs/nextcloud_app/index.html | 12 +- docs/prereleases/index.html | 12 +- docs/spec/e2ee/index.html | 393 ++++++++++++++++ docs/spec/history/index.html | 421 ++++++++++++++++++ docs/spec/{ => sync_lock}/index.html | 168 +++---- docs/stats/index.html | 12 +- docs/terminal/index.html | 12 +- joplin.code-workspace | 9 +- package-lock.json | 37 +- package.json | 3 + readme/e2ee.md | 2 +- readme/spec/e2ee.md | 0 readme/spec/sync_lock.md | 63 +++ tsconfig.json | 4 + 138 files changed, 3686 insertions(+), 647 deletions(-) create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/.resource/5fcd3813d8ec4fb29d2a9d08da81a0a0 create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/.resource/bda6d120223140afbe7f03ef1d876400 create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/.sync/readme.txt create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/.sync/version.txt create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/04c4e932fe3c4c4a9450c09208bd6c21.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/26b9c0dc3ff146ed99031e259bc1240b.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/3a8eaf72f62847689176a952a0b321a0.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/3d675395b5cd4d1e9d7ca4f045f41493.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/49e1777d9c17439fb612cce85700d16d.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/51799b7f7fda4bff84954305f707ca72.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/5fcd3813d8ec4fb29d2a9d08da81a0a0.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/673415563b2f4db2aae8665ddf9fbc67.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/6d05a0cf086043129720b5da35210efd.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/7ac37541e8404239babdaf1d3fa39c90.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/8a17074d4ec24de7b5a1aa666f7d8b38.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/8d4de93d82e2468baa8e539d10d67510.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/a1a0987e82cc400c90582492f814c23c.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/a2fc1b9ae0d04c8bbeab6d299d0193cc.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/a807b6e7d6594567934938dbfcd2bcdf.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/aa7dca873bdc47beaa9465e04610619d.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/bda6d120223140afbe7f03ef1d876400.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/ce59e12313e84ae299b6166068755253.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/ed20d91ef4e64fc0910088112c077188.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/f58c1af04627410da55d9c771b28bece.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/e2ee/info.json create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/.resource/006a89df4de64a22b4b1fa71f87fd258 create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/.resource/6f60ca35b0e4423fb49f9e097449fd99 create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/.sync/readme.txt create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/.sync/version.txt create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/006a89df4de64a22b4b1fa71f87fd258.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/106ec766eba54715b19dc899e1de6906.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/23d35df6c34848ec86c42c3194051ecc.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/2a914b3fb8fb43819b976eb4e5be80e3.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/2fa39884ba3b47a489dae93dc20021f2.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/352dcd65cd6e4b09a93669378b8e2b50.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/38341af5a8764d4d9318f58f778e3240.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/40f117103de1405586b4289a55e0ea22.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/45867f53ece54da38eed83288882e374.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/50fdc4447c334b00a4dde44344aceb25.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/567486477f4249d38feadf6c5ec6e03d.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/6cb91bb296ee458589eea0256ada06fa.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/6f60ca35b0e4423fb49f9e097449fd99.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/7ae4083db8e64328a4d3ccab6279c6bf.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/a91bf5ddf3a749d2be010e9a04e5a1cc.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/b684a65012c74c508b891935ecf2f5b1.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/bf551517ef7c40be9477168677d0b77a.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/c4e45cadb2e84beb801980155a707e21.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/edd3cb394ada4d389c01c2bdca09b3ef.md create mode 100644 CliClient/tests/support/syncTargetSnapshots/2/normal/info.json create mode 100644 CliClient/tests/synchronizer_LockHandler.ts create mode 100644 CliClient/tests/synchronizer_MigrationHandler.ts create mode 100644 ElectronClient/gui/Root_UpgradeSyncTarget.tsx create mode 100644 ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.tsx create mode 100644 ReactNativeClient/lib/services/synchronizer/LockHandler.ts create mode 100644 ReactNativeClient/lib/services/synchronizer/MigrationHandler.ts create mode 100644 ReactNativeClient/lib/services/synchronizer/gui/useSyncTargetUpgrade.ts create mode 100644 ReactNativeClient/lib/services/synchronizer/migrations/1.ts create mode 100644 ReactNativeClient/lib/services/synchronizer/migrations/2.ts create mode 100644 ReactNativeClient/lib/services/synchronizer/utils/types.ts create mode 100644 docs/spec/e2ee/index.html create mode 100644 docs/spec/history/index.html rename docs/spec/{ => sync_lock}/index.html (58%) create mode 100644 readme/spec/e2ee.md create mode 100644 readme/spec/sync_lock.md diff --git a/.eslintignore b/.eslintignore index a1938b9eb..319d580dd 100644 --- a/.eslintignore +++ b/.eslintignore @@ -61,6 +61,8 @@ Modules/TinyMCE/IconPack/postinstall.js Modules/TinyMCE/langs/ # AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD +CliClient/tests/synchronizer_LockHandler.js +CliClient/tests/synchronizer_MigrationHandler.js CliClient/app/LinkSelector.js CliClient/build/LinkSelector.js ElectronClient/commands/focusElement.js @@ -133,6 +135,7 @@ ElectronClient/gui/NoteList/commands/focusElementNoteList.js ElectronClient/gui/NoteListItem.js ElectronClient/gui/NoteToolbar/NoteToolbar.js ElectronClient/gui/ResourceScreen.js +ElectronClient/gui/Root_UpgradeSyncTarget.js ElectronClient/gui/ShareNoteDialog.js ElectronClient/gui/SideBar/commands/focusElementSideBar.js ReactNativeClient/lib/AsyncActionQueue.js @@ -140,6 +143,7 @@ ReactNativeClient/lib/checkPermissions.js ReactNativeClient/lib/commands/historyBackward.js ReactNativeClient/lib/commands/historyForward.js ReactNativeClient/lib/commands/synchronize.js +ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js ReactNativeClient/lib/hooks/useEffectDebugger.js ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.js ReactNativeClient/lib/hooks/usePrevious.js @@ -161,6 +165,12 @@ ReactNativeClient/lib/services/ResourceEditWatcher/reducer.js ReactNativeClient/lib/services/rest/actionApi.desktop.js ReactNativeClient/lib/services/rest/errors.js ReactNativeClient/lib/services/SettingUtils.js +ReactNativeClient/lib/services/synchronizer/gui/useSyncTargetUpgrade.js +ReactNativeClient/lib/services/synchronizer/LockHandler.js +ReactNativeClient/lib/services/synchronizer/MigrationHandler.js +ReactNativeClient/lib/services/synchronizer/migrations/1.js +ReactNativeClient/lib/services/synchronizer/migrations/2.js +ReactNativeClient/lib/services/synchronizer/utils/types.js ReactNativeClient/lib/services/UndoRedoService.js ReactNativeClient/lib/ShareExtension.js ReactNativeClient/lib/shareHandler.js diff --git a/.gitignore b/.gitignore index 5d4ea617b..82ce24b79 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,8 @@ Tools/commit_hook.txt *.map # AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD +CliClient/tests/synchronizer_LockHandler.js +CliClient/tests/synchronizer_MigrationHandler.js CliClient/app/LinkSelector.js CliClient/build/LinkSelector.js ElectronClient/commands/focusElement.js @@ -124,6 +126,7 @@ ElectronClient/gui/NoteList/commands/focusElementNoteList.js ElectronClient/gui/NoteListItem.js ElectronClient/gui/NoteToolbar/NoteToolbar.js ElectronClient/gui/ResourceScreen.js +ElectronClient/gui/Root_UpgradeSyncTarget.js ElectronClient/gui/ShareNoteDialog.js ElectronClient/gui/SideBar/commands/focusElementSideBar.js ReactNativeClient/lib/AsyncActionQueue.js @@ -131,6 +134,7 @@ ReactNativeClient/lib/checkPermissions.js ReactNativeClient/lib/commands/historyBackward.js ReactNativeClient/lib/commands/historyForward.js ReactNativeClient/lib/commands/synchronize.js +ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js ReactNativeClient/lib/hooks/useEffectDebugger.js ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.js ReactNativeClient/lib/hooks/usePrevious.js @@ -152,6 +156,12 @@ ReactNativeClient/lib/services/ResourceEditWatcher/reducer.js ReactNativeClient/lib/services/rest/actionApi.desktop.js ReactNativeClient/lib/services/rest/errors.js ReactNativeClient/lib/services/SettingUtils.js +ReactNativeClient/lib/services/synchronizer/gui/useSyncTargetUpgrade.js +ReactNativeClient/lib/services/synchronizer/LockHandler.js +ReactNativeClient/lib/services/synchronizer/MigrationHandler.js +ReactNativeClient/lib/services/synchronizer/migrations/1.js +ReactNativeClient/lib/services/synchronizer/migrations/2.js +ReactNativeClient/lib/services/synchronizer/utils/types.js ReactNativeClient/lib/services/UndoRedoService.js ReactNativeClient/lib/ShareExtension.js ReactNativeClient/lib/shareHandler.js diff --git a/.travis.yml b/.travis.yml index 7d55aa43a..7b8c6cb15 100644 --- a/.travis.yml +++ b/.travis.yml @@ -69,7 +69,7 @@ script: # and that would break the desktop release. if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then cd CliClient - npm run test + npm run test-ci testResult=$? if [ $testResult -ne 0 ]; then exit $testResult diff --git a/CliClient/.gitignore b/CliClient/.gitignore index 9c303b4fd..71d0d0f9c 100644 --- a/CliClient/.gitignore +++ b/CliClient/.gitignore @@ -20,4 +20,6 @@ out.txt linkToLocal.sh yarn-error.log tests/support/dropbox-auth.txt +tests/support/nextcloud-auth.json +tests/support/onedrive-auth.txt build/ \ No newline at end of file diff --git a/CliClient/app/cli-utils.js b/CliClient/app/cli-utils.js index 40d072613..2e25b7ac6 100644 --- a/CliClient/app/cli-utils.js +++ b/CliClient/app/cli-utils.js @@ -2,6 +2,7 @@ const yargParser = require('yargs-parser'); const { _ } = require('lib/locale.js'); const { time } = require('lib/time-utils.js'); const stringPadding = require('string-padding'); +const { Logger } = require('lib/logger.js'); const cliUtils = {}; @@ -245,4 +246,17 @@ cliUtils.redrawDone = function() { redrawStarted_ = false; }; +cliUtils.stdoutLogger = function(stdout) { + const stdoutFn = (...s) => stdout(s.join(' ')); + + const logger = new Logger(); + logger.addTarget('console', { console: { + info: stdoutFn, + warn: stdoutFn, + error: stdoutFn, + } }); + + return logger; +}; + module.exports = { cliUtils }; diff --git a/CliClient/app/command-sync.js b/CliClient/app/command-sync.js index d18050651..cabe6e514 100644 --- a/CliClient/app/command-sync.js +++ b/CliClient/app/command-sync.js @@ -11,6 +11,7 @@ const md5 = require('md5'); const locker = require('proper-lockfile'); const fs = require('fs-extra'); const SyncTargetRegistry = require('lib/SyncTargetRegistry'); +const MigrationHandler = require('lib/services/synchronizer/MigrationHandler').default; class Command extends BaseCommand { constructor() { @@ -29,7 +30,10 @@ class Command extends BaseCommand { } options() { - return [['--target ', _('Sync to provided target (defaults to sync.target config value)')]]; + return [ + ['--target ', _('Sync to provided target (defaults to sync.target config value)')], + ['--upgrade', _('Upgrade the sync target to the latest version.')], + ]; } static lockFile(filePath) { @@ -148,12 +152,8 @@ class Command extends BaseCommand { const syncTarget = reg.syncTarget(this.syncTargetId_); if (!(await syncTarget.isAuthenticated())) { - app() - .gui() - .showConsole(); - app() - .gui() - .maximizeConsole(); + app().gui().showConsole(); + app().gui().maximizeConsole(); const authDone = await this.doAuth(); if (!authDone) return cleanUp(); @@ -176,6 +176,34 @@ class Command extends BaseCommand { if (!sync) throw new Error(_('Cannot initialise synchroniser.')); + if (args.options.upgrade) { + let migrationError = null; + + try { + const migrationHandler = new MigrationHandler( + sync.api(), + sync.lockHandler(), + Setting.value('appType'), + Setting.value('clientId') + ); + + migrationHandler.setLogger(cliUtils.stdoutLogger(this.stdout.bind(this))); + + await migrationHandler.upgrade(); + } catch (error) { + migrationError = error; + } + + if (!migrationError) { + Setting.setValue('sync.upgradeState', Setting.SYNC_UPGRADE_STATE_IDLE); + await Setting.saveAll(); + } + + if (migrationError) throw migrationError; + + return cleanUp(); + } + this.stdout(_('Starting synchronisation...')); const contextKey = `sync.${this.syncTargetId_}.context`; @@ -210,6 +238,12 @@ class Command extends BaseCommand { throw error; } + if (Setting.value('sync.upgradeState') > Setting.SYNC_UPGRADE_STATE_IDLE) { + this.stdout(`/!\\ ${_('Sync target must be upgraded! Run `%s` to proceed.', 'sync --upgrade')}`); + app().gui().showConsole(); + app().gui().maximizeConsole(); + } + cleanUp(); } diff --git a/CliClient/gulpfile.js b/CliClient/gulpfile.js index 55d2c4509..1018f6531 100644 --- a/CliClient/gulpfile.js +++ b/CliClient/gulpfile.js @@ -38,6 +38,8 @@ tasks.prepareTestBuild = { 'lib/', 'locales/', 'node_modules/', + '*.ts', + '*.tsx', ], }); diff --git a/CliClient/package.json b/CliClient/package.json index 752f3b11d..76068596a 100644 --- a/CliClient/package.json +++ b/CliClient/package.json @@ -4,7 +4,8 @@ "license": "MIT", "author": "Laurent Cozic", "scripts": { - "test": "gulp buildTests -L && node node_modules/jasmine/bin/jasmine.js --config=tests/support/jasmine.json", + "test": "gulp buildTests -L && node node_modules/jasmine/bin/jasmine.js --fail-fast=true --config=tests/support/jasmine.json", + "test-ci": "gulp buildTests -L && node node_modules/jasmine/bin/jasmine.js --config=tests/support/jasmine.json", "postinstall": "npm run build && patch-package --patch-dir ../patches", "build": "gulp build", "start": "gulp build -L && node 'build/main.js' --stack-trace-enabled --log-level debug --env dev" diff --git a/CliClient/tests/EnexToHtml.js b/CliClient/tests/EnexToHtml.js index d9d28f69e..32d97d245 100644 --- a/CliClient/tests/EnexToHtml.js +++ b/CliClient/tests/EnexToHtml.js @@ -4,8 +4,6 @@ const { asyncTest, setupDatabaseAndSynchronizer, switchClient } = require('test- const { shim } = require('lib/shim'); const { enexXmlToHtml } = require('lib/import-enex-html-gen.js'); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 60 * 60 * 1000; // Can run for a while since everything is in the same test unit - process.on('unhandledRejection', (reason, p) => { console.warn('Unhandled Rejection at: Promise', p, 'reason:', reason); }); diff --git a/CliClient/tests/EnexToMd.js b/CliClient/tests/EnexToMd.js index 58fed5e2b..6328eb65b 100644 --- a/CliClient/tests/EnexToMd.js +++ b/CliClient/tests/EnexToMd.js @@ -12,8 +12,6 @@ const BaseModel = require('lib/BaseModel.js'); const { shim } = require('lib/shim'); const { enexXmlToMd } = require('lib/import-enex-md-gen.js'); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 60 * 60 * 1000; // Can run for a while since everything is in the same test unit - process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); }); diff --git a/CliClient/tests/HtmlToHtml.js b/CliClient/tests/HtmlToHtml.js index da614190a..8d55eca73 100644 --- a/CliClient/tests/HtmlToHtml.js +++ b/CliClient/tests/HtmlToHtml.js @@ -13,8 +13,6 @@ const { shim } = require('lib/shim'); const HtmlToHtml = require('lib/joplin-renderer/HtmlToHtml'); const { enexXmlToMd } = require('lib/import-enex-md-gen.js'); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 60 * 60 * 1000; // Can run for a while since everything is in the same test unit - process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); }); diff --git a/CliClient/tests/HtmlToMd.js b/CliClient/tests/HtmlToMd.js index 5d8ded29c..1a747a1fe 100644 --- a/CliClient/tests/HtmlToMd.js +++ b/CliClient/tests/HtmlToMd.js @@ -13,8 +13,6 @@ const { shim } = require('lib/shim'); const HtmlToMd = require('lib/HtmlToMd'); const { enexXmlToMd } = require('lib/import-enex-md-gen.js'); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 60 * 60 * 1000; // Can run for a while since everything is in the same test unit - process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); }); diff --git a/CliClient/tests/MdToHtml.js b/CliClient/tests/MdToHtml.js index 3fd306342..667d92d9d 100644 --- a/CliClient/tests/MdToHtml.js +++ b/CliClient/tests/MdToHtml.js @@ -14,8 +14,6 @@ const MdToHtml = require('lib/joplin-renderer/MdToHtml'); const { enexXmlToMd } = require('lib/import-enex-md-gen.js'); const { themeStyle } = require('lib/theme'); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 60 * 60 * 1000; // Can run for a while since everything is in the same test unit - process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); }); diff --git a/CliClient/tests/file_api_driver.js b/CliClient/tests/file_api_driver.js index 2d714500a..dac48db61 100644 --- a/CliClient/tests/file_api_driver.js +++ b/CliClient/tests/file_api_driver.js @@ -9,118 +9,126 @@ const { shim } = require('lib/shim.js'); const fs = require('fs-extra'); const Setting = require('lib/models/Setting.js'); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); }); -let api = null; +const api = null; + +// NOTE: These tests work with S3 and memory driver, but not +// with other targets like file system or Nextcloud. +// All this is tested in an indirect way in tests/synchronizer +// anyway. +// We keep the file here as it could be useful as a spec for +// what calls a sync target should support, but it would +// need to be fixed first. + + // To test out an FileApi implementation: // * add a SyncTarget for your driver in `test-utils.js` // * set `syncTargetId_` to your New SyncTarget: // `const syncTargetId_ = SyncTargetRegistry.nameToId('memory');` -describe('fileApi', function() { +// describe('fileApi', function() { - beforeEach(async (done) => { - api = new fileApi(); - api.clearRoot(); - done(); - }); +// beforeEach(async (done) => { +// api = new fileApi(); +// api.clearRoot(); +// done(); +// }); - describe('list', function() { - it('should return items with relative path', asyncTest(async () => { - await api.mkdir('.subfolder'); - await api.put('1', 'something on root 1'); - await api.put('.subfolder/1', 'something subfolder 1'); - await api.put('.subfolder/2', 'something subfolder 2'); - await api.put('.subfolder/3', 'something subfolder 3'); - sleep(0.8); +// describe('list', function() { +// it('should return items with relative path', asyncTest(async () => { +// await api.mkdir('.subfolder'); +// await api.put('1', 'something on root 1'); +// await api.put('.subfolder/1', 'something subfolder 1'); +// await api.put('.subfolder/2', 'something subfolder 2'); +// await api.put('.subfolder/3', 'something subfolder 3'); +// sleep(0.8); - const response = await api.list('.subfolder'); - const items = response.items; - expect(items.length).toBe(3); - expect(items[0].path).toBe('1'); - })); +// const response = await api.list('.subfolder'); +// const items = response.items; +// expect(items.length).toBe(3); +// expect(items[0].path).toBe('1'); +// })); - it('should default to only files on root directory', asyncTest(async () => { - await api.mkdir('.subfolder'); - await api.put('.subfolder/1', 'something subfolder 1'); - await api.put('file1', 'something 1'); - await api.put('file2', 'something 2'); - sleep(0.6); +// it('should default to only files on root directory', asyncTest(async () => { +// await api.mkdir('.subfolder'); +// await api.put('.subfolder/1', 'something subfolder 1'); +// await api.put('file1', 'something 1'); +// await api.put('file2', 'something 2'); +// sleep(0.6); - const response = await api.list(); - expect(response.items.length).toBe(2); - })); - }); // list +// const response = await api.list(); +// expect(response.items.length).toBe(2); +// })); +// }); // list - describe('delete', function() { - it('should not error if file does not exist', asyncTest(async () => { - const hasThrown = await checkThrowAsync(async () => await api.delete('nonexistant_file')); - expect(hasThrown).toBe(false); - })); +// describe('delete', function() { +// it('should not error if file does not exist', asyncTest(async () => { +// const hasThrown = await checkThrowAsync(async () => await api.delete('nonexistant_file')); +// expect(hasThrown).toBe(false); +// })); - it('should delete specific file given full path', asyncTest(async () => { - await api.mkdir('deleteDir'); - await api.put('deleteDir/1', 'something 1'); - await api.put('deleteDir/2', 'something 2'); - sleep(0.4); +// it('should delete specific file given full path', asyncTest(async () => { +// await api.mkdir('deleteDir'); +// await api.put('deleteDir/1', 'something 1'); +// await api.put('deleteDir/2', 'something 2'); +// sleep(0.4); - await api.delete('deleteDir/1'); - let response = await api.list('deleteDir'); - expect(response.items.length).toBe(1); - response = await api.list('deleteDir/1'); - expect(response.items.length).toBe(0); - })); - }); // delete +// await api.delete('deleteDir/1'); +// let response = await api.list('deleteDir'); +// expect(response.items.length).toBe(1); +// response = await api.list('deleteDir/1'); +// expect(response.items.length).toBe(0); +// })); +// }); // delete - describe('get', function() { - it('should return null if object does not exist', asyncTest(async () => { - const response = await api.get('nonexistant_file'); - expect(response).toBe(null); - })); +// describe('get', function() { +// it('should return null if object does not exist', asyncTest(async () => { +// const response = await api.get('nonexistant_file'); +// expect(response).toBe(null); +// })); - it('should return UTF-8 encoded string by default', asyncTest(async () => { - await api.put('testnote.md', 'something 2'); +// it('should return UTF-8 encoded string by default', asyncTest(async () => { +// await api.put('testnote.md', 'something 2'); - const response = await api.get('testnote.md'); - expect(response).toBe('something 2'); - })); +// const response = await api.get('testnote.md'); +// expect(response).toBe('something 2'); +// })); - it('should return a Response object and writes file to options.path, if options.target is "file"', asyncTest(async () => { - const localFilePath = `${Setting.value('tempDir')}/${uuid.create()}.md`; - await api.put('testnote.md', 'something 2'); - sleep(0.2); +// it('should return a Response object and writes file to options.path, if options.target is "file"', asyncTest(async () => { +// const localFilePath = `${Setting.value('tempDir')}/${uuid.create()}.md`; +// await api.put('testnote.md', 'something 2'); +// sleep(0.2); - const response = await api.get('testnote.md', { target: 'file', path: localFilePath }); - expect(typeof response).toBe('object'); - // expect(response.path).toBe(localFilePath); - expect(fs.existsSync(localFilePath)).toBe(true); - expect(fs.readFileSync(localFilePath, 'utf8')).toBe('something 2'); - })); - }); // get +// const response = await api.get('testnote.md', { target: 'file', path: localFilePath }); +// expect(typeof response).toBe('object'); +// // expect(response.path).toBe(localFilePath); +// expect(fs.existsSync(localFilePath)).toBe(true); +// expect(fs.readFileSync(localFilePath, 'utf8')).toBe('something 2'); +// })); +// }); // get - describe('put', function() { - it('should create file to remote path and content', asyncTest(async () => { - await api.put('putTest.md', 'I am your content'); - sleep(0.2); +// describe('put', function() { +// it('should create file to remote path and content', asyncTest(async () => { +// await api.put('putTest.md', 'I am your content'); +// sleep(0.2); - const response = await api.get('putTest.md'); - expect(response).toBe('I am your content'); - })); +// const response = await api.get('putTest.md'); +// expect(response).toBe('I am your content'); +// })); - it('should upload file in options.path to remote path, if options.source is "file"', asyncTest(async () => { - const localFilePath = `${Setting.value('tempDir')}/${uuid.create()}.md`; - fs.writeFileSync(localFilePath, 'I am the local file.'); +// it('should upload file in options.path to remote path, if options.source is "file"', asyncTest(async () => { +// const localFilePath = `${Setting.value('tempDir')}/${uuid.create()}.md`; +// fs.writeFileSync(localFilePath, 'I am the local file.'); - await api.put('testfile', 'ignore me', { source: 'file', path: localFilePath }); - sleep(0.2); +// await api.put('testfile', 'ignore me', { source: 'file', path: localFilePath }); +// sleep(0.2); - const response = await api.get('testfile'); - expect(response).toBe('I am the local file.'); - })); - }); // put +// const response = await api.get('testfile'); +// expect(response).toBe('I am the local file.'); +// })); +// }); // put -}); +// }); diff --git a/CliClient/tests/models_Resource.js b/CliClient/tests/models_Resource.js index 3c693bb4d..3f8015491 100644 --- a/CliClient/tests/models_Resource.js +++ b/CliClient/tests/models_Resource.js @@ -10,8 +10,6 @@ const Resource = require('lib/models/Resource.js'); const BaseModel = require('lib/BaseModel.js'); const { shim } = require('lib/shim'); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; // The first test is slow because the database needs to be built - process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); }); diff --git a/CliClient/tests/services_EncryptionService.js b/CliClient/tests/services_EncryptionService.js index 6871b1069..a6fe4c0ce 100644 --- a/CliClient/tests/services_EncryptionService.js +++ b/CliClient/tests/services_EncryptionService.js @@ -19,8 +19,6 @@ process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); }); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; // The first test is slow because the database needs to be built - let service = null; describe('services_EncryptionService', function() { diff --git a/CliClient/tests/services_InteropService.js b/CliClient/tests/services_InteropService.js index eeec0d446..166241a33 100644 --- a/CliClient/tests/services_InteropService.js +++ b/CliClient/tests/services_InteropService.js @@ -15,8 +15,6 @@ const ArrayUtils = require('lib/ArrayUtils'); const ObjectUtils = require('lib/ObjectUtils'); const { shim } = require('lib/shim.js'); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); }); diff --git a/CliClient/tests/services_InteropService_Exporter_Md.js b/CliClient/tests/services_InteropService_Exporter_Md.js index 5b4ca14d1..09f5de7c1 100644 --- a/CliClient/tests/services_InteropService_Exporter_Md.js +++ b/CliClient/tests/services_InteropService_Exporter_Md.js @@ -11,8 +11,6 @@ const Resource = require('lib/models/Resource.js'); const Note = require('lib/models/Note.js'); const { shim } = require('lib/shim.js'); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - const exportDir = `${__dirname}/export`; process.on('unhandledRejection', (reason, p) => { diff --git a/CliClient/tests/services_KvStore.js b/CliClient/tests/services_KvStore.js index 5c1e461f3..59111f3a5 100644 --- a/CliClient/tests/services_KvStore.js +++ b/CliClient/tests/services_KvStore.js @@ -9,8 +9,6 @@ process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); }); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - function setupStore() { const store = KvStore.instance(); store.setDb(db()); diff --git a/CliClient/tests/services_ResourceService.js b/CliClient/tests/services_ResourceService.js index b9ed02b76..149fa8273 100644 --- a/CliClient/tests/services_ResourceService.js +++ b/CliClient/tests/services_ResourceService.js @@ -23,8 +23,6 @@ process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); }); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - function exportDir() { return `${__dirname}/export`; } diff --git a/CliClient/tests/services_UndoRedoService.js b/CliClient/tests/services_UndoRedoService.js index ae8ca5935..54cd7c411 100644 --- a/CliClient/tests/services_UndoRedoService.js +++ b/CliClient/tests/services_UndoRedoService.js @@ -10,8 +10,6 @@ // console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); // }); -// jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - // describe('services_UndoRedoService', function() { // beforeEach(async (done) => { diff --git a/CliClient/tests/services_rest_Api.js b/CliClient/tests/services_rest_Api.js index 8a16ccc93..05590d9e2 100644 --- a/CliClient/tests/services_rest_Api.js +++ b/CliClient/tests/services_rest_Api.js @@ -11,8 +11,6 @@ const Tag = require('lib/models/Tag'); const NoteTag = require('lib/models/NoteTag'); const { shim } = require('lib/shim'); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); }); diff --git a/CliClient/tests/support/jasmine.json b/CliClient/tests/support/jasmine.json index 58403b8ba..2a2a922b9 100644 --- a/CliClient/tests/support/jasmine.json +++ b/CliClient/tests/support/jasmine.json @@ -4,6 +4,6 @@ "*.js", "!test-utils.js" ], - "stopSpecOnExpectationFailure": true, + "stopSpecOnExpectationFailure": false, "random": true } diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/.resource/5fcd3813d8ec4fb29d2a9d08da81a0a0 b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/.resource/5fcd3813d8ec4fb29d2a9d08da81a0a0 new file mode 100644 index 000000000..c607c097a --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/.resource/5fcd3813d8ec4fb29d2a9d08da81a0a0 @@ -0,0 +1 @@ +JED0100002205a1a0987e82cc400c90582492f814c23c00137c{"iv":"4qbO48bUQNxEbyYaMlBjhA==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"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"} \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/.resource/bda6d120223140afbe7f03ef1d876400 b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/.resource/bda6d120223140afbe7f03ef1d876400 new file mode 100644 index 000000000..aa3cc5cd4 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/.resource/bda6d120223140afbe7f03ef1d876400 @@ -0,0 +1 @@ +JED0100002205a1a0987e82cc400c90582492f814c23c00137c{"iv":"MXV+gJwY5jpGvZjFBN0asg==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"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"} \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/.sync/readme.txt b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/.sync/readme.txt new file mode 100644 index 000000000..e46819c44 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/.sync/readme.txt @@ -0,0 +1 @@ +2020-07-16: In the new sync format, the version number is stored in /info.json. However, for backward compatibility, we need to keep the old version.txt file here, otherwise old clients will automatically recreate it, and assume a sync target version 1. So we keep it here but set its value to "2", so that old clients know that they need to be upgraded. This directory can be removed after a year or so, once we are confident that all clients have been upgraded to recent versions. \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/.sync/version.txt b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/.sync/version.txt new file mode 100644 index 000000000..d8263ee98 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/.sync/version.txt @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/04c4e932fe3c4c4a9450c09208bd6c21.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/04c4e932fe3c4c4a9450c09208bd6c21.md new file mode 100644 index 000000000..273702a69 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/04c4e932fe3c4c4a9450c09208bd6c21.md @@ -0,0 +1,24 @@ +id: 04c4e932fe3c4c4a9450c09208bd6c21 +parent_id: 3d675395b5cd4d1e9d7ca4f045f41493 +created_time: +updated_time: 2020-07-25T10:55:20.793Z +is_conflict: +latitude: +longitude: +altitude: +author: +source_url: +is_todo: +todo_due: +todo_completed: +source: +source_application: +application_data: +order: +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c000470{"iv":"gJXa88pt5ZzaYAlD4ZCSkA==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"8jM7A4Kx9RfLOHz/uriiE3r5zz6AwnUNVaHFiiF3Sl5La7spsW0hAzoAJuUHncbIE9Rks6RH5g+k7Y4J5zxlpHlsbXYFzvYyh706kemlwg/k/lDPkMK47BPPYICcCCOeUvc+IXAjgcr52RX29yuBYhqQDyJVhFgkaEmbYYmYXbomZrzOWeXF4O0nqVFt7sawFUg3Vu0+gESRK5SV24OL6l9Qw6rRh+kwCO0E8piKq31CDrx3741E3X2LOR2jw6i+EIs/pyPMRDPWg8o3YOF5Ka1u4NkMXM14pSc/VYFiJoR2kDh4alXKIaJaJeIRjQ/rWNz42s3eT/GpoIRxPKwfG5DwD8xT5Ns5HJ0aD2E3es++BjOSeeqi8MkeCdyHLtgIQB9FaILLMtzGu6lUwP6VlxJ7HJhvtA1T+tfKMlTrzNYxJCcKGg2IUU+Qv+8LnSjybIia5weKj58emRNAXyVAWT6CQupoe1c3XxM6hPInggOZkghQOV4rpMuKniZ3DMIuddgFpdXtD8q4pemQkXMQfRtUafyqz69zH9Bkn4RS7yEROfRfqi5TrAUTGnTyPnyAQdyORLHCNBJBheHMa+s37LSe0ZBYmGxhnl1VMWjvUatIZphF7EUlseFC3jzxMv5fYsi8BbmpyLoqH56Odogc6e+ToPuBvRuVdIBHXfT4k5B62+KkWAmedBkjwJIgp6EWaaTGmVwwBL/xWgkKL87qsMmMn+Obj/e/3cDBQm0ubg8gvsWiGsA0cL0jBM3QKMgTb1D123zG2s8DWdIoSSByjwW4sVkb9hH6v3RCPoOyxV1JubcuDsbGnnrL9hhmx/5ahHnoHhUD2vYVLsI5xRmktEUpBbf9BcMY6RGhUXWM40djLLQuTAbYFF49o9X233BlbQrQndEnF6mhy3vLSotJdeGc8PWbSenicXaqJm/2U2beCAeQ5TgcDg5hlru1JitD2TIvJuv5e7Z1UFXSc/P2TEFsIP549bCt7gDpJl8="} +encryption_applied: 1 +markup_language: +is_shared: +type_: 1 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/26b9c0dc3ff146ed99031e259bc1240b.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/26b9c0dc3ff146ed99031e259bc1240b.md new file mode 100644 index 000000000..31e1e7dac --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/26b9c0dc3ff146ed99031e259bc1240b.md @@ -0,0 +1,24 @@ +id: 26b9c0dc3ff146ed99031e259bc1240b +parent_id: 3d675395b5cd4d1e9d7ca4f045f41493 +created_time: +updated_time: 2020-07-25T10:55:20.807Z +is_conflict: +latitude: +longitude: +altitude: +author: +source_url: +is_todo: +todo_due: +todo_completed: +source: +source_application: +application_data: +order: +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c000470{"iv":"4LKOBhs4VTm2kEFjWsE66g==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"T4CSUbg3Ma11ffHKa8t6Zfu42DYcKGv6+p87c/V4wXsnHEaLFk2vV9iZg0CDOpAfOZaOCBrWSUsUI3FYIWjIEg8Wb2GsIrYz5i03B68yAjjN4hDtulyuLAlFzQUWw6xyPwaCebHUZHekearKFL/sQl2aNClxsKz/uT1/g27ANiilywFxp+GwQ/MxI0YhR7Qdr7Ri6YbySOSVExInQqMY2ERsWhgLQpR0NFSZLVIi3wJmlG4PR4uAxJgHu29Z2tvaMjW/lKUbCaqt5vsbP4qw1BqgNjt8it0qLkf9TwW3y4sxMCXI5zo5cdlwbMEcuDxvU2zfDBfWHgnIIekSR3csOTBswpQ/csXIKyZomlP+7ys7liGtoDssCrbH8YBwGW1KkzBmaKYhP9Q1JOWC8/HWI/YBkSWX0AQIhkwUjPVY+WgzfKUFD4XpUaKm9xmSCfj67GqSFUlB0MhTer1aFj4L3T/YOSZQevaUwmcnJzRpAmArhEAnHlrm4DO5AqubrAd88hPGWiCo5Ud9PYsLBeDiRJaQroA0VCOuzGLTyq5p/YfPedNBI08cN5cLuKc8bx5W4HBp6ZYv6oDkkbR9eZQyY2qbvkZoOQDlG9QKrenvIprrtRgFCO9NUfEYzxApSpyzVRfBv4LFxRTlpjihaGxKcA77U7xfqszFxJNyQj49hK8/SWR+/m4TOmqMRxwWm638t/NYz1MhLU2Vee9tA847ITVbZCgpxKRCVSJytYuHi8gshrTom/zjJPMJxoZnQpoWRB9nEbd2s+Tqev7U5lLPiMwjBzwNNnRLKkI2EFRxIWhOxSUwZuxGSIzhb8dAC1T6/ngwvtFTmYZxx3/XYZnsZO47IeYUbHb9SPbUSUXoQeU9rNDekcWusfrbod9Y7ON0aHoiytX54xLFOTUXyFLY7OeGM0C6QeumvOLTJ/WLHj6ReBB0vVipJULfNW5r4GEhD3Cf8uMf99mqXIYOFpBJj/WQesO72R9PjYrjk7c="} +encryption_applied: 1 +markup_language: +is_shared: +type_: 1 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/3a8eaf72f62847689176a952a0b321a0.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/3a8eaf72f62847689176a952a0b321a0.md new file mode 100644 index 000000000..5c64392fc --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/3a8eaf72f62847689176a952a0b321a0.md @@ -0,0 +1,24 @@ +id: 3a8eaf72f62847689176a952a0b321a0 +parent_id: aa7dca873bdc47beaa9465e04610619d +created_time: +updated_time: 2020-07-25T10:55:20.782Z +is_conflict: +latitude: +longitude: +altitude: +author: +source_url: +is_todo: +todo_due: +todo_completed: +source: +source_application: +application_data: +order: +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c0004c8{"iv":"iOtmBH1egyWEQ4howS5ssQ==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"BbJdyFskBpsOLSHMogZbAF4GgPJsY07EwoQjmiZl2WDT6QezrTnLQ4BsYjMLiLHphtjs3MqxK2p11cfQLPY4fAoVywQeTLFjMyncWsnAFZaDnCvZ/VCwbI3yTi/XOVgQ342AAIZFQ47tAkNFmH6CEgLK+g9BW2fcBxHcnky70XfyE7FI3HNl/d0qGIBWViv6LJKl5F4ywbky75/Myb029g3u+TQvmxZXDWjbDewWFdvIj82KdR12XSf+oQJQpeuwR2yHioX8qjTC4/3bLeQ9CumCRHrrSpxTqjCcvMH3w3+OnfMnmT83MxQa+mQJzMhZCaFbeEw5Mvbs1lgcyhsCC6yWDmgTFwhfJfz6KqZxtmkqiLt+E26ej8nd344DaEJ2GSHrbpkgIgkzYJndtiFGWnHnMoKKKsgAyOcTB85mQRvwZqsx+StKGkdNkqfbSQhtspi3XWr6TchjJgE2sqchWMZqyk6HSGVNVROptJZELPyEBDqG+vq7JrHPEagUCGUYBHEL2ZfKIy6ecU31Q7Hmpgpqyct/iRKr8mKrCLPbrUGiL27Yov7Mn8+MDwnpyGSw8tGYG29LCOADAkbSHl8dtYsF7dQl0VULipdwVv5KRO0ZFzacIu5/ciw8mx79wfZSVcKy06YYvSsGOYuyvRxXarftPBc9abVhSL7OLfIxwaNJH5GBus97fEcaWPTMTIoZuj7qRWsR5vjG7DQ1PWlWh/5+zNQ7mP5SANJCIjTEu5138hGhOc54sMrP7HPRfzEVIgiX6fOQ9lyJGwn65nn7SRrlsIiVLeT4Hna2y8mcXRw6p+J1WiqZyn5Sq8MyhBx8Q/Uc3IMVwZd7auOnL2BLr2SG+9z0YHUr5xPR9WADQiPw/d6GoXs9Jo0mzqAhFjX6WKjMo0RYeQVCAOnIvm7XTz65pmwVIxqI++nN3RB3e8HQCnS/ERBiSuBb5cEylsP1NV0Mzp7JSJKrPiRF6TJED4pJ+HHScRV4Sg0hrrZiF/ZkxJ+VGEcgCr8IT/QrMesHjXPaZ5f3ov8Amz57Eq7WLaNP+4lOxzyy8AjYaPdBlswx47PN412Yna7a0qG11l4="} +encryption_applied: 1 +markup_language: +is_shared: +type_: 1 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/3d675395b5cd4d1e9d7ca4f045f41493.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/3d675395b5cd4d1e9d7ca4f045f41493.md new file mode 100644 index 000000000..7ea049700 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/3d675395b5cd4d1e9d7ca4f045f41493.md @@ -0,0 +1,10 @@ +id: 3d675395b5cd4d1e9d7ca4f045f41493 +created_time: +updated_time: 2020-07-25T10:55:20.632Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c000280{"iv":"bbWuqdBgF3f4nSVt1RXDZA==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"2nlg8ecuDKCTHuAVnU7JqpJ774cjEb5OC+HVImvBQe1sVBxK4Cypf31LLXYX7Kz9T+QZkB7EQSnH42+zMlYWLJgQzqLcd4OglSjwnfdFf4puH5xIk6mlYhCNu+H9nvYYa4FQJTXXqAi0Bp2fj7ta0uMePGxKUOZVB/tZ3YPS+rFo+4/C82SoTKY5JDPjEgtrF7ZaokRV51K/LMufT3ApZKcKp4TnK3lm0TCGcUqebONdk0jtbcDQnCy/HmM8P4UZGx/Hcw4dDcrwwSrtvJQrp0sIQ0XwMQwr2VLeK40rS4TAEQ/SeodQcN4K6kkSOS6IEDxps0s/742HZJUDfIfJ95zvBySqsIFXEvMGepwIEB35Vkt9uSzJDoCeAPd1tzI3EE1bEtJ66AuAGv3bZ6EbBiH/PszED1gMxEvmHscoOvZRzHn73SkhPLO/4n1VJhjNJgj0E9KdlLl0k2Mla9E6veIGDhgqdIjWcC+Z+g0ctpRoILRi8zi6nnSH"} +encryption_applied: 1 +parent_id: +is_shared: +type_: 2 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/49e1777d9c17439fb612cce85700d16d.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/49e1777d9c17439fb612cce85700d16d.md new file mode 100644 index 000000000..3a9dc3935 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/49e1777d9c17439fb612cce85700d16d.md @@ -0,0 +1,24 @@ +id: 49e1777d9c17439fb612cce85700d16d +parent_id: 673415563b2f4db2aae8665ddf9fbc67 +created_time: +updated_time: 2020-07-25T10:55:20.924Z +is_conflict: +latitude: +longitude: +altitude: +author: +source_url: +is_todo: +todo_due: +todo_completed: +source: +source_application: +application_data: +order: +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c0004c8{"iv":"OWv0KZHC+nkbL4+rXmhV4Q==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"L0eNB9OR+DkJ5qatAYN+UlY8+GVQBZJoV438yV76JqGmBibDZEiJmVdhgv9A8pwWCfN+vUP8Kd2DIPCtSgn3d0Hs5ZTJmK78TqkiF7ax/gE7Ri4Ys8ri58Ct2gZ9LTaK+EybJypqjtHrO87dHxyLrROcONuI+m+5JpBoPFkyDor7U1pcM1P0C4VvYhR5zxIRiDEuWL/lVinoogC4OulkhkWBOJl0OCF28WeKGAwGnjV/S0EjF7FDlSXWytHuMAIfk7UkVmlSAFDOwxabVBjuVYmpxxLCTHR/okaJRiZob+1ykZfT+Tvv2Y27zC8Kffarg1t5ZZhcA7vJbl8p5gLhEM7XJtgwaQ27jr8cJVdO8UsjjVoQQf1U/E2mZSS5KPFzma6w0vG33RvoZ37jzPq50v+jCPivt+zLda5mwV0s6E1Lf+M2eWVaOpIpxHiHYGUdbZRPGB7+ZbCmcXbGLezpsIIb/+AYbnE2Tj5Ygj66OBLttJXbaJpvni5IrXaeEkXrPT4ouIMkB1nUeSvEGG6tXCkQv2k2bSddFUQEkRXl22J8U7gMlbdvbh0G8W7bRvLNzME/vtxN+A4IlY0QW20Q4bEmX41B9z9SDdkrjNCydmejN8LxHCTWSl9jHtxD5NSzaRXgtDrOoQgdPQ0zMgXbe81Tsw3Dgf7DIGa7/+kTn5NBi+iqV9l0y9diRRJ9BtHChM9jdwcG2yk14iuSflX8MR5ALDd//USiodRZhHsuDWfDSS1/a7OzJhZCDGXICAnNmrWUwA/JfuXHEiGTsZ/Hq04vfDNHyX/ywM+g7uCjjyPiU2EtuiMwhVORwms7fwFeolQvu8JvG41LWttWo5UJBCEo9DZu1Ikzutv8Qa9xuMKTFCFnIXFtiMruMEGw8paANubodB1RnI2q5AHEHGO3HIo90DFDdcrpiWHv6XOblKhlXe4Q8nS7wMxcHK/P4AWsgRQc/UnkzRsk86HOl0pS09d/ZJ6VR7wOFC4/xb+YLcZgvAlS81Grs3SsXzwlPAH1z59qUxGkzAwO6BlbmmassiqArHetYJC1jje5dQBNfWUFDmGkCKblXgfkm47Oub4="} +encryption_applied: 1 +markup_language: +is_shared: +type_: 1 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/51799b7f7fda4bff84954305f707ca72.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/51799b7f7fda4bff84954305f707ca72.md new file mode 100644 index 000000000..be80b3c40 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/51799b7f7fda4bff84954305f707ca72.md @@ -0,0 +1,10 @@ +id: 51799b7f7fda4bff84954305f707ca72 +created_time: +updated_time: 2020-07-25T10:55:20.634Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c0002b0{"iv":"VMKkOgt/Zhpw0WSe4YNy8Q==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"j0+JoccW00HDUH0jgeLYIhj6VTYbSYgoSncwpFVBPK+yRXwpefjfL9KKmCBNBNibOd2TvJRh1Tk20BRwr2U3kZK31Ytwnpeq7dMuYZX/rkSpJRZ5QvJ2lwJbPCT5kg1Cv/5kE4zD7bhWD7k8u4cWKTdrz69tsqWWBj6lCXD92ns4Iq85btKvGBT/ExvXFsvOjG1rKKzWua1WDkMXKyS0yRutHZ7GrlgJve2QMvoh28OMCPFyNb7Z4/gzCZjMe44MfCCRIIEyyi1332DSSyXMsgGZuyUwQKNDC3+exlK0sfA4pKZhkT9uBNp4jRGehjxmwq+a/olX4H2qdKkpAjR1UznxXCCyRfew6arokr1K8ZT1TC2DJzM8jc7wrt7hX8FTCY57YO9+jJuhdlVVY4S6YkwfFwRTvC8NQSuBWrydU5IwvNABICr4o1IAB/gfA0CTVI3Ua4DcETh1m/7ptUtTM7vDQJ4+Xb+KzhOm1c7mYCothLPxRU/A/CjoGAwzAIFNM92YCi5UPl/aP9P836trbm52XGggtrb8Ofe9uOA="} +encryption_applied: 1 +parent_id: 3d675395b5cd4d1e9d7ca4f045f41493 +is_shared: +type_: 2 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/5fcd3813d8ec4fb29d2a9d08da81a0a0.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/5fcd3813d8ec4fb29d2a9d08da81a0a0.md new file mode 100644 index 000000000..a26207847 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/5fcd3813d8ec4fb29d2a9d08da81a0a0.md @@ -0,0 +1,14 @@ +id: 5fcd3813d8ec4fb29d2a9d08da81a0a0 +mime: +filename: +created_time: +updated_time: 2020-07-25T10:55:20.921Z +user_created_time: +user_updated_time: +file_extension: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c000308{"iv":"3Rz70DM1fx/t+x96IgfbVA==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"O0o0M5yISa/BJT40Wddy9F2HD00jgI4z8w0tY/BsuDQQHLZ/bJJMJg3qZvXkcWEHrBr/ki2TNOSvV4xCWG3Q2IbbLQjp2lpoBGRULtkzGqKrCtQL/oJhlbYACv3Au2b0nhgozeW6Uj+24aT6a9lTnDlvOvhLbmKP3wjNbMqzYot6Df6Fz/s4ZgUbBqYlTJBkvfu9d2eH0NPPfHc6ZzUMRjk9tOPCQjKnl2f7lfg46YKDqM5VbQR3oE68jlFm5ufmeVVgAE2CZAkv2b1dE/ouh2EKYalVBkrPjOmQuw0MEW4sam0wDy6IlxINxY42ldYL0ASDN53hIzLK3UcL6igz1QhnWTtY7fSHp2OROvVD23Yq7XNORXjWq9C5+99Tp0hryunUZEwTAP60IpfECbi16RyOcs+u07B+uJ1VdJ2uWJ6261XSTTONU5M/jrQA6e9RQoyVsmuE2CmNzv/X1esDVu0JhX92MgadQ0529WN51vqKQ0toR7FMazKZXDFLLh6iWrwz8yPZCRLJOgFF1wnojk86CaZ0Z5bvZDdGAdgJC+ttYWqQI1TPd8CYdpO0K5Sjcb/mmRdmO3hThJ0z4qj+FBrwzCceNgeJPbm/yWhp4h2LLQGvYW0PlxV53neDB5pw"} +encryption_applied: 1 +encryption_blob_encrypted: +size: +is_shared: +type_: 4 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/673415563b2f4db2aae8665ddf9fbc67.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/673415563b2f4db2aae8665ddf9fbc67.md new file mode 100644 index 000000000..1b7764e38 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/673415563b2f4db2aae8665ddf9fbc67.md @@ -0,0 +1,10 @@ +id: 673415563b2f4db2aae8665ddf9fbc67 +created_time: +updated_time: 2020-07-25T10:55:20.815Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c000280{"iv":"kCJkRvCMeK+RKb41oOeAtA==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"7Fzu0jh8AejqVH/SjqFIRZGBGA4pMMc0l6v6vJWvN+YXe2FesG+xlpmIeq1EB52h5F/WumQnvEaJWSISK2TA/VygkkUy0xqLlwNsLQmdfvMapJDnT5XXprwqdMgMg9LZ0isjpi8pqvYBSUk+Bzbm2RvuCosUxNEv8deJalYoDNyBajjhZIYSyAeH+2yMs5tQv/AqgEuUy3AOQIr6fvk1DlKcSt4FjIFnw+jFudT6djIrUHlnq2LnKT9zxYVfh1XBLXHCQna0df3aGlMG37DUzlxHX2VNt2Gp9CXqGY2PXuVAA6DcBtAMjy7BKh+nESGhWPbLSfXk53EOfmm5Iis9drqZRoCl4rjZoioehQv7kSpUMPq4712yKTIFYnlOIPzlCBPdlOnWfdcGcwq3qitbtOmZNW4qvLWTlwkRo0H1W7DmfEASUFheqIWCqBJuJ6Wk2ONmFPtGPWutIabYUVsZKAQpK6qYd3arR5t3plq/jJfMFrrG/7edKFEM"} +encryption_applied: 1 +parent_id: +is_shared: +type_: 2 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/6d05a0cf086043129720b5da35210efd.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/6d05a0cf086043129720b5da35210efd.md new file mode 100644 index 000000000..0a93804d1 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/6d05a0cf086043129720b5da35210efd.md @@ -0,0 +1,11 @@ +id: 6d05a0cf086043129720b5da35210efd +note_id: 26b9c0dc3ff146ed99031e259bc1240b +tag_id: 8a17074d4ec24de7b5a1aa666f7d8b38 +created_time: +updated_time: 2020-07-25T10:55:20.809Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c0002d8{"iv":"2Vou/WCQoWwTOzfTBzaPTQ==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"AZ9IJMctvBFEfDaN7rIcdZGHBvquxEx0W9l0VstK3Gmwv98eiUhajXAQD3MsfCM7JyZPjh17RdDIcqmGxDyY7GMJflFOTRYXaOyOE+e/nT/pvbo9TCegkYO7HWq108I7Ndt4VOLmbe99O/glsE8J5G93qaiqR15fL8sET346T+kVkuBEoWBoxYZ0W5eNSdtU3TEl9ieOsmJ3vx+xt+85+6HGsrB/HXLaEi1rHKklUUp8EDUavh3BiAunHr315oqedJbdbV8VlwehJvtYaZvyivvshsVuiJLA6QRgKHwbM8DFeVNdDiq5E5LNBsoNnAQzskqwSjt3E7ONkQMv65hRaTFE/DHisJ+IC261J/0wYXCWOKxF+8ybTKPc3XqRzuMZZkEAgOmW48OBVsjU72pNpx2e53HwiLABRlQp2a/0MVfZrI+dVjGLP0Azgifj0IJJeAhDqJN6nmZHTxhlkv9kDH4e3508qKpX/+EB0EHVo06nO/0kB+hbR5FyyQhedRbOMclfkbsiNRZfjg1vpYEtHPF5u4pqyT+atHDOh3q8Hdku9uzL9QGa0KUxylZG98B218sVOgUfQeqVKA=="} +encryption_applied: 1 +is_shared: +type_: 6 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/7ac37541e8404239babdaf1d3fa39c90.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/7ac37541e8404239babdaf1d3fa39c90.md new file mode 100644 index 000000000..dcf7dc5ab --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/7ac37541e8404239babdaf1d3fa39c90.md @@ -0,0 +1,24 @@ +id: 7ac37541e8404239babdaf1d3fa39c90 +parent_id: aa7dca873bdc47beaa9465e04610619d +created_time: +updated_time: 2020-07-25T10:55:20.792Z +is_conflict: +latitude: +longitude: +altitude: +author: +source_url: +is_todo: +todo_due: +todo_completed: +source: +source_application: +application_data: +order: +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c000470{"iv":"bhbCBStJc0FV0QfurbZZRw==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"vYNW1c21bdKGNLhcpPlu/WwRPGZC3kfn2ZJLJj0Q5jxsLw+ZFCBvZlYMfgW5KP14PjhmQZhaCrzfJn35uLaO7E3OZUnH5jZnHyGpcL7GE78hml+odtVJrPsjTltMLKx3TXtcza3WPQjGPJyL/ekU+YPI5+tt/DcgHofzDJ8K8vvHyf7/OQ4oXkK5K2AnL0mzLNqaY2swuZsuHASGX9KBKAQ6EzJqLvd2Fm459im8kTY3v1oaLKWfchQHYDPkvXVjzXCgAsRrjUY1b4MdcBODoWk8Hs7AxqLGdbDcYMjs4T3tUjJ+8g+osZU1hMYEl9B180RBt3/ZEn03SrvZsGn7kQ5ILYes8Og0AZ4R0WVmwABOObTYy+qgg/2dSuveyDs5hsBVcVYnQOAC8lYEodhOu1XZsEEJxh+h3fvzSN8grd5fL5F7BbZ50Elz0Ba3YJFh2yWWLT5AR7yxsj+CSxoCZAiAEm97KgeeA9exY89o9EuLt7bq+VuGFNPRppldHCF5si/u9CmR6Dv50QVRmMdNU3AjyVRNESXQ/BxhwGluAOKd0dV65hjf96TufN+BhVKOrDl/ztTkt57buNPx+8rWNOUodJSu2zJIvajfPWCSbWAMCGinjD5uy95SsNuyiUZulS1NadR+eKVA9oZURzcf1ZHDIJ2oyWhoNW5dmhtZyQ5vmT7IrmE/PQRKmTwEi/2rYF+EI8NFxWdZ/e20le9UM67LwWfkzqDY2I9ZJC1Xxu8gG/FeEkP7qADhEF/QR7u+WvIfCISksTl4XjyA+pfhsRgU14D9vqRmkaMgrU0SOqDusQdC932Htzr9Aun8eLjATBsZJ57zqEcaiaXVl5aZb7yic5iVXi9KYis84ijFr0vP0QfNpxIjBigvDGWO/xTcGme8pkoMyjXCuOhs6uc/7KoBhO4V+ZrsvDAF0QQVq6l0tl//cUmCPkSlHhcVIT3fDm7Ry4GuNy6gik6q0xhNMr8d98Q4rqxajgn/bBo="} +encryption_applied: 1 +markup_language: +is_shared: +type_: 1 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/8a17074d4ec24de7b5a1aa666f7d8b38.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/8a17074d4ec24de7b5a1aa666f7d8b38.md new file mode 100644 index 000000000..35e30cf66 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/8a17074d4ec24de7b5a1aa666f7d8b38.md @@ -0,0 +1,10 @@ +id: 8a17074d4ec24de7b5a1aa666f7d8b38 +created_time: +updated_time: 2020-07-25T10:55:20.801Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c00027c{"iv":"+bwkkcvF8+iRUp42297fAQ==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"GuuoQYblFgQ9OlYr+A4UVDNSjq6k/sFgE9kW6VNbee4LFSYFmxLtkRFgmoCJe97yopB/JLDPVyn+InOpEAQvRyGFbgku/yElAy1v/PEL3VKST3QezhpNHB+qgvUUPIqkkFtmo67CtvUsSD20txIPPbm+y2dabjaoY+Xi0TE4XYAMUjCDR2vozWrgtMylETyV7xzwWw991YM/HufWlktjsNKiRQvanEhda5z4xaIIk42zhqnqHfnhwDAvy1eTrfIy3giTgB0bPNYxY0ZvlFARfNuHF1owzKpBB38lHGvW77CK1elBsDdkzCKD4stm5LOlUHuQ6w6vPLuGbpcSJJZk2vaeer6ydoG8BPG+xUQofesL5QXpAx1CWqXsH+o7j0NBHclR4lic2JiVMHBhKIRmHvM+a1EzqlT/YL46Bh9P6dCbPqLCKrBJxOJwozj8DfCVG/xMLrCn11vA09lq395gQlH1qJ1c6XQ6vexC/iuDB7US8KarYpns"} +encryption_applied: 1 +is_shared: +parent_id: +type_: 5 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/8d4de93d82e2468baa8e539d10d67510.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/8d4de93d82e2468baa8e539d10d67510.md new file mode 100644 index 000000000..0403271d1 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/8d4de93d82e2468baa8e539d10d67510.md @@ -0,0 +1,10 @@ +id: 8d4de93d82e2468baa8e539d10d67510 +created_time: +updated_time: 2020-07-25T10:55:20.813Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c000280{"iv":"oope5TviAxbb3opHGHqzWw==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"UML4deeatwKX3QkbH57smGVgvQxzXk/My6rkupu9AhP4W4g7J6iFKs30Xbt9sLdrBaHsshI4d7Tp44YA/5fASSHosHr2DjSLH9AZnQ35eCGKjHaXKMXPWAdQwSfCM6mB6z2BXFA2GU0Ign0GfJvSwTNZo4WIG7s+pjPG8puaONlvJ0E6dPkZJoEM8IGrQku0N6yvnQu82HlVNWUJs/bi4Rd7leegPDYoMowC31xmF0uK09afBlPd2UixL+4qnq9bLiIaF4UKYlE+Cd0G+1tTjPklebRxhfdkDxdJVOImrjWdba8h2gRZak3Gm+i6ZLM5H6doZyWUBs/EpTOU2TOh80+Gmd4DdOdJ6yVASND5hsY5QbjpGJ/6M7RoastJPB9HSM+5mSDU51oSeqIEjrcgvr2j0XB5IGc8zY6Y1Ebgg5VAV2Ds7p1Avak3Jb+R3bF9oA2ZyrrxK+LJfc0T31ZgvwzDEqvPwt7cHosjwjNrjLiakyhekFDE1+ee"} +encryption_applied: 1 +parent_id: +is_shared: +type_: 2 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/a1a0987e82cc400c90582492f814c23c.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/a1a0987e82cc400c90582492f814c23c.md new file mode 100644 index 000000000..73393cbcc --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/a1a0987e82cc400c90582492f814c23c.md @@ -0,0 +1,8 @@ +id: a1a0987e82cc400c90582492f814c23c +created_time: 2020-07-25T10:55:20.995Z +updated_time: 2020-07-25T10:55:20.995Z +source_application: net.cozic.joplintest-cli +encryption_method: 4 +checksum: +content: {"iv":"lAWEbGcNeJKdyBtAFJhCJg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"L0lL3nxewe0=","ct":"ZonX+RPsUjs7KznJXZb2HA3WDPc5vBnqcAU2FSphHIYYP6FEjPSmiGbAmCcScs2WsOkSm6t+4Elt5nu44XPxj7PR/O5JkpOlP2WNrIMovS7dhpm2fFhvnywMeYfVYJGvOvaOxUsAnbTjNayUxIYFKoKEB6o3pmp9Msu8ZESc46mnnjbL7VDuiCWIVp/PCf9hK4l1Id8hLAy7Xd2Jl+ToItD7LYWkuA8+xEa6WvW3l7tEyPeAYKQGze3GqL3p+R1E6bXZ4XV7K193JlYwDvDSORq5eGR290FmjaGk0twuSerzx33YJFNfGpYx2YBEqYD9JeHmEabDgBn2sbZRbCNVRsEx7YwAvUzgAH92Vpzw9m59zCdKVxEBekkNUfOAk9ylDhl/1qnYmHd6bpnO8ruU0NrYV0cAfpgwIkD01stmGo3Nq0w5hAS2qtpZBvHickJR+3nb/G+HPCrnHvQVjUZo/7PRfnTuqdF+ZxRM/ENxL8s545x60ugJLOFUC4KU3VmkAt3YdmamgoS8O/7t8pR/vgY3Ll9DbYSDfL91w1mgZ0fo+XYI+Y/cgDpNhiTwcWSn8QjyXPBIO5Y0Rv1t6Sh+G8VOhf90riUI7RJG4t9Y69YEyqbbdsDLCcAbTxPL950guHz9na3KQq2ALiflPIMgKjvD9PAJqx3Cm/aoTFUZiah3tJ4vmBxpMg=="} +type_: 9 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/a2fc1b9ae0d04c8bbeab6d299d0193cc.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/a2fc1b9ae0d04c8bbeab6d299d0193cc.md new file mode 100644 index 000000000..56b42f264 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/a2fc1b9ae0d04c8bbeab6d299d0193cc.md @@ -0,0 +1,10 @@ +id: a2fc1b9ae0d04c8bbeab6d299d0193cc +created_time: +updated_time: 2020-07-25T10:55:20.785Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c00027c{"iv":"SR6yWBcxM5ngXZzQPnrfcg==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"GkMaEQSVNWpYVstVT+viNzY0b6Ol3QzdgthJ4xIYEdLGZpaLCbBoN1bcYK6VVm8HLegUvH4HjeKlGQQEgX8nhwIaI/DGpWp5KMK+4d7QDSbz/JrDz8mJYpMPIgaTVd/elAs/aojUoJC7ZMEqIiZJi08xkiZeMxj5sFk3Bp+t7Fdg9q/HGTKIoxZBRKHke2j5IiZhLmaeI1soFksi6UmpofejXJB3e/U6p91j2dZVOuI/XpV4aI+Qqut5sU6nWB/kM1/DgTmSJGwYcMmx87zUqLxTs0efWOOuMH+598PTYTACW8QGXx1ErjeiiT9XnXo3yte5Bq2OhLf/Bs2YfkToCejQ7pnLOmVRFX26M8CQzhOqVm9lGxP2eB+ArgHXuUwEFWFDQaNI4LmwzXoXDQLA2HWxCEI5tfFZMD2ge9uTU0TBQ2cvVd282R0kM++LwaNdcDNFjPkQLp6G8Cbyql34sksiTPlshXHF86yoYKxpHLdfyBQ4uE96"} +encryption_applied: 1 +is_shared: +parent_id: +type_: 5 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/a807b6e7d6594567934938dbfcd2bcdf.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/a807b6e7d6594567934938dbfcd2bcdf.md new file mode 100644 index 000000000..3faa62739 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/a807b6e7d6594567934938dbfcd2bcdf.md @@ -0,0 +1,11 @@ +id: a807b6e7d6594567934938dbfcd2bcdf +note_id: 04c4e932fe3c4c4a9450c09208bd6c21 +tag_id: a2fc1b9ae0d04c8bbeab6d299d0193cc +created_time: +updated_time: 2020-07-25T10:55:20.795Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c0002d8{"iv":"EPjRvnAQokTnBFvw2ZeRkQ==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"xBFojIoEUsUNbGAu8CibWcTn7Pn8N/zC6UkIxla9Gul8STudN/HLxdX5zBnlLzIiXvumAt/OtRQ8aOjmGxrpPudPH220Shd/eH7jLfh59PxpDNWRrUZLwO1rxUozJjpFAJsqiHGurmsLjtWzMM2p1zeoSRbIacb8X/Sk3BiD3mnnbBNOHoSbV3Rli+mBRgs5SkWqJSvwHbFzjJIk7vv5Uavkwchh21EFOrGwsNlYby5/JIZTz84e3JjXOR0pVs3D33cZM1nSv0ijnJwlLS6Aty+UGfGZl0pq1FdB+Rw9BVAge2+eptKfzmN+bfUEWTIEfKbPWnaokoAa8Jcs9qlJHIqCAAmYRs20iHggjliPzYQ0JO/YXlfsQlpqPS4r+vrEt8V2caCXG0+VLV2XcmJgFjU1oZuXuunJNlD9iobxdgSjxDbmEdYZ/FzT6BjUpTVXbjZLEChq3tUz2PB+JLqK4uNM7J9ZPASW2/M5CH6NdmESk7gafjYM70TQJ8AFsDQNZ3xuA9/kw4xvkW4s8PniELPhMgbSbOlI8ACWP0UReMgvi8h8/fxNc9JILEizuXgN9Vv5Ya32rT8CDw=="} +encryption_applied: 1 +is_shared: +type_: 6 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/aa7dca873bdc47beaa9465e04610619d.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/aa7dca873bdc47beaa9465e04610619d.md new file mode 100644 index 000000000..5c0310fe4 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/aa7dca873bdc47beaa9465e04610619d.md @@ -0,0 +1,10 @@ +id: aa7dca873bdc47beaa9465e04610619d +created_time: +updated_time: 2020-07-25T10:55:20.635Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c0002b0{"iv":"/hdpjX6UaArAOXSsk6W8eg==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"7qX8Fym/M6iCMLeN1M1NuTgPzBKFlleRtwTdGAJCNR6s88rgC4XYlT0AA/mAJ28FDw61JO29ZnVQTLulyuAhmsNtxY6Bar005sLHlEhX/s+/LRyfdLKhzVWVMuKvGeg9F8omkDVDjE9oOcx2DqM+wV9kQe7d2sDWYpu/fEk7T9MNBWI+vodqfw3iNLOiQWzQq5Xrqy7lpb/fj4sSgtKGZouSkbhcckjaKYl5IpblfZffnRF5T770Our6ufID3kTFAorqD4Zg6lY4SUKLrWWAptPv4/wQtBa4J5mQ0XUy3OSSjh4EP70H2js2NrDouKFsaRB2KyKk0/JuMmnMhuajyb9ozIDGwWEmi9zKZZ5FrvqLCKY53hhqxyQ8BMOrrHm8T/GVmfFMKN2ZkO8eLdoTETx8qqyErB9oHg6FsKCJS+943BcsrEuDHaSKUFTV+y5JNiEHIg+hBqMwc53z5mNgylP5eYqvje+t8zo0ZCXPJaMArlSohSQOE2pga9b2k8pwtczIQk1ZL36CX0WcDJVe5ir+c8NtMHG7omHolaU="} +encryption_applied: 1 +parent_id: 3d675395b5cd4d1e9d7ca4f045f41493 +is_shared: +type_: 2 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/bda6d120223140afbe7f03ef1d876400.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/bda6d120223140afbe7f03ef1d876400.md new file mode 100644 index 000000000..ca7c9c26c --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/bda6d120223140afbe7f03ef1d876400.md @@ -0,0 +1,14 @@ +id: bda6d120223140afbe7f03ef1d876400 +mime: +filename: +created_time: +updated_time: 2020-07-25T10:55:20.778Z +user_created_time: +user_updated_time: +file_extension: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c000308{"iv":"zyEI38ZCb2UGn3cy0Ejjjg==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"vqxf3Ul0liOt86X6uiY3/tyGGpv1nb7Nhj/xX7j+BBrUCeVJEG49cxUGwH94ZCKI1LDnFW1wiiItsdmZySyZZXTmcaVmw3lIA3hDRmS6QaSevRM8dSg1m7zMArXVGKwHPAhfysZF5wtS5rMf4cZSlr4ib5w064Ux8LqYEYS7IIDqczt4qORZUrzwigx2V54426w4PsUD4xzUI4K5PIdBewWinaRsmZ/Dwf4tLHJa0B3BYT/j4T0Iuznw+1O6JXUZc3G12lYLDPL9NAmX7BQ+aTzjCrvrbAq+ew0Sefhzb3kUxp6VM9QBbD9sPWSE0L3rr97t0smJYA8SVPBcwmiPRU6tsNx6ISc0/pQMe6KxZNQVid2/K1/e3OmkFItI5y20PXRs9IuUhRhylQ6/2dL6MR1d514PA3BFQyfsq1EBL8waO0p9A3qMFxNkpgrxail/Q21XbSaojTA9oW2+o3e7jMecwUjVlR2kvZG+by+FcE4SwZz09nBVvFEU4hmRDnKkRYNwnB5WbvVGsfa+gn5uj3HTFY/XIgK9160oGSncKVGkoG9wpoQ+JhKqhwjBZ0rIA/fOaD02ndEMpKlxoBjIPAtXTS/3rRYIypvajiI1qc/2FYgBCl1PvbWle1yOFsXT"} +encryption_applied: 1 +encryption_blob_encrypted: +size: +is_shared: +type_: 4 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/ce59e12313e84ae299b6166068755253.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/ce59e12313e84ae299b6166068755253.md new file mode 100644 index 000000000..8ce068d9a --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/ce59e12313e84ae299b6166068755253.md @@ -0,0 +1,11 @@ +id: ce59e12313e84ae299b6166068755253 +note_id: 49e1777d9c17439fb612cce85700d16d +tag_id: 8a17074d4ec24de7b5a1aa666f7d8b38 +created_time: +updated_time: 2020-07-25T10:55:20.926Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c0002d8{"iv":"h2tVkjGBWm/rXhSD6tyneQ==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"l41akNAnKt7pRrD5evL4ebPZJ+9wgV1bian4MPJUiUsK63AXJspadGbgMEZ0lrBDody4dC1lauUKdiDc3CFL715xGyscvMR9LNhqHqNWqg2PPMTLbwPThNi8X0OkaeH5tftpxBElD3eYvWRyoSy8mqytzkmaqDdgGBHZQcaqjHcB8f8B7gnNgT2EAhsHHe+KsN/AwD4v1CzJAs2ODrGZK6Bx76doZzH5vRFZ4gyYHoo0N0Ec9AaSl2UtiPH171GD4i5J7r3Puvif1sFZk4EPTfyfpJ+xxwWMM/10a6o9T4kIWOiFTFPi0o/l0ocD4ghUNX2FKxrfRtCcQk+D5qlRXhrTaJDj6SFOAAkkseddotgtvU+y44kagu47XKV+frEjAW6HMznpZBTrJ3OVLXf33uvOLVLM5c1RDiiBj20kNQ0m4+XuGAsXxrcxFj/uAzCO48Q1AvaLE3xlRP6rs+h6+JP8CAmmd5Grxm4Y73ww8IcJyzfajiQj6siScgNRw6LVY7lI8MV6UwxI0fah8MsqcSS2ZMWx/DJpqlbuoRhLWVeBLp3LNq1VT78LAZVqJsn6LesUSCeZd/tXeQ=="} +encryption_applied: 1 +is_shared: +type_: 6 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/ed20d91ef4e64fc0910088112c077188.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/ed20d91ef4e64fc0910088112c077188.md new file mode 100644 index 000000000..9ef2588e1 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/ed20d91ef4e64fc0910088112c077188.md @@ -0,0 +1,11 @@ +id: ed20d91ef4e64fc0910088112c077188 +note_id: 3a8eaf72f62847689176a952a0b321a0 +tag_id: a2fc1b9ae0d04c8bbeab6d299d0193cc +created_time: +updated_time: 2020-07-25T10:55:20.788Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c0002d8{"iv":"WIWEiOpZBFro4/AXC+mtcg==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"GREDeqYxU2c9LLKu7WNzDhaCMyZRmo8eL6E1Kx/0rvHT7A8tI1ZK/yTSNvfLcVWU164pxvkCtRB6fPij8dVA6P1uLcdhH6ztuvekCcHsb2PPdOtX4Hytc3X5Hi0oHvgVTnMnmY00XpODR22M0LPUE0/OoJYajTGM5Cx93cieTUB+qyxyyIHGBhjhw8vsI/CIjpvZIogOQDm4D6ptg1gmFhDrJx04C8tfZxpMI7iszBTyfsnIOBtRsbeONnqHxcOweIHkIMRKQlBcIRg0yM8vm0Ft1wyQzRg0B/KOTcfctdV1yUTnY3txVTb5o29tH829fyYKF0q2rLtqlVNfN56gaHgpkXC/6szW+UNkCmJI/v+EgkorfBB7B8NPH7v3zVw01u9QWuETi8RKjcMo7ZUMdMDqQYi4vjqSsmWtfAfs5/uUG260nS+cu3OxNJm1UtdIJ+l2e1e36EwbHbCiiKRF9deQaLWBmm43ZmqoukpmX9FFzjv78s2JbeFnDKc5tiw7BkYU9yETv7DgC7f7T8D9xDQDJGELkXbzzNBGoqniWFgWJc7Akt+Gof3uKZUIQO8lRKjTOpgxP2+7/A=="} +encryption_applied: 1 +is_shared: +type_: 6 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/f58c1af04627410da55d9c771b28bece.md b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/f58c1af04627410da55d9c771b28bece.md new file mode 100644 index 000000000..75b2d4cd6 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/f58c1af04627410da55d9c771b28bece.md @@ -0,0 +1,11 @@ +id: f58c1af04627410da55d9c771b28bece +note_id: 04c4e932fe3c4c4a9450c09208bd6c21 +tag_id: 8a17074d4ec24de7b5a1aa666f7d8b38 +created_time: +updated_time: 2020-07-25T10:55:20.803Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED0100002205a1a0987e82cc400c90582492f814c23c0002d8{"iv":"j4Au5F1MRKUgSevr56iisw==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gyo7bQeqz2w=","ct":"HvRLpScsoN1juxw18ostkjkjcxO+VWaprzz15PzE+3LU0KfoPqo0g2GgVPpbmp9dNyuyv+akfj5u5/buMZipiyGteEOa5WxoJ16KJ9qIYjv+kxu9kcfteDhHP6gTz1Mc0DfQRlLfZ5EbcpYQwkcNvdF71t8JsH5QwqA27P/wk5TKZDM/gd641zL92tNViAM4dZws2FDeWvgb4xRU3L7tfSBVoR5DXBKPl5syNrr8m2prdolydWms+RZuQQnFWIMn0jIKucSx56YEwcmCsAdsOLpNd8/MLqHfUOafrCQYDd9QIWEbNz509wWoXiu/Cjl49B+xx0ACa5z/Ey0yBQAwibPLMBq2yAQgxo6SWG00reOkGKQmxYIkD7mQa87zUtsCRWPebFohDV2LfAbbPFScsqNsH2wNhVXJZJ4JcOKMUR1R4hx66P158wOn9VY1Rf3zyJujiqzAhGivdvMQ2qp1TyAoiA8ibPizZIEPh0oyYf7EoZf1mIO/hyFMfs31xSbfdfXhUn57BCgVkndA7NRT2xoEdqBgymLMp2z7ll7F8xjMEG+8wUlAWNXzNDjhzPza2D2s8Co9pqgs9g=="} +encryption_applied: 1 +is_shared: +type_: 6 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/e2ee/info.json b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/info.json new file mode 100644 index 000000000..218abba16 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/e2ee/info.json @@ -0,0 +1 @@ +{"version":2} \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/.resource/006a89df4de64a22b4b1fa71f87fd258 b/CliClient/tests/support/syncTargetSnapshots/2/normal/.resource/006a89df4de64a22b4b1fa71f87fd258 new file mode 100644 index 0000000000000000000000000000000000000000..b258679de6bf0f62a6ae9c65f3b8e85d1dba3c50 GIT binary patch literal 2720 zcmb7=c{tRI8pnTQhOvxgvehui*eVml%viE3YYRg()Im|%ml-=J!ko}3OZI&?*;C<^ zC2N*4wuZrIbPR=}O{M1QKF@ugd;h%O_s`G!e&6r&DKD?2hE-Qm!Klj1qYcp*oQ}S} zKI)k9Nn>4ete(EkZxfK9pr9~J7$GW(&{39G*7?81?E&EYKnM^G1}OqOa1a;{;`Rfw z002Pu!~T~*JYWd#e&T(4LJR8dCB^d1Cspc@u@`iq04G*Lt3c?m{~4lk2^TU9B#8!yF?^g932$qFI+E z2xWr0Z1K)(LrmB8I6*%xIWx&&XJ>WHbduxglXvArDt{WjjTx(@2~1m3qj zRrJPma`cpnfXuQYUWmG9i%E6(0c)gowxzaxg-K9@D5K|PBqy3OAjo7LUh z_ETHNKM^bW{>-_BKo!OsPX*Gg_&0;@o{E)Jx#aE51u9wPeiL-o&m!YbGM^l5EgXq~ zjdgrSdm&+TH6YXEN&uFi7<65Ea8{tY8l#tp)wqr)7xR6u6f=Cd^I|=~?_=rCS|zl- z?Vg*^nRq4dX-R#psjKffb2#CdFer&`ao{THp>ckc6?3cToM*|kD*zgg3W^I8C}e}~ zVndNL(qfjViuAc;ydFueWR5r-0*+3YoOfHT-9Gy2c^(y&(=h&3YFEOnzVJb6NW#yF zz?uH_9T(9DF<#RK&b9*C6SE?x8#-E47g@FVZ}sCB+LD%YT$Z7!u6uJR;hgkj`xkxT zmM3IEEHgotfyW;O&8>FU$Y&Skyv_$l>cYzveQDw zgMnWn>fXw^jY66!ZX_-+P%N}X|2J0#G*N@51eqPQSdW_k1&m|mtrDK-4s%t7zOl!2}(~=a^h#h{_F=>WcZ5Kdml~;l+c2SgZG<+ukAjSKj@RFb;7`FQg=7xiGRcU!1BLCCCG0M zR*h{Ivi)!1irI&;nS+eJtxf7ZQvRiYBh~d90Tvm=uG!AKzAV;5-2%~?(X2BBCgqcK z$6lty>8g})!EF6 z!Sh`Y%dzwpPEANMU7+uzxo7Kfb$hQ}rD{z`DLlT^27DfOG8k^;nml(`&*${4Rk|h5Uw^J_jP#oAC!=7=Rp8tN6@3nzynpSVn>;|w^ zi%D6vIJ2NZ`grmSbvP`lTHHNUif&fik|sh*{t(K;1&txq_-vtYZzVtXv!w{S3h z1=TsV`{1cJ*>OD9_UNMe+00y_%U)YiAclrX(5lp!JYQ9rSChDbr_j~NnW0r1_Kj;< z&;u(l!+T1@!-eZb1+LjbcV3Kkx4DwZDVp#CMN?}YM7Zezumax3-9*Rp5!K0G$bGG1 zg>1x1dU>`-FR3H%1{;xdRI{?BR!60;EaAQDdDP0iEq#vSBKl2^E5#I@++PJsFb~_p zaRHvcS7IcyX3Uy35#BFT`zqXCaeVh?eXP7?4!zNRD?EQ{D8IdFLf;z!F`BpvGuoEw z{#IVoU*nen_h2k2zZ|Ld-MOX_)}LP95UrHezHk@E@`@P@%itWG3&JkQ4Qqw$HAGW8 z8G+jbHO=`k7kgodNY;ki1Izx6$1%9xPmE}$;dMzIS|N39Q2sb)Z0`J9v%>k3p5{M$ zN(*Ef<8H2c%h?yTP{%vv9&JdUttviBi{Qj>#t^W2jo$4FjgbS4%7|;C(*+6Y8yE{@ z!)Vf-e?nSig@-~747HCO)4(G*5qZ1i9|P$pv%l--zE{hN5}fz5$T-@0BoAlM@V8uF z8Nsqw=@!b|Qyg~RMV#uRLe1!wF>_Xj6{7`;hOGsZ_6R zCTr3HDLM65e+y!y=?N`w(pDvDYawYa0+-&+_l)TgHq~5mjMs?>sW(`VPi}SAil|!& zU|WuCTJHJ(oE|ZPSBr=?-JOvtfq8Wt+8)vQtV|vwsOW3%fY8iS_v#PV=*51XAUj0S zI7i|Rf%WVjqxh&-c=PFNx6fx&Ll%k`vts6oZHwOE9+y|JH*6W}Fie518X6))d`igl zX-Y3(?|CyGMtENP8UgZE4>z0N?O9>p<^sX1PnIsK`*(+;1P6|3rp!8dd={mIA!U~; zT9B3*&PA$qb{9F6uIUf+!%Puels*-T#pW*VOh@g#GQD}7rsy{@@@jB9tfR60uxZCj z@$E}^UJ?6jpCM6*Gjr^ly6l2KbtZF+CN_Ak0}Li_#&Q8%UTtlDW29=arI9rvI#j{EO1V|%ZO?T0p`{J2nQx?t zy(n^U21h&yXZUhgDwY*Yh^)D<5{;1=RW5y=v&d|EOY2n74%>kDWl8}6l*Nl8~RxTuLBfhBy9ZuYpVW6+cI_F1S zo$OymYHmmBwDv6hbm~s~lfL*PuY8WgAtKaG znf{?}^&jK+o~+$Fo?^t%pE}3+3wf8!#Q*>R literal 0 HcmV?d00001 diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/.resource/6f60ca35b0e4423fb49f9e097449fd99 b/CliClient/tests/support/syncTargetSnapshots/2/normal/.resource/6f60ca35b0e4423fb49f9e097449fd99 new file mode 100644 index 0000000000000000000000000000000000000000..b258679de6bf0f62a6ae9c65f3b8e85d1dba3c50 GIT binary patch literal 2720 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 literal 0 HcmV?d00001 diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/.sync/readme.txt b/CliClient/tests/support/syncTargetSnapshots/2/normal/.sync/readme.txt new file mode 100644 index 000000000..e46819c44 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/.sync/readme.txt @@ -0,0 +1 @@ +2020-07-16: In the new sync format, the version number is stored in /info.json. However, for backward compatibility, we need to keep the old version.txt file here, otherwise old clients will automatically recreate it, and assume a sync target version 1. So we keep it here but set its value to "2", so that old clients know that they need to be upgraded. This directory can be removed after a year or so, once we are confident that all clients have been upgraded to recent versions. \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/.sync/version.txt b/CliClient/tests/support/syncTargetSnapshots/2/normal/.sync/version.txt new file mode 100644 index 000000000..d8263ee98 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/.sync/version.txt @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/006a89df4de64a22b4b1fa71f87fd258.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/006a89df4de64a22b4b1fa71f87fd258.md new file mode 100644 index 000000000..f9a6541b2 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/006a89df4de64a22b4b1fa71f87fd258.md @@ -0,0 +1,16 @@ +photo.jpg + +id: 006a89df4de64a22b4b1fa71f87fd258 +mime: image/jpeg +filename: +created_time: 2020-07-25T10:55:18.547Z +updated_time: 2020-07-25T10:55:18.547Z +user_created_time: 2020-07-25T10:55:18.547Z +user_updated_time: 2020-07-25T10:55:18.547Z +file_extension: jpg +encryption_cipher_text: +encryption_applied: 0 +encryption_blob_encrypted: 0 +size: 2720 +is_shared: 0 +type_: 4 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/106ec766eba54715b19dc899e1de6906.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/106ec766eba54715b19dc899e1de6906.md new file mode 100644 index 000000000..b129e27f4 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/106ec766eba54715b19dc899e1de6906.md @@ -0,0 +1,11 @@ +id: 106ec766eba54715b19dc899e1de6906 +note_id: bf551517ef7c40be9477168677d0b77a +tag_id: b684a65012c74c508b891935ecf2f5b1 +created_time: 2020-07-25T10:55:18.556Z +updated_time: 2020-07-25T10:55:18.556Z +user_created_time: 2020-07-25T10:55:18.556Z +user_updated_time: 2020-07-25T10:55:18.556Z +encryption_cipher_text: +encryption_applied: 0 +is_shared: 0 +type_: 6 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/23d35df6c34848ec86c42c3194051ecc.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/23d35df6c34848ec86c42c3194051ecc.md new file mode 100644 index 000000000..27eb2db6e --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/23d35df6c34848ec86c42c3194051ecc.md @@ -0,0 +1,11 @@ +id: 23d35df6c34848ec86c42c3194051ecc +note_id: a91bf5ddf3a749d2be010e9a04e5a1cc +tag_id: b684a65012c74c508b891935ecf2f5b1 +created_time: 2020-07-25T10:55:18.439Z +updated_time: 2020-07-25T10:55:18.439Z +user_created_time: 2020-07-25T10:55:18.439Z +user_updated_time: 2020-07-25T10:55:18.439Z +encryption_cipher_text: +encryption_applied: 0 +is_shared: 0 +type_: 6 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/2a914b3fb8fb43819b976eb4e5be80e3.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/2a914b3fb8fb43819b976eb4e5be80e3.md new file mode 100644 index 000000000..857847f64 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/2a914b3fb8fb43819b976eb4e5be80e3.md @@ -0,0 +1,28 @@ +note1 + +![photo.jpg](:/6f60ca35b0e4423fb49f9e097449fd99) + +id: 2a914b3fb8fb43819b976eb4e5be80e3 +parent_id: 2fa39884ba3b47a489dae93dc20021f2 +created_time: 2020-07-25T10:55:18.127Z +updated_time: 2020-07-25T10:55:18.403Z +is_conflict: 0 +latitude: 0.00000000 +longitude: 0.00000000 +altitude: 0.0000 +author: +source_url: +is_todo: 0 +todo_due: 0 +todo_completed: 0 +source: joplin +source_application: net.cozic.joplintest-cli +application_data: +order: 1595674518127 +user_created_time: 2020-07-25T10:55:18.127Z +user_updated_time: 2020-07-25T10:55:18.403Z +encryption_cipher_text: +encryption_applied: 0 +markup_language: 1 +is_shared: 0 +type_: 1 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/2fa39884ba3b47a489dae93dc20021f2.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/2fa39884ba3b47a489dae93dc20021f2.md new file mode 100644 index 000000000..1ebd477e4 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/2fa39884ba3b47a489dae93dc20021f2.md @@ -0,0 +1,12 @@ +subFolder2 + +id: 2fa39884ba3b47a489dae93dc20021f2 +created_time: 2020-07-25T10:55:18.125Z +updated_time: 2020-07-25T10:55:18.125Z +user_created_time: 2020-07-25T10:55:18.125Z +user_updated_time: 2020-07-25T10:55:18.125Z +encryption_cipher_text: +encryption_applied: 0 +parent_id: c4e45cadb2e84beb801980155a707e21 +is_shared: 0 +type_: 2 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/352dcd65cd6e4b09a93669378b8e2b50.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/352dcd65cd6e4b09a93669378b8e2b50.md new file mode 100644 index 000000000..da9b3eec2 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/352dcd65cd6e4b09a93669378b8e2b50.md @@ -0,0 +1,12 @@ +subFolder1 + +id: 352dcd65cd6e4b09a93669378b8e2b50 +created_time: 2020-07-25T10:55:18.122Z +updated_time: 2020-07-25T10:55:18.122Z +user_created_time: 2020-07-25T10:55:18.122Z +user_updated_time: 2020-07-25T10:55:18.122Z +encryption_cipher_text: +encryption_applied: 0 +parent_id: c4e45cadb2e84beb801980155a707e21 +is_shared: 0 +type_: 2 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/38341af5a8764d4d9318f58f778e3240.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/38341af5a8764d4d9318f58f778e3240.md new file mode 100644 index 000000000..394b5ca71 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/38341af5a8764d4d9318f58f778e3240.md @@ -0,0 +1,11 @@ +id: 38341af5a8764d4d9318f58f778e3240 +note_id: 45867f53ece54da38eed83288882e374 +tag_id: 6cb91bb296ee458589eea0256ada06fa +created_time: 2020-07-25T10:55:18.425Z +updated_time: 2020-07-25T10:55:18.425Z +user_created_time: 2020-07-25T10:55:18.425Z +user_updated_time: 2020-07-25T10:55:18.425Z +encryption_cipher_text: +encryption_applied: 0 +is_shared: 0 +type_: 6 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/40f117103de1405586b4289a55e0ea22.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/40f117103de1405586b4289a55e0ea22.md new file mode 100644 index 000000000..5119de180 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/40f117103de1405586b4289a55e0ea22.md @@ -0,0 +1,12 @@ +folder3 + +id: 40f117103de1405586b4289a55e0ea22 +created_time: 2020-07-25T10:55:18.443Z +updated_time: 2020-07-25T10:55:18.443Z +user_created_time: 2020-07-25T10:55:18.443Z +user_updated_time: 2020-07-25T10:55:18.443Z +encryption_cipher_text: +encryption_applied: 0 +parent_id: +is_shared: 0 +type_: 2 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/45867f53ece54da38eed83288882e374.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/45867f53ece54da38eed83288882e374.md new file mode 100644 index 000000000..0c230db7c --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/45867f53ece54da38eed83288882e374.md @@ -0,0 +1,26 @@ +note3 + +id: 45867f53ece54da38eed83288882e374 +parent_id: c4e45cadb2e84beb801980155a707e21 +created_time: 2020-07-25T10:55:18.424Z +updated_time: 2020-07-25T10:55:18.424Z +is_conflict: 0 +latitude: 0.00000000 +longitude: 0.00000000 +altitude: 0.0000 +author: +source_url: +is_todo: 0 +todo_due: 0 +todo_completed: 0 +source: joplin +source_application: net.cozic.joplintest-cli +application_data: +order: 1595674518424 +user_created_time: 2020-07-25T10:55:18.424Z +user_updated_time: 2020-07-25T10:55:18.424Z +encryption_cipher_text: +encryption_applied: 0 +markup_language: 1 +is_shared: 0 +type_: 1 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/50fdc4447c334b00a4dde44344aceb25.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/50fdc4447c334b00a4dde44344aceb25.md new file mode 100644 index 000000000..dc2ea29bb --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/50fdc4447c334b00a4dde44344aceb25.md @@ -0,0 +1,11 @@ +id: 50fdc4447c334b00a4dde44344aceb25 +note_id: 45867f53ece54da38eed83288882e374 +tag_id: b684a65012c74c508b891935ecf2f5b1 +created_time: 2020-07-25T10:55:18.434Z +updated_time: 2020-07-25T10:55:18.434Z +user_created_time: 2020-07-25T10:55:18.434Z +user_updated_time: 2020-07-25T10:55:18.434Z +encryption_cipher_text: +encryption_applied: 0 +is_shared: 0 +type_: 6 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/567486477f4249d38feadf6c5ec6e03d.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/567486477f4249d38feadf6c5ec6e03d.md new file mode 100644 index 000000000..d44a1a0aa --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/567486477f4249d38feadf6c5ec6e03d.md @@ -0,0 +1,11 @@ +id: 567486477f4249d38feadf6c5ec6e03d +note_id: 2a914b3fb8fb43819b976eb4e5be80e3 +tag_id: 6cb91bb296ee458589eea0256ada06fa +created_time: 2020-07-25T10:55:18.416Z +updated_time: 2020-07-25T10:55:18.416Z +user_created_time: 2020-07-25T10:55:18.416Z +user_updated_time: 2020-07-25T10:55:18.416Z +encryption_cipher_text: +encryption_applied: 0 +is_shared: 0 +type_: 6 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/6cb91bb296ee458589eea0256ada06fa.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/6cb91bb296ee458589eea0256ada06fa.md new file mode 100644 index 000000000..933027e48 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/6cb91bb296ee458589eea0256ada06fa.md @@ -0,0 +1,12 @@ +tag1 + +id: 6cb91bb296ee458589eea0256ada06fa +created_time: 2020-07-25T10:55:18.411Z +updated_time: 2020-07-25T10:55:18.411Z +user_created_time: 2020-07-25T10:55:18.411Z +user_updated_time: 2020-07-25T10:55:18.411Z +encryption_cipher_text: +encryption_applied: 0 +is_shared: 0 +parent_id: +type_: 5 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/6f60ca35b0e4423fb49f9e097449fd99.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/6f60ca35b0e4423fb49f9e097449fd99.md new file mode 100644 index 000000000..91723b711 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/6f60ca35b0e4423fb49f9e097449fd99.md @@ -0,0 +1,16 @@ +photo.jpg + +id: 6f60ca35b0e4423fb49f9e097449fd99 +mime: image/jpeg +filename: +created_time: 2020-07-25T10:55:18.397Z +updated_time: 2020-07-25T10:55:18.397Z +user_created_time: 2020-07-25T10:55:18.397Z +user_updated_time: 2020-07-25T10:55:18.397Z +file_extension: jpg +encryption_cipher_text: +encryption_applied: 0 +encryption_blob_encrypted: 0 +size: 2720 +is_shared: 0 +type_: 4 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/7ae4083db8e64328a4d3ccab6279c6bf.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/7ae4083db8e64328a4d3ccab6279c6bf.md new file mode 100644 index 000000000..91d42f6ab --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/7ae4083db8e64328a4d3ccab6279c6bf.md @@ -0,0 +1,12 @@ +folder2 + +id: 7ae4083db8e64328a4d3ccab6279c6bf +created_time: 2020-07-25T10:55:18.442Z +updated_time: 2020-07-25T10:55:18.442Z +user_created_time: 2020-07-25T10:55:18.442Z +user_updated_time: 2020-07-25T10:55:18.442Z +encryption_cipher_text: +encryption_applied: 0 +parent_id: +is_shared: 0 +type_: 2 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/a91bf5ddf3a749d2be010e9a04e5a1cc.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/a91bf5ddf3a749d2be010e9a04e5a1cc.md new file mode 100644 index 000000000..898b8a407 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/a91bf5ddf3a749d2be010e9a04e5a1cc.md @@ -0,0 +1,26 @@ +note4 + +id: a91bf5ddf3a749d2be010e9a04e5a1cc +parent_id: c4e45cadb2e84beb801980155a707e21 +created_time: 2020-07-25T10:55:18.437Z +updated_time: 2020-07-25T10:55:18.437Z +is_conflict: 0 +latitude: 0.00000000 +longitude: 0.00000000 +altitude: 0.0000 +author: +source_url: +is_todo: 0 +todo_due: 0 +todo_completed: 0 +source: joplin +source_application: net.cozic.joplintest-cli +application_data: +order: 1595674518437 +user_created_time: 2020-07-25T10:55:18.437Z +user_updated_time: 2020-07-25T10:55:18.437Z +encryption_cipher_text: +encryption_applied: 0 +markup_language: 1 +is_shared: 0 +type_: 1 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/b684a65012c74c508b891935ecf2f5b1.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/b684a65012c74c508b891935ecf2f5b1.md new file mode 100644 index 000000000..915146fbb --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/b684a65012c74c508b891935ecf2f5b1.md @@ -0,0 +1,12 @@ +tag2 + +id: b684a65012c74c508b891935ecf2f5b1 +created_time: 2020-07-25T10:55:18.432Z +updated_time: 2020-07-25T10:55:18.432Z +user_created_time: 2020-07-25T10:55:18.432Z +user_updated_time: 2020-07-25T10:55:18.432Z +encryption_cipher_text: +encryption_applied: 0 +is_shared: 0 +parent_id: +type_: 5 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/bf551517ef7c40be9477168677d0b77a.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/bf551517ef7c40be9477168677d0b77a.md new file mode 100644 index 000000000..121187870 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/bf551517ef7c40be9477168677d0b77a.md @@ -0,0 +1,28 @@ +note5 + +![photo.jpg](:/006a89df4de64a22b4b1fa71f87fd258) + +id: bf551517ef7c40be9477168677d0b77a +parent_id: 40f117103de1405586b4289a55e0ea22 +created_time: 2020-07-25T10:55:18.444Z +updated_time: 2020-07-25T10:55:18.551Z +is_conflict: 0 +latitude: 0.00000000 +longitude: 0.00000000 +altitude: 0.0000 +author: +source_url: +is_todo: 0 +todo_due: 0 +todo_completed: 0 +source: joplin +source_application: net.cozic.joplintest-cli +application_data: +order: 1595674518444 +user_created_time: 2020-07-25T10:55:18.444Z +user_updated_time: 2020-07-25T10:55:18.551Z +encryption_cipher_text: +encryption_applied: 0 +markup_language: 1 +is_shared: 0 +type_: 1 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/c4e45cadb2e84beb801980155a707e21.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/c4e45cadb2e84beb801980155a707e21.md new file mode 100644 index 000000000..14ca3c559 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/c4e45cadb2e84beb801980155a707e21.md @@ -0,0 +1,12 @@ +folder1 + +id: c4e45cadb2e84beb801980155a707e21 +created_time: 2020-07-25T10:55:18.120Z +updated_time: 2020-07-25T10:55:18.120Z +user_created_time: 2020-07-25T10:55:18.120Z +user_updated_time: 2020-07-25T10:55:18.120Z +encryption_cipher_text: +encryption_applied: 0 +parent_id: +is_shared: 0 +type_: 2 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/edd3cb394ada4d389c01c2bdca09b3ef.md b/CliClient/tests/support/syncTargetSnapshots/2/normal/edd3cb394ada4d389c01c2bdca09b3ef.md new file mode 100644 index 000000000..51d2f4e7c --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/edd3cb394ada4d389c01c2bdca09b3ef.md @@ -0,0 +1,26 @@ +note2 + +id: edd3cb394ada4d389c01c2bdca09b3ef +parent_id: 2fa39884ba3b47a489dae93dc20021f2 +created_time: 2020-07-25T10:55:18.422Z +updated_time: 2020-07-25T10:55:18.422Z +is_conflict: 0 +latitude: 0.00000000 +longitude: 0.00000000 +altitude: 0.0000 +author: +source_url: +is_todo: 0 +todo_due: 0 +todo_completed: 0 +source: joplin +source_application: net.cozic.joplintest-cli +application_data: +order: 1595674518422 +user_created_time: 2020-07-25T10:55:18.422Z +user_updated_time: 2020-07-25T10:55:18.422Z +encryption_cipher_text: +encryption_applied: 0 +markup_language: 1 +is_shared: 0 +type_: 1 \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetSnapshots/2/normal/info.json b/CliClient/tests/support/syncTargetSnapshots/2/normal/info.json new file mode 100644 index 000000000..218abba16 --- /dev/null +++ b/CliClient/tests/support/syncTargetSnapshots/2/normal/info.json @@ -0,0 +1 @@ +{"version":2} \ No newline at end of file diff --git a/CliClient/tests/support/syncTargetUtils.js b/CliClient/tests/support/syncTargetUtils.js index 8935451a7..22235d780 100644 --- a/CliClient/tests/support/syncTargetUtils.js +++ b/CliClient/tests/support/syncTargetUtils.js @@ -5,9 +5,13 @@ const Setting = require('lib/models/Setting'); const Folder = require('lib/models/Folder'); const Note = require('lib/models/Note'); const Tag = require('lib/models/Tag'); +const Resource = require('lib/models/Resource'); +const markdownUtils = require('lib/markdownUtils'); const {shim} = require('lib/shim'); const fs = require('fs-extra'); +const snapshotBaseDir = `${__dirname}/../../tests/support/syncTargetSnapshots`; + const testData = { folder1: { subFolder1: {}, @@ -96,6 +100,12 @@ async function checkTestData(data) { await recurseCheck(data); } +async function deploySyncTargetSnapshot(syncTargetType, syncVersion) { + const sourceDir = `${snapshotBaseDir}/${syncVersion}/${syncTargetType}`; + await fs.remove(syncDir); + await fs.copy(sourceDir, syncDir); +} + async function main(syncTargetType) { const validSyncTargetTypes = ['normal', 'e2ee']; if (!validSyncTargetTypes.includes(syncTargetType)) throw new Error('Sync target type must be: ' + validSyncTargetTypes.join(', ')); @@ -112,7 +122,7 @@ async function main(syncTargetType) { await synchronizer().start(); if (!Setting.value('syncVersion')) throw new Error('syncVersion is not set'); - const destDir = `${__dirname}/../../tests/support/syncTargetSnapshots/${Setting.value('syncVersion')}/${syncTargetType}`; + const destDir = `${snapshotBaseDir}/${Setting.value('syncVersion')}/${syncTargetType}`; await fs.mkdirp(destDir); // Create intermediate directories await fs.remove(destDir); await fs.mkdirp(destDir); @@ -125,4 +135,5 @@ module.exports = { checkTestData, main, testData, + deploySyncTargetSnapshot, }; \ No newline at end of file diff --git a/CliClient/tests/synchronizer.js b/CliClient/tests/synchronizer.js index edcfec69d..38c589d13 100644 --- a/CliClient/tests/synchronizer.js +++ b/CliClient/tests/synchronizer.js @@ -3,7 +3,7 @@ require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); -const { setupDatabase, allSyncTargetItemsEncrypted, tempFilePath, resourceFetcher, kvStore, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, checkThrowAsync, asyncTest } = require('test-utils.js'); +const { setupDatabase, synchronizerStart, syncTargetName, allSyncTargetItemsEncrypted, tempFilePath, resourceFetcher, kvStore, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, checkThrowAsync, asyncTest } = require('test-utils.js'); const { shim } = require('lib/shim.js'); const fs = require('fs-extra'); const Folder = require('lib/models/Folder.js'); @@ -24,8 +24,6 @@ process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); }); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 + 30000; // The first test is slow because the database needs to be built - async function allNotesFolders() { const folders = await Folder.all(); const notes = await Note.all(); @@ -33,7 +31,7 @@ async function allNotesFolders() { } async function remoteItemsByTypes(types) { - const list = await fileApi().list(); + const list = await fileApi().list('', { includeDirs: false, syncItemsOnly: true }); if (list.has_more) throw new Error('Not implemented!!!'); const files = list.items; @@ -106,7 +104,7 @@ describe('synchronizer', function() { const all = await allNotesFolders(); - await synchronizer().start(); + await synchronizerStart(); await localNotesFoldersSameAsRemote(all, expect); })); @@ -114,12 +112,12 @@ describe('synchronizer', function() { it('should update remote items', asyncTest(async () => { const folder = await Folder.save({ title: 'folder1' }); const note = await Note.save({ title: 'un', parent_id: folder.id }); - await synchronizer().start(); + await synchronizerStart(); await Note.save({ title: 'un UPDATE', id: note.id }); const all = await allNotesFolders(); - await synchronizer().start(); + await synchronizerStart(); await localNotesFoldersSameAsRemote(all, expect); })); @@ -127,11 +125,11 @@ describe('synchronizer', function() { it('should create local items', asyncTest(async () => { const folder = await Folder.save({ title: 'folder1' }); await Note.save({ title: 'un', parent_id: folder.id }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); const all = await allNotesFolders(); @@ -141,11 +139,11 @@ describe('synchronizer', function() { it('should update local items', asyncTest(async () => { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'un', parent_id: folder1.id }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); await sleep(0.1); @@ -154,11 +152,11 @@ describe('synchronizer', function() { await Note.save(note2); note2 = await Note.load(note2.id); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); - await synchronizer().start(); + await synchronizerStart(); const all = await allNotesFolders(); @@ -168,16 +166,16 @@ describe('synchronizer', function() { it('should resolve note conflicts', asyncTest(async () => { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'un', parent_id: folder1.id }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); let note2 = await Note.load(note1.id); note2.title = 'Updated on client 2'; await Note.save(note2); note2 = await Note.load(note2.id); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); @@ -185,7 +183,7 @@ describe('synchronizer', function() { note2conf.title = 'Updated on client 1'; await Note.save(note2conf); note2conf = await Note.load(note1.id); - await synchronizer().start(); + await synchronizerStart(); const conflictedNotes = await Note.conflictedNotes(); expect(conflictedNotes.length).toBe(1); @@ -209,11 +207,11 @@ describe('synchronizer', function() { it('should resolve folders conflicts', asyncTest(async () => { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'un', parent_id: folder1.id }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); // ---------------------------------- - await synchronizer().start(); + await synchronizerStart(); await sleep(0.1); @@ -222,7 +220,7 @@ describe('synchronizer', function() { await Folder.save(folder1_modRemote); folder1_modRemote = await Folder.load(folder1_modRemote.id); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); // ---------------------------------- @@ -233,7 +231,7 @@ describe('synchronizer', function() { await Folder.save(folder1_modLocal); folder1_modLocal = await Folder.load(folder1.id); - await synchronizer().start(); + await synchronizerStart(); const folder1_final = await Folder.load(folder1.id); expect(folder1_final.title).toBe(folder1_modRemote.title); @@ -242,17 +240,17 @@ describe('synchronizer', function() { it('should delete remote notes', asyncTest(async () => { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'un', parent_id: folder1.id }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); await sleep(0.1); await Note.delete(note1.id); - await synchronizer().start(); + await synchronizerStart(); const remotes = await remoteNotesAndFolders(); expect(remotes.length).toBe(1); @@ -265,17 +263,17 @@ describe('synchronizer', function() { it('should not created deleted_items entries for items deleted via sync', asyncTest(async () => { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'un', parent_id: folder1.id }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); await Folder.delete(folder1.id); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); - await synchronizer().start(); + await synchronizerStart(); const deletedItems = await BaseItem.deletedItems(syncTargetId()); expect(deletedItems.length).toBe(0); })); @@ -288,39 +286,39 @@ describe('synchronizer', function() { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'un', parent_id: folder1.id }); const note2 = await Note.save({ title: 'deux', parent_id: folder1.id }); - let context1 = await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - let context2 = await synchronizer().start(); + await synchronizerStart(); await Note.delete(note1.id); - context2 = await synchronizer().start({ context: context2 }); + await synchronizerStart(); await switchClient(1); - context1 = await synchronizer().start({ context: context1 }); + await synchronizerStart(); const items = await allNotesFolders(); expect(items.length).toBe(2); const deletedItems = await BaseItem.deletedItems(syncTargetId()); expect(deletedItems.length).toBe(0); await Note.delete(note2.id); - context1 = await synchronizer().start({ context: context1 }); + await synchronizerStart(); })); it('should delete remote folder', asyncTest(async () => { const folder1 = await Folder.save({ title: 'folder1' }); const folder2 = await Folder.save({ title: 'folder2' }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); await sleep(0.1); await Folder.delete(folder2.id); - await synchronizer().start(); + await synchronizerStart(); const all = await allNotesFolders(); await localNotesFoldersSameAsRemote(all, expect); @@ -329,35 +327,35 @@ describe('synchronizer', function() { it('should delete local folder', asyncTest(async () => { const folder1 = await Folder.save({ title: 'folder1' }); const folder2 = await Folder.save({ title: 'folder2' }); - const context1 = await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - const context2 = await synchronizer().start(); + await synchronizerStart(); await Folder.delete(folder2.id); - await synchronizer().start({ context: context2 }); + await synchronizerStart(); await switchClient(1); - await synchronizer().start({ context: context1 }); + await synchronizerStart(); const items = await allNotesFolders(); await localNotesFoldersSameAsRemote(items, expect); })); it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', asyncTest(async () => { const folder1 = await Folder.save({ title: 'folder1' }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); await Folder.delete(folder1.id); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); const note = await Note.save({ title: 'note1', parent_id: folder1.id }); - await synchronizer().start(); + await synchronizerStart(); const items = await allNotesFolders(); expect(items.length).toBe(1); expect(items[0].title).toBe('note1'); @@ -367,18 +365,18 @@ describe('synchronizer', function() { it('should resolve conflict if note has been deleted remotely and locally', asyncTest(async () => { const folder = await Folder.save({ title: 'folder' }); const note = await Note.save({ title: 'note', parent_id: folder.title }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); await Note.delete(note.id); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); await Note.delete(note.id); - await synchronizer().start(); + await synchronizerStart(); const items = await allNotesFolders(); expect(items.length).toBe(1); @@ -393,34 +391,28 @@ describe('synchronizer', function() { const folder1 = await Folder.save({ title: 'folder1' }); const folder2 = await Folder.save({ title: 'folder2' }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); - + await synchronizerStart(); await sleep(0.1); - await Folder.delete(folder1.id); await switchClient(1); await Folder.delete(folder2.id); - - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); - + await synchronizerStart(); const items2 = await allNotesFolders(); await switchClient(1); - await synchronizer().start(); - + await synchronizerStart(); const items1 = await allNotesFolders(); - expect(items1.length).toBe(0); expect(items1.length).toBe(items2.length); })); @@ -428,24 +420,24 @@ describe('synchronizer', function() { it('should handle conflict when remote note is deleted then local note is modified', asyncTest(async () => { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'un', parent_id: folder1.id }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); await sleep(0.1); await Note.delete(note1.id); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); const newTitle = 'Modified after having been deleted'; await Note.save({ id: note1.id, title: newTitle }); - await synchronizer().start(); + await synchronizerStart(); const conflictedNotes = await Note.conflictedNotes(); @@ -461,17 +453,17 @@ describe('synchronizer', function() { const folder1 = await Folder.save({ title: 'folder1' }); const folder2 = await Folder.save({ title: 'folder2' }); const note1 = await Note.save({ title: 'un', parent_id: folder1.id }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); await sleep(0.1); await Folder.delete(folder1.id); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); @@ -480,7 +472,7 @@ describe('synchronizer', function() { const newTitle = 'Modified after having been deleted'; await Folder.save({ id: folder1.id, title: newTitle }); - await synchronizer().start(); + await synchronizerStart(); const items = await allNotesFolders(); @@ -493,13 +485,13 @@ describe('synchronizer', function() { await switchClient(2); let remoteF2 = await Folder.save({ title: 'folder' }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); await sleep(0.1); - await synchronizer().start(); + await synchronizerStart(); const localF2 = await Folder.load(remoteF2.id); @@ -509,12 +501,12 @@ describe('synchronizer', function() { // that synchronizing it applies the title change remotely, and that new title // should be retrieved by client 2. - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); await sleep(0.1); - await synchronizer().start(); + await synchronizerStart(); remoteF2 = await Folder.load(remoteF2.id); @@ -532,11 +524,11 @@ describe('synchronizer', function() { const n1 = await Note.save({ title: 'mynote' }); const n2 = await Note.save({ title: 'mynote2' }); const tag = await Tag.save({ title: 'mytag' }); - let context1 = await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - let context2 = await synchronizer().start(); + await synchronizerStart(); if (withEncryption) { const masterKey_2 = await MasterKey.load(masterKey.id); await encryptionService().loadMasterKey_(masterKey_2, '123456', true); @@ -550,21 +542,21 @@ describe('synchronizer', function() { await Tag.addNote(remoteTag.id, n2.id); let noteIds = await Tag.noteIds(tag.id); expect(noteIds.length).toBe(2); - context2 = await synchronizer().start({ context: context2 }); + await synchronizerStart(); await switchClient(1); - context1 = await synchronizer().start({ context: context1 }); + await synchronizerStart(); let remoteNoteIds = await Tag.noteIds(tag.id); expect(remoteNoteIds.length).toBe(2); await Tag.removeNote(tag.id, n1.id); remoteNoteIds = await Tag.noteIds(tag.id); expect(remoteNoteIds.length).toBe(1); - context1 = await synchronizer().start({ context: context1 }); + await synchronizerStart(); await switchClient(2); - context2 = await synchronizer().start({ context: context2 }); + await synchronizerStart(); noteIds = await Tag.noteIds(tag.id); expect(noteIds.length).toBe(1); expect(remoteNoteIds[0]).toBe(noteIds[0]); @@ -581,11 +573,11 @@ describe('synchronizer', function() { it('should not sync notes with conflicts', asyncTest(async () => { const f1 = await Folder.save({ title: 'folder' }); const n1 = await Note.save({ title: 'mynote', parent_id: f1.id, is_conflict: 1 }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); const notes = await Note.all(); const folders = await Folder.all(); expect(notes.length).toBe(0); @@ -595,11 +587,11 @@ describe('synchronizer', function() { it('should not try to delete on remote conflicted notes that have been deleted', asyncTest(async () => { const f1 = await Folder.save({ title: 'folder' }); const n1 = await Note.save({ title: 'mynote', parent_id: f1.id }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); await Note.save({ id: n1.id, is_conflict: 1 }); await Note.delete(n1.id); const deletedItems = await BaseItem.deletedItems(syncTargetId()); @@ -615,11 +607,11 @@ describe('synchronizer', function() { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'un', is_todo: 1, parent_id: folder1.id }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); if (withEncryption) { await loadEncryptionMasterKey(null, true); await decryptionWorker().start(); @@ -628,7 +620,7 @@ describe('synchronizer', function() { note2.todo_completed = time.unixMs() - 1; await Note.save(note2); note2 = await Note.load(note2.id); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); @@ -636,7 +628,7 @@ describe('synchronizer', function() { note2conf.todo_completed = time.unixMs(); await Note.save(note2conf); note2conf = await Note.load(note1.id); - await synchronizer().start(); + await synchronizerStart(); if (!withEncryption) { // That was previously a common conflict: @@ -677,17 +669,17 @@ describe('synchronizer', function() { it('items should be downloaded again when user cancels in the middle of delta operation', asyncTest(async () => { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'un', is_todo: 1, parent_id: folder1.id }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); synchronizer().testingHooks_ = ['cancelDeltaLoop2']; - const context = await synchronizer().start(); + await synchronizerStart(); let notes = await Note.all(); expect(notes.length).toBe(0); synchronizer().testingHooks_ = []; - await synchronizer().start({ context: context }); + await synchronizerStart(); notes = await Note.all(); expect(notes.length).toBe(1); })); @@ -696,18 +688,18 @@ describe('synchronizer', function() { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'un', is_todo: 1, parent_id: folder1.id }); const noteId = note1.id; - await synchronizer().start(); + await synchronizerStart(); let disabledItems = await BaseItem.syncDisabledItems(syncTargetId()); expect(disabledItems.length).toBe(0); await Note.save({ id: noteId, title: 'un mod' }); synchronizer().testingHooks_ = ['notesRejectedByTarget']; - await synchronizer().start(); + await synchronizerStart(); synchronizer().testingHooks_ = []; - await synchronizer().start(); // Another sync to check that this item is now excluded from sync + await synchronizerStart(); // Another sync to check that this item is now excluded from sync await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); const notes = await Note.all(); expect(notes.length).toBe(1); expect(notes[0].title).toBe('un'); @@ -723,14 +715,14 @@ describe('synchronizer', function() { const masterKey = await loadEncryptionMasterKey(); const folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'un', body: 'to be encrypted', parent_id: folder1.id }); - await synchronizer().start(); + await synchronizerStart(); // After synchronisation, remote items should be encrypted but local ones remain plain text note1 = await Note.load(note1.id); expect(note1.title).toBe('un'); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); let folder1_2 = await Folder.load(folder1.id); let note1_2 = await Note.load(note1.id); const masterKey_2 = await MasterKey.load(masterKey.id); @@ -765,13 +757,13 @@ describe('synchronizer', function() { Setting.setValue('encryption.enabled', true); await loadEncryptionMasterKey(); let folder1 = await Folder.save({ title: 'folder1' }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); // Synchronising should enable encryption since we're going to get a master key expect(Setting.value('encryption.enabled')).toBe(false); - await synchronizer().start(); + await synchronizerStart(); expect(Setting.value('encryption.enabled')).toBe(true); // Check that we got the master key from client 1 @@ -785,11 +777,11 @@ describe('synchronizer', function() { // Technically it's incorrect to set the property of an encrypted variable but it allows confirming // that encryption doesn't work if user hasn't supplied a password. await BaseItem.forceSync(folder1.id); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); - await synchronizer().start(); + await synchronizerStart(); folder1 = await Folder.load(folder1.id); expect(folder1.title).toBe('folder1'); // Still at old value @@ -807,11 +799,11 @@ describe('synchronizer', function() { await Folder.save({ id: folder1.id, title: 'change test' }); // If we sync now, this time client 1 should get the changes we did earlier - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); - await synchronizer().start(); + await synchronizerStart(); // Decrypt the data we just got await decryptionWorker().start(); folder1 = await Folder.load(folder1.id); @@ -821,8 +813,8 @@ describe('synchronizer', function() { it('should encrypt existing notes too when enabling E2EE', asyncTest(async () => { // First create a folder, without encryption enabled, and sync it const folder1 = await Folder.save({ title: 'folder1' }); - await synchronizer().start(); - let files = await fileApi().list(); + await synchronizerStart(); + let files = await fileApi().list('', { includeDirs: false, syncItemsOnly: true }); let content = await fileApi().get(files.items[0].path); expect(content.indexOf('folder1') >= 0).toBe(true); @@ -831,11 +823,11 @@ describe('synchronizer', function() { masterKey = await MasterKey.save(masterKey); await encryptionService().enableEncryption(masterKey, '123456'); await encryptionService().loadMasterKeysFromSettings(); - await synchronizer().start(); + await synchronizerStart(); // Even though the folder has not been changed it should have been synced again so that // an encrypted version of it replaces the decrypted version. - files = await fileApi().list(); + files = await fileApi().list('', { includeDirs: false, syncItemsOnly: true }); expect(files.items.length).toBe(2); // By checking that the folder title is not present, we can confirm that the item has indeed been encrypted // One of the two items is the master key @@ -853,12 +845,12 @@ describe('synchronizer', function() { await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`); const resource1 = (await Resource.all())[0]; const resourcePath1 = Resource.fullPath(resource1); - await synchronizer().start(); + await synchronizerStart(); expect((await remoteNotesFoldersResources()).length).toBe(3); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); const allResources = await Resource.all(); expect(allResources.length).toBe(1); let resource1_2 = allResources[0]; @@ -886,11 +878,11 @@ describe('synchronizer', function() { await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`); let resource1 = (await Resource.all())[0]; const resourcePath1 = Resource.fullPath(resource1); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); const fetcher = new ResourceFetcher(() => { return { @@ -913,11 +905,11 @@ describe('synchronizer', function() { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); let r1 = (await Resource.all())[0]; await Resource.setFileSizeOnly(r1.id, -1); r1 = await Resource.load(r1.id); @@ -938,17 +930,17 @@ describe('synchronizer', function() { await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`); const resource1 = (await Resource.all())[0]; const resourcePath1 = Resource.fullPath(resource1); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); let allResources = await Resource.all(); expect(allResources.length).toBe(1); const all = await fileApi().list(); expect((await remoteNotesFoldersResources()).length).toBe(3); await Resource.delete(resource1.id); - await synchronizer().start(); + await synchronizerStart(); expect((await remoteNotesFoldersResources()).length).toBe(2); const remoteBlob = await fileApi().stat(`.resource/${resource1.id}`); @@ -957,7 +949,7 @@ describe('synchronizer', function() { await switchClient(1); expect(await shim.fsDriver().exists(resourcePath1)).toBe(true); - await synchronizer().start(); + await synchronizerStart(); allResources = await Resource.all(); expect(allResources.length).toBe(0); expect(await shim.fsDriver().exists(resourcePath1)).toBe(false); @@ -972,11 +964,11 @@ describe('synchronizer', function() { await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`); const resource1 = (await Resource.all())[0]; const resourcePath1 = Resource.fullPath(resource1); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456'); await encryptionService().loadMasterKeysFromSettings(); @@ -997,11 +989,11 @@ describe('synchronizer', function() { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, tempFile); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); await resourceFetcher().start(); await resourceFetcher().waitForAllFinished(); let resource1_2 = (await Resource.all())[0]; @@ -1013,11 +1005,11 @@ describe('synchronizer', function() { const newSize = resource1_2.size; expect(originalSize).toBe(4); expect(newSize).toBe(8); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); - await synchronizer().start(); + await synchronizerStart(); await resourceFetcher().start(); await resourceFetcher().waitForAllFinished(); const resource1_1 = (await Resource.all())[0]; @@ -1032,20 +1024,20 @@ describe('synchronizer', function() { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, tempFile); - await synchronizer().start(); + await synchronizerStart(); } await switchClient(2); { - await synchronizer().start(); + await synchronizerStart(); await resourceFetcher().start(); await resourceFetcher().waitForAllFinished(); const resource = (await Resource.all())[0]; const modFile2 = tempFilePath('txt'); await shim.fsDriver().writeFile(modFile2, '1234 MOD 2', 'utf8'); await Resource.updateResourceBlobContent(resource.id, modFile2); - await synchronizer().start(); + await synchronizerStart(); } await switchClient(1); @@ -1056,7 +1048,7 @@ describe('synchronizer', function() { const modFile1 = tempFilePath('txt'); await shim.fsDriver().writeFile(modFile1, '1234 MOD 1', 'utf8'); await Resource.updateResourceBlobContent(resource.id, modFile1); - await synchronizer().start(); // CONFLICT + await synchronizerStart(); // CONFLICT // If we try to read the resource content now, it should throw because the local // content has been moved to the conflict notebook, and the new local content @@ -1092,13 +1084,13 @@ describe('synchronizer', function() { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, tempFile); - await synchronizer().start(); + await synchronizerStart(); } await switchClient(2); { - await synchronizer().start(); + await synchronizerStart(); await resourceFetcher().start(); await resourceFetcher().waitForAllFinished(); } @@ -1108,7 +1100,7 @@ describe('synchronizer', function() { { const resource = (await Resource.all())[0]; await Resource.delete(resource.id); - await synchronizer().start(); + await synchronizerStart(); } @@ -1117,7 +1109,7 @@ describe('synchronizer', function() { { const originalResource = (await Resource.all())[0]; await Resource.save({ id: originalResource.id, title: 'modified resource' }); - await synchronizer().start(); // CONFLICT + await synchronizerStart(); // CONFLICT const deletedResource = await Resource.load(originalResource.id); expect(!deletedResource).toBe(true); @@ -1135,14 +1127,14 @@ describe('synchronizer', function() { const masterKey = await loadEncryptionMasterKey(); const folder1 = await Folder.save({ title: 'folder1' }); - await synchronizer().start(); + await synchronizerStart(); let allEncrypted = await allSyncTargetItemsEncrypted(); expect(allEncrypted).toBe(true); await encryptionService().disableEncryption(); - await synchronizer().start(); + await synchronizerStart(); allEncrypted = await allSyncTargetItemsEncrypted(); expect(allEncrypted).toBe(false); })); @@ -1156,11 +1148,11 @@ describe('synchronizer', function() { const masterKey = await loadEncryptionMasterKey(); const folder1 = await Folder.save({ title: 'folder1' }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); expect(Setting.value('encryption.enabled')).toBe(true); // If we try to disable encryption now, it should throw an error because some items are @@ -1179,7 +1171,7 @@ describe('synchronizer', function() { expect(hasThrown).toBe(false); // If we sync now the target should receive the decrypted items - await synchronizer().start(); + await synchronizerStart(); const allEncrypted = await allSyncTargetItemsEncrypted(); expect(allEncrypted).toBe(false); })); @@ -1194,11 +1186,11 @@ describe('synchronizer', function() { const resource1 = (await Resource.all())[0]; await Resource.setFileSizeOnly(resource1.id, -1); const resourcePath1 = Resource.fullPath(resource1); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456'); await encryptionService().loadMasterKeysFromSettings(); @@ -1217,7 +1209,7 @@ describe('synchronizer', function() { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`); - await synchronizer().start(); + await synchronizerStart(); expect(await allSyncTargetItemsEncrypted()).toBe(false); @@ -1225,7 +1217,7 @@ describe('synchronizer', function() { await encryptionService().enableEncryption(masterKey, '123456'); await encryptionService().loadMasterKeysFromSettings(); - await synchronizer().start(); + await synchronizerStart(); expect(await allSyncTargetItemsEncrypted()).toBe(true); })); @@ -1239,7 +1231,7 @@ describe('synchronizer', function() { const masterKey = await loadEncryptionMasterKey(); await encryptionService().enableEncryption(masterKey, '123456'); await encryptionService().loadMasterKeysFromSettings(); - await synchronizer().start(); + await synchronizerStart(); const resource1 = (await Resource.all())[0]; expect(resource1.encryption_blob_encrypted).toBe(0); @@ -1250,7 +1242,7 @@ describe('synchronizer', function() { await Note.save({ title: 'Fahrräder', body: 'Fahrräder', parent_id: folder.id }); const all = await allNotesFolders(); - await synchronizer().start(); + await synchronizerStart(); await localNotesFoldersSameAsRemote(all, expect); })); @@ -1258,24 +1250,24 @@ describe('synchronizer', function() { it('should update remote items but not pull remote changes', asyncTest(async () => { const folder = await Folder.save({ title: 'folder1' }); const note = await Note.save({ title: 'un', parent_id: folder.id }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); await Note.save({ title: 'deux', parent_id: folder.id }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); await Note.save({ title: 'un UPDATE', id: note.id }); - await synchronizer().start({ syncSteps: ['update_remote'] }); + await synchronizerStart(null, { syncSteps: ['update_remote'] }); const all = await allNotesFolders(); expect(all.length).toBe(2); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); const note2 = await Note.load(note.id); expect(note2.title).toBe('un UPDATE'); })); @@ -1284,7 +1276,7 @@ describe('synchronizer', function() { // Create the Welcome items on two separate clients await WelcomeUtils.createWelcomeItems(); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); @@ -1294,7 +1286,7 @@ describe('synchronizer', function() { expect(beforeFolderCount === 1).toBe(true); expect(beforeNoteCount > 1).toBe(true); - await synchronizer().start(); + await synchronizerStart(); const afterFolderCount = (await Folder.all()).length; const afterNoteCount = (await Note.all()).length; @@ -1307,11 +1299,11 @@ describe('synchronizer', function() { const f1 = (await Folder.all())[0]; await Folder.save({ id: f1.id, title: 'Welcome MOD' }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); - await synchronizer().start(); + await synchronizerStart(); const f1_1 = await Folder.load(f1.id); expect(f1_1.title).toBe('Welcome MOD'); @@ -1324,20 +1316,20 @@ describe('synchronizer', function() { // 2 and is going to be synced. const n1 = await Note.save({ title: 'testing' }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); await Note.save({ id: n1.id, title: 'mod from client 2' }); await revisionService().collectRevisions(); const allRevs1 = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id); expect(allRevs1.length).toBe(1); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); - await synchronizer().start(); + await synchronizerStart(); const allRevs2 = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id); expect(allRevs2.length).toBe(1); expect(allRevs2[0].id).toBe(allRevs1[0].id); @@ -1345,22 +1337,22 @@ describe('synchronizer', function() { it('should not save revisions when deleting a note via sync', asyncTest(async () => { const n1 = await Note.save({ title: 'testing' }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); await Note.delete(n1.id); await revisionService().collectRevisions(); // REV 1 { const allRevs = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id); expect(allRevs.length).toBe(1); } - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); - await synchronizer().start(); // The local note gets deleted here, however a new rev is *not* created + await synchronizerStart(); // The local note gets deleted here, however a new rev is *not* created { const allRevs = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id); expect(allRevs.length).toBe(1); @@ -1382,18 +1374,18 @@ describe('synchronizer', function() { // So in the end we need to make sure that we don't create these unecessary additional revisions. const n1 = await Note.save({ title: 'testing' }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); await Note.save({ id: n1.id, title: 'mod from client 2' }); await revisionService().collectRevisions(); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); - await synchronizer().start(); + await synchronizerStart(); { const allRevs = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id); @@ -1422,6 +1414,9 @@ describe('synchronizer', function() { // due to unecessary data being saved, but a possible edge case and we simply need to check // all the data is valid. + // Note: this test seems to be a bit shaky because it doesn't work if the synchronizer + // context is passed around (via synchronizerStart()), but it should. + const n1 = await Note.save({ title: 'note' }); await Note.save({ id: n1.id, title: 'note REV1' }); await revisionService().collectRevisions(); // REV1 @@ -1455,7 +1450,7 @@ describe('synchronizer', function() { const previousMax = synchronizer().maxResourceSize_; synchronizer().maxResourceSize_ = 1; - await synchronizer().start(); + await synchronizerStart(); synchronizer().maxResourceSize_ = previousMax; const syncItems = await BaseItem.allSyncItems(syncTargetId()); @@ -1481,12 +1476,12 @@ describe('synchronizer', function() { await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`); const resource = (await Resource.all())[0]; await Resource.setLocalState(resource.id, { fetch_status: Resource.FETCH_STATUS_IDLE }); - await synchronizer().start(); + await synchronizerStart(); expect((await remoteResources()).length).toBe(0); await Resource.setLocalState(resource.id, { fetch_status: Resource.FETCH_STATUS_DONE }); - await synchronizer().start(); + await synchronizerStart(); expect((await remoteResources()).length).toBe(1); })); @@ -1497,12 +1492,12 @@ describe('synchronizer', function() { const masterKey = await loadEncryptionMasterKey(); await encryptionService().enableEncryption(masterKey, '123456'); await encryptionService().loadMasterKeysFromSettings(); - await synchronizer().start(); + await synchronizerStart(); expect(await allSyncTargetItemsEncrypted()).toBe(true); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456'); await encryptionService().loadMasterKeysFromSettings(); await decryptionWorker().start(); @@ -1546,11 +1541,11 @@ describe('synchronizer', function() { const masterKey = await loadEncryptionMasterKey(); await encryptionService().enableEncryption(masterKey, '123456'); await encryptionService().loadMasterKeysFromSettings(); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456'); await encryptionService().loadMasterKeysFromSettings(); @@ -1568,11 +1563,11 @@ describe('synchronizer', function() { const masterKey = await loadEncryptionMasterKey(); await encryptionService().enableEncryption(masterKey, '123456'); await encryptionService().loadMasterKeysFromSettings(); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); // First, simulate a broken note and check that the decryption worker // gives up decrypting after a number of tries. This is mainly relevant @@ -1622,63 +1617,32 @@ describe('synchronizer', function() { })); it('should not wipe out user data when syncing with an empty target', asyncTest(async () => { + // Only these targets support the wipeOutFailSafe flag (in other words, the targets that use basicDelta) + if (!['nextcloud', 'memory', 'filesystem', 'amazon_s3'].includes(syncTargetName())) return; + for (let i = 0; i < 10; i++) await Note.save({ title: 'note' }); Setting.setValue('sync.wipeOutFailSafe', true); - await synchronizer().start(); + await synchronizerStart(); await fileApi().clearRoot(); // oops - await synchronizer().start(); + await synchronizerStart(); expect((await Note.all()).length).toBe(10); // but since the fail-safe if on, the notes have not been deleted Setting.setValue('sync.wipeOutFailSafe', false); // Now switch it off - await synchronizer().start(); + await synchronizerStart(); expect((await Note.all()).length).toBe(0); // Since the fail-safe was off, the data has been cleared // Handle case where the sync target has been wiped out, then the user creates one note and sync. for (let i = 0; i < 10; i++) await Note.save({ title: 'note' }); Setting.setValue('sync.wipeOutFailSafe', true); - await synchronizer().start(); + await synchronizerStart(); await fileApi().clearRoot(); await Note.save({ title: 'ma note encore' }); - await synchronizer().start(); + await synchronizerStart(); expect((await Note.all()).length).toBe(11); })); - it('should not sync if client sync version is lower than target', asyncTest(async () => { - // This should work - syncing two clients with same supported sync target version - await synchronizer().start(); - await switchClient(2); - await synchronizer().start(); - - // This should not work - syncing two clients, but one of them has not been upgraded yet to the latest sync version - await switchClient(1); - Setting.setConstant('syncVersion', 2); - await synchronizer().start(); - - await switchClient(2); - Setting.setConstant('syncVersion', 1); - const hasThrown = await checkThrowAsync(async () => synchronizer().start({ throwOnError: true })); - expect(hasThrown).toBe(true); - })); - - it('should not sync when target is locked', asyncTest(async () => { - await synchronizer().start(); - await synchronizer().acquireLock_(); - - await switchClient(2); - const hasThrown = await checkThrowAsync(async () => synchronizer().start({ throwOnError: true })); - expect(hasThrown).toBe(true); - })); - - it('should clear a lock if it was created by the same app as the current one', asyncTest(async () => { - await synchronizer().start(); - await synchronizer().acquireLock_(); - expect((await synchronizer().lockFiles_()).length).toBe(1); - await synchronizer().start({ throwOnError: true }); - expect((await synchronizer().lockFiles_()).length).toBe(0); - })); - it('should not encrypt notes that are shared', asyncTest(async () => { Setting.setValue('encryption.enabled', true); await loadEncryptionMasterKey(); @@ -1686,11 +1650,11 @@ describe('synchronizer', function() { const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'un', parent_id: folder1.id }); let note2 = await Note.save({ title: 'deux', parent_id: folder1.id }); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); await switchClient(1); @@ -1702,11 +1666,11 @@ describe('synchronizer', function() { expect(note2.user_updated_time).toBe(origNote2.user_updated_time); expect(note2.user_created_time).toBe(origNote2.user_created_time); - await synchronizer().start(); + await synchronizerStart(); await switchClient(2); - await synchronizer().start(); + await synchronizerStart(); // The shared note should be decrypted const note2_2 = await Note.load(note2.id); diff --git a/CliClient/tests/synchronizer_LockHandler.ts b/CliClient/tests/synchronizer_LockHandler.ts new file mode 100644 index 000000000..a7d1f1e76 --- /dev/null +++ b/CliClient/tests/synchronizer_LockHandler.ts @@ -0,0 +1,201 @@ +import LockHandler, { LockType, LockHandlerOptions, Lock } from 'lib/services/synchronizer/LockHandler'; + +require('app-module-path').addPath(__dirname); + +const { isNetworkSyncTarget, asyncTest, fileApi, setupDatabaseAndSynchronizer, synchronizer, switchClient, msleep, expectThrow, expectNotThrow } = require('test-utils.js'); + +process.on('unhandledRejection', (reason:any, p:any) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); +}); + +// For tests with memory of file system we can use low intervals to make the tests faster. +// However if we use such low values with network sync targets, some calls might randomly fail with +// ECONNRESET and similar errors (Dropbox or OneDrive migth also throttle). Also we can't use a +// low lock TTL value because the lock might expire between the time it's written and the time it's checked. +// For that reason we add this multiplier for non-memory sync targets. +const timeoutMultipler = isNetworkSyncTarget() ? 100 : 1; + +let lockHandler_:LockHandler = null; + +function newLockHandler(options:LockHandlerOptions = null):LockHandler { + return new LockHandler(fileApi(), options); +} + +function lockHandler():LockHandler { + if (lockHandler_) return lockHandler_; + lockHandler_ = new LockHandler(fileApi()); + return lockHandler_; +} + +describe('synchronizer_LockHandler', function() { + + beforeEach(async (done:Function) => { + // logger.setLevel(Logger.LEVEL_WARN); + lockHandler_ = null; + await setupDatabaseAndSynchronizer(1); + await setupDatabaseAndSynchronizer(2); + await switchClient(1); + await synchronizer().start(); // Need to sync once to setup the sync target and allow locks to work + // logger.setLevel(Logger.LEVEL_DEBUG); + done(); + }); + + it('should acquire and release a sync lock', asyncTest(async () => { + await lockHandler().acquireLock(LockType.Sync, 'mobile', '123456'); + const locks = await lockHandler().locks(LockType.Sync); + expect(locks.length).toBe(1); + expect(locks[0].type).toBe(LockType.Sync); + expect(locks[0].clientId).toBe('123456'); + expect(locks[0].clientType).toBe('mobile'); + + await lockHandler().releaseLock(LockType.Sync, 'mobile', '123456'); + expect((await lockHandler().locks(LockType.Sync)).length).toBe(0); + })); + + it('should not use files that are not locks', asyncTest(async () => { + await fileApi().put('locks/desktop.ini', 'a'); + await fileApi().put('locks/exclusive.json', 'a'); + await fileApi().put('locks/garbage.json', 'a'); + await fileApi().put('locks/sync_mobile_72c4d1b7253a4475bfb2f977117d26ed.json', 'a'); + + const locks = await lockHandler().locks(LockType.Sync); + expect(locks.length).toBe(1); + })); + + it('should allow multiple sync locks', asyncTest(async () => { + await lockHandler().acquireLock(LockType.Sync, 'mobile', '111'); + + await switchClient(2); + + await lockHandler().acquireLock(LockType.Sync, 'mobile', '222'); + + expect((await lockHandler().locks(LockType.Sync)).length).toBe(2); + + { + await lockHandler().releaseLock(LockType.Sync, 'mobile', '222'); + const locks = await lockHandler().locks(LockType.Sync); + expect(locks.length).toBe(1); + expect(locks[0].clientId).toBe('111'); + } + })); + + it('should auto-refresh a lock', asyncTest(async () => { + const handler = newLockHandler({ autoRefreshInterval: 100 * timeoutMultipler }); + const lock = await handler.acquireLock(LockType.Sync, 'desktop', '111'); + const lockBefore = await handler.activeLock(LockType.Sync, 'desktop', '111'); + handler.startAutoLockRefresh(lock, () => {}); + await msleep(500 * timeoutMultipler); + const lockAfter = await handler.activeLock(LockType.Sync, 'desktop', '111'); + expect(lockAfter.updatedTime).toBeGreaterThan(lockBefore.updatedTime); + handler.stopAutoLockRefresh(lock); + })); + + it('should call the error handler when lock has expired while being auto-refreshed', asyncTest(async () => { + const handler = newLockHandler({ + lockTtl: 50 * timeoutMultipler, + autoRefreshInterval: 200 * timeoutMultipler, + }); + + const lock = await handler.acquireLock(LockType.Sync, 'desktop', '111'); + let autoLockError:any = null; + handler.startAutoLockRefresh(lock, (error:any) => { + autoLockError = error; + }); + + await msleep(250 * timeoutMultipler); + + expect(autoLockError.code).toBe('lockExpired'); + + handler.stopAutoLockRefresh(lock); + })); + + it('should not allow sync locks if there is an exclusive lock', asyncTest(async () => { + await lockHandler().acquireLock(LockType.Exclusive, 'desktop', '111'); + + await expectThrow(async () => { + await lockHandler().acquireLock(LockType.Sync, 'mobile', '222'); + }, 'hasExclusiveLock'); + })); + + it('should not allow exclusive lock if there are sync locks', asyncTest(async () => { + const lockHandler = newLockHandler({ lockTtl: 1000 * 60 * 60 }); + + await lockHandler.acquireLock(LockType.Sync, 'mobile', '111'); + await lockHandler.acquireLock(LockType.Sync, 'mobile', '222'); + + await expectThrow(async () => { + await lockHandler.acquireLock(LockType.Exclusive, 'desktop', '333'); + }, 'hasSyncLock'); + })); + + it('should allow exclusive lock if the sync locks have expired', asyncTest(async () => { + const lockHandler = newLockHandler({ lockTtl: 500 * timeoutMultipler }); + + await lockHandler.acquireLock(LockType.Sync, 'mobile', '111'); + await lockHandler.acquireLock(LockType.Sync, 'mobile', '222'); + + await msleep(600 * timeoutMultipler); + + await expectNotThrow(async () => { + await lockHandler.acquireLock(LockType.Exclusive, 'desktop', '333'); + }); + })); + + it('should decide what is the active exclusive lock', asyncTest(async () => { + const lockHandler = newLockHandler(); + + { + const lock1:Lock = { type: LockType.Exclusive, clientId: '1', clientType: 'd' }; + const lock2:Lock = { type: LockType.Exclusive, clientId: '2', clientType: 'd' }; + await lockHandler.saveLock_(lock1); + await msleep(100); + await lockHandler.saveLock_(lock2); + + const activeLock = await lockHandler.activeLock(LockType.Exclusive); + expect(activeLock.clientId).toBe('1'); + } + })); + + // it('should not have race conditions', asyncTest(async () => { + // const lockHandler = newLockHandler(); + + // const clients = []; + // for (let i = 0; i < 20; i++) { + // clients.push({ + // id: 'client' + i, + // type: 'desktop', + // }); + // } + + // for (let loopIndex = 0; loopIndex < 1000; loopIndex++) { + // const promises:Promise[] = []; + // for (let clientIndex = 0; clientIndex < clients.length; clientIndex++) { + // const client = clients[clientIndex]; + + // promises.push( + // lockHandler.acquireLock(LockType.Exclusive, client.type, client.id).catch(() => {}) + // ); + + // // if (gotLock) { + // // await msleep(100); + // // const locks = await lockHandler.locks(LockType.Exclusive); + // // console.info('======================================='); + // // console.info(locks); + // // lockHandler.releaseLock(LockType.Exclusive, client.type, client.id); + // // } + + // // await msleep(500); + // } + + // const result = await Promise.all(promises); + // const locks = result.filter((lock:any) => !!lock); + + // expect(locks.length).toBe(1); + // const lock:Lock = locks[0] as Lock; + // const allLocks = await lockHandler.locks(); + // console.info('================================', allLocks); + // lockHandler.releaseLock(LockType.Exclusive, lock.clientType, lock.clientId); + // } + // })); + +}); diff --git a/CliClient/tests/synchronizer_MigrationHandler.ts b/CliClient/tests/synchronizer_MigrationHandler.ts new file mode 100644 index 000000000..b16fa5133 --- /dev/null +++ b/CliClient/tests/synchronizer_MigrationHandler.ts @@ -0,0 +1,159 @@ +import LockHandler from 'lib/services/synchronizer/LockHandler'; +import MigrationHandler from 'lib/services/synchronizer/MigrationHandler'; + +// To create a sync target snapshot for the current syncVersion: +// - In test-utils, set syncTargetName_ to "filesystem" +// - Then run: +// gulp buildTests -L && node tests-build/support/createSyncTargetSnapshot.js normal && node tests-build/support/createSyncTargetSnapshot.js e2ee + +require('app-module-path').addPath(__dirname); + +const { asyncTest, setSyncTargetName, fileApi, synchronizer, decryptionWorker, encryptionService, setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } = require('test-utils.js'); +const { deploySyncTargetSnapshot, testData, checkTestData } = require('./support/syncTargetUtils'); +const Setting = require('lib/models/Setting'); +const MasterKey = require('lib/models/MasterKey'); + +const specTimeout = 60000 * 10; // Nextcloud tests can be slow + +let lockHandler_:LockHandler = null; +let migrationHandler_:MigrationHandler = null; + +function lockHandler():LockHandler { + if (lockHandler_) return lockHandler_; + lockHandler_ = new LockHandler(fileApi()); + return lockHandler_; +} + +function migrationHandler(clientId:string = 'abcd'):MigrationHandler { + if (migrationHandler_) return migrationHandler_; + migrationHandler_ = new MigrationHandler(fileApi(), lockHandler(), 'desktop', clientId); + return migrationHandler_; +} + +interface MigrationTests { + [key:string]: Function; +} + +const migrationTests:MigrationTests = { + 2: async function() { + const items = (await fileApi().list('', { includeHidden: true })).items; + expect(items.filter((i:any) => i.path === '.resource' && i.isDir).length).toBe(1); + expect(items.filter((i:any) => i.path === 'locks' && i.isDir).length).toBe(1); + expect(items.filter((i:any) => i.path === 'temp' && i.isDir).length).toBe(1); + expect(items.filter((i:any) => i.path === 'info.json' && !i.isDir).length).toBe(1); + + const versionForOldClients = await fileApi().get('.sync/version.txt'); + expect(versionForOldClients).toBe('2'); + }, +}; + +let previousSyncTargetName:string = ''; + +describe('synchronizer_MigrationHandler', function() { + + beforeEach(async (done:Function) => { + // To test the migrations, we have to use the filesystem sync target + // because the sync target snapshots are plain files. Eventually + // it should be possible to copy a filesystem target to memory + // but for now that will do. + previousSyncTargetName = setSyncTargetName('filesystem'); + lockHandler_ = null; + migrationHandler_ = null; + await setupDatabaseAndSynchronizer(1); + await setupDatabaseAndSynchronizer(2); + await switchClient(1); + done(); + }); + + afterEach(async (done:Function) => { + setSyncTargetName(previousSyncTargetName); + done(); + }); + + it('should not allow syncing if the sync target is out-dated', asyncTest(async () => { + await synchronizer().start(); + await fileApi().put('info.json', `{"version":${Setting.value('syncVersion') - 1}}`); + await expectThrow(async () => await migrationHandler().checkCanSync(), 'outdatedSyncTarget'); + }), specTimeout); + + it('should not allow syncing if the client is out-dated', asyncTest(async () => { + await synchronizer().start(); + await fileApi().put('info.json', `{"version":${Setting.value('syncVersion') + 1}}`); + await expectThrow(async () => await migrationHandler().checkCanSync(), 'outdatedClient'); + }), specTimeout); + + for (const migrationVersionString in migrationTests) { + const migrationVersion = Number(migrationVersionString); + + it(`should migrate (${migrationVersion})`, asyncTest(async () => { + await deploySyncTargetSnapshot('normal', migrationVersion - 1); + + const info = await migrationHandler().fetchSyncTargetInfo(); + expect(info.version).toBe(migrationVersion - 1); + + // Now, migrate to the new version + await migrationHandler().upgrade(migrationVersion); + + // Verify that it has been upgraded + const newInfo = await migrationHandler().fetchSyncTargetInfo(); + expect(newInfo.version).toBe(migrationVersion); + await migrationTests[migrationVersion](); + + // Now sync with that upgraded target + await synchronizer().start(); + + // Check that the data has not been altered + await expectNotThrow(async () => await checkTestData(testData)); + + // Check what happens if we switch to a different client and sync + await switchClient(2); + Setting.setConstant('syncVersion', migrationVersion); + await synchronizer().start(); + await expectNotThrow(async () => await checkTestData(testData)); + }), specTimeout); + + it(`should migrate (E2EE) (${migrationVersion})`, asyncTest(async () => { + // First create some test data that will be used to validate + // that the migration didn't alter any data. + await deploySyncTargetSnapshot('e2ee', migrationVersion - 1); + + // Now, migrate to the new version + Setting.setConstant('syncVersion', migrationVersion); + await migrationHandler().upgrade(migrationVersion); + + // Verify that it has been upgraded + const newInfo = await migrationHandler().fetchSyncTargetInfo(); + expect(newInfo.version).toBe(migrationVersion); + await migrationTests[migrationVersion](); + + // Now sync with that upgraded target + await synchronizer().start(); + + // Decrypt the data + const masterKey = (await MasterKey.all())[0]; + Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456'); + await encryptionService().loadMasterKeysFromSettings(); + await decryptionWorker().start(); + + // Check that the data has not been altered + await expectNotThrow(async () => await checkTestData(testData)); + + // Check what happens if we switch to a different client and sync + await switchClient(2); + Setting.setConstant('syncVersion', migrationVersion); + await synchronizer().start(); + + // Should throw because data hasn't been decrypted yet + await expectThrow(async () => await checkTestData(testData)); + + // Enable E2EE and decrypt + Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456'); + await encryptionService().loadMasterKeysFromSettings(); + await decryptionWorker().start(); + + // Should not throw because data is decrypted + await expectNotThrow(async () => await checkTestData(testData)); + }), specTimeout); + } + +}); diff --git a/CliClient/tests/test-utils.js b/CliClient/tests/test-utils.js index adac9dff3..127419c7b 100644 --- a/CliClient/tests/test-utils.js +++ b/CliClient/tests/test-utils.js @@ -21,6 +21,7 @@ const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js'); const { FileApiDriverLocal } = require('lib/file-api-driver-local.js'); const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav.js'); const { FileApiDriverDropbox } = require('lib/file-api-driver-dropbox.js'); +const { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js'); const { FileApiDriverAmazonS3 } = require('lib/file-api-driver-amazon-s3.js'); const BaseService = require('lib/services/BaseService.js'); const { FsDriverNode } = require('lib/fs-driver-node.js'); @@ -43,6 +44,7 @@ const ResourceFetcher = require('lib/services/ResourceFetcher.js'); const KvStore = require('lib/services/KvStore.js'); const WebDavApi = require('lib/WebDavApi'); const DropboxApi = require('lib/DropboxApi'); +const { OneDriveApi } = require('lib/onedrive-api'); const { loadKeychainServiceAndSettings } = require('lib/services/SettingUtils'); const KeychainServiceDriver = require('lib/services/keychain/KeychainServiceDriver.node').default; const KeychainServiceDriverDummy = require('lib/services/keychain/KeychainServiceDriver.dummy').default; @@ -50,14 +52,15 @@ const md5 = require('md5'); const S3 = require('aws-sdk/clients/s3'); const databases_ = []; -const synchronizers_ = []; +let synchronizers_ = []; +const synchronizerContexts_ = {}; +const fileApis_ = {}; const encryptionServices_ = []; const revisionServices_ = []; const decryptionWorkers_ = []; const resourceServices_ = []; const resourceFetchers_ = []; const kvStores_ = []; -let fileApi_ = null; let currentClient_ = 1; // The line `process.on('unhandledRejection'...` in all the test files is going to @@ -88,16 +91,39 @@ SyncTargetRegistry.addClass(SyncTargetNextcloud); SyncTargetRegistry.addClass(SyncTargetDropbox); SyncTargetRegistry.addClass(SyncTargetAmazonS3); -// const syncTargetId_ = SyncTargetRegistry.nameToId("nextcloud"); -const syncTargetId_ = SyncTargetRegistry.nameToId('memory'); -// const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem'); -// const syncTargetId_ = SyncTargetRegistry.nameToId('dropbox'); -// const syncTargetId_ = SyncTargetRegistry.nameToId('amazon_s3'); +let syncTargetName_ = ''; +let syncTargetId_ = null; +let sleepTime = 0; +let isNetworkSyncTarget_ = false; + +function syncTargetName() { + return syncTargetName_; +} + +function setSyncTargetName(name) { + if (name === syncTargetName_) return syncTargetName_; + const previousName = syncTargetName_; + syncTargetName_ = name; + syncTargetId_ = SyncTargetRegistry.nameToId(syncTargetName_); + sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 100;// 400; + isNetworkSyncTarget_ = ['nextcloud', 'dropbox', 'onedrive', 'amazon_s3'].includes(syncTargetName_); + synchronizers_ = []; + return previousName; +} + +setSyncTargetName('memory'); +// setSyncTargetName('nextcloud'); +// setSyncTargetName('dropbox'); +// setSyncTargetName('onedrive'); +// setSyncTargetName('amazon_s3'); + +console.info(`Testing with sync target: ${syncTargetName_}`); + const syncDir = `${__dirname}/../tests/sync`; -const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 100;// 400; - -console.info(`Testing with sync target: ${SyncTargetRegistry.idToName(syncTargetId_)}`); +let defaultJasmineTimeout = 90 * 1000; +if (isNetworkSyncTarget_) defaultJasmineTimeout = 60 * 1000 * 10; +if (typeof jasmine !== 'undefined') jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultJasmineTimeout; const dbLogger = new Logger(); dbLogger.addTarget('console'); @@ -129,6 +155,10 @@ function syncTargetId() { return syncTargetId_; } +function isNetworkSyncTarget() { + return isNetworkSyncTarget_; +} + function sleep(n) { return new Promise((resolve, reject) => { setTimeout(() => { @@ -137,6 +167,14 @@ function sleep(n) { }); } +function msleep(ms) { + return new Promise((resolve, reject) => { + setTimeout(() => { + resolve(); + }, ms); + }); +} + function currentClientId() { return currentClient_; } @@ -252,9 +290,11 @@ async function setupDatabaseAndSynchronizer(id = null, options = null) { if (!synchronizers_[id]) { const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_); const syncTarget = new SyncTargetClass(db(id)); + await initFileApi(); syncTarget.setFileApi(fileApi()); syncTarget.setLogger(logger); synchronizers_[id] = await syncTarget.synchronizer(); + synchronizerContexts_[id] = null; } encryptionServices_[id] = new EncryptionService(); @@ -278,6 +318,19 @@ function synchronizer(id = null) { return synchronizers_[id]; } +// This is like calling synchronizer.start() but it handles the +// complexity of passing around the sync context depending on +// the client. +async function synchronizerStart(id = null, extraOptions = null) { + if (id === null) id = currentClient_; + const context = synchronizerContexts_[id]; + const options = Object.assign({}, extraOptions); + if (context) options.context = context; + const newContext = await synchronizer(id).start(options); + synchronizerContexts_[id] = newContext; + return newContext; +} + function encryptionService(id = null) { if (id === null) id = currentClient_; return encryptionServices_[id]; @@ -331,44 +384,67 @@ async function loadEncryptionMasterKey(id = null, useExisting = false) { return masterKey; } -function fileApi() { - if (fileApi_) return fileApi_; +async function initFileApi() { + if (fileApis_[syncTargetId_]) return; + let fileApi = null; if (syncTargetId_ == SyncTargetRegistry.nameToId('filesystem')) { fs.removeSync(syncDir); fs.mkdirpSync(syncDir, 0o755); - fileApi_ = new FileApi(syncDir, new FileApiDriverLocal()); + fileApi = new FileApi(syncDir, new FileApiDriverLocal()); } else if (syncTargetId_ == SyncTargetRegistry.nameToId('memory')) { - fileApi_ = new FileApi('/root', new FileApiDriverMemory()); + fileApi = new FileApi('/root', new FileApiDriverMemory()); } else if (syncTargetId_ == SyncTargetRegistry.nameToId('nextcloud')) { - const options = { - baseUrl: () => 'http://nextcloud.local/remote.php/dav/files/admin/JoplinTest', - username: () => 'admin', - password: () => '123456', - }; - - const api = new WebDavApi(options); - fileApi_ = new FileApi('', new FileApiDriverWebDav(api)); + const options = require(`${__dirname}/../tests/support/nextcloud-auth.json`); + const api = new WebDavApi({ + baseUrl: () => options.baseUrl, + username: () => options.username, + password: () => options.password, + }); + fileApi = new FileApi('', new FileApiDriverWebDav(api)); } else if (syncTargetId_ == SyncTargetRegistry.nameToId('dropbox')) { + // To get a token, go to the App Console: + // https://www.dropbox.com/developers/apps/ + // Then select "JoplinTest" and click "Generated access token" const api = new DropboxApi(); const authTokenPath = `${__dirname}/support/dropbox-auth.txt`; const authToken = fs.readFileSync(authTokenPath, 'utf8'); if (!authToken) throw new Error(`Dropbox auth token missing in ${authTokenPath}`); api.setAuthToken(authToken); - fileApi_ = new FileApi('', new FileApiDriverDropbox(api)); + fileApi = new FileApi('', new FileApiDriverDropbox(api)); + } else if (syncTargetId_ == SyncTargetRegistry.nameToId('onedrive')) { + // To get a token, open the URL below, then copy the *complete* + // redirection URL in onedrive-auth.txt. Keep in mind that auth data + // only lasts 1h for OneDrive. + // https://login.live.com/oauth20_authorize.srf?client_id=f1e68e1e-a729-4514-b041-4fdd5c7ac03a&scope=files.readwrite,offline_access&response_type=token&redirect_uri=https://joplinapp.org + const { parameters, setEnvOverride } = require('lib/parameters.js'); + Setting.setConstant('env', 'dev'); + setEnvOverride('test'); + const config = parameters().oneDriveTest; + const api = new OneDriveApi(config.id, config.secret, false); + const authData = fs.readFileSync(`${__dirname}/support/onedrive-auth.txt`, 'utf8'); + const urlInfo = require('url-parse')(authData, true); + const auth = require('querystring').parse(urlInfo.hash.substr(1)); + api.setAuth(auth); + const appDir = await api.appDirectory(); + fileApi = new FileApi(appDir, new FileApiDriverOneDrive(api)); } else if (syncTargetId_ == SyncTargetRegistry.nameToId('amazon_s3')) { const amazonS3CredsPath = `${__dirname}/support/amazon-s3-auth.json`; const amazonS3Creds = require(amazonS3CredsPath); if (!amazonS3Creds || !amazonS3Creds.accessKeyId) throw new Error(`AWS auth JSON missing in ${amazonS3CredsPath} format should be: { "accessKeyId": "", "secretAccessKey": "", "bucket": "mybucket"}`); const api = new S3({ accessKeyId: amazonS3Creds.accessKeyId, secretAccessKey: amazonS3Creds.secretAccessKey, s3UseArnRegion: true }); - fileApi_ = new FileApi('', new FileApiDriverAmazonS3(api, amazonS3Creds.bucket)); + fileApi = new FileApi('', new FileApiDriverAmazonS3(api, amazonS3Creds.bucket)); } + fileApi.setLogger(logger); + fileApi.setSyncTargetId(syncTargetId_); + fileApi.requestRepeatCount_ = isNetworkSyncTarget_ ? 1 : 0; - fileApi_.setLogger(logger); - fileApi_.setSyncTargetId(syncTargetId_); - fileApi_.requestRepeatCount_ = 0; - return fileApi_; + fileApis_[syncTargetId_] = fileApi; +} + +function fileApi() { + return fileApis_[syncTargetId_]; } function objectsEqual(o1, o2) { @@ -390,6 +466,41 @@ async function checkThrowAsync(asyncFn) { return hasThrown; } +async function expectThrow(asyncFn, errorCode = undefined) { + let hasThrown = false; + let thrownError = null; + try { + await asyncFn(); + } catch (error) { + hasThrown = true; + thrownError = error; + } + + if (!hasThrown) { + expect('not throw').toBe('throw', 'Expected function to throw an error but did not'); + } else if (thrownError.code !== errorCode) { + console.error(thrownError); + expect(`error code: ${thrownError.code}`).toBe(`error code: ${errorCode}`); + } else { + expect(true).toBe(true); + } +} + +async function expectNotThrow(asyncFn) { + let thrownError = null; + try { + await asyncFn(); + } catch (error) { + thrownError = error; + } + + if (thrownError) { + expect(thrownError.message).toBe('', 'Expected function not to throw an error but it did'); + } else { + expect(true).toBe(true); + } +} + function checkThrow(fn) { let hasThrown = false; try { @@ -427,13 +538,15 @@ function asyncTest(callback) { } async function allSyncTargetItemsEncrypted() { - const list = await fileApi().list(); + const list = await fileApi().list('', { includeDirs: false }); const files = list.items; let totalCount = 0; let encryptedCount = 0; for (let i = 0; i < files.length; i++) { const file = files[i]; + if (!BaseItem.isSystemPath(file.path)) continue; + const remoteContentString = await fileApi().get(file.path); const remoteContent = await BaseItem.unserialize(remoteContentString); const ItemClass = BaseItem.itemClass(remoteContent); @@ -585,4 +698,4 @@ class TestApp extends BaseApplication { } } -module.exports = { syncDir, kvStore, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp }; +module.exports = { synchronizerStart, syncTargetName, setSyncTargetName, syncDir, isNetworkSyncTarget, kvStore, expectThrow, logger, expectNotThrow, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, msleep, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp }; diff --git a/ElectronClient/app.js b/ElectronClient/app.js index 2b688344b..6bd6bc987 100644 --- a/ElectronClient/app.js +++ b/ElectronClient/app.js @@ -1022,7 +1022,7 @@ class Application extends BaseApplication { } const sortNoteReverseItem = menu.getMenuItemById('sort:notes:reverse'); - sortNoteReverseItem.enabled = state.settings['notes.sortOrder.field'] !== 'order'; + if (sortNoteReverseItem) sortNoteReverseItem.enabled = state.settings['notes.sortOrder.field'] !== 'order'; // const devToolsMenuItem = menu.getMenuItemById('help:toggleDevTools'); // devToolsMenuItem.checked = state.devToolsVisible; @@ -1083,39 +1083,6 @@ class Application extends BaseApplication { return cssString; } - // async createManyNotes() { - // return; - // const folderIds = []; - - // const randomFolderId = (folderIds) => { - // if (!folderIds.length) return ''; - // const idx = Math.floor(Math.random() * folderIds.length); - // if (idx > folderIds.length - 1) throw new Error('Invalid index ' + idx + ' / ' + folderIds.length); - // return folderIds[idx]; - // } - - // let rootFolderCount = 0; - // let folderCount = 100; - - // for (let i = 0; i < folderCount; i++) { - // let parentId = ''; - - // if (Math.random() >= 0.9 || rootFolderCount >= folderCount / 10) { - // parentId = randomFolderId(folderIds); - // } else { - // rootFolderCount++; - // } - - // const folder = await Folder.save({ title: 'folder' + i, parent_id: parentId }); - // folderIds.push(folder.id); - // } - - // for (let i = 0; i < 10000; i++) { - // const parentId = randomFolderId(folderIds); - // Note.save({ title: 'note' + i, parent_id: parentId }); - // } - // } - async start(argv) { const electronIsDev = require('electron-is-dev'); @@ -1125,7 +1092,11 @@ class Application extends BaseApplication { argv = await super.start(argv); - const dir = Setting.value('profileDir'); + if (Setting.value('sync.upgradeState') === Setting.SYNC_UPGRADE_STATE_MUST_DO) { + return { action: 'upgradeSyncTarget' }; + } + + const dir = Setting.value('profileDir'); // Loads app-wide styles. (Markdown preview-specific styles loaded in app.js) const filename = Setting.custom_css_files.JOPLIN_APP; diff --git a/ElectronClient/bridge.js b/ElectronClient/bridge.js index e25f0629b..c5a79e662 100644 --- a/ElectronClient/bridge.js +++ b/ElectronClient/bridge.js @@ -182,6 +182,15 @@ class Bridge { } } + restart() { + // Note that in this case we are not sending the "appClose" event + // to notify services and component that the app is about to close + // but for the current use-case it's not really needed. + const { app } = require('electron'); + app.relaunch(); + app.exit(); + } + } let bridge_ = null; diff --git a/ElectronClient/gui/MainScreen/MainScreen.jsx b/ElectronClient/gui/MainScreen/MainScreen.jsx index d40ef2d8e..3c580e7eb 100644 --- a/ElectronClient/gui/MainScreen/MainScreen.jsx +++ b/ElectronClient/gui/MainScreen/MainScreen.jsx @@ -224,7 +224,7 @@ class MainScreenComponent extends React.Component { this.styles_.messageBox = { width: width, - height: 30, + height: 50, display: 'flex', alignItems: 'center', paddingLeft: 10, @@ -315,8 +315,23 @@ class MainScreenComponent extends React.Component { }); }; + const onRestartAndUpgrade = async () => { + Setting.setValue('sync.upgradeState', Setting.SYNC_UPGRADE_STATE_MUST_DO); + await Setting.saveAll(); + bridge().restart(); + }; + let msg = null; - if (this.props.hasDisabledSyncItems) { + if (this.props.shouldUpgradeSyncTarget) { + msg = ( + + {_('The sync target needs to be upgraded before Joplin can sync. The operation may take a few minutes to complete and the app needs to be restarted. To proceed please click on the link.')}{' '} + onRestartAndUpgrade()}> + {_('Restart and upgrade')} + + + ); + } else if (this.props.hasDisabledSyncItems) { msg = ( {_('Some items cannot be synchronised.')}{' '} @@ -371,7 +386,7 @@ class MainScreenComponent extends React.Component { } messageBoxVisible() { - return this.props.hasDisabledSyncItems || this.props.showMissingMasterKeyMessage || this.props.showNeedUpgradingMasterKeyMessage || this.props.showShouldReencryptMessage || this.props.hasDisabledEncryptionItems; + return this.props.hasDisabledSyncItems || this.props.showMissingMasterKeyMessage || this.props.showNeedUpgradingMasterKeyMessage || this.props.showShouldReencryptMessage || this.props.hasDisabledEncryptionItems || this.props.shouldUpgradeSyncTarget; } registerCommands() { @@ -492,6 +507,7 @@ const mapStateToProps = state => { showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length, showNeedUpgradingMasterKeyMessage: !!EncryptionService.instance().masterKeysThatNeedUpgrading(state.masterKeys).length, showShouldReencryptMessage: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES, + shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO, selectedFolderId: state.selectedFolderId, sidebarWidth: state.settings['style.sidebar.width'], noteListWidth: state.settings['style.noteList.width'], diff --git a/ElectronClient/gui/Root_UpgradeSyncTarget.tsx b/ElectronClient/gui/Root_UpgradeSyncTarget.tsx new file mode 100644 index 000000000..a25e0d8b0 --- /dev/null +++ b/ElectronClient/gui/Root_UpgradeSyncTarget.tsx @@ -0,0 +1,102 @@ +import * as React from 'react'; +import { useEffect } from 'react'; +import useSyncTargetUpgrade, { SyncTargetUpgradeResult } from 'lib/services/synchronizer/gui/useSyncTargetUpgrade'; + +const { render } = require('react-dom'); +const ipcRenderer = require('electron').ipcRenderer; +const Setting = require('lib/models/Setting'); +const { bridge } = require('electron').remote.require('./bridge'); + +function useAppCloseHandler(upgradeResult:SyncTargetUpgradeResult) { + useEffect(function() { + async function onAppClose() { + let canClose = true; + + if (!upgradeResult.done) { + canClose = confirm('The synchronisation target upgrade is still running and it is recommanded to let it finish. Close the application anyway?'); + } + + if (canClose) { + // We set the state back to IDLE so that the app can start normally and + // potentially the user can fix issues if any, export the data, etc. + // The message to upgrade will show up again if they try to sync. + Setting.setValue('sync.upgradeState', Setting.SYNC_UPGRADE_STATE_IDLE); + await Setting.saveAll(); + } + + ipcRenderer.send('asynchronous-message', 'appCloseReply', { + canClose: canClose, + }); + } + + ipcRenderer.on('appClose', onAppClose); + + return () => { + ipcRenderer.off('appClose', onAppClose); + }; + }, [upgradeResult.done]); +} + +function useStyle() { + useEffect(function() { + const element = document.createElement('style'); + element.appendChild(document.createTextNode(` + body { + font-family: sans-serif; + padding: 5px 20px; + color: #333333; + } + + .errorBox { + border: 1px solid red; + padding: 5px 20px; + background-color: #ffeeee; + } + + pre { + overflow-x: scroll; + } + `)); + document.head.appendChild(element); + }, []); +} + +function useRestartOnDone(upgradeResult:SyncTargetUpgradeResult) { + useEffect(function() { + if (upgradeResult.done) { + bridge().restart(); + } + }, [upgradeResult.done]); +} + +function Root_UpgradeSyncTarget() { + const upgradeResult = useSyncTargetUpgrade(); + + useStyle(); + useRestartOnDone(upgradeResult); + useAppCloseHandler(upgradeResult); + + function renderUpgradeError() { + if (!upgradeResult.error) return null; + + return ( +
+

Error

+

The sync target could not be upgraded due to an error. For support, please copy the complete content of this page and paste it in the forum: https://discourse.joplinapp.org/

+

The full error was:

+

{upgradeResult.error.message}

+
{upgradeResult.error.stack}
+
+ ); + } + + return ( +
+

Joplin upgrade in progress...

+

Please wait while the sync target is being upgraded. It may take a few seconds or a few minutes depending on the upgrade. The application will automatically restart once it is completed.

+ {renderUpgradeError()} +
+ ); +} + +render(, document.getElementById('react-root')); diff --git a/ElectronClient/main-html.js b/ElectronClient/main-html.js index 5fe0dab33..d4d2d85d2 100644 --- a/ElectronClient/main-html.js +++ b/ElectronClient/main-html.js @@ -95,8 +95,12 @@ document.addEventListener('auxclick', event => event.preventDefault()); // which would open a new browser window. document.addEventListener('click', (event) => event.preventDefault()); -app().start(bridge().processArgv()).then(() => { - require('./gui/Root.min.js'); +app().start(bridge().processArgv()).then((result) => { + if (!result || !result.action) { + require('./gui/Root.min.js'); + } else if (result.action === 'upgradeSyncTarget') { + require('./gui/Root_UpgradeSyncTarget'); + } }).catch((error) => { const env = bridge().env(); diff --git a/ElectronClient/package-lock.json b/ElectronClient/package-lock.json index cc376e256..cd0fd8310 100644 --- a/ElectronClient/package-lock.json +++ b/ElectronClient/package-lock.json @@ -235,6 +235,12 @@ "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==", "dev": true }, + "@types/jasmine": { + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.11.tgz", + "integrity": "sha512-fg1rOd/DehQTIJTifGqGVY6q92lDgnLfs7C6t1ccSwQrMyoTGSoH6wWzhJDZb6ezhsdwAX4EIBLe8w5fXWmEng==", + "dev": true + }, "@types/node": { "version": "12.12.38", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.38.tgz", diff --git a/ElectronClient/package.json b/ElectronClient/package.json index f6a2bc3b3..8703b75d6 100644 --- a/ElectronClient/package.json +++ b/ElectronClient/package.json @@ -78,6 +78,7 @@ }, "homepage": "https://github.com/laurent22/joplin#readme", "devDependencies": { + "@types/jasmine": "^3.5.11", "ajv": "^6.5.0", "app-builder-bin": "^1.9.11", "babel-cli": "^6.26.0", diff --git a/README.md b/README.md index 6741b29ab..b91113e91 100644 --- a/README.md +++ b/README.md @@ -81,11 +81,16 @@ The Web Clipper is a browser extension that allows you to save web pages and scr - [Markdown Guide](https://github.com/laurent22/joplin/blob/master/readme/markdown.md) - [How to enable end-to-end encryption](https://github.com/laurent22/joplin/blob/master/readme/e2ee.md) - [What is a conflict?](https://github.com/laurent22/joplin/blob/master/readme/conflict.md) - - [End-to-end encryption spec](https://github.com/laurent22/joplin/blob/master/readme/spec.md) - [How to enable debug mode](https://github.com/laurent22/joplin/blob/master/readme/debugging.md) - [API documentation](https://github.com/laurent22/joplin/blob/master/readme/api.md) - [FAQ](https://github.com/laurent22/joplin/blob/master/readme/faq.md) +- Development + + - [End-to-end encryption spec](https://github.com/laurent22/joplin/blob/master/readme/spec/e2ee.md) + - [Note History spec](https://github.com/laurent22/joplin/blob/master/readme/spec/history.md) + - [Sync Lock spec](https://github.com/laurent22/joplin/blob/master/readme/spec/sync_lock.md) + - Google Summer of Code 2020 - [Google Summer of Code 2020](https://github.com/laurent22/joplin/blob/master/readme/gsoc2020/index.md) diff --git a/ReactNativeClient/lib/BaseSyncTarget.js b/ReactNativeClient/lib/BaseSyncTarget.js index d0da78c32..cd6e2c742 100644 --- a/ReactNativeClient/lib/BaseSyncTarget.js +++ b/ReactNativeClient/lib/BaseSyncTarget.js @@ -13,10 +13,6 @@ class BaseSyncTarget { return false; } - static resourceDirName() { - return '.resource'; - } - option(name, defaultValue = null) { return this.options_ && name in this.options_ ? this.options_[name] : defaultValue; } diff --git a/ReactNativeClient/lib/SyncTargetOneDrive.js b/ReactNativeClient/lib/SyncTargetOneDrive.js index fa1ba2ee8..57a197239 100644 --- a/ReactNativeClient/lib/SyncTargetOneDrive.js +++ b/ReactNativeClient/lib/SyncTargetOneDrive.js @@ -33,8 +33,15 @@ class SyncTargetOneDrive extends BaseSyncTarget { return SyncTargetOneDrive.id(); } + isTesting() { + const p = parameters(); + return !!p.oneDriveTest; + } + oneDriveParameters() { - return parameters().oneDrive; + const p = parameters(); + if (p.oneDriveTest) return p.oneDriveTest; + return p.oneDrive; } authRouteName() { @@ -42,6 +49,10 @@ class SyncTargetOneDrive extends BaseSyncTarget { } api() { + if (this.isTesting()) { + return this.fileApi_.driver().api(); + } + if (this.api_) return this.api_; const isPublic = Setting.value('appType') != 'cli' && Setting.value('appType') != 'desktop'; diff --git a/ReactNativeClient/lib/components/screen-header.js b/ReactNativeClient/lib/components/screen-header.js index 70315ece2..86e921071 100644 --- a/ReactNativeClient/lib/components/screen-header.js +++ b/ReactNativeClient/lib/components/screen-header.js @@ -450,6 +450,7 @@ class ScreenHeaderComponent extends React.PureComponent { if (this.props.showMissingMasterKeyMessage) warningComps.push(this.renderWarningBox('EncryptionConfig', _('Press to set the decryption password.'))); if (this.props.hasDisabledSyncItems) warningComps.push(this.renderWarningBox('Status', _('Some items cannot be synchronised. Press for more info.'))); + if (this.props.shouldUpgradeSyncTarget && this.props.showShouldUpgradeSyncTargetMessage !== false) warningComps.push(this.renderWarningBox('UpgradeSyncTarget', _('The sync target needs to be upgraded. Press this banner to proceed.'))); const showSideMenuButton = !!this.props.showSideMenuButton && !this.props.noteSelectionEnabled; const showSelectAllButton = this.props.noteSelectionEnabled; @@ -536,6 +537,7 @@ const ScreenHeader = connect(state => { selectedNoteIds: state.selectedNoteIds, showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length, hasDisabledSyncItems: state.hasDisabledSyncItems, + shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO, }; })(ScreenHeaderComponent); diff --git a/ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.tsx b/ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.tsx new file mode 100644 index 000000000..52bb50798 --- /dev/null +++ b/ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import { View, Text, ScrollView } from 'react-native'; +import useSyncTargetUpgrade from 'lib/services/synchronizer/gui/useSyncTargetUpgrade'; + +const { connect } = require('react-redux'); +const { themeStyle } = require('lib/components/global-style.js'); +const { ScreenHeader } = require('lib/components/screen-header.js'); +const { _ } = require('lib/locale.js'); + +function UpgradeSyncTargetScreen(props:any) { + const upgradeResult = useSyncTargetUpgrade(); + + const theme = themeStyle(props.theme); + + const lineStyle = { ...theme.normalText, marginBottom: 20 }; + const stackTraceStyle = { ...theme.normalText, flexWrap: 'nowrap', fontSize: theme.fontSize * 0.5, color: theme.colorFaded }; + const headerStyle = { ...theme.headerStyle, marginBottom: 20 }; + + function renderUpgradeError() { + if (!upgradeResult.error) return null; + + return ( + + Error + The sync target could not be upgraded due to an error. For support, please copy the content of this page and paste it in the forum: https://discourse.joplinapp.org/ + The full error was: + {upgradeResult.error.message} + {upgradeResult.error.stack} + + ); + } + + function renderInProgress() { + if (upgradeResult.error || upgradeResult.done) return null; + + return ( + + Joplin upgrade in progress... + Please wait while the sync target is being upgraded. It may take a few seconds or a few minutes depending on the upgrade. + Make sure you leave your device on and the app opened while the upgrade is in progress. + + ); + } + + function renderDone() { + if (upgradeResult.error || !upgradeResult.done) return null; + + return ( + + Upgrade complete + The upgrade has been applied successfully. Please press Back to exit this screen. + + ); + } + + return ( + + + + {renderInProgress()} + {renderDone()} + {renderUpgradeError()} + + + ); +} + +export default connect((state:any) => { + return { + theme: state.settings.theme, + }; +})(UpgradeSyncTargetScreen); diff --git a/ReactNativeClient/lib/file-api-driver-memory.js b/ReactNativeClient/lib/file-api-driver-memory.js index 762a81e0c..568ef3aab 100644 --- a/ReactNativeClient/lib/file-api-driver-memory.js +++ b/ReactNativeClient/lib/file-api-driver-memory.js @@ -111,7 +111,7 @@ class FileApiDriverMemory { this.items_.push(item); } else { this.items_[index].content = this.encodeContent_(content); - this.items_[index].updated_time = time.unix(); + this.items_[index].updated_time = time.unixMs(); } } diff --git a/ReactNativeClient/lib/file-api-driver-onedrive.js b/ReactNativeClient/lib/file-api-driver-onedrive.js index ffca4d6c1..bf5ecf94c 100644 --- a/ReactNativeClient/lib/file-api-driver-onedrive.js +++ b/ReactNativeClient/lib/file-api-driver-onedrive.js @@ -78,6 +78,10 @@ class FileApiDriverOneDrive { } async list(path, options = null) { + options = Object.assign({}, { + context: null, + }, options); + let query = this.itemFilter_(); let url = `${this.makePath_(path)}:/children`; @@ -186,8 +190,23 @@ class FileApiDriverOneDrive { return this.pathCache_[path]; } - clearRoot() { - throw new Error('Not implemented'); + async clearRoot() { + const recurseItems = async (path) => { + const result = await this.list(this.fileApi_.fullPath_(path)); + const output = []; + + for (const item of result.items) { + const fullPath = `${path}/${item.path}`; + if (item.isDir) { + await recurseItems(fullPath); + } + await this.delete(this.fileApi_.fullPath_(fullPath)); + } + + return output; + }; + + await recurseItems(''); } async delta(path, options = null) { diff --git a/ReactNativeClient/lib/file-api-driver-webdav.js b/ReactNativeClient/lib/file-api-driver-webdav.js index f43ec498d..98068755d 100644 --- a/ReactNativeClient/lib/file-api-driver-webdav.js +++ b/ReactNativeClient/lib/file-api-driver-webdav.js @@ -89,7 +89,7 @@ class FileApiDriverWebDav { async delta(path, options) { const getDirStats = async path => { - const result = await this.list(path); + const result = await this.list(path, { includeDirs: false }); return result.items; }; @@ -130,13 +130,25 @@ class FileApiDriverWebDav { } async list(path) { - // See mkdir() call for explanation - if (!path.endsWith('/')) path = `${path}/`; - - const result = await this.api().execPropFind(path, 1, ['d:getlastmodified', 'd:resourcetype']); + // See mkdir() call for explanation about trailing slash + const result = await this.api().execPropFind(!path.endsWith('/') ? `${path}/` : path, 1, ['d:getlastmodified', 'd:resourcetype']); const resources = this.api().arrayFromJson(result, ['d:multistatus', 'd:response']); - const stats = this.statsFromResources_(resources); + + const stats = this.statsFromResources_(resources).map((stat) => { + if (path && stat.path.indexOf(`${path}/`) === 0) { + const s = stat.path.substr(path.length + 1); + if (s.split('/').length === 1) { + return { + ...stat, + path: stat.path.substr(path.length + 1), + }; + } + } + return stat; + }).filter((stat) => { + return stat.path !== rtrimSlashes(path); + }); return { items: stats, diff --git a/ReactNativeClient/lib/file-api.js b/ReactNativeClient/lib/file-api.js index bf12d318c..f53e97ff5 100644 --- a/ReactNativeClient/lib/file-api.js +++ b/ReactNativeClient/lib/file-api.js @@ -128,6 +128,8 @@ class FileApi { if (!options) options = {}; if (!('includeHidden' in options)) options.includeHidden = false; if (!('context' in options)) options.context = null; + if (!('includeDirs' in options)) options.includeDirs = true; + if (!('syncItemsOnly' in options)) options.syncItemsOnly = false; this.logger().debug(`list ${this.baseDir()}`); @@ -141,6 +143,14 @@ class FileApi { result.items = temp; } + if (!options.includeDirs) { + result.items = result.items.filter(f => !f.isDir); + } + + if (options.syncItemsOnly) { + result.items = result.items.filter(f => !f.isDir && BaseItem.isSystemPath(f.path)); + } + return result; } diff --git a/ReactNativeClient/lib/models/Setting.js b/ReactNativeClient/lib/models/Setting.js index 1864bc6ff..5dfadcfad 100644 --- a/ReactNativeClient/lib/models/Setting.js +++ b/ReactNativeClient/lib/models/Setting.js @@ -86,6 +86,12 @@ class Setting extends BaseModel { }, }, + 'sync.upgradeState': { + value: Setting.SYNC_UPGRADE_STATE_IDLE, + type: Setting.TYPE_INT, + public: false, + }, + 'sync.2.path': { value: '', type: Setting.TYPE_STRING, @@ -1324,6 +1330,10 @@ Setting.SHOULD_REENCRYPT_NO = 0; // Data doesn't need to be re-encrypted Setting.SHOULD_REENCRYPT_YES = 1; // Data should be re-encrypted Setting.SHOULD_REENCRYPT_NOTIFIED = 2; // Data should be re-encrypted, and user has been notified +Setting.SYNC_UPGRADE_STATE_IDLE = 0; // Doesn't need to be upgraded +Setting.SYNC_UPGRADE_STATE_SHOULD_DO = 1; // Should be upgraded, but waiting for user to confirm +Setting.SYNC_UPGRADE_STATE_MUST_DO = 2; // Must be upgraded - on next restart, the upgrade will start + Setting.custom_css_files = { JOPLIN_APP: 'userchrome.css', RENDERED_MARKDOWN: 'userstyle.css', @@ -1344,7 +1354,7 @@ Setting.constants_ = { templateDir: '', tempDir: '', flagOpenDevTools: false, - syncVersion: 1, + syncVersion: 2, }; Setting.autoSaveEnabled = true; diff --git a/ReactNativeClient/lib/onedrive-api.js b/ReactNativeClient/lib/onedrive-api.js index b8439e7bd..e67adcf46 100644 --- a/ReactNativeClient/lib/onedrive-api.js +++ b/ReactNativeClient/lib/onedrive-api.js @@ -225,7 +225,10 @@ class OneDriveApi { // In general, `path` contains a path relative to the base URL, but in some // cases the full URL is provided (for example, when it's a URL that was // retrieved from the API). - if (url.indexOf('https://') !== 0) url = `https://graph.microsoft.com/v1.0${path}`; + if (url.indexOf('https://') !== 0) { + const slash = path.indexOf('/') === 0 ? '' : '/'; + url = `https://graph.microsoft.com/v1.0${slash}${path}`; + } if (query) { url += url.indexOf('?') < 0 ? '?' : '&'; diff --git a/ReactNativeClient/lib/parameters.js b/ReactNativeClient/lib/parameters.js index 69b376f42..33ce5961f 100644 --- a/ReactNativeClient/lib/parameters.js +++ b/ReactNativeClient/lib/parameters.js @@ -2,6 +2,13 @@ const Setting = require('lib/models/Setting.js'); const parameters_ = {}; +parameters_.test = { + oneDriveTest: { + id: 'f1e68e1e-a729-4514-b041-4fdd5c7ac03a', + secret: '~PC7cwAC_AXGICk_V0~12SmI9lbaC-MBDT', + }, +}; + parameters_.dev = { oneDrive: { id: 'cbabb902-d276-4ea4-aa88-062a5889d6dc', @@ -32,7 +39,13 @@ parameters_.prod = { }, }; +let envOverride_ = null; +function setEnvOverride(env) { + envOverride_ = env; +} + function parameters(env = null) { + if (envOverride_) env = envOverride_; if (env === null) env = Setting.value('env'); const output = parameters_[env]; if (Setting.value('isDemo')) { @@ -41,4 +54,4 @@ function parameters(env = null) { return output; } -module.exports = { parameters }; +module.exports = { parameters, setEnvOverride }; diff --git a/ReactNativeClient/lib/services/BaseService.js b/ReactNativeClient/lib/services/BaseService.js index bec30e362..47e44e2cf 100644 --- a/ReactNativeClient/lib/services/BaseService.js +++ b/ReactNativeClient/lib/services/BaseService.js @@ -1,8 +1,13 @@ class BaseService { logger() { + if (this.instanceLogger_) return this.instanceLogger_; if (!BaseService.logger_) throw new Error('BaseService.logger_ not set!!'); return BaseService.logger_; } + + setLogger(v) { + this.instanceLogger_ = v; + } } BaseService.logger_ = null; diff --git a/ReactNativeClient/lib/services/ResourceFetcher.js b/ReactNativeClient/lib/services/ResourceFetcher.js index 2c8aa01bb..9c185ce56 100644 --- a/ReactNativeClient/lib/services/ResourceFetcher.js +++ b/ReactNativeClient/lib/services/ResourceFetcher.js @@ -2,7 +2,7 @@ const Resource = require('lib/models/Resource'); const Setting = require('lib/models/Setting'); const BaseService = require('lib/services/BaseService'); const ResourceService = require('lib/services/ResourceService'); -const BaseSyncTarget = require('lib/BaseSyncTarget'); +const { Dirnames } = require('lib/services/synchronizer/utils/types'); const { Logger } = require('lib/logger.js'); const EventEmitter = require('events'); const { shim } = require('lib/shim'); @@ -17,7 +17,6 @@ class ResourceFetcher extends BaseService { this.logger_ = new Logger(); this.queue_ = []; this.fetchingItems_ = {}; - this.resourceDirName_ = BaseSyncTarget.resourceDirName(); this.maxDownloads_ = 3; this.addingResources_ = false; this.eventEmitter_ = new EventEmitter(); @@ -159,7 +158,7 @@ class ResourceFetcher extends BaseService { this.fetchingItems_[resourceId] = resource; const localResourceContentPath = Resource.fullPath(resource, !!resource.encryption_blob_encrypted); - const remoteResourceContentPath = `${this.resourceDirName_}/${resource.id}`; + const remoteResourceContentPath = `${Dirnames.Resources}/${resource.id}`; await Resource.setLocalState(resource, { fetch_status: Resource.FETCH_STATUS_STARTED }); diff --git a/ReactNativeClient/lib/services/synchronizer/LockHandler.ts b/ReactNativeClient/lib/services/synchronizer/LockHandler.ts new file mode 100644 index 000000000..7148f9534 --- /dev/null +++ b/ReactNativeClient/lib/services/synchronizer/LockHandler.ts @@ -0,0 +1,348 @@ +import { Dirnames } from './utils/types'; +const JoplinError = require('lib/JoplinError'); +const { time } = require('lib/time-utils'); +const { fileExtension, filename } = require('lib/path-utils.js'); + +export enum LockType { + None = '', + Sync = 'sync', + Exclusive = 'exclusive', +} + +export interface Lock { + type: LockType, + clientType: string, + clientId: string, + updatedTime?: number, +} + +interface RefreshTimer { + id: any, + inProgress: boolean +} + +interface RefreshTimers { + [key:string]: RefreshTimer; +} + +export interface LockHandlerOptions { + autoRefreshInterval?: number, + lockTtl?: number, +} + +export default class LockHandler { + + private api_:any = null; + private refreshTimers_:RefreshTimers = {}; + private autoRefreshInterval_:number = 1000 * 60; + private lockTtl_:number = 1000 * 60 * 3; + + constructor(api:any, options:LockHandlerOptions = null) { + if (!options) options = {}; + + this.api_ = api; + if ('lockTtl' in options) this.lockTtl_ = options.lockTtl; + if ('autoRefreshInterval' in options) this.autoRefreshInterval_ = options.autoRefreshInterval; + } + + public get lockTtl():number { + return this.lockTtl_; + } + + // Should only be done for testing purposes since all clients should + // use the same lock max age. + public set lockTtl(v:number) { + this.lockTtl_ = v; + } + + private lockFilename(lock:Lock) { + return `${[lock.type, lock.clientType, lock.clientId].join('_')}.json`; + } + + private lockTypeFromFilename(name:string):LockType { + const ext = fileExtension(name); + if (ext !== 'json') return LockType.None; + if (name.indexOf(LockType.Sync) === 0) return LockType.Sync; + if (name.indexOf(LockType.Exclusive) === 0) return LockType.Exclusive; + return LockType.None; + } + + private lockFilePath(lock:Lock) { + return `${Dirnames.Locks}/${this.lockFilename(lock)}`; + } + + private lockFileToObject(file:any):Lock { + const p = filename(file.path).split('_'); + + return { + type: p[0], + clientType: p[1], + clientId: p[2], + updatedTime: file.updated_time, + }; + } + + async locks(lockType:LockType = null):Promise { + const result = await this.api_.list(Dirnames.Locks); + if (result.hasMore) throw new Error('hasMore not handled'); // Shouldn't happen anyway + + const output = []; + for (const file of result.items) { + const type = this.lockTypeFromFilename(file.path); + if (type === LockType.None) continue; + if (lockType && type !== lockType) continue; + const lock = this.lockFileToObject(file); + output.push(lock); + } + + return output; + } + + private lockIsActive(lock:Lock):boolean { + return Date.now() - lock.updatedTime < this.lockTtl; + } + + async hasActiveLock(lockType:LockType, clientType:string = null, clientId:string = null) { + const lock = await this.activeLock(lockType, clientType, clientId); + return !!lock; + } + + // Finds if there's an active lock for this clientType and clientId and returns it. + // If clientType and clientId are not specified, returns the first active lock + // of that type instead. + async activeLock(lockType:LockType, clientType:string = null, clientId:string = null) { + const locks = await this.locks(lockType); + + if (lockType === LockType.Exclusive) { + const activeLocks = locks + .slice() + .filter((lock:Lock) => this.lockIsActive(lock)) + .sort((a:Lock, b:Lock) => { + if (a.updatedTime === b.updatedTime) { + return a.clientId < b.clientId ? -1 : +1; + } + return a.updatedTime < b.updatedTime ? -1 : +1; + }); + + if (!activeLocks.length) return null; + const activeLock = activeLocks[0]; + + if (clientType && clientType !== activeLock.clientType) return null; + if (clientId && clientId !== activeLock.clientId) return null; + return activeLock; + } else if (lockType === LockType.Sync) { + for (const lock of locks) { + if (clientType && lock.clientType !== clientType) continue; + if (clientId && lock.clientId !== clientId) continue; + if (this.lockIsActive(lock)) return lock; + } + return null; + } + + throw new Error(`Unsupported lock type: ${lockType}`); + } + + private async saveLock(lock:Lock) { + await this.api_.put(this.lockFilePath(lock), JSON.stringify(lock)); + } + + // This is for testing only + public async saveLock_(lock:Lock) { + return this.saveLock(lock); + } + + private async acquireSyncLock(clientType:string, clientId:string):Promise { + try { + let isFirstPass = true; + while (true) { + const [exclusiveLock, syncLock] = await Promise.all([ + this.activeLock(LockType.Exclusive), + this.activeLock(LockType.Sync, clientType, clientId), + ]); + + if (exclusiveLock) { + throw new JoplinError(`Cannot acquire sync lock because the following client has an exclusive lock on the sync target: ${this.lockToClientString(exclusiveLock)}`, 'hasExclusiveLock'); + } + + if (syncLock) { + // Normally the second pass should happen immediately afterwards, but if for some reason + // (slow network, etc.) it took more than 10 seconds then refresh the lock. + if (isFirstPass || Date.now() - syncLock.updatedTime > 1000 * 10) { + await this.saveLock(syncLock); + } + return syncLock; + } + + // Something wrong happened, which means we saved a lock but we didn't read + // it back. Could be application error or server issue. + if (!isFirstPass) throw new Error('Cannot acquire sync lock: either the lock could be written but not read back. Or it was expired before it was read again.'); + + await this.saveLock({ + type: LockType.Sync, + clientType: clientType, + clientId: clientId, + }); + + isFirstPass = false; + } + } catch (error) { + await this.releaseLock(LockType.Sync, clientType, clientId); + throw error; + } + } + + private lockToClientString(lock:Lock):string { + return `(${lock.clientType} #${lock.clientId})`; + } + + private async acquireExclusiveLock(clientType:string, clientId:string, timeoutMs:number = 0):Promise { + // The logic to acquire an exclusive lock, while avoiding race conditions is as follow: + // + // - Check if there is a lock file present + // + // - If there is a lock file, see if I'm the one owning it by checking that its content has my identifier. + // - If that's the case, just write to the data file then delete the lock file. + // - If that's not the case, just wait a second or a small random length of time and try the whole cycle again-. + // + // -If there is no lock file, create one with my identifier and try the whole cycle again to avoid race condition (re-check that the lock file is really mine)-. + + const startTime = Date.now(); + + async function waitForTimeout() { + if (!timeoutMs) return false; + + const elapsed = Date.now() - startTime; + if (timeoutMs && elapsed < timeoutMs) { + await time.sleep(2); + return true; + } + return false; + } + + try { + while (true) { + const [activeSyncLock, activeExclusiveLock] = await Promise.all([ + this.activeLock(LockType.Sync), + this.activeLock(LockType.Exclusive), + ]); + + if (activeSyncLock) { + if (await waitForTimeout()) continue; + throw new JoplinError(`Cannot acquire exclusive lock because the following clients have a sync lock on the target: ${this.lockToClientString(activeSyncLock)}`, 'hasSyncLock'); + } + + if (activeExclusiveLock) { + if (activeExclusiveLock.clientId === clientId) { + // Save it again to refresh the timestamp + await this.saveLock(activeExclusiveLock); + return activeExclusiveLock; + } else { + // If there's already an exclusive lock, wait for it to be released + if (await waitForTimeout()) continue; + throw new JoplinError(`Cannot acquire exclusive lock because the following client has an exclusive lock on the sync target: ${this.lockToClientString(activeExclusiveLock)}`, 'hasExclusiveLock'); + } + } else { + // If there's not already an exclusive lock, acquire one + // then loop again to check that we really got the lock + // (to prevent race conditions) + await this.saveLock({ + type: LockType.Exclusive, + clientType: clientType, + clientId: clientId, + }); + + await time.msleep(100); + } + } + } catch (error) { + await this.releaseLock(LockType.Exclusive, clientType, clientId); + throw error; + } + } + + private autoLockRefreshHandle(lock:Lock) { + return [lock.type, lock.clientType, lock.clientId].join('_'); + } + + startAutoLockRefresh(lock:Lock, errorHandler:Function):string { + const handle = this.autoLockRefreshHandle(lock); + if (this.refreshTimers_[handle]) { + throw new Error(`There is already a timer refreshing this lock: ${handle}`); + } + + this.refreshTimers_[handle] = { + id: null, + inProgress: false, + }; + + this.refreshTimers_[handle].id = setInterval(async () => { + if (this.refreshTimers_[handle].inProgress) return; + + const defer = () => { + if (!this.refreshTimers_[handle]) return; + this.refreshTimers_[handle].inProgress = false; + }; + + this.refreshTimers_[handle].inProgress = true; + + let error = null; + const hasActiveLock = await this.hasActiveLock(lock.type, lock.clientType, lock.clientId); + if (!this.refreshTimers_[handle]) return defer(); // Timeout has been cleared + + if (!hasActiveLock) { + error = new JoplinError('Lock has expired', 'lockExpired'); + } else { + try { + await this.acquireLock(lock.type, lock.clientType, lock.clientId); + if (!this.refreshTimers_[handle]) return defer(); // Timeout has been cleared + } catch (e) { + error = e; + } + } + + if (error) { + if (this.refreshTimers_[handle]) { + clearInterval(this.refreshTimers_[handle].id); + delete this.refreshTimers_[handle]; + } + errorHandler(error); + } + + defer(); + }, this.autoRefreshInterval_); + + return handle; + } + + stopAutoLockRefresh(lock:Lock) { + const handle = this.autoLockRefreshHandle(lock); + if (!this.refreshTimers_[handle]) { + // Should not throw an error because lock may have been cleared in startAutoLockRefresh + // if there was an error. + // throw new Error(`There is no such lock being auto-refreshed: ${this.lockToString(lock)}`); + return; + } + + clearInterval(this.refreshTimers_[handle].id); + delete this.refreshTimers_[handle]; + } + + async acquireLock(lockType:LockType, clientType:string, clientId:string, timeoutMs:number = 0):Promise { + if (lockType === LockType.Sync) { + return this.acquireSyncLock(clientType, clientId); + } else if (lockType === LockType.Exclusive) { + return this.acquireExclusiveLock(clientType, clientId, timeoutMs); + } else { + throw new Error(`Invalid lock type: ${lockType}`); + } + } + + async releaseLock(lockType:LockType, clientType:string, clientId:string) { + await this.api_.delete(this.lockFilePath({ + type: lockType, + clientType: clientType, + clientId: clientId, + })); + } + +} diff --git a/ReactNativeClient/lib/services/synchronizer/MigrationHandler.ts b/ReactNativeClient/lib/services/synchronizer/MigrationHandler.ts new file mode 100644 index 000000000..f8a8dea60 --- /dev/null +++ b/ReactNativeClient/lib/services/synchronizer/MigrationHandler.ts @@ -0,0 +1,141 @@ +import LockHandler, { LockType } from './LockHandler'; +import { Dirnames } from './utils/types'; +const BaseService = require('lib/services/BaseService.js'); + +// To add a new migration: +// - Add the migration logic in ./migrations/VERSION_NUM.js +// - Add the file to the array below. +// - Set Setting.syncVersion to VERSION_NUM in models/Setting.js +// - Add tests in synchronizer_migrationHandler +const migrations = [ + null, + require('./migrations/1.js').default, + require('./migrations/2.js').default, +]; + +const Setting = require('lib/models/Setting'); +const { sprintf } = require('sprintf-js'); +const JoplinError = require('lib/JoplinError'); + +interface SyncTargetInfo { + version: number, +} + +export default class MigrationHandler extends BaseService { + + private api_:any = null; + private lockHandler_:LockHandler = null; + private clientType_:string; + private clientId_:string; + + constructor(api:any, lockHandler:LockHandler, clientType:string, clientId:string) { + super(); + this.api_ = api; + this.lockHandler_ = lockHandler; + this.clientType_ = clientType; + this.clientId_ = clientId; + } + + public async fetchSyncTargetInfo():Promise { + const syncTargetInfoText = await this.api_.get('info.json'); + + // Returns version 0 if the sync target is empty + let output:SyncTargetInfo = { version: 0 }; + + if (syncTargetInfoText) { + output = JSON.parse(syncTargetInfoText); + if (!output.version) throw new Error('Missing "version" field in info.json'); + } else { + const oldVersion = await this.api_.get('.sync/version.txt'); + if (oldVersion) output = { version: 1 }; + } + + return output; + } + + private serializeSyncTargetInfo(info:SyncTargetInfo) { + return JSON.stringify(info); + } + + async checkCanSync():Promise { + const supportedSyncTargetVersion = Setting.value('syncVersion'); + const syncTargetInfo = await this.fetchSyncTargetInfo(); + + if (syncTargetInfo.version) { + if (syncTargetInfo.version > supportedSyncTargetVersion) { + throw new JoplinError(sprintf('Sync version of the target (%d) is greater than the version supported by the client (%d). Please upgrade your client.', syncTargetInfo.version, supportedSyncTargetVersion), 'outdatedClient'); + } else if (syncTargetInfo.version < supportedSyncTargetVersion) { + throw new JoplinError(sprintf('Sync version of the target (%d) is lower than the version supported by the client (%d). Please upgrade the sync target.', syncTargetInfo.version, supportedSyncTargetVersion), 'outdatedSyncTarget'); + } + } + + return syncTargetInfo; + } + + async upgrade(targetVersion:number = 0) { + const supportedSyncTargetVersion = Setting.value('syncVersion'); + const syncTargetInfo = await this.fetchSyncTargetInfo(); + + if (syncTargetInfo.version > supportedSyncTargetVersion) { + throw new JoplinError(sprintf('Sync version of the target (%d) is greater than the version supported by the client (%d). Please upgrade your client.', syncTargetInfo.version, supportedSyncTargetVersion), 'outdatedClient'); + } + + // if (supportedSyncTargetVersion !== migrations.length - 1) { + // // Sanity check - it means a migration has been added by syncVersion has not be incremented or vice-versa, + // // so abort as it can cause strange issues. + // throw new JoplinError('Application error: mismatch between max supported sync version and max migration number: ' + supportedSyncTargetVersion + ' / ' + (migrations.length - 1)); + // } + + // Special case for version 1 because it didn't have the lock folder and without + // it the lock handler will break. So we create the directory now. + // Also if the sync target version is 0, it means it's a new one so we need the + // lock folder first before doing anything else. + if (syncTargetInfo.version === 0 || syncTargetInfo.version === 1) { + this.logger().info('MigrationHandler: Sync target version is 0 or 1 - creating "locks" directory:', syncTargetInfo); + await this.api_.mkdir(Dirnames.Locks); + } + + this.logger().info('MigrationHandler: Acquiring exclusive lock'); + const exclusiveLock = await this.lockHandler_.acquireLock(LockType.Exclusive, this.clientType_, this.clientId_, 1000 * 30); + let autoLockError = null; + this.lockHandler_.startAutoLockRefresh(exclusiveLock, (error:any) => { + autoLockError = error; + }); + + this.logger().info('MigrationHandler: Acquired exclusive lock:', exclusiveLock); + + try { + for (let newVersion = syncTargetInfo.version + 1; newVersion < migrations.length; newVersion++) { + if (targetVersion && newVersion > targetVersion) break; + + const fromVersion = newVersion - 1; + + this.logger().info(`MigrationHandler: Migrating from version ${fromVersion} to version ${newVersion}`); + + const migration = migrations[newVersion]; + if (!migration) continue; + + try { + if (autoLockError) throw autoLockError; + await migration(this.api_); + if (autoLockError) throw autoLockError; + + await this.api_.put('info.json', this.serializeSyncTargetInfo({ + ...syncTargetInfo, + version: newVersion, + })); + + this.logger().info(`MigrationHandler: Done migrating from version ${fromVersion} to version ${newVersion}`); + } catch (error) { + error.message = `Could not upgrade from version ${fromVersion} to version ${newVersion}: ${error.message}`; + throw error; + } + } + } finally { + this.logger().info('MigrationHandler: Releasing exclusive lock'); + this.lockHandler_.stopAutoLockRefresh(exclusiveLock); + await this.lockHandler_.releaseLock(LockType.Exclusive, this.clientType_, this.clientId_); + } + } + +} diff --git a/ReactNativeClient/lib/services/synchronizer/gui/useSyncTargetUpgrade.ts b/ReactNativeClient/lib/services/synchronizer/gui/useSyncTargetUpgrade.ts new file mode 100644 index 000000000..86d60b304 --- /dev/null +++ b/ReactNativeClient/lib/services/synchronizer/gui/useSyncTargetUpgrade.ts @@ -0,0 +1,50 @@ +import { useEffect, useState } from 'react'; +import MigrationHandler from 'lib/services/synchronizer/MigrationHandler'; +const Setting = require('lib/models/Setting'); +const { reg } = require('lib/registry'); + +export interface SyncTargetUpgradeResult { + done: boolean, + error: any, +} + +export default function useSyncTargetUpgrade():SyncTargetUpgradeResult { + const [upgradeResult, setUpgradeResult] = useState({ + done: false, + error: null, + }); + + async function upgradeSyncTarget() { + let error = null; + try { + const synchronizer = await reg.syncTarget().synchronizer(); + + const migrationHandler = new MigrationHandler( + synchronizer.api(), + synchronizer.lockHandler(), + Setting.value('appType'), + Setting.value('clientId') + ); + + await migrationHandler.upgrade(); + } catch (e) { + error = e; + } + + if (!error) { + Setting.setValue('sync.upgradeState', Setting.SYNC_UPGRADE_STATE_IDLE); + await Setting.saveAll(); + } + + setUpgradeResult({ + done: true, + error: error, + }); + } + + useEffect(function() { + upgradeSyncTarget(); + }, []); + + return upgradeResult; +} diff --git a/ReactNativeClient/lib/services/synchronizer/migrations/1.ts b/ReactNativeClient/lib/services/synchronizer/migrations/1.ts new file mode 100644 index 000000000..8918f36e0 --- /dev/null +++ b/ReactNativeClient/lib/services/synchronizer/migrations/1.ts @@ -0,0 +1,9 @@ +export default async function(api:any) { + await Promise.all([ + api.mkdir('.resource'), + api.mkdir('.sync'), + api.mkdir('.lock'), + ]); + + await api.put('.sync/version.txt', '1'); +} diff --git a/ReactNativeClient/lib/services/synchronizer/migrations/2.ts b/ReactNativeClient/lib/services/synchronizer/migrations/2.ts new file mode 100644 index 000000000..fdf763005 --- /dev/null +++ b/ReactNativeClient/lib/services/synchronizer/migrations/2.ts @@ -0,0 +1,10 @@ +import { Dirnames } from '../utils/types'; + +export default async function(api:any) { + await Promise.all([ + api.put('.sync/version.txt', '2'), + api.put('.sync/readme.txt', '2020-07-16: In the new sync format, the version number is stored in /info.json. However, for backward compatibility, we need to keep the old version.txt file here, otherwise old clients will automatically recreate it, and assume a sync target version 1. So we keep it here but set its value to "2", so that old clients know that they need to be upgraded. This directory can be removed after a year or so, once we are confident that all clients have been upgraded to recent versions.'), + api.mkdir(Dirnames.Locks), + api.mkdir(Dirnames.Temp), + ]); +} diff --git a/ReactNativeClient/lib/services/synchronizer/utils/types.ts b/ReactNativeClient/lib/services/synchronizer/utils/types.ts new file mode 100644 index 000000000..27c3697f0 --- /dev/null +++ b/ReactNativeClient/lib/services/synchronizer/utils/types.ts @@ -0,0 +1,6 @@ +// eslint-disable-next-line import/prefer-default-export +export enum Dirnames { + Locks = 'locks', + Resources = '.resource', + Temp = 'temp', +} diff --git a/ReactNativeClient/lib/synchronizer.js b/ReactNativeClient/lib/synchronizer.js index cb6e4c5aa..465997d67 100644 --- a/ReactNativeClient/lib/synchronizer.js +++ b/ReactNativeClient/lib/synchronizer.js @@ -12,19 +12,18 @@ const { time } = require('lib/time-utils.js'); const { Logger } = require('lib/logger.js'); const { _ } = require('lib/locale.js'); const { shim } = require('lib/shim.js'); -const { filename, fileExtension } = require('lib/path-utils'); +// const { filename, fileExtension } = require('lib/path-utils'); const JoplinError = require('lib/JoplinError'); -const BaseSyncTarget = require('lib/BaseSyncTarget'); const TaskQueue = require('lib/TaskQueue'); +const LockHandler = require('lib/services/synchronizer/LockHandler').default; +const MigrationHandler = require('lib/services/synchronizer/MigrationHandler').default; +const { Dirnames } = require('lib/services/synchronizer/utils/types'); class Synchronizer { constructor(db, api, appType) { this.state_ = 'idle'; this.db_ = db; this.api_ = api; - this.syncDirName_ = '.sync'; - this.lockDirName_ = '.lock'; - this.resourceDirName_ = BaseSyncTarget.resourceDirName(); this.logger_ = new Logger(); this.appType_ = appType; this.cancelling_ = false; @@ -66,6 +65,18 @@ class Synchronizer { return this.logger_; } + lockHandler() { + if (this.lockHandler_) return this.lockHandler_; + this.lockHandler_ = new LockHandler(this.api()); + return this.lockHandler_; + } + + migrationHandler() { + if (this.migrationHandler_) return this.migrationHandler_; + this.migrationHandler_ = new MigrationHandler(this.api(), this.lockHandler(), this.appType_, this.clientId_); + return this.migrationHandler_; + } + maxResourceSize() { if (this.maxResourceSize_ !== null) return this.maxResourceSize_; return this.appType_ === 'mobile' ? 100 * 1000 * 1000 : Infinity; @@ -205,73 +216,40 @@ class Synchronizer { return state; } - async acquireLock_() { - await this.checkLock_(); - await this.api().put(`${this.lockDirName_}/${this.clientId()}_${Date.now()}.lock`, `${Date.now()}`); - } - - async releaseLock_() { - const lockFiles = await this.lockFiles_(); - for (const lockFile of lockFiles) { - const p = this.parseLockFilePath(lockFile.path); - if (p.clientId === this.clientId()) { - await this.api().delete(p.fullPath); - } - } - } - - async lockFiles_() { - const output = await this.api().list(this.lockDirName_); - return output.items.filter((p) => { - const ext = fileExtension(p.path); - return ext === 'lock'; - }); - } - - parseLockFilePath(path) { - const splitted = filename(path).split('_'); - const fullPath = `${this.lockDirName_}/${path}`; - if (splitted.length !== 2) throw new Error(`Sync target appears to be locked but lock filename is invalid: ${fullPath}. Please delete it on the sync target to continue.`); - return { - clientId: splitted[0], - timestamp: Number(splitted[1]), - fullPath: fullPath, - }; - } - - async checkLock_() { - const lockFiles = await this.lockFiles_(); - if (lockFiles.length) { - const lock = this.parseLockFilePath(lockFiles[0].path); - - if (lock.clientId === this.clientId()) { - await this.releaseLock_(); - } else { - throw new Error(`The sync target was locked by client ${lock.clientId} on ${time.unixMsToLocalDateTime(lock.timestamp)} and cannot be accessed. If no app is currently operating on the sync target, you can delete the files in the "${this.lockDirName_}" directory on the sync target to resume.`); - } - } - } - - async checkSyncTargetVersion_() { - const supportedSyncTargetVersion = Setting.value('syncVersion'); - const syncTargetVersion = await this.api().get('.sync/version.txt'); - - if (!syncTargetVersion) { - await this.api().put('.sync/version.txt', `${supportedSyncTargetVersion}`); - } else { - if (Number(syncTargetVersion) > supportedSyncTargetVersion) { - throw new Error(sprintf('Sync version of the target (%d) does not match sync version supported by client (%d). Please upgrade your client.', Number(syncTargetVersion), supportedSyncTargetVersion)); - } else { - await this.api().put('.sync/version.txt', `${supportedSyncTargetVersion}`); - // TODO: do upgrade job - } - } - } - isFullSync(steps) { return steps.includes('update_remote') && steps.includes('delete_remote') && steps.includes('delta'); } + // TODO: test lockErrorStatus_ + async lockErrorStatus_() { + const hasActiveExclusiveLock = await this.lockHandler().hasActiveLock('exclusive'); + if (hasActiveExclusiveLock) return 'hasExclusiveLock'; + + const hasActiveSyncLock = await this.lockHandler().hasActiveLock('sync', this.appType_, this.clientId_); + if (!hasActiveSyncLock) return 'syncLockGone'; + + return ''; + } + + async apiCall(fnName, ...args) { + if (this.syncTargetIsLocked_) throw new JoplinError('Sync target is locked - aborting API call', 'lockError'); + + try { + const output = await this.api()[fnName](...args); + return output; + } catch (error) { + const lockStatus = await this.lockErrorStatus_(); + // When there's an error due to a lock, we re-wrap the error and change the error code so that error handling + // does not do special processing on the original error. For example, if a resource could not be downloaded, + // don't mark it as a "cannotSyncItem" since we don't know that. + if (lockStatus) { + throw new JoplinError(`Sync target lock error: ${lockStatus}. Original error was: ${error.message}`, 'lockError'); + } else { + throw error; + } + } + } + // Synchronisation is done in three major steps: // // 1. UPLOAD: Send to the sync target the items that have changed since the last sync. @@ -300,6 +278,7 @@ class Synchronizer { const syncTargetId = this.api().syncTargetId(); + this.syncTargetIsLocked_ = false; this.cancelling_ = false; const masterKeysBefore = await MasterKey.count(); @@ -319,19 +298,38 @@ class Synchronizer { }; const resourceRemotePath = resourceId => { - return `${this.resourceDirName_}/${resourceId}`; + return `${Dirnames.Resources}/${resourceId}`; }; let errorToThrow = null; + let syncLock = null; try { - await this.api().mkdir(this.syncDirName_); - await this.api().mkdir(this.lockDirName_); - this.api().setTempDirName(this.syncDirName_); - await this.api().mkdir(this.resourceDirName_); + try { + const syncTargetInfo = await this.migrationHandler().checkCanSync(); - await this.checkLock_(); - await this.checkSyncTargetVersion_(); + this.logger().info('Sync target info:', syncTargetInfo); + + if (!syncTargetInfo.version) { + this.logger().info('Sync target is new - setting it up...'); + await this.migrationHandler().upgrade(Setting.value('syncVersion')); + } + } catch (error) { + if (error.code === 'outdatedSyncTarget') { + Setting.setValue('sync.upgradeState', Setting.SYNC_UPGRADE_STATE_SHOULD_DO); + } + throw error; + } + + this.api().setTempDirName(Dirnames.Temp); + + syncLock = await this.lockHandler().acquireLock('sync', this.appType_, this.clientId_); + + this.lockHandler().startAutoLockRefresh(syncLock, (error) => { + this.logger().warn('Could not refresh lock - cancelling sync. Error was:', error); + this.syncTargetIsLocked_ = true; + this.cancel(); + }); // ======================================================================== // 1. UPLOAD @@ -369,7 +367,7 @@ class Synchronizer { // (by setting an updated_time less than current time). 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 = await this.api().stat(path); + const remote = await this.apiCall('stat', path); let action = null; let reason = ''; @@ -407,7 +405,7 @@ class Synchronizer { // OneDrive does not appear to have accurate timestamps as lastModifiedDateTime would occasionally be // a few seconds ahead of what it was set with setTimestamp() try { - remoteContent = await this.api().get(path); + remoteContent = await this.apiCall('get', path); } catch (error) { if (error.code === 'rejectedByTarget') { this.progressReport_.errors.push(error); @@ -450,7 +448,7 @@ class Synchronizer { this.logger().warn(`Uploading a large resource (resourceId: ${local.id}, size:${local.size} bytes) which may tie up the sync process.`); } - await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' }); + await this.apiCall('put', remoteContentPath, null, { path: localResourceContentPath, source: 'file' }); } catch (error) { if (error && ['rejectedByTarget', 'fileNotFound'].indexOf(error.code) >= 0) { await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message); @@ -467,7 +465,7 @@ class Synchronizer { try { if (this.testingHooks_.indexOf('notesRejectedByTarget') >= 0 && local.type_ === BaseModel.TYPE_NOTE) throw new JoplinError('Testing rejectedByTarget', 'rejectedByTarget'); const content = await ItemClass.serializeForSync(local); - await this.api().put(path, content); + await this.apiCall('put', path, content); } catch (error) { if (error && error.code === 'rejectedByTarget') { await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message); @@ -593,11 +591,11 @@ class Synchronizer { const item = deletedItems[i]; const path = BaseItem.systemPath(item.item_id); this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted'); - await this.api().delete(path); + await this.apiCall('delete', path); if (item.item_type === BaseModel.TYPE_RESOURCE) { const remoteContentPath = resourceRemotePath(item.item_id); - await this.api().delete(remoteContentPath); + await this.apiCall('delete', remoteContentPath); } await BaseItem.remoteDeletedItem(syncTargetId, item.item_id); @@ -628,7 +626,7 @@ class Synchronizer { while (true) { if (this.cancelling() || hasCancelled) break; - const listResult = await this.api().delta('', { + const listResult = await this.apiCall('delta', '', { context: context, // allItemIdsHandler() provides a way for drivers that don't have a delta API to @@ -653,7 +651,7 @@ class Synchronizer { if (this.cancelling()) break; this.downloadQueue_.push(remote.path, async () => { - return this.api().get(remote.path); + return this.apiCall('get', remote.path); }); } @@ -669,7 +667,7 @@ class Synchronizer { if (!BaseItem.isSystemPath(remote.path)) continue; // The delta API might return things like the .sync, .resource or the root folder const loadContent = async () => { - const task = await this.downloadQueue_.waitForResult(path); // await this.api().get(path); + const task = await this.downloadQueue_.waitForResult(path); // await this.apiCall('get', path); if (task.error) throw task.error; if (!task.result) return null; return await BaseItem.unserialize(task.result); @@ -832,13 +830,13 @@ class Synchronizer { } catch (error) { if (throwOnError) { errorToThrow = error; - } else if (error && ['cannotEncryptEncrypted', 'noActiveMasterKey', 'processingPathTwice', 'failSafe'].indexOf(error.code) >= 0) { + } else if (error && ['cannotEncryptEncrypted', 'noActiveMasterKey', 'processingPathTwice', 'failSafe', 'lockError', 'outdatedSyncTarget'].indexOf(error.code) >= 0) { // Only log an info statement for this since this is a common condition that is reported // in the application, and needs to be resolved by the user. // Or it's a temporary issue that will be resolved on next sync. this.logger().info(error.message); - if (error.code === 'failSafe') { + if (error.code === 'failSafe' || error.code === 'lockError') { // Get the message to display on UI, but not in testing to avoid poluting stdout if (!shim.isTestingEnv()) this.progressReport_.errors.push(error.message); this.logLastRequests(); @@ -857,6 +855,13 @@ class Synchronizer { } } + if (syncLock) { + this.lockHandler().stopAutoLockRefresh(syncLock); + await this.lockHandler().releaseLock('sync', this.appType_, this.clientId_); + } + + this.syncTargetIsLocked_ = false; + if (this.cancelling()) { this.logger().info('Synchronisation was cancelled.'); this.cancelling_ = false; diff --git a/ReactNativeClient/root.js b/ReactNativeClient/root.js index 54878558f..5d9b1b7b9 100644 --- a/ReactNativeClient/root.js +++ b/ReactNativeClient/root.js @@ -44,6 +44,7 @@ const { SearchScreen } = require('lib/components/screens/search.js'); const { OneDriveLoginScreen } = require('lib/components/screens/onedrive-login.js'); const { EncryptionConfigScreen } = require('lib/components/screens/encryption-config.js'); const { DropboxLoginScreen } = require('lib/components/screens/dropbox-login.js'); +const UpgradeSyncTargetScreen = require('lib/components/screens/UpgradeSyncTargetScreen').default; const Setting = require('lib/models/Setting.js'); const { MenuContext } = require('react-native-popup-menu'); const { SideMenu } = require('lib/components/side-menu.js'); @@ -720,6 +721,7 @@ class AppComponent extends React.Component { OneDriveLogin: { screen: OneDriveLoginScreen }, DropboxLogin: { screen: DropboxLoginScreen }, EncryptionConfig: { screen: EncryptionConfigScreen }, + UpgradeSyncTarget: { screen: UpgradeSyncTargetScreen }, Log: { screen: LogScreen }, Status: { screen: StatusScreen }, Search: { screen: SearchScreen }, diff --git a/Tools/build-website.js b/Tools/build-website.js index c56b2ea51..66e5feab3 100644 --- a/Tools/build-website.js +++ b/Tools/build-website.js @@ -90,6 +90,9 @@ https://github.com/laurent22/joplin/blob/master/{{{sourceMarkdownFile}}} #toc ul { margin-bottom: 10px; } + #toc > ul > li { + margin-bottom: 10px; + } #toc { padding-bottom: 1em; } @@ -572,23 +575,25 @@ async function main() { renderMdToHtml(makeHomePageMd(), `${rootDir}/docs/index.html`, { sourceMarkdownFile: 'README.md' }); const sources = [ - ['readme/changelog.md', 'docs/changelog/index.html', { title: 'Changelog (Desktop App)' }], + ['readme/api.md', 'docs/api/index.html', { title: 'REST API' }], ['readme/changelog_cli.md', 'docs/changelog_cli/index.html', { title: 'Changelog (CLI App)' }], + ['readme/changelog.md', 'docs/changelog/index.html', { title: 'Changelog (Desktop App)' }], ['readme/clipper.md', 'docs/clipper/index.html', { title: 'Web Clipper' }], + ['readme/conflict.md', 'docs/conflict/index.html', { title: 'What is a conflict?' }], ['readme/debugging.md', 'docs/debugging/index.html', { title: 'Debugging' }], ['readme/desktop.md', 'docs/desktop/index.html', { title: 'Desktop Application' }], ['readme/donate.md', 'docs/donate/index.html', { title: 'Donate' }], ['readme/e2ee.md', 'docs/e2ee/index.html', { title: 'End-To-End Encryption' }], ['readme/faq.md', 'docs/faq/index.html', { title: 'FAQ' }], + ['readme/markdown.md', 'docs/markdown/index.html', { title: 'Markdown Guide' }], ['readme/mobile.md', 'docs/mobile/index.html', { title: 'Mobile Application' }], - ['readme/spec.md', 'docs/spec/index.html', { title: 'Specifications' }], + ['readme/nextcloud_app.md', 'docs/nextcloud_app/index.html', { title: 'Joplin Web API for Nextcloud' }], + ['readme/prereleases.md', 'docs/prereleases/index.html', { title: 'Pre-releases' }], + ['readme/spec/e2ee.md', 'docs/spec/e2ee/index.html', { title: 'E2EE Specifications' }], + ['readme/spec/history.md', 'docs/spec/history/index.html', { title: 'Note History Specifications' }], + ['readme/spec/sync_lock.md', 'docs/spec/sync_lock/index.html', { title: 'Sync Lock Specifications' }], ['readme/stats.md', 'docs/stats/index.html', { title: 'Statistics' }], ['readme/terminal.md', 'docs/terminal/index.html', { title: 'Terminal Application' }], - ['readme/api.md', 'docs/api/index.html', { title: 'REST API' }], - ['readme/prereleases.md', 'docs/prereleases/index.html', { title: 'Pre-releases' }], - ['readme/markdown.md', 'docs/markdown/index.html', { title: 'Markdown Guide' }], - ['readme/conflict.md', 'docs/conflict/index.html', { title: 'What is a conflict?' }], - ['readme/nextcloud_app.md', 'docs/nextcloud_app/index.html', { title: 'Joplin Web API for Nextcloud' }], ['readme/gsoc2020/index.md', 'docs/gsoc2020/index.html', { title: 'Google Summer of Code' }], ['readme/gsoc2020/ideas.md', 'docs/gsoc2020/ideas/index.html', { title: 'GSoC: Project Ideas' }], diff --git a/docs/api/index.html b/docs/api/index.html index f0e15ac07..96551a52b 100644 --- a/docs/api/index.html +++ b/docs/api/index.html @@ -86,6 +86,9 @@ https://github.com/laurent22/joplin/blob/master/readme/api.md #toc ul { margin-bottom: 10px; } + #toc > ul > li { + margin-bottom: 10px; + } #toc { padding-bottom: 1em; } @@ -316,13 +319,20 @@ https://github.com/laurent22/joplin/blob/master/readme/api.md
  • Markdown Guide
  • How to enable end-to-end encryption
  • What is a conflict?
  • -
  • End-to-end encryption spec
  • How to enable debug mode
  • API documentation
  • FAQ
  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

    • Google Summer of Code 2020
    • diff --git a/docs/spec/e2ee/index.html b/docs/spec/e2ee/index.html new file mode 100644 index 000000000..fb84ee4c2 --- /dev/null +++ b/docs/spec/e2ee/index.html @@ -0,0 +1,393 @@ + + + + + + + E2EE Specifications | Joplin + + + + + + + + + + + + + +
      + +
      + +

      Joplin

      +

      An open source note taking and to-do application with synchronisation capabilities

      +
      + + + +
      + + + + + + + + diff --git a/docs/spec/history/index.html b/docs/spec/history/index.html new file mode 100644 index 000000000..70e40dcbe --- /dev/null +++ b/docs/spec/history/index.html @@ -0,0 +1,421 @@ + + + + + + + Note History Specifications | Joplin + + + + + + + + + + + + + +
      + +
      + +

      Joplin

      +

      An open source note taking and to-do application with synchronisation capabilities

      +
      + + + +
      + +

      Note history🔗

      +

      The note history preserves versions of the notes at regular interval. All the revisions are synced and shared across all devices.

      +

      Revision format🔗

      +

      To save space, only the diff of a note is saved: the title and body are saved as text diff, while the other properties are saved as an object diff (i.e. only the modified properties are saved).

      +

      Advantages: it saves space, and writes are fast.

      +

      Disadvantages: reading a note version back is slower since it needs to be rebuilt, starting from the oldest revision and applying diffs to it one at a time.

      +

      Revision service🔗

      +

      Every time an object is changed in Joplin, some metadata is added to the changed_items table. The revision service uses this to know what notes need a new revision. Specifically it will create a revision under these conditions:

      +
        +
      1. +

        The note hadn't had a revision for more than 10 minutes

        +
      2. +
      3. +

        The note was recently modified, but before that it hadn't had a revision for more than 7 days

        +
      4. +
      +

      Condition 1 saves the current state of the note (i.e. after the edit). Condition 2 saves the state has it was before the edit.

      +

      The reason for that is that we save revisions every 10 minutes, but if you make many changes within a few minutes and then stop modifying the note, the final revision will not contain the current content of the note. Basically at one point (let's say at t1) the service will see there's a revision from less than 10 minutes, and will not save a new one.

      +

      That's why when you change the note again more than 7 days later, we save that revision that wasn't saved at t1. The logic is a bit complicated but the goal is to preserve the last significant state of a note. If you make many changes to a note then stop editing it for several months, the last significant state was at the end of that series of edits, so we need to save that.

      +

      Additionally, notes that were created before the service existed never had revisions. So the 7 days logic ensure that they get one the first time they are modified.

      +

      Revision deletion🔗

      +

      Revisions are deleted once they are older than a given interval as set in revisionService.oldNoteInterval (90 days by default).

      +

      Disabling the service🔗

      +

      When disabled, no new revision is saved, but the existing one remain there, and will only be deleted after the interval specified in revisionService.oldNoteInterval.

      +

      Revision settings are global🔗

      +

      Since all the revisions are synced across all devices, it means these settings are kind of global. So for example, if on one device you set it to keep revisions for 30 days, and on another to 100 days, the revisions older than 30 days will be deleted, and then this deletion will be synced. So in practice it means revisions are kept for whatever is the minimum number of days as set on any of the devices. In that particular case, the 100 days setting will be essentially ignored, and only the 30 days one will apply.

      +

      Why is there less than 10 minutes between some revisions?🔗

      +

      It can happen if a note is changed on two different devices within less than 10 minutes. A revision will be created on each device, then when they are synced it will appear that there's less than 10 min between the revisions.

      + + + + + + + diff --git a/docs/spec/index.html b/docs/spec/sync_lock/index.html similarity index 58% rename from docs/spec/index.html rename to docs/spec/sync_lock/index.html index efa5553ef..b2358016a 100644 --- a/docs/spec/index.html +++ b/docs/spec/sync_lock/index.html @@ -5,16 +5,16 @@ !!! WARNING !!! -This file was auto-generated from readme/spec.md and any manual change +This file was auto-generated from readme/spec/sync_lock.md and any manual change made to it will be overwritten. To make a change to this file please modify the source Markdown file: -https://github.com/laurent22/joplin/blob/master/readme/spec.md +https://github.com/laurent22/joplin/blob/master/readme/spec/sync_lock.md --> - Specifications | Joplin + Sync Lock Specifications | Joplin @@ -86,6 +86,9 @@ https://github.com/laurent22/joplin/blob/master/readme/spec.md #toc ul { margin-bottom: 10px; } + #toc > ul > li { + margin-bottom: 10px; + } #toc { padding-bottom: 1em; } @@ -272,7 +275,7 @@ https://github.com/laurent22/joplin/blob/master/readme/spec.md -
      +
      @@ -316,13 +319,20 @@ https://github.com/laurent22/joplin/blob/master/readme/spec.md
    • Markdown Guide
    • How to enable end-to-end encryption
    • What is a conflict?
    • -
    • End-to-end encryption spec
    • How to enable debug mode
    • API documentation
    • FAQ
  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

    -

    Encryption🔗

    -

    Encrypted data is encoded to ASCII because encryption/decryption functions in React Native can only deal with strings. So for compatibility with all the apps we need to use the lowest common denominator.

    -

    Encrypted data format🔗

    -

    Header🔗

    - - - - - - - - - - - - - - - - - -
    NameSize
    Identifier3 chars ("JED")
    Version number2 chars (Hexa string)
    -

    This is followed by the encryption metadata:

    - - - - - - - - - - - - - - - - - - - - - -
    NameSize
    Length6 chars (Hexa string)
    Encryption method2 chars (Hexa string)
    Master key ID32 chars (Hexa string)
    -

    See lib/services/EncryptionService.js for the list of available encryption methods.

    -

    Data chunk🔗

    -

    The data is encoded in one or more chunks for performance reasons. That way it is possible to take a block of data from one file and encrypt it to another block in another file. Encrypting/decrypting the whole file in one go would not work (on mobile especially).

    - - - - - - - - - - - - - - - - - -
    NameSize
    Length6 chars (Hexa string)
    Data("Length" bytes) (ASCII)
    -

    Master Keys🔗

    -

    The master keys are used to encrypt and decrypt data. They can be generated from the Encryption Service and are saved to the database. They are themselves encrypted via a user password using a strong encryption method.

    -

    These encrypted master keys are transmitted with the sync data so that they can be available to each client. Each client will need to supply the user password to decrypt each key.

    -

    The application supports multiple master keys in order to handle cases where one offline client starts encrypting notes, then another offline client starts encrypting notes too, and later both sync. Both master keys will have to be decrypted separately with the user password.

    -

    Only one master key can be active for encryption purposes. For decryption, the algorithm will check the Master Key ID in the header, then check if it's available to the current app and, if so, use this for decryption.

    -

    Encryption Service🔗

    -

    The applications make use of the EncryptionService class to handle encryption and decryption. Before it can be used, a least one master key must be loaded into it and be marked as "active".

    -

    Encryption workflow🔗

    -

    Items are encrypted only during synchronisation, when they are serialised (via BaseItem.serializeForSync), so before being sent to the sync target.

    -

    They are decrypted by DecryptionWorker in the background.

    -

    The apps handle displaying both decrypted and encrypted items, so that user is aware that these items are there even if not yet decrypted. Encrypted items are mostly read-only to the user, except that they can be deleted.

    -

    Enabling and disabling encryption🔗

    -

    Enabling/disabling E2EE while two clients are in sync might have an unintuitive behaviour (although that behaviour might be correct), so below some scenarios are explained:

    +

    Lock types🔗

    +

    There are two types of locks:

      -
    • -

      If client 1 enables E2EE, all items will be synced to target and will appear encrypted on target. Although all items have been re-uploaded to the target, their timestamps did not change (because the item data itself has not changed, only its representation). Because of this, client 2 will not re-download the items - it does not need to do so anyway since it has already the item data.

      +
    • SYNC: Used when synchronising a client with a target. There can be multiple SYNC locks simultaneously.
    • +
    • EXCLUSIVE: Used when a client upgrades a sync target. There can be only one EXCLUSIVE lock.
    • +
    +

    Timeout🔗

    +

    When a client acquires a lock, it must refresh it every X seconds. A lock timeout after Y seconds (where X < Y). A lock with a timestamp greater than Y is considered expired and can be ignored by other clients. A client that tries to refresh a lock that has expired should fail.

    +

    For example, if a client is currently syncing, it must stop doing so if it couldn't refresh the lock with Y seconds.

    +

    For example, if a client is upgrading a target, it must stop doing so if it couldn't refresh the lock within Y seconds.

    +

    Acquiring a SYNC lock🔗

    +
      +
    • The client check if there is a valid EXCLUSIVE lock on the target
    • +
    • If there is, it must stop the sync process
    • +
    • Otherwise it checks if it owns a SYNC lock on the target +
        +
      • If it does, it starts syncing +
          +
        • When syncing is done, it releases the SYNC lock
        • +
      • -
      • -

        When a client sync and download a master key for the first time, encryption will be automatically enabled (user will need to supply the master key password). In that case, all items that are not encrypted will be re-synced. Uploading only non-encrypted items is an optimisation since if an item is already encrypted locally it means it's encrypted on target too.

        -
      • -
      • -

        If both clients are in sync with E2EE enabled: if client 1 disable E2EE, it's going to re-upload all the items unencrypted. Client 2 again will not re-download the items for the same reason as above (data did not change, only representation). Note that user must manually disable E2EE on all clients otherwise some will continue to upload encrypted items. Since synchronisation is stateless, clients do not know whether other clients use E2EE or not so this step has to be manual.

        -
      • -
      • -

        Although messy, Joplin supports having some clients send encrypted items and others unencrypted ones. The situation gets resolved once all the clients have the same E2EE settings.

        -
      • -
      • -

        Currently, there is no way to delete encryption keys if you do not need them anymore or if you disabled the encryption completely. You will get a persistant notification to provide a Master Key password on a new device, even if encryption is disabled. Entering the Master Key(s) password and still having the encryption disabled will get rid of the notification. See Delete E2EE Master Keys for more info.

        +
      • If it doesn't, it acquires a SYNC lock and repeat the complete process from the beginning (to avoid race conditions)
      • +
    +

    Acquiring an EXCLUSIVE lock🔗

    +
      +
    • The client check if there is a valid EXCLUSIVE or SYNC lock on the target
    • +
    • If there is, it must stop the upgrade process (or wait till target is unlocked)
    • +
    • Otherwise it checks if it owns an EXCLUSIVE lock on the target +
        +
      • If it does, it starts upgrading the target +
          +
        • When upgrading is done, it releases the EXCLUSIVE lock
        • +
        +
      • +
      • If it doesn't, it acquires an EXCLUSIVE lock and repeat the complete process from the beginning (to avoid race conditions)
      • +
      +
    • +
    +

    Lock files🔗

    +

    The lock files are in format <lockType>_<clientType>_<clientId>.json with lockType being "exclusive" or "sync", clientType being "desktop", "mobile" or "cli" and clientId is the globally unique ID assigned to a client profile when it is created.

    +

    The have the following content:

    +
    {
    +    "type": "exclusive",
    +    "clientType": <string>,
    +    "clientId": <string>,
    +    "updatedTime": <timestamp in milliseconds>,
    +}
    +
    +

    (Note that the lock file content is for information purpose only. Its content is not used in the lock algorithm since all data can be derived from the filename and file timestamp)

    +

    Although only one client can acquire an exclusive lock, there can be multiple exclusive_*.json lock files in the lock folder (for example if a client crashed before releasing a lock or if two clients try to acquire a lock at the exact same time). In this case, only the oldest lock amongst the active ones is the valid one. If there are two locks with the same timestamp, the one with lowest client ID is the valid one.

    +

    Sync Target Migration🔗

    +

    First the app checks the sync target version - if it's new (no version), it set it up by upgrading to the latest sync version.

    +

    If it's the same as the client supported version, it syncs as normal.

    +

    If it's lower than the client supported version, the client does not allow sync and instead displays a message asking the user to upgrade the sync target (upgradeState = SHOULD_UPGRADE).

    +

    If the user click on the link to upgrade, upgradeState becomes MUST_UPGRADE, and the app restarts.

    +

    On startup, the app check the upgradeState setting. If it is MUST_UPGRADE it displays the upgrade screen and starts upgrarding. Once done it sets upgradeState back to IDLE, and restart the app.

    diff --git a/docs/stats/index.html b/docs/stats/index.html index 4e7276de3..1e41ee894 100644 --- a/docs/stats/index.html +++ b/docs/stats/index.html @@ -86,6 +86,9 @@ https://github.com/laurent22/joplin/blob/master/readme/stats.md #toc ul { margin-bottom: 10px; } + #toc > ul > li { + margin-bottom: 10px; + } #toc { padding-bottom: 1em; } @@ -316,13 +319,20 @@ https://github.com/laurent22/joplin/blob/master/readme/stats.md
  • Markdown Guide
  • How to enable end-to-end encryption
  • What is a conflict?
  • -
  • End-to-end encryption spec
  • How to enable debug mode
  • API documentation
  • FAQ
  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

  • +

    Development

    + +
  • +
  • Google Summer of Code 2020

    • Google Summer of Code 2020
    • diff --git a/joplin.code-workspace b/joplin.code-workspace index 23150c3cb..a432d5a33 100644 --- a/joplin.code-workspace +++ b/joplin.code-workspace @@ -356,7 +356,14 @@ "ReactNativeClient/lib/commands/historyBackward.js": true, "ReactNativeClient/lib/commands/historyForward.js": true, "CliClient/tests/support/amazon-s3-auth.json": true, - "ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js": true, + "CliClient/tests/synchronizer_LockHandler.js": true, + "CliClient/tests/synchronizer_MigrationHandler.js": true, + "ElectronClient/gui/Root_UpgradeSyncTarget.js": true, + "ReactNativeClient/lib/services/synchronizer/LockHandler.js": true, + "ReactNativeClient/lib/services/synchronizer/MigrationHandler.js": true, + "ReactNativeClient/lib/services/synchronizer/migrations/2.js": true, + "ReactNativeClient/lib/services/synchronizer/utils/types.js": true, + "ReactNativeClient/lib/services/synchronizer/gui/useSyncTargetUpgrade.js": true, "ReactNativeClient/lib/hooks/useEffectDebugger.js": true, "ReactNativeClient/lib/services/ResourceEditWatcher/index.js": true, "ReactNativeClient/lib/services/ResourceEditWatcher/reducer.js": true diff --git a/package-lock.json b/package-lock.json index 715979293..69f74a3dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,6 +92,12 @@ "hoist-non-react-statics": "^3.3.0" } }, + "@types/jasmine": { + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.11.tgz", + "integrity": "sha512-fg1rOd/DehQTIJTifGqGVY6q92lDgnLfs7C6t1ccSwQrMyoTGSoH6wWzhJDZb6ezhsdwAX4EIBLe8w5fXWmEng==", + "dev": true + }, "@types/json-schema": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", @@ -119,14 +125,12 @@ "@types/prop-types": { "version": "15.7.3", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", - "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", - "dev": true + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, "@types/react": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.0.tgz", "integrity": "sha512-eOct1hyZI9YZf/eqNlYu7jxA9qyTw1EGXruAJhHhBDBpc00W0C1vwlnh+hkOf7UFZkNK+UxnFBpwAZe3d7XJhQ==", - "dev": true, "requires": { "@types/prop-types": "*", "csstype": "^2.2.0" @@ -141,6 +145,14 @@ "@types/react": "*" } }, + "@types/react-native": { + "version": "0.63.1", + "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.1.tgz", + "integrity": "sha512-mo2DAgliCqdNyivBa0/JL8JIkebt9TU0ATmsvtUvypIP5qN+YJekbVPpHt6WLXEZyBm7LtmIqxbjIHqeoaojsg==", + "requires": { + "@types/react": "*" + } + }, "@types/react-redux": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.7.tgz", @@ -1469,8 +1481,7 @@ "csstype": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.9.tgz", - "integrity": "sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q==", - "dev": true + "integrity": "sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q==" }, "d": { "version": "1.0.1", @@ -4175,6 +4186,22 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, + "jasmine": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.5.0.tgz", + "integrity": "sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==", + "dev": true, + "requires": { + "glob": "^7.1.4", + "jasmine-core": "~3.5.0" + } + }, + "jasmine-core": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", + "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", + "dev": true + }, "joplin-turndown": { "version": "4.0.29", "resolved": "https://registry.npmjs.org/joplin-turndown/-/joplin-turndown-4.0.29.tgz", diff --git a/package.json b/package.json index 3083770ab..d02bf9f83 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "license": "MIT", "devDependencies": { + "@types/jasmine": "^3.5.11", "@types/react": "^16.9.0", "@types/react-dom": "^16.9.0", "@types/react-redux": "^7.1.7", @@ -33,10 +34,12 @@ "glob": "^7.1.6", "gulp": "^4.0.2", "husky": "^3.0.2", + "jasmine": "^3.5.0", "lint-staged": "^9.2.1", "typescript": "^3.7.3" }, "dependencies": { + "@types/react-native": "^0.63.1", "follow-redirects": "^1.11.0", "immer": "^7.0.5", "joplin-turndown": "^4.0.29", diff --git a/readme/e2ee.md b/readme/e2ee.md index 92b9d8fc7..5f760a5c0 100644 --- a/readme/e2ee.md +++ b/readme/e2ee.md @@ -29,4 +29,4 @@ Follow the same procedure as above but instead disable E2EE on each device one b # Technical specification -For a more technical description, mostly relevant for development or to review the method being used, please see the [Encryption specification](https://joplinapp.org/spec/). +For a more technical description, mostly relevant for development or to review the method being used, please see the [Encryption specification](https://joplinapp.org/spec/e2ee/). diff --git a/readme/spec/e2ee.md b/readme/spec/e2ee.md new file mode 100644 index 000000000..e69de29bb diff --git a/readme/spec/sync_lock.md b/readme/spec/sync_lock.md new file mode 100644 index 000000000..5968af07b --- /dev/null +++ b/readme/spec/sync_lock.md @@ -0,0 +1,63 @@ +# Lock types + +There are two types of locks: + +- **SYNC**: Used when synchronising a client with a target. There can be multiple SYNC locks simultaneously. +- **EXCLUSIVE**: Used when a client upgrades a sync target. There can be only one EXCLUSIVE lock. + +# Timeout + +When a client acquires a lock, it must refresh it every X seconds. A lock timeout after Y seconds (where X < Y). A lock with a timestamp greater than Y is considered expired and can be ignored by other clients. A client that tries to refresh a lock that has expired should fail. + +For example, if a client is currently syncing, it must stop doing so if it couldn't refresh the lock with Y seconds. + +For example, if a client is upgrading a target, it must stop doing so if it couldn't refresh the lock within Y seconds. + +# Acquiring a SYNC lock + +- The client check if there is a valid EXCLUSIVE lock on the target +- If there is, it must stop the sync process +- Otherwise it checks if it owns a SYNC lock on the target + - If it does, it starts syncing + - When syncing is done, it releases the SYNC lock + - If it doesn't, it acquires a SYNC lock and repeat the complete process from the beginning (to avoid race conditions) + +# Acquiring an EXCLUSIVE lock + +- The client check if there is a valid EXCLUSIVE or SYNC lock on the target +- If there is, it must stop the upgrade process (or wait till target is unlocked) +- Otherwise it checks if it owns an EXCLUSIVE lock on the target + - If it does, it starts upgrading the target + - When upgrading is done, it releases the EXCLUSIVE lock + - If it doesn't, it acquires an EXCLUSIVE lock and repeat the complete process from the beginning (to avoid race conditions) + +# Lock files + +The lock files are in format `__.json` with lockType being "exclusive" or "sync", clientType being "desktop", "mobile" or "cli" and clientId is the globally unique ID assigned to a client profile when it is created. + +The have the following content: + +```json +{ + "type": "exclusive", + "clientType": , + "clientId": , + "updatedTime": , +} +``` + +(Note that the lock file content is for information purpose only. Its content is not used in the lock algorithm since all data can be derived from the filename and file timestamp) + +Although only one client can acquire an exclusive lock, there can be multiple `exclusive_*.json` lock files in the lock folder (for example if a client crashed before releasing a lock or if two clients try to acquire a lock at the exact same time). In this case, only the oldest lock amongst the active ones is the valid one. If there are two locks with the same timestamp, the one with lowest client ID is the valid one. + +# Sync Target Migration + +First the app checks the sync target version - if it's new (no version), it set it up by upgrading to the latest sync version. + +If it's the same as the client supported version, it syncs as normal. + +If it's lower than the client supported version, the client does not allow sync and instead displays a message asking the user to upgrade the sync target (upgradeState = SHOULD_UPGRADE). + +If the user click on the link to upgrade, upgradeState becomes MUST_UPGRADE, and the app restarts. + +On startup, the app check the upgradeState setting. If it is MUST_UPGRADE it displays the upgrade screen and starts upgrarding. Once done it sets upgradeState back to IDLE, and restart the app. \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 122587e7a..2d36916b0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,10 @@ "sourceMap": true, "jsx": "react", "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "lib/*": ["./ReactNativeClient/lib/*"], + }, }, "include": [ "ReactNativeClient/**/*",