mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-24 08:12:24 +02:00
sync fixes
This commit is contained in:
parent
b3939887b8
commit
a4374e3bdb
@ -5,7 +5,6 @@ rm -f "$CLIENT_DIR/tests-build/src"
|
||||
mkdir -p "$CLIENT_DIR/tests-build/data"
|
||||
ln -s "$CLIENT_DIR/build/src" "$CLIENT_DIR/tests-build"
|
||||
|
||||
npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/base-model.js
|
||||
|
||||
#npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/synchronizer.js
|
||||
#npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/base-model.js
|
||||
npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/synchronizer.js
|
||||
#npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/services/note-folder-service.js
|
@ -38,7 +38,7 @@ describe('Synchronizer', function() {
|
||||
beforeEach( async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await setupDatabaseAndSynchronizer(2);
|
||||
switchClient(1);
|
||||
await switchClient(1);
|
||||
done();
|
||||
});
|
||||
|
||||
@ -77,7 +77,7 @@ describe('Synchronizer', function() {
|
||||
await Note.save({ title: "un", parent_id: folder.id });
|
||||
await synchronizer().start();
|
||||
|
||||
switchClient(2);
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
@ -92,7 +92,7 @@ describe('Synchronizer', function() {
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
|
||||
switchClient(2);
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
@ -108,7 +108,7 @@ describe('Synchronizer', function() {
|
||||
|
||||
let files = await fileApi().list();
|
||||
|
||||
switchClient(1);
|
||||
await switchClient(1);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
@ -126,7 +126,7 @@ describe('Synchronizer', function() {
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
|
||||
switchClient(2);
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
@ -139,7 +139,7 @@ describe('Synchronizer', function() {
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
switchClient(1);
|
||||
await switchClient(1);
|
||||
|
||||
await sleep(0.1);
|
||||
|
||||
@ -150,19 +150,17 @@ describe('Synchronizer', function() {
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
let conflictFolder = await Folder.conflictFolder();
|
||||
let conflictedNotes = await Note.all(conflictFolder.id);
|
||||
let conflictedNotes = await Note.conflicedNotes();
|
||||
|
||||
expect(conflictedNotes.length).toBe(1);
|
||||
|
||||
// Other than the id (since the conflicted note is a duplicate), parent_id (which is now the Conflicts folder) and sync_time,
|
||||
// the note must be the same in every way, to make sure no data has been lost.
|
||||
// Other than the id (since the conflicted note is a duplicate), and the is_conflict property
|
||||
// the conflicted and original note must be the same in every way, to make sure no data has been lost.
|
||||
let conflictedNote = conflictedNotes[0];
|
||||
expect(conflictedNote.id == note2conf.id).toBe(false);
|
||||
expect(conflictedNote.parent_id == note2conf.parent_id).toBe(false);
|
||||
for (let n in conflictedNote) {
|
||||
if (!conflictedNote.hasOwnProperty(n)) continue;
|
||||
if (n == 'id' || n == 'parent_id') continue;
|
||||
if (n == 'id' || n == 'is_conflict') continue;
|
||||
expect(conflictedNote[n]).toBe(note2conf[n], 'Property: ' + n);
|
||||
}
|
||||
|
||||
@ -181,7 +179,7 @@ describe('Synchronizer', function() {
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
|
||||
switchClient(2); // ----------------------------------
|
||||
await switchClient(2); // ----------------------------------
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
@ -194,7 +192,7 @@ describe('Synchronizer', function() {
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
switchClient(1); // ----------------------------------
|
||||
await switchClient(1); // ----------------------------------
|
||||
|
||||
await sleep(0.1);
|
||||
|
||||
@ -211,21 +209,68 @@ describe('Synchronizer', function() {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should delete remote items', async (done) => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
await sleep(0.1);
|
||||
|
||||
await Note.delete(note1.id);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
let files = await fileApi().list();
|
||||
|
||||
expect(files.length).toBe(1);
|
||||
expect(files[0].path).toBe(Folder.systemPath(folder1));
|
||||
|
||||
let deletedItems = await BaseModel.deletedItems();
|
||||
expect(deletedItems.length).toBe(0);
|
||||
|
||||
// it('should delete local items', async (done) => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should delete local items', async (done) => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
await sleep(0.1);
|
||||
|
||||
await Note.delete(note1.id);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(1);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
let items = await Folder.all(true);
|
||||
|
||||
expect(items.length).toBe(1);
|
||||
|
||||
let deletedItems = await BaseModel.deletedItems();
|
||||
|
||||
expect(deletedItems.length).toBe(0);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
// it('should handle conflict when remote note is deleted then local note is modified', async (done) => {
|
||||
// let folder1 = await Folder.save({ title: "folder1" });
|
||||
// let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
// await synchronizer().start();
|
||||
|
||||
// switchClient(2);
|
||||
// await switchClient(2);
|
||||
|
||||
// await synchronizer().start();
|
||||
|
||||
@ -235,57 +280,20 @@ describe('Synchronizer', function() {
|
||||
|
||||
// await synchronizer().start();
|
||||
|
||||
// switchClient(1);
|
||||
// await switchClient(1);
|
||||
|
||||
// let files = await fileApi().list();
|
||||
// console.info(files);
|
||||
|
||||
// // await synchronizer().start();
|
||||
|
||||
// // note1 = await Note.load(note1.id);
|
||||
|
||||
// // expect(!note1).toBe(true);
|
||||
|
||||
// done();
|
||||
// });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// it('should delete remote items', async (done) => {
|
||||
// let folder1 = await Folder.save({ title: "folder1" });
|
||||
// let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
// await synchronizer().start();
|
||||
|
||||
// switchClient(2);
|
||||
// await Note.save({ id: note1.id, title: 'Modified after having been deleted' });
|
||||
|
||||
// await synchronizer().start();
|
||||
|
||||
// await sleep(0.1);
|
||||
// // let items = await Folder.all(true);
|
||||
|
||||
// await Note.delete(note1.id);
|
||||
// // expect(items.length).toBe(1);
|
||||
|
||||
// await synchronizer().start();
|
||||
|
||||
// switchClient(1);
|
||||
|
||||
// let files = await fileApi().list();
|
||||
// console.info(files);
|
||||
|
||||
// await synchronizer().start();
|
||||
|
||||
// note1 = await Note.load(note1.id);
|
||||
|
||||
// expect(!note1).toBe(true);
|
||||
// // let deletedItems = await BaseModel.deletedItems();
|
||||
|
||||
// // expect(deletedItems.length).toBe(0);
|
||||
|
||||
// done();
|
||||
// });
|
||||
|
||||
|
@ -24,11 +24,16 @@ function sleep(n) {
|
||||
}
|
||||
|
||||
function switchClient(id) {
|
||||
Setting.saveAll();
|
||||
|
||||
currentClient_ = id;
|
||||
BaseModel.db_ = databases_[id];
|
||||
Folder.db_ = databases_[id];
|
||||
Note.db_ = databases_[id];
|
||||
BaseItem.db_ = databases_[id];
|
||||
Setting.db_ = databases_[id];
|
||||
|
||||
return Setting.load();
|
||||
}
|
||||
|
||||
function clearDatabase(id = null) {
|
||||
|
@ -103,6 +103,7 @@ class BaseModel {
|
||||
options = Object.assign({}, options);
|
||||
}
|
||||
if (!('trackChanges' in options)) options.trackChanges = true;
|
||||
if (!('trackDeleted' in options)) options.trackDeleted = null;
|
||||
if (!('isNew' in options)) options.isNew = 'auto';
|
||||
if (!('autoTimestamp' in options)) options.autoTimestamp = true;
|
||||
return options;
|
||||
@ -247,6 +248,10 @@ class BaseModel {
|
||||
return this.db().selectAll('SELECT * FROM deleted_items');
|
||||
}
|
||||
|
||||
static remoteDeletedItem(itemId) {
|
||||
return this.db().exec('DELETE FROM deleted_items WHERE item_id = ?', [itemId]);
|
||||
}
|
||||
|
||||
static delete(id, options = null) {
|
||||
options = this.modOptions(options);
|
||||
|
||||
@ -256,7 +261,9 @@ class BaseModel {
|
||||
}
|
||||
|
||||
return this.db().exec('DELETE FROM ' + this.tableName() + ' WHERE id = ?', [id]).then(() => {
|
||||
if (this.trackDeleted()) {
|
||||
let trackDeleted = this.trackDeleted();
|
||||
if (options.trackDeleted !== null) trackDeleted = options.trackDeleted;
|
||||
if (trackDeleted) {
|
||||
return this.db().exec('INSERT INTO deleted_items (item_type, item_id, deleted_time) VALUES (?, ?, ?)', [this.itemType(), id, time.unixMs()]);
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ CREATE TABLE notes (
|
||||
created_time INT NOT NULL DEFAULT 0,
|
||||
updated_time INT NOT NULL DEFAULT 0,
|
||||
sync_time INT NOT NULL DEFAULT 0,
|
||||
is_conflict INT NOT NULL DEFAULT 0,
|
||||
latitude NUMERIC NOT NULL DEFAULT 0,
|
||||
longitude NUMERIC NOT NULL DEFAULT 0,
|
||||
altitude NUMERIC NOT NULL DEFAULT 0,
|
||||
@ -37,7 +38,7 @@ CREATE TABLE notes (
|
||||
);
|
||||
|
||||
CREATE TABLE deleted_items (
|
||||
id TEXT PRIMARY KEY,
|
||||
id INTEGER PRIMARY KEY,
|
||||
item_type INT NOT NULL,
|
||||
item_id TEXT NOT NULL,
|
||||
deleted_time INT NOT NULL
|
||||
@ -267,6 +268,7 @@ class Database {
|
||||
let params = [];
|
||||
for (let key in data) {
|
||||
if (!data.hasOwnProperty(key)) continue;
|
||||
if (key[key.length - 1] == '_') continue;
|
||||
if (keySql != '') keySql += ', ';
|
||||
if (valueSql != '') valueSql += ', ';
|
||||
keySql += '`' + key + '`';
|
||||
@ -286,6 +288,7 @@ class Database {
|
||||
let params = [];
|
||||
for (let key in data) {
|
||||
if (!data.hasOwnProperty(key)) continue;
|
||||
if (key[key.length - 1] == '_') continue;
|
||||
if (sql != '') sql += ', ';
|
||||
sql += '`' + key + '`=?';
|
||||
params.push(data[key]);
|
||||
|
@ -2,7 +2,6 @@ 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';
|
||||
import moment from 'moment';
|
||||
@ -13,8 +12,9 @@ class BaseItem extends BaseModel {
|
||||
return true;
|
||||
}
|
||||
|
||||
static systemPath(item) {
|
||||
return folderItemFilename(item) + '.md';
|
||||
static systemPath(itemOrId) {
|
||||
if (typeof itemOrId === 'string') return itemOrId + '.md';
|
||||
return itemOrId.id + '.md';
|
||||
}
|
||||
|
||||
static itemClass(item) {
|
||||
|
@ -3,7 +3,6 @@ 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';
|
||||
import { BaseItem } from 'src/models/base-item.js';
|
||||
@ -38,7 +37,7 @@ class Folder extends BaseItem {
|
||||
}
|
||||
|
||||
static syncedNoteIds() {
|
||||
return this.db().selectAll('SELECT id FROM notes WHERE sync_time > 0').then((rows) => {
|
||||
return this.db().selectAll('SELECT id FROM notes WHERE is_conflict = 0 AND sync_time > 0').then((rows) => {
|
||||
let output = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
output.push(rows[i].id);
|
||||
@ -48,7 +47,7 @@ class Folder extends BaseItem {
|
||||
}
|
||||
|
||||
static noteIds(parentId) {
|
||||
return this.db().selectAll('SELECT id FROM notes WHERE parent_id = ?', [parentId]).then((rows) => {
|
||||
return this.db().selectAll('SELECT id FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId]).then((rows) => {
|
||||
let output = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let row = rows[i];
|
||||
@ -87,29 +86,17 @@ class Folder extends BaseItem {
|
||||
}
|
||||
|
||||
static loadNoteByField(folderId, field, value) {
|
||||
return this.modelSelectAll('SELECT * FROM notes WHERE `parent_id` = ? AND `' + field + '` = ?', [folderId, value]);
|
||||
return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0 AND `parent_id` = ? AND `' + field + '` = ?', [folderId, value]);
|
||||
}
|
||||
|
||||
static async all(includeNotes = false) {
|
||||
let folders = await Folder.modelSelectAll('SELECT * FROM folders');
|
||||
if (!includeNotes) return folders;
|
||||
|
||||
let notes = await Note.modelSelectAll('SELECT * FROM notes');
|
||||
let notes = await Note.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0');
|
||||
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) {
|
||||
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));
|
||||
|
@ -2,7 +2,6 @@ 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 { BaseItem } from 'src/models/base-item.js';
|
||||
import moment from 'moment';
|
||||
|
||||
@ -45,13 +44,15 @@ class Note extends BaseItem {
|
||||
}
|
||||
|
||||
static previews(parentId) {
|
||||
return this.modelSelectAll('SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE parent_id = ?', [parentId]);
|
||||
//return this.db().selectAll('SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE parent_id = ?', [parentId]);
|
||||
return this.modelSelectAll('SELECT ' + this.previewFieldsSql() + ' FROM is_conflict = 0 AND notes WHERE parent_id = ?', [parentId]);
|
||||
}
|
||||
|
||||
static preview(noteId) {
|
||||
return this.modelSelectOne('SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE id = ?', [noteId]);
|
||||
//return this.db().selectOne('SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE id = ?', [noteId]);
|
||||
return this.modelSelectOne('SELECT ' + this.previewFieldsSql() + ' FROM is_conflict = 0 AND notes WHERE id = ?', [noteId]);
|
||||
}
|
||||
|
||||
static conflicedNotes() {
|
||||
return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 1');
|
||||
}
|
||||
|
||||
static updateGeolocation(noteId) {
|
||||
@ -74,7 +75,7 @@ class Note extends BaseItem {
|
||||
}
|
||||
|
||||
static all(parentId) {
|
||||
return this.modelSelectAll('SELECT * FROM notes WHERE parent_id = ?', [parentId]);
|
||||
return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId]);
|
||||
}
|
||||
|
||||
static save(o, options = null) {
|
||||
|
@ -113,11 +113,4 @@ function escapeFilename(s, maxLength = 32) {
|
||||
return output.substr(0, maxLength);
|
||||
}
|
||||
|
||||
function folderItemFilename(item) {
|
||||
return item.id;
|
||||
// let output = escapeFilename(item.title).trim();
|
||||
// if (!output.length) output = '_';
|
||||
// return output + '.' + item.id.substr(0, 7);
|
||||
}
|
||||
|
||||
export { removeDiacritics, escapeFilename, folderItemFilename };
|
||||
export { removeDiacritics, escapeFilename };
|
@ -69,35 +69,36 @@ class Synchronizer {
|
||||
console.info('Sync action (1): ' + action);
|
||||
|
||||
if (action == 'createRemote' || action == 'updateRemote') {
|
||||
|
||||
await this.api().put(path, content);
|
||||
await this.api().setTimestamp(path, local.updated_time);
|
||||
|
||||
await ItemClass.save({ id: local.id, sync_time: time.unixMs(), type_: local.type_ }, { autoTimestamp: false });
|
||||
|
||||
} else if (action == 'folderConflict') {
|
||||
|
||||
let remoteContent = await this.api().get(path);
|
||||
local = BaseItem.unserialize(remoteContent);
|
||||
updateSyncTimeOnly = false;
|
||||
|
||||
local.sync_time = time.unixMs();
|
||||
await ItemClass.save(local, { autoTimestamp: false });
|
||||
|
||||
} 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;
|
||||
conflictedNote.is_conflict = 1;
|
||||
await Note.save(conflictedNote, { autoTimestamp: false });
|
||||
|
||||
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();
|
||||
}
|
||||
local.sync_time = time.unixMs();
|
||||
await ItemClass.save(local, { autoTimestamp: false });
|
||||
|
||||
await ItemClass.save(newLocal, { autoTimestamp: false });
|
||||
}
|
||||
|
||||
donePaths.push(path);
|
||||
}
|
||||
@ -106,7 +107,20 @@ class Synchronizer {
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Then, loop through all the remote items, find those that
|
||||
// Delete the remote items that have been deleted locally.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
let deletedItems = await BaseModel.deletedItems();
|
||||
for (let i = 0; i < deletedItems.length; i++) {
|
||||
let item = deletedItems[i];
|
||||
let path = BaseItem.systemPath(item.item_id)
|
||||
console.info('Sync action (2): deleteRemote');
|
||||
await this.api().delete(path);
|
||||
await BaseModel.remoteDeletedItem(item.item_id);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Loop through all the remote items, find those that
|
||||
// have been updated, and apply the changes to local.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
@ -133,7 +147,7 @@ class Synchronizer {
|
||||
|
||||
if (!action) continue;
|
||||
|
||||
console.info('Sync action (2): ' + action);
|
||||
console.info('Sync action (3): ' + action);
|
||||
|
||||
if (action == 'createLocal' || action == 'updateLocal') {
|
||||
let content = await this.api().get(path);
|
||||
@ -156,13 +170,13 @@ class Synchronizer {
|
||||
// means the item has been deleted.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
// let noteIds = Folder.syncedNoteIds();
|
||||
// for (let i = 0; i < noteIds.length; i++) {
|
||||
// if (remoteIds.indexOf(noteIds[i]) < 0) {
|
||||
// console.info('Sync action (3): Delete ' + noteIds[i]);
|
||||
// await Note.delete(noteIds[i]);
|
||||
// }
|
||||
// }
|
||||
let noteIds = await Folder.syncedNoteIds();
|
||||
for (let i = 0; i < noteIds.length; i++) {
|
||||
if (remoteIds.indexOf(noteIds[i]) < 0) {
|
||||
console.info('Sync action (4): deleteLocal: ' + noteIds[i]);
|
||||
await Note.delete(noteIds[i], { trackDeleted: false });
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user