You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Test sync
This commit is contained in:
		
							
								
								
									
										3
									
								
								CliClient/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								CliClient/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,5 @@ | |||||||
| build/ | build/ | ||||||
| node_modules/ | node_modules/ | ||||||
| app/src | app/src | ||||||
| spec-build/ | tests-build/ | ||||||
|  | tests/src | ||||||
| @@ -33,6 +33,6 @@ | |||||||
|     "babelbuild": "babel app -d build", |     "babelbuild": "babel app -d build", | ||||||
|     "build": "babel-changed app -d build --source-maps && babel-changed app/src/models -d build/src/models --source-maps && babel-changed app/src/services -d build/src/services --source-maps", |     "build": "babel-changed app -d build --source-maps && babel-changed app/src/models -d build/src/models --source-maps && babel-changed app/src/services -d build/src/services --source-maps", | ||||||
|     "clean": "babel-changed --reset", |     "clean": "babel-changed --reset", | ||||||
|     "test": "babel-changed spec -d spec-build --source-maps && jasmine" |     "test": "babel-changed tests -d tests-build --source-maps && jasmine" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| #!/bin/bash | #!/bin/bash | ||||||
| CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" | CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" | ||||||
|  |  | ||||||
| rm -f "$CLIENT_DIR/spec-build/src" | rm -f "$CLIENT_DIR/tests-build/src" | ||||||
| ln -s "$CLIENT_DIR/build/src" "$CLIENT_DIR/spec-build" | mkdir -p "$CLIENT_DIR/tests-build" | ||||||
|  | ln -s "$CLIENT_DIR/build/src" "$CLIENT_DIR/tests-build" | ||||||
|  |  | ||||||
| npm build && NODE_PATH="$CLIENT_DIR/spec-build/" npm test spec-build/synchronizer.js | npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/synchronizer.js | ||||||
| @@ -1 +0,0 @@ | |||||||
| /home/laurent/src/notes/CliClient/../ReactNativeClient/src |  | ||||||
| @@ -1,32 +0,0 @@ | |||||||
| import { Synchronizer } from 'src/synchronizer.js'; |  | ||||||
| import { FileApi } from 'src/file-api.js'; |  | ||||||
| import { FileApiDriverMemory } from 'src/file-api-driver-memory.js'; |  | ||||||
|  |  | ||||||
| describe("syncActions", function() { |  | ||||||
|  |  | ||||||
| 	let fileDriver = new FileApiDriverMemory(); |  | ||||||
| 	let fileApi = new FileApi('/root', fileDriver); |  | ||||||
| 	let synchronizer = new Synchronizer(null, fileApi); |  | ||||||
|  |  | ||||||
| 	it("and so is a spec", function() { |  | ||||||
| 		let localItems = []; |  | ||||||
| 		localItems.push({ path: 'test', isDir: true, updatedTime: 1497370000 }); |  | ||||||
| 		localItems.push({ path: 'test/un', updatedTime: 1497370000 }); |  | ||||||
| 		localItems.push({ path: 'test/deux', updatedTime: 1497370000 }); |  | ||||||
|  |  | ||||||
| 		let remoteItems = []; |  | ||||||
|  |  | ||||||
| 		let actions = synchronizer.syncActions(localItems, remoteItems, 0); |  | ||||||
|  |  | ||||||
| 		expect(actions.length).toBe(3); |  | ||||||
| 		 |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 		// synchronizer.format(); |  | ||||||
| 		// synchronizer.mkdir('test'); |  | ||||||
| 		// synchronizer.touch('test/un'); |  | ||||||
| 		// synchronizer.touch('test/deux'); |  | ||||||
| 		// synchronizer.touch('test/trois'); |  | ||||||
| 	}); |  | ||||||
| }); |  | ||||||
							
								
								
									
										160
									
								
								CliClient/tests/synchronizer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								CliClient/tests/synchronizer.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,160 @@ | |||||||
|  | import { Synchronizer } from 'src/synchronizer.js'; | ||||||
|  | import { FileApi } from 'src/file-api.js'; | ||||||
|  | import { FileApiDriverMemory } from 'src/file-api-driver-memory.js'; | ||||||
|  | import { time } from 'src/time-utils.js'; | ||||||
|  |  | ||||||
|  | describe('Synchronizer syncActions', function() { | ||||||
|  |  | ||||||
|  | 	let fileDriver = new FileApiDriverMemory(); | ||||||
|  | 	let fileApi = new FileApi('/root', fileDriver); | ||||||
|  | 	let synchronizer = new Synchronizer(null, fileApi); | ||||||
|  |  | ||||||
|  | 	// 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'); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	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(); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | }); | ||||||
| @@ -96,35 +96,103 @@ class Synchronizer { | |||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	itemIsSameDate(item, date) { | ||||||
|  | 		return Math.abs(item.updatedTime - date) <= 1; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	itemIsNewerThan(item, date) { | 	itemIsNewerThan(item, date) { | ||||||
|  | 		if (this.itemIsSameDate(item, date)) return false; | ||||||
| 		return item.updatedTime > date; | 		return item.updatedTime > date; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	itemIsOlderThan(item, date) { | 	itemIsOlderThan(item, date) { | ||||||
| 		return !this.itemIsNewerThan(item, date); | 		if (this.itemIsSameDate(item, date)) return false; | ||||||
|  | 		return item.updatedTime < date; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	syncActions(localItems, remoteItems, lastSyncTime) { | 	// Assumption: it's not possible to, for example, have a directory one the dest | ||||||
|  | 	// and a file with the same name on the source. It's not possible because the | ||||||
|  | 	// file and directory names are UUID so should be unique. | ||||||
|  | 	syncActions(localItems, remoteItems, deletedLocalPaths) { | ||||||
| 		let output = []; | 		let output = []; | ||||||
|  | 		let donePaths = []; | ||||||
|  |  | ||||||
| 		for (let i = 0; i < localItems.length; i++) { | 		for (let i = 0; i < localItems.length; i++) { | ||||||
| 			let item = localItems[i]; | 			let local = localItems[i]; | ||||||
| 			let remoteItem = this.itemByPath(remoteItems, item.path); | 			let remote = this.itemByPath(remoteItems, local.path); | ||||||
|  |  | ||||||
| 			let action = { | 			let action = { | ||||||
| 				localItem: item, | 				local: local, | ||||||
| 				remoteItem: remoteItem, | 				remote: remote, | ||||||
| 			}; | 			}; | ||||||
| 			if (!remoteItem) { |  | ||||||
| 				action.type = 'create'; | 			if (!remote) { | ||||||
| 				action.where = 'there'; | 				if (local.lastSyncTime) { | ||||||
| 			} else { | 					// The item has been synced previously and now is no longer in the dest | ||||||
| 				if (this.itemIsOlderThan(remoteItem, lastSyncTime)) { | 					// which means it has been deleted. | ||||||
| 					action.type = 'update'; | 					action.type = 'delete'; | ||||||
| 					action.where = 'there'; | 					action.dest = 'local'; | ||||||
| 				} else { | 				} else { | ||||||
| 					action.type = 'conflict'; // Move local to /Conflict; Copy remote here | 					// The item has never been synced and is not present in the dest | ||||||
| 					action.where = 'here'; | 					// which means it is new | ||||||
|  | 					action.type = 'create'; | ||||||
|  | 					action.dest = 'remote'; | ||||||
| 				} | 				} | ||||||
|  | 			} else { | ||||||
|  | 				if (this.itemIsOlderThan(local, local.lastSyncTime)) continue; | ||||||
|  |  | ||||||
|  | 				if (this.itemIsOlderThan(remote, local.lastSyncTime)) { | ||||||
|  | 					action.type = 'update'; | ||||||
|  | 					action.dest = 'remote'; | ||||||
|  | 				} else { | ||||||
|  | 					action.type = 'conflict'; | ||||||
|  | 					if (local.isDir) { | ||||||
|  | 						// 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' }, | ||||||
|  | 						]; | ||||||
|  | 					} else { | ||||||
|  | 						action.solution = [ | ||||||
|  | 							{ type: 'copy-to-remote-conflict-dir', dest: 'local' }, | ||||||
|  | 							{ type: 'copy-to-local-conflict-dir', dest: 'local' }, | ||||||
|  | 							{ type: 'update', dest: 'local' }, | ||||||
|  | 						]; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			donePaths.push(local.path); | ||||||
|  |  | ||||||
|  | 			output.push(action); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for (let i = 0; i < remoteItems.length; i++) { | ||||||
|  | 			let remote = remoteItems[i]; | ||||||
|  | 			if (donePaths.indexOf(remote.path) >= 0) continue; // Already handled in the previous loop | ||||||
|  | 			let local = this.itemByPath(localItems, remote.path); | ||||||
|  |  | ||||||
|  | 			let action = { | ||||||
|  | 				local: local, | ||||||
|  | 				remote: remote, | ||||||
|  | 			}; | ||||||
|  |  | ||||||
|  | 			if (!local) { | ||||||
|  | 				if (deletedLocalPaths.indexOf(remote.path) >= 0) { | ||||||
|  | 					action.type = 'delete'; | ||||||
|  | 					action.dest = 'remote'; | ||||||
|  | 				} else { | ||||||
|  | 					action.type = 'create'; | ||||||
|  | 					action.dest = 'local'; | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				if (this.itemIsOlderThan(remote, local.lastSyncTime)) 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. | ||||||
|  | 				action.type = 'update'; | ||||||
|  | 				action.dest = 'local'; | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			output.push(action); | 			output.push(action); | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								ReactNativeClient/src/time-utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								ReactNativeClient/src/time-utils.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | let time = { | ||||||
|  |  | ||||||
|  | 	unix() { | ||||||
|  | 		return Math.round((new Date()).getTime() / 1000); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { time }; | ||||||
| @@ -11,8 +11,9 @@ | |||||||
| 				"app/data/uploads", | 				"app/data/uploads", | ||||||
| 				"CliClient/node_modules", | 				"CliClient/node_modules", | ||||||
| 				"CliClient/build", | 				"CliClient/build", | ||||||
| 				"CliClient/spec-build", | 				"CliClient/tests-build", | ||||||
| 				"CliClient/app/src", | 				"CliClient/app/src", | ||||||
|  | 				"CliClient/tests/src", | ||||||
| 				"ReactNativeClient/node_modules", | 				"ReactNativeClient/node_modules", | ||||||
| 				"ReactNativeClient/android/app/build", | 				"ReactNativeClient/android/app/build", | ||||||
| 				"ReactNativeClient/android/build", | 				"ReactNativeClient/android/build", | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user