You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	refactored sync class
This commit is contained in:
		| @@ -463,9 +463,9 @@ async function createLocalItems() { | ||||
| // 				} | ||||
|  | ||||
| // 				if (!currentFolder) { | ||||
| // 					this.log(Folder.toFriendlyString(item)); | ||||
| // 					this.log(Folder.serialize(item)); | ||||
| // 				} else { | ||||
| // 					this.log(Note.toFriendlyString(item)); | ||||
| // 					this.log(Note.serialize(item)); | ||||
| // 				} | ||||
| // 			}).catch((error) => { | ||||
| // 				this.log(error); | ||||
|   | ||||
| @@ -527,7 +527,7 @@ function saveNoteToWebApi(note) { | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| function noteToFriendlyString_format(propName, propValue) { | ||||
| function noteserialize_format(propName, propValue) { | ||||
| 	if (['created_time', 'updated_time'].indexOf(propName) >= 0) { | ||||
| 		if (!propValue) return ''; | ||||
| 		propValue = moment.unix(propValue).format('YYYY-MM-DD hh:mm:ss'); | ||||
| @@ -538,7 +538,7 @@ function noteToFriendlyString_format(propName, propValue) { | ||||
| 	return propValue; | ||||
| } | ||||
|  | ||||
| function noteToFriendlyString(note) { | ||||
| function noteserialize(note) { | ||||
| 	let shownKeys = ["author", "longitude", "latitude", "is_todo", "todo_due", "todo_completed", 'created_time', 'updated_time']; | ||||
| 	let output = []; | ||||
|  | ||||
| @@ -548,7 +548,7 @@ function noteToFriendlyString(note) { | ||||
| 	output.push(''); | ||||
| 	for (let i = 0; i < shownKeys.length; i++) { | ||||
| 		let v = note[shownKeys[i]]; | ||||
| 		v = noteToFriendlyString_format(shownKeys[i], v); | ||||
| 		v = noteserialize_format(shownKeys[i], v); | ||||
| 		output.push(shownKeys[i] + ': ' + v); | ||||
| 	} | ||||
|  | ||||
| @@ -623,7 +623,7 @@ const baseNoteDir = '/home/laurent/Temp/TestImport'; | ||||
| // }); | ||||
|  | ||||
| function saveNoteToDisk(folder, note) { | ||||
| 	const noteContent = noteToFriendlyString(note); | ||||
| 	const noteContent = noteserialize(note); | ||||
| 	const notePath = baseNoteDir + '/' + folderFilename(folder) + '/' + noteFilename(note); | ||||
|  | ||||
| 	// console.info('==================================================='); | ||||
| @@ -694,7 +694,7 @@ function importEnex(parentFolder, stream) { | ||||
|  | ||||
| 						saveNoteToDisk(parentFolder, note); | ||||
|  | ||||
| 						// console.info(noteToFriendlyString(note)); | ||||
| 						// console.info(noteserialize(note)); | ||||
| 						// console.info('========================================================================================================================='); | ||||
|  | ||||
| 						//saveNoteToWebApi(note); | ||||
|   | ||||
| @@ -6,8 +6,16 @@ import { Note } from 'src/models/note.js'; | ||||
| import { BaseItem } from 'src/models/base-item.js'; | ||||
| import { BaseModel } from 'src/base-model.js'; | ||||
|  | ||||
| process.on('unhandledRejection', (reason, p) => { | ||||
| 	console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); | ||||
| 	// application specific logging, throwing an error, or other logic here | ||||
| }); | ||||
|  | ||||
| async function localItemsSameAsRemote(locals, expect) { | ||||
| 	try { | ||||
| 		let files = await fileApi().list(); | ||||
| 		expect(locals.length).toBe(files.length); | ||||
|  | ||||
| 		for (let i = 0; i < locals.length; i++) { | ||||
| 			let dbItem = locals[i]; | ||||
| 			let path = BaseItem.systemPath(dbItem); | ||||
| @@ -19,10 +27,10 @@ async function localItemsSameAsRemote(locals, expect) { | ||||
| 			// console.info('======================='); | ||||
|  | ||||
| 			expect(!!remote).toBe(true); | ||||
| 			expect(remote.updatedTime).toBe(dbItem.updated_time); | ||||
| 			expect(remote.updated_time).toBe(dbItem.updated_time); | ||||
|  | ||||
| 			let remoteContent = await fileApi().get(path); | ||||
| 			remoteContent = dbItem.type_ == BaseModel.ITEM_TYPE_NOTE ? Note.fromFriendlyString(remoteContent) : Folder.fromFriendlyString(remoteContent); | ||||
| 			remoteContent = dbItem.type_ == BaseModel.ITEM_TYPE_NOTE ? Note.unserialize(remoteContent) : Folder.unserialize(remoteContent); | ||||
| 			expect(remoteContent.title).toBe(dbItem.title); | ||||
| 		} | ||||
| 	} catch (error) { | ||||
| @@ -39,94 +47,72 @@ describe('Synchronizer', function() { | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| 	// it('should create remote items', async (done) => { | ||||
| 	// 	let folder = await Folder.save({ title: "folder1" }); | ||||
| 	// 	await Note.save({ title: "un", parent_id: folder.id }); | ||||
| 	it('should create remote items', async (done) => { | ||||
| 		let folder = await Folder.save({ title: "folder1" }); | ||||
| 		await Note.save({ title: "un", parent_id: folder.id }); | ||||
|  | ||||
| 	// 	let all = await Folder.all(true); | ||||
| 		let all = await Folder.all(true); | ||||
|  | ||||
| 	// 	await synchronizer().start(); | ||||
| 		await synchronizer().start(); | ||||
|  | ||||
| 	// 	await localItemsSameAsRemote(all, expect); | ||||
| 		await localItemsSameAsRemote(all, expect); | ||||
|  | ||||
| 	// 	done(); | ||||
| 	// }); | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| 	// it('should update remote item', async (done) => { | ||||
| 	// 	let folder = await Folder.save({ title: "folder1" }); | ||||
| 	// 	let note = await Note.save({ title: "un", parent_id: folder.id }); | ||||
| 	it('should update remote item', async (done) => { | ||||
| 		let folder = await Folder.save({ title: "folder1" }); | ||||
| 		let note = await Note.save({ title: "un", parent_id: folder.id }); | ||||
| 		await synchronizer().start(); | ||||
|  | ||||
| 	// 	await sleep(1); | ||||
| 		await sleep(1); | ||||
|  | ||||
| 	// 	await Note.save({ title: "un UPDATE", id: note.id }); | ||||
| 		await Note.save({ title: "un UPDATE", id: note.id }); | ||||
|  | ||||
| 	// 	let all = await Folder.all(true); | ||||
| 	// 	await synchronizer().start(); | ||||
| 		let all = await Folder.all(true); | ||||
| 		await synchronizer().start(); | ||||
|  | ||||
| 	// 	await localItemsSameAsRemote(all, expect); | ||||
| 		await localItemsSameAsRemote(all, expect); | ||||
|  | ||||
| 	// 	done(); | ||||
| 	// }); | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| 	// it('should create local items', async (done) => { | ||||
| 	// 	let folder = await Folder.save({ title: "folder1" }); | ||||
| 	// 	await Note.save({ title: "un", parent_id: folder.id }); | ||||
| 	// 	await synchronizer().start(); | ||||
| 	// 	await clearDatabase(); | ||||
| 	// 	await synchronizer().start(); | ||||
| 	it('should create local items', async (done) => { | ||||
| 		let folder = await Folder.save({ title: "folder1" }); | ||||
| 		await Note.save({ title: "un", parent_id: folder.id }); | ||||
| 		await synchronizer().start(); | ||||
|  | ||||
| 	// 	let all = await Folder.all(true); | ||||
| 	// 	await localItemsSameAsRemote(all, expect); | ||||
| 		switchClient(2); | ||||
|  | ||||
| 	// 	done(); | ||||
| 	// }); | ||||
| 		await synchronizer().start(); | ||||
|  | ||||
| 	// it('should create same items on client 2', async (done) => { | ||||
| 	// 	let folder = await Folder.save({ title: "folder1" }); | ||||
| 	// 	let note = await Note.save({ title: "un", parent_id: folder.id }); | ||||
| 	// 	await synchronizer().start(); | ||||
| 		let all = await Folder.all(true); | ||||
| 		await localItemsSameAsRemote(all, expect); | ||||
|  | ||||
| 	// 	await sleep(1); | ||||
|  | ||||
| 	// 	switchClient(2); | ||||
|  | ||||
| 	// 	await synchronizer().start(); | ||||
|  | ||||
| 	// 	let folder2 = await Folder.load(folder.id); | ||||
| 	// 	let note2 = await Note.load(note.id); | ||||
|  | ||||
| 	// 	expect(!!folder2).toBe(true); | ||||
| 	// 	expect(!!note2).toBe(true); | ||||
|  | ||||
| 	// 	expect(folder.title).toBe(folder.title); | ||||
| 	// 	expect(folder.updated_time).toBe(folder.updated_time); | ||||
|  | ||||
| 	// 	expect(note.title).toBe(note.title); | ||||
| 	// 	expect(note.updated_time).toBe(note.updated_time); | ||||
| 	// 	expect(note.body).toBe(note.body); | ||||
|  | ||||
| 	// 	done(); | ||||
| 	// }); | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| 	it('should update local items', async (done) => { | ||||
| 		let folder1 = await Folder.save({ title: "folder1" }); | ||||
| 		let note1 = await Note.save({ title: "un", parent_id: folder1.id }); | ||||
| 		await synchronizer().start(); | ||||
|  | ||||
| 		await sleep(1); | ||||
|  | ||||
| 		switchClient(2); | ||||
|  | ||||
| 		await synchronizer().start(); | ||||
|  | ||||
| 		await sleep(1); | ||||
|  | ||||
| 		let note2 = await Note.load(note1.id); | ||||
| 		note2.title = "Updated on client 2"; | ||||
| 		await Note.save(note2); | ||||
|  | ||||
| 		let all = await Folder.all(true); | ||||
| 		note2 = await Note.load(note2.id); | ||||
|  | ||||
| 		await synchronizer().start(); | ||||
|  | ||||
| 		let files = await fileApi().list(); | ||||
|  | ||||
| 		switchClient(1); | ||||
|  | ||||
| 		await synchronizer().start(); | ||||
| @@ -140,177 +126,4 @@ describe('Synchronizer', function() { | ||||
| 		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(); | ||||
| // // 	}); | ||||
|  | ||||
| // // }); | ||||
|  | ||||
| }); | ||||
| @@ -135,13 +135,13 @@ void CliApplication::saveNoteIfFileChanged(Note& note, const QDateTime& original | ||||
| // 	if (propKey.isEmpty()) { | ||||
| // 		QStringList propKeys = settings.allKeys(); | ||||
| // 		for (int i = 0; i < propKeys.size(); i++) { | ||||
| // 			qStdout() << settings.keyValueToFriendlyString(propKeys[i]) << endl; | ||||
| // 			qStdout() << settings.keyValueserialize(propKeys[i]) << endl; | ||||
| // 		} | ||||
| // 		return 0; | ||||
| // 	} | ||||
|  | ||||
| // 	if (propValue.isEmpty()) { | ||||
| // 		qStdout() << settings.keyValueToFriendlyString(propKey) << endl; | ||||
| // 		qStdout() << settings.keyValueserialize(propKey) << endl; | ||||
| // 		return 0; | ||||
| // 	} | ||||
|  | ||||
| @@ -386,7 +386,7 @@ int CliApplication::exec() { | ||||
|  | ||||
| 			QString noteFilePath = QString("%1/%2.txt").arg(paths::noteDraftsDir()).arg(note.idString()); | ||||
|  | ||||
| 			if (!filePutContents(noteFilePath, note.toFriendlyString())) { | ||||
| 			if (!filePutContents(noteFilePath, note.serialize())) { | ||||
| 				qStderr() << QString("Cannot open %1 for writing").arg(noteFilePath) << endl; | ||||
| 				return 1; | ||||
| 			} | ||||
| @@ -431,13 +431,13 @@ int CliApplication::exec() { | ||||
| 		if (propKey.isEmpty()) { | ||||
| 			QStringList propKeys = settings.allKeys(); | ||||
| 			for (int i = 0; i < propKeys.size(); i++) { | ||||
| 				qStdout() << settings.keyValueToFriendlyString(propKeys[i]) << endl; | ||||
| 				qStdout() << settings.keyValueserialize(propKeys[i]) << endl; | ||||
| 			} | ||||
| 			return 0; | ||||
| 		} | ||||
|  | ||||
| 		if (propValue.isEmpty()) { | ||||
| 			qStdout() << settings.keyValueToFriendlyString(propKey) << endl; | ||||
| 			qStdout() << settings.keyValueserialize(propKey) << endl; | ||||
| 			return 0; | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ namespace jop { | ||||
|  | ||||
| Item::Item() {} | ||||
|  | ||||
| QString Item::toFriendlyString() const { | ||||
| QString Item::serialize() const { | ||||
| 	QStringList shownKeys; | ||||
| 	shownKeys << "author" << "longitude" << "latitude" << "is_todo" << "todo_due" << "todo_completed"; | ||||
|  | ||||
|   | ||||
| @@ -14,7 +14,7 @@ class Item : public BaseModel { | ||||
| public: | ||||
|  | ||||
| 	Item(); | ||||
| 	QString toFriendlyString() const; | ||||
| 	QString serialize() const; | ||||
| 	void patchFriendlyString(const QString& patch); | ||||
|  | ||||
| }; | ||||
|   | ||||
| @@ -35,6 +35,6 @@ int Settings::valueInt(const QString &name, int defaultValue) { | ||||
| 	return value(name, defaultValue).toInt(); | ||||
| } | ||||
|  | ||||
| QString Settings::keyValueToFriendlyString(const QString& key) const { | ||||
| QString Settings::keyValueserialize(const QString& key) const { | ||||
| 	return QString("%1 = %2").arg(key).arg(value(key).toString()); | ||||
| } | ||||
| @@ -15,7 +15,7 @@ public: | ||||
| 	Settings(); | ||||
|  | ||||
| 	static void initialize(); | ||||
| 	QString keyValueToFriendlyString(const QString& key) const; | ||||
| 	QString keyValueserialize(const QString& key) const; | ||||
|  | ||||
| public slots: | ||||
|  | ||||
|   | ||||
| @@ -161,7 +161,7 @@ class BaseModel { | ||||
| 		let itemId = o.id; | ||||
|  | ||||
| 		if (options.autoTimestamp && this.hasField('updated_time')) { | ||||
| 			o.updated_time = time.unix(); | ||||
| 			o.updated_time = time.unixMs(); | ||||
| 		} | ||||
|  | ||||
| 		if (options.isNew) { | ||||
| @@ -171,7 +171,7 @@ class BaseModel { | ||||
| 			} | ||||
|  | ||||
| 			if (!o.created_time && this.hasField('created_time')) { | ||||
| 				o.created_time = time.unix(); | ||||
| 				o.created_time = time.unixMs(); | ||||
| 			} | ||||
|  | ||||
| 			query = Database.insertQuery(this.tableName(), o); | ||||
|   | ||||
| @@ -32,10 +32,10 @@ class FileApiDriverLocal { | ||||
| 	metadataFromStats_(path, stats) { | ||||
| 		return { | ||||
| 			path: path, | ||||
| 			createdTime: this.statTimeToUnixTimestamp_(stats.birthtime), | ||||
| 			updatedTime: this.statTimeToUnixTimestamp_(stats.mtime), | ||||
| 			createdTimeOrig: stats.birthtime, | ||||
| 			updatedTimeOrig: stats.mtime, | ||||
| 			created_time: this.statTimeToUnixTimestamp_(stats.birthtime), | ||||
| 			updated_time: this.statTimeToUnixTimestamp_(stats.mtime), | ||||
| 			created_time_orig: stats.birthtime, | ||||
| 			updated_time_orig: stats.mtime, | ||||
| 			isDir: stats.isDirectory(), | ||||
| 		}; | ||||
| 	} | ||||
|   | ||||
| @@ -23,12 +23,12 @@ class FileApiDriverMemory { | ||||
| 	} | ||||
|  | ||||
| 	newItem(path, isDir = false) { | ||||
| 		let now = time.unix(); | ||||
| 		let now = time.unixMs(); | ||||
| 		return { | ||||
| 			path: path, | ||||
| 			isDir: isDir, | ||||
| 			updatedTime: now, | ||||
| 			createdTime: now, | ||||
| 			updated_time: now, // In milliseconds!! | ||||
| 			created_time: now, // In milliseconds!! | ||||
| 			content: '', | ||||
| 		}; | ||||
| 	} | ||||
| @@ -41,7 +41,7 @@ class FileApiDriverMemory { | ||||
| 	setTimestamp(path, timestamp) { | ||||
| 		let item = this.itemByPath(path); | ||||
| 		if (!item) return Promise.reject(new Error('File not found: ' + path)); | ||||
| 		item.updatedTime = timestamp; | ||||
| 		item.updated_time = timestamp; | ||||
| 		return Promise.resolve(); | ||||
| 	} | ||||
|  | ||||
| @@ -85,7 +85,7 @@ class FileApiDriverMemory { | ||||
| 			this.items_.push(item); | ||||
| 		} else { | ||||
| 			this.items_[index].content = content; | ||||
| 			this.items_[index].updatedTime = time.unix(); | ||||
| 			this.items_[index].updated_time = time.unix(); | ||||
| 		} | ||||
| 		return Promise.resolve(); | ||||
| 	} | ||||
|   | ||||
| @@ -41,34 +41,6 @@ class FileApi { | ||||
| 		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); | ||||
| 		// 	// 				} | ||||
| 		// 	// 			}); | ||||
| 		// 	// 		}); | ||||
| 		// 	// 	} | ||||
|  | ||||
| 		// 	// 	return promiseChain(chain).then(() => { | ||||
| 		// 	// 		return items; | ||||
| 		// 	// 	}); | ||||
| 		// 	// } else { | ||||
| 		// 	// 	return items; | ||||
| 		// 	// } | ||||
| 		// }); | ||||
| 	} | ||||
|  | ||||
| 	setTimestamp(path, timestamp) { | ||||
| @@ -81,7 +53,6 @@ class FileApi { | ||||
| 	} | ||||
|  | ||||
| 	stat(path) { | ||||
| 		//console.info('stat ' + path); | ||||
| 		return this.driver_.stat(this.fullPath_(path)).then((output) => { | ||||
| 			if (!output) return output; | ||||
| 			output.path = path; | ||||
| @@ -90,12 +61,10 @@ class FileApi { | ||||
| 	} | ||||
|  | ||||
| 	get(path) { | ||||
| 		//console.info('get ' + path); | ||||
| 		return this.driver_.get(this.fullPath_(path)); | ||||
| 	} | ||||
|  | ||||
| 	put(path, content) { | ||||
| 		//console.info('put ' + path); | ||||
| 		return this.driver_.put(this.fullPath_(path), content); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import { Note } from 'src/models/note.js'; | ||||
| import { Folder } from 'src/models/folder.js'; | ||||
| import { folderItemFilename } from 'src/string-utils.js' | ||||
| import { Database } from 'src/database.js'; | ||||
| import { time } from 'src/time-utils.js'; | ||||
| import moment from 'moment'; | ||||
|  | ||||
| class BaseItem extends BaseModel { | ||||
| @@ -17,8 +18,15 @@ class BaseItem extends BaseModel { | ||||
|  | ||||
| 	static itemClass(item) { | ||||
| 		if (!item) throw new Error('Item cannot be null'); | ||||
| 		if (!('type_' in item)) throw new Error('Item does not have a type_ property'); | ||||
| 		return item.type_ == BaseModel.ITEM_TYPE_NOTE ? Note : Folder; | ||||
|  | ||||
| 		if (typeof item === 'object') { | ||||
| 			if (!('type_' in item)) throw new Error('Item does not have a type_ property'); | ||||
| 			return item.type_ == BaseModel.ITEM_TYPE_NOTE ? Note : Folder; | ||||
| 		} else { | ||||
| 			if (Number(item) === BaseModel.ITEM_TYPE_NOTE) return Note; | ||||
| 			if (Number(item) === BaseModel.ITEM_TYPE_FOLDER) return Folder; | ||||
| 			throw new Error('Unknown type: ' + item); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	static pathToId(path) { | ||||
| @@ -34,10 +42,10 @@ class BaseItem extends BaseModel { | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	static toFriendlyString_format(propName, propValue) { | ||||
| 	static serialize_format(propName, propValue) { | ||||
| 		if (['created_time', 'updated_time'].indexOf(propName) >= 0) { | ||||
| 			if (!propValue) return ''; | ||||
| 			propValue = moment.unix(propValue).utc().format('YYYY-MM-DD HH:mm:ss') + 'Z'; | ||||
| 			propValue = moment.unix(propValue / 1000).utc().format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z'; | ||||
| 		} else if (propValue === null || propValue === undefined) { | ||||
| 			propValue = ''; | ||||
| 		} | ||||
| @@ -45,20 +53,22 @@ class BaseItem extends BaseModel { | ||||
| 		return propValue; | ||||
| 	} | ||||
|  | ||||
| 	static fromFriendlyString_format(propName, propValue) { | ||||
| 	static unserialize_format(type, propName, propValue) { | ||||
| 		if (propName == 'type_') return propValue; | ||||
|  | ||||
| 		let ItemClass = this.itemClass(type); | ||||
|  | ||||
| 		if (['created_time', 'updated_time'].indexOf(propName) >= 0) { | ||||
| 			if (!propValue) return 0; | ||||
| 			propValue = moment(propValue, 'YYYY-MM-DD HH:mm:ssZ').unix(); | ||||
| 			propValue = moment(propValue, 'YYYY-MM-DDTHH:mm:ss.SSSZ').format('x'); | ||||
| 		} else { | ||||
| 			propValue = Database.formatValue(this.fieldType(propName), propValue); | ||||
| 			propValue = Database.formatValue(ItemClass.fieldType(propName), propValue); | ||||
| 		} | ||||
|  | ||||
| 		return propValue; | ||||
| 	} | ||||
|  | ||||
| 	static toFriendlyString(item, type = null, shownKeys = null) { | ||||
| 	static serialize(item, type = null, shownKeys = null) { | ||||
| 		let output = []; | ||||
|  | ||||
| 		output.push(item.title); | ||||
| @@ -67,14 +77,14 @@ class BaseItem extends BaseModel { | ||||
| 		output.push(''); | ||||
| 		for (let i = 0; i < shownKeys.length; i++) { | ||||
| 			let v = item[shownKeys[i]]; | ||||
| 			v = this.toFriendlyString_format(shownKeys[i], v); | ||||
| 			v = this.serialize_format(shownKeys[i], v); | ||||
| 			output.push(shownKeys[i] + ': ' + v); | ||||
| 		} | ||||
|  | ||||
| 		return output.join("\n"); | ||||
| 	} | ||||
|  | ||||
| 	static fromFriendlyString(content) { | ||||
| 	static unserialize(content) { | ||||
| 		let lines = content.split("\n"); | ||||
| 		let output = {}; | ||||
| 		let state = 'readingProps'; | ||||
| @@ -94,7 +104,7 @@ class BaseItem extends BaseModel { | ||||
| 				if (p < 0) throw new Error('Invalid property format: ' + line + ": " + content); | ||||
| 				let key = line.substr(0, p).trim(); | ||||
| 				let value = line.substr(p + 1).trim(); | ||||
| 				output[key] = this.fromFriendlyString_format(key, value); | ||||
| 				output[key] = value; | ||||
| 			} else if (state == 'readingBody') { | ||||
| 				body.splice(0, 0, line); | ||||
| 			} | ||||
| @@ -104,11 +114,29 @@ class BaseItem extends BaseModel { | ||||
|  | ||||
| 		let title = body.splice(0, 2); | ||||
| 		output.title = title[0]; | ||||
|  | ||||
| 		if (!output.type_) throw new Error('Missing required property: type_: ' + content); | ||||
| 		output.type_ = Number(output.type_); | ||||
|  | ||||
| 		if (output.type_ == BaseModel.ITEM_TYPE_NOTE) output.body = body.join("\n"); | ||||
|  | ||||
| 		for (let n in output) { | ||||
| 			if (!output.hasOwnProperty(n)) continue; | ||||
| 			output[n] = this.unserialize_format(output.type_, n, output[n]); | ||||
| 		} | ||||
|  | ||||
| 		return output; | ||||
| 	} | ||||
|  | ||||
| 	static itemsThatNeedSync(limit = 100) { | ||||
| 		return Folder.modelSelectAll('SELECT * FROM folders WHERE sync_time < updated_time LIMIT ' + limit).then((items) => { | ||||
| 			if (items.length) return { hasMore: true, items: items }; | ||||
| 			return Note.modelSelectAll('SELECT * FROM notes WHERE sync_time < updated_time LIMIT ' + limit).then((items) => { | ||||
| 				return { hasMore: items.length >= limit, items: items }; | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| export { BaseItem }; | ||||
| @@ -13,8 +13,8 @@ class Folder extends BaseItem { | ||||
| 		return 'folders'; | ||||
| 	} | ||||
|  | ||||
| 	static toFriendlyString(folder) { | ||||
| 		return super.toFriendlyString(folder, 'folder', ['id', 'created_time', 'updated_time', 'type_']); | ||||
| 	static serialize(folder) { | ||||
| 		return super.serialize(folder, 'folder', ['id', 'created_time', 'updated_time', 'type_']); | ||||
| 	} | ||||
|  | ||||
| 	static itemType() { | ||||
|   | ||||
| @@ -12,8 +12,8 @@ class Note extends BaseItem { | ||||
| 		return 'notes'; | ||||
| 	} | ||||
|  | ||||
| 	static toFriendlyString(note, type = null, shownKeys = null) { | ||||
| 		return super.toFriendlyString(note, 'note', ["author", "longitude", "latitude", "is_todo", "todo_due", "todo_completed", 'created_time', 'updated_time', 'id', 'parent_id', 'type_']); | ||||
| 	static serialize(note, type = null, shownKeys = null) { | ||||
| 		return super.serialize(note, 'note', ["author", "longitude", "latitude", "is_todo", "todo_due", "todo_completed", 'created_time', 'updated_time', 'id', 'parent_id', 'type_']); | ||||
| 	} | ||||
|  | ||||
| 	static itemType() { | ||||
|   | ||||
| @@ -1,34 +1,17 @@ | ||||
| require('babel-plugin-transform-runtime'); | ||||
|  | ||||
| import { Log } from 'src/log.js'; | ||||
| import { Setting } from 'src/models/setting.js'; | ||||
| import { Change } from 'src/models/change.js'; | ||||
| import { Folder } from 'src/models/folder.js'; | ||||
| import { Note } from 'src/models/note.js'; | ||||
| import { BaseItem } from 'src/models/base-item.js'; | ||||
| import { BaseModel } from 'src/base-model.js'; | ||||
| import { promiseChain } from 'src/promise-utils.js'; | ||||
| import { NoteFolderService } from 'src/services/note-folder-service.js'; | ||||
| import { time } from 'src/time-utils.js'; | ||||
| import { sprintf } from 'sprintf-js'; | ||||
| //import { promiseWhile } from 'src/promise-utils.js'; | ||||
| import moment from 'moment'; | ||||
|  | ||||
| const fs = require('fs'); | ||||
| const path = require('path'); | ||||
| import { time } from 'src/time-utils.js'; | ||||
| import { Log } from 'src/log.js' | ||||
|  | ||||
| class Synchronizer { | ||||
|  | ||||
| 	constructor(db, api) { | ||||
| 		this.state_ = 'idle'; | ||||
| 		this.db_ = db; | ||||
| 		this.api_ = api; | ||||
| 	} | ||||
|  | ||||
| 	state() { | ||||
| 		return this.state_; | ||||
| 	} | ||||
|  | ||||
| 	db() { | ||||
| 		return this.db_; | ||||
| 	} | ||||
| @@ -37,382 +20,101 @@ class Synchronizer { | ||||
| 		return this.api_; | ||||
| 	} | ||||
|  | ||||
| 	loadParentAndItem(change) { | ||||
| 		if (change.item_type == BaseModel.ITEM_TYPE_NOTE) { | ||||
| 			return Note.load(change.item_id).then((note) => { | ||||
| 				if (!note) return { parent:null, item: null }; | ||||
| 	async start() { | ||||
| 		// ------------------------------------------------------------------------ | ||||
| 		// First, find all the items that have been changed since the | ||||
| 		// last sync and apply the changes to remote. | ||||
| 		// ------------------------------------------------------------------------ | ||||
|  | ||||
| 				return Folder.load(note.parent_id).then((folder) => { | ||||
| 					return Promise.resolve({ parent: folder, item: note }); | ||||
| 				}); | ||||
| 			}); | ||||
| 		} else { | ||||
| 			return Folder.load(change.item_id).then((folder) => { | ||||
| 				return Promise.resolve({ parent: null, item: folder }); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	remoteFileByPath(remoteFiles, path) { | ||||
| 		for (let i = 0; i < remoteFiles.length; i++) { | ||||
| 			if (remoteFiles[i].path == path) return remoteFiles[i]; | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
|  | ||||
| 	conflictDir(remoteFiles) { | ||||
| 		let d = this.remoteFileByPath('Conflicts'); | ||||
| 		if (!d) { | ||||
| 			return this.api().mkdir('Conflicts').then(() => { | ||||
| 				return 'Conflicts'; | ||||
| 			}); | ||||
| 		} else { | ||||
| 			return Promise.resolve('Conflicts'); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	moveConflict(item) { | ||||
| 		// No need to handle folder conflicts | ||||
| 		if (item.type == 'folder') return Promise.resolve(); | ||||
|  | ||||
| 		return this.conflictDir().then((conflictDirPath) => { | ||||
| 			let p = path.basename(item.path).split('.'); | ||||
| 			let pos = item.type == 'folder' ? p.length - 1 : p.length - 2; | ||||
| 			p.splice(pos, 0, moment().format('YYYYMMDDThhmmss')); | ||||
| 			let newPath = p.join('.'); | ||||
| 			return this.api().move(item.path, conflictDirPath + '/' + newPath); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	itemByPath(items, path) { | ||||
| 		for (let i = 0; i < items.length; i++) { | ||||
| 			if (items[i].path == path) return items[i]; | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
|  | ||||
| 	itemIsSameDate(item, date) { | ||||
| 		return item.updatedTime === date; | ||||
| 	} | ||||
|  | ||||
| 	itemIsStrictlyNewerThan(item, date) { | ||||
| 		return item.updatedTime > date; | ||||
| 	} | ||||
|  | ||||
| 	itemIsStrictlyOlderThan(item, date) { | ||||
| 		return item.updatedTime < date; | ||||
| 	} | ||||
|  | ||||
| 	dbItemToSyncItem(dbItem) { | ||||
| 		if (!dbItem) return null; | ||||
|  | ||||
| 		return { | ||||
| 			type: dbItem.type_ == BaseModel.ITEM_TYPE_FOLDER ? 'folder' : 'note', | ||||
| 			path: Folder.systemPath(dbItem), | ||||
| 			syncTime: dbItem.sync_time, | ||||
| 			updatedTime: dbItem.updated_time, | ||||
| 			dbItem: dbItem, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	remoteItemToSyncItem(remoteItem) { | ||||
| 		if (!remoteItem) return null; | ||||
|  | ||||
| 		return { | ||||
| 			type: remoteItem.content.type_ == BaseModel.ITEM_TYPE_FOLDER ? 'folder' : 'note', | ||||
| 			path: remoteItem.path, | ||||
| 			syncTime: 0, | ||||
| 			updatedTime: remoteItem.updatedTime, | ||||
| 			remoteItem: remoteItem, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	syncAction(localItem, remoteItem, deletedLocalPaths) { | ||||
| 		let output = this.syncActions(localItem ? [localItem] : [], remoteItem ? [remoteItem] : [], deletedLocalPaths); | ||||
| 		if (output.length > 1) throw new Error('Invalid number of actions returned'); | ||||
| 		return output.length ? output[0] : null; | ||||
| 	} | ||||
|  | ||||
| 	// 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. | ||||
| 	// Each item must have these properties: | ||||
| 	// - path | ||||
| 	// - type | ||||
| 	// - syncTime | ||||
| 	// - updatedTime | ||||
| 	syncActions(localItems, remoteItems, deletedLocalPaths) { | ||||
| 		let output = []; | ||||
| 		let donePaths = []; | ||||
|  | ||||
| 		// console.info('=================================================='); | ||||
| 		// console.info(localItems, remoteItems); | ||||
|  | ||||
| 		for (let i = 0; i < localItems.length; i++) { | ||||
| 			let local = localItems[i]; | ||||
| 			let remote = this.itemByPath(remoteItems, local.path); | ||||
|  | ||||
| 			let action = { | ||||
| 				local: local, | ||||
| 				remote: remote, | ||||
| 			}; | ||||
|  | ||||
| 			if (!remote) { | ||||
| 				if (local.syncTime) { | ||||
| 					action.type = 'delete'; | ||||
| 					action.dest = 'local'; | ||||
| 					action.reason = 'Local has been synced to remote previously, but remote no longer exist, which means remote has been deleted'; | ||||
| 				} else { | ||||
| 					action.type = 'create'; | ||||
| 					action.dest = 'remote'; | ||||
| 					action.reason = 'Local has never been synced to remote, and remote does not exists, which means remote must be created'; | ||||
| 				} | ||||
| 			} else { | ||||
| 				if (this.itemIsStrictlyOlderThan(local, local.syncTime)) continue; | ||||
|  | ||||
| 				if (this.itemIsStrictlyOlderThan(remote, local.updatedTime)) { | ||||
| 					action.type = 'update'; | ||||
| 					action.dest = 'remote'; | ||||
| 					action.reason = sprintf('Remote (%s) was modified before updated time of local (%s).', moment.unix(remote.updatedTime).toISOString(), moment.unix(local.syncTime).toISOString(),); | ||||
| 				} else if (this.itemIsStrictlyNewerThan(remote, local.syncTime) && this.itemIsStrictlyNewerThan(local, local.syncTime)) { | ||||
| 					action.type = 'conflict'; | ||||
| 					action.reason = sprintf('Both remote (%s) and local (%s) were modified after the last sync (%s).', | ||||
| 						moment.unix(remote.updatedTime).toISOString(), | ||||
| 						moment.unix(local.updatedTime).toISOString(), | ||||
| 						moment.unix(local.syncTime).toISOString() | ||||
| 					); | ||||
|  | ||||
| 					if (local.type == 'folder') { | ||||
| 						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' }, | ||||
| 						]; | ||||
| 					} | ||||
| 				} else if (this.itemIsStrictlyNewerThan(remote, local.syncTime) && local.updatedTime <= local.syncTime) { | ||||
| 					action.type = 'update'; | ||||
| 					action.dest = 'local'; | ||||
| 					action.reason = sprintf('Remote (%s) was modified after update time of local (%s). And sync time (%s) is the same or more recent than local update time', moment.unix(remote.updatedTime).toISOString(), moment.unix(local.updatedTime).toISOString(), moment.unix(local.syncTime).toISOString()); | ||||
| 				} else { | ||||
| 					continue; // Neither local nor remote item have been changed recently | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			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.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)) { | ||||
| 					console.error('Remote cannot be newer than last sync time', remote, local); | ||||
| 					throw new Error('Remote 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 local (%s).', moment.unix(remote.updatedTime).toISOString(), moment.unix(local.updatedTime).toISOString(),);; | ||||
| 				} else { | ||||
| 					continue; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			output.push(action); | ||||
| 		} | ||||
|  | ||||
| 		// console.info('-----------------------------------------'); | ||||
| 		// console.info(output); | ||||
|  | ||||
| 		return output; | ||||
| 	} | ||||
|  | ||||
| 	processState(state) { | ||||
| 		Log.info('Sync: processing: ' + state); | ||||
| 		this.state_ = state; | ||||
|  | ||||
| 		if (state == 'uploadChanges') { | ||||
| 			return this.processState_uploadChanges(); | ||||
| 		} else if (state == 'downloadChanges') { | ||||
| 			//return this.processState('idle'); | ||||
| 			return this.processState_downloadChanges(); | ||||
| 		} else if (state == 'idle') { | ||||
| 			// Nothing | ||||
| 			return Promise.resolve(); | ||||
| 		} else { | ||||
| 			throw new Error('Invalid state: ' . state); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	processSyncAction(action) { | ||||
| 		//console.info('Sync action: ', action); | ||||
| 		//console.info('Sync action: ' + JSON.stringify(action)); | ||||
|  | ||||
| 		if (!action) return Promise.resolve(); | ||||
|  | ||||
| 		console.info('Sync action: ' + action.type + ' ' + action.dest + ': ' + action.reason); | ||||
|  | ||||
| 		if (action.type == 'conflict') { | ||||
| 			console.info(action); | ||||
|  | ||||
| 		} else { | ||||
| 			let syncItem = action[action.dest == 'local' ? 'remote' : 'local']; | ||||
| 			let path = syncItem.path; | ||||
|  | ||||
| 			if (action.type == 'create') { | ||||
| 				if (action.dest == 'remote') { | ||||
| 					let content = null; | ||||
| 					let dbItem = syncItem.dbItem; | ||||
|  | ||||
| 					if (syncItem.type == 'folder') { | ||||
| 						content = Folder.toFriendlyString(dbItem); | ||||
| 					} else { | ||||
| 						content = Note.toFriendlyString(dbItem); | ||||
| 					} | ||||
|  | ||||
| 					return this.api().put(path, content).then(() => { | ||||
| 						return this.api().setTimestamp(path, dbItem.updated_time); | ||||
| 					}); | ||||
|  | ||||
| 					// TODO: save sync_time | ||||
| 				} else { | ||||
| 					let dbItem = syncItem.remoteItem.content; | ||||
| 					dbItem.sync_time = time.unix(); | ||||
| 					dbItem.updated_time = action.remote.updatedTime; | ||||
| 					if (syncItem.type == 'folder') { | ||||
| 						return Folder.save(dbItem, { isNew: true, autoTimestamp: false }); | ||||
| 					} else { | ||||
| 						return Note.save(dbItem, { isNew: true, autoTimestamp: false }); | ||||
| 					} | ||||
|  | ||||
| 					// TODO: save sync_time | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if (action.type == 'update') { | ||||
| 				if (action.dest == 'remote') { | ||||
| 					let dbItem = syncItem.dbItem; | ||||
| 					let ItemClass = BaseItem.itemClass(dbItem); | ||||
| 					let content = ItemClass.toFriendlyString(dbItem); | ||||
| 					//console.info('PUT', content); | ||||
| 					return this.api().put(path, content).then(() => { | ||||
| 						return this.api().setTimestamp(path, dbItem.updated_time); | ||||
| 					}).then(() => { | ||||
| 						let toSave = { id: dbItem.id, sync_time: time.unix() }; | ||||
| 						return NoteFolderService.save(syncItem.type, dbItem, null, { autoTimestamp: false }); | ||||
| 					}); | ||||
| 				} else { | ||||
| 					let dbItem = Object.assign({}, syncItem.remoteItem.content); | ||||
| 					dbItem.sync_time = time.unix(); | ||||
| 					return NoteFolderService.save(syncItem.type, dbItem, action.local.dbItem, { autoTimestamp: false }); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return Promise.resolve(); // TODO | ||||
| 	} | ||||
|  | ||||
| 	async processLocalItem(dbItem) { | ||||
| 		let localItem = this.dbItemToSyncItem(dbItem); | ||||
| 		 | ||||
| 		let remoteItem = await this.api().stat(localItem.path); | ||||
| 		let action = this.syncAction(localItem, remoteItem, []); | ||||
| 		await this.processSyncAction(action); | ||||
|  | ||||
| 		let toSave = Object.assign({}, dbItem); | ||||
| 		toSave.sync_time = time.unix(); | ||||
| 		return NoteFolderService.save(localItem.type, toSave, dbItem, { autoTimestamp: false }); | ||||
| 	} | ||||
|  | ||||
| 	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); | ||||
|  | ||||
| 		let dbItem = await BaseItem.loadItemByPath(remoteItem.path); | ||||
| 		let localSyncItem = this.dbItemToSyncItem(dbItem); | ||||
|  | ||||
| 		let action = this.syncAction(localSyncItem, remoteSyncItem, []); | ||||
| 		return this.processSyncAction(action); | ||||
| 	} | ||||
|  | ||||
| 	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); | ||||
| 			let result = await BaseItem.itemsThatNeedSync(); | ||||
| 			let locals = result.items; | ||||
|  | ||||
| 			for (let i = 0; i < locals.length; i++) { | ||||
| 				let local = locals[i]; | ||||
| 				let ItemClass = BaseItem.itemClass(local); | ||||
| 				let path = BaseItem.systemPath(local); | ||||
|  | ||||
| 				// Safety check to avoid infinite loops: | ||||
| 				if (donePaths.indexOf(path) > 0) throw new Error(sprintf('Processing a path that has already been done: %s. sync_time was not updated?', path)); | ||||
|  | ||||
| 				let remote = await this.api().stat(path); | ||||
| 				let content = ItemClass.serialize(local); | ||||
| 				let action = null; | ||||
|  | ||||
| 				if (!remote) { | ||||
| 					action = 'createRemote'; | ||||
| 				} else { | ||||
| 					if (remote.updated_time > local.updated_time) { | ||||
| 						action = 'conflict'; | ||||
| 					} else { | ||||
| 						action = 'updateRemote'; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				if (action == 'createRemote' || action == 'updateRemote') { | ||||
| 					await this.api().put(path, content); | ||||
| 					await this.api().setTimestamp(path, local.updated_time); | ||||
| 				} else if (action == 'conflict') { | ||||
| 					console.warn('FOUND CONFLICT', local, remote); | ||||
| 				} | ||||
|  | ||||
| 				let newLocal = { id: local.id, sync_time: time.unixMs(), type_: local.type_ }; | ||||
| 				await ItemClass.save(newLocal, { autoTimestamp: false }); | ||||
|  | ||||
| 				donePaths.push(path); | ||||
| 			} | ||||
|  | ||||
| 			if (!result.hasMore) break; | ||||
| 		} | ||||
|  | ||||
| 		//console.info('DOWNLOAD CHANGE DISABLED'); return Promise.resolve();  | ||||
| 		 | ||||
| 		return this.processState('downloadChanges'); | ||||
| 	} | ||||
| 		// ------------------------------------------------------------------------ | ||||
| 		// Then, loop through all the remote items, find those that | ||||
| 		// have been updated, and apply the changes to local. | ||||
| 		// ------------------------------------------------------------------------ | ||||
|  | ||||
| 	async processState_downloadChanges() { | ||||
| 		let items = await this.api().list(); | ||||
| 		for (let i = 0; i < items.length; i++) { | ||||
| 			await this.processRemoteItem(items[i]); | ||||
| 		// At this point all the local items that have changed have been pushed to remote | ||||
| 		// or handled as conflicts, so no conflict is possible after this. | ||||
|  | ||||
| 		let remotes = await this.api().list(); | ||||
| 		for (let i = 0; i < remotes.length; i++) { | ||||
| 			let remote = remotes[i]; | ||||
| 			let path = remote.path; | ||||
| 			if (donePaths.indexOf(path) > 0) continue; | ||||
|  | ||||
| 			let action = null; | ||||
| 			let local = await BaseItem.loadItemByPath(path); | ||||
| 			if (!local) { | ||||
| 				action = 'createLocal'; | ||||
| 			} else { | ||||
| 				if (remote.updated_time > local.updated_time) { | ||||
| 					action = 'updateLocal'; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if (!action) continue; | ||||
|  | ||||
| 			if (action == 'createLocal' || action == 'updateLocal') { | ||||
| 				let content = await this.api().get(path); | ||||
| 				if (!content) { | ||||
| 					Log.warn('Remote item has been deleted between now and the list() call? In that case it will handled during the next sync: ' + path); | ||||
| 					continue; | ||||
| 				} | ||||
| 				content = BaseItem.unserialize(content); | ||||
| 				let ItemClass = BaseItem.itemClass(content); | ||||
|  | ||||
| 				content.sync_time = time.unixMs(); | ||||
| 				let options = { autoTimestamp: false }; | ||||
| 				if (action == 'createLocal') options.isNew = true; | ||||
| 				await ItemClass.save(content, options); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return this.processState('idle'); | ||||
|  | ||||
| 		return Promise.resolve(); | ||||
| 	} | ||||
|  | ||||
| 	start() { | ||||
| 		Log.info('Sync: start'); | ||||
|  | ||||
| 		if (this.state() != 'idle') { | ||||
| 			return Promise.reject('Cannot start synchronizer because synchronization already in progress. State: ' + this.state()); | ||||
| 		} | ||||
|  | ||||
| 		this.state_ = 'started'; | ||||
|  | ||||
| 		// if (!this.api().session()) { | ||||
| 		// 	Log.info("Sync: cannot start synchronizer because user is not logged in."); | ||||
| 		// 	return; | ||||
| 		// } | ||||
|  | ||||
| 		return this.processState('uploadChanges').catch((error) => { | ||||
| 			console.info('Synchronizer error:', error); | ||||
| 			throw error; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	 | ||||
|  | ||||
| } | ||||
|  | ||||
| export { Synchronizer }; | ||||
| @@ -65,7 +65,7 @@ class Synchronizer { | ||||
| 					} else if (c.type == Change.TYPE_CREATE) { | ||||
| 						p = this.loadParentAndItem(c).then((result) => { | ||||
| 							let options = { | ||||
| 								contents: Note.toFriendlyString(result.item), | ||||
| 								contents: Note.serialize(result.item), | ||||
| 								path: Note.systemPath(result.parent, result.item), | ||||
| 								mode: 'overwrite', | ||||
| 								// client_modified:  | ||||
| @@ -79,7 +79,7 @@ class Synchronizer { | ||||
|  | ||||
| 						// 	console.info(item); | ||||
| 						// 	let options = { | ||||
| 						// 		contents: Note.toFriendlyString(item), | ||||
| 						// 		contents: Note.serialize(item), | ||||
| 						// 		path: Note.systemPath(item), | ||||
| 						// 		mode: 'overwrite', | ||||
| 						// 		// client_modified:  | ||||
| @@ -87,7 +87,7 @@ class Synchronizer { | ||||
|  | ||||
| 						// 	// console.info(options); | ||||
|  | ||||
| 						// 	//let content = Note.toFriendlyString(item); | ||||
| 						// 	//let content = Note.serialize(item); | ||||
| 						// 	//console.info(content); | ||||
|  | ||||
| 						// 	//console.info('SYNC', item); | ||||
|   | ||||
							
								
								
									
										418
									
								
								ReactNativeClient/src/synchronizer_old2.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										418
									
								
								ReactNativeClient/src/synchronizer_old2.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,418 @@ | ||||
| require('babel-plugin-transform-runtime'); | ||||
|  | ||||
| import { Log } from 'src/log.js'; | ||||
| import { Setting } from 'src/models/setting.js'; | ||||
| import { Change } from 'src/models/change.js'; | ||||
| import { Folder } from 'src/models/folder.js'; | ||||
| import { Note } from 'src/models/note.js'; | ||||
| import { BaseItem } from 'src/models/base-item.js'; | ||||
| import { BaseModel } from 'src/base-model.js'; | ||||
| import { promiseChain } from 'src/promise-utils.js'; | ||||
| import { NoteFolderService } from 'src/services/note-folder-service.js'; | ||||
| import { time } from 'src/time-utils.js'; | ||||
| import { sprintf } from 'sprintf-js'; | ||||
| //import { promiseWhile } from 'src/promise-utils.js'; | ||||
| import moment from 'moment'; | ||||
|  | ||||
| const fs = require('fs'); | ||||
| const path = require('path'); | ||||
|  | ||||
| class Synchronizer { | ||||
|  | ||||
| 	constructor(db, api) { | ||||
| 		this.state_ = 'idle'; | ||||
| 		this.db_ = db; | ||||
| 		this.api_ = api; | ||||
| 	} | ||||
|  | ||||
| 	state() { | ||||
| 		return this.state_; | ||||
| 	} | ||||
|  | ||||
| 	db() { | ||||
| 		return this.db_; | ||||
| 	} | ||||
|  | ||||
| 	api() { | ||||
| 		return this.api_; | ||||
| 	} | ||||
|  | ||||
| 	loadParentAndItem(change) { | ||||
| 		if (change.item_type == BaseModel.ITEM_TYPE_NOTE) { | ||||
| 			return Note.load(change.item_id).then((note) => { | ||||
| 				if (!note) return { parent:null, item: null }; | ||||
|  | ||||
| 				return Folder.load(note.parent_id).then((folder) => { | ||||
| 					return Promise.resolve({ parent: folder, item: note }); | ||||
| 				}); | ||||
| 			}); | ||||
| 		} else { | ||||
| 			return Folder.load(change.item_id).then((folder) => { | ||||
| 				return Promise.resolve({ parent: null, item: folder }); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	remoteFileByPath(remoteFiles, path) { | ||||
| 		for (let i = 0; i < remoteFiles.length; i++) { | ||||
| 			if (remoteFiles[i].path == path) return remoteFiles[i]; | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
|  | ||||
| 	conflictDir(remoteFiles) { | ||||
| 		let d = this.remoteFileByPath('Conflicts'); | ||||
| 		if (!d) { | ||||
| 			return this.api().mkdir('Conflicts').then(() => { | ||||
| 				return 'Conflicts'; | ||||
| 			}); | ||||
| 		} else { | ||||
| 			return Promise.resolve('Conflicts'); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	moveConflict(item) { | ||||
| 		// No need to handle folder conflicts | ||||
| 		if (item.type == 'folder') return Promise.resolve(); | ||||
|  | ||||
| 		return this.conflictDir().then((conflictDirPath) => { | ||||
| 			let p = path.basename(item.path).split('.'); | ||||
| 			let pos = item.type == 'folder' ? p.length - 1 : p.length - 2; | ||||
| 			p.splice(pos, 0, moment().format('YYYYMMDDThhmmss')); | ||||
| 			let newPath = p.join('.'); | ||||
| 			return this.api().move(item.path, conflictDirPath + '/' + newPath); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	itemByPath(items, path) { | ||||
| 		for (let i = 0; i < items.length; i++) { | ||||
| 			if (items[i].path == path) return items[i]; | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
|  | ||||
| 	itemIsSameDate(item, date) { | ||||
| 		return item.updated_time === date; | ||||
| 	} | ||||
|  | ||||
| 	itemIsStrictlyNewerThan(item, date) { | ||||
| 		return item.updated_time > date; | ||||
| 	} | ||||
|  | ||||
| 	itemIsStrictlyOlderThan(item, date) { | ||||
| 		return item.updated_time < date; | ||||
| 	} | ||||
|  | ||||
| 	dbItemToSyncItem(dbItem) { | ||||
| 		if (!dbItem) return null; | ||||
|  | ||||
| 		return { | ||||
| 			type: dbItem.type_ == BaseModel.ITEM_TYPE_FOLDER ? 'folder' : 'note', | ||||
| 			path: Folder.systemPath(dbItem), | ||||
| 			syncTime: dbItem.sync_time, | ||||
| 			updated_time: dbItem.updated_time, | ||||
| 			dbItem: dbItem, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	remoteItemToSyncItem(remoteItem) { | ||||
| 		if (!remoteItem) return null; | ||||
|  | ||||
| 		return { | ||||
| 			type: remoteItem.content.type_ == BaseModel.ITEM_TYPE_FOLDER ? 'folder' : 'note', | ||||
| 			path: remoteItem.path, | ||||
| 			syncTime: 0, | ||||
| 			updated_time: remoteItem.updated_time, | ||||
| 			remoteItem: remoteItem, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	syncAction(localItem, remoteItem, deletedLocalPaths) { | ||||
| 		let output = this.syncActions(localItem ? [localItem] : [], remoteItem ? [remoteItem] : [], deletedLocalPaths); | ||||
| 		if (output.length > 1) throw new Error('Invalid number of actions returned'); | ||||
| 		return output.length ? output[0] : null; | ||||
| 	} | ||||
|  | ||||
| 	// 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. | ||||
| 	// Each item must have these properties: | ||||
| 	// - path | ||||
| 	// - type | ||||
| 	// - syncTime | ||||
| 	// - updated_time | ||||
| 	syncActions(localItems, remoteItems, deletedLocalPaths) { | ||||
| 		let output = []; | ||||
| 		let donePaths = []; | ||||
|  | ||||
| 		// console.info('=================================================='); | ||||
| 		// console.info(localItems, remoteItems); | ||||
|  | ||||
| 		for (let i = 0; i < localItems.length; i++) { | ||||
| 			let local = localItems[i]; | ||||
| 			let remote = this.itemByPath(remoteItems, local.path); | ||||
|  | ||||
| 			let action = { | ||||
| 				local: local, | ||||
| 				remote: remote, | ||||
| 			}; | ||||
|  | ||||
| 			if (!remote) { | ||||
| 				if (local.syncTime) { | ||||
| 					action.type = 'delete'; | ||||
| 					action.dest = 'local'; | ||||
| 					action.reason = 'Local has been synced to remote previously, but remote no longer exist, which means remote has been deleted'; | ||||
| 				} else { | ||||
| 					action.type = 'create'; | ||||
| 					action.dest = 'remote'; | ||||
| 					action.reason = 'Local has never been synced to remote, and remote does not exists, which means remote must be created'; | ||||
| 				} | ||||
| 			} else { | ||||
| 				if (this.itemIsStrictlyOlderThan(local, local.syncTime)) continue; | ||||
|  | ||||
| 				if (this.itemIsStrictlyOlderThan(remote, local.updated_time)) { | ||||
| 					action.type = 'update'; | ||||
| 					action.dest = 'remote'; | ||||
| 					action.reason = sprintf('Remote (%s) was modified before updated time of local (%s).', moment.unix(remote.updated_time).toISOString(), moment.unix(local.syncTime).toISOString(),); | ||||
| 				} else if (this.itemIsStrictlyNewerThan(remote, local.syncTime) && this.itemIsStrictlyNewerThan(local, local.syncTime)) { | ||||
| 					action.type = 'conflict'; | ||||
| 					action.reason = sprintf('Both remote (%s) and local (%s) were modified after the last sync (%s).', | ||||
| 						moment.unix(remote.updated_time).toISOString(), | ||||
| 						moment.unix(local.updated_time).toISOString(), | ||||
| 						moment.unix(local.syncTime).toISOString() | ||||
| 					); | ||||
|  | ||||
| 					if (local.type == 'folder') { | ||||
| 						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' }, | ||||
| 						]; | ||||
| 					} | ||||
| 				} else if (this.itemIsStrictlyNewerThan(remote, local.syncTime) && local.updated_time <= local.syncTime) { | ||||
| 					action.type = 'update'; | ||||
| 					action.dest = 'local'; | ||||
| 					action.reason = sprintf('Remote (%s) was modified after update time of local (%s). And sync time (%s) is the same or more recent than local update time', moment.unix(remote.updated_time).toISOString(), moment.unix(local.updated_time).toISOString(), moment.unix(local.syncTime).toISOString()); | ||||
| 				} else { | ||||
| 					continue; // Neither local nor remote item have been changed recently | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			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.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)) { | ||||
| 					console.error('Remote cannot be newer than last sync time', remote, local); | ||||
| 					throw new Error('Remote cannot be newer than last sync time'); | ||||
| 				} | ||||
| 				 | ||||
| 				if (this.itemIsStrictlyNewerThan(remote, local.updated_time)) { | ||||
| 					action.type = 'update'; | ||||
| 					action.dest = 'local'; | ||||
| 					action.reason = sprintf('Remote (%s) was modified after local (%s).', moment.unix(remote.updated_time).toISOString(), moment.unix(local.updated_time).toISOString(),);; | ||||
| 				} else { | ||||
| 					continue; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			output.push(action); | ||||
| 		} | ||||
|  | ||||
| 		// console.info('-----------------------------------------'); | ||||
| 		// console.info(output); | ||||
|  | ||||
| 		return output; | ||||
| 	} | ||||
|  | ||||
| 	processState(state) { | ||||
| 		Log.info('Sync: processing: ' + state); | ||||
| 		this.state_ = state; | ||||
|  | ||||
| 		if (state == 'uploadChanges') { | ||||
| 			return this.processState_uploadChanges(); | ||||
| 		} else if (state == 'downloadChanges') { | ||||
| 			//return this.processState('idle'); | ||||
| 			return this.processState_downloadChanges(); | ||||
| 		} else if (state == 'idle') { | ||||
| 			// Nothing | ||||
| 			return Promise.resolve(); | ||||
| 		} else { | ||||
| 			throw new Error('Invalid state: ' . state); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	processSyncAction(action) { | ||||
| 		//console.info('Sync action: ', action); | ||||
| 		//console.info('Sync action: ' + JSON.stringify(action)); | ||||
|  | ||||
| 		if (!action) return Promise.resolve(); | ||||
|  | ||||
| 		console.info('Sync action: ' + action.type + ' ' + action.dest + ': ' + action.reason); | ||||
|  | ||||
| 		if (action.type == 'conflict') { | ||||
| 			console.info(action); | ||||
|  | ||||
| 		} else { | ||||
| 			let syncItem = action[action.dest == 'local' ? 'remote' : 'local']; | ||||
| 			let path = syncItem.path; | ||||
|  | ||||
| 			if (action.type == 'create') { | ||||
| 				if (action.dest == 'remote') { | ||||
| 					let content = null; | ||||
| 					let dbItem = syncItem.dbItem; | ||||
|  | ||||
| 					if (syncItem.type == 'folder') { | ||||
| 						content = Folder.serialize(dbItem); | ||||
| 					} else { | ||||
| 						content = Note.serialize(dbItem); | ||||
| 					} | ||||
|  | ||||
| 					return this.api().put(path, content).then(() => { | ||||
| 						return this.api().setTimestamp(path, dbItem.updated_time); | ||||
| 					}); | ||||
|  | ||||
| 					// TODO: save sync_time | ||||
| 				} else { | ||||
| 					let dbItem = syncItem.remoteItem.content; | ||||
| 					dbItem.sync_time = time.unix(); | ||||
| 					dbItem.updated_time = action.remote.updated_time; | ||||
| 					if (syncItem.type == 'folder') { | ||||
| 						return Folder.save(dbItem, { isNew: true, autoTimestamp: false }); | ||||
| 					} else { | ||||
| 						return Note.save(dbItem, { isNew: true, autoTimestamp: false }); | ||||
| 					} | ||||
|  | ||||
| 					// TODO: save sync_time | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if (action.type == 'update') { | ||||
| 				if (action.dest == 'remote') { | ||||
| 					let dbItem = syncItem.dbItem; | ||||
| 					let ItemClass = BaseItem.itemClass(dbItem); | ||||
| 					let content = ItemClass.serialize(dbItem); | ||||
| 					//console.info('PUT', content); | ||||
| 					return this.api().put(path, content).then(() => { | ||||
| 						return this.api().setTimestamp(path, dbItem.updated_time); | ||||
| 					}).then(() => { | ||||
| 						let toSave = { id: dbItem.id, sync_time: time.unix() }; | ||||
| 						return NoteFolderService.save(syncItem.type, dbItem, null, { autoTimestamp: false }); | ||||
| 					}); | ||||
| 				} else { | ||||
| 					let dbItem = Object.assign({}, syncItem.remoteItem.content); | ||||
| 					dbItem.sync_time = time.unix(); | ||||
| 					return NoteFolderService.save(syncItem.type, dbItem, action.local.dbItem, { autoTimestamp: false }); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return Promise.resolve(); // TODO | ||||
| 	} | ||||
|  | ||||
| 	async processLocalItem(dbItem) { | ||||
| 		let localItem = this.dbItemToSyncItem(dbItem); | ||||
| 		 | ||||
| 		let remoteItem = await this.api().stat(localItem.path); | ||||
| 		let action = this.syncAction(localItem, remoteItem, []); | ||||
| 		await this.processSyncAction(action); | ||||
|  | ||||
| 		let toSave = Object.assign({}, dbItem); | ||||
| 		toSave.sync_time = time.unix(); | ||||
| 		return NoteFolderService.save(localItem.type, toSave, dbItem, { autoTimestamp: false }); | ||||
| 	} | ||||
|  | ||||
| 	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.unserialize(content); | ||||
| 		let remoteSyncItem = this.remoteItemToSyncItem(remoteItem); | ||||
|  | ||||
| 		let dbItem = await BaseItem.loadItemByPath(remoteItem.path); | ||||
| 		let localSyncItem = this.dbItemToSyncItem(dbItem); | ||||
|  | ||||
| 		let action = this.syncAction(localSyncItem, remoteSyncItem, []); | ||||
| 		return this.processSyncAction(action); | ||||
| 	} | ||||
|  | ||||
| 	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); | ||||
| 			} | ||||
|  | ||||
| 			if (!result.hasMore) break; | ||||
| 		} | ||||
|  | ||||
| 		//console.info('DOWNLOAD CHANGE DISABLED'); return Promise.resolve();  | ||||
| 		 | ||||
| 		return this.processState('downloadChanges'); | ||||
| 	} | ||||
|  | ||||
| 	async processState_downloadChanges() { | ||||
| 		let items = await this.api().list(); | ||||
| 		for (let i = 0; i < items.length; i++) { | ||||
| 			await this.processRemoteItem(items[i]); | ||||
| 		} | ||||
|  | ||||
| 		return this.processState('idle'); | ||||
| 	} | ||||
|  | ||||
| 	start() { | ||||
| 		Log.info('Sync: start'); | ||||
|  | ||||
| 		if (this.state() != 'idle') { | ||||
| 			return Promise.reject('Cannot start synchronizer because synchronization already in progress. State: ' + this.state()); | ||||
| 		} | ||||
|  | ||||
| 		this.state_ = 'started'; | ||||
|  | ||||
| 		// if (!this.api().session()) { | ||||
| 		// 	Log.info("Sync: cannot start synchronizer because user is not logged in."); | ||||
| 		// 	return; | ||||
| 		// } | ||||
|  | ||||
| 		return this.processState('uploadChanges').catch((error) => { | ||||
| 			console.info('Synchronizer error:', error); | ||||
| 			throw error; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	 | ||||
|  | ||||
| } | ||||
|  | ||||
| export { Synchronizer }; | ||||
| @@ -2,6 +2,14 @@ let time = { | ||||
|  | ||||
| 	unix() { | ||||
| 		return Math.round((new Date()).getTime() / 1000); | ||||
| 	}, | ||||
|  | ||||
| 	unixMs() { | ||||
| 		return (new Date()).getTime(); | ||||
| 	}, | ||||
|  | ||||
| 	unixMsToS(ms) { | ||||
| 		return Math.round(ms / 1000); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user