mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +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/
|
||||
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
CliClient/tests/synchronizer_LockHandler.js
|
||||
ElectronClient/commands/focusElement.js
|
||||
ElectronClient/commands/startExternalEditing.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/errors.js
|
||||
ReactNativeClient/lib/services/SettingUtils.js
|
||||
ReactNativeClient/lib/services/synchronizer/LockHandler.js
|
||||
ReactNativeClient/lib/services/UndoRedoService.js
|
||||
ReactNativeClient/lib/ShareExtension.js
|
||||
ReactNativeClient/lib/shareHandler.js
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -51,7 +51,6 @@ Tools/commit_hook.txt
|
||||
*.map
|
||||
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
CliClient/tests/synchronizer_LockHandler.js
|
||||
ElectronClient/commands/focusElement.js
|
||||
ElectronClient/commands/startExternalEditing.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/errors.js
|
||||
ReactNativeClient/lib/services/SettingUtils.js
|
||||
ReactNativeClient/lib/services/synchronizer/LockHandler.js
|
||||
ReactNativeClient/lib/services/UndoRedoService.js
|
||||
ReactNativeClient/lib/ShareExtension.js
|
||||
ReactNativeClient/lib/shareHandler.js
|
||||
|
@ -37,8 +37,6 @@ tasks.buildTests = {
|
||||
'lib/',
|
||||
'locales/',
|
||||
'node_modules/',
|
||||
'*.ts',
|
||||
'*.tsx',
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -33,7 +33,7 @@ async function allNotesFolders() {
|
||||
}
|
||||
|
||||
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!!!');
|
||||
const files = list.items;
|
||||
|
||||
@ -822,7 +822,7 @@ describe('synchronizer', function() {
|
||||
// First create a folder, without encryption enabled, and sync it
|
||||
const folder1 = await Folder.save({ title: 'folder1' });
|
||||
await synchronizer().start();
|
||||
let files = await fileApi().list('', { includeDirs: false });
|
||||
let files = await fileApi().list();
|
||||
let content = await fileApi().get(files.items[0].path);
|
||||
expect(content.indexOf('folder1') >= 0).toBe(true);
|
||||
|
||||
@ -1662,22 +1662,22 @@ describe('synchronizer', function() {
|
||||
expect(hasThrown).toBe(true);
|
||||
}));
|
||||
|
||||
// it('should not sync when target is locked', asyncTest(async () => {
|
||||
// await synchronizer().start();
|
||||
// await synchronizer().acquireLock_();
|
||||
it('should not sync when target is locked', asyncTest(async () => {
|
||||
await synchronizer().start();
|
||||
await synchronizer().acquireLock_();
|
||||
|
||||
// await switchClient(2);
|
||||
// const hasThrown = await checkThrowAsync(async () => synchronizer().start({ throwOnError: true }));
|
||||
// expect(hasThrown).toBe(true);
|
||||
// }));
|
||||
await switchClient(2);
|
||||
const hasThrown = await checkThrowAsync(async () => synchronizer().start({ throwOnError: true }));
|
||||
expect(hasThrown).toBe(true);
|
||||
}));
|
||||
|
||||
// it('should clear a lock if it was created by the same app as the current one', asyncTest(async () => {
|
||||
// await synchronizer().start();
|
||||
// await synchronizer().acquireLock_();
|
||||
// expect((await synchronizer().lockFiles_()).length).toBe(1);
|
||||
// await synchronizer().start({ throwOnError: true });
|
||||
// expect((await synchronizer().lockFiles_()).length).toBe(0);
|
||||
// }));
|
||||
it('should clear a lock if it was created by the same app as the current one', asyncTest(async () => {
|
||||
await synchronizer().start();
|
||||
await synchronizer().acquireLock_();
|
||||
expect((await synchronizer().lockFiles_()).length).toBe(1);
|
||||
await synchronizer().start({ throwOnError: true });
|
||||
expect((await synchronizer().lockFiles_()).length).toBe(0);
|
||||
}));
|
||||
|
||||
it('should not encrypt notes that are shared', asyncTest(async () => {
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
|
@ -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() {
|
||||
return currentClient_;
|
||||
}
|
||||
@ -386,40 +378,6 @@ async function checkThrowAsync(asyncFn) {
|
||||
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) {
|
||||
let hasThrown = false;
|
||||
try {
|
||||
@ -457,7 +415,7 @@ function asyncTest(callback) {
|
||||
}
|
||||
|
||||
async function allSyncTargetItemsEncrypted() {
|
||||
const list = await fileApi().list('', { includeDirs: false });
|
||||
const list = await fileApi().list();
|
||||
const files = list.items;
|
||||
|
||||
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==",
|
||||
"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",
|
||||
|
@ -79,7 +79,6 @@
|
||||
},
|
||||
"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",
|
||||
|
@ -111,7 +111,7 @@ class FileApiDriverMemory {
|
||||
this.items_.push(item);
|
||||
} else {
|
||||
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) {
|
||||
const getDirStats = async path => {
|
||||
const result = await this.list(path, { includeDirs: false });
|
||||
const result = await this.list(path);
|
||||
return result.items;
|
||||
};
|
||||
|
||||
|
@ -128,7 +128,6 @@ 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;
|
||||
|
||||
this.logger().debug(`list ${this.baseDir()}`);
|
||||
|
||||
@ -142,10 +141,6 @@ class FileApi {
|
||||
result.items = temp;
|
||||
}
|
||||
|
||||
if (!options.includeHidden) {
|
||||
result.items = result.items.filter(f => !f.isDir);
|
||||
}
|
||||
|
||||
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 { _ } = require('lib/locale.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
// const { filename, fileExtension } = require('lib/path-utils');
|
||||
const { filename, fileExtension } = require('lib/path-utils');
|
||||
const JoplinError = require('lib/JoplinError');
|
||||
const BaseSyncTarget = require('lib/BaseSyncTarget');
|
||||
const TaskQueue = require('lib/TaskQueue');
|
||||
const LockHandler = require('lib/services/synchronizer/LockHandler').default;
|
||||
|
||||
class Synchronizer {
|
||||
constructor(db, api, appType) {
|
||||
@ -24,7 +23,7 @@ class Synchronizer {
|
||||
this.db_ = db;
|
||||
this.api_ = api;
|
||||
this.syncDirName_ = '.sync';
|
||||
this.lockDirName_ = 'locks';
|
||||
this.lockDirName_ = '.lock';
|
||||
this.resourceDirName_ = BaseSyncTarget.resourceDirName();
|
||||
this.logger_ = new Logger();
|
||||
this.appType_ = appType;
|
||||
@ -67,12 +66,6 @@ class Synchronizer {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
lockHandler() {
|
||||
if (this.lockHandler_) return this.lockHandler_;
|
||||
this.lockHandler_ = new LockHandler(this.api(), this.lockDirName_);
|
||||
return this.lockHandler_;
|
||||
}
|
||||
|
||||
maxResourceSize() {
|
||||
if (this.maxResourceSize_ !== null) return this.maxResourceSize_;
|
||||
return this.appType_ === 'mobile' ? 10 * 1000 * 1000 : Infinity;
|
||||
@ -212,17 +205,64 @@ class Synchronizer {
|
||||
return state;
|
||||
}
|
||||
|
||||
async acquireLock_() {
|
||||
await this.checkLock_();
|
||||
await this.api().put(`${this.lockDirName_}/${this.clientId()}_${Date.now()}.lock`, `${Date.now()}`);
|
||||
}
|
||||
|
||||
async releaseLock_() {
|
||||
const lockFiles = await this.lockFiles_();
|
||||
for (const lockFile of lockFiles) {
|
||||
const p = this.parseLockFilePath(lockFile.path);
|
||||
if (p.clientId === this.clientId()) {
|
||||
await this.api().delete(p.fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async lockFiles_() {
|
||||
const output = await this.api().list(this.lockDirName_);
|
||||
return output.items.filter((p) => {
|
||||
const ext = fileExtension(p.path);
|
||||
return ext === 'lock';
|
||||
});
|
||||
}
|
||||
|
||||
parseLockFilePath(path) {
|
||||
const splitted = filename(path).split('_');
|
||||
const fullPath = `${this.lockDirName_}/${path}`;
|
||||
if (splitted.length !== 2) throw new Error(`Sync target appears to be locked but lock filename is invalid: ${fullPath}. Please delete it on the sync target to continue.`);
|
||||
return {
|
||||
clientId: splitted[0],
|
||||
timestamp: Number(splitted[1]),
|
||||
fullPath: fullPath,
|
||||
};
|
||||
}
|
||||
|
||||
async checkLock_() {
|
||||
const lockFiles = await this.lockFiles_();
|
||||
if (lockFiles.length) {
|
||||
const lock = this.parseLockFilePath(lockFiles[0].path);
|
||||
|
||||
if (lock.clientId === this.clientId()) {
|
||||
await this.releaseLock_();
|
||||
} else {
|
||||
throw new Error(`The sync target was locked by client ${lock.clientId} on ${time.unixMsToLocalDateTime(lock.timestamp)} and cannot be accessed. If no app is currently operating on the sync target, you can delete the files in the "${this.lockDirName_}" directory on the sync target to resume.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkSyncTargetVersion_() {
|
||||
const supportedSyncTargetVersion = Setting.value('syncVersion');
|
||||
const syncTargetVersion = await this.apiCall('get', '.sync/version.txt');
|
||||
const syncTargetVersion = await this.api().get('.sync/version.txt');
|
||||
|
||||
if (!syncTargetVersion) {
|
||||
await this.apiCall('put', '.sync/version.txt', `${supportedSyncTargetVersion}`);
|
||||
await this.api().put('.sync/version.txt', `${supportedSyncTargetVersion}`);
|
||||
} else {
|
||||
if (Number(syncTargetVersion) > supportedSyncTargetVersion) {
|
||||
throw new Error(sprintf('Sync version of the target (%d) does not match sync version supported by client (%d). Please upgrade your client.', Number(syncTargetVersion), supportedSyncTargetVersion));
|
||||
} else {
|
||||
await this.apiCall('put', '.sync/version.txt', `${supportedSyncTargetVersion}`);
|
||||
await this.api().put('.sync/version.txt', `${supportedSyncTargetVersion}`);
|
||||
// TODO: do upgrade job
|
||||
}
|
||||
}
|
||||
@ -232,44 +272,6 @@ class Synchronizer {
|
||||
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:
|
||||
//
|
||||
// 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();
|
||||
|
||||
this.syncTargetIsLocked_ = false;
|
||||
this.cancelling_ = false;
|
||||
|
||||
const masterKeysBefore = await MasterKey.count();
|
||||
@ -324,27 +325,12 @@ class Synchronizer {
|
||||
let errorToThrow = null;
|
||||
|
||||
try {
|
||||
await this.apiCall('mkdir', this.syncDirName_);
|
||||
await this.apiCall('mkdir', this.lockDirName_);
|
||||
await this.api().mkdir(this.syncDirName_);
|
||||
await this.api().mkdir(this.lockDirName_);
|
||||
this.api().setTempDirName(this.syncDirName_);
|
||||
await this.apiCall('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.api().mkdir(this.resourceDirName_);
|
||||
|
||||
await this.checkLock_();
|
||||
await this.checkSyncTargetVersion_();
|
||||
|
||||
// ========================================================================
|
||||
@ -383,7 +369,7 @@ class Synchronizer {
|
||||
// (by setting an updated_time less than current time).
|
||||
if (donePaths.indexOf(path) >= 0) throw new JoplinError(sprintf('Processing a path that has already been done: %s. sync_time was not updated? Remote item has an updated_time in the future?', path), 'processingPathTwice');
|
||||
|
||||
const remote = await this.apiCall('stat', path);
|
||||
const remote = await this.api().stat(path);
|
||||
let action = null;
|
||||
|
||||
let reason = '';
|
||||
@ -421,7 +407,7 @@ class Synchronizer {
|
||||
// OneDrive does not appear to have accurate timestamps as lastModifiedDateTime would occasionally be
|
||||
// a few seconds ahead of what it was set with setTimestamp()
|
||||
try {
|
||||
remoteContent = await this.apiCall('get', path);
|
||||
remoteContent = await this.api().get(path);
|
||||
} catch (error) {
|
||||
if (error.code === 'rejectedByTarget') {
|
||||
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.`);
|
||||
}
|
||||
|
||||
await this.apiCall('put', remoteContentPath, null, { path: localResourceContentPath, source: 'file' });
|
||||
await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' });
|
||||
} catch (error) {
|
||||
if (error && ['rejectedByTarget', 'fileNotFound'].indexOf(error.code) >= 0) {
|
||||
await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message);
|
||||
@ -481,7 +467,7 @@ class Synchronizer {
|
||||
try {
|
||||
if (this.testingHooks_.indexOf('notesRejectedByTarget') >= 0 && local.type_ === BaseModel.TYPE_NOTE) throw new JoplinError('Testing rejectedByTarget', 'rejectedByTarget');
|
||||
const content = await ItemClass.serializeForSync(local);
|
||||
await this.apiCall('put', path, content);
|
||||
await this.api().put(path, content);
|
||||
} catch (error) {
|
||||
if (error && error.code === 'rejectedByTarget') {
|
||||
await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message);
|
||||
@ -607,11 +593,11 @@ class Synchronizer {
|
||||
const item = deletedItems[i];
|
||||
const path = BaseItem.systemPath(item.item_id);
|
||||
this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
|
||||
await this.apiCall('delete', path);
|
||||
await this.api().delete(path);
|
||||
|
||||
if (item.item_type === BaseModel.TYPE_RESOURCE) {
|
||||
const remoteContentPath = resourceRemotePath(item.item_id);
|
||||
await this.apiCall('delete', remoteContentPath);
|
||||
await this.api().delete(remoteContentPath);
|
||||
}
|
||||
|
||||
await BaseItem.remoteDeletedItem(syncTargetId, item.item_id);
|
||||
@ -642,7 +628,7 @@ class Synchronizer {
|
||||
while (true) {
|
||||
if (this.cancelling() || hasCancelled) break;
|
||||
|
||||
const listResult = await this.apiCall('delta', '', {
|
||||
const listResult = await this.api().delta('', {
|
||||
context: context,
|
||||
|
||||
// allItemIdsHandler() provides a way for drivers that don't have a delta API to
|
||||
@ -667,7 +653,7 @@ class Synchronizer {
|
||||
if (this.cancelling()) break;
|
||||
|
||||
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
|
||||
|
||||
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.result) return null;
|
||||
return await BaseItem.unserialize(task.result);
|
||||
@ -846,13 +832,13 @@ class Synchronizer {
|
||||
} catch (error) {
|
||||
if (throwOnError) {
|
||||
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
|
||||
// in the application, and needs to be resolved by the user.
|
||||
// Or it's a temporary issue that will be resolved on next sync.
|
||||
this.logger().info(error.message);
|
||||
|
||||
if (error.code === 'failSafe' || error.code === 'lockError') {
|
||||
if (error.code === 'failSafe') {
|
||||
// Get the message to display on UI, but not in testing to avoid poluting stdout
|
||||
if (!shim.isTestingEnv()) this.progressReport_.errors.push(error.message);
|
||||
this.logLastRequests();
|
||||
@ -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()) {
|
||||
this.logger().info('Synchronisation was cancelled.');
|
||||
this.cancelling_ = false;
|
||||
|
22
package-lock.json
generated
22
package-lock.json
generated
@ -92,12 +92,6 @@
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"@types/jasmine": {
|
||||
"version": "3.5.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.11.tgz",
|
||||
"integrity": "sha512-fg1rOd/DehQTIJTifGqGVY6q92lDgnLfs7C6t1ccSwQrMyoTGSoH6wWzhJDZb6ezhsdwAX4EIBLe8w5fXWmEng==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz",
|
||||
@ -4195,22 +4189,6 @@
|
||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
|
||||
},
|
||||
"jasmine": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.5.0.tgz",
|
||||
"integrity": "sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "^7.1.4",
|
||||
"jasmine-core": "~3.5.0"
|
||||
}
|
||||
},
|
||||
"jasmine-core": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz",
|
||||
"integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==",
|
||||
"dev": true
|
||||
},
|
||||
"joplin-turndown": {
|
||||
"version": "4.0.28",
|
||||
"resolved": "https://registry.npmjs.org/joplin-turndown/-/joplin-turndown-4.0.28.tgz",
|
||||
|
@ -20,7 +20,6 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jasmine": "^3.5.11",
|
||||
"@types/react": "^16.9.0",
|
||||
"@types/react-dom": "^16.9.0",
|
||||
"@types/react-redux": "^7.1.7",
|
||||
@ -34,7 +33,6 @@
|
||||
"glob": "^7.1.6",
|
||||
"gulp": "^4.0.2",
|
||||
"husky": "^3.0.2",
|
||||
"jasmine": "^3.5.0",
|
||||
"lint-staged": "^9.2.1",
|
||||
"typescript": "^3.7.3"
|
||||
},
|
||||
|
@ -15,10 +15,6 @@
|
||||
"sourceMap": true,
|
||||
"jsx": "react",
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"lib/*": ["./ReactNativeClient/lib/*"],
|
||||
},
|
||||
},
|
||||
"include": [
|
||||
"ReactNativeClient/**/*",
|
||||
|
Loading…
Reference in New Issue
Block a user