You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	sync
This commit is contained in:
		| @@ -17,57 +17,6 @@ import { NoteFolderService } from 'src/services/note-folder-service.js'; | |||||||
|  |  | ||||||
|  |  | ||||||
| let db = new Database(new DatabaseDriverNode()); | let db = new Database(new DatabaseDriverNode()); | ||||||
| db.setDebugEnabled(false); |  | ||||||
|  |  | ||||||
| // function whilePromise(callback) { |  | ||||||
| // 	let isDone = false; |  | ||||||
|  |  | ||||||
| // 	function done() { |  | ||||||
| // 		isDone = true; |  | ||||||
| // 	} |  | ||||||
|  |  | ||||||
| // 	let iterationDone = false; |  | ||||||
| // 	let p = callback(done).then(() => { |  | ||||||
| // 		iterationDone = true; |  | ||||||
| // 	}); |  | ||||||
|  |  | ||||||
| // 	let iid = setInterval(() => { |  | ||||||
| // 		if (iterationDone) { |  | ||||||
| // 			if (isDone) { |  | ||||||
| // 				clearInterval(iid); |  | ||||||
| // 				return; |  | ||||||
| // 			} |  | ||||||
|  |  | ||||||
| // 			iterationDone = false; |  | ||||||
| // 			callback(done).then(() => { |  | ||||||
| // 				iterationDone = true; |  | ||||||
| // 			}); |  | ||||||
| // 		} |  | ||||||
| // 	}, 100); |  | ||||||
| // } |  | ||||||
|  |  | ||||||
| // function myPromise() { |  | ||||||
| // 	return new Promise((resolve, reject) => { |  | ||||||
| // 		setTimeout(() => { |  | ||||||
| // 			resolve(); |  | ||||||
| // 		}, 500); |  | ||||||
| // 	}); |  | ||||||
| // } |  | ||||||
|  |  | ||||||
| // let counter = 0; |  | ||||||
| // whilePromise((done) => { |  | ||||||
| // 	return myPromise().then(() => { |  | ||||||
| // 		counter++; |  | ||||||
| // 		console.info(counter); |  | ||||||
| // 		if (counter == 5) { |  | ||||||
| // 			done(); |  | ||||||
| // 		} |  | ||||||
| // 	}); |  | ||||||
| // }); |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| let fileDriver = new FileApiDriverLocal(); | let fileDriver = new FileApiDriverLocal(); | ||||||
| let fileApi = new FileApi('/home/laurent/Temp/TestImport', fileDriver); | let fileApi = new FileApi('/home/laurent/Temp/TestImport', fileDriver); | ||||||
| let synchronizer = new Synchronizer(db, fileApi); | let synchronizer = new Synchronizer(db, fileApi); | ||||||
| @@ -124,9 +73,11 @@ function createLocalItems() { | |||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | db.setDebugEnabled(true); | ||||||
| db.open({ name: '/home/laurent/Temp/test-sync.sqlite3' }).then(() => { | db.open({ name: '/home/laurent/Temp/test-sync.sqlite3' }).then(() => { | ||||||
|  	BaseModel.db_ = db; |  	BaseModel.db_ = db; | ||||||
|  	return clearDatabase().then(createLocalItems); |  	//return clearDatabase(); | ||||||
|  |  	//return clearDatabase().then(createLocalItems); | ||||||
| }).then(() => { | }).then(() => { | ||||||
| 	return synchronizer.start(); | 	return synchronizer.start(); | ||||||
| }).catch((error) => { | }).catch((error) => { | ||||||
|   | |||||||
| @@ -28,7 +28,6 @@ function setupDatabase(done) { | |||||||
| 	return fs.unlink(filePath).catch(() => { | 	return fs.unlink(filePath).catch(() => { | ||||||
| 		// Don't care if the file doesn't exist | 		// Don't care if the file doesn't exist | ||||||
| 	}).then(() => { | 	}).then(() => { | ||||||
| 		//console.info('Opening database ' + filePath); |  | ||||||
| 		database_ = new Database(new DatabaseDriverNode()); | 		database_ = new Database(new DatabaseDriverNode()); | ||||||
| 		database_.setDebugEnabled(false); | 		database_.setDebugEnabled(false); | ||||||
| 		return database_.open({ name: filePath }).then(() => { | 		return database_.open({ name: filePath }).then(() => { | ||||||
|   | |||||||
| @@ -36,11 +36,20 @@ class BaseModel { | |||||||
| 		return this.db().tableFieldNames(this.tableName()); | 		return this.db().tableFieldNames(this.tableName()); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	static fieldType(name) { | ||||||
|  | 		let fields = this.fields(); | ||||||
|  | 		for (let i = 0; i < fields.length; i++) { | ||||||
|  | 			if (fields[i].name == name) return fields[i].type; | ||||||
|  | 		} | ||||||
|  | 		throw new Error('Unknown field: ' + name); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	static fields() { | 	static fields() { | ||||||
| 		return this.db().tableFields(this.tableName()); | 		return this.db().tableFields(this.tableName()); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	static identifyItemType(item) { | 	static identifyItemType(item) { | ||||||
|  | 		if (!item) throw new Error('Cannot identify undefined item'); | ||||||
| 		if ('body' in item || ('parent_id' in item && !!item.parent_id)) return BaseModel.ITEM_TYPE_NOTE; | 		if ('body' in item || ('parent_id' in item && !!item.parent_id)) return BaseModel.ITEM_TYPE_NOTE; | ||||||
| 		if ('sync_time' in item) return BaseModel.ITEM_TYPE_FOLDER; | 		if ('sync_time' in item) return BaseModel.ITEM_TYPE_FOLDER; | ||||||
| 		throw new Error('Cannot identify item: ' + JSON.stringify(item)); | 		throw new Error('Cannot identify item: ' + JSON.stringify(item)); | ||||||
| @@ -161,7 +170,7 @@ class BaseModel { | |||||||
| 		queries.push(saveQuery); | 		queries.push(saveQuery); | ||||||
|  |  | ||||||
| 		// TODO: DISABLED DISABLED DISABLED DISABLED DISABLED DISABLED DISABLED DISABLED DISABLED DISABLED  | 		// TODO: DISABLED DISABLED DISABLED DISABLED DISABLED DISABLED DISABLED DISABLED DISABLED DISABLED  | ||||||
| 		if (0&& ptions.trackChanges && this.trackChanges()) { | 		if (0&& options.trackChanges && this.trackChanges()) { | ||||||
| 			// Cannot import this class the normal way due to cyclical dependencies between Change and BaseModel | 			// Cannot import this class the normal way due to cyclical dependencies between Change and BaseModel | ||||||
| 			// which are not handled by React Native. | 			// which are not handled by React Native. | ||||||
| 			const { Change } = require('src/models/change.js'); | 			const { Change } = require('src/models/change.js'); | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ CREATE TABLE folders ( | |||||||
| 	created_time INT NOT NULL DEFAULT 0, | 	created_time INT NOT NULL DEFAULT 0, | ||||||
| 	updated_time INT NOT NULL DEFAULT 0, | 	updated_time INT NOT NULL DEFAULT 0, | ||||||
| 	sync_time INT NOT NULL DEFAULT 0, | 	sync_time INT NOT NULL DEFAULT 0, | ||||||
| 	is_default BOOLEAN NOT NULL DEFAULT 0 | 	is_default INT NOT NULL DEFAULT 0 | ||||||
| ); | ); | ||||||
|  |  | ||||||
| CREATE TABLE notes ( | CREATE TABLE notes ( | ||||||
| @@ -28,9 +28,9 @@ CREATE TABLE notes ( | |||||||
| 	source TEXT NOT NULL DEFAULT "", | 	source TEXT NOT NULL DEFAULT "", | ||||||
| 	author TEXT NOT NULL DEFAULT "", | 	author TEXT NOT NULL DEFAULT "", | ||||||
| 	source_url TEXT NOT NULL DEFAULT "", | 	source_url TEXT NOT NULL DEFAULT "", | ||||||
| 	is_todo BOOLEAN NOT NULL DEFAULT 0, | 	is_todo INT NOT NULL DEFAULT 0, | ||||||
| 	todo_due INT NOT NULL DEFAULT 0, | 	todo_due INT NOT NULL DEFAULT 0, | ||||||
| 	todo_completed BOOLEAN NOT NULL DEFAULT 0, | 	todo_completed INT NOT NULL DEFAULT 0, | ||||||
| 	source_application TEXT NOT NULL DEFAULT "", | 	source_application TEXT NOT NULL DEFAULT "", | ||||||
| 	application_data TEXT NOT NULL DEFAULT "", | 	application_data TEXT NOT NULL DEFAULT "", | ||||||
| 	\`order\` INT NOT NULL DEFAULT 0 | 	\`order\` INT NOT NULL DEFAULT 0 | ||||||
| @@ -162,9 +162,7 @@ class Database { | |||||||
| 		if (this.inTransaction_) { | 		if (this.inTransaction_) { | ||||||
| 			return new Promise((resolve, reject) => { | 			return new Promise((resolve, reject) => { | ||||||
| 				let iid = setInterval(() => { | 				let iid = setInterval(() => { | ||||||
| 					console.info('Waiting...'); |  | ||||||
| 					if (!this.inTransaction_) { | 					if (!this.inTransaction_) { | ||||||
| 						console.info('OKKKKKKKKKKK'); |  | ||||||
| 						clearInterval(iid); | 						clearInterval(iid); | ||||||
| 						this.transactionExecBatch(queries).then(() => { | 						this.transactionExecBatch(queries).then(() => { | ||||||
| 							resolve(); | 							resolve(); | ||||||
| @@ -224,7 +222,6 @@ class Database { | |||||||
| 		if (value === null || value === undefined) return null; | 		if (value === null || value === undefined) return null; | ||||||
| 		if (type == this.TYPE_INT) return Number(value); | 		if (type == this.TYPE_INT) return Number(value); | ||||||
| 		if (type == this.TYPE_TEXT) return value; | 		if (type == this.TYPE_TEXT) return value; | ||||||
| 		if (type == this.TYPE_BOOLEAN) return !!Number(value); |  | ||||||
| 		if (type == this.TYPE_NUMERIC) return Number(value); | 		if (type == this.TYPE_NUMERIC) return Number(value); | ||||||
| 		throw new Error('Unknown type: ' + type); | 		throw new Error('Unknown type: ' + type); | ||||||
| 	} | 	} | ||||||
| @@ -249,7 +246,7 @@ class Database { | |||||||
| 	logQuery(sql, params = null) { | 	logQuery(sql, params = null) { | ||||||
| 		if (!this.debugMode()) return; | 		if (!this.debugMode()) return; | ||||||
| 		if (params !== null) { | 		if (params !== null) { | ||||||
| 			Log.debug('DB: ' + sql, params); | 			Log.debug('DB: ' + sql, JSON.stringify(params)); | ||||||
| 		} else { | 		} else { | ||||||
| 			Log.debug('DB: ' + sql); | 			Log.debug('DB: ' + sql); | ||||||
| 		} | 		} | ||||||
| @@ -430,7 +427,6 @@ class Database { | |||||||
|  |  | ||||||
| Database.TYPE_INT = 1; | Database.TYPE_INT = 1; | ||||||
| Database.TYPE_TEXT = 2; | Database.TYPE_TEXT = 2; | ||||||
| Database.TYPE_BOOLEAN = 3; | Database.TYPE_NUMERIC = 3; | ||||||
| Database.TYPE_NUMERIC = 4; |  | ||||||
|  |  | ||||||
| export { Database }; | export { Database }; | ||||||
| @@ -13,11 +13,25 @@ class FileApi { | |||||||
| 		return output; | 		return output; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	scopeItemToBaseDir_(item) { | ||||||
|  | 		let output = Object.assign({}, item); | ||||||
|  | 		output.path = item.path.substr(this.baseDir_.length + 1); | ||||||
|  | 		return output; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	scopeItemsToBaseDir_(items) { | ||||||
|  | 		let output = []; | ||||||
|  | 		for (let i = 0; i < items.length; i++) { | ||||||
|  | 			output.push(this.scopeItemToBaseDir_(items[i])); | ||||||
|  | 		} | ||||||
|  | 		return output; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	listDirectories() { | 	listDirectories() { | ||||||
| 		return this.driver_.list(this.fullPath_('')).then((items) => { | 		return this.driver_.list(this.fullPath_('')).then((items) => { | ||||||
| 			let output = []; | 			let output = []; | ||||||
| 			for (let i = 0; i < items.length; i++) { | 			for (let i = 0; i < items.length; i++) { | ||||||
| 				if (items[i].isDir) output.push(items[i]); | 				if (items[i].isDir) output.push(this.scopeItemToBaseDir_(items[i])); | ||||||
| 			} | 			} | ||||||
| 			return output; | 			return output; | ||||||
| 		}); | 		}); | ||||||
| @@ -26,6 +40,7 @@ class FileApi { | |||||||
| 	list(path = '', recursive = false, context = null) { | 	list(path = '', recursive = false, context = null) { | ||||||
| 		let fullPath = this.fullPath_(path); | 		let fullPath = this.fullPath_(path); | ||||||
| 		return this.driver_.list(fullPath).then((items) => { | 		return this.driver_.list(fullPath).then((items) => { | ||||||
|  | 			items = this.scopeItemsToBaseDir_(items); | ||||||
| 			if (recursive) { | 			if (recursive) { | ||||||
| 				let chain = []; | 				let chain = []; | ||||||
| 				for (let i = 0; i < items.length; i++) { | 				for (let i = 0; i < items.length; i++) { | ||||||
|   | |||||||
							
								
								
									
										110
									
								
								ReactNativeClient/src/models/base-item.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								ReactNativeClient/src/models/base-item.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | |||||||
|  | import { BaseModel } from 'src/base-model.js'; | ||||||
|  | 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 moment from 'moment'; | ||||||
|  |  | ||||||
|  | class BaseItem extends BaseModel { | ||||||
|  |  | ||||||
|  | 	static useUuid() { | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static systemPath(item) { | ||||||
|  | 		return folderItemFilename(item) + '.md'; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static pathToId(path) { | ||||||
|  | 		let s = path.split('.'); | ||||||
|  | 		return s[0]; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static loadItemByPath(path) { | ||||||
|  | 		let id = this.pathToId(path); | ||||||
|  | 		return Note.load(id).then((item) => { | ||||||
|  | 			if (item) return item; | ||||||
|  | 			return Folder.load(id); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static toFriendlyString_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'; | ||||||
|  | 		} else if (propValue === null || propValue === undefined) { | ||||||
|  | 			propValue = ''; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return propValue; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static fromFriendlyString_format(propName, propValue) { | ||||||
|  | 		if (propName == 'type') return propValue; | ||||||
|  |  | ||||||
|  | 		if (['created_time', 'updated_time'].indexOf(propName) >= 0) { | ||||||
|  | 			if (!propValue) return 0; | ||||||
|  | 			propValue = moment(propValue, 'YYYY-MM-DD HH:mm:ssZ').unix(); | ||||||
|  | 		} else { | ||||||
|  | 			propValue = Database.formatValue(this.fieldType(propName), propValue); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return propValue; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static toFriendlyString(item, type = null, shownKeys = null) { | ||||||
|  | 		let output = []; | ||||||
|  |  | ||||||
|  | 		output.push(item.title); | ||||||
|  | 		output.push(''); | ||||||
|  | 		output.push(type == 'note' ? item.body : ''); | ||||||
|  | 		output.push(''); | ||||||
|  | 		for (let i = 0; i < shownKeys.length; i++) { | ||||||
|  | 			let v = item[shownKeys[i]]; | ||||||
|  | 			v = this.toFriendlyString_format(shownKeys[i], v); | ||||||
|  | 			output.push(shownKeys[i] + ': ' + v); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		output.push('type: ' + type); | ||||||
|  |  | ||||||
|  | 		return output.join("\n"); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static fromFriendlyString(content) { | ||||||
|  | 		let lines = content.split("\n"); | ||||||
|  | 		let output = {}; | ||||||
|  | 		let state = 'readingProps'; | ||||||
|  | 		let body = []; | ||||||
|  | 		for (let i = lines.length - 1; i >= 0; i--) { | ||||||
|  | 			let line = lines[i]; | ||||||
|  |  | ||||||
|  | 			if (state == 'readingProps') { | ||||||
|  | 				line = line.trim(); | ||||||
|  |  | ||||||
|  | 				if (line == '') { | ||||||
|  | 					state = 'readingBody'; | ||||||
|  | 					continue; | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				let p = line.indexOf(':'); | ||||||
|  | 				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); | ||||||
|  | 			} else if (state == 'readingBody') { | ||||||
|  | 				body.splice(0, 0, line); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if (body.length < 3) throw new Error('Invalid body size: ' + body.length + ': ' + content); | ||||||
|  |  | ||||||
|  | 		let title = body.splice(0, 2); | ||||||
|  | 		output.title = title[0]; | ||||||
|  | 		if (output.type == 'note') output.body = body.join("\n"); | ||||||
|  |  | ||||||
|  | 		return output; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { BaseItem }; | ||||||
| @@ -5,57 +5,16 @@ import { Note } from 'src/models/note.js'; | |||||||
| import { folderItemFilename } from 'src/string-utils.js' | import { folderItemFilename } from 'src/string-utils.js' | ||||||
| import { _ } from 'src/locale.js'; | import { _ } from 'src/locale.js'; | ||||||
| import moment from 'moment'; | import moment from 'moment'; | ||||||
|  | import { BaseItem } from 'src/models/base-item.js'; | ||||||
|  |  | ||||||
| class Folder extends BaseModel { | class Folder extends BaseItem { | ||||||
|  |  | ||||||
| 	static tableName() { | 	static tableName() { | ||||||
| 		return 'folders'; | 		return 'folders'; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	static filename(folder) { |  | ||||||
| 		return folderItemFilename(folder); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	static systemPath(parent, folder) { |  | ||||||
| 		return this.filename(folder); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	static systemMetadataPath(parent, folder) { |  | ||||||
| 		return this.systemPath(parent, folder) + '/.folder.md'; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// TODO: share with Note class |  | ||||||
| 	static toFriendlyString_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'); |  | ||||||
| 		} else if (propValue === null || propValue === undefined) { |  | ||||||
| 			propValue = ''; |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		return propValue; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// TODO: share with Note class |  | ||||||
| 	static toFriendlyString(folder) { | 	static toFriendlyString(folder) { | ||||||
| 		let shownKeys = ['created_time', 'updated_time']; | 		return super.toFriendlyString(folder, 'folder', ['id', 'created_time', 'updated_time']); | ||||||
| 		let output = []; |  | ||||||
|  |  | ||||||
| 		output.push(folder.title); |  | ||||||
| 		output.push(''); |  | ||||||
| 		output.push(''); // For consistency with the notes, leave an empty line where the body should be |  | ||||||
| 		output.push(''); |  | ||||||
| 		for (let i = 0; i < shownKeys.length; i++) { |  | ||||||
| 			let v = folder[shownKeys[i]]; |  | ||||||
| 			v = this.toFriendlyString_format(shownKeys[i], v); |  | ||||||
| 			output.push(shownKeys[i] + ': ' + v); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		return output.join("\n"); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	static useUuid() { |  | ||||||
| 		return true; |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	static itemType() { | 	static itemType() { | ||||||
|   | |||||||
| @@ -3,68 +3,17 @@ import { Log } from 'src/log.js'; | |||||||
| import { Folder } from 'src/models/folder.js'; | import { Folder } from 'src/models/folder.js'; | ||||||
| import { Geolocation } from 'src/geolocation.js'; | import { Geolocation } from 'src/geolocation.js'; | ||||||
| import { folderItemFilename } from 'src/string-utils.js' | import { folderItemFilename } from 'src/string-utils.js' | ||||||
|  | import { BaseItem } from 'src/models/base-item.js'; | ||||||
| import moment from 'moment'; | import moment from 'moment'; | ||||||
|  |  | ||||||
| class Note extends BaseModel { | class Note extends BaseItem { | ||||||
|  |  | ||||||
| 	static tableName() { | 	static tableName() { | ||||||
| 		return 'notes'; | 		return 'notes'; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	static toFriendlyString_format(propName, propValue) { | 	static toFriendlyString(note, type = null, shownKeys = null) { | ||||||
| 		if (['created_time', 'updated_time'].indexOf(propName) >= 0) { | 		return super.toFriendlyString(note, 'note', ["author", "longitude", "latitude", "is_todo", "todo_due", "todo_completed", 'created_time', 'updated_time', 'id', 'parent_id']); | ||||||
| 			if (!propValue) return ''; |  | ||||||
| 			propValue = moment.unix(propValue).format('YYYY-MM-DD hh:mm:ss'); |  | ||||||
| 		} else if (propValue === null || propValue === undefined) { |  | ||||||
| 			propValue = ''; |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		return propValue; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	static toFriendlyString(note) { |  | ||||||
| 		let shownKeys = ["author", "longitude", "latitude", "is_todo", "todo_due", "todo_completed", 'created_time', 'updated_time']; |  | ||||||
| 		let output = []; |  | ||||||
|  |  | ||||||
| 		output.push(note.title); |  | ||||||
| 		output.push(""); |  | ||||||
| 		output.push(note.body); |  | ||||||
| 		output.push(''); |  | ||||||
| 		for (let i = 0; i < shownKeys.length; i++) { |  | ||||||
| 			let v = note[shownKeys[i]]; |  | ||||||
| 			v = this.toFriendlyString_format(shownKeys[i], v); |  | ||||||
| 			output.push(shownKeys[i] + ': ' + v); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		return output.join("\n"); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// static fromFriendlyString(item) { |  | ||||||
| 	// 	let lines = []; |  | ||||||
| 	// 	// mynote |  | ||||||
|  |  | ||||||
| 	// 	// abcdefg\nsecond line\n\nline after two newline |  | ||||||
|  |  | ||||||
| 	// 	// author:  |  | ||||||
| 	// 	// longitude: -3.4596633911132812 |  | ||||||
| 	// 	// latitude: 48.73219093634444 |  | ||||||
| 	// 	// is_todo: 0 |  | ||||||
| 	// 	// todo_due: 0 |  | ||||||
| 	// 	// todo_completed: 0 |  | ||||||
| 	// 	// created_time: 2017-06-12 05:02:38 |  | ||||||
| 	// 	// updated_time: 2017-06-12 05:02:38 |  | ||||||
| 	// } |  | ||||||
|  |  | ||||||
| 	static filename(note) { |  | ||||||
| 		return folderItemFilename(note) + '.md'; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	static systemPath(parentFolder, note) { |  | ||||||
| 		return Folder.systemPath(null, parentFolder) + '/' + this.filename(note); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	static useUuid() { |  | ||||||
| 		return true; |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	static itemType() { | 	static itemType() { | ||||||
|   | |||||||
| @@ -20,6 +20,12 @@ class NoteFolderService extends BaseService { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// console.info(item); | ||||||
|  | 		// console.info(oldItem); | ||||||
|  | 		// console.info('DIFF', diff); | ||||||
|  | 		// return Promise.resolve(); | ||||||
|  |  | ||||||
|  |  | ||||||
| 		let ItemClass = null; | 		let ItemClass = null; | ||||||
| 		if (type == 'note') { | 		if (type == 'note') { | ||||||
| 			ItemClass = Note; | 			ItemClass = Note; | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import { Setting } from 'src/models/setting.js'; | |||||||
| import { Change } from 'src/models/change.js'; | import { Change } from 'src/models/change.js'; | ||||||
| import { Folder } from 'src/models/folder.js'; | import { Folder } from 'src/models/folder.js'; | ||||||
| import { Note } from 'src/models/note.js'; | import { Note } from 'src/models/note.js'; | ||||||
|  | import { BaseItem } from 'src/models/base-item.js'; | ||||||
| import { BaseModel } from 'src/base-model.js'; | import { BaseModel } from 'src/base-model.js'; | ||||||
| import { promiseChain } from 'src/promise-utils.js'; | import { promiseChain } from 'src/promise-utils.js'; | ||||||
| import { NoteFolderService } from 'src/services/note-folder-service.js'; | import { NoteFolderService } from 'src/services/note-folder-service.js'; | ||||||
| @@ -69,11 +70,11 @@ class Synchronizer { | |||||||
|  |  | ||||||
| 	moveConflict(item) { | 	moveConflict(item) { | ||||||
| 		// No need to handle folder conflicts | 		// No need to handle folder conflicts | ||||||
| 		if (item.isDir) return Promise.resolve(); | 		if (item.type == 'folder') return Promise.resolve(); | ||||||
|  |  | ||||||
| 		return this.conflictDir().then((conflictDirPath) => { | 		return this.conflictDir().then((conflictDirPath) => { | ||||||
| 			let p = path.basename(item.path).split('.'); | 			let p = path.basename(item.path).split('.'); | ||||||
| 			let pos = item.isDir ? p.length - 1 : p.length - 2; | 			let pos = item.type == 'folder' ? p.length - 1 : p.length - 2; | ||||||
| 			p.splice(pos, 0, moment().format('YYYYMMDDThhmmss')); | 			p.splice(pos, 0, moment().format('YYYYMMDDThhmmss')); | ||||||
| 			let newPath = p.join('.'); | 			let newPath = p.join('.'); | ||||||
| 			return this.api().move(item.path, conflictDirPath + '/' + newPath); | 			return this.api().move(item.path, conflictDirPath + '/' + newPath); | ||||||
| @@ -106,34 +107,35 @@ class Synchronizer { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	dbItemToSyncItem(dbItem) { | 	dbItemToSyncItem(dbItem) { | ||||||
| 		let p = Promise.resolve(null); | 		if (!dbItem) return null; | ||||||
|  |  | ||||||
| 		let itemType = BaseModel.identifyItemType(dbItem); | 		let itemType = BaseModel.identifyItemType(dbItem); | ||||||
| 		let ItemClass = null; |  | ||||||
|  |  | ||||||
| 		if (itemType == BaseModel.ITEM_TYPE_NOTE) { | 		return { | ||||||
| 			ItemClass = Note; | 			type: itemType == BaseModel.ITEM_TYPE_FOLDER ? 'folder' : 'note', | ||||||
| 			p = Folder.load(dbItem.parent_id); | 			path: Folder.systemPath(dbItem), | ||||||
| 		} else { | 			syncTime: dbItem.sync_time, | ||||||
| 			ItemClass = Folder; | 			updatedTime: dbItem.updated_time, | ||||||
| 		} | 			dbItem: dbItem, | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 		return p.then((dbParent) => { | 	remoteItemToSyncItem(remoteItem) { | ||||||
| 			let path = ItemClass.systemPath(dbParent, dbItem); | 		if (!remoteItem) return null; | ||||||
| 			return { |  | ||||||
| 				isDir: itemType == BaseModel.ITEM_TYPE_FOLDER, | 		return { | ||||||
| 				path: path, | 			type: remoteItem.content.type, | ||||||
| 				syncTime: dbItem.sync_time, | 			path: remoteItem.path, | ||||||
| 				updatedTime: dbItem.updated_time, | 			syncTime: 0, | ||||||
| 				dbParent: dbParent, | 			updatedTime: remoteItem.updatedTime, | ||||||
| 				dbItem: dbItem, | 			remoteItem: remoteItem, | ||||||
| 			}; | 		}; | ||||||
| 		}); |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	syncAction(localItem, remoteItem, deletedLocalPaths) { | 	syncAction(localItem, remoteItem, deletedLocalPaths) { | ||||||
| 		let output = this.syncActions(localItem ? [localItem] : [], remoteItem ? [remoteItem] : [], deletedLocalPaths); | 		let output = this.syncActions(localItem ? [localItem] : [], remoteItem ? [remoteItem] : [], deletedLocalPaths); | ||||||
| 		if (output.length !== 1) throw new Error('Invalid number of actions returned'); | 		if (output.length > 1) throw new Error('Invalid number of actions returned'); | ||||||
| 		return output[0]; | 		return output.length ? output[0] : null; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Assumption: it's not possible to, for example, have a directory one the dest | 	// Assumption: it's not possible to, for example, have a directory one the dest | ||||||
| @@ -141,13 +143,16 @@ class Synchronizer { | |||||||
| 	// file and directory names are UUID so should be unique. | 	// file and directory names are UUID so should be unique. | ||||||
| 	// Each item must have these properties: | 	// Each item must have these properties: | ||||||
| 	// - path | 	// - path | ||||||
| 	// - isDir | 	// - type | ||||||
| 	// - syncTime | 	// - syncTime | ||||||
| 	// - updatedTime | 	// - updatedTime | ||||||
| 	syncActions(localItems, remoteItems, deletedLocalPaths) { | 	syncActions(localItems, remoteItems, deletedLocalPaths) { | ||||||
| 		let output = []; | 		let output = []; | ||||||
| 		let donePaths = []; | 		let donePaths = []; | ||||||
|  |  | ||||||
|  | 		// console.info('=================================================='); | ||||||
|  | 		// console.info(localItems, remoteItems); | ||||||
|  |  | ||||||
| 		for (let i = 0; i < localItems.length; i++) { | 		for (let i = 0; i < localItems.length; i++) { | ||||||
| 			let local = localItems[i]; | 			let local = localItems[i]; | ||||||
| 			let remote = this.itemByPath(remoteItems, local.path); | 			let remote = this.itemByPath(remoteItems, local.path); | ||||||
| @@ -177,7 +182,7 @@ class Synchronizer { | |||||||
| 					action.dest = 'remote'; | 					action.dest = 'remote'; | ||||||
| 				} else { | 				} else { | ||||||
| 					action.type = 'conflict'; | 					action.type = 'conflict'; | ||||||
| 					if (local.isDir) { | 					if (local.type == 'folder') { | ||||||
| 						// For folders, currently we don't completely handle conflicts, we just | 						// For folders, currently we don't completely handle conflicts, we just | ||||||
| 						// we just update the local dir (.folder metadata file) with the remote | 						// 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 | 						// version. It means the local version is lost but shouldn't be a big deal | ||||||
| @@ -229,334 +234,12 @@ class Synchronizer { | |||||||
| 			output.push(action); | 			output.push(action); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// console.info('-----------------------------------------'); | ||||||
|  | 		// console.info(output); | ||||||
|  |  | ||||||
| 		return output; | 		return output; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	processState_uploadChanges() { |  | ||||||
| 		let remoteFiles = []; |  | ||||||
| 		let processedChangeIds = []; |  | ||||||
| 		return this.api().list('', true).then((items) => { |  | ||||||
| 			remoteFiles = items; |  | ||||||
| 			return Change.all(); |  | ||||||
| 		}).then((changes) => { |  | ||||||
| 			let mergedChanges = Change.mergeChanges(changes); |  | ||||||
| 			let chain = []; |  | ||||||
| 			const lastSyncTime = Setting.value('sync.lastUpdateTime'); |  | ||||||
| 			for (let i = 0; i < mergedChanges.length; i++) { |  | ||||||
| 				let c = mergedChanges[i]; |  | ||||||
| 				chain.push(() => { |  | ||||||
| 					let p = null; |  | ||||||
|  |  | ||||||
| 					let ItemClass = null;					 |  | ||||||
| 					if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) { |  | ||||||
| 						ItemClass = Folder; |  | ||||||
| 					} else if (c.item_type == BaseModel.ITEM_TYPE_NOTE) { |  | ||||||
| 						ItemClass = Note; |  | ||||||
| 					} |  | ||||||
|  |  | ||||||
| 					if (c.type == Change.TYPE_NOOP) { |  | ||||||
| 						p = Promise.resolve(); |  | ||||||
| 					} else if (c.type == Change.TYPE_CREATE) { |  | ||||||
| 						p = this.loadParentAndItem(c).then((result) => { |  | ||||||
| 							let item = result.item; |  | ||||||
| 							let parent = result.parent; |  | ||||||
| 							if (!item) return; // Change refers to an object that doesn't exist (has probably been deleted directly in the database) |  | ||||||
|  |  | ||||||
| 							let path = ItemClass.systemPath(parent, item); |  | ||||||
|  |  | ||||||
| 							let remoteFile = this.remoteFileByPath(remoteFiles, path); |  | ||||||
| 							let p = null; |  | ||||||
| 							if (remoteFile) { |  | ||||||
| 								p = this.moveConflict(remoteFile); |  | ||||||
| 							} else { |  | ||||||
| 								p = Promise.resolve(); |  | ||||||
| 							} |  | ||||||
|  |  | ||||||
| 							return p.then(() => { |  | ||||||
| 								if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) { |  | ||||||
| 									return this.api().mkdir(path).then(() => { |  | ||||||
| 										return this.api().put(Folder.systemMetadataPath(parent, item), Folder.toFriendlyString(item)); |  | ||||||
| 									}).then(() => { |  | ||||||
| 										return this.api().setTimestamp(Folder.systemMetadataPath(parent, item), item.updated_time); |  | ||||||
| 									}); |  | ||||||
| 								} else { |  | ||||||
| 									return this.api().put(path, Note.toFriendlyString(item)).then(() => { |  | ||||||
| 										return this.api().setTimestamp(path, item.updated_time); |  | ||||||
| 									}); |  | ||||||
| 								} |  | ||||||
| 							}); |  | ||||||
| 						}); |  | ||||||
| 					} else if (c.type == Change.TYPE_UPDATE) { |  | ||||||
| 						p = this.loadParentAndItem(c).then((result) => { |  | ||||||
| 							if (!result.item) return; // Change refers to an object that doesn't exist (has probably been deleted directly in the database) |  | ||||||
|  |  | ||||||
| 							let path = ItemClass.systemPath(result.parent, result.item); |  | ||||||
|  |  | ||||||
| 							let remoteFile = this.remoteFileByPath(remoteFiles, path); |  | ||||||
| 							let p = null; |  | ||||||
| 							if (remoteFile && remoteFile.updatedTime > lastSyncTime) { |  | ||||||
| 								console.info('CONFLICT:', lastSyncTime, remoteFile); |  | ||||||
| 								//console.info(moment.unix(remoteFile.updatedTime), moment.unix(result.item.updated_time)); |  | ||||||
| 								p = this.moveConflict(remoteFile); |  | ||||||
| 							} else { |  | ||||||
| 								p = Promise.resolve(); |  | ||||||
| 							} |  | ||||||
|  |  | ||||||
| 							console.info('Uploading change:', JSON.stringify(result.item)); |  | ||||||
|  |  | ||||||
| 							return p.then(() => { |  | ||||||
| 								if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) { |  | ||||||
| 									return this.api().put(Folder.systemMetadataPath(result.parent, result.item), Folder.toFriendlyString(result.item)); |  | ||||||
| 								} else { |  | ||||||
| 									return this.api().put(path, Note.toFriendlyString(result.item)); |  | ||||||
| 								} |  | ||||||
| 							}); |  | ||||||
| 						}); |  | ||||||
| 					} |  | ||||||
|  |  | ||||||
| 					// TODO: handle DELETE |  | ||||||
|  |  | ||||||
| 					return p.then(() => { |  | ||||||
| 						processedChangeIds = processedChangeIds.concat(c.ids); |  | ||||||
| 					}).catch((error) => { |  | ||||||
| 						Log.warn('Failed applying changes', c.ids, error); |  | ||||||
| 						// This is fine - trying to apply changes to an object that has been deleted |  | ||||||
| 						// if (error.type == 'NotFoundException') { |  | ||||||
| 						// 	processedChangeIds = processedChangeIds.concat(c.ids); |  | ||||||
| 						// } else { |  | ||||||
| 						// 	throw error; |  | ||||||
| 						// } |  | ||||||
| 					}); |  | ||||||
| 				}); |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			return promiseChain(chain); |  | ||||||
| 		// }).then(() => { |  | ||||||
| 		// 	console.info(remoteFiles); |  | ||||||
| 		// 	for (let i = 0; i < remoteFiles.length; i++) { |  | ||||||
| 		// 		const remoteFile = remoteFiles[i]; |  | ||||||
| 				 |  | ||||||
| 		// 	} |  | ||||||
| 		}).catch((error) => { |  | ||||||
| 			Log.error('Synchronization was interrupted due to an error:', error); |  | ||||||
| 		}).then(() => { |  | ||||||
| 			//Log.info('IDs to delete: ', processedChangeIds); |  | ||||||
| 			//return Change.deleteMultiple(processedChangeIds); |  | ||||||
| 		}).then(() => { |  | ||||||
| 			this.processState('downloadChanges'); |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 		// }).then(() => { |  | ||||||
| 		// 	return Change.all(); |  | ||||||
| 		// }).then((changes) => { |  | ||||||
| 		// 	let mergedChanges = Change.mergeChanges(changes); |  | ||||||
| 		// 	let chain = []; |  | ||||||
| 		// 	let processedChangeIds = []; |  | ||||||
| 		// 	for (let i = 0; i < mergedChanges.length; i++) { |  | ||||||
| 		// 		let c = mergedChanges[i]; |  | ||||||
| 		// 		chain.push(() => { |  | ||||||
| 		// 			let p = null; |  | ||||||
|  |  | ||||||
| 		// 			let ItemClass = null;					 |  | ||||||
| 		// 			let path = null; |  | ||||||
| 		// 			if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) { |  | ||||||
| 		// 				ItemClass = Folder; |  | ||||||
| 		// 				path = 'folders'; |  | ||||||
| 		// 			} else if (c.item_type == BaseModel.ITEM_TYPE_NOTE) { |  | ||||||
| 		// 				ItemClass = Note; |  | ||||||
| 		// 				path = 'notes'; |  | ||||||
| 		// 			} |  | ||||||
|  |  | ||||||
| 		// 			if (c.type == Change.TYPE_NOOP) { |  | ||||||
| 		// 				p = Promise.resolve(); |  | ||||||
| 		// 			} else if (c.type == Change.TYPE_CREATE) { |  | ||||||
| 		// 				p = this.loadParentAndItem(c).then((result) => { |  | ||||||
| 		// 					// let options = { |  | ||||||
| 		// 					// 	contents: Note.toFriendlyString(result.item), |  | ||||||
| 		// 					// 	path: Note.systemPath(result.parent, result.item), |  | ||||||
| 		// 					// 	mode: 'overwrite', |  | ||||||
| 		// 					// 	// client_modified:  |  | ||||||
| 		// 					// };						 |  | ||||||
|  |  | ||||||
| 		// 					// return this.api().filesUpload(options).then((result) => { |  | ||||||
| 		// 					// 	console.info('DROPBOX', result); |  | ||||||
| 		// 					// }); |  | ||||||
| 		// 				}); |  | ||||||
| 		// 				// p = ItemClass.load(c.item_id).then((item) => { |  | ||||||
|  |  | ||||||
| 		// 				// 	console.info(item); |  | ||||||
| 		// 				// 	let options = { |  | ||||||
| 		// 				// 		contents: Note.toFriendlyString(item), |  | ||||||
| 		// 				// 		path: Note.systemPath(item), |  | ||||||
| 		// 				// 		mode: 'overwrite', |  | ||||||
| 		// 				// 		// client_modified:  |  | ||||||
| 		// 				// 	}; |  | ||||||
|  |  | ||||||
| 		// 				// 	// console.info(options); |  | ||||||
|  |  | ||||||
| 		// 				// 	//let content = Note.toFriendlyString(item); |  | ||||||
| 		// 				// 	//console.info(content); |  | ||||||
|  |  | ||||||
| 		// 				// 	//console.info('SYNC', item); |  | ||||||
| 		// 				// 	//return this.api().put(path + '/' + item.id, null, item); |  | ||||||
| 		// 				// }); |  | ||||||
| 		// 			} else if (c.type == Change.TYPE_UPDATE) { |  | ||||||
| 		// 				p = ItemClass.load(c.item_id).then((item) => { |  | ||||||
| 		// 					//return this.api().patch(path + '/' + item.id, null, item); |  | ||||||
| 		// 				}); |  | ||||||
| 		// 			} else if (c.type == Change.TYPE_DELETE) { |  | ||||||
| 		// 				p = this.api().delete(path + '/' + c.item_id); |  | ||||||
| 		// 			} |  | ||||||
|  |  | ||||||
| 		// 			return p.then(() => { |  | ||||||
| 		// 				processedChangeIds = processedChangeIds.concat(c.ids); |  | ||||||
| 		// 			}).catch((error) => { |  | ||||||
| 		// 				// Log.warn('Failed applying changes', c.ids, error.message, error.type); |  | ||||||
| 		// 				// This is fine - trying to apply changes to an object that has been deleted |  | ||||||
| 		// 				if (error.type == 'NotFoundException') { |  | ||||||
| 		// 					processedChangeIds = processedChangeIds.concat(c.ids); |  | ||||||
| 		// 				} else { |  | ||||||
| 		// 					throw error; |  | ||||||
| 		// 				} |  | ||||||
| 		// 			}); |  | ||||||
| 		// 		}); |  | ||||||
| 		// 	} |  | ||||||
|  |  | ||||||
| 		// 	return promiseChain(chain).catch((error) => { |  | ||||||
| 		// 		Log.warn('Synchronization was interrupted due to an error:', error); |  | ||||||
| 		// 	}).then(() => { |  | ||||||
| 		// 		// Log.info('IDs to delete: ', processedChangeIds); |  | ||||||
| 		// 		// Change.deleteMultiple(processedChangeIds); |  | ||||||
| 		// 	}).then(() => { |  | ||||||
| 		// 		this.processState('downloadChanges'); |  | ||||||
| 		// 	}); |  | ||||||
| 		// }); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	processState_downloadChanges() { |  | ||||||
| 		// return this.api().list('', true).then((items) => { |  | ||||||
| 		// 	remoteFiles = items; |  | ||||||
| 		// 	return Change.all(); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 		// let maxRevId = null; |  | ||||||
| 		// let hasMore = false; |  | ||||||
| 		// this.api().get('synchronizer', { rev_id: Setting.value('sync.lastRevId') }).then((syncOperations) => { |  | ||||||
| 		// 	hasMore = syncOperations.has_more; |  | ||||||
| 		// 	let chain = []; |  | ||||||
| 		// 	for (let i = 0; i < syncOperations.items.length; i++) { |  | ||||||
| 		// 		let syncOp = syncOperations.items[i]; |  | ||||||
| 		// 		if (syncOp.id > maxRevId) maxRevId = syncOp.id; |  | ||||||
|  |  | ||||||
| 		// 		let ItemClass = null;					 |  | ||||||
| 		// 		if (syncOp.item_type == 'folder') { |  | ||||||
| 		// 			ItemClass = Folder; |  | ||||||
| 		// 		} else if (syncOp.item_type == 'note') { |  | ||||||
| 		// 			ItemClass = Note; |  | ||||||
| 		// 		} |  | ||||||
|  |  | ||||||
| 		// 		if (syncOp.type == 'create') { |  | ||||||
| 		// 			chain.push(() => { |  | ||||||
| 		// 				let item = ItemClass.fromApiResult(syncOp.item); |  | ||||||
| 		// 				// TODO: automatically handle NULL fields by checking type and default value of field |  | ||||||
| 		// 				if ('parent_id' in item && !item.parent_id) item.parent_id = ''; |  | ||||||
| 		// 				return ItemClass.save(item, { isNew: true, trackChanges: false }); |  | ||||||
| 		// 			}); |  | ||||||
| 		// 		} |  | ||||||
|  |  | ||||||
| 		// 		if (syncOp.type == 'update') { |  | ||||||
| 		// 			chain.push(() => { |  | ||||||
| 		// 				return ItemClass.load(syncOp.item_id).then((item) => { |  | ||||||
| 		// 					if (!item) return; |  | ||||||
| 		// 					item = ItemClass.applyPatch(item, syncOp.item); |  | ||||||
| 		// 					return ItemClass.save(item, { trackChanges: false }); |  | ||||||
| 		// 				}); |  | ||||||
| 		// 			}); |  | ||||||
| 		// 		} |  | ||||||
|  |  | ||||||
| 		// 		if (syncOp.type == 'delete') { |  | ||||||
| 		// 			chain.push(() => { |  | ||||||
| 		// 				return ItemClass.delete(syncOp.item_id, { trackChanges: false }); |  | ||||||
| 		// 			}); |  | ||||||
| 		// 		} |  | ||||||
| 		// 	} |  | ||||||
| 		// 	return promiseChain(chain); |  | ||||||
| 		// }).then(() => { |  | ||||||
| 		// 	Log.info('All items synced. has_more = ', hasMore); |  | ||||||
| 		// 	if (maxRevId) { |  | ||||||
| 		// 		Setting.setValue('sync.lastRevId', maxRevId); |  | ||||||
| 		// 		return Setting.saveAll(); |  | ||||||
| 		// 	} |  | ||||||
| 		// }).then(() => { |  | ||||||
| 		// 	if (hasMore) { |  | ||||||
| 		// 		this.processState('downloadChanges'); |  | ||||||
| 		// 	} else { |  | ||||||
| 		// 		this.processState('idle'); |  | ||||||
| 		// 	} |  | ||||||
| 		// }).catch((error) => { |  | ||||||
| 		// 	Log.warn('Sync error', error); |  | ||||||
| 		// }); |  | ||||||
|  |  | ||||||
| 		// let maxRevId = null; |  | ||||||
| 		// let hasMore = false; |  | ||||||
| 		// this.api().get('synchronizer', { rev_id: Setting.value('sync.lastRevId') }).then((syncOperations) => { |  | ||||||
| 		// 	hasMore = syncOperations.has_more; |  | ||||||
| 		// 	let chain = []; |  | ||||||
| 		// 	for (let i = 0; i < syncOperations.items.length; i++) { |  | ||||||
| 		// 		let syncOp = syncOperations.items[i]; |  | ||||||
| 		// 		if (syncOp.id > maxRevId) maxRevId = syncOp.id; |  | ||||||
|  |  | ||||||
| 		// 		let ItemClass = null;					 |  | ||||||
| 		// 		if (syncOp.item_type == 'folder') { |  | ||||||
| 		// 			ItemClass = Folder; |  | ||||||
| 		// 		} else if (syncOp.item_type == 'note') { |  | ||||||
| 		// 			ItemClass = Note; |  | ||||||
| 		// 		} |  | ||||||
|  |  | ||||||
| 		// 		if (syncOp.type == 'create') { |  | ||||||
| 		// 			chain.push(() => { |  | ||||||
| 		// 				let item = ItemClass.fromApiResult(syncOp.item); |  | ||||||
| 		// 				// TODO: automatically handle NULL fields by checking type and default value of field |  | ||||||
| 		// 				if ('parent_id' in item && !item.parent_id) item.parent_id = ''; |  | ||||||
| 		// 				return ItemClass.save(item, { isNew: true, trackChanges: false }); |  | ||||||
| 		// 			}); |  | ||||||
| 		// 		} |  | ||||||
|  |  | ||||||
| 		// 		if (syncOp.type == 'update') { |  | ||||||
| 		// 			chain.push(() => { |  | ||||||
| 		// 				return ItemClass.load(syncOp.item_id).then((item) => { |  | ||||||
| 		// 					if (!item) return; |  | ||||||
| 		// 					item = ItemClass.applyPatch(item, syncOp.item); |  | ||||||
| 		// 					return ItemClass.save(item, { trackChanges: false }); |  | ||||||
| 		// 				}); |  | ||||||
| 		// 			}); |  | ||||||
| 		// 		} |  | ||||||
|  |  | ||||||
| 		// 		if (syncOp.type == 'delete') { |  | ||||||
| 		// 			chain.push(() => { |  | ||||||
| 		// 				return ItemClass.delete(syncOp.item_id, { trackChanges: false }); |  | ||||||
| 		// 			}); |  | ||||||
| 		// 		} |  | ||||||
| 		// 	} |  | ||||||
| 		// 	return promiseChain(chain); |  | ||||||
| 		// }).then(() => { |  | ||||||
| 		// 	Log.info('All items synced. has_more = ', hasMore); |  | ||||||
| 		// 	if (maxRevId) { |  | ||||||
| 		// 		Setting.setValue('sync.lastRevId', maxRevId); |  | ||||||
| 		// 		return Setting.saveAll(); |  | ||||||
| 		// 	} |  | ||||||
| 		// }).then(() => { |  | ||||||
| 		// 	if (hasMore) { |  | ||||||
| 		// 		this.processState('downloadChanges'); |  | ||||||
| 		// 	} else { |  | ||||||
| 		// 		this.processState('idle'); |  | ||||||
| 		// 	} |  | ||||||
| 		// }).catch((error) => { |  | ||||||
| 		// 	Log.warn('Sync error', error); |  | ||||||
| 		// }); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	processState(state) { | 	processState(state) { | ||||||
| 		Log.info('Sync: processing: ' + state); | 		Log.info('Sync: processing: ' + state); | ||||||
| 		this.state_ = state; | 		this.state_ = state; | ||||||
| @@ -564,37 +247,76 @@ class Synchronizer { | |||||||
| 		if (state == 'uploadChanges') { | 		if (state == 'uploadChanges') { | ||||||
| 			return this.processState_uploadChanges(); | 			return this.processState_uploadChanges(); | ||||||
| 		} else if (state == 'downloadChanges') { | 		} else if (state == 'downloadChanges') { | ||||||
| 			return this.processState('idle'); | 			//return this.processState('idle'); | ||||||
| 			//this.processState_downloadChanges(); | 			return this.processState_downloadChanges(); | ||||||
| 		} else if (state == 'idle') { | 		} else if (state == 'idle') { | ||||||
| 			// Nothing | 			// Nothing | ||||||
|  | 			return Promise.resolve(); | ||||||
| 		} else { | 		} else { | ||||||
| 			throw new Error('Invalid state: ' . state); | 			throw new Error('Invalid state: ' . state); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	processSyncAction(action) { | 	processSyncAction(action) { | ||||||
| 		// console.info(action); | 		console.info('Sync action: ', action); | ||||||
|  | 		//console.info('Sync action: ' + JSON.stringify(action)); | ||||||
|  |  | ||||||
|  | 		if (!action) return Promise.resolve(); | ||||||
|  |  | ||||||
| 		if (action.type == 'conflict') { | 		if (action.type == 'conflict') { | ||||||
|  |  | ||||||
| 		} else { | 		} else { | ||||||
| 			let item = action[action.dest == 'local' ? 'remote' : 'local']; | 			let syncItem = action[action.dest == 'local' ? 'remote' : 'local']; | ||||||
| 			let ItemClass = null; | 			let path = syncItem.path; | ||||||
| 			if (item.isDir) { |  | ||||||
| 				ItemClass = Folder; |  | ||||||
| 			} else { |  | ||||||
| 				ItemClass = Note; |  | ||||||
| 			} |  | ||||||
| 			let path = ItemClass.systemPath(item.dbParent, item.dbItem); |  | ||||||
|  |  | ||||||
| 			if (action.type == 'create') { | 			if (action.type == 'create') { | ||||||
| 				if (action.dest == 'remote') { | 				if (action.dest == 'remote') { | ||||||
| 					if (item.isDir) { | 					let content = null; | ||||||
| 						return this.api().mkdir(path); |  | ||||||
|  | 					if (syncItem.type == 'folder') { | ||||||
|  | 						content = Folder.toFriendlyString(syncItem.dbItem); | ||||||
| 					} else { | 					} else { | ||||||
| 						return this.api().put(path, Note.toFriendlyString(item.dbItem)); | 						content = Note.toFriendlyString(syncItem.dbItem); | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
|  | 					return this.api().put(path, content).then(() => { | ||||||
|  | 						return this.api().setTimestamp(path, syncItem.updatedTime); | ||||||
|  | 					}); | ||||||
|  | 				} else { | ||||||
|  | 					let dbItem = syncItem.remoteItem.content; | ||||||
|  | 					dbItem.sync_time = time.unix(); | ||||||
|  | 					if (syncItem.type == 'folder') { | ||||||
|  | 						return Folder.save(dbItem, { isNew: true }); | ||||||
|  | 					} else { | ||||||
|  | 						return Note.save(dbItem, { isNew: true }); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if (action.type == 'update') { | ||||||
|  | 				if (action.dest == 'remote') { | ||||||
|  | 					// let content = null; | ||||||
|  |  | ||||||
|  | 					// if (syncItem.type == 'folder') { | ||||||
|  | 					// 	content = Folder.toFriendlyString(syncItem.dbItem); | ||||||
|  | 					// } else { | ||||||
|  | 					// 	content = Note.toFriendlyString(syncItem.dbItem); | ||||||
|  | 					// } | ||||||
|  |  | ||||||
|  | 					// return this.api().put(path, content).then(() => { | ||||||
|  | 					// 	return this.api().setTimestamp(path, syncItem.updatedTime); | ||||||
|  | 					// }); | ||||||
|  | 				} else { | ||||||
|  | 					let dbItem = syncItem.remoteItem.content; | ||||||
|  | 					dbItem.sync_time = time.unix(); | ||||||
|  | 					return NoteFolderService.save(syncItem.type, dbItem, action.local.dbItem); | ||||||
|  | 					// let dbItem = syncItem.remoteItem.content; | ||||||
|  | 					// dbItem.sync_time = time.unix(); | ||||||
|  | 					// if (syncItem.type == 'folder') { | ||||||
|  | 					// 	return Folder.save(dbItem, { isNew: true }); | ||||||
|  | 					// } else { | ||||||
|  | 					// 	return Note.save(dbItem, { isNew: true }); | ||||||
|  | 					// } | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @@ -603,18 +325,14 @@ class Synchronizer { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	processLocalItem(dbItem) { | 	processLocalItem(dbItem) { | ||||||
| 		//console.info(dbItem); | 		let localItem = this.dbItemToSyncItem(dbItem); | ||||||
| 		let localItem = null; | 		 | ||||||
| 		return this.dbItemToSyncItem(dbItem).then((r) => { | 		return this.api().stat(localItem.path).then((remoteItem) => { | ||||||
| 			localItem = r; |  | ||||||
| 			return this.api().stat(localItem.path); |  | ||||||
| 		}).then((remoteItem) => { |  | ||||||
| 			let action = this.syncAction(localItem, remoteItem, []); | 			let action = this.syncAction(localItem, remoteItem, []); | ||||||
| 			//console.info(action); |  | ||||||
| 			return this.processSyncAction(action); | 			return this.processSyncAction(action); | ||||||
| 		}).then(() => { | 		}).then(() => { | ||||||
| 			dbItem.sync_time = time.unix(); | 			dbItem.sync_time = time.unix(); | ||||||
| 			if (localItem.isDir) { | 			if (localItem.type == 'folder') { | ||||||
| 				return Folder.save(dbItem); | 				return Folder.save(dbItem); | ||||||
| 			} else { | 			} else { | ||||||
| 				return Note.save(dbItem); | 				return Note.save(dbItem); | ||||||
| @@ -622,16 +340,21 @@ class Synchronizer { | |||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	start() { | 	processRemoteItem(remoteItem) { | ||||||
| 		Log.info('Sync: start'); | 		let remoteSyncItem = null; | ||||||
|  | 		return this.api().get(remoteItem.path).then((content) => { | ||||||
|  | 			remoteItem.content = Note.fromFriendlyString(content); | ||||||
|  | 			remoteSyncItem = this.remoteItemToSyncItem(remoteItem); | ||||||
|  | 			return BaseItem.loadItemByPath(remoteItem.path); | ||||||
|  | 		}).then((dbItem) => { | ||||||
|  | 			let localSyncItem = this.dbItemToSyncItem(dbItem); | ||||||
|  | 			let action = this.syncAction(localSyncItem, remoteSyncItem, []); | ||||||
|  | 			return this.processSyncAction(action); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 		if (this.state() != 'idle') { | 	processState_uploadChanges() { | ||||||
| 			return Promise.reject('Cannot start synchronizer because synchronization already in progress. State: ' + this.state()); | 		return new Promise((resolve, reject) => { | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		this.state_ = 'started'; |  | ||||||
|  |  | ||||||
| 		return this.api().listDirectories().then((items) => { |  | ||||||
| 			var context = null; | 			var context = null; | ||||||
| 			let limit = 2; | 			let limit = 2; | ||||||
| 			let finishedReading = false; | 			let finishedReading = false; | ||||||
| @@ -645,21 +368,17 @@ class Synchronizer { | |||||||
| 					let chain = []; | 					let chain = []; | ||||||
| 					for (let i = 0; i < result.items.length; i++) { | 					for (let i = 0; i < result.items.length; i++) { | ||||||
| 						let item = result.items[i]; | 						let item = result.items[i]; | ||||||
| 						console.info(JSON.stringify(item)); |  | ||||||
| 						chain.push(() => { | 						chain.push(() => { | ||||||
| 							//return Promise.resolve(); |  | ||||||
| 							return this.processLocalItem(item); | 							return this.processLocalItem(item); | ||||||
| 						}); | 						}); | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
| 					return promiseChain(chain).then(() => { | 					return promiseChain(chain).then(() => { | ||||||
| 						console.info('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'); |  | ||||||
| 						if (!context.hasMore) finishedReading = true; | 						if (!context.hasMore) finishedReading = true; | ||||||
| 						isReading = false; | 						isReading = false; | ||||||
| 					}); | 					}); | ||||||
| 				}).catch((error) => { | 				}).catch((error) => { | ||||||
| 					console.error(error); | 					rejec(error); | ||||||
| 					throw error; |  | ||||||
| 				}); | 				}); | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| @@ -667,25 +386,44 @@ class Synchronizer { | |||||||
| 				if (isReading) return; | 				if (isReading) return; | ||||||
| 				if (finishedReading) { | 				if (finishedReading) { | ||||||
| 					clearInterval(iid); | 					clearInterval(iid); | ||||||
|  | 					resolve(); | ||||||
| 					return; | 					return; | ||||||
| 				} | 				} | ||||||
| 				readItems(); | 				readItems(); | ||||||
| 			}, 100); | 			}, 100); | ||||||
|  |  | ||||||
| 		}).then(() => { | 		}).then(() => { | ||||||
| 			this.state_ = 'idle'; | 			//console.info('DOWNLOAD DISABLED'); | ||||||
|  | 			return this.processState('downloadChanges'); | ||||||
| 		}); | 		}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 		//return NoteFolderService.itemsThatNeedSync | 	processState_downloadChanges() { | ||||||
|  | 		return this.api().list().then((items) => { | ||||||
|  | 			let chain = []; | ||||||
|  | 			for (let i = 0; i < items.length; i++) { | ||||||
|  | 				chain.push(() => { | ||||||
|  | 					return this.processRemoteItem(items[i]); | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
|  | 			return promiseChain(chain); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 		 | 	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()) { | 		// if (!this.api().session()) { | ||||||
| 		// 	Log.info("Sync: cannot start synchronizer because user is not logged in."); | 		// 	Log.info("Sync: cannot start synchronizer because user is not logged in."); | ||||||
| 		// 	return; | 		// 	return; | ||||||
| 		// } | 		// } | ||||||
|  |  | ||||||
| 		//return this.processState('uploadChanges'); | 		return this.processState('uploadChanges'); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	 | 	 | ||||||
|   | |||||||
| @@ -30,7 +30,6 @@ class Synchronizer { | |||||||
| 		if (change.item_type == BaseModel.ITEM_TYPE_NOTE) { | 		if (change.item_type == BaseModel.ITEM_TYPE_NOTE) { | ||||||
| 			return Note.load(change.item_id).then((note) => { | 			return Note.load(change.item_id).then((note) => { | ||||||
| 				return Folder.load(note.parent_id).then((folder) => { | 				return Folder.load(note.parent_id).then((folder) => { | ||||||
| 					console.info('xxxxxxxxx',note); |  | ||||||
| 					return Promise.resolve({ parent: folder, item: note }); | 					return Promise.resolve({ parent: folder, item: note }); | ||||||
| 				}); | 				}); | ||||||
| 			}); | 			}); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user