You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Various changes
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -29,4 +29,5 @@ app/data/uploads/ | ||||
| sparse_test.php | ||||
| INFO.md | ||||
| /web/env.php | ||||
| sync_staging.sh | ||||
| sync_staging.sh | ||||
| *.swp | ||||
							
								
								
									
										211
									
								
								CliClient/app/cmd.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								CliClient/app/cmd.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,211 @@ | ||||
| import { FileApi } from 'src/file-api.js'; | ||||
| import { FileApiDriverLocal } from 'src/file-api-driver-local.js'; | ||||
| import { Database } from 'src/database.js'; | ||||
| import { DatabaseDriverNode } from 'src/database-driver-node.js'; | ||||
| import { BaseModel } from 'src/base-model.js'; | ||||
| import { Folder } from 'src/models/folder.js'; | ||||
| import { Note } from 'src/models/note.js'; | ||||
| import { Synchronizer } from 'src/synchronizer.js'; | ||||
| import { uuid } from 'src/uuid.js'; | ||||
| import { sprintf } from 'sprintf-js'; | ||||
| import { _ } from 'src/locale.js'; | ||||
| import { NoteFolderService } from 'src/services/note-folder-service.js'; | ||||
|  | ||||
| const vorpal = require('vorpal')(); | ||||
|  | ||||
| let db = new Database(new DatabaseDriverNode()); | ||||
| db.setDebugEnabled(false); | ||||
| db.open({ name: '/home/laurent/Temp/test.sqlite3' }).then(() => { | ||||
| 	BaseModel.db_ = db; | ||||
|  | ||||
| 	let commands = []; | ||||
| 	let currentFolder = null; | ||||
|  | ||||
| 	function switchCurrentFolder(folder) { | ||||
| 		currentFolder = folder; | ||||
| 		updatePrompt(); | ||||
| 	} | ||||
|  | ||||
| 	function promptString() { | ||||
| 		let path = '~'; | ||||
| 		if (currentFolder) { | ||||
| 			path += '/' + currentFolder.title; | ||||
| 		} | ||||
| 		return 'joplin:' + path + '$ '; | ||||
| 	} | ||||
|  | ||||
| 	function updatePrompt() { | ||||
| 		vorpal.delimiter(promptString()); | ||||
| 	} | ||||
|  | ||||
| 	process.stdin.on('keypress', (_, key) => { | ||||
| 		if (key && key.name === 'return') { | ||||
| 			updatePrompt(); | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	commands.push({ | ||||
| 		usage: 'cd <list-title>', | ||||
| 		description: 'Moved to [list-title] - all further operations will happen within this list. Use `cd ..` to go back one level.', | ||||
| 		action: function (args, end) { | ||||
| 			let folderTitle = args['list-title']; | ||||
|  | ||||
| 			if (folderTitle == '..') { | ||||
| 				switchCurrentFolder(null); | ||||
| 				end(); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			if (folderTitle == '.') { | ||||
| 				end(); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			Folder.loadByField('title', folderTitle).then((folder) => { | ||||
| 				switchCurrentFolder(folder); | ||||
| 				end(); | ||||
| 			}); | ||||
| 		}, | ||||
| 	}); | ||||
|  | ||||
| 	commands.push({ | ||||
| 		usage: 'mklist <list-title>', | ||||
| 		description: 'Creates a new list', | ||||
| 		action: function (args, end) { | ||||
| 			NoteFolderService.save('folder', { title: args['list-title'] }).catch((error) => { | ||||
| 				this.log(error); | ||||
| 			}).then((folder) => { | ||||
| 				switchCurrentFolder(folder); | ||||
| 				end(); | ||||
| 			}); | ||||
| 		}, | ||||
| 	}); | ||||
|  | ||||
| 	commands.push({ | ||||
| 		usage: 'mknote <note-title>', | ||||
| 		description: 'Creates a new note', | ||||
| 		action: function (args, end) { | ||||
| 			if (!currentFolder) { | ||||
| 				this.log('Notes can only be created within a list.'); | ||||
| 				end(); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			let note = { | ||||
| 				title: args['note-title'], | ||||
| 				parent_id: currentFolder.id, | ||||
| 			}; | ||||
| 			NoteFolderService.save('note', note).catch((error) => { | ||||
| 				this.log(error); | ||||
| 			}).then((note) => { | ||||
| 				end(); | ||||
| 			}); | ||||
| 		}, | ||||
| 	}); | ||||
|  | ||||
| 	commands.push({ | ||||
| 		usage: 'edit <item-title> <prop-name> [prop-value]', | ||||
| 		description: 'Sets the given <prop-name> of the given item.', | ||||
| 		action: function (args, end) { | ||||
| 			let promise = null; | ||||
| 			let title = args['item-title']; | ||||
| 			let propName = args['prop-name']; | ||||
| 			let propValue = args['prop-value']; | ||||
|  | ||||
| 			if (!currentFolder) { | ||||
| 				promise = Folder.loadByField('title', title); | ||||
| 			} else { | ||||
| 				promise = Folder.loadNoteByField(currentFolder.id, 'title', title); | ||||
| 			} | ||||
|  | ||||
| 			promise.then((item) => { | ||||
| 				if (!item) { | ||||
| 					this.log(_('No item with title "%s" found.', title)); | ||||
| 					end(); | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 				let newItem = Object.assign({}, item); | ||||
| 				newItem[propName] = propValue; | ||||
| 				let itemType = currentFolder ? 'note' : 'folder'; | ||||
| 				return NoteFolderService.save(itemType, newItem, item); | ||||
| 			}).catch((error) => { | ||||
| 				this.log(error); | ||||
| 			}).then(() => { | ||||
| 				end(); | ||||
| 			}); | ||||
| 		}, | ||||
| 	}); | ||||
|  | ||||
| 	commands.push({ | ||||
| 		usage: 'ls [list-title]', | ||||
| 		description: 'Lists items in [list-title].', | ||||
| 		action: function (args, end) { | ||||
| 			let folderTitle = args['list-title']; | ||||
|  | ||||
| 			let promise = null; | ||||
|  | ||||
| 			if (folderTitle) { | ||||
| 				promise = Folder.loadByField('title', folderTitle); | ||||
| 			} else if (currentFolder) { | ||||
| 				promise = Promise.resolve(currentFolder); | ||||
| 			} else { | ||||
| 				promise = Promise.resolve('root'); | ||||
| 			} | ||||
|  | ||||
| 			promise.then((folder) => { | ||||
| 				let p = null | ||||
| 				let postfix = ''; | ||||
| 				if (folder === 'root') { | ||||
| 					p = Folder.all(); | ||||
| 					postfix = '/'; | ||||
| 				} else if (!folder) { | ||||
| 					throw new Error(_('Unknown list: "%s"', folderTitle)); | ||||
| 				} else { | ||||
| 					p = Note.previews(folder.id); | ||||
| 				} | ||||
|  | ||||
| 				return p.then((previews) => { | ||||
| 					for (let i = 0; i < previews.length; i++) { | ||||
| 						this.log(previews[i].title + postfix); | ||||
| 					} | ||||
| 				}); | ||||
| 			}).catch((error) => { | ||||
| 				this.log(error); | ||||
| 			}).then(() => { | ||||
| 				end(); | ||||
| 			}); | ||||
| 		}, | ||||
| 	}); | ||||
|  | ||||
| 	// commands.push({ | ||||
| 	// 	usage: 'sync', | ||||
| 	// 	description: 'Synchronizes with remote storage.', | ||||
| 	// 	action: function (args, end) { | ||||
|  | ||||
| 	// 	}, | ||||
| 	// }); | ||||
|  | ||||
| 	for (let i = 0; i < commands.length; i++) { | ||||
| 		let c = commands[i]; | ||||
| 		let o = vorpal.command(c.usage, c.description); | ||||
| 		o.action(c.action); | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	let driver = new FileApiDriverLocal(); | ||||
| 	let api = new FileApi('/home/laurent/Temp/TestImport', driver); | ||||
| 	//let api = new FileApi('/home/laurent/Temp/backup_test_dest', driver); | ||||
|  | ||||
| 	// api.list('', true).then((files) => { | ||||
| 	// 	console.info(files); | ||||
| 	// }).catch((error) => { | ||||
| 	// 	console.error(error); | ||||
| 	// }); | ||||
| 	let synchronizer = new Synchronizer(db, api); | ||||
| 	synchronizer.start().catch((error) => { | ||||
| 		console.error(error); | ||||
| 	}); | ||||
|  | ||||
| 	//vorpal.delimiter(promptString()).show(); | ||||
| }); | ||||
							
								
								
									
										93
									
								
								CliClient/app/file-api-test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								CliClient/app/file-api-test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| import { FileApi } from 'src/file-api.js'; | ||||
| import { FileApiDriverLocal } from 'src/file-api-driver-local.js'; | ||||
| import { Database } from 'src/database.js'; | ||||
| import { DatabaseDriverNode } from 'src/database-driver-node.js'; | ||||
| import { Log } from 'src/log.js'; | ||||
|  | ||||
| const fs = require('fs'); | ||||
|  | ||||
| // let driver = new FileApiDriverLocal(); | ||||
| // let api = new FileApi('/home/laurent/Temp/TestImport', driver); | ||||
|  | ||||
| // api.list('/').then((items) => { | ||||
| // 	console.info(items); | ||||
| // }).then(() => { | ||||
| // 	return api.get('un.txt'); | ||||
| // }).then((content) => { | ||||
| // 	console.info(content); | ||||
| // }).then(() => { | ||||
| // 	return api.mkdir('TESTING'); | ||||
| // }).then(() => { | ||||
| // 	return api.put('un.txt', 'testing change'); | ||||
| // }).then(() => { | ||||
| // 	return api.delete('deux.txt'); | ||||
| // }).catch((error) => { | ||||
| // 	console.error('ERROR', error); | ||||
| // }); | ||||
|  | ||||
| Log.setLevel(Log.LEVEL_DEBUG); | ||||
|  | ||||
| let db = new Database(new DatabaseDriverNode()); | ||||
| db.setDebugEnabled(true); | ||||
| db.open({ name: '/home/laurent/Temp/test.sqlite3' }).then(() => { | ||||
| 	return db.selectAll('SELECT * FROM table_fields'); | ||||
| }).then((rows) => { | ||||
| 	 | ||||
| }); | ||||
|  | ||||
| 	//'/home/laurent/Temp/TestImport' | ||||
|  | ||||
|  | ||||
| // var sqlite3 = require('sqlite3').verbose(); | ||||
| // var db = new sqlite3.Database(':memory:'); | ||||
|  | ||||
| // db.run("CREATE TABLE lorem (info TEXT)", () => { | ||||
| // 	db.exec('INSERT INTO lorem VALUES "un"', () => { | ||||
| // 		db.exec('INSERT INTO lorem VALUES "deux"', () => { | ||||
| // 			let st = db.prepare("SELECT rowid AS id, info FROM lorem", () => { | ||||
| // 				st.get((error, row) => { | ||||
| // 					console.info(row); | ||||
| // 				}); | ||||
| // 			}); | ||||
| // 		}); | ||||
| // 	}); | ||||
| // }); | ||||
|  | ||||
| // var stmt = db.prepare("INSERT INTO lorem VALUES (?)"); | ||||
| // for (var i = 0; i < 10; i++) { | ||||
| // stmt.run("Ipsum " + i); | ||||
| // } | ||||
| // stmt.finalize(); | ||||
|  | ||||
| // let st = db.prepare("SELECT rowid AS id, info FROM lorem"); | ||||
| // st.get({}, (row) => { | ||||
| // console.info('xx',row); | ||||
| // }); | ||||
|  | ||||
|  | ||||
| // st.finalize(); | ||||
|  | ||||
|  | ||||
| //db.serialize(function() { | ||||
|  //  db.run("CREATE TABLE lorem (info TEXT)"); | ||||
|  | ||||
|  //  var stmt = db.prepare("INSERT INTO lorem VALUES (?)"); | ||||
|  //  for (var i = 0; i < 10; i++) { | ||||
|  //      stmt.run("Ipsum " + i); | ||||
|  //  } | ||||
|  //  stmt.finalize(); | ||||
|  | ||||
|  //  let st = db.prepare("SELECT rowid AS id, info FROM lorem"); | ||||
| 	// st.get({}, (row) => { | ||||
| 	// 	console.info('xx',row); | ||||
| 	// }); | ||||
|  | ||||
|  | ||||
| 	// st.finalize(); | ||||
|  | ||||
|   // db.each("SELECT rowid AS id, info FROM lorem", function(err, row) { | ||||
|   //     console.log(row.id + ": " + row.info); | ||||
|   // }); | ||||
| //}); | ||||
|  | ||||
| //db.close(); | ||||
| @@ -4,6 +4,7 @@ import { uuid } from 'src/uuid.js'; | ||||
| import moment from 'moment'; | ||||
| import { promiseChain } from 'src/promise-chain.js'; | ||||
| import { WebApi } from 'src/web-api.js' | ||||
| import { folderItemFilename } from 'src/string-utils.js' | ||||
| import jsSHA from "jssha"; | ||||
|  | ||||
| let webApi = new WebApi('http://joplin.local'); | ||||
| @@ -519,13 +520,124 @@ function saveNoteToWebApi(note) { | ||||
| 	delete data.tags; | ||||
|  | ||||
| 	webApi.post('notes', null, data).then((r) => { | ||||
| 		console.info(r); | ||||
| 		//console.info(r); | ||||
| 	}).catch((error) => { | ||||
| 		console.error("Error for note: " + note.title); | ||||
| 		console.error(error); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| function noteToFriendlyString_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; | ||||
| } | ||||
|  | ||||
| function noteToFriendlyString(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 = noteToFriendlyString_format(shownKeys[i], v); | ||||
| 		output.push(shownKeys[i] + ': ' + v); | ||||
| 	} | ||||
|  | ||||
| 	return output.join("\n"); | ||||
| } | ||||
|  | ||||
| // function folderItemFilename(item) { | ||||
| // 	let output = escapeFilename(item.title).trim(); | ||||
| // 	if (!output.length) output = '_'; | ||||
| // 	return output + '.' + item.id.substr(0, 7); | ||||
| // } | ||||
|  | ||||
| function noteFilename(note) { | ||||
| 	return folderItemFilename(note) + '.md'; | ||||
| } | ||||
|  | ||||
| function folderFilename(folder) { | ||||
| 	return folderItemFilename(folder); | ||||
| } | ||||
|  | ||||
| function filePutContents(filePath, content) { | ||||
| 	return new Promise((resolve, reject) => { | ||||
| 		fs.writeFile(filePath, content, function(error) { | ||||
| 			if (error) { | ||||
| 				reject(error); | ||||
| 			} else { | ||||
| 				resolve(); | ||||
| 			} | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| function setModifiedTime(filePath, time) { | ||||
| 	return new Promise((resolve, reject) => { | ||||
| 		fs.utimes(filePath, time, time, (error) => { | ||||
| 			if (error) { | ||||
| 				reject(error); | ||||
| 				return; | ||||
| 			} | ||||
| 			resolve(); | ||||
| 		}) | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| function createDirectory(path) { | ||||
| 	return new Promise((resolve, reject) => { | ||||
| 		fs.exists(path, (exists) => { | ||||
| 			if (exists) { | ||||
| 				resolve(); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			const mkdirp = require('mkdirp'); | ||||
| 		 | ||||
| 			mkdirp(path, (error) => { | ||||
| 				if (error) { | ||||
| 					reject(error); | ||||
| 				} else { | ||||
| 					resolve(); | ||||
| 				} | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| const baseNoteDir = '/home/laurent/Temp/TestImport'; | ||||
|  | ||||
| // createDirectory('/home/laurent/Temp/TestImport').then(() => { | ||||
| // 	console.info('OK'); | ||||
| // }).catch((error) => { | ||||
| // 	console.error(error); | ||||
| // }); | ||||
|  | ||||
| function saveNoteToDisk(folder, note) { | ||||
| 	const noteContent = noteToFriendlyString(note); | ||||
| 	const notePath = baseNoteDir + '/' + folderFilename(folder) + '/' + noteFilename(note); | ||||
|  | ||||
| 	// console.info('==================================================='); | ||||
| 	// console.info(note);//noteContent); | ||||
| 	return filePutContents(notePath, noteContent).then(() => { | ||||
| 		return setModifiedTime(notePath, note.updated_time ? note.updated_time : note.created_time); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| function saveFolderToDisk(folder) { | ||||
| 	let path = baseNoteDir + '/' + folderFilename(folder); | ||||
| 	return createDirectory(path); | ||||
| } | ||||
|  | ||||
| function createNoteId(note) { | ||||
| 	let shaObj = new jsSHA("SHA-256", "TEXT"); | ||||
| 	shaObj.update(note.title + '_' + note.body + "_" + note.created_time + "_" + note.updated_time + "_"); | ||||
| @@ -533,7 +645,7 @@ function createNoteId(note) { | ||||
| 	return hash.substr(0, 32); | ||||
| } | ||||
|  | ||||
| function importEnex(parentId, stream) { | ||||
| function importEnex(parentFolder, stream) { | ||||
| 	return new Promise((resolve, reject) => { | ||||
| 		let options = {}; | ||||
| 		let strict = true; | ||||
| @@ -576,10 +688,16 @@ function importEnex(parentId, stream) { | ||||
| 							firstAttachment = false; | ||||
| 						} | ||||
|  | ||||
| 						note.parent_id = parentId; | ||||
| 						note.parent_id = parentFolder.id; | ||||
| 						note.body = processMdArrayNewLines(result.lines); | ||||
| 						note.id = uuid.create(); | ||||
|  | ||||
| 						saveNoteToWebApi(note); | ||||
| 						saveNoteToDisk(parentFolder, note); | ||||
|  | ||||
| 						// console.info(noteToFriendlyString(note)); | ||||
| 						// console.info('========================================================================================================================='); | ||||
|  | ||||
| 						//saveNoteToWebApi(note); | ||||
|  | ||||
| 						// console.info('======== NOTE ============================================================================'); | ||||
| 						// let c = note.content; | ||||
| @@ -669,7 +787,7 @@ function importEnex(parentId, stream) { | ||||
| 				if (notes.length >= 10) { | ||||
| 					stream.pause(); | ||||
| 					processNotes().then(() => { | ||||
| 						//stream.resume(); | ||||
| 						stream.resume(); | ||||
| 					}).catch((error) => { | ||||
| 						console.info('Error processing note', error); | ||||
| 					}); | ||||
| @@ -722,8 +840,8 @@ function importEnex(parentId, stream) { | ||||
| // TODO: make it persistent and random | ||||
| const clientId = 'AB78AB78AB78AB78AB78AB78AB78AB78'; | ||||
|  | ||||
| //const folderTitle = 'Laurent'; | ||||
| const folderTitle = 'Voiture'; | ||||
| const folderTitle = 'Laurent'; | ||||
| //const folderTitle = 'Voiture'; | ||||
|  | ||||
| webApi.post('sessions', null, { | ||||
| 	email: 'laurent@cozic.net', | ||||
| @@ -745,11 +863,15 @@ webApi.post('sessions', null, { | ||||
| 	} | ||||
|  | ||||
| 	return folder ? Promise.resolve(folder) : webApi.post('folders', null, { title: folderTitle }); | ||||
| }).then((folder) => { | ||||
| 	return saveFolderToDisk(folder).then(() => { | ||||
| 		return folder; | ||||
| 	}); | ||||
| }).then((folder) => { | ||||
| 	let fileStream = fs.createReadStream('/mnt/c/Users/Laurent/Desktop/' + folderTitle + '.enex'); | ||||
| 	//let fileStream = fs.createReadStream('/mnt/c/Users/Laurent/Desktop/afaire.enex'); | ||||
| 	//let fileStream = fs.createReadStream('/mnt/c/Users/Laurent/Desktop/testtags.enex'); | ||||
| 	importEnex(folder.id, fileStream).then(() => { | ||||
| 	importEnex(folder, fileStream).then(() => { | ||||
| 		//console.info('DONE IMPORTING'); | ||||
| 	}).catch((error) => { | ||||
| 		console.error('Cannot import', error); | ||||
|   | ||||
| @@ -5,14 +5,19 @@ | ||||
|   "dependencies": { | ||||
|     "app-module-path": "^2.2.0", | ||||
|     "form-data": "^2.1.4", | ||||
|     "fs-extra": "^3.0.1", | ||||
|     "jssha": "^2.3.0", | ||||
|     "mkdirp": "^0.5.1", | ||||
|     "moment": "^2.18.1", | ||||
|     "node-fetch": "^1.7.1", | ||||
|     "promise": "^7.1.1", | ||||
|     "react": "16.0.0-alpha.6", | ||||
|     "sax": "^1.2.2", | ||||
|     "sprintf-js": "^1.1.1", | ||||
|     "sqlite3": "^3.1.8", | ||||
|     "string-to-stream": "^1.1.0", | ||||
|     "uuid": "^3.0.1" | ||||
|     "uuid": "^3.0.1", | ||||
|     "vorpal": "^1.12.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "babel-changed": "^7.0.0", | ||||
| @@ -22,7 +27,8 @@ | ||||
|     "query-string": "4.3.4" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "build": "babel-changed app -d build", | ||||
|     "babelbuild": "babel app -d build", | ||||
|     "build": "babel-changed app -d build && babel-changed app/src/models -d build/src/models && babel-changed app/src/services -d build/src/services", | ||||
|     "clean": "babel-changed --reset" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -4,4 +4,6 @@ CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" | ||||
| rm -f "$CLIENT_DIR/app/src" | ||||
| ln -s "$CLIENT_DIR/../ReactNativeClient/src" "$CLIENT_DIR/app" | ||||
|  | ||||
| npm run build && NODE_PATH="$CLIENT_DIR/build/" node build/import-enex.js | ||||
| #npm run build && NODE_PATH="$CLIENT_DIR/build/" node build/import-enex.js | ||||
| #npm run build && NODE_PATH="$CLIENT_DIR/build/" node build/file-api-test.js | ||||
| npm run build && NODE_PATH="$CLIENT_DIR/build/" node build/cmd.js | ||||
| @@ -126,6 +126,7 @@ android { | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     compile project(':react-native-fs') | ||||
|     compile fileTree(dir: "libs", include: ["*.jar"]) | ||||
|     compile "com.android.support:appcompat-v7:23.0.1" | ||||
|     compile "com.facebook.react:react-native:+"  // From node_modules | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package com.awesomeproject; | ||||
| import android.app.Application; | ||||
|  | ||||
| import com.facebook.react.ReactApplication; | ||||
| import com.rnfs.RNFSPackage; | ||||
| import com.facebook.react.ReactNativeHost; | ||||
| import com.facebook.react.ReactPackage; | ||||
| import com.facebook.react.shell.MainReactPackage; | ||||
| @@ -24,7 +25,8 @@ public class MainApplication extends Application implements ReactApplication { | ||||
|     protected List<ReactPackage> getPackages() { | ||||
|       return Arrays.<ReactPackage>asList( | ||||
|           new SQLitePluginPackage(), | ||||
|           new MainReactPackage() | ||||
|           new MainReactPackage(), | ||||
|             new RNFSPackage() | ||||
|       ); | ||||
|     } | ||||
|   }; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| rootProject.name = 'AwesomeProject' | ||||
| include ':react-native-fs' | ||||
| project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android') | ||||
|  | ||||
| include ':app' | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,6 @@ | ||||
| 	}; | ||||
| 	objectVersion = 46; | ||||
| 	objects = { | ||||
|  | ||||
| /* Begin PBXBuildFile section */ | ||||
| 		00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; | ||||
| 		00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; | ||||
| @@ -36,6 +35,7 @@ | ||||
| 		2DCD954D1E0B4F2C00145EB5 /* AwesomeProjectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* AwesomeProjectTests.m */; }; | ||||
| 		5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; }; | ||||
| 		832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; | ||||
| 		EA51DDC9EBFC469F8214B3AD /* libRNFS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8E646D13B9444DE81EC441D /* libRNFS.a */; }; | ||||
| /* End PBXBuildFile section */ | ||||
|  | ||||
| /* Begin PBXContainerItemProxy section */ | ||||
| @@ -255,6 +255,8 @@ | ||||
| 		5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = "<group>"; }; | ||||
| 		78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = "<group>"; }; | ||||
| 		832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = "<group>"; }; | ||||
| 		8C2AA97067234408AD5BFD90 /* RNFS.xcodeproj */ = {isa = PBXFileReference; name = "RNFS.xcodeproj"; path = "../node_modules/react-native-fs/RNFS.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; }; | ||||
| 		A8E646D13B9444DE81EC441D /* libRNFS.a */ = {isa = PBXFileReference; name = "libRNFS.a"; path = "libRNFS.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; }; | ||||
| /* End PBXFileReference section */ | ||||
|  | ||||
| /* Begin PBXFrameworksBuildPhase section */ | ||||
| @@ -281,6 +283,7 @@ | ||||
| 				832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, | ||||
| 				00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, | ||||
| 				139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */, | ||||
| 				EA51DDC9EBFC469F8214B3AD /* libRNFS.a in Frameworks */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| @@ -447,6 +450,7 @@ | ||||
| 				832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, | ||||
| 				00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, | ||||
| 				139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */, | ||||
| 				8C2AA97067234408AD5BFD90 /* RNFS.xcodeproj */, | ||||
| 			); | ||||
| 			name = Libraries; | ||||
| 			sourceTree = "<group>"; | ||||
| @@ -564,7 +568,7 @@ | ||||
| 		83CBB9F71A601CBA00E9B192 /* Project object */ = { | ||||
| 			isa = PBXProject; | ||||
| 			attributes = { | ||||
| 				LastUpgradeCheck = 0610; | ||||
| 				LastUpgradeCheck = 610; | ||||
| 				ORGANIZATIONNAME = Facebook; | ||||
| 				TargetAttributes = { | ||||
| 					00E356ED1AD99517003FC87E = { | ||||
| @@ -972,6 +976,14 @@ | ||||
| 				); | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AwesomeProject.app/AwesomeProject"; | ||||
| 				LIBRARY_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"\"$(SRCROOT)/$(TARGET_NAME)\"", | ||||
| 				); | ||||
| 				HEADER_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"$(SRCROOT)/../node_modules/react-native-fs/**", | ||||
| 				); | ||||
| 			}; | ||||
| 			name = Debug; | ||||
| 		}; | ||||
| @@ -989,6 +1001,14 @@ | ||||
| 				); | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AwesomeProject.app/AwesomeProject"; | ||||
| 				LIBRARY_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"\"$(SRCROOT)/$(TARGET_NAME)\"", | ||||
| 				); | ||||
| 				HEADER_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"$(SRCROOT)/../node_modules/react-native-fs/**", | ||||
| 				); | ||||
| 			}; | ||||
| 			name = Release; | ||||
| 		}; | ||||
| @@ -1007,6 +1027,10 @@ | ||||
| 				); | ||||
| 				PRODUCT_NAME = AwesomeProject; | ||||
| 				VERSIONING_SYSTEM = "apple-generic"; | ||||
| 				HEADER_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"$(SRCROOT)/../node_modules/react-native-fs/**", | ||||
| 				); | ||||
| 			}; | ||||
| 			name = Debug; | ||||
| 		}; | ||||
| @@ -1024,6 +1048,10 @@ | ||||
| 				); | ||||
| 				PRODUCT_NAME = AwesomeProject; | ||||
| 				VERSIONING_SYSTEM = "apple-generic"; | ||||
| 				HEADER_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"$(SRCROOT)/../node_modules/react-native-fs/**", | ||||
| 				); | ||||
| 			}; | ||||
| 			name = Release; | ||||
| 		}; | ||||
| @@ -1050,6 +1078,14 @@ | ||||
| 				SDKROOT = appletvos; | ||||
| 				TARGETED_DEVICE_FAMILY = 3; | ||||
| 				TVOS_DEPLOYMENT_TARGET = 9.2; | ||||
| 				LIBRARY_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"\"$(SRCROOT)/$(TARGET_NAME)\"", | ||||
| 				); | ||||
| 				HEADER_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"$(SRCROOT)/../node_modules/react-native-fs/**", | ||||
| 				); | ||||
| 			}; | ||||
| 			name = Debug; | ||||
| 		}; | ||||
| @@ -1076,6 +1112,14 @@ | ||||
| 				SDKROOT = appletvos; | ||||
| 				TARGETED_DEVICE_FAMILY = 3; | ||||
| 				TVOS_DEPLOYMENT_TARGET = 9.2; | ||||
| 				LIBRARY_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"\"$(SRCROOT)/$(TARGET_NAME)\"", | ||||
| 				); | ||||
| 				HEADER_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"$(SRCROOT)/../node_modules/react-native-fs/**", | ||||
| 				); | ||||
| 			}; | ||||
| 			name = Release; | ||||
| 		}; | ||||
| @@ -1097,6 +1141,10 @@ | ||||
| 				SDKROOT = appletvos; | ||||
| 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AwesomeProject-tvOS.app/AwesomeProject-tvOS"; | ||||
| 				TVOS_DEPLOYMENT_TARGET = 10.1; | ||||
| 				LIBRARY_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"\"$(SRCROOT)/$(TARGET_NAME)\"", | ||||
| 				); | ||||
| 			}; | ||||
| 			name = Debug; | ||||
| 		}; | ||||
| @@ -1118,6 +1166,10 @@ | ||||
| 				SDKROOT = appletvos; | ||||
| 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AwesomeProject-tvOS.app/AwesomeProject-tvOS"; | ||||
| 				TVOS_DEPLOYMENT_TARGET = 10.1; | ||||
| 				LIBRARY_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"\"$(SRCROOT)/$(TARGET_NAME)\"", | ||||
| 				); | ||||
| 			}; | ||||
| 			name = Release; | ||||
| 		}; | ||||
|   | ||||
| @@ -7,12 +7,15 @@ | ||||
|     "test": "jest" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "dropbox": "^2.5.4", | ||||
|     "form-data": "^2.1.4", | ||||
|     "moment": "^2.18.1", | ||||
|     "node-fetch": "^1.7.1", | ||||
|     "react": "16.0.0-alpha.6", | ||||
|     "react-native": "0.44.0", | ||||
|     "react-native-action-button": "^2.6.9", | ||||
|     "react-native-checkbox": "^1.1.0", | ||||
|     "react-native-fs": "^2.3.3", | ||||
|     "react-native-popup-menu": "^0.7.4", | ||||
|     "react-native-side-menu": "^0.20.1", | ||||
|     "react-native-vector-icons": "^2.0.3", | ||||
|   | ||||
| @@ -72,7 +72,12 @@ class BaseModel { | ||||
| 	} | ||||
|  | ||||
| 	static load(id) { | ||||
| 		return this.db().selectOne('SELECT * FROM ' + this.tableName() + ' WHERE id = ?', [id]); | ||||
| 		return this.loadByField('id', id); | ||||
| 		//return this.db().selectOne('SELECT * FROM ' + this.tableName() + ' WHERE id = ?', [id]); | ||||
| 	} | ||||
|  | ||||
| 	static loadByField(fieldName, fieldValue) {	 | ||||
| 		return this.db().selectOne('SELECT * FROM ' + this.tableName() + ' WHERE `' + fieldName + '` = ?', [fieldValue]); | ||||
| 	} | ||||
|  | ||||
| 	static applyPatch(model, patch) { | ||||
| @@ -143,43 +148,44 @@ class BaseModel { | ||||
| 		options = this.modOptions(options); | ||||
|  | ||||
| 		let isNew = options.isNew == 'auto' ? !o.id : options.isNew; | ||||
| 		let query = this.saveQuery(o, isNew); | ||||
|  | ||||
| 		return this.db().transaction((tx) => { | ||||
| 			tx.executeSql(query.sql, query.params); | ||||
| 		let queries = []; | ||||
| 		let saveQuery = this.saveQuery(o, isNew); | ||||
| 		let itemId = saveQuery.id; | ||||
|  | ||||
| 			if (options.trackChanges && this.trackChanges()) { | ||||
| 				// Cannot import this class the normal way due to cyclical dependencies between Change and BaseModel | ||||
| 				// which are not handled by React Native. | ||||
| 				const { Change } = require('src/models/change.js'); | ||||
| 		queries.push(saveQuery); | ||||
|  | ||||
| 		if (options.trackChanges && this.trackChanges()) { | ||||
| 			// Cannot import this class the normal way due to cyclical dependencies between Change and BaseModel | ||||
| 			// which are not handled by React Native. | ||||
| 			const { Change } = require('src/models/change.js'); | ||||
|  | ||||
| 			if (isNew) { | ||||
| 				let change = Change.newChange(); | ||||
| 				change.type = Change.TYPE_CREATE; | ||||
| 				change.item_id = itemId; | ||||
| 				change.item_type = this.itemType(); | ||||
|  | ||||
| 				queries.push(Change.saveQuery(change)); | ||||
| 			} else { | ||||
| 				for (let n in o) { | ||||
| 					if (!o.hasOwnProperty(n)) continue; | ||||
| 					if (n == 'id') continue; | ||||
|  | ||||
| 				if (isNew) { | ||||
| 					let change = Change.newChange(); | ||||
| 					change.type = Change.TYPE_CREATE; | ||||
| 					change.item_id = query.id; | ||||
| 					change.type = Change.TYPE_UPDATE; | ||||
| 					change.item_id = itemId; | ||||
| 					change.item_type = this.itemType(); | ||||
| 					change.item_field = n; | ||||
|  | ||||
| 					let changeQuery = Change.saveQuery(change); | ||||
| 					tx.executeSql(changeQuery.sql, changeQuery.params); | ||||
| 				} else { | ||||
| 					for (let n in o) { | ||||
| 						if (!o.hasOwnProperty(n)) continue; | ||||
| 						if (n == 'id') continue; | ||||
|  | ||||
| 						let change = Change.newChange(); | ||||
| 						change.type = Change.TYPE_UPDATE; | ||||
| 						change.item_id = query.id; | ||||
| 						change.item_type = this.itemType(); | ||||
| 						change.item_field = n; | ||||
|  | ||||
| 						let changeQuery = Change.saveQuery(change); | ||||
| 						tx.executeSql(changeQuery.sql, changeQuery.params); | ||||
| 					} | ||||
| 					queries.push(Change.saveQuery(change)); | ||||
| 				} | ||||
| 			} | ||||
| 		}).then((r) => { | ||||
| 		} | ||||
|  | ||||
| 		return this.db().transactionExecBatch(queries).then(() => { | ||||
| 			o = Object.assign({}, o); | ||||
| 			o.id = query.id; | ||||
| 			o.id = itemId; | ||||
| 			return o; | ||||
| 		}).catch((error) => { | ||||
| 			Log.error('Cannot save model', error); | ||||
| @@ -220,5 +226,6 @@ BaseModel.ITEM_TYPE_FOLDER = 2; | ||||
| BaseModel.tableInfo_ = null; | ||||
| BaseModel.tableKeys_ = null; | ||||
| BaseModel.db_ = null; | ||||
| BaseModel.dispatch = function(o) {}; | ||||
|  | ||||
| export { BaseModel }; | ||||
| @@ -17,10 +17,6 @@ const styles = StyleSheet.create({ | ||||
|  | ||||
| class ScreenHeaderComponent extends Component { | ||||
|  | ||||
| 	static defaultProps = { | ||||
| 		menuOptions: [], | ||||
| 	}; | ||||
|  | ||||
| 	showBackButton() { | ||||
| 		// Note: this is hardcoded for now because navigation.state doesn't tell whether | ||||
| 		// it's possible to go back or not. Maybe it's possible to get this information | ||||
| @@ -111,6 +107,10 @@ class ScreenHeaderComponent extends Component { | ||||
|  | ||||
| } | ||||
|  | ||||
| ScreenHeaderComponent.defaultProps = { | ||||
| 	menuOptions: [], | ||||
| }; | ||||
|  | ||||
| const ScreenHeader = connect( | ||||
| 	(state) => { | ||||
| 		return { user: state.user }; | ||||
|   | ||||
| @@ -61,7 +61,7 @@ class LoginScreenComponent extends React.Component { | ||||
|  | ||||
| 			Registry.api().setSession(session.id); | ||||
|  | ||||
| 			Registry.synchronizer().start(); | ||||
| 			//Registry.synchronizer().start(); | ||||
| 		}).catch((error) => { | ||||
| 			this.setState({ errorMessage: _('Could not login: %s)', error.message) }); | ||||
| 		}); | ||||
|   | ||||
							
								
								
									
										63
									
								
								ReactNativeClient/src/database-driver-node.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								ReactNativeClient/src/database-driver-node.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| const sqlite3 = require('sqlite3').verbose(); | ||||
| const Promise = require('promise'); | ||||
|  | ||||
| class DatabaseDriverNode { | ||||
|  | ||||
| 	open(options) { | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			this.db_ = new sqlite3.Database(options.name, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (error) => { | ||||
| 				if (error) { | ||||
| 					reject(error); | ||||
| 					return; | ||||
| 				} | ||||
| 				resolve(); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	setDebugEnabled(v) { | ||||
| 		// ?? | ||||
| 	} | ||||
|  | ||||
| 	selectOne(sql, params = null) { | ||||
| 		if (!params) params = {}; | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			this.db_.get(sql, params, (error, row) => { | ||||
| 				if (error) { | ||||
| 					reject(error); | ||||
| 					return; | ||||
| 				} | ||||
| 				resolve(row); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	selectAll(sql, params = null) { | ||||
| 		if (!params) params = {}; | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			this.db_.all(sql, params, (error, row) => { | ||||
| 				if (error) { | ||||
| 					reject(error); | ||||
| 					return; | ||||
| 				} | ||||
| 				resolve(row); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	exec(sql, params = null) { | ||||
| 		if (!params) params = {}; | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			this.db_.run(sql, params, (error) => { | ||||
| 				if (error) { | ||||
| 					reject(error); | ||||
| 					return; | ||||
| 				} | ||||
| 				resolve(); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| export { DatabaseDriverNode }; | ||||
							
								
								
									
										52
									
								
								ReactNativeClient/src/database-driver-react-native.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								ReactNativeClient/src/database-driver-react-native.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| import SQLite from 'react-native-sqlite-storage'; | ||||
|  | ||||
| class DatabaseDriverReactNative { | ||||
|  | ||||
| 	open(options) { | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			SQLite.openDatabase({ name: options.name }, (db) => { | ||||
| 				this.db_ = db; | ||||
| 				resolve(); | ||||
| 			}, (error) => { | ||||
| 				reject(error); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	setDebugEnabled(v) { | ||||
| 		SQLite.DEBUG(v); | ||||
| 	} | ||||
|  | ||||
| 	selectOne(sql, params = null) { | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			this.db_.executeSql(sql, params, (r) => { | ||||
| 				resolve(r.rows.length ? r.rows.item(0) : null); | ||||
| 			}, (error) => { | ||||
| 				reject(error); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	selectAll(sql, params = null) { | ||||
| 		return this.exec(sql, params).then((r) => { | ||||
| 			let output = [] | ||||
| 			for (let i = 0; i < r.rows.length; i++) { | ||||
| 				output.push(r.rows.item(i)); | ||||
| 			} | ||||
| 			return output; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	exec(sql, params = null) { | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			this.db_.executeSql(sql, params, (r) => { | ||||
| 				resolve(r); | ||||
| 			}, (error) => { | ||||
| 				reject(error); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| export { DatabaseDriverReactNative } | ||||
| @@ -1,4 +1,3 @@ | ||||
| import SQLite from 'react-native-sqlite-storage'; | ||||
| import { Log } from 'src/log.js'; | ||||
| import { uuid } from 'src/uuid.js'; | ||||
| import { promiseChain } from 'src/promise-chain.js'; | ||||
| @@ -94,14 +93,15 @@ INSERT INTO version (version) VALUES (1); | ||||
|  | ||||
| class Database { | ||||
|  | ||||
| 	constructor() { | ||||
| 	constructor(driver) { | ||||
| 		this.debugMode_ = false; | ||||
| 		this.initialized_ = false; | ||||
| 		this.tableFields_ = null; | ||||
| 		this.driver_ = driver; | ||||
| 	} | ||||
|  | ||||
| 	setDebugEnabled(v) { | ||||
| 		SQLite.DEBUG(v); | ||||
| 		this.driver_.setDebugEnabled(v); | ||||
| 		this.debugMode_ = v; | ||||
| 	} | ||||
|  | ||||
| @@ -113,14 +113,43 @@ class Database { | ||||
| 		return this.initialized_; | ||||
| 	} | ||||
|  | ||||
| 	open() { | ||||
| 		this.db_ = SQLite.openDatabase({ name: '/storage/emulated/0/Download/joplin-32.sqlite' }, (db) => { | ||||
| 	driver() { | ||||
| 		return this.driver_; | ||||
| 	} | ||||
|  | ||||
| 	open(options) { | ||||
| 		return this.driver().open(options).then((db) => { | ||||
| 			Log.info('Database was open successfully'); | ||||
| 		}, (error) => { | ||||
| 			return this.initialize(); | ||||
| 		}).catch((error) => { | ||||
| 			Log.error('Cannot open database: ', error); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 		return this.initialize(); | ||||
| 	selectOne(sql, params = null) { | ||||
| 		this.logQuery(sql, params); | ||||
| 		return this.driver().selectOne(sql, params); | ||||
| 	} | ||||
|  | ||||
| 	selectAll(sql, params = null) { | ||||
| 		this.logQuery(sql, params); | ||||
| 		return this.driver().selectAll(sql, params); | ||||
| 	} | ||||
|  | ||||
| 	exec(sql, params = null) { | ||||
| 		this.logQuery(sql, params); | ||||
| 		return this.driver().exec(sql, params); | ||||
| 	} | ||||
|  | ||||
| 	transactionExecBatch(queries) { | ||||
| 		let chain = []; | ||||
| 		for (let i = 0; i < queries.length; i++) { | ||||
| 			let query = this.wrapQuery(queries[i]); | ||||
| 			chain.push(() => { | ||||
| 				return this.exec(query.sql, query.params); | ||||
| 			}); | ||||
| 		} | ||||
| 		return promiseChain(chain); | ||||
| 	} | ||||
|  | ||||
| 	static enumId(type, s) { | ||||
| @@ -177,41 +206,7 @@ class Database { | ||||
|  | ||||
| 	logQuery(sql, params = null) { | ||||
| 		if (!this.debugMode()) return; | ||||
| 		//Log.debug('DB: ' + sql, params); | ||||
| 	} | ||||
|  | ||||
| 	selectOne(sql, params = null) { | ||||
| 		this.logQuery(sql, params); | ||||
|  | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			this.db_.executeSql(sql, params, (r) => { | ||||
| 				resolve(r.rows.length ? r.rows.item(0) : null); | ||||
| 			}, (error) => { | ||||
| 				reject(error); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	selectAll(sql, params = null) { | ||||
| 		this.logQuery(sql, params); | ||||
|  | ||||
| 		return this.exec(sql, params); | ||||
| 	} | ||||
|  | ||||
| 	exec(sql, params = null) { | ||||
| 		this.logQuery(sql, params); | ||||
|  | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			this.db_.executeSql(sql, params, (r) => { | ||||
| 				resolve(r); | ||||
| 			}, (error) => { | ||||
| 				reject(error); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	executeSql(sql, params = null) { | ||||
| 		return this.exec(sql, params); | ||||
| 		Log.debug('DB: ' + sql, params); | ||||
| 	} | ||||
|  | ||||
| 	static insertQuery(tableName, data) { | ||||
| @@ -254,29 +249,53 @@ class Database { | ||||
| 	} | ||||
|  | ||||
| 	transaction(readyCallack) { | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			this.db_.transaction( | ||||
| 				readyCallack, | ||||
| 				(error) => { reject(error); }, | ||||
| 				() => { resolve(); } | ||||
| 			); | ||||
| 		}); | ||||
| 		throw new Error('transaction() DEPRECATED'); | ||||
| 		// return new Promise((resolve, reject) => { | ||||
| 		// 	this.db_.transaction( | ||||
| 		// 		readyCallack, | ||||
| 		// 		(error) => { reject(error); }, | ||||
| 		// 		() => { resolve(); } | ||||
| 		// 	); | ||||
| 		// }); | ||||
| 	} | ||||
|  | ||||
| 	wrapQueries(queries) { | ||||
| 		let output = []; | ||||
| 		for (let i = 0; i < queries.length; i++) { | ||||
| 			output.push(this.wrapQuery(queries[i])); | ||||
| 		} | ||||
| 		return output; | ||||
| 	} | ||||
|  | ||||
| 	wrapQuery(sql, params = null) { | ||||
| 		if (!sql) throw new Error('Cannot wrap empty string: ' + sql); | ||||
|  | ||||
| 		if (sql.constructor === Array) { | ||||
| 			let output = {}; | ||||
| 			output.sql = sql[0]; | ||||
| 			output.params = sql.length >= 2 ? sql[1] : null; | ||||
| 			return output; | ||||
| 		} else if (typeof sql === 'string') { | ||||
| 			return { sql: sql, params: params }; | ||||
| 		} else { | ||||
| 			return sql; // Already wrapped | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	refreshTableFields() { | ||||
| 		return this.exec('SELECT name FROM sqlite_master WHERE type="table"').then((tableResults) => { | ||||
| 		let queries = []; | ||||
| 		queries.push(this.wrapQuery('DELETE FROM table_fields')); | ||||
|  | ||||
| 		return this.selectAll('SELECT name FROM sqlite_master WHERE type="table"').then((tableRows) => { | ||||
| 			let chain = []; | ||||
| 			for (let i = 0; i < tableResults.rows.length; i++) { | ||||
| 				let row = tableResults.rows.item(i); | ||||
| 				let tableName = row.name; | ||||
| 			for (let i = 0; i < tableRows.length; i++) { | ||||
| 				let tableName = tableRows[i].name; | ||||
| 				if (tableName == 'android_metadata') continue; | ||||
| 				if (tableName == 'table_fields') continue; | ||||
|  | ||||
| 				chain.push((queries) => { | ||||
| 					if (!queries) queries = []; | ||||
| 					return this.exec('PRAGMA table_info("' + tableName + '")').then((pragmaResult) => { | ||||
| 						for (let i = 0; i < pragmaResult.rows.length; i++) { | ||||
| 							let item = pragmaResult.rows.item(i); | ||||
| 				chain.push(() => { | ||||
| 					return this.selectAll('PRAGMA table_info("' + tableName + '")').then((pragmas) => { | ||||
| 						for (let i = 0; i < pragmas.length; i++) { | ||||
| 							let item = pragmas[i]; | ||||
| 							// In SQLite, if the default value is a string it has double quotes around it, so remove them here | ||||
| 							let defaultValue = item.dflt_value; | ||||
| 							if (typeof defaultValue == 'string' && defaultValue.length >= 2 && defaultValue[0] == '"' && defaultValue[defaultValue.length - 1] == '"') { | ||||
| @@ -290,19 +309,13 @@ class Database { | ||||
| 							}); | ||||
| 							queries.push(q); | ||||
| 						} | ||||
| 						return queries; | ||||
| 					}); | ||||
| 				}); | ||||
| 			} | ||||
|  | ||||
| 			return promiseChain(chain); | ||||
| 		}).then((queries) => { | ||||
| 			return this.transaction((tx) => { | ||||
| 				tx.executeSql('DELETE FROM table_fields'); | ||||
| 				for (let i = 0; i < queries.length; i++) { | ||||
| 					tx.executeSql(queries[i].sql, queries[i].params); | ||||
| 				} | ||||
| 			}); | ||||
| 		}).then(() => { | ||||
| 			return this.transactionExecBatch(queries); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| @@ -314,12 +327,13 @@ class Database { | ||||
| 			// TODO: version update logic | ||||
|  | ||||
| 			// TODO: only do this if db has been updated: | ||||
| 			return this.refreshTableFields(); | ||||
| 			// return this.refreshTableFields(); | ||||
| 		}).then(() => { | ||||
| 			return this.exec('SELECT * FROM table_fields').then((r) => { | ||||
| 				this.tableFields_ = {}; | ||||
| 				for (let i = 0; i < r.rows.length; i++) { | ||||
| 					let row = r.rows.item(i); | ||||
| 			this.tableFields_ = {}; | ||||
|  | ||||
| 			return this.selectAll('SELECT * FROM table_fields').then((rows) => { | ||||
| 				for (let i = 0; i < rows.length; i++) { | ||||
| 					let row = rows[i]; | ||||
| 					if (!this.tableFields_[row.table_name]) this.tableFields_[row.table_name] = []; | ||||
| 					this.tableFields_[row.table_name].push({ | ||||
| 						name: row.field_name, | ||||
| @@ -329,7 +343,6 @@ class Database { | ||||
| 				} | ||||
| 			}); | ||||
|  | ||||
|  | ||||
| 			 | ||||
| 		// }).then(() => { | ||||
| 		// 	let p = this.exec('DELETE FROM notes').then(() => { | ||||
| @@ -348,11 +361,9 @@ class Database { | ||||
|  | ||||
| 		// 	return p; | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| 		}).catch((error) => { | ||||
| 			if (error && error.code != 0) { | ||||
| 			//console.info(error.code); | ||||
| 			if (error && error.code != 0 && error.code != 'SQLITE_ERROR') { | ||||
| 				Log.error(error); | ||||
| 				return; | ||||
| 			} | ||||
| @@ -364,19 +375,18 @@ class Database { | ||||
|  | ||||
| 			Log.info('Database is new - creating the schema...'); | ||||
|  | ||||
| 			let statements = this.sqlStringToLines(structureSql) | ||||
| 			return this.transaction((tx) => { | ||||
| 				for (let i = 0; i < statements.length; i++) { | ||||
| 					tx.executeSql(statements[i]); | ||||
| 				} | ||||
| 				tx.executeSql('INSERT INTO settings (`key`, `value`, `type`) VALUES ("clientId", "' + uuid.create() + '", "' + Database.enumId('settings', 'string') + '")'); | ||||
| 				tx.executeSql('INSERT INTO folders (`id`, `title`, `is_default`, `created_time`) VALUES ("' + uuid.create() + '", "' + _('Default list') + '", 1, ' + Math.round((new Date()).getTime() / 1000) + ')'); | ||||
| 			}).then(() => { | ||||
| 			let queries = this.wrapQueries(this.sqlStringToLines(structureSql)); | ||||
| 			queries.push(this.wrapQuery('INSERT INTO settings (`key`, `value`, `type`) VALUES ("clientId", "' + uuid.create() + '", "' + Database.enumId('settings', 'string') + '")')); | ||||
| 			queries.push(this.wrapQuery('INSERT INTO folders (`id`, `title`, `is_default`, `created_time`) VALUES ("' + uuid.create() + '", "' + _('Default list') + '", 1, ' + Math.round((new Date()).getTime() / 1000) + ')')); | ||||
|  | ||||
| 			return this.transactionExecBatch(queries).then(() => { | ||||
| 				Log.info('Database schema created successfully'); | ||||
| 				// Calling initialize() now that the db has been created will make it go through | ||||
| 				// the normal db update process (applying any additional patch). | ||||
| 				return this.refreshTableFields(); | ||||
| 			}).then(() => { | ||||
| 				return this.initialize(); | ||||
| 			}) | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
|   | ||||
							
								
								
									
										143
									
								
								ReactNativeClient/src/file-api-driver-local.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								ReactNativeClient/src/file-api-driver-local.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | ||||
| import fs from 'fs'; | ||||
| import fse from 'fs-extra'; | ||||
| import { promiseChain } from 'src/promise-chain.js'; | ||||
| import moment from 'moment'; | ||||
|  | ||||
| class FileApiDriverLocal { | ||||
|  | ||||
| 	stat(path) { | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			fs.stat(path, (error, s) => { | ||||
| 				if (error) { | ||||
| 					reject(error); | ||||
| 					return; | ||||
| 				} | ||||
| 				resolve(s); | ||||
| 			});			 | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	statTimeToUnixTimestamp_(time) { | ||||
| 		let m = moment(time, 'YYYY-MM-DDTHH:mm:ss.SSSZ'); | ||||
| 		if (!m.isValid()) { | ||||
| 			throw new Error('Invalid date: ' + time); | ||||
| 		} | ||||
| 		return Math.round(m.toDate().getTime() / 1000); | ||||
| 	} | ||||
|  | ||||
| 	metadataFromStats_(name, stats) { | ||||
| 		return { | ||||
| 			name: name, | ||||
| 			createdTime: this.statTimeToUnixTimestamp_(stats.birthtime), | ||||
| 			updatedTime: this.statTimeToUnixTimestamp_(stats.mtime), | ||||
| 			isDir: stats.isDirectory(), | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	list(path) { | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			fs.readdir(path, (error, items) => { | ||||
| 				if (error) { | ||||
| 					reject(error); | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 				let chain = []; | ||||
| 				for (let i = 0; i < items.length; i++) { | ||||
| 					chain.push((output) => { | ||||
| 						if (!output) output = []; | ||||
| 						return this.stat(path + '/' + items[i]).then((stat) => { | ||||
| 							let md = this.metadataFromStats_(items[i], stat); | ||||
| 							output.push(md); | ||||
| 							return output;							 | ||||
| 						}); | ||||
| 					}); | ||||
| 				} | ||||
|  | ||||
| 				return promiseChain(chain).then((results) => { | ||||
| 					if (!results) results = []; | ||||
| 					resolve(results); | ||||
| 				}).catch((error) => { | ||||
| 					reject(error); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	get(path) { | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			fs.readFile(path, 'utf8', (error, content) => { | ||||
| 				if (error) { | ||||
| 					reject(error); | ||||
| 					return; | ||||
| 				} | ||||
| 				return resolve(content); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	mkdir(path) { | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			fs.exists(path, (exists) => { | ||||
| 				if (exists) { | ||||
| 					resolve(); | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 				const mkdirp = require('mkdirp'); | ||||
| 			 | ||||
| 				mkdirp(path, (error) => { | ||||
| 					if (error) { | ||||
| 						reject(error); | ||||
| 					} else { | ||||
| 						resolve(); | ||||
| 					} | ||||
| 				}); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	put(path, content) { | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			fs.writeFile(path, content, function(error) { | ||||
| 				if (error) { | ||||
| 					reject(error); | ||||
| 				} else { | ||||
| 					resolve(); | ||||
| 				} | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	delete(path) { | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			fs.unlink(path, function(error) { | ||||
| 				if (error) { | ||||
| 					if (error && error.code == 'ENOENT') { | ||||
| 						// File doesn't exist - it's fine | ||||
| 						resolve(); | ||||
| 					} else { | ||||
| 						reject(error); | ||||
| 					} | ||||
| 				} else { | ||||
| 					resolve(); | ||||
| 				} | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	move(oldPath, newPath) { | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			fse.move(oldPath, newPath, function(error) { | ||||
| 				if (error) { | ||||
| 					reject(error); | ||||
| 				} else { | ||||
| 					resolve(); | ||||
| 				} | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| export { FileApiDriverLocal }; | ||||
							
								
								
									
										60
									
								
								ReactNativeClient/src/file-api.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								ReactNativeClient/src/file-api.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| import { promiseChain } from 'src/promise-chain.js'; | ||||
|  | ||||
| class FileApi { | ||||
|  | ||||
| 	constructor(baseDir, driver) { | ||||
| 		this.baseDir_ = baseDir; | ||||
| 		this.driver_ = driver; | ||||
| 	} | ||||
|  | ||||
| 	list(path, recursive = false) { | ||||
| 		return this.driver_.list(this.baseDir_ + '/' + path, recursive).then((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(path + '/' + item.name, true).then((children) => { | ||||
| 							for (let j = 0; j < children.length; j++) { | ||||
| 								let md = children[j]; | ||||
| 								md.name = item.name + '/' + md.name;  | ||||
| 								items.push(md); | ||||
| 							} | ||||
| 						}); | ||||
| 					}); | ||||
| 				} | ||||
|  | ||||
| 				return promiseChain(chain).then(() => { | ||||
| 					return items; | ||||
| 				}); | ||||
| 			} else { | ||||
| 				return items; | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	mkdir(path) { | ||||
| 		return this.driver_.mkdir(this.baseDir_ + '/' + path); | ||||
| 	} | ||||
|  | ||||
| 	get(path) { | ||||
| 		return this.driver_.get(this.baseDir_ + '/' + path); | ||||
| 	} | ||||
|  | ||||
| 	put(path, content) { | ||||
| 		return this.driver_.put(this.baseDir_ + '/' + path, content); | ||||
| 	} | ||||
|  | ||||
| 	delete(path) { | ||||
| 		return this.driver_.delete(this.baseDir_ + '/' + path); | ||||
| 	} | ||||
|  | ||||
| 	move(oldPath, newPath) { | ||||
| 		return this.driver_.move(this.baseDir_ + '/' + oldPath, this.baseDir_ + '/' + newPath); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| export { FileApi }; | ||||
| @@ -16,6 +16,11 @@ class Geolocation { | ||||
| 	} | ||||
|  | ||||
| 	static currentPosition(options = null) { | ||||
| 		if (typeof navigator === 'undefined') { | ||||
| 			// TODO | ||||
| 			return Promise.resolve(this.currentPosition_testResponse()); | ||||
| 		} | ||||
|  | ||||
| 		if (!options) options = {}; | ||||
| 		if (!('enableHighAccuracy' in options)) options.enableHighAccuracy = true; | ||||
| 		if (!('timeout' in options)) options.timeout = 10000; | ||||
|   | ||||
| @@ -7,12 +7,12 @@ class Log { | ||||
| 	} | ||||
|  | ||||
| 	static level() { | ||||
| 		return this.level_ === undefined ? Log.LEVEL_ERROR : this.level_; | ||||
| 		return this.level_ === undefined ? Log.LEVEL_DEBUG : this.level_; | ||||
| 	} | ||||
|  | ||||
| 	static debug(...o) { | ||||
| 		if (Log.level() > Log.LEVEL_DEBUG) return; | ||||
| 		console.debug(...o); | ||||
| 		console.info(...o); | ||||
| 	} | ||||
|  | ||||
| 	static info(...o) { | ||||
|   | ||||
| @@ -18,24 +18,20 @@ class Change extends BaseModel { | ||||
| 	} | ||||
|  | ||||
| 	static all() { | ||||
| 		return this.db().selectAll('SELECT * FROM changes').then((r) => { | ||||
| 			let output = []; | ||||
| 			for (let i = 0; i < r.rows.length; i++) { | ||||
| 				output.push(r.rows.item(i)); | ||||
| 			} | ||||
| 			return output; | ||||
| 		}); | ||||
| 		return this.db().selectAll('SELECT * FROM changes'); | ||||
| 	} | ||||
|  | ||||
| 	static deleteMultiple(ids) { | ||||
| 		if (ids.length == 0) return Promise.resolve(); | ||||
|  | ||||
| 		return this.db().transaction((tx) => { | ||||
| 			let sql = ''; | ||||
| 			for (let i = 0; i < ids.length; i++) { | ||||
| 				tx.executeSql('DELETE FROM changes WHERE id = ?', [ids[i]]); | ||||
| 			}			 | ||||
| 		}); | ||||
| 		console.warn('TODO: deleteMultiple: CHECK THAT IT WORKS'); | ||||
|  | ||||
| 		let queries = []; | ||||
| 		for (let i = 0; i < ids.length; i++) { | ||||
| 			queries.push(['DELETE FROM changes WHERE id = ?', [ids[i]]]); | ||||
| 		} | ||||
|  | ||||
| 		return this.db().transactionExecBatch(queries); | ||||
| 	} | ||||
|  | ||||
| 	static mergeChanges(changes) { | ||||
| @@ -45,6 +41,7 @@ class Change extends BaseModel { | ||||
|  | ||||
| 		for (let i = 0; i < changes.length; i++) { | ||||
| 			let change = changes[i]; | ||||
| 			let mergedChange = null; | ||||
|  | ||||
| 			if (itemChanges[change.item_id]) { | ||||
| 				mergedChange = itemChanges[change.item_id]; | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { BaseModel } from 'src/base-model.js'; | ||||
| import { Log } from 'src/log.js'; | ||||
| import { promiseChain } from 'src/promise-chain.js'; | ||||
| import { Note } from 'src/models/note.js'; | ||||
| import { folderItemFilename } from 'src/string-utils.js' | ||||
| import { _ } from 'src/locale.js'; | ||||
|  | ||||
| class Folder extends BaseModel { | ||||
| @@ -10,6 +11,14 @@ class Folder extends BaseModel { | ||||
| 		return 'folders'; | ||||
| 	} | ||||
|  | ||||
| 	static filename(folder) { | ||||
| 		return folderItemFilename(folder); | ||||
| 	} | ||||
|  | ||||
| 	static systemPath(parent, folder) { | ||||
| 		return this.filename(folder); | ||||
| 	} | ||||
|  | ||||
| 	static useUuid() { | ||||
| 		return true; | ||||
| 	} | ||||
| @@ -21,7 +30,7 @@ class Folder extends BaseModel { | ||||
| 	static trackChanges() { | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	 | ||||
| 	static newFolder() { | ||||
| 		return { | ||||
| 			id: null, | ||||
| @@ -30,10 +39,10 @@ class Folder extends BaseModel { | ||||
| 	} | ||||
|  | ||||
| 	static noteIds(id) { | ||||
| 		return this.db().exec('SELECT id FROM notes WHERE parent_id = ?', [id]).then((r) => { | ||||
| 		return this.db().selectAll('SELECT id FROM notes WHERE parent_id = ?', [id]).then((rows) => {			 | ||||
| 			let output = []; | ||||
| 			for (let i = 0; i < r.rows.length; i++) { | ||||
| 				let row = r.rows.item(i); | ||||
| 			for (let i = 0; i < rows.length; i++) { | ||||
| 				let row = rows[i]; | ||||
| 				output.push(row.id); | ||||
| 			} | ||||
| 			return output; | ||||
| @@ -66,23 +75,25 @@ class Folder extends BaseModel { | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	static loadNoteByField(folderId, field, value) { | ||||
| 		return this.db().selectOne('SELECT * FROM notes WHERE `parent_id` = ? AND `' + field + '` = ?', [folderId, value]); | ||||
| 	} | ||||
|  | ||||
| 	static all() { | ||||
| 		return this.db().selectAll('SELECT * FROM folders').then((r) => { | ||||
| 			let output = []; | ||||
| 			for (let i = 0; i < r.rows.length; i++) { | ||||
| 				output.push(r.rows.item(i)); | ||||
| 			} | ||||
| 			return output; | ||||
| 		}); | ||||
| 		return this.db().selectAll('SELECT * FROM folders'); | ||||
| 	} | ||||
|  | ||||
| 	static save(o, options = null) { | ||||
| 		return super.save(o, options).then((folder) => { | ||||
| 			this.dispatch({ | ||||
| 				type: 'FOLDERS_UPDATE_ONE', | ||||
| 				folder: folder, | ||||
| 		return Folder.loadByField('title', o.title).then((existingFolder) => { | ||||
| 			if (existingFolder && existingFolder.id != o.id) throw new Error(_('A folder with title "%s" already exists', o.title)); | ||||
|  | ||||
| 			return super.save(o, options).then((folder) => { | ||||
| 				this.dispatch({ | ||||
| 					type: 'FOLDERS_UPDATE_ONE', | ||||
| 					folder: folder, | ||||
| 				}); | ||||
| 				return folder; | ||||
| 			}); | ||||
| 			return folder; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| import { BaseModel } from 'src/base-model.js'; | ||||
| import { Log } from 'src/log.js'; | ||||
| import { Folder } from 'src/models/folder.js'; | ||||
| import { Geolocation } from 'src/geolocation.js'; | ||||
| import { folderItemFilename } from 'src/string-utils.js' | ||||
| import moment from 'moment'; | ||||
|  | ||||
| class Note extends BaseModel { | ||||
|  | ||||
| @@ -8,6 +11,42 @@ class Note extends BaseModel { | ||||
| 		return 'notes'; | ||||
| 	} | ||||
|  | ||||
| 	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; | ||||
| 	} | ||||
|  | ||||
| 	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 filename(note) { | ||||
| 		return folderItemFilename(note) + '.md'; | ||||
| 	} | ||||
|  | ||||
| 	static systemPath(parentFolder, note) { | ||||
| 		return Folder.systemPath(null, parentFolder) + '/' + this.filename(note); | ||||
| 	} | ||||
|  | ||||
| 	static useUuid() { | ||||
| 		return true; | ||||
| 	} | ||||
| @@ -37,13 +76,7 @@ class Note extends BaseModel { | ||||
| 	} | ||||
|  | ||||
| 	static previews(parentId) { | ||||
| 		return this.db().selectAll('SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE parent_id = ?', [parentId]).then((r) => { | ||||
| 			let output = []; | ||||
| 			for (let i = 0; i < r.rows.length; i++) { | ||||
| 				output.push(r.rows.item(i)); | ||||
| 			} | ||||
| 			return output; | ||||
| 		}); | ||||
| 		return this.db().selectAll('SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE parent_id = ?', [parentId]); | ||||
| 	} | ||||
|  | ||||
| 	static preview(noteId) { | ||||
|   | ||||
| @@ -27,10 +27,8 @@ class Setting extends BaseModel { | ||||
|  | ||||
| 	static load() { | ||||
| 		this.cache_ = []; | ||||
| 		return this.db().selectAll('SELECT * FROM settings').then((r) => { | ||||
| 			for (let i = 0; i < r.rows.length; i++) { | ||||
| 				this.cache_.push(r.rows.item(i)); | ||||
| 			} | ||||
| 		return this.db().selectAll('SELECT * FROM settings').then((rows) => { | ||||
| 			this.cache_ = rows; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| @@ -89,13 +87,13 @@ class Setting extends BaseModel { | ||||
| 		clearTimeout(this.updateTimeoutId_); | ||||
| 		this.updateTimeoutId_ = null; | ||||
|  | ||||
| 		return BaseModel.db().transaction((tx) => { | ||||
| 			tx.executeSql('DELETE FROM settings'); | ||||
| 			for (let i = 0; i < this.cache_.length; i++) { | ||||
| 				let q = Database.insertQuery(this.tableName(), this.cache_[i]); | ||||
| 				tx.executeSql(q.sql, q.params); | ||||
| 			} | ||||
| 		}).then(() => { | ||||
| 		let queries = []; | ||||
| 		queries.push('DELETE FROM settings'); | ||||
| 		for (let i = 0; i < this.cache_.length; i++) { | ||||
| 			queries.push(Database.insertQuery(this.tableName(), this.cache_[i]));			 | ||||
| 		} | ||||
|  | ||||
| 		return BaseModel.db().transactionExecBatch(queries).then(() => { | ||||
| 			Log.info('Settings have been saved.'); | ||||
| 		}).catch((error) => { | ||||
| 			Log.warn('Could not save settings', error); | ||||
|   | ||||
| @@ -25,6 +25,7 @@ import { MenuContext } from 'react-native-popup-menu'; | ||||
| import { SideMenu } from 'src/components/side-menu.js'; | ||||
| import { SideMenuContent } from 'src/components/side-menu-content.js'; | ||||
| import { NoteFolderService } from 'src/services/note-folder-service.js'; | ||||
| import { DatabaseDriverReactNative } from 'src/database-driver-react-native'; | ||||
|  | ||||
| let defaultState = { | ||||
| 	notes: [], | ||||
| @@ -89,8 +90,6 @@ const reducer = (state = defaultState, action) => { | ||||
| 		// update it within the note array if it already exists. | ||||
| 		case 'NOTES_UPDATE_ONE': | ||||
|  | ||||
| 			Log.info('NOITTEOJTNEONTOE', action.note); | ||||
|  | ||||
| 			let newNotes = state.notes.splice(0); | ||||
| 			var found = false; | ||||
| 			for (let i = 0; i < newNotes.length; i++) { | ||||
| @@ -191,7 +190,7 @@ const AppNavigator = StackNavigator({ | ||||
| class AppComponent extends React.Component { | ||||
|  | ||||
| 	componentDidMount() { | ||||
| 		let db = new Database(); | ||||
| 		let db = new Database(new DatabaseDriverReactNative()); | ||||
| 		//db.setDebugEnabled(Registry.debugMode()); | ||||
| 		db.setDebugEnabled(false); | ||||
|  | ||||
| @@ -199,7 +198,7 @@ class AppComponent extends React.Component { | ||||
| 		BaseModel.db_ = db; | ||||
| 		NoteFolderService.dispatch = this.props.dispatch; | ||||
|  | ||||
| 		db.open().then(() => { | ||||
| 		db.open({ name: '/storage/emulated/0/Download/joplin-42.sqlite' }).then(() => { | ||||
| 			Log.info('Database is ready.'); | ||||
| 			Registry.setDb(db); | ||||
| 		}).then(() => { | ||||
| @@ -207,6 +206,21 @@ class AppComponent extends React.Component { | ||||
| 			return Setting.load(); | ||||
| 		}).then(() => { | ||||
| 			let user = Setting.object('user'); | ||||
|  | ||||
| 			if (!user || !user.session) { | ||||
| 				user = { | ||||
| 					email: 'laurent@cozic.net', | ||||
| 					session: "02d0e9ca42cbbc2d35efb1bc790b9eec", | ||||
| 				} | ||||
| 				Setting.setObject('user', user); | ||||
| 				this.props.dispatch({ | ||||
| 					type: 'USER_SET', | ||||
| 					user: user, | ||||
| 				}); | ||||
| 			} | ||||
|  | ||||
| 			Setting.setValue('sync.lastRevId', '123456'); | ||||
|  | ||||
| 			Log.info('Client ID', Setting.value('clientId')); | ||||
| 			Log.info('User', user); | ||||
|  | ||||
| @@ -241,9 +255,25 @@ class AppComponent extends React.Component { | ||||
| 			// 	folderId: folder.id, | ||||
| 			// }); | ||||
| 		}).then(() => { | ||||
| 			let synchronizer = new Synchronizer(db, Registry.api()); | ||||
| 			Registry.setSynchronizer(synchronizer); | ||||
| 			synchronizer.start(); | ||||
|  | ||||
| 			var Dropbox = require('dropbox'); | ||||
| 			var dropboxApi = new Dropbox({ accessToken: '' }); | ||||
| 			// dbx.filesListFolder({path: '/Joplin/Laurent.4e847cc'}) | ||||
| 			// .then(function(response) { | ||||
| 			// //console.log('DROPBOX RESPONSE', response); | ||||
| 			// console.log('DROPBOX RESPONSE', response.entries.length, response.has_more); | ||||
| 			// }) | ||||
| 			// .catch(function(error) { | ||||
| 			// console.log('DROPBOX ERROR', error); | ||||
| 			// }); | ||||
|  | ||||
| 			// return this.api_; | ||||
|  | ||||
|  | ||||
| 			// let synchronizer = new Synchronizer(db, Registry.api()); | ||||
| 			// let synchronizer = new Synchronizer(db, dropboxApi); | ||||
| 			// Registry.setSynchronizer(synchronizer); | ||||
| 			// synchronizer.start(); | ||||
| 		}).catch((error) => { | ||||
| 			Log.error('Initialization error:', error); | ||||
| 		}); | ||||
|   | ||||
| @@ -31,21 +31,21 @@ class NoteFolderService extends BaseService { | ||||
| 			output = item; | ||||
| 			if (isNew && type == 'note') return Note.updateGeolocation(item.id); | ||||
| 		}).then(() => { | ||||
| 			Registry.synchronizer().start(); | ||||
| //			Registry.synchronizer().start(); | ||||
| 			return output; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	static setField(type, itemId, fieldName, fieldValue, oldValue = undefined) { | ||||
| 		// TODO: not really consistent as the promise will return 'null' while | ||||
| 		// this.save will return the note or folder. Currently not used, and maybe not needed. | ||||
| 		if (oldValue !== undefined && fieldValue === oldValue) return Promise.resolve(); | ||||
| 	// static setField(type, itemId, fieldName, fieldValue, oldValue = undefined) { | ||||
| 	// 	// TODO: not really consistent as the promise will return 'null' while | ||||
| 	// 	// this.save will return the note or folder. Currently not used, and maybe not needed. | ||||
| 	// 	if (oldValue !== undefined && fieldValue === oldValue) return Promise.resolve(); | ||||
|  | ||||
| 		let item = { id: itemId }; | ||||
| 		item[fieldName] = fieldValue; | ||||
| 		let oldItem = { id: itemId }; | ||||
| 		return this.save(type, item, oldItem); | ||||
| 	} | ||||
| 	// 	let item = { id: itemId }; | ||||
| 	// 	item[fieldName] = fieldValue; | ||||
| 	// 	let oldItem = { id: itemId }; | ||||
| 	// 	return this.save(type, item, oldItem); | ||||
| 	// } | ||||
|  | ||||
| 	static openNoteList(folderId) { | ||||
| 		return Note.previews(folderId).then((notes) => { | ||||
|   | ||||
							
								
								
									
										122
									
								
								ReactNativeClient/src/string-utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								ReactNativeClient/src/string-utils.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| function removeDiacritics(str) { | ||||
|  | ||||
| 	var defaultDiacriticsRemovalMap = [ | ||||
| 		{'base':'A', 'letters':/[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g}, | ||||
| 		{'base':'AA','letters':/[\uA732]/g}, | ||||
| 		{'base':'AE','letters':/[\u00C6\u01FC\u01E2]/g}, | ||||
| 		{'base':'AO','letters':/[\uA734]/g}, | ||||
| 		{'base':'AU','letters':/[\uA736]/g}, | ||||
| 		{'base':'AV','letters':/[\uA738\uA73A]/g}, | ||||
| 		{'base':'AY','letters':/[\uA73C]/g}, | ||||
| 		{'base':'B', 'letters':/[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181]/g}, | ||||
| 		{'base':'C', 'letters':/[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E]/g}, | ||||
| 		{'base':'D', 'letters':/[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779]/g}, | ||||
| 		{'base':'DZ','letters':/[\u01F1\u01C4]/g}, | ||||
| 		{'base':'Dz','letters':/[\u01F2\u01C5]/g}, | ||||
| 		{'base':'E', 'letters':/[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E]/g}, | ||||
| 		{'base':'F', 'letters':/[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B]/g}, | ||||
| 		{'base':'G', 'letters':/[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E]/g}, | ||||
| 		{'base':'H', 'letters':/[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D]/g}, | ||||
| 		{'base':'I', 'letters':/[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197]/g}, | ||||
| 		{'base':'J', 'letters':/[\u004A\u24BF\uFF2A\u0134\u0248]/g}, | ||||
| 		{'base':'K', 'letters':/[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2]/g}, | ||||
| 		{'base':'L', 'letters':/[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780]/g}, | ||||
| 		{'base':'LJ','letters':/[\u01C7]/g}, | ||||
| 		{'base':'Lj','letters':/[\u01C8]/g}, | ||||
| 		{'base':'M', 'letters':/[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C]/g}, | ||||
| 		{'base':'N', 'letters':/[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4]/g}, | ||||
| 		{'base':'NJ','letters':/[\u01CA]/g}, | ||||
| 		{'base':'Nj','letters':/[\u01CB]/g}, | ||||
| 		{'base':'O', 'letters':/[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C]/g}, | ||||
| 		{'base':'OI','letters':/[\u01A2]/g}, | ||||
| 		{'base':'OO','letters':/[\uA74E]/g}, | ||||
| 		{'base':'OU','letters':/[\u0222]/g}, | ||||
| 		{'base':'P', 'letters':/[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754]/g}, | ||||
| 		{'base':'Q', 'letters':/[\u0051\u24C6\uFF31\uA756\uA758\u024A]/g}, | ||||
| 		{'base':'R', 'letters':/[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782]/g}, | ||||
| 		{'base':'S', 'letters':/[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784]/g}, | ||||
| 		{'base':'T', 'letters':/[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786]/g}, | ||||
| 		{'base':'TZ','letters':/[\uA728]/g}, | ||||
| 		{'base':'U', 'letters':/[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244]/g}, | ||||
| 		{'base':'V', 'letters':/[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245]/g}, | ||||
| 		{'base':'VY','letters':/[\uA760]/g}, | ||||
| 		{'base':'W', 'letters':/[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72]/g}, | ||||
| 		{'base':'X', 'letters':/[\u0058\u24CD\uFF38\u1E8A\u1E8C]/g}, | ||||
| 		{'base':'Y', 'letters':/[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE]/g}, | ||||
| 		{'base':'Z', 'letters':/[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762]/g}, | ||||
| 		{'base':'a', 'letters':/[\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250]/g}, | ||||
| 		{'base':'aa','letters':/[\uA733]/g}, | ||||
| 		{'base':'ae','letters':/[\u00E6\u01FD\u01E3]/g}, | ||||
| 		{'base':'ao','letters':/[\uA735]/g}, | ||||
| 		{'base':'au','letters':/[\uA737]/g}, | ||||
| 		{'base':'av','letters':/[\uA739\uA73B]/g}, | ||||
| 		{'base':'ay','letters':/[\uA73D]/g}, | ||||
| 		{'base':'b', 'letters':/[\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253]/g}, | ||||
| 		{'base':'c', 'letters':/[\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184]/g}, | ||||
| 		{'base':'d', 'letters':/[\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A]/g}, | ||||
| 		{'base':'dz','letters':/[\u01F3\u01C6]/g}, | ||||
| 		{'base':'e', 'letters':/[\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD]/g}, | ||||
| 		{'base':'f', 'letters':/[\u0066\u24D5\uFF46\u1E1F\u0192\uA77C]/g}, | ||||
| 		{'base':'g', 'letters':/[\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F]/g}, | ||||
| 		{'base':'h', 'letters':/[\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265]/g}, | ||||
| 		{'base':'hv','letters':/[\u0195]/g}, | ||||
| 		{'base':'i', 'letters':/[\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131]/g}, | ||||
| 		{'base':'j', 'letters':/[\u006A\u24D9\uFF4A\u0135\u01F0\u0249]/g}, | ||||
| 		{'base':'k', 'letters':/[\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3]/g}, | ||||
| 		{'base':'l', 'letters':/[\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747]/g}, | ||||
| 		{'base':'lj','letters':/[\u01C9]/g}, | ||||
| 		{'base':'m', 'letters':/[\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F]/g}, | ||||
| 		{'base':'n', 'letters':/[\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5]/g}, | ||||
| 		{'base':'nj','letters':/[\u01CC]/g}, | ||||
| 		{'base':'o', 'letters':/[\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275]/g}, | ||||
| 		{'base':'oi','letters':/[\u01A3]/g}, | ||||
| 		{'base':'ou','letters':/[\u0223]/g}, | ||||
| 		{'base':'oo','letters':/[\uA74F]/g}, | ||||
| 		{'base':'p','letters':/[\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755]/g}, | ||||
| 		{'base':'q','letters':/[\u0071\u24E0\uFF51\u024B\uA757\uA759]/g}, | ||||
| 		{'base':'r','letters':/[\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783]/g}, | ||||
| 		{'base':'s','letters':/[\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B]/g}, | ||||
| 		{'base':'t','letters':/[\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787]/g}, | ||||
| 		{'base':'tz','letters':/[\uA729]/g}, | ||||
| 		{'base':'u','letters':/[\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289]/g}, | ||||
| 		{'base':'v','letters':/[\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C]/g}, | ||||
| 		{'base':'vy','letters':/[\uA761]/g}, | ||||
| 		{'base':'w','letters':/[\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73]/g}, | ||||
| 		{'base':'x','letters':/[\u0078\u24E7\uFF58\u1E8B\u1E8D]/g}, | ||||
| 		{'base':'y','letters':/[\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF]/g}, | ||||
| 		{'base':'z','letters':/[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763]/g} | ||||
| 	]; | ||||
|  | ||||
| 	for(var i=0; i<defaultDiacriticsRemovalMap.length; i++) { | ||||
| 		str = str.replace(defaultDiacriticsRemovalMap[i].letters, defaultDiacriticsRemovalMap[i].base); | ||||
| 	} | ||||
|  | ||||
| 	return str; | ||||
| } | ||||
|  | ||||
| function escapeFilename(s, maxLength = 32) { | ||||
| 	let output = removeDiacritics(s); | ||||
| 	output = output.replace("\n\r", " "); | ||||
| 	output = output.replace("\r\n", " "); | ||||
| 	output = output.replace("\r", " "); | ||||
| 	output = output.replace("\n", " "); | ||||
| 	output = output.replace("\t", " "); | ||||
| 	output = output.replace("\0", ""); | ||||
|  | ||||
| 	const unsafe = "/\\:*\"'?<>|"; // In Windows | ||||
| 	for (let i = 0; i < unsafe.length; i++) { | ||||
| 		output = output.replace(unsafe[i], '_'); | ||||
| 	} | ||||
|  | ||||
| 	if (output.toLowerCase() == 'nul') output = 'n_l'; // For Windows... | ||||
|  | ||||
| 	return output.substr(0, maxLength); | ||||
| } | ||||
|  | ||||
| function folderItemFilename(item) { | ||||
| 	let output = escapeFilename(item.title).trim(); | ||||
| 	if (!output.length) output = '_'; | ||||
| 	return output + '.' + item.id.substr(0, 7); | ||||
| } | ||||
|  | ||||
| export { removeDiacritics, escapeFilename, folderItemFilename }; | ||||
| @@ -5,6 +5,10 @@ import { Folder } from 'src/models/folder.js'; | ||||
| import { Note } from 'src/models/note.js'; | ||||
| import { BaseModel } from 'src/base-model.js'; | ||||
| import { promiseChain } from 'src/promise-chain.js'; | ||||
| import moment from 'moment'; | ||||
|  | ||||
| const fs = require('fs'); | ||||
| const path = require('path'); | ||||
|  | ||||
| class Synchronizer { | ||||
|  | ||||
| @@ -26,63 +30,213 @@ 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 }; | ||||
|  | ||||
| 				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 }); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	remoteFileByName(remoteFiles, name) { | ||||
| 		for (let i = 0; i < remoteFiles.length; i++) { | ||||
| 			if (remoteFiles[i].name == name) return remoteFiles[i]; | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
|  | ||||
| 	conflictDir(remoteFiles) { | ||||
| 		let d = this.remoteFileByName('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.isDir) return Promise.resolve(); | ||||
|  | ||||
| 		return this.conflictDir().then((conflictDirPath) => { | ||||
| 			let p = path.basename(item.name).split('.'); | ||||
| 			let pos = item.isDir ? p.length - 1 : p.length - 2; | ||||
| 			p.splice(pos, 0, moment().format('YYYYMMDDThhmmss')); | ||||
| 			let newName = p.join('.'); | ||||
| 			return this.api().move(item.name, conflictDirPath + '/' + newName); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	processState_uploadChanges() { | ||||
| 		Change.all().then((changes) => { | ||||
| 		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 = []; | ||||
| 			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 = ItemClass.load(c.item_id).then((item) => { | ||||
| 							return this.api().put(path + '/' + item.id, null, item); | ||||
| 						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.remoteFileByName(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); | ||||
| 								} else { | ||||
| 									return this.api().put(path, Note.toFriendlyString(result.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); | ||||
| 					} | ||||
|  | ||||
| 					// TODO: handle UPDATE | ||||
| 					// TODO: handle DELETE | ||||
|  | ||||
| 					return p.then(() => { | ||||
| 						processedChangeIds = processedChangeIds.concat(c.ids); | ||||
| 					}).catch((error) => { | ||||
| 						Log.warn('Failed applying changes', c.ids, error.message, error.type); | ||||
| 						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; | ||||
| 						} | ||||
| 						// 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); | ||||
| 			}); | ||||
| 			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'); | ||||
| 		}); | ||||
|  | ||||
|  | ||||
| 		// }).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() { | ||||
| @@ -150,9 +304,10 @@ class Synchronizer { | ||||
| 		this.state_ = state; | ||||
|  | ||||
| 		if (state == 'uploadChanges') { | ||||
| 			processState_uploadChanges(); | ||||
| 			return this.processState_uploadChanges(); | ||||
| 		} else if (state == 'downloadChanges') { | ||||
| 			processState_downloadChanges(); | ||||
| 			return this.processState('idle'); | ||||
| 			//this.processState_downloadChanges(); | ||||
| 		} else if (state == 'idle') { | ||||
| 			// Nothing | ||||
| 		} else { | ||||
| @@ -168,12 +323,12 @@ class Synchronizer { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		if (!this.api().session()) { | ||||
| 			Log.info("Sync: cannot start synchronizer because user is not logged in."); | ||||
| 			return; | ||||
| 		} | ||||
| 		// if (!this.api().session()) { | ||||
| 		// 	Log.info("Sync: cannot start synchronizer because user is not logged in."); | ||||
| 		// 	return; | ||||
| 		// } | ||||
|  | ||||
| 		this.processState('uploadChanges'); | ||||
| 		return this.processState('uploadChanges'); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										224
									
								
								ReactNativeClient/src/synchronizer_old.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								ReactNativeClient/src/synchronizer_old.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,224 @@ | ||||
| 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 { BaseModel } from 'src/base-model.js'; | ||||
| import { promiseChain } from 'src/promise-chain.js'; | ||||
|  | ||||
| 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) => { | ||||
| 				return Folder.load(note.parent_id).then((folder) => { | ||||
| 					console.info('xxxxxxxxx',note); | ||||
| 					return Promise.resolve({ parent: folder, item: note }); | ||||
| 				}); | ||||
| 			}); | ||||
| 		} else { | ||||
| 			return Folder.load(change.item_id).then((folder) => { | ||||
| 				return Promise.resolve({ parent: null, item: folder }); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	processState_uploadChanges() { | ||||
| 		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() { | ||||
| 		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) { | ||||
| 		Log.info('Sync: processing: ' + state); | ||||
| 		this.state_ = state; | ||||
|  | ||||
| 		if (state == 'uploadChanges') { | ||||
| 			this.processState_uploadChanges(); | ||||
| 		} else if (state == 'downloadChanges') { | ||||
| 			this.processState('idle'); | ||||
| 			//this.processState_downloadChanges(); | ||||
| 		} else if (state == 'idle') { | ||||
| 			// Nothing | ||||
| 		} else { | ||||
| 			throw new Error('Invalid state: ' . state); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	start() { | ||||
| 		Log.info('Sync: start'); | ||||
|  | ||||
| 		if (this.state() != 'idle') { | ||||
| 			Log.info("Sync: cannot start synchronizer because synchronization already in progress. State: " + this.state()); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// if (!this.api().session()) { | ||||
| 		// 	Log.info("Sync: cannot start synchronizer because user is not logged in."); | ||||
| 		// 	return; | ||||
| 		// } | ||||
|  | ||||
| 		this.processState('uploadChanges'); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| export { Synchronizer }; | ||||
| @@ -3,6 +3,7 @@ import { isNode } from 'src/env.js'; | ||||
| import { stringify } from 'query-string'; | ||||
|  | ||||
| if (isNode()) { | ||||
| 	// TODO: doesn't work in React-Native - FormData gets set to "undefined" | ||||
| 	// Needs to be in a variable otherwise ReactNative will try to load this module (and fails due to | ||||
| 	// missing node modules), even if isNode() is false. | ||||
| 	let modulePath = 'src/shim.js'; | ||||
| @@ -29,6 +30,7 @@ class WebApi { | ||||
| 	constructor(baseUrl) { | ||||
| 		this.baseUrl_ = baseUrl; | ||||
| 		this.session_ = null; | ||||
| 		this.retryInterval_ = 500; | ||||
| 	} | ||||
|  | ||||
| 	setSession(v) { | ||||
| @@ -88,42 +90,62 @@ class WebApi { | ||||
| 		return cmd.join(' '); | ||||
| 	} | ||||
|  | ||||
| 	exec(method, path, query, data) { | ||||
| 	delay(milliseconds) { | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			if (this.session_) { | ||||
| 				query = query ? Object.assign({}, query) : {}; | ||||
| 				if (!query.session) query.session = this.session_; | ||||
| 			setTimeout(() => { | ||||
| 				resolve(); | ||||
| 			}, milliseconds); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	exec(method, path, query, data) { | ||||
| 		return this.execNoRetry(method, path, query, data).then((data) => { | ||||
| 			this.retryInterval_ = 500; | ||||
| 			return data; | ||||
| 		}).catch((error) => { | ||||
| 			if (error.errno == 'ECONNRESET') { | ||||
| 				this.retryInterval_ += 500; | ||||
| 				console.warn('Got error ' + error.errno + '. Retrying in ' + this.retryInterval_); | ||||
| 				return this.delay(this.retryInterval_).then(() => { | ||||
| 					return this.exec(method, path, query, data); | ||||
| 				}); | ||||
| 			} else { | ||||
| 				this.retryInterval_ = 500; | ||||
| 				reject(error); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	execNoRetry(method, path, query, data) { | ||||
| 		if (this.session_) { | ||||
| 			query = query ? Object.assign({}, query) : {}; | ||||
| 			if (!query.session) query.session = this.session_; | ||||
| 		} | ||||
|  | ||||
| 		let r = this.makeRequest(method, path, query, data); | ||||
|  | ||||
| 		Log.debug(WebApi.toCurl(r, data)); | ||||
| 		//console.info(WebApi.toCurl(r, data)); | ||||
|  | ||||
| 		return fetch(r.url, r.options).then((response) => { | ||||
| 			let responseClone = response.clone(); | ||||
|  | ||||
| 			if (!response.ok) { | ||||
| 				return responseClone.text().then((text) => { | ||||
| 					throw new WebApiError('HTTP ' + response.status + ': ' + response.statusText + ': ' + text); | ||||
| 				}); | ||||
| 			} | ||||
|  | ||||
| 			let r = this.makeRequest(method, path, query, data); | ||||
|  | ||||
| 			//Log.debug(WebApi.toCurl(r, data)); | ||||
| 			//console.info(WebApi.toCurl(r, data)); | ||||
|  | ||||
| 			fetch(r.url, r.options).then(function(response) { | ||||
| 				let responseClone = response.clone(); | ||||
|  | ||||
| 				if (!response.ok) { | ||||
| 					return responseClone.text().then(function(text) { | ||||
| 						reject(new WebApiError('HTTP ' + response.status + ': ' + response.statusText + ': ' + text)); | ||||
| 					}); | ||||
| 			return response.json().then((data) => { | ||||
| 				if (data && data.error) { | ||||
| 					throw new WebApiError(data); | ||||
| 				} else { | ||||
| 					return data; | ||||
| 				} | ||||
|  | ||||
| 				return response.json().then(function(data) { | ||||
| 					if (data && data.error) { | ||||
| 						reject(new WebApiError(data)); | ||||
| 					} else { | ||||
| 						resolve(data); | ||||
| 					} | ||||
| 				}).catch(function(error) { | ||||
| 					responseClone.text().then(function(text) { | ||||
| 						reject(new WebApiError('Cannot parse JSON: ' + text)); | ||||
| 					}); | ||||
| 			}).catch((error) => { | ||||
| 				return responseClone.text().then((text) => { | ||||
| 					throw new WebApiError('Cannot parse JSON: ' + text); | ||||
| 				}); | ||||
| 			}).then(function(data) { | ||||
| 				resolve(data); | ||||
| 			}).catch(function(error) { | ||||
| 				reject(error); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|   | ||||
| @@ -28,7 +28,6 @@ | ||||
|         "sensio/distribution-bundle": "^5.0", | ||||
|         "sensio/framework-extra-bundle": "^3.0.2", | ||||
|         "incenteev/composer-parameter-handler": "^2.0", | ||||
|  | ||||
|         "illuminate/database": "*", | ||||
|         "yetanotherape/diff-match-patch": "*" | ||||
|     }, | ||||
|   | ||||
| @@ -15,11 +15,13 @@ class SynchronizerController extends ApiController { | ||||
| 	 * @Route("/synchronizer") | ||||
| 	 */ | ||||
| 	public function allAction(Request $request) { | ||||
| 		$lastChangeId = (int)$request->query->get('rev_id'); | ||||
|  | ||||
| 		if (!$this->user() || !$this->session()) throw new UnauthorizedException(); | ||||
|  | ||||
| 		$actions = Change::changesDoneAfterId($this->user()->id, $this->session()->client_id, $lastChangeId); | ||||
| 		$lastChangeId = (int)$request->query->get('rev_id'); | ||||
| 		$limit = (int)$request->query->get('limit'); | ||||
| 		//$curl 'http://192.168.1.3/synchronizer?rev_id=6973&session=02d0e9ca42cbbc2d35efb1bc790b9eec' | ||||
|  | ||||
| 		$actions = Change::changesDoneAfterId($this->user()->id, $this->session()->client_id, $lastChangeId, $limit); | ||||
| 		return static::successResponse($actions); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -14,11 +14,26 @@ class Diff { | ||||
| 		return self::$dmp_; | ||||
| 	} | ||||
|  | ||||
| 	// Temporary fix to go around diffmatchpach bug: | ||||
| 	// https://github.com/yetanotherape/diff-match-patch/issues/9 | ||||
| 	static private function encodingFix($s) { | ||||
| 		return $s; | ||||
| 		return iconv('UTF-8', 'ISO-8859-1//IGNORE', $s); | ||||
| 	} | ||||
|  | ||||
| 	static public function decodeFix($s) { | ||||
| 		return $s; | ||||
| 		return iconv('ISO-8859-1', 'UTF-8', $s); | ||||
| 	} | ||||
|  | ||||
| 	static public function diff($from, $to) { | ||||
| 		$from = self::encodingFix($from); | ||||
| 		$to = self::encodingFix($to); | ||||
| 		return self::dmp()->patch_toText(self::dmp()->patch_make($from, $to)); | ||||
| 	} | ||||
|  | ||||
| 	static public function patch($from, $diff) { | ||||
| 		$from = self::encodingFix($from); | ||||
| 		return self::dmp()->patch_apply(self::dmp()->patch_fromText($diff), $from); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ class Change extends BaseModel { | ||||
| 		'type' => array('create', 'update', 'delete'), | ||||
| 	); | ||||
|  | ||||
| 	static public function changesDoneAfterId($userId, $clientId, $fromChangeId) { | ||||
| 	static public function changesDoneAfterId($userId, $clientId, $fromChangeId, $limit = null) { | ||||
| 		// Simplification: | ||||
| 		// | ||||
| 		// - If create, update, delete => return nothing | ||||
| @@ -19,7 +19,8 @@ class Change extends BaseModel { | ||||
| 		// - If update, update, update => return last | ||||
|  | ||||
| 		// $limit = 10000; | ||||
| 		$limit = 100; | ||||
| 		if ((int)$limit <= 0) $limit = 100; | ||||
| 		if ($limit > 1000) $limit = 1000; | ||||
| 		$changes = self::where('id', '>', $fromChangeId) | ||||
| 		               ->where('user_id', '=', $userId) | ||||
| 		               ->where('client_id', '!=', $clientId) | ||||
|   | ||||
| @@ -6,6 +6,19 @@ require_once dirname(__FILE__) . '/TestUtils.php'; | ||||
| require_once dirname(__FILE__) . '/BaseTestCase.php'; | ||||
| require_once dirname(__FILE__) . '/BaseControllerTestCase.php'; | ||||
|  | ||||
|  | ||||
|  | ||||
| // use DiffMatchPatch\DiffMatchPatch; | ||||
| // $dmp = new DiffMatchPatch(); | ||||
| // $diff = $dmp->patch_make('car', 'car 🚘'); | ||||
| // var_dump($dmp->patch_toText($diff)); | ||||
| // var_dump($dmp->patch_apply($diff, 'car')); | ||||
|  | ||||
| // //$dmp->patch_toText($dmp->patch_make($from, $to)); | ||||
| // die(); | ||||
|  | ||||
|  | ||||
|  | ||||
| $dbConfig = array( | ||||
| 	'dbName' => 'notes_test', | ||||
| 	'user' => 'root', | ||||
|   | ||||
| @@ -31,6 +31,7 @@ try { | ||||
| 		'error' => $e->getMessage(), | ||||
| 		'code' => $e->getCode(), | ||||
| 		'type' => $errorType, | ||||
| 		'trace' => $e->getTraceAsString(), | ||||
| 	); | ||||
| 	if ($errorType == 'NotFoundHttpException') { | ||||
| 		header('HTTP/1.1 404 Not found'); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user