You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Handle sync conflicts
This commit is contained in:
		| @@ -3,6 +3,7 @@ import { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, | ||||
| import { createFoldersAndNotes } from 'test-data.js'; | ||||
| import { Folder } from 'src/models/folder.js'; | ||||
| import { Note } from 'src/models/note.js'; | ||||
| import { Setting } from 'src/models/setting.js'; | ||||
| import { BaseItem } from 'src/models/base-item.js'; | ||||
| import { BaseModel } from 'src/base-model.js'; | ||||
|  | ||||
| @@ -125,5 +126,5 @@ describe('Synchronizer', function() { | ||||
|  | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| 	 | ||||
| }); | ||||
| @@ -4,6 +4,7 @@ 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 { Setting } from 'src/models/setting.js'; | ||||
| import { BaseItem } from 'src/models/base-item.js'; | ||||
| import { Synchronizer } from 'src/synchronizer.js'; | ||||
| import { FileApi } from 'src/file-api.js'; | ||||
| @@ -47,7 +48,9 @@ function setupDatabase(id = null) { | ||||
| 	if (id === null) id = currentClient_; | ||||
|  | ||||
| 	if (databases_[id]) { | ||||
| 		return clearDatabase(id); | ||||
| 		return clearDatabase(id).then(() => { | ||||
| 			return Setting.load(); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	const filePath = __dirname + '/data/test-' + id + '.sqlite'; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { BaseModel } from 'src/base-model.js'; | ||||
| import { Note } from 'src/models/note.js'; | ||||
| import { Folder } from 'src/models/folder.js'; | ||||
| import { Setting } from 'src/models/setting.js'; | ||||
| import { folderItemFilename } from 'src/string-utils.js' | ||||
| import { Database } from 'src/database.js'; | ||||
| import { time } from 'src/time-utils.js'; | ||||
| @@ -129,9 +130,10 @@ class BaseItem extends BaseModel { | ||||
| 	} | ||||
|  | ||||
| 	static itemsThatNeedSync(limit = 100) { | ||||
| 		return Folder.modelSelectAll('SELECT * FROM folders WHERE sync_time < updated_time LIMIT ' + limit).then((items) => { | ||||
| 		let conflictFolderId = Setting.value('sync.conflictFolderId'); | ||||
| 		return Folder.modelSelectAll('SELECT * FROM folders WHERE sync_time < updated_time AND id != ? LIMIT ' + limit, [conflictFolderId]).then((items) => { | ||||
| 			if (items.length) return { hasMore: true, items: items }; | ||||
| 			return Note.modelSelectAll('SELECT * FROM notes WHERE sync_time < updated_time LIMIT ' + limit).then((items) => { | ||||
| 			return Note.modelSelectAll('SELECT * FROM notes WHERE sync_time < updated_time AND parent_id != ? LIMIT ' + limit, [conflictFolderId]).then((items) => { | ||||
| 				return { hasMore: items.length >= limit, items: items }; | ||||
| 			}); | ||||
| 		}); | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { BaseModel } from 'src/base-model.js'; | ||||
| import { Log } from 'src/log.js'; | ||||
| import { promiseChain } from 'src/promise-utils.js'; | ||||
| import { Note } from 'src/models/note.js'; | ||||
| import { Setting } from 'src/models/setting.js'; | ||||
| import { folderItemFilename } from 'src/string-utils.js' | ||||
| import { _ } from 'src/locale.js'; | ||||
| import moment from 'moment'; | ||||
| @@ -80,7 +81,18 @@ class Folder extends BaseItem { | ||||
|  | ||||
| 		let notes = await Note.modelSelectAll('SELECT * FROM notes'); | ||||
| 		return folders.concat(notes); | ||||
| 	} | ||||
|  | ||||
| 	static conflictFolder() { | ||||
| 		let folderId = Setting.value('sync.conflictFolderId'); | ||||
| 		if (!folderId) { | ||||
| 			return Folder.save({ title: _('Conflicts') }).then((folder) => { | ||||
| 				Setting.setValue('sync.conflictFolderId', folder.id); | ||||
| 				return folder; | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		return Folder.load(folderId); | ||||
| 	} | ||||
|  | ||||
| 	static save(o, options = null) { | ||||
|   | ||||
| @@ -33,6 +33,8 @@ class Setting extends BaseModel { | ||||
| 	} | ||||
|  | ||||
| 	static setValue(key, value) { | ||||
| 		if (!this.cache_) throw new Error('Settings have not been initialized!'); | ||||
| 		 | ||||
| 		for (let i = 0; i < this.cache_.length; i++) { | ||||
| 			if (this.cache_[i].key == key) { | ||||
| 				if (this.cache_[i].value === value) return; | ||||
| @@ -49,6 +51,8 @@ class Setting extends BaseModel { | ||||
| 	} | ||||
|  | ||||
| 	static value(key) { | ||||
| 		if (!this.cache_) throw new Error('Settings have not been initialized!'); | ||||
|  | ||||
| 		for (let i = 0; i < this.cache_.length; i++) { | ||||
| 			if (this.cache_[i].key == key) { | ||||
| 				return this.cache_[i].value; | ||||
| @@ -118,6 +122,7 @@ Setting.defaults_ = { | ||||
| 	'user.session': { value: '', type: 'string' }, | ||||
| 	'sync.lastRevId': { value: 0, type: 'int' }, // DEPRECATED | ||||
| 	'sync.lastUpdateTime': { value: 0, type: 'int' }, | ||||
| 	'sync.conflictFolderId': { value: '', type: 'string' }, | ||||
| }; | ||||
|  | ||||
| export { Setting }; | ||||
| @@ -1,6 +1,7 @@ | ||||
| require('babel-plugin-transform-runtime'); | ||||
|  | ||||
| import { BaseItem } from 'src/models/base-item.js'; | ||||
| import { BaseModel } from 'src/base-model.js'; | ||||
| import { sprintf } from 'sprintf-js'; | ||||
| import { time } from 'src/time-utils.js'; | ||||
| import { Log } from 'src/log.js' | ||||
| @@ -42,12 +43,13 @@ class Synchronizer { | ||||
| 				let remote = await this.api().stat(path); | ||||
| 				let content = ItemClass.serialize(local); | ||||
| 				let action = null; | ||||
| 				let updateSyncTimeOnly = true; | ||||
|  | ||||
| 				if (!remote) { | ||||
| 					action = 'createRemote'; | ||||
| 				} else { | ||||
| 					if (remote.updated_time > local.updated_time) { | ||||
| 						action = 'conflict'; | ||||
| 					if (remote.updated_time > local.updated_time && local.type_ == BaseModel.ITEM_TYPE_NOTE) { | ||||
| 						action = 'noteConflict'; | ||||
| 					} else { | ||||
| 						action = 'updateRemote'; | ||||
| 					} | ||||
| @@ -56,11 +58,28 @@ class Synchronizer { | ||||
| 				if (action == 'createRemote' || action == 'updateRemote') { | ||||
| 					await this.api().put(path, content); | ||||
| 					await this.api().setTimestamp(path, local.updated_time); | ||||
| 				} else if (action == 'conflict') { | ||||
| 					console.warn('FOUND CONFLICT', local, remote); | ||||
| 				} else if (action == 'noteConflict') { | ||||
| 					// - Create a duplicate of local note into Conflicts folder (to preserve the user's changes) | ||||
| 					// - Overwrite local note with remote note | ||||
| 					let conflictFolder = await Folder.conflictFolder(); | ||||
| 					let conflictedNote = Object.assign({}, local); | ||||
| 					delete conflictedNote.id; | ||||
| 					conflictedNote.parent_id = conflictFolder.id; | ||||
| 					await Note.save(conflictedNote); | ||||
|  | ||||
| 					let remoteContent = await this.api().get(path); | ||||
| 					local = BaseItem.unserialize(remoteContent); | ||||
| 					updateSyncTimeOnly = false; | ||||
| 				} | ||||
|  | ||||
| 				let newLocal = null; | ||||
| 				if (updateSyncTimeOnly) { | ||||
| 					newLocal = { id: local.id, sync_time: time.unixMs(), type_: local.type_ }; | ||||
| 				} else { | ||||
| 					newLocal = local; | ||||
| 					newLocal.sync_time = time.unixMs(); | ||||
| 				} | ||||
|  | ||||
| 				let newLocal = { id: local.id, sync_time: time.unixMs(), type_: local.type_ }; | ||||
| 				await ItemClass.save(newLocal, { autoTimestamp: false }); | ||||
|  | ||||
| 				donePaths.push(path); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user