1
0
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:
Laurent Cozic 2020-07-12 16:38:54 +01:00
parent a7eae2e033
commit e0a87d6253
16 changed files with 87 additions and 522 deletions

View File

@ -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
View File

@ -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

View File

@ -37,8 +37,6 @@ tasks.buildTests = {
'lib/',
'locales/',
'node_modules/',
'*.ts',
'*.tsx',
],
});

View File

@ -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);

View File

@ -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');
});
}));
});

View File

@ -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 };

View File

@ -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",

View File

@ -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",

View File

@ -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();
}
}

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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,
}));
}
}

View File

@ -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
View File

@ -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",

View File

@ -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"
},

View File

@ -15,10 +15,6 @@
"sourceMap": true,
"jsx": "react",
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"lib/*": ["./ReactNativeClient/lib/*"],
},
},
"include": [
"ReactNativeClient/**/*",