mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-02 12:47:41 +02:00
Revert "All: Add support for sync target lock"
This reverts commit 51235f191d
.
Not ready yet, moving to a branch
This commit is contained in:
parent
a7eae2e033
commit
e0a87d6253
@ -61,7 +61,6 @@ Modules/TinyMCE/IconPack/postinstall.js
|
|||||||
Modules/TinyMCE/langs/
|
Modules/TinyMCE/langs/
|
||||||
|
|
||||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||||
CliClient/tests/synchronizer_LockHandler.js
|
|
||||||
ElectronClient/commands/focusElement.js
|
ElectronClient/commands/focusElement.js
|
||||||
ElectronClient/commands/startExternalEditing.js
|
ElectronClient/commands/startExternalEditing.js
|
||||||
ElectronClient/commands/stopExternalEditing.js
|
ElectronClient/commands/stopExternalEditing.js
|
||||||
@ -155,7 +154,6 @@ ReactNativeClient/lib/services/ResourceEditWatcher.js
|
|||||||
ReactNativeClient/lib/services/rest/actionApi.desktop.js
|
ReactNativeClient/lib/services/rest/actionApi.desktop.js
|
||||||
ReactNativeClient/lib/services/rest/errors.js
|
ReactNativeClient/lib/services/rest/errors.js
|
||||||
ReactNativeClient/lib/services/SettingUtils.js
|
ReactNativeClient/lib/services/SettingUtils.js
|
||||||
ReactNativeClient/lib/services/synchronizer/LockHandler.js
|
|
||||||
ReactNativeClient/lib/services/UndoRedoService.js
|
ReactNativeClient/lib/services/UndoRedoService.js
|
||||||
ReactNativeClient/lib/ShareExtension.js
|
ReactNativeClient/lib/ShareExtension.js
|
||||||
ReactNativeClient/lib/shareHandler.js
|
ReactNativeClient/lib/shareHandler.js
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -51,7 +51,6 @@ Tools/commit_hook.txt
|
|||||||
*.map
|
*.map
|
||||||
|
|
||||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||||
CliClient/tests/synchronizer_LockHandler.js
|
|
||||||
ElectronClient/commands/focusElement.js
|
ElectronClient/commands/focusElement.js
|
||||||
ElectronClient/commands/startExternalEditing.js
|
ElectronClient/commands/startExternalEditing.js
|
||||||
ElectronClient/commands/stopExternalEditing.js
|
ElectronClient/commands/stopExternalEditing.js
|
||||||
@ -145,7 +144,6 @@ ReactNativeClient/lib/services/ResourceEditWatcher.js
|
|||||||
ReactNativeClient/lib/services/rest/actionApi.desktop.js
|
ReactNativeClient/lib/services/rest/actionApi.desktop.js
|
||||||
ReactNativeClient/lib/services/rest/errors.js
|
ReactNativeClient/lib/services/rest/errors.js
|
||||||
ReactNativeClient/lib/services/SettingUtils.js
|
ReactNativeClient/lib/services/SettingUtils.js
|
||||||
ReactNativeClient/lib/services/synchronizer/LockHandler.js
|
|
||||||
ReactNativeClient/lib/services/UndoRedoService.js
|
ReactNativeClient/lib/services/UndoRedoService.js
|
||||||
ReactNativeClient/lib/ShareExtension.js
|
ReactNativeClient/lib/ShareExtension.js
|
||||||
ReactNativeClient/lib/shareHandler.js
|
ReactNativeClient/lib/shareHandler.js
|
||||||
|
@ -37,8 +37,6 @@ tasks.buildTests = {
|
|||||||
'lib/',
|
'lib/',
|
||||||
'locales/',
|
'locales/',
|
||||||
'node_modules/',
|
'node_modules/',
|
||||||
'*.ts',
|
|
||||||
'*.tsx',
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ async function allNotesFolders() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function remoteItemsByTypes(types) {
|
async function remoteItemsByTypes(types) {
|
||||||
const list = await fileApi().list('', { includeDirs: false });
|
const list = await fileApi().list();
|
||||||
if (list.has_more) throw new Error('Not implemented!!!');
|
if (list.has_more) throw new Error('Not implemented!!!');
|
||||||
const files = list.items;
|
const files = list.items;
|
||||||
|
|
||||||
@ -822,7 +822,7 @@ describe('synchronizer', function() {
|
|||||||
// First create a folder, without encryption enabled, and sync it
|
// First create a folder, without encryption enabled, and sync it
|
||||||
const folder1 = await Folder.save({ title: 'folder1' });
|
const folder1 = await Folder.save({ title: 'folder1' });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
let files = await fileApi().list('', { includeDirs: false });
|
let files = await fileApi().list();
|
||||||
let content = await fileApi().get(files.items[0].path);
|
let content = await fileApi().get(files.items[0].path);
|
||||||
expect(content.indexOf('folder1') >= 0).toBe(true);
|
expect(content.indexOf('folder1') >= 0).toBe(true);
|
||||||
|
|
||||||
@ -1662,22 +1662,22 @@ describe('synchronizer', function() {
|
|||||||
expect(hasThrown).toBe(true);
|
expect(hasThrown).toBe(true);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// it('should not sync when target is locked', asyncTest(async () => {
|
it('should not sync when target is locked', asyncTest(async () => {
|
||||||
// await synchronizer().start();
|
await synchronizer().start();
|
||||||
// await synchronizer().acquireLock_();
|
await synchronizer().acquireLock_();
|
||||||
|
|
||||||
// await switchClient(2);
|
await switchClient(2);
|
||||||
// const hasThrown = await checkThrowAsync(async () => synchronizer().start({ throwOnError: true }));
|
const hasThrown = await checkThrowAsync(async () => synchronizer().start({ throwOnError: true }));
|
||||||
// expect(hasThrown).toBe(true);
|
expect(hasThrown).toBe(true);
|
||||||
// }));
|
}));
|
||||||
|
|
||||||
// it('should clear a lock if it was created by the same app as the current one', asyncTest(async () => {
|
it('should clear a lock if it was created by the same app as the current one', asyncTest(async () => {
|
||||||
// await synchronizer().start();
|
await synchronizer().start();
|
||||||
// await synchronizer().acquireLock_();
|
await synchronizer().acquireLock_();
|
||||||
// expect((await synchronizer().lockFiles_()).length).toBe(1);
|
expect((await synchronizer().lockFiles_()).length).toBe(1);
|
||||||
// await synchronizer().start({ throwOnError: true });
|
await synchronizer().start({ throwOnError: true });
|
||||||
// expect((await synchronizer().lockFiles_()).length).toBe(0);
|
expect((await synchronizer().lockFiles_()).length).toBe(0);
|
||||||
// }));
|
}));
|
||||||
|
|
||||||
it('should not encrypt notes that are shared', asyncTest(async () => {
|
it('should not encrypt notes that are shared', asyncTest(async () => {
|
||||||
Setting.setValue('encryption.enabled', true);
|
Setting.setValue('encryption.enabled', true);
|
||||||
|
@ -1,104 +0,0 @@
|
|||||||
import LockHandler, { LockType } from 'lib/services/synchronizer/LockHandler';
|
|
||||||
|
|
||||||
require('app-module-path').addPath(__dirname);
|
|
||||||
|
|
||||||
const { asyncTest, fileApi, setupDatabaseAndSynchronizer, switchClient, msleep, expectThrow, expectNotThrow } = require('test-utils.js');
|
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason:any, p:any) => {
|
|
||||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
|
||||||
});
|
|
||||||
|
|
||||||
let lockHandler_:LockHandler = null;
|
|
||||||
const locksDirname = 'locks';
|
|
||||||
|
|
||||||
function lockHandler():LockHandler {
|
|
||||||
if (lockHandler_) return lockHandler_;
|
|
||||||
lockHandler_ = new LockHandler(fileApi(), locksDirname);
|
|
||||||
return lockHandler_;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('synchronizer_LockHandler', function() {
|
|
||||||
|
|
||||||
beforeEach(async (done:Function) => {
|
|
||||||
lockHandler_ = null;
|
|
||||||
await setupDatabaseAndSynchronizer(1);
|
|
||||||
await setupDatabaseAndSynchronizer(2);
|
|
||||||
await switchClient(1);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should acquire and release a sync lock', asyncTest(async () => {
|
|
||||||
await lockHandler().acquireLock(LockType.Sync, 'mobile', '123456');
|
|
||||||
const locks = await lockHandler().syncLocks();
|
|
||||||
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().syncLocks()).length).toBe(0);
|
|
||||||
}));
|
|
||||||
|
|
||||||
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().syncLocks()).length).toBe(2);
|
|
||||||
|
|
||||||
{
|
|
||||||
await lockHandler().releaseLock(LockType.Sync, 'mobile', '222');
|
|
||||||
const locks = await lockHandler().syncLocks();
|
|
||||||
expect(locks.length).toBe(1);
|
|
||||||
expect(locks[0].clientId).toBe('111');
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should refresh sync lock timestamp when acquiring again', asyncTest(async () => {
|
|
||||||
await lockHandler().acquireLock(LockType.Sync, 'mobile', '111');
|
|
||||||
|
|
||||||
const beforeTime = (await lockHandler().syncLocks())[0].updatedTime;
|
|
||||||
await msleep(1);
|
|
||||||
|
|
||||||
await lockHandler().acquireLock(LockType.Sync, 'mobile', '111');
|
|
||||||
|
|
||||||
const afterTime = (await lockHandler().syncLocks())[0].updatedTime;
|
|
||||||
|
|
||||||
expect(beforeTime).toBeLessThan(afterTime);
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should not allow sync locks if there is an exclusive lock', asyncTest(async () => {
|
|
||||||
await lockHandler().acquireLock(LockType.Exclusive, 'desktop', '111');
|
|
||||||
|
|
||||||
expectThrow(async () => {
|
|
||||||
await lockHandler().acquireLock(LockType.Sync, 'mobile', '222');
|
|
||||||
}, 'hasExclusiveLock');
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should not allow exclusive lock if there are sync locks', asyncTest(async () => {
|
|
||||||
lockHandler().syncLockMaxAge = 1000 * 60 * 60;
|
|
||||||
|
|
||||||
await lockHandler().acquireLock(LockType.Sync, 'mobile', '111');
|
|
||||||
await lockHandler().acquireLock(LockType.Sync, 'mobile', '222');
|
|
||||||
|
|
||||||
expectThrow(async () => {
|
|
||||||
await lockHandler().acquireLock(LockType.Exclusive, 'desktop', '333');
|
|
||||||
}, 'hasSyncLock');
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should allow exclusive lock if the sync locks have expired', asyncTest(async () => {
|
|
||||||
lockHandler().syncLockMaxAge = 1;
|
|
||||||
|
|
||||||
await lockHandler().acquireLock(LockType.Sync, 'mobile', '111');
|
|
||||||
await lockHandler().acquireLock(LockType.Sync, 'mobile', '222');
|
|
||||||
|
|
||||||
await msleep(2);
|
|
||||||
|
|
||||||
expectNotThrow(async () => {
|
|
||||||
await lockHandler().acquireLock(LockType.Exclusive, 'desktop', '333');
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
});
|
|
@ -132,14 +132,6 @@ function sleep(n) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function msleep(ms) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve();
|
|
||||||
}, ms);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function currentClientId() {
|
function currentClientId() {
|
||||||
return currentClient_;
|
return currentClient_;
|
||||||
}
|
}
|
||||||
@ -386,40 +378,6 @@ async function checkThrowAsync(asyncFn) {
|
|||||||
return hasThrown;
|
return hasThrown;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function expectThrow(asyncFn, errorCode = undefined) {
|
|
||||||
let hasThrown = false;
|
|
||||||
let thrownErrorCode = null;
|
|
||||||
try {
|
|
||||||
await asyncFn();
|
|
||||||
} catch (error) {
|
|
||||||
hasThrown = true;
|
|
||||||
thrownErrorCode = error.code;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasThrown) {
|
|
||||||
expect('not throw').toBe('throw', 'Expected function to throw an error but did not');
|
|
||||||
} else if (thrownErrorCode !== errorCode) {
|
|
||||||
expect(`error code: ${thrownErrorCode}`).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) {
|
function checkThrow(fn) {
|
||||||
let hasThrown = false;
|
let hasThrown = false;
|
||||||
try {
|
try {
|
||||||
@ -457,7 +415,7 @@ function asyncTest(callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function allSyncTargetItemsEncrypted() {
|
async function allSyncTargetItemsEncrypted() {
|
||||||
const list = await fileApi().list('', { includeDirs: false });
|
const list = await fileApi().list();
|
||||||
const files = list.items;
|
const files = list.items;
|
||||||
|
|
||||||
let totalCount = 0;
|
let totalCount = 0;
|
||||||
@ -615,4 +573,4 @@ class TestApp extends BaseApplication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { kvStore, expectThrow, 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 };
|
module.exports = { 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 };
|
||||||
|
6
ElectronClient/package-lock.json
generated
6
ElectronClient/package-lock.json
generated
@ -235,12 +235,6 @@
|
|||||||
"integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==",
|
"integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==",
|
||||||
"dev": true
|
"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": {
|
"@types/node": {
|
||||||
"version": "12.12.38",
|
"version": "12.12.38",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.38.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.38.tgz",
|
||||||
|
@ -79,7 +79,6 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/laurent22/joplin#readme",
|
"homepage": "https://github.com/laurent22/joplin#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jasmine": "^3.5.11",
|
|
||||||
"ajv": "^6.5.0",
|
"ajv": "^6.5.0",
|
||||||
"app-builder-bin": "^1.9.11",
|
"app-builder-bin": "^1.9.11",
|
||||||
"babel-cli": "^6.26.0",
|
"babel-cli": "^6.26.0",
|
||||||
|
@ -111,7 +111,7 @@ class FileApiDriverMemory {
|
|||||||
this.items_.push(item);
|
this.items_.push(item);
|
||||||
} else {
|
} else {
|
||||||
this.items_[index].content = this.encodeContent_(content);
|
this.items_[index].content = this.encodeContent_(content);
|
||||||
this.items_[index].updated_time = time.unixMs();
|
this.items_[index].updated_time = time.unix();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ class FileApiDriverWebDav {
|
|||||||
|
|
||||||
async delta(path, options) {
|
async delta(path, options) {
|
||||||
const getDirStats = async path => {
|
const getDirStats = async path => {
|
||||||
const result = await this.list(path, { includeDirs: false });
|
const result = await this.list(path);
|
||||||
return result.items;
|
return result.items;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -128,7 +128,6 @@ class FileApi {
|
|||||||
if (!options) options = {};
|
if (!options) options = {};
|
||||||
if (!('includeHidden' in options)) options.includeHidden = false;
|
if (!('includeHidden' in options)) options.includeHidden = false;
|
||||||
if (!('context' in options)) options.context = null;
|
if (!('context' in options)) options.context = null;
|
||||||
if (!('includeDirs' in options)) options.includeDirs = true;
|
|
||||||
|
|
||||||
this.logger().debug(`list ${this.baseDir()}`);
|
this.logger().debug(`list ${this.baseDir()}`);
|
||||||
|
|
||||||
@ -142,10 +141,6 @@ class FileApi {
|
|||||||
result.items = temp;
|
result.items = temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.includeHidden) {
|
|
||||||
result.items = result.items.filter(f => !f.isDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,220 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
const exclusiveFilename = 'exclusive.json';
|
|
||||||
|
|
||||||
export default class LockHandler {
|
|
||||||
|
|
||||||
private api_:any = null;
|
|
||||||
private lockDirPath_:any = null;
|
|
||||||
private syncLockMaxAge_:number = 1000 * 60 * 3;
|
|
||||||
|
|
||||||
constructor(api:any, lockDirPath:string) {
|
|
||||||
this.api_ = api;
|
|
||||||
this.lockDirPath_ = lockDirPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get syncLockMaxAge():number {
|
|
||||||
return this.syncLockMaxAge_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should only be done for testing purposes since all clients should
|
|
||||||
// use the same lock max age.
|
|
||||||
public set syncLockMaxAge(v:number) {
|
|
||||||
this.syncLockMaxAge_ = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
private lockFilename(lock:Lock) {
|
|
||||||
if (lock.type === LockType.Exclusive) {
|
|
||||||
return exclusiveFilename;
|
|
||||||
} else {
|
|
||||||
return `${[lock.type, lock.clientType, lock.clientId].join('_')}.json`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private lockTypeFromFilename(name:string):LockType {
|
|
||||||
if (name === exclusiveFilename) return LockType.Exclusive;
|
|
||||||
return LockType.Sync;
|
|
||||||
}
|
|
||||||
|
|
||||||
private lockFilePath(lock:Lock) {
|
|
||||||
return `${this.lockDirPath_}/${this.lockFilename(lock)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private exclusiveFilePath():string {
|
|
||||||
return `${this.lockDirPath_}/${exclusiveFilename}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private syncLockFileToObject(file:any):Lock {
|
|
||||||
const p = filename(file.path).split('_');
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: p[0],
|
|
||||||
clientType: p[1],
|
|
||||||
clientId: p[2],
|
|
||||||
updatedTime: file.updated_time,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncLocks():Promise<Lock[]> {
|
|
||||||
const result = await this.api_.list(this.lockDirPath_);
|
|
||||||
if (result.hasMore) throw new Error('hasMore not handled'); // Shouldn't happen anyway
|
|
||||||
|
|
||||||
const output = [];
|
|
||||||
for (const file of result.items) {
|
|
||||||
const ext = fileExtension(file.path);
|
|
||||||
if (ext !== 'json') continue;
|
|
||||||
|
|
||||||
const type = this.lockTypeFromFilename(file.path);
|
|
||||||
if (type !== LockType.Sync) continue;
|
|
||||||
|
|
||||||
const lock = this.syncLockFileToObject(file);
|
|
||||||
output.push(lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async exclusiveLock():Promise<Lock> {
|
|
||||||
const stat = await this.api_.stat(this.exclusiveFilePath());
|
|
||||||
if (!stat) return null;
|
|
||||||
|
|
||||||
const contentText = await this.api_.get(this.exclusiveFilePath());
|
|
||||||
if (!contentText) return null; // race condition
|
|
||||||
|
|
||||||
const lock:Lock = JSON.parse(contentText) as Lock;
|
|
||||||
lock.updatedTime = stat.updated_time;
|
|
||||||
return lock;
|
|
||||||
}
|
|
||||||
|
|
||||||
private lockIsActive(lock:Lock):boolean {
|
|
||||||
return Date.now() - lock.updatedTime < this.syncLockMaxAge;
|
|
||||||
}
|
|
||||||
|
|
||||||
async hasActiveExclusiveLock():Promise<boolean> {
|
|
||||||
const lock = await this.exclusiveLock();
|
|
||||||
return !!lock && this.lockIsActive(lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
async hasActiveSyncLock(clientType:string, clientId:string) {
|
|
||||||
const locks = await this.syncLocks();
|
|
||||||
for (const lock of locks) {
|
|
||||||
if (lock.clientType === clientType && lock.clientId === clientId && this.lockIsActive(lock)) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async saveLock(lock:Lock) {
|
|
||||||
await this.api_.put(this.lockFilePath(lock), JSON.stringify(lock));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async acquireSyncLock(clientType:string, clientId:string) {
|
|
||||||
const exclusiveLock = await this.exclusiveLock();
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.saveLock({
|
|
||||||
type: LockType.Sync,
|
|
||||||
clientType: clientType,
|
|
||||||
clientId: clientId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private lockToClientString(lock:Lock):string {
|
|
||||||
return `(${lock.clientType} #${lock.clientId})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async acquireExclusiveLock(clientType:string, clientId:string, timeoutMs:number = 0) {
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const syncLocks = await this.syncLocks();
|
|
||||||
const activeSyncLocks = syncLocks.filter(lock => this.lockIsActive(lock));
|
|
||||||
|
|
||||||
if (activeSyncLocks.length) {
|
|
||||||
if (await waitForTimeout()) continue;
|
|
||||||
const lockString = activeSyncLocks.map(l => this.lockToClientString(l)).join(', ');
|
|
||||||
throw new JoplinError(`Cannot acquire exclusive lock because the following clients have a sync lock on the target: ${lockString}`, 'hasSyncLock');
|
|
||||||
}
|
|
||||||
|
|
||||||
const exclusiveLock = await this.exclusiveLock();
|
|
||||||
|
|
||||||
if (exclusiveLock) {
|
|
||||||
if (exclusiveLock.clientId === clientId) {
|
|
||||||
// Save it again to refresh the timestamp
|
|
||||||
await this.saveLock(exclusiveLock);
|
|
||||||
return;
|
|
||||||
} 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(exclusiveLock)}`, '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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async acquireLock(lockType:LockType, clientType:string, clientId:string, timeoutMs:number = 0) {
|
|
||||||
if (lockType === LockType.Sync) {
|
|
||||||
await this.acquireSyncLock(clientType, clientId);
|
|
||||||
} else if (lockType === LockType.Exclusive) {
|
|
||||||
await 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,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -12,11 +12,10 @@ const { time } = require('lib/time-utils.js');
|
|||||||
const { Logger } = require('lib/logger.js');
|
const { Logger } = require('lib/logger.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { shim } = require('lib/shim.js');
|
const { shim } = require('lib/shim.js');
|
||||||
// const { filename, fileExtension } = require('lib/path-utils');
|
const { filename, fileExtension } = require('lib/path-utils');
|
||||||
const JoplinError = require('lib/JoplinError');
|
const JoplinError = require('lib/JoplinError');
|
||||||
const BaseSyncTarget = require('lib/BaseSyncTarget');
|
const BaseSyncTarget = require('lib/BaseSyncTarget');
|
||||||
const TaskQueue = require('lib/TaskQueue');
|
const TaskQueue = require('lib/TaskQueue');
|
||||||
const LockHandler = require('lib/services/synchronizer/LockHandler').default;
|
|
||||||
|
|
||||||
class Synchronizer {
|
class Synchronizer {
|
||||||
constructor(db, api, appType) {
|
constructor(db, api, appType) {
|
||||||
@ -24,7 +23,7 @@ class Synchronizer {
|
|||||||
this.db_ = db;
|
this.db_ = db;
|
||||||
this.api_ = api;
|
this.api_ = api;
|
||||||
this.syncDirName_ = '.sync';
|
this.syncDirName_ = '.sync';
|
||||||
this.lockDirName_ = 'locks';
|
this.lockDirName_ = '.lock';
|
||||||
this.resourceDirName_ = BaseSyncTarget.resourceDirName();
|
this.resourceDirName_ = BaseSyncTarget.resourceDirName();
|
||||||
this.logger_ = new Logger();
|
this.logger_ = new Logger();
|
||||||
this.appType_ = appType;
|
this.appType_ = appType;
|
||||||
@ -67,12 +66,6 @@ class Synchronizer {
|
|||||||
return this.logger_;
|
return this.logger_;
|
||||||
}
|
}
|
||||||
|
|
||||||
lockHandler() {
|
|
||||||
if (this.lockHandler_) return this.lockHandler_;
|
|
||||||
this.lockHandler_ = new LockHandler(this.api(), this.lockDirName_);
|
|
||||||
return this.lockHandler_;
|
|
||||||
}
|
|
||||||
|
|
||||||
maxResourceSize() {
|
maxResourceSize() {
|
||||||
if (this.maxResourceSize_ !== null) return this.maxResourceSize_;
|
if (this.maxResourceSize_ !== null) return this.maxResourceSize_;
|
||||||
return this.appType_ === 'mobile' ? 10 * 1000 * 1000 : Infinity;
|
return this.appType_ === 'mobile' ? 10 * 1000 * 1000 : Infinity;
|
||||||
@ -212,17 +205,64 @@ class Synchronizer {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async acquireLock_() {
|
||||||
|
await this.checkLock_();
|
||||||
|
await this.api().put(`${this.lockDirName_}/${this.clientId()}_${Date.now()}.lock`, `${Date.now()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseLock_() {
|
||||||
|
const lockFiles = await this.lockFiles_();
|
||||||
|
for (const lockFile of lockFiles) {
|
||||||
|
const p = this.parseLockFilePath(lockFile.path);
|
||||||
|
if (p.clientId === this.clientId()) {
|
||||||
|
await this.api().delete(p.fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async lockFiles_() {
|
||||||
|
const output = await this.api().list(this.lockDirName_);
|
||||||
|
return output.items.filter((p) => {
|
||||||
|
const ext = fileExtension(p.path);
|
||||||
|
return ext === 'lock';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
parseLockFilePath(path) {
|
||||||
|
const splitted = filename(path).split('_');
|
||||||
|
const fullPath = `${this.lockDirName_}/${path}`;
|
||||||
|
if (splitted.length !== 2) throw new Error(`Sync target appears to be locked but lock filename is invalid: ${fullPath}. Please delete it on the sync target to continue.`);
|
||||||
|
return {
|
||||||
|
clientId: splitted[0],
|
||||||
|
timestamp: Number(splitted[1]),
|
||||||
|
fullPath: fullPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkLock_() {
|
||||||
|
const lockFiles = await this.lockFiles_();
|
||||||
|
if (lockFiles.length) {
|
||||||
|
const lock = this.parseLockFilePath(lockFiles[0].path);
|
||||||
|
|
||||||
|
if (lock.clientId === this.clientId()) {
|
||||||
|
await this.releaseLock_();
|
||||||
|
} else {
|
||||||
|
throw new Error(`The sync target was locked by client ${lock.clientId} on ${time.unixMsToLocalDateTime(lock.timestamp)} and cannot be accessed. If no app is currently operating on the sync target, you can delete the files in the "${this.lockDirName_}" directory on the sync target to resume.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async checkSyncTargetVersion_() {
|
async checkSyncTargetVersion_() {
|
||||||
const supportedSyncTargetVersion = Setting.value('syncVersion');
|
const supportedSyncTargetVersion = Setting.value('syncVersion');
|
||||||
const syncTargetVersion = await this.apiCall('get', '.sync/version.txt');
|
const syncTargetVersion = await this.api().get('.sync/version.txt');
|
||||||
|
|
||||||
if (!syncTargetVersion) {
|
if (!syncTargetVersion) {
|
||||||
await this.apiCall('put', '.sync/version.txt', `${supportedSyncTargetVersion}`);
|
await this.api().put('.sync/version.txt', `${supportedSyncTargetVersion}`);
|
||||||
} else {
|
} else {
|
||||||
if (Number(syncTargetVersion) > supportedSyncTargetVersion) {
|
if (Number(syncTargetVersion) > supportedSyncTargetVersion) {
|
||||||
throw new Error(sprintf('Sync version of the target (%d) does not match sync version supported by client (%d). Please upgrade your client.', Number(syncTargetVersion), supportedSyncTargetVersion));
|
throw new Error(sprintf('Sync version of the target (%d) does not match sync version supported by client (%d). Please upgrade your client.', Number(syncTargetVersion), supportedSyncTargetVersion));
|
||||||
} else {
|
} else {
|
||||||
await this.apiCall('put', '.sync/version.txt', `${supportedSyncTargetVersion}`);
|
await this.api().put('.sync/version.txt', `${supportedSyncTargetVersion}`);
|
||||||
// TODO: do upgrade job
|
// TODO: do upgrade job
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -232,44 +272,6 @@ class Synchronizer {
|
|||||||
return steps.includes('update_remote') && steps.includes('delete_remote') && steps.includes('delta');
|
return steps.includes('update_remote') && steps.includes('delete_remote') && steps.includes('delta');
|
||||||
}
|
}
|
||||||
|
|
||||||
async lockErrorStatus_() {
|
|
||||||
const hasActiveExclusiveLock = await this.lockHandler().hasActiveExclusiveLock();
|
|
||||||
if (hasActiveExclusiveLock) return 'hasExclusiveLock';
|
|
||||||
|
|
||||||
const hasActiveSyncLock = await this.lockHandler().hasActiveSyncLock();
|
|
||||||
if (!hasActiveSyncLock) return 'syncLockGone';
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async apiCall(fnName, ...args) {
|
|
||||||
if (this.syncTargetIsLocked_) throw new JoplinError('Sync target is locked - aborting API call', 'lockError');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const output = await this.api()[fnName](...args);
|
|
||||||
return output;
|
|
||||||
} catch (error) {
|
|
||||||
const lockStatus = await this.lockErrorStatus_();
|
|
||||||
// When there's an error due to a lock, we re-wrap the error and change the error code so that error handling
|
|
||||||
// does not do special processing on the original error. For example, if a resource could not be downloaded,
|
|
||||||
// don't mark it as a "cannotSyncItem" since we don't know that.
|
|
||||||
if (lockStatus) {
|
|
||||||
throw new JoplinError(`Sync target lock error: ${lockStatus}. Original error was: ${error.message}`, 'lockError');
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshLock() {
|
|
||||||
if (this.state_ !== 'in_progress') {
|
|
||||||
this.logger().warn('Trying to refresh lock, but sync is not in progress');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.lockHandler().acquireLock('sync', this.appType_, this.clientId_);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Synchronisation is done in three major steps:
|
// Synchronisation is done in three major steps:
|
||||||
//
|
//
|
||||||
// 1. UPLOAD: Send to the sync target the items that have changed since the last sync.
|
// 1. UPLOAD: Send to the sync target the items that have changed since the last sync.
|
||||||
@ -298,7 +300,6 @@ class Synchronizer {
|
|||||||
|
|
||||||
const syncTargetId = this.api().syncTargetId();
|
const syncTargetId = this.api().syncTargetId();
|
||||||
|
|
||||||
this.syncTargetIsLocked_ = false;
|
|
||||||
this.cancelling_ = false;
|
this.cancelling_ = false;
|
||||||
|
|
||||||
const masterKeysBefore = await MasterKey.count();
|
const masterKeysBefore = await MasterKey.count();
|
||||||
@ -324,27 +325,12 @@ class Synchronizer {
|
|||||||
let errorToThrow = null;
|
let errorToThrow = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.apiCall('mkdir', this.syncDirName_);
|
await this.api().mkdir(this.syncDirName_);
|
||||||
await this.apiCall('mkdir', this.lockDirName_);
|
await this.api().mkdir(this.lockDirName_);
|
||||||
this.api().setTempDirName(this.syncDirName_);
|
this.api().setTempDirName(this.syncDirName_);
|
||||||
await this.apiCall('mkdir', this.resourceDirName_);
|
await this.api().mkdir(this.resourceDirName_);
|
||||||
|
|
||||||
await this.lockHandler().acquireLock('sync', this.appType_, this.clientId_);
|
|
||||||
|
|
||||||
if (this.refreshLockIID_) clearInterval(this.refreshLockIID_);
|
|
||||||
|
|
||||||
this.refreshLockIID_ = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
await this.refreshLock();
|
|
||||||
} catch (error) {
|
|
||||||
this.logger().warn('Could not refresh lock - cancelling sync. Error was:', error);
|
|
||||||
clearInterval(this.refreshLockIID_);
|
|
||||||
this.syncTargetIsLocked_ = true;
|
|
||||||
this.refreshLockIID_ = null;
|
|
||||||
this.cancel();
|
|
||||||
}
|
|
||||||
}, 1000 * 60);
|
|
||||||
|
|
||||||
|
await this.checkLock_();
|
||||||
await this.checkSyncTargetVersion_();
|
await this.checkSyncTargetVersion_();
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
@ -383,7 +369,7 @@ class Synchronizer {
|
|||||||
// (by setting an updated_time less than current time).
|
// (by setting an updated_time less than current time).
|
||||||
if (donePaths.indexOf(path) >= 0) throw new JoplinError(sprintf('Processing a path that has already been done: %s. sync_time was not updated? Remote item has an updated_time in the future?', path), 'processingPathTwice');
|
if (donePaths.indexOf(path) >= 0) throw new JoplinError(sprintf('Processing a path that has already been done: %s. sync_time was not updated? Remote item has an updated_time in the future?', path), 'processingPathTwice');
|
||||||
|
|
||||||
const remote = await this.apiCall('stat', path);
|
const remote = await this.api().stat(path);
|
||||||
let action = null;
|
let action = null;
|
||||||
|
|
||||||
let reason = '';
|
let reason = '';
|
||||||
@ -421,7 +407,7 @@ class Synchronizer {
|
|||||||
// OneDrive does not appear to have accurate timestamps as lastModifiedDateTime would occasionally be
|
// OneDrive does not appear to have accurate timestamps as lastModifiedDateTime would occasionally be
|
||||||
// a few seconds ahead of what it was set with setTimestamp()
|
// a few seconds ahead of what it was set with setTimestamp()
|
||||||
try {
|
try {
|
||||||
remoteContent = await this.apiCall('get', path);
|
remoteContent = await this.api().get(path);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'rejectedByTarget') {
|
if (error.code === 'rejectedByTarget') {
|
||||||
this.progressReport_.errors.push(error);
|
this.progressReport_.errors.push(error);
|
||||||
@ -464,7 +450,7 @@ class Synchronizer {
|
|||||||
this.logger().warn(`Uploading a large resource (resourceId: ${local.id}, size:${local.size} bytes) which may tie up the sync process.`);
|
this.logger().warn(`Uploading a large resource (resourceId: ${local.id}, size:${local.size} bytes) which may tie up the sync process.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.apiCall('put', remoteContentPath, null, { path: localResourceContentPath, source: 'file' });
|
await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error && ['rejectedByTarget', 'fileNotFound'].indexOf(error.code) >= 0) {
|
if (error && ['rejectedByTarget', 'fileNotFound'].indexOf(error.code) >= 0) {
|
||||||
await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message);
|
await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message);
|
||||||
@ -481,7 +467,7 @@ class Synchronizer {
|
|||||||
try {
|
try {
|
||||||
if (this.testingHooks_.indexOf('notesRejectedByTarget') >= 0 && local.type_ === BaseModel.TYPE_NOTE) throw new JoplinError('Testing rejectedByTarget', 'rejectedByTarget');
|
if (this.testingHooks_.indexOf('notesRejectedByTarget') >= 0 && local.type_ === BaseModel.TYPE_NOTE) throw new JoplinError('Testing rejectedByTarget', 'rejectedByTarget');
|
||||||
const content = await ItemClass.serializeForSync(local);
|
const content = await ItemClass.serializeForSync(local);
|
||||||
await this.apiCall('put', path, content);
|
await this.api().put(path, content);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error && error.code === 'rejectedByTarget') {
|
if (error && error.code === 'rejectedByTarget') {
|
||||||
await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message);
|
await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message);
|
||||||
@ -607,11 +593,11 @@ class Synchronizer {
|
|||||||
const item = deletedItems[i];
|
const item = deletedItems[i];
|
||||||
const path = BaseItem.systemPath(item.item_id);
|
const path = BaseItem.systemPath(item.item_id);
|
||||||
this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
|
this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
|
||||||
await this.apiCall('delete', path);
|
await this.api().delete(path);
|
||||||
|
|
||||||
if (item.item_type === BaseModel.TYPE_RESOURCE) {
|
if (item.item_type === BaseModel.TYPE_RESOURCE) {
|
||||||
const remoteContentPath = resourceRemotePath(item.item_id);
|
const remoteContentPath = resourceRemotePath(item.item_id);
|
||||||
await this.apiCall('delete', remoteContentPath);
|
await this.api().delete(remoteContentPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
await BaseItem.remoteDeletedItem(syncTargetId, item.item_id);
|
await BaseItem.remoteDeletedItem(syncTargetId, item.item_id);
|
||||||
@ -642,7 +628,7 @@ class Synchronizer {
|
|||||||
while (true) {
|
while (true) {
|
||||||
if (this.cancelling() || hasCancelled) break;
|
if (this.cancelling() || hasCancelled) break;
|
||||||
|
|
||||||
const listResult = await this.apiCall('delta', '', {
|
const listResult = await this.api().delta('', {
|
||||||
context: context,
|
context: context,
|
||||||
|
|
||||||
// allItemIdsHandler() provides a way for drivers that don't have a delta API to
|
// allItemIdsHandler() provides a way for drivers that don't have a delta API to
|
||||||
@ -667,7 +653,7 @@ class Synchronizer {
|
|||||||
if (this.cancelling()) break;
|
if (this.cancelling()) break;
|
||||||
|
|
||||||
this.downloadQueue_.push(remote.path, async () => {
|
this.downloadQueue_.push(remote.path, async () => {
|
||||||
return this.apiCall('get', remote.path);
|
return this.api().get(remote.path);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -683,7 +669,7 @@ class Synchronizer {
|
|||||||
if (!BaseItem.isSystemPath(remote.path)) continue; // The delta API might return things like the .sync, .resource or the root folder
|
if (!BaseItem.isSystemPath(remote.path)) continue; // The delta API might return things like the .sync, .resource or the root folder
|
||||||
|
|
||||||
const loadContent = async () => {
|
const loadContent = async () => {
|
||||||
const task = await this.downloadQueue_.waitForResult(path); // await this.apiCall('get', path);
|
const task = await this.downloadQueue_.waitForResult(path); // await this.api().get(path);
|
||||||
if (task.error) throw task.error;
|
if (task.error) throw task.error;
|
||||||
if (!task.result) return null;
|
if (!task.result) return null;
|
||||||
return await BaseItem.unserialize(task.result);
|
return await BaseItem.unserialize(task.result);
|
||||||
@ -846,13 +832,13 @@ class Synchronizer {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (throwOnError) {
|
if (throwOnError) {
|
||||||
errorToThrow = error;
|
errorToThrow = error;
|
||||||
} else if (error && ['cannotEncryptEncrypted', 'noActiveMasterKey', 'processingPathTwice', 'failSafe', 'lockError'].indexOf(error.code) >= 0) {
|
} else if (error && ['cannotEncryptEncrypted', 'noActiveMasterKey', 'processingPathTwice', 'failSafe'].indexOf(error.code) >= 0) {
|
||||||
// Only log an info statement for this since this is a common condition that is reported
|
// Only log an info statement for this since this is a common condition that is reported
|
||||||
// in the application, and needs to be resolved by the user.
|
// in the application, and needs to be resolved by the user.
|
||||||
// Or it's a temporary issue that will be resolved on next sync.
|
// Or it's a temporary issue that will be resolved on next sync.
|
||||||
this.logger().info(error.message);
|
this.logger().info(error.message);
|
||||||
|
|
||||||
if (error.code === 'failSafe' || error.code === 'lockError') {
|
if (error.code === 'failSafe') {
|
||||||
// Get the message to display on UI, but not in testing to avoid poluting stdout
|
// Get the message to display on UI, but not in testing to avoid poluting stdout
|
||||||
if (!shim.isTestingEnv()) this.progressReport_.errors.push(error.message);
|
if (!shim.isTestingEnv()) this.progressReport_.errors.push(error.message);
|
||||||
this.logLastRequests();
|
this.logLastRequests();
|
||||||
@ -871,15 +857,6 @@ class Synchronizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.lockHandler().releaseLock('sync', this.appType_, this.clientId_);
|
|
||||||
|
|
||||||
if (this.refreshLockIID_) {
|
|
||||||
clearInterval(this.refreshLockIID_);
|
|
||||||
this.refreshLockIID_ = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncTargetIsLocked_ = false;
|
|
||||||
|
|
||||||
if (this.cancelling()) {
|
if (this.cancelling()) {
|
||||||
this.logger().info('Synchronisation was cancelled.');
|
this.logger().info('Synchronisation was cancelled.');
|
||||||
this.cancelling_ = false;
|
this.cancelling_ = false;
|
||||||
|
22
package-lock.json
generated
22
package-lock.json
generated
@ -92,12 +92,6 @@
|
|||||||
"hoist-non-react-statics": "^3.3.0"
|
"hoist-non-react-statics": "^3.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/jasmine": {
|
|
||||||
"version": "3.5.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.11.tgz",
|
|
||||||
"integrity": "sha512-fg1rOd/DehQTIJTifGqGVY6q92lDgnLfs7C6t1ccSwQrMyoTGSoH6wWzhJDZb6ezhsdwAX4EIBLe8w5fXWmEng==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/json-schema": {
|
"@types/json-schema": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz",
|
||||||
@ -4195,22 +4189,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||||
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
|
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
|
||||||
},
|
},
|
||||||
"jasmine": {
|
|
||||||
"version": "3.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.5.0.tgz",
|
|
||||||
"integrity": "sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"glob": "^7.1.4",
|
|
||||||
"jasmine-core": "~3.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"jasmine-core": {
|
|
||||||
"version": "3.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz",
|
|
||||||
"integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"joplin-turndown": {
|
"joplin-turndown": {
|
||||||
"version": "4.0.28",
|
"version": "4.0.28",
|
||||||
"resolved": "https://registry.npmjs.org/joplin-turndown/-/joplin-turndown-4.0.28.tgz",
|
"resolved": "https://registry.npmjs.org/joplin-turndown/-/joplin-turndown-4.0.28.tgz",
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jasmine": "^3.5.11",
|
|
||||||
"@types/react": "^16.9.0",
|
"@types/react": "^16.9.0",
|
||||||
"@types/react-dom": "^16.9.0",
|
"@types/react-dom": "^16.9.0",
|
||||||
"@types/react-redux": "^7.1.7",
|
"@types/react-redux": "^7.1.7",
|
||||||
@ -34,7 +33,6 @@
|
|||||||
"glob": "^7.1.6",
|
"glob": "^7.1.6",
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
"husky": "^3.0.2",
|
"husky": "^3.0.2",
|
||||||
"jasmine": "^3.5.0",
|
|
||||||
"lint-staged": "^9.2.1",
|
"lint-staged": "^9.2.1",
|
||||||
"typescript": "^3.7.3"
|
"typescript": "^3.7.3"
|
||||||
},
|
},
|
||||||
|
@ -15,10 +15,6 @@
|
|||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"lib/*": ["./ReactNativeClient/lib/*"],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"ReactNativeClient/**/*",
|
"ReactNativeClient/**/*",
|
||||||
|
Loading…
Reference in New Issue
Block a user