You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	testing sync
This commit is contained in:
		| @@ -59,12 +59,13 @@ async function runTest() { | ||||
|  | ||||
| 	await synchronizer.start(); | ||||
|  | ||||
| 	note1 = await Note.load(note1.id); | ||||
| 	//console.info(note1); | ||||
| 	note1.title = 'un update'; | ||||
| 	await Note.save(note1); | ||||
| 	// note1 = await Note.load(note1.id); | ||||
| 	// note1.title = 'un update'; | ||||
| 	// //console.info('AVANT', note1); | ||||
| 	// note1 = await Note.save(note1); | ||||
| 	// //console.info('APRES', note1); | ||||
|  | ||||
| 	return await synchronizer.start(); | ||||
| 	// return await synchronizer.start(); | ||||
| } | ||||
|  | ||||
| runTest().catch((error) => { | ||||
|   | ||||
							
								
								
									
										0
									
								
								CliClient/b
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								CliClient/b
									
									
									
									
									
										Normal file
									
								
							| @@ -27,11 +27,11 @@ | ||||
|   "devDependencies": { | ||||
|     "babel-changed": "^7.0.0", | ||||
|     "babel-cli": "^6.24.1", | ||||
|     "babel-preset-env": "^1.5.1", | ||||
|     "babel-preset-react": "^6.24.1", | ||||
|     "babel-plugin-syntax-async-functions": "^6.1.4", | ||||
|     "babel-plugin-transform-regenerator": "^6.1.4", | ||||
|     "babel-preset-env": "^1.5.1", | ||||
|     "babel-preset-es2015": "^6.1.4", | ||||
|     "babel-preset-react": "^6.24.1", | ||||
|     "jasmine": "^2.6.0" | ||||
|   }, | ||||
|   "scripts": { | ||||
|   | ||||
| @@ -1,175 +1,207 @@ | ||||
| import { time } from 'src/time-utils.js'; | ||||
| import { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi } from 'test-utils.js'; | ||||
| import { createFoldersAndNotes } from 'test-data.js'; | ||||
| import { Folder } from 'src/models/folder.js'; | ||||
| import { Note } from 'src/models/note.js'; | ||||
| import { BaseItem } from 'src/models/base-item.js'; | ||||
|  | ||||
| // Note: set 1 matches set 1 of createRemoteItems() | ||||
| function createLocalItems(id, updatedTime, lastSyncTime) { | ||||
| 	let output = []; | ||||
| 	if (id === 1) { | ||||
| 		output.push({ path: 'test', isDir: true, updatedTime: updatedTime, lastSyncTime: lastSyncTime }); | ||||
| 		output.push({ path: 'test/un', updatedTime: updatedTime, lastSyncTime: lastSyncTime }); | ||||
| 	} else { | ||||
| 		throw new Error('Invalid ID'); | ||||
| 	} | ||||
| 	return output; | ||||
| } | ||||
|  | ||||
| function createRemoteItems(id = 1, updatedTime = null) { | ||||
| 	if (!updatedTime) updatedTime = time.unix(); | ||||
|  | ||||
| 	if (id === 1) { | ||||
| 		return fileApi().format() | ||||
| 		.then(() => fileApi().mkdir('test')) | ||||
| 		.then(() => fileApi().put('test/un', 'abcd')) | ||||
| 		.then(() => fileApi().list('', true)) | ||||
| 		.then((items) => { | ||||
| 			for (let i = 0; i < items.length; i++) { | ||||
| 				items[i].updatedTime = updatedTime; | ||||
| 			} | ||||
| 			return items; | ||||
| 		}); | ||||
| 	} else { | ||||
| 		throw new Error('Invalid ID'); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| describe('Synchronizer syncActions', function() { | ||||
|  | ||||
| 	beforeEach(function(done) { | ||||
| 		setupDatabaseAndSynchronizer(done); | ||||
| 	}); | ||||
|  | ||||
| 	it('should create remote items', function() { | ||||
| 		let localItems = createLocalItems(1, time.unix(), 0); | ||||
| 		let remoteItems = []; | ||||
|  | ||||
| 		let actions = synchronizer().syncActions(localItems, remoteItems, []); | ||||
|  | ||||
| 		expect(actions.length).toBe(2); | ||||
| 		for (let i = 0; i < actions.length; i++) { | ||||
| 			expect(actions[i].type).toBe('create'); | ||||
| 			expect(actions[i].dest).toBe('remote'); | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	it('should update remote items', function(done) {	 | ||||
| 		createRemoteItems(1).then((remoteItems) => { | ||||
| 			let lastSyncTime = time.unix() + 1000; | ||||
| 			let localItems = createLocalItems(1, lastSyncTime + 1000, lastSyncTime); | ||||
| 			let actions = synchronizer().syncActions(localItems, remoteItems, []); | ||||
|  | ||||
| 			expect(actions.length).toBe(2); | ||||
| 			for (let i = 0; i < actions.length; i++) { | ||||
| 				expect(actions[i].type).toBe('update'); | ||||
| 				expect(actions[i].dest).toBe('remote'); | ||||
| 			} | ||||
| describe('Synchronizer', function() { | ||||
|  | ||||
| 	beforeEach( async (done) => { | ||||
| 		await setupDatabaseAndSynchronizer(); | ||||
| 		done(); | ||||
| 	}); | ||||
| 	}); | ||||
|  | ||||
| 	it('should detect conflict', function(done) { | ||||
| 		// Simulate this scenario: | ||||
| 		// - Client 1 create items | ||||
| 		// - Client 1 sync | ||||
| 		// - Client 2 sync | ||||
| 		// - Client 2 change items | ||||
| 		// - Client 2 sync | ||||
| 		// - Client 1 change items | ||||
| 		// - Client 1 sync | ||||
| 		// => Conflict | ||||
| 	it('should create remote items', async (done) => { | ||||
| 		let folder = await Folder.save({ title: "folder1" }); | ||||
| 		await Note.save({ title: "un", parent_id: folder.id }); | ||||
|  | ||||
| 		createRemoteItems(1).then((remoteItems) => { | ||||
| 			let localItems = createLocalItems(1, time.unix() + 1000, time.unix() - 1000); | ||||
| 			let actions = synchronizer().syncActions(localItems, remoteItems, []); | ||||
| 		let all = await Folder.all(true); | ||||
|  | ||||
| 			expect(actions.length).toBe(2); | ||||
| 			for (let i = 0; i < actions.length; i++) { | ||||
| 				expect(actions[i].type).toBe('conflict'); | ||||
| 		await synchronizer().start(); | ||||
|  | ||||
| 		for (let i = 0; i < all.length; i++) { | ||||
| 			let dbItem = all[i]; | ||||
| 			let path = BaseItem.systemPath(all[i]); | ||||
| 			let remote = await fileApi().stat(path); | ||||
| 			expect(!!remote).toBe(true); | ||||
| 			expect(remote.updatedTime).toBe(dbItem.updated_time); | ||||
| 		} | ||||
|  | ||||
| 		done(); | ||||
| 	}); | ||||
| 	}); | ||||
|  | ||||
|  | ||||
| 	it('should create local file', function(done) { | ||||
| 		createRemoteItems(1).then((remoteItems) => { | ||||
| 			let localItems = []; | ||||
| 			let actions = synchronizer().syncActions(localItems, remoteItems, []); | ||||
|  | ||||
| 			expect(actions.length).toBe(2); | ||||
| 			for (let i = 0; i < actions.length; i++) { | ||||
| 				expect(actions[i].type).toBe('create'); | ||||
| 				expect(actions[i].dest).toBe('local'); | ||||
| 			} | ||||
|  | ||||
| 			done(); | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('should delete remote files', function(done) { | ||||
| 		createRemoteItems(1).then((remoteItems) => { | ||||
| 			let localItems = createLocalItems(1, time.unix(), time.unix()); | ||||
| 			let deletedItemPaths = [localItems[0].path, localItems[1].path]; | ||||
| 			let actions = synchronizer().syncActions([], remoteItems, deletedItemPaths); | ||||
|  | ||||
| 			expect(actions.length).toBe(2); | ||||
| 			for (let i = 0; i < actions.length; i++) { | ||||
| 				expect(actions[i].type).toBe('delete'); | ||||
| 				expect(actions[i].dest).toBe('remote'); | ||||
| 			} | ||||
|  | ||||
| 			done(); | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('should delete local files', function(done) { | ||||
| 		let lastSyncTime = time.unix(); | ||||
| 		createRemoteItems(1, lastSyncTime - 1000).then((remoteItems) => { | ||||
| 			let localItems = createLocalItems(1, lastSyncTime - 1000, lastSyncTime); | ||||
| 			let actions = synchronizer().syncActions(localItems, [], []); | ||||
|  | ||||
| 			expect(actions.length).toBe(2); | ||||
| 			for (let i = 0; i < actions.length; i++) { | ||||
| 				expect(actions[i].type).toBe('delete'); | ||||
| 				expect(actions[i].dest).toBe('local'); | ||||
| 			} | ||||
|  | ||||
| 			done(); | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('should update local files', function(done) { | ||||
| 		let lastSyncTime = time.unix(); | ||||
| 		createRemoteItems(1, lastSyncTime + 1000).then((remoteItems) => { | ||||
| 			let localItems = createLocalItems(1, lastSyncTime - 1000, lastSyncTime); | ||||
| 			let actions = synchronizer().syncActions(localItems, remoteItems, []); | ||||
|  | ||||
| 			expect(actions.length).toBe(2); | ||||
| 			for (let i = 0; i < actions.length; i++) { | ||||
| 				expect(actions[i].type).toBe('update'); | ||||
| 				expect(actions[i].dest).toBe('local'); | ||||
| 			} | ||||
|  | ||||
| 			done(); | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| }); | ||||
|  | ||||
| describe('Synchronizer start', function() { | ||||
|  | ||||
| 	beforeEach(function(done) { | ||||
| 		setupDatabaseAndSynchronizer(done); | ||||
| 	}); | ||||
|  | ||||
| 	it('should create remote items', function(done) { | ||||
| 		createFoldersAndNotes().then(() => { | ||||
| 			return synchronizer().start(); | ||||
| 		} | ||||
| 	}).then(() => { | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| }); | ||||
|  | ||||
| // // Note: set 1 matches set 1 of createRemoteItems() | ||||
| // function createLocalItems(id, updatedTime, lastSyncTime) { | ||||
| // 	let output = []; | ||||
| // 	if (id === 1) { | ||||
| // 		output.push({ path: 'test', isDir: true, updatedTime: updatedTime, lastSyncTime: lastSyncTime }); | ||||
| // 		output.push({ path: 'test/un', updatedTime: updatedTime, lastSyncTime: lastSyncTime }); | ||||
| // 	} else { | ||||
| // 		throw new Error('Invalid ID'); | ||||
| // 	} | ||||
| // 	return output; | ||||
| // } | ||||
|  | ||||
| // function createRemoteItems(id = 1, updatedTime = null) { | ||||
| // 	if (!updatedTime) updatedTime = time.unix(); | ||||
|  | ||||
| // 	if (id === 1) { | ||||
| // 		return fileApi().format() | ||||
| // 		.then(() => fileApi().mkdir('test')) | ||||
| // 		.then(() => fileApi().put('test/un', 'abcd')) | ||||
| // 		.then(() => fileApi().list('', true)) | ||||
| // 		.then((items) => { | ||||
| // 			for (let i = 0; i < items.length; i++) { | ||||
| // 				items[i].updatedTime = updatedTime; | ||||
| // 			} | ||||
| // 			return items; | ||||
| // 		}); | ||||
| // 	} else { | ||||
| // 		throw new Error('Invalid ID'); | ||||
| // 	} | ||||
| // } | ||||
|  | ||||
| // describe('Synchronizer syncActions', function() { | ||||
|  | ||||
| // 	beforeEach(function(done) { | ||||
| // 		setupDatabaseAndSynchronizer(done); | ||||
| // 	}); | ||||
|  | ||||
| // 	it('should create remote items', function() { | ||||
| // 		let localItems = createLocalItems(1, time.unix(), 0); | ||||
| // 		let remoteItems = []; | ||||
|  | ||||
| // 		let actions = synchronizer().syncActions(localItems, remoteItems, []); | ||||
|  | ||||
| // 		expect(actions.length).toBe(2); | ||||
| // 		for (let i = 0; i < actions.length; i++) { | ||||
| // 			expect(actions[i].type).toBe('create'); | ||||
| // 			expect(actions[i].dest).toBe('remote'); | ||||
| // 		} | ||||
| // 	}); | ||||
|  | ||||
| // 	it('should update remote items', function(done) {	 | ||||
| // 		createRemoteItems(1).then((remoteItems) => { | ||||
| // 			let lastSyncTime = time.unix() + 1000; | ||||
| // 			let localItems = createLocalItems(1, lastSyncTime + 1000, lastSyncTime); | ||||
| // 			let actions = synchronizer().syncActions(localItems, remoteItems, []); | ||||
|  | ||||
| // 			expect(actions.length).toBe(2); | ||||
| // 			for (let i = 0; i < actions.length; i++) { | ||||
| // 				expect(actions[i].type).toBe('update'); | ||||
| // 				expect(actions[i].dest).toBe('remote'); | ||||
| // 			} | ||||
|  | ||||
| // 			done(); | ||||
| // 		}); | ||||
| // 	}); | ||||
|  | ||||
| // 	it('should detect conflict', function(done) { | ||||
| // 		// Simulate this scenario: | ||||
| // 		// - Client 1 create items | ||||
| // 		// - Client 1 sync | ||||
| // 		// - Client 2 sync | ||||
| // 		// - Client 2 change items | ||||
| // 		// - Client 2 sync | ||||
| // 		// - Client 1 change items | ||||
| // 		// - Client 1 sync | ||||
| // 		// => Conflict | ||||
|  | ||||
| // 		createRemoteItems(1).then((remoteItems) => { | ||||
| // 			let localItems = createLocalItems(1, time.unix() + 1000, time.unix() - 1000); | ||||
| // 			let actions = synchronizer().syncActions(localItems, remoteItems, []); | ||||
|  | ||||
| // 			expect(actions.length).toBe(2); | ||||
| // 			for (let i = 0; i < actions.length; i++) { | ||||
| // 				expect(actions[i].type).toBe('conflict'); | ||||
| // 			} | ||||
|  | ||||
| // 			done(); | ||||
| // 		}); | ||||
| // 	}); | ||||
|  | ||||
|  | ||||
| // 	it('should create local file', function(done) { | ||||
| // 		createRemoteItems(1).then((remoteItems) => { | ||||
| // 			let localItems = []; | ||||
| // 			let actions = synchronizer().syncActions(localItems, remoteItems, []); | ||||
|  | ||||
| // 			expect(actions.length).toBe(2); | ||||
| // 			for (let i = 0; i < actions.length; i++) { | ||||
| // 				expect(actions[i].type).toBe('create'); | ||||
| // 				expect(actions[i].dest).toBe('local'); | ||||
| // 			} | ||||
|  | ||||
| // 			done(); | ||||
| // 		}); | ||||
| // 	}); | ||||
|  | ||||
| // 	it('should delete remote files', function(done) { | ||||
| // 		createRemoteItems(1).then((remoteItems) => { | ||||
| // 			let localItems = createLocalItems(1, time.unix(), time.unix()); | ||||
| // 			let deletedItemPaths = [localItems[0].path, localItems[1].path]; | ||||
| // 			let actions = synchronizer().syncActions([], remoteItems, deletedItemPaths); | ||||
|  | ||||
| // 			expect(actions.length).toBe(2); | ||||
| // 			for (let i = 0; i < actions.length; i++) { | ||||
| // 				expect(actions[i].type).toBe('delete'); | ||||
| // 				expect(actions[i].dest).toBe('remote'); | ||||
| // 			} | ||||
|  | ||||
| // 			done(); | ||||
| // 		}); | ||||
| // 	}); | ||||
|  | ||||
| // 	it('should delete local files', function(done) { | ||||
| // 		let lastSyncTime = time.unix(); | ||||
| // 		createRemoteItems(1, lastSyncTime - 1000).then((remoteItems) => { | ||||
| // 			let localItems = createLocalItems(1, lastSyncTime - 1000, lastSyncTime); | ||||
| // 			let actions = synchronizer().syncActions(localItems, [], []); | ||||
|  | ||||
| // 			expect(actions.length).toBe(2); | ||||
| // 			for (let i = 0; i < actions.length; i++) { | ||||
| // 				expect(actions[i].type).toBe('delete'); | ||||
| // 				expect(actions[i].dest).toBe('local'); | ||||
| // 			} | ||||
|  | ||||
| // 			done(); | ||||
| // 		}); | ||||
| // 	}); | ||||
|  | ||||
| // 	it('should update local files', function(done) { | ||||
| // 		let lastSyncTime = time.unix(); | ||||
| // 		createRemoteItems(1, lastSyncTime + 1000).then((remoteItems) => { | ||||
| // 			let localItems = createLocalItems(1, lastSyncTime - 1000, lastSyncTime); | ||||
| // 			let actions = synchronizer().syncActions(localItems, remoteItems, []); | ||||
|  | ||||
| // 			expect(actions.length).toBe(2); | ||||
| // 			for (let i = 0; i < actions.length; i++) { | ||||
| // 				expect(actions[i].type).toBe('update'); | ||||
| // 				expect(actions[i].dest).toBe('local'); | ||||
| // 			} | ||||
|  | ||||
| // 			done(); | ||||
| // 		}); | ||||
| // 	}); | ||||
|  | ||||
| // }); | ||||
|  | ||||
| // // describe('Synchronizer start', function() { | ||||
|  | ||||
| // // 	beforeEach(function(done) { | ||||
| // // 		setupDatabaseAndSynchronizer(done); | ||||
| // // 	}); | ||||
|  | ||||
| // // 	it('should create remote items', function(done) { | ||||
| // // 		createFoldersAndNotes().then(() => { | ||||
| // // 			return synchronizer().start(); | ||||
| // // 		} | ||||
| // // 	}).then(() => { | ||||
| // // 		done(); | ||||
| // // 	}); | ||||
|  | ||||
| // // }); | ||||
|  | ||||
|   | ||||
| @@ -37,15 +37,14 @@ function setupDatabase(done) { | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| function setupDatabaseAndSynchronizer(done) { | ||||
| 	return setupDatabase().then(() => { | ||||
| async function setupDatabaseAndSynchronizer() { | ||||
| 	await setupDatabase(); | ||||
|  | ||||
| 	if (!synchronizer_) { | ||||
| 		let fileDriver = new FileApiDriverMemory(); | ||||
| 		fileApi_ = new FileApi('/root', fileDriver); | ||||
| 			synchronizer_ = new Synchronizer(db(), fileApi); | ||||
| 		synchronizer_ = new Synchronizer(db(), fileApi_); | ||||
| 	} | ||||
| 		done(); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| function db() { | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { Log } from 'src/log.js'; | ||||
| import { Database } from 'src/database.js'; | ||||
| import { uuid } from 'src/uuid.js'; | ||||
| import { time } from 'src/time-utils.js'; | ||||
|  | ||||
| class BaseModel { | ||||
|  | ||||
| @@ -99,6 +100,7 @@ class BaseModel { | ||||
| 		} | ||||
| 		if (!('trackChanges' in options)) options.trackChanges = true; | ||||
| 		if (!('isNew' in options)) options.isNew = 'auto'; | ||||
| 		if (!('autoTimestamp' in options)) options.autoTimestamp = true; | ||||
| 		return options; | ||||
| 	} | ||||
|  | ||||
| @@ -137,6 +139,7 @@ class BaseModel { | ||||
| 	static diffObjects(oldModel, newModel) { | ||||
| 		let output = {}; | ||||
| 		for (let n in newModel) { | ||||
| 			if (n == 'type_') continue; | ||||
| 			if (!newModel.hasOwnProperty(n)) continue; | ||||
| 			if (!(n in oldModel) || newModel[n] !== oldModel[n]) { | ||||
| 				output[n] = newModel[n]; | ||||
| @@ -145,9 +148,7 @@ class BaseModel { | ||||
| 		return output; | ||||
| 	} | ||||
|  | ||||
| 	static saveQuery(o, isNew = 'auto') { | ||||
| 		if (isNew == 'auto') isNew = !o.id; | ||||
|  | ||||
| 	static saveQuery(o, options) { | ||||
| 		let temp = {} | ||||
| 		let fieldNames = this.fieldNames(); | ||||
| 		for (let i = 0; i < fieldNames.length; i++) { | ||||
| @@ -156,22 +157,21 @@ class BaseModel { | ||||
| 		} | ||||
| 		o = temp; | ||||
|  | ||||
| 		let query = ''; | ||||
| 		let query = {}; | ||||
| 		let itemId = o.id; | ||||
|  | ||||
| 		if (!o.updated_time && this.hasField('updated_time')) { | ||||
| 			o.updated_time = Math.round((new Date()).getTime() / 1000); | ||||
| 		if (options.autoTimestamp && this.hasField('updated_time')) { | ||||
| 			o.updated_time = time.unix(); | ||||
| 		} | ||||
|  | ||||
| 		if (isNew) { | ||||
| 		if (options.isNew) { | ||||
| 			if (this.useUuid() && !o.id) { | ||||
| 				//o = Object.assign({}, o); | ||||
| 				itemId = uuid.create(); | ||||
| 				o.id = itemId; | ||||
| 			} | ||||
|  | ||||
| 			if (!o.created_time && this.hasField('created_time')) { | ||||
| 				o.created_time = Math.round((new Date()).getTime() / 1000); | ||||
| 				o.created_time = time.unix(); | ||||
| 			} | ||||
|  | ||||
| 			query = Database.insertQuery(this.tableName(), o); | ||||
| @@ -192,10 +192,10 @@ class BaseModel { | ||||
| 	static save(o, options = null) { | ||||
| 		options = this.modOptions(options); | ||||
|  | ||||
| 		let isNew = options.isNew == 'auto' ? !o.id : options.isNew; | ||||
| 		options.isNew = options.isNew == 'auto' ? !o.id : options.isNew; | ||||
|  | ||||
| 		let queries = []; | ||||
| 		let saveQuery = this.saveQuery(o, isNew); | ||||
| 		let saveQuery = this.saveQuery(o, options); | ||||
| 		let itemId = saveQuery.id; | ||||
|  | ||||
| 		queries.push(saveQuery); | ||||
|   | ||||
| @@ -253,6 +253,8 @@ class Database { | ||||
| 	} | ||||
|  | ||||
| 	static insertQuery(tableName, data) { | ||||
| 		if (!data || !Object.keys(data).length) throw new Error('Data is empty'); | ||||
|  | ||||
| 		let keySql= ''; | ||||
| 		let valueSql = ''; | ||||
| 		let params = []; | ||||
| @@ -271,6 +273,8 @@ class Database { | ||||
| 	} | ||||
|  | ||||
| 	static updateQuery(tableName, data, where) { | ||||
| 		if (!data || !Object.keys(data).length) throw new Error('Data is empty'); | ||||
|  | ||||
| 		let sql = ''; | ||||
| 		let params = []; | ||||
| 		for (let key in data) { | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import { time } from 'src/time-utils.js'; | ||||
|  | ||||
| class FileApiDriverMemory { | ||||
|  | ||||
| 	constructor(baseDir) { | ||||
| @@ -21,23 +23,23 @@ class FileApiDriverMemory { | ||||
| 	} | ||||
|  | ||||
| 	newItem(path, isDir = false) { | ||||
| 		let now = time.unix(); | ||||
| 		return { | ||||
| 			path: path, | ||||
| 			isDir: isDir, | ||||
| 			updatedTime: this.currentTimestamp(), | ||||
| 			createdTime: this.currentTimestamp(), | ||||
| 			updatedTime: now, | ||||
| 			createdTime: now, | ||||
| 			content: '', | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	stat(path) { | ||||
| 		let item = this.itemIndexByPath(path); | ||||
| 		if (!item) return Promise.reject(new Error('File not found: ' + path)); | ||||
| 		return Promise.resolve(item); | ||||
| 		let item = this.itemByPath(path); | ||||
| 		return Promise.resolve(item ? Object.assign({}, item) : null); | ||||
| 	} | ||||
|  | ||||
| 	setTimestamp(path, timestamp) { | ||||
| 		let item = this.itemIndexByPath(path); | ||||
| 		let item = this.itemByPath(path); | ||||
| 		if (!item) return Promise.reject(new Error('File not found: ' + path)); | ||||
| 		item.updatedTime = timestamp; | ||||
| 		return Promise.resolve(); | ||||
| @@ -53,7 +55,6 @@ class FileApiDriverMemory { | ||||
| 				let s = item.path.substr(path.length + 1); | ||||
| 				if (s.split('/').length === 1) { | ||||
| 					let it = Object.assign({}, item); | ||||
| 					it.path = it.path.substr(path.length + 1); | ||||
| 					output.push(it); | ||||
| 				} | ||||
| 			} | ||||
| @@ -64,7 +65,7 @@ class FileApiDriverMemory { | ||||
|  | ||||
| 	get(path) { | ||||
| 		let item = this.itemByPath(path); | ||||
| 		if (!item) return Promise.reject(new Error('File not found: ' + path)); | ||||
| 		if (!item) return Promise.resolve(null); | ||||
| 		if (item.isDir) return Promise.reject(new Error(path + ' is a directory, not a file')); | ||||
| 		return Promise.resolve(item.content); | ||||
| 	} | ||||
| @@ -84,6 +85,7 @@ class FileApiDriverMemory { | ||||
| 			this.items_.push(item); | ||||
| 		} else { | ||||
| 			this.items_[index].content = content; | ||||
| 			this.items_[index].updatedTime = time.unix(); | ||||
| 		} | ||||
| 		return Promise.resolve(); | ||||
| 	} | ||||
|   | ||||
| @@ -37,34 +37,38 @@ class FileApi { | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	list(path = '', recursive = false, context = null) { | ||||
| 		let fullPath = this.fullPath_(path); | ||||
| 		return this.driver_.list(fullPath).then((items) => { | ||||
| 			items = this.scopeItemsToBaseDir_(items); | ||||
| 			if (recursive) { | ||||
| 				let chain = []; | ||||
| 				for (let i = 0; i < items.length; i++) { | ||||
| 					let item = items[i]; | ||||
| 					if (!item.isDir) continue; | ||||
| 	list() { | ||||
| 		return this.driver_.list(this.baseDir_).then((items) => { | ||||
| 			return this.scopeItemsToBaseDir_(items); | ||||
| 		}); | ||||
| 		// let fullPath = this.fullPath_(path); | ||||
| 		// return this.driver_.list(fullPath).then((items) => { | ||||
| 		// 	return items; | ||||
| 		// 	// items = this.scopeItemsToBaseDir_(items); | ||||
| 		// 	// if (recursive) { | ||||
| 		// 	// 	let chain = []; | ||||
| 		// 	// 	for (let i = 0; i < items.length; i++) { | ||||
| 		// 	// 		let item = items[i]; | ||||
| 		// 	// 		if (!item.isDir) continue; | ||||
|  | ||||
| 					chain.push(() => { | ||||
| 						return this.list(item.path, true).then((children) => { | ||||
| 							for (let j = 0; j < children.length; j++) { | ||||
| 								let md = children[j]; | ||||
| 								md.path = item.path + '/' + md.path;  | ||||
| 								items.push(md); | ||||
| 							} | ||||
| 						}); | ||||
| 					}); | ||||
| 				} | ||||
| 		// 	// 		chain.push(() => { | ||||
| 		// 	// 			return this.list(item.path, true).then((children) => { | ||||
| 		// 	// 				for (let j = 0; j < children.length; j++) { | ||||
| 		// 	// 					let md = children[j]; | ||||
| 		// 	// 					md.path = item.path + '/' + md.path;  | ||||
| 		// 	// 					items.push(md); | ||||
| 		// 	// 				} | ||||
| 		// 	// 			}); | ||||
| 		// 	// 		}); | ||||
| 		// 	// 	} | ||||
|  | ||||
| 				return promiseChain(chain).then(() => { | ||||
| 					return items; | ||||
| 				}); | ||||
| 			} else { | ||||
| 				return items; | ||||
| 			} | ||||
| 		}); | ||||
| 		// 	// 	return promiseChain(chain).then(() => { | ||||
| 		// 	// 		return items; | ||||
| 		// 	// 	}); | ||||
| 		// 	// } else { | ||||
| 		// 	// 	return items; | ||||
| 		// 	// } | ||||
| 		// }); | ||||
| 	} | ||||
|  | ||||
| 	setTimestamp(path, timestamp) { | ||||
| @@ -77,7 +81,7 @@ class FileApi { | ||||
| 	} | ||||
|  | ||||
| 	stat(path) { | ||||
| 		console.info('stat ' + path); | ||||
| 		//console.info('stat ' + path); | ||||
| 		return this.driver_.stat(this.fullPath_(path)).then((output) => { | ||||
| 			if (!output) return output; | ||||
| 			output.path = path; | ||||
| @@ -86,12 +90,12 @@ class FileApi { | ||||
| 	} | ||||
|  | ||||
| 	get(path) { | ||||
| 		console.info('get ' + path); | ||||
| 		//console.info('get ' + path); | ||||
| 		return this.driver_.get(this.fullPath_(path)); | ||||
| 	} | ||||
|  | ||||
| 	put(path, content) { | ||||
| 		console.info('put ' + path); | ||||
| 		//console.info('put ' + path); | ||||
| 		return this.driver_.put(this.fullPath_(path), content); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -74,9 +74,20 @@ class Folder extends BaseItem { | ||||
| 		//return this.db().selectOne('SELECT * FROM notes WHERE `parent_id` = ? AND `' + field + '` = ?', [folderId, value]); | ||||
| 	} | ||||
|  | ||||
| 	static all() { | ||||
| 		return this.modelSelectAll('SELECT * FROM folders'); | ||||
| 		// return this.db().selectAll('SELECT * FROM folders'); | ||||
| 	static async all(includeNotes = false) { | ||||
| 		let folders = await this.modelSelectAll('SELECT * FROM folders'); | ||||
| 		if (!includeNotes) return folders; | ||||
|  | ||||
| 		let output = []; | ||||
| 		for (let i = 0; i < folders.length; i++) { | ||||
| 			let folder = folders[i]; | ||||
| 			let notes = await Note.all(folder.id); | ||||
| 			output.push(folder); | ||||
| 			output = output.concat(notes); | ||||
| 		} | ||||
|  | ||||
| 		return output; | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	static save(o, options = null) { | ||||
|   | ||||
| @@ -69,6 +69,10 @@ class Note extends BaseItem { | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	static all(parentId) { | ||||
| 		return this.modelSelectAll('SELECT * FROM notes WHERE parent_id = ?', [parentId]); | ||||
| 	} | ||||
|  | ||||
| 	static save(o, options = null) { | ||||
| 		return super.save(o, options).then((result) => { | ||||
| 			// 'result' could be a partial one at this point (if, for example, only one property of it was saved) | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import { Registry } from 'src/registry.js'; | ||||
|  | ||||
| class NoteFolderService extends BaseService { | ||||
|  | ||||
| 	static save(type, item, oldItem) { | ||||
| 	static save(type, item, oldItem, options = null) { | ||||
| 		let diff = null; | ||||
| 		if (oldItem) { | ||||
| 			diff = BaseModel.diffObjects(oldItem, item); | ||||
| @@ -32,7 +32,9 @@ class NoteFolderService extends BaseService { | ||||
| 			toSave.id = item.id; | ||||
| 		} | ||||
|  | ||||
| 		return ItemClass.save(toSave).then((savedItem) => { | ||||
| 		console.info(toSave); | ||||
|  | ||||
| 		return ItemClass.save(toSave, options).then((savedItem) => { | ||||
| 			output = Object.assign(item, savedItem); | ||||
| 			if (isNew && type == 'note') return Note.updateGeolocation(output.id); | ||||
| 		}).then(() => { | ||||
|   | ||||
| @@ -161,22 +161,21 @@ class Synchronizer { | ||||
|  | ||||
| 			if (!remote) { | ||||
| 				if (local.syncTime) { | ||||
| 					// The item has been synced previously and now is no longer in the dest | ||||
| 					// which means it has been deleted. | ||||
| 					action.type = 'delete'; | ||||
| 					action.dest = 'local'; | ||||
| 					action.reason = 'Local item has been synced to remote previously, but remote no longer exist, which means it has been deleted'; | ||||
| 				} else { | ||||
| 					// The item has never been synced and is not present in the dest | ||||
| 					// which means it is new | ||||
| 					action.type = 'create'; | ||||
| 					action.dest = 'remote'; | ||||
| 					action.reason = 'Local item has never been synced to remote, and remote does not exists, which means it is new'; | ||||
| 				} | ||||
| 			} else { | ||||
| 				if (this.itemIsStrictlyOlderThan(local, local.syncTime)) continue; | ||||
|  | ||||
| 				if (this.itemIsStrictlyOlderThan(remote, local.syncTime)) { | ||||
| 				if (this.itemIsStrictlyOlderThan(remote, local.updatedTime)) { | ||||
| 					action.type = 'update'; | ||||
| 					action.dest = 'remote'; | ||||
| 					action.reason = sprintf('Remote (%s) was modified after last sync of local (%s).', moment.unix(remote.updatedTime).toISOString(), moment.unix(local.syncTime).toISOString(),); | ||||
| 				} else if (this.itemIsStrictlyNewerThan(remote, local.syncTime)) { | ||||
| 					action.type = 'conflict'; | ||||
| 					action.reason = sprintf('Both remote (%s) and local items (%s) were modified after the last sync (%s).', | ||||
| @@ -186,10 +185,6 @@ class Synchronizer { | ||||
| 					); | ||||
|  | ||||
| 					if (local.type == 'folder') { | ||||
| 						// For folders, currently we don't completely handle conflicts, we just | ||||
| 						// we just update the local dir (.folder metadata file) with the remote | ||||
| 						// version. It means the local version is lost but shouldn't be a big deal | ||||
| 						// and should be rare (at worst, the folder name needs to renamed). | ||||
| 						action.solution = [ | ||||
| 							{ type: 'update', dest: 'local' }, | ||||
| 						]; | ||||
| @@ -230,10 +225,20 @@ class Synchronizer { | ||||
| 				} | ||||
| 			} else { | ||||
| 				if (this.itemIsStrictlyOlderThan(remote, local.syncTime)) continue; // Already have this version | ||||
|  | ||||
| 				// Note: no conflict is possible here since if the local item has been | ||||
| 				// modified since the last sync, it's been processed in the previous loop. | ||||
| 				// So throw an exception is this normally impossible condition happens anyway. | ||||
| 				// It's handled at condition this.itemIsStrictlyNewerThan(remote, local.syncTime) in above loop | ||||
| 				if (this.itemIsStrictlyNewerThan(remote, local.syncTime)) throw new Error('Remote item cannot be newer than last sync time.'); | ||||
|  | ||||
| 				if (this.itemIsStrictlyNewerThan(remote, local.updatedTime)) { | ||||
| 					action.type = 'update'; | ||||
| 					action.dest = 'local'; | ||||
| 					action.reason = sprintf('Remote (%s) was modified after last sync of local (%s).', moment.unix(remote.updatedTime).toISOString(), moment.unix(local.syncTime).toISOString(),);; | ||||
| 				} else { | ||||
| 					continue; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			output.push(action); | ||||
| @@ -268,7 +273,7 @@ class Synchronizer { | ||||
|  | ||||
| 		if (!action) return Promise.resolve(); | ||||
|  | ||||
| 		console.info('Sync action: ' + action.type + ' ' + action.dest); | ||||
| 		console.info('Sync action: ' + action.type + ' ' + action.dest + ': ' + action.reason); | ||||
|  | ||||
| 		if (action.type == 'conflict') { | ||||
| 			console.info(action); | ||||
| @@ -293,10 +298,11 @@ class Synchronizer { | ||||
| 				} else { | ||||
| 					let dbItem = syncItem.remoteItem.content; | ||||
| 					dbItem.sync_time = time.unix(); | ||||
| 					dbItem.updated_time = dbItem.sync_time; | ||||
| 					if (syncItem.type == 'folder') { | ||||
| 						return Folder.save(dbItem, { isNew: true }); | ||||
| 						return Folder.save(dbItem, { isNew: true, autoTimestamp: false }); | ||||
| 					} else { | ||||
| 						return Note.save(dbItem, { isNew: true }); | ||||
| 						return Note.save(dbItem, { isNew: true, autoTimestamp: false }); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| @@ -317,7 +323,8 @@ class Synchronizer { | ||||
| 				} else { | ||||
| 					let dbItem = syncItem.remoteItem.content; | ||||
| 					dbItem.sync_time = time.unix(); | ||||
| 					return NoteFolderService.save(syncItem.type, dbItem, action.local.dbItem); | ||||
| 					dbItem.updated_time = dbItem.sync_time; | ||||
| 					return NoteFolderService.save(syncItem.type, dbItem, action.local.dbItem, { autoTimestamp: false }); | ||||
| 					// let dbItem = syncItem.remoteItem.content; | ||||
| 					// dbItem.sync_time = time.unix(); | ||||
| 					// if (syncItem.type == 'folder') { | ||||
| @@ -349,6 +356,7 @@ class Synchronizer { | ||||
|  | ||||
| 	async processRemoteItem(remoteItem) { | ||||
| 		let content = await this.api().get(remoteItem.path); | ||||
| 		if (!content) throw new Error('Cannot get content for: ' + remoteItem.path); | ||||
| 		remoteItem.content = Note.fromFriendlyString(content); | ||||
| 		let remoteSyncItem = this.remoteItemToSyncItem(remoteItem); | ||||
|  | ||||
| @@ -362,6 +370,7 @@ class Synchronizer { | ||||
| 	async processState_uploadChanges() { | ||||
| 		while (true) { | ||||
| 			let result = await NoteFolderService.itemsThatNeedSync(50); | ||||
| 			console.info('Items that need sync: ' + result.items.length); | ||||
| 			for (let i = 0; i < result.items.length; i++) { | ||||
| 				let item = result.items[i]; | ||||
| 				await this.processLocalItem(item); | ||||
| @@ -370,6 +379,8 @@ class Synchronizer { | ||||
| 			if (!result.hasMore) break; | ||||
| 		} | ||||
|  | ||||
| 		//console.info('DOWNLOAD CHANGE DISABLED'); return Promise.resolve();  | ||||
| 		 | ||||
| 		return this.processState('downloadChanges'); | ||||
| 	} | ||||
|  | ||||
| @@ -396,7 +407,10 @@ class Synchronizer { | ||||
| 		// 	return; | ||||
| 		// } | ||||
|  | ||||
| 		return this.processState('uploadChanges'); | ||||
| 		return this.processState('uploadChanges').catch((error) => { | ||||
| 			console.info('Synchronizer error:', error); | ||||
| 			throw error; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user