You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	CLI v1.0.165
This commit is contained in:
		
							
								
								
									
										2
									
								
								CliClient/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								CliClient/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "joplin", | ||||
|   "version": "1.0.164", | ||||
|   "version": "1.0.165", | ||||
|   "lockfileVersion": 1, | ||||
|   "requires": true, | ||||
|   "dependencies": { | ||||
|   | ||||
| @@ -27,7 +27,7 @@ | ||||
|     ], | ||||
|     "owner": "Laurent Cozic" | ||||
|   }, | ||||
|   "version": "1.0.164", | ||||
|   "version": "1.0.165", | ||||
|   "bin": { | ||||
|     "joplin": "./main.js" | ||||
|   }, | ||||
|   | ||||
							
								
								
									
										87
									
								
								CliClient/tests/synchronizer_LockHandler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								CliClient/tests/synchronizer_LockHandler.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| 'use strict'; | ||||
| const __awaiter = (this && this.__awaiter) || function(thisArg, _arguments, P, generator) { | ||||
| 	function adopt(value) { return value instanceof P ? value : new P(function(resolve) { resolve(value); }); } | ||||
| 	return new (P || (P = Promise))(function(resolve, reject) { | ||||
| 		function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||||
| 		function rejected(value) { try { step(generator['throw'](value)); } catch (e) { reject(e); } } | ||||
| 		function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||||
| 		step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||||
| 	}); | ||||
| }; | ||||
| Object.defineProperty(exports, '__esModule', { value: true }); | ||||
| const LockHandler_1 = require('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, p) => { | ||||
| 	console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); | ||||
| }); | ||||
| let lockHandler_ = null; | ||||
| const locksDirname = 'locks'; | ||||
| function lockHandler() { | ||||
| 	if (lockHandler_) { return lockHandler_; } | ||||
| 	lockHandler_ = new LockHandler_1.default(fileApi(), locksDirname); | ||||
| 	return lockHandler_; | ||||
| } | ||||
| describe('synchronizer_LockHandler', function() { | ||||
| 	beforeEach((done) => __awaiter(this, void 0, void 0, function* () { | ||||
| 		lockHandler_ = null; | ||||
| 		yield setupDatabaseAndSynchronizer(1); | ||||
| 		yield setupDatabaseAndSynchronizer(2); | ||||
| 		yield switchClient(1); | ||||
| 		done(); | ||||
| 	})); | ||||
| 	it('should acquire and release a sync lock', asyncTest(() => __awaiter(this, void 0, void 0, function* () { | ||||
| 		yield lockHandler().acquireLock(LockHandler_1.LockType.Sync, 'mobile', '123456'); | ||||
| 		const locks = yield lockHandler().syncLocks(); | ||||
| 		expect(locks.length).toBe(1); | ||||
| 		expect(locks[0].type).toBe(LockHandler_1.LockType.Sync); | ||||
| 		expect(locks[0].clientId).toBe('123456'); | ||||
| 		expect(locks[0].clientType).toBe('mobile'); | ||||
| 		yield lockHandler().releaseLock(LockHandler_1.LockType.Sync, 'mobile', '123456'); | ||||
| 		expect((yield lockHandler().syncLocks()).length).toBe(0); | ||||
| 	}))); | ||||
| 	it('should allow multiple sync locks', asyncTest(() => __awaiter(this, void 0, void 0, function* () { | ||||
| 		yield lockHandler().acquireLock(LockHandler_1.LockType.Sync, 'mobile', '111'); | ||||
| 		yield switchClient(2); | ||||
| 		yield lockHandler().acquireLock(LockHandler_1.LockType.Sync, 'mobile', '222'); | ||||
| 		expect((yield lockHandler().syncLocks()).length).toBe(2); | ||||
| 		{ | ||||
| 			yield lockHandler().releaseLock(LockHandler_1.LockType.Sync, 'mobile', '222'); | ||||
| 			const locks = yield lockHandler().syncLocks(); | ||||
| 			expect(locks.length).toBe(1); | ||||
| 			expect(locks[0].clientId).toBe('111'); | ||||
| 		} | ||||
| 	}))); | ||||
| 	it('should refresh sync lock timestamp when acquiring again', asyncTest(() => __awaiter(this, void 0, void 0, function* () { | ||||
| 		yield lockHandler().acquireLock(LockHandler_1.LockType.Sync, 'mobile', '111'); | ||||
| 		const beforeTime = (yield lockHandler().syncLocks())[0].updatedTime; | ||||
| 		yield msleep(1); | ||||
| 		yield lockHandler().acquireLock(LockHandler_1.LockType.Sync, 'mobile', '111'); | ||||
| 		const afterTime = (yield lockHandler().syncLocks())[0].updatedTime; | ||||
| 		expect(beforeTime).toBeLessThan(afterTime); | ||||
| 	}))); | ||||
| 	it('should not allow sync locks if there is an exclusive lock', asyncTest(() => __awaiter(this, void 0, void 0, function* () { | ||||
| 		yield lockHandler().acquireLock(LockHandler_1.LockType.Exclusive, 'desktop', '111'); | ||||
| 		expectThrow(() => __awaiter(this, void 0, void 0, function* () { | ||||
| 			yield lockHandler().acquireLock(LockHandler_1.LockType.Sync, 'mobile', '222'); | ||||
| 		}), 'hasExclusiveLock'); | ||||
| 	}))); | ||||
| 	it('should not allow exclusive lock if there are sync locks', asyncTest(() => __awaiter(this, void 0, void 0, function* () { | ||||
| 		lockHandler().syncLockMaxAge = 1000 * 60 * 60; | ||||
| 		yield lockHandler().acquireLock(LockHandler_1.LockType.Sync, 'mobile', '111'); | ||||
| 		yield lockHandler().acquireLock(LockHandler_1.LockType.Sync, 'mobile', '222'); | ||||
| 		expectThrow(() => __awaiter(this, void 0, void 0, function* () { | ||||
| 			yield lockHandler().acquireLock(LockHandler_1.LockType.Exclusive, 'desktop', '333'); | ||||
| 		}), 'hasSyncLock'); | ||||
| 	}))); | ||||
| 	it('should allow exclusive lock if the sync locks have expired', asyncTest(() => __awaiter(this, void 0, void 0, function* () { | ||||
| 		lockHandler().syncLockMaxAge = 1; | ||||
| 		yield lockHandler().acquireLock(LockHandler_1.LockType.Sync, 'mobile', '111'); | ||||
| 		yield lockHandler().acquireLock(LockHandler_1.LockType.Sync, 'mobile', '222'); | ||||
| 		yield msleep(2); | ||||
| 		expectNotThrow(() => __awaiter(this, void 0, void 0, function* () { | ||||
| 			yield lockHandler().acquireLock(LockHandler_1.LockType.Exclusive, 'desktop', '333'); | ||||
| 		})); | ||||
| 	}))); | ||||
| }); | ||||
| // # sourceMappingURL=synchronizer_LockHandler.js.map | ||||
							
								
								
									
										207
									
								
								ReactNativeClient/lib/services/synchronizer/LockHandler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								ReactNativeClient/lib/services/synchronizer/LockHandler.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,207 @@ | ||||
| 'use strict'; | ||||
| const __awaiter = (this && this.__awaiter) || function(thisArg, _arguments, P, generator) { | ||||
| 	function adopt(value) { return value instanceof P ? value : new P(function(resolve) { resolve(value); }); } | ||||
| 	return new (P || (P = Promise))(function(resolve, reject) { | ||||
| 		function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||||
| 		function rejected(value) { try { step(generator['throw'](value)); } catch (e) { reject(e); } } | ||||
| 		function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||||
| 		step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||||
| 	}); | ||||
| }; | ||||
| Object.defineProperty(exports, '__esModule', { value: true }); | ||||
| const JoplinError = require('lib/JoplinError'); | ||||
| const { time } = require('lib/time-utils'); | ||||
| const { fileExtension, filename } = require('lib/path-utils.js'); | ||||
| let LockType; | ||||
| (function(LockType) { | ||||
| 	LockType['None'] = ''; | ||||
| 	LockType['Sync'] = 'sync'; | ||||
| 	LockType['Exclusive'] = 'exclusive'; | ||||
| })(LockType = exports.LockType || (exports.LockType = {})); | ||||
| const exclusiveFilename = 'exclusive.json'; | ||||
| class LockHandler { | ||||
| 	constructor(api, lockDirPath) { | ||||
| 		this.api_ = null; | ||||
| 		this.lockDirPath_ = null; | ||||
| 		this.syncLockMaxAge_ = 1000 * 60 * 3; | ||||
| 		this.api_ = api; | ||||
| 		this.lockDirPath_ = lockDirPath; | ||||
| 	} | ||||
| 	get syncLockMaxAge() { | ||||
| 		return this.syncLockMaxAge_; | ||||
| 	} | ||||
| 	// Should only be done for testing purposes since all clients should | ||||
| 	// use the same lock max age. | ||||
| 	set syncLockMaxAge(v) { | ||||
| 		this.syncLockMaxAge_ = v; | ||||
| 	} | ||||
| 	lockFilename(lock) { | ||||
| 		if (lock.type === LockType.Exclusive) { | ||||
| 			return exclusiveFilename; | ||||
| 		} else { | ||||
| 			return `${[lock.type, lock.clientType, lock.clientId].join('_')}.json`; | ||||
| 		} | ||||
| 	} | ||||
| 	lockTypeFromFilename(name) { | ||||
| 		if (name === exclusiveFilename) { return LockType.Exclusive; } | ||||
| 		return LockType.Sync; | ||||
| 	} | ||||
| 	lockFilePath(lock) { | ||||
| 		return `${this.lockDirPath_}/${this.lockFilename(lock)}`; | ||||
| 	} | ||||
| 	exclusiveFilePath() { | ||||
| 		return `${this.lockDirPath_}/${exclusiveFilename}`; | ||||
| 	} | ||||
| 	syncLockFileToObject(file) { | ||||
| 		const p = filename(file.path).split('_'); | ||||
| 		return { | ||||
| 			type: p[0], | ||||
| 			clientType: p[1], | ||||
| 			clientId: p[2], | ||||
| 			updatedTime: file.updated_time, | ||||
| 		}; | ||||
| 	} | ||||
| 	syncLocks() { | ||||
| 		return __awaiter(this, void 0, void 0, function* () { | ||||
| 			const result = yield 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; | ||||
| 		}); | ||||
| 	} | ||||
| 	exclusiveLock() { | ||||
| 		return __awaiter(this, void 0, void 0, function* () { | ||||
| 			const stat = yield this.api_.stat(this.exclusiveFilePath()); | ||||
| 			if (!stat) { return null; } | ||||
| 			const contentText = yield this.api_.get(this.exclusiveFilePath()); | ||||
| 			if (!contentText) { return null; } // race condition | ||||
| 			const lock = JSON.parse(contentText); | ||||
| 			lock.updatedTime = stat.updated_time; | ||||
| 			return lock; | ||||
| 		}); | ||||
| 	} | ||||
| 	lockIsActive(lock) { | ||||
| 		return Date.now() - lock.updatedTime < this.syncLockMaxAge; | ||||
| 	} | ||||
| 	hasActiveExclusiveLock() { | ||||
| 		return __awaiter(this, void 0, void 0, function* () { | ||||
| 			const lock = yield this.exclusiveLock(); | ||||
| 			return !!lock && this.lockIsActive(lock); | ||||
| 		}); | ||||
| 	} | ||||
| 	hasActiveSyncLock(clientType, clientId) { | ||||
| 		return __awaiter(this, void 0, void 0, function* () { | ||||
| 			const locks = yield this.syncLocks(); | ||||
| 			for (const lock of locks) { | ||||
| 				if (lock.clientType === clientType && lock.clientId === clientId && this.lockIsActive(lock)) { return true; } | ||||
| 			} | ||||
| 			return false; | ||||
| 		}); | ||||
| 	} | ||||
| 	saveLock(lock) { | ||||
| 		return __awaiter(this, void 0, void 0, function* () { | ||||
| 			yield this.api_.put(this.lockFilePath(lock), JSON.stringify(lock)); | ||||
| 		}); | ||||
| 	} | ||||
| 	acquireSyncLock(clientType, clientId) { | ||||
| 		return __awaiter(this, void 0, void 0, function* () { | ||||
| 			const exclusiveLock = yield 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'); | ||||
| 			} | ||||
| 			yield this.saveLock({ | ||||
| 				type: LockType.Sync, | ||||
| 				clientType: clientType, | ||||
| 				clientId: clientId, | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
| 	lockToClientString(lock) { | ||||
| 		return `(${lock.clientType} #${lock.clientId})`; | ||||
| 	} | ||||
| 	acquireExclusiveLock(clientType, clientId, timeoutMs = 0) { | ||||
| 		return __awaiter(this, void 0, void 0, function* () { | ||||
| 			// 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(); | ||||
| 			function waitForTimeout() { | ||||
| 				return __awaiter(this, void 0, void 0, function* () { | ||||
| 					if (!timeoutMs) { return false; } | ||||
| 					const elapsed = Date.now() - startTime; | ||||
| 					if (timeoutMs && elapsed < timeoutMs) { | ||||
| 						yield time.sleep(2); | ||||
| 						return true; | ||||
| 					} | ||||
| 					return false; | ||||
| 				}); | ||||
| 			} | ||||
| 			while (true) { | ||||
| 				const syncLocks = yield this.syncLocks(); | ||||
| 				const activeSyncLocks = syncLocks.filter(lock => this.lockIsActive(lock)); | ||||
| 				if (activeSyncLocks.length) { | ||||
| 					if (yield 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 = yield this.exclusiveLock(); | ||||
| 				if (exclusiveLock) { | ||||
| 					if (exclusiveLock.clientId === clientId) { | ||||
| 						// Save it again to refresh the timestamp | ||||
| 						yield this.saveLock(exclusiveLock); | ||||
| 						return; | ||||
| 					} else { | ||||
| 						// If there's already an exclusive lock, wait for it to be released | ||||
| 						if (yield 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) | ||||
| 					yield this.saveLock({ | ||||
| 						type: LockType.Exclusive, | ||||
| 						clientType: clientType, | ||||
| 						clientId: clientId, | ||||
| 					}); | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| 	acquireLock(lockType, clientType, clientId, timeoutMs = 0) { | ||||
| 		return __awaiter(this, void 0, void 0, function* () { | ||||
| 			if (lockType === LockType.Sync) { | ||||
| 				yield this.acquireSyncLock(clientType, clientId); | ||||
| 			} else if (lockType === LockType.Exclusive) { | ||||
| 				yield this.acquireExclusiveLock(clientType, clientId, timeoutMs); | ||||
| 			} else { | ||||
| 				throw new Error(`Invalid lock type: ${lockType}`); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| 	releaseLock(lockType, clientType, clientId) { | ||||
| 		return __awaiter(this, void 0, void 0, function* () { | ||||
| 			yield this.api_.delete(this.lockFilePath({ | ||||
| 				type: lockType, | ||||
| 				clientType: clientType, | ||||
| 				clientId: clientId, | ||||
| 			})); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
| exports.default = LockHandler; | ||||
| // # sourceMappingURL=LockHandler.js.map | ||||
| @@ -1,5 +1,18 @@ | ||||
| # Joplin terminal app changelog | ||||
|  | ||||
| ## [cli-v1.0.165](https://github.com/laurent22/joplin/releases/tag/cli-v1.0.165) - 2020-07-10T18:51:42Z | ||||
|  | ||||
| - New: Translation: Add bahasa indonesia (id_ID.po) (#3246 by [@ffadilaputra](https://github.com/ffadilaputra)) | ||||
| - Improved: Allow importing ENEX files as HTML | ||||
| - Improved: Disable support for HTML export for now as it does not work | ||||
| - Improved: Upload attachments > 4 MB when using OneDrive (#3195) (#173 by [@TheOnlyTrueJonathanHeard](https://github.com/TheOnlyTrueJonathanHeard)) | ||||
| - Fixed: Fixed import of checkboxes in ENEX files (#3402) | ||||
| - Fixed: Fixed various bugs related to the import of ENEX files as HTML | ||||
| - Fixed: Only de-duplicate imported notebook titles when needed (#2331) | ||||
| - Fixed: Prevent desktop.ini file from breaking sync lock (#3381) | ||||
| - Fixed: Prevent notebook to be the parent of itself (#3334) | ||||
| - Fixed: Sync would fail in some cases due to a database error (#3234) | ||||
|  | ||||
| ## [cli-v1.0.164](https://github.com/laurent22/joplin/releases/tag/cli-v1.0.164) - 2020-05-13T15:30:22Z | ||||
|  | ||||
| - New: Added support for basic search | ||||
|   | ||||
		Reference in New Issue
	
	Block a user