mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
All: Add mechanism to lock and upgrade sync targets (#3524)
This commit is contained in:
parent
88f22fabf7
commit
0c147236a3
@ -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
|
||||
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -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
|
||||
|
@ -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
|
||||
|
2
CliClient/.gitignore
vendored
2
CliClient/.gitignore
vendored
@ -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/
|
@ -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 };
|
||||
|
@ -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 <target>', _('Sync to provided target (defaults to sync.target config value)')]];
|
||||
return [
|
||||
['--target <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();
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,8 @@ tasks.prepareTestBuild = {
|
||||
'lib/',
|
||||
'locales/',
|
||||
'node_modules/',
|
||||
'*.ts',
|
||||
'*.tsx',
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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
|
||||
|
||||
});
|
||||
// });
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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) => {
|
||||
|
@ -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());
|
||||
|
@ -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`;
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -4,6 +4,6 @@
|
||||
"*.js",
|
||||
"!test-utils.js"
|
||||
],
|
||||
"stopSpecOnExpectationFailure": true,
|
||||
"stopSpecOnExpectationFailure": false,
|
||||
"random": true
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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.
|
@ -0,0 +1 @@
|
||||
2
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1 @@
|
||||
{"version":2}
|
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
@ -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.
|
@ -0,0 +1 @@
|
||||
2
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1 @@
|
||||
{"version":2}
|
@ -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,
|
||||
};
|
File diff suppressed because it is too large
Load Diff
201
CliClient/tests/synchronizer_LockHandler.ts
Normal file
201
CliClient/tests/synchronizer_LockHandler.ts
Normal file
@ -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<void | Lock>[] = [];
|
||||
// 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);
|
||||
// }
|
||||
// }));
|
||||
|
||||
});
|
159
CliClient/tests/synchronizer_MigrationHandler.ts
Normal file
159
CliClient/tests/synchronizer_MigrationHandler.ts
Normal file
@ -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);
|
||||
}
|
||||
|
||||
});
|
@ -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 };
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 = (
|
||||
<span>
|
||||
{_('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.')}{' '}
|
||||
<a href="#" onClick={() => onRestartAndUpgrade()}>
|
||||
{_('Restart and upgrade')}
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
} else if (this.props.hasDisabledSyncItems) {
|
||||
msg = (
|
||||
<span>
|
||||
{_('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'],
|
||||
|
102
ElectronClient/gui/Root_UpgradeSyncTarget.tsx
Normal file
102
ElectronClient/gui/Root_UpgradeSyncTarget.tsx
Normal file
@ -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 (
|
||||
<div className="errorBox">
|
||||
<h2>Error</h2>
|
||||
<p>The sync target could not be upgraded due to an error. For support, please copy the <em>complete</em> content of this page and paste it in the forum: https://discourse.joplinapp.org/</p>
|
||||
<p>The full error was:</p>
|
||||
<p>{upgradeResult.error.message}</p>
|
||||
<pre>{upgradeResult.error.stack}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Joplin upgrade in progress...</h2>
|
||||
<p>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.</p>
|
||||
{renderUpgradeError()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<Root_UpgradeSyncTarget />, document.getElementById('react-root'));
|
@ -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();
|
||||
|
||||
|
6
ElectronClient/package-lock.json
generated
6
ElectronClient/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 (
|
||||
<View style={{ backgroundColor: theme.backgroundColor, flex: 1, flexDirection: 'column' }}>
|
||||
<Text style={headerStyle}>Error</Text>
|
||||
<Text style={lineStyle}>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/</Text>
|
||||
<Text style={lineStyle}>The full error was:</Text>
|
||||
<Text style={lineStyle}>{upgradeResult.error.message}</Text>
|
||||
<Text style={stackTraceStyle}>{upgradeResult.error.stack}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function renderInProgress() {
|
||||
if (upgradeResult.error || upgradeResult.done) return null;
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text style={headerStyle}>Joplin upgrade in progress...</Text>
|
||||
<Text style={lineStyle}>Please wait while the sync target is being upgraded. It may take a few seconds or a few minutes depending on the upgrade.</Text>
|
||||
<Text style={lineStyle}>Make sure you leave your device on and the app opened while the upgrade is in progress.</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDone() {
|
||||
if (upgradeResult.error || !upgradeResult.done) return null;
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text style={headerStyle}>Upgrade complete</Text>
|
||||
<Text style={lineStyle}>The upgrade has been applied successfully. Please press Back to exit this screen.</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView style={{ flex: 1, flexDirection: 'column', backgroundColor: theme.backgroundColor }}>
|
||||
<ScreenHeader title={_('Sync Target Upgrade')} parentComponent={this} showShouldUpgradeSyncTargetMessage={false} showSearchButton={false} showBackButton={upgradeResult.done}/>
|
||||
<View style={{ padding: 15, flex: 1 }}>
|
||||
{renderInProgress()}
|
||||
{renderDone()}
|
||||
{renderUpgradeError()}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect((state:any) => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
};
|
||||
})(UpgradeSyncTargetScreen);
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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 ? '?' : '&';
|
||||
|
@ -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 };
|
||||
|
@ -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;
|
||||
|
@ -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 });
|
||||
|
||||
|
348
ReactNativeClient/lib/services/synchronizer/LockHandler.ts
Normal file
348
ReactNativeClient/lib/services/synchronizer/LockHandler.ts
Normal file
@ -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<Lock[]> {
|
||||
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<Lock> {
|
||||
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<Lock> {
|
||||
// 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<Lock> {
|
||||
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,
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
141
ReactNativeClient/lib/services/synchronizer/MigrationHandler.ts
Normal file
141
ReactNativeClient/lib/services/synchronizer/MigrationHandler.ts
Normal file
@ -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<SyncTargetInfo> {
|
||||
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<SyncTargetInfo> {
|
||||
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_);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user