mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
testing sync
This commit is contained in:
parent
06dc16bcb4
commit
2e8a56ad5e
@ -59,12 +59,13 @@ async function runTest() {
|
|||||||
|
|
||||||
await synchronizer.start();
|
await synchronizer.start();
|
||||||
|
|
||||||
note1 = await Note.load(note1.id);
|
// note1 = await Note.load(note1.id);
|
||||||
//console.info(note1);
|
// note1.title = 'un update';
|
||||||
note1.title = 'un update';
|
// //console.info('AVANT', note1);
|
||||||
await Note.save(note1);
|
// note1 = await Note.save(note1);
|
||||||
|
// //console.info('APRES', note1);
|
||||||
|
|
||||||
return await synchronizer.start();
|
// return await synchronizer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
runTest().catch((error) => {
|
runTest().catch((error) => {
|
||||||
|
0
CliClient/b
Normal file
0
CliClient/b
Normal file
@ -27,11 +27,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-changed": "^7.0.0",
|
"babel-changed": "^7.0.0",
|
||||||
"babel-cli": "^6.24.1",
|
"babel-cli": "^6.24.1",
|
||||||
"babel-preset-env": "^1.5.1",
|
|
||||||
"babel-preset-react": "^6.24.1",
|
|
||||||
"babel-plugin-syntax-async-functions": "^6.1.4",
|
"babel-plugin-syntax-async-functions": "^6.1.4",
|
||||||
"babel-plugin-transform-regenerator": "^6.1.4",
|
"babel-plugin-transform-regenerator": "^6.1.4",
|
||||||
|
"babel-preset-env": "^1.5.1",
|
||||||
"babel-preset-es2015": "^6.1.4",
|
"babel-preset-es2015": "^6.1.4",
|
||||||
|
"babel-preset-react": "^6.24.1",
|
||||||
"jasmine": "^2.6.0"
|
"jasmine": "^2.6.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -1,175 +1,207 @@
|
|||||||
import { time } from 'src/time-utils.js';
|
import { time } from 'src/time-utils.js';
|
||||||
import { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi } from 'test-utils.js';
|
import { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi } from 'test-utils.js';
|
||||||
import { createFoldersAndNotes } from 'test-data.js';
|
import { createFoldersAndNotes } from 'test-data.js';
|
||||||
|
import { Folder } from 'src/models/folder.js';
|
||||||
|
import { Note } from 'src/models/note.js';
|
||||||
|
import { BaseItem } from 'src/models/base-item.js';
|
||||||
|
|
||||||
// Note: set 1 matches set 1 of createRemoteItems()
|
describe('Synchronizer', function() {
|
||||||
function createLocalItems(id, updatedTime, lastSyncTime) {
|
|
||||||
let output = [];
|
|
||||||
if (id === 1) {
|
|
||||||
output.push({ path: 'test', isDir: true, updatedTime: updatedTime, lastSyncTime: lastSyncTime });
|
|
||||||
output.push({ path: 'test/un', updatedTime: updatedTime, lastSyncTime: lastSyncTime });
|
|
||||||
} else {
|
|
||||||
throw new Error('Invalid ID');
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createRemoteItems(id = 1, updatedTime = null) {
|
|
||||||
if (!updatedTime) updatedTime = time.unix();
|
|
||||||
|
|
||||||
if (id === 1) {
|
|
||||||
return fileApi().format()
|
|
||||||
.then(() => fileApi().mkdir('test'))
|
|
||||||
.then(() => fileApi().put('test/un', 'abcd'))
|
|
||||||
.then(() => fileApi().list('', true))
|
|
||||||
.then((items) => {
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
items[i].updatedTime = updatedTime;
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error('Invalid ID');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Synchronizer syncActions', function() {
|
|
||||||
|
|
||||||
beforeEach(function(done) {
|
|
||||||
setupDatabaseAndSynchronizer(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create remote items', function() {
|
|
||||||
let localItems = createLocalItems(1, time.unix(), 0);
|
|
||||||
let remoteItems = [];
|
|
||||||
|
|
||||||
let actions = synchronizer().syncActions(localItems, remoteItems, []);
|
|
||||||
|
|
||||||
expect(actions.length).toBe(2);
|
|
||||||
for (let i = 0; i < actions.length; i++) {
|
|
||||||
expect(actions[i].type).toBe('create');
|
|
||||||
expect(actions[i].dest).toBe('remote');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update remote items', function(done) {
|
|
||||||
createRemoteItems(1).then((remoteItems) => {
|
|
||||||
let lastSyncTime = time.unix() + 1000;
|
|
||||||
let localItems = createLocalItems(1, lastSyncTime + 1000, lastSyncTime);
|
|
||||||
let actions = synchronizer().syncActions(localItems, remoteItems, []);
|
|
||||||
|
|
||||||
expect(actions.length).toBe(2);
|
|
||||||
for (let i = 0; i < actions.length; i++) {
|
|
||||||
expect(actions[i].type).toBe('update');
|
|
||||||
expect(actions[i].dest).toBe('remote');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
beforeEach( async (done) => {
|
||||||
|
await setupDatabaseAndSynchronizer();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect conflict', function(done) {
|
it('should create remote items', async (done) => {
|
||||||
// Simulate this scenario:
|
let folder = await Folder.save({ title: "folder1" });
|
||||||
// - Client 1 create items
|
await Note.save({ title: "un", parent_id: folder.id });
|
||||||
// - Client 1 sync
|
|
||||||
// - Client 2 sync
|
|
||||||
// - Client 2 change items
|
|
||||||
// - Client 2 sync
|
|
||||||
// - Client 1 change items
|
|
||||||
// - Client 1 sync
|
|
||||||
// => Conflict
|
|
||||||
|
|
||||||
createRemoteItems(1).then((remoteItems) => {
|
let all = await Folder.all(true);
|
||||||
let localItems = createLocalItems(1, time.unix() + 1000, time.unix() - 1000);
|
|
||||||
let actions = synchronizer().syncActions(localItems, remoteItems, []);
|
|
||||||
|
|
||||||
expect(actions.length).toBe(2);
|
await synchronizer().start();
|
||||||
for (let i = 0; i < actions.length; i++) {
|
|
||||||
expect(actions[i].type).toBe('conflict');
|
for (let i = 0; i < all.length; i++) {
|
||||||
|
let dbItem = all[i];
|
||||||
|
let path = BaseItem.systemPath(all[i]);
|
||||||
|
let remote = await fileApi().stat(path);
|
||||||
|
expect(!!remote).toBe(true);
|
||||||
|
expect(remote.updatedTime).toBe(dbItem.updated_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should create local file', function(done) {
|
|
||||||
createRemoteItems(1).then((remoteItems) => {
|
|
||||||
let localItems = [];
|
|
||||||
let actions = synchronizer().syncActions(localItems, remoteItems, []);
|
|
||||||
|
|
||||||
expect(actions.length).toBe(2);
|
|
||||||
for (let i = 0; i < actions.length; i++) {
|
|
||||||
expect(actions[i].type).toBe('create');
|
|
||||||
expect(actions[i].dest).toBe('local');
|
|
||||||
}
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete remote files', function(done) {
|
|
||||||
createRemoteItems(1).then((remoteItems) => {
|
|
||||||
let localItems = createLocalItems(1, time.unix(), time.unix());
|
|
||||||
let deletedItemPaths = [localItems[0].path, localItems[1].path];
|
|
||||||
let actions = synchronizer().syncActions([], remoteItems, deletedItemPaths);
|
|
||||||
|
|
||||||
expect(actions.length).toBe(2);
|
|
||||||
for (let i = 0; i < actions.length; i++) {
|
|
||||||
expect(actions[i].type).toBe('delete');
|
|
||||||
expect(actions[i].dest).toBe('remote');
|
|
||||||
}
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete local files', function(done) {
|
|
||||||
let lastSyncTime = time.unix();
|
|
||||||
createRemoteItems(1, lastSyncTime - 1000).then((remoteItems) => {
|
|
||||||
let localItems = createLocalItems(1, lastSyncTime - 1000, lastSyncTime);
|
|
||||||
let actions = synchronizer().syncActions(localItems, [], []);
|
|
||||||
|
|
||||||
expect(actions.length).toBe(2);
|
|
||||||
for (let i = 0; i < actions.length; i++) {
|
|
||||||
expect(actions[i].type).toBe('delete');
|
|
||||||
expect(actions[i].dest).toBe('local');
|
|
||||||
}
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update local files', function(done) {
|
|
||||||
let lastSyncTime = time.unix();
|
|
||||||
createRemoteItems(1, lastSyncTime + 1000).then((remoteItems) => {
|
|
||||||
let localItems = createLocalItems(1, lastSyncTime - 1000, lastSyncTime);
|
|
||||||
let actions = synchronizer().syncActions(localItems, remoteItems, []);
|
|
||||||
|
|
||||||
expect(actions.length).toBe(2);
|
|
||||||
for (let i = 0; i < actions.length; i++) {
|
|
||||||
expect(actions[i].type).toBe('update');
|
|
||||||
expect(actions[i].dest).toBe('local');
|
|
||||||
}
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Synchronizer start', function() {
|
|
||||||
|
|
||||||
beforeEach(function(done) {
|
|
||||||
setupDatabaseAndSynchronizer(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create remote items', function(done) {
|
|
||||||
createFoldersAndNotes().then(() => {
|
|
||||||
return synchronizer().start();
|
|
||||||
}
|
|
||||||
}).then(() => {
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// // Note: set 1 matches set 1 of createRemoteItems()
|
||||||
|
// function createLocalItems(id, updatedTime, lastSyncTime) {
|
||||||
|
// let output = [];
|
||||||
|
// if (id === 1) {
|
||||||
|
// output.push({ path: 'test', isDir: true, updatedTime: updatedTime, lastSyncTime: lastSyncTime });
|
||||||
|
// output.push({ path: 'test/un', updatedTime: updatedTime, lastSyncTime: lastSyncTime });
|
||||||
|
// } else {
|
||||||
|
// throw new Error('Invalid ID');
|
||||||
|
// }
|
||||||
|
// return output;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function createRemoteItems(id = 1, updatedTime = null) {
|
||||||
|
// if (!updatedTime) updatedTime = time.unix();
|
||||||
|
|
||||||
|
// if (id === 1) {
|
||||||
|
// return fileApi().format()
|
||||||
|
// .then(() => fileApi().mkdir('test'))
|
||||||
|
// .then(() => fileApi().put('test/un', 'abcd'))
|
||||||
|
// .then(() => fileApi().list('', true))
|
||||||
|
// .then((items) => {
|
||||||
|
// for (let i = 0; i < items.length; i++) {
|
||||||
|
// items[i].updatedTime = updatedTime;
|
||||||
|
// }
|
||||||
|
// return items;
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// throw new Error('Invalid ID');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// describe('Synchronizer syncActions', function() {
|
||||||
|
|
||||||
|
// beforeEach(function(done) {
|
||||||
|
// setupDatabaseAndSynchronizer(done);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it('should create remote items', function() {
|
||||||
|
// let localItems = createLocalItems(1, time.unix(), 0);
|
||||||
|
// let remoteItems = [];
|
||||||
|
|
||||||
|
// let actions = synchronizer().syncActions(localItems, remoteItems, []);
|
||||||
|
|
||||||
|
// expect(actions.length).toBe(2);
|
||||||
|
// for (let i = 0; i < actions.length; i++) {
|
||||||
|
// expect(actions[i].type).toBe('create');
|
||||||
|
// expect(actions[i].dest).toBe('remote');
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it('should update remote items', function(done) {
|
||||||
|
// createRemoteItems(1).then((remoteItems) => {
|
||||||
|
// let lastSyncTime = time.unix() + 1000;
|
||||||
|
// let localItems = createLocalItems(1, lastSyncTime + 1000, lastSyncTime);
|
||||||
|
// let actions = synchronizer().syncActions(localItems, remoteItems, []);
|
||||||
|
|
||||||
|
// expect(actions.length).toBe(2);
|
||||||
|
// for (let i = 0; i < actions.length; i++) {
|
||||||
|
// expect(actions[i].type).toBe('update');
|
||||||
|
// expect(actions[i].dest).toBe('remote');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// done();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it('should detect conflict', function(done) {
|
||||||
|
// // Simulate this scenario:
|
||||||
|
// // - Client 1 create items
|
||||||
|
// // - Client 1 sync
|
||||||
|
// // - Client 2 sync
|
||||||
|
// // - Client 2 change items
|
||||||
|
// // - Client 2 sync
|
||||||
|
// // - Client 1 change items
|
||||||
|
// // - Client 1 sync
|
||||||
|
// // => Conflict
|
||||||
|
|
||||||
|
// createRemoteItems(1).then((remoteItems) => {
|
||||||
|
// let localItems = createLocalItems(1, time.unix() + 1000, time.unix() - 1000);
|
||||||
|
// let actions = synchronizer().syncActions(localItems, remoteItems, []);
|
||||||
|
|
||||||
|
// expect(actions.length).toBe(2);
|
||||||
|
// for (let i = 0; i < actions.length; i++) {
|
||||||
|
// expect(actions[i].type).toBe('conflict');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// done();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
// it('should create local file', function(done) {
|
||||||
|
// createRemoteItems(1).then((remoteItems) => {
|
||||||
|
// let localItems = [];
|
||||||
|
// let actions = synchronizer().syncActions(localItems, remoteItems, []);
|
||||||
|
|
||||||
|
// expect(actions.length).toBe(2);
|
||||||
|
// for (let i = 0; i < actions.length; i++) {
|
||||||
|
// expect(actions[i].type).toBe('create');
|
||||||
|
// expect(actions[i].dest).toBe('local');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// done();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it('should delete remote files', function(done) {
|
||||||
|
// createRemoteItems(1).then((remoteItems) => {
|
||||||
|
// let localItems = createLocalItems(1, time.unix(), time.unix());
|
||||||
|
// let deletedItemPaths = [localItems[0].path, localItems[1].path];
|
||||||
|
// let actions = synchronizer().syncActions([], remoteItems, deletedItemPaths);
|
||||||
|
|
||||||
|
// expect(actions.length).toBe(2);
|
||||||
|
// for (let i = 0; i < actions.length; i++) {
|
||||||
|
// expect(actions[i].type).toBe('delete');
|
||||||
|
// expect(actions[i].dest).toBe('remote');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// done();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it('should delete local files', function(done) {
|
||||||
|
// let lastSyncTime = time.unix();
|
||||||
|
// createRemoteItems(1, lastSyncTime - 1000).then((remoteItems) => {
|
||||||
|
// let localItems = createLocalItems(1, lastSyncTime - 1000, lastSyncTime);
|
||||||
|
// let actions = synchronizer().syncActions(localItems, [], []);
|
||||||
|
|
||||||
|
// expect(actions.length).toBe(2);
|
||||||
|
// for (let i = 0; i < actions.length; i++) {
|
||||||
|
// expect(actions[i].type).toBe('delete');
|
||||||
|
// expect(actions[i].dest).toBe('local');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// done();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it('should update local files', function(done) {
|
||||||
|
// let lastSyncTime = time.unix();
|
||||||
|
// createRemoteItems(1, lastSyncTime + 1000).then((remoteItems) => {
|
||||||
|
// let localItems = createLocalItems(1, lastSyncTime - 1000, lastSyncTime);
|
||||||
|
// let actions = synchronizer().syncActions(localItems, remoteItems, []);
|
||||||
|
|
||||||
|
// expect(actions.length).toBe(2);
|
||||||
|
// for (let i = 0; i < actions.length; i++) {
|
||||||
|
// expect(actions[i].type).toBe('update');
|
||||||
|
// expect(actions[i].dest).toBe('local');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// done();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // describe('Synchronizer start', function() {
|
||||||
|
|
||||||
|
// // beforeEach(function(done) {
|
||||||
|
// // setupDatabaseAndSynchronizer(done);
|
||||||
|
// // });
|
||||||
|
|
||||||
|
// // it('should create remote items', function(done) {
|
||||||
|
// // createFoldersAndNotes().then(() => {
|
||||||
|
// // return synchronizer().start();
|
||||||
|
// // }
|
||||||
|
// // }).then(() => {
|
||||||
|
// // done();
|
||||||
|
// // });
|
||||||
|
|
||||||
|
// // });
|
||||||
|
|
||||||
|
@ -37,15 +37,14 @@ function setupDatabase(done) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupDatabaseAndSynchronizer(done) {
|
async function setupDatabaseAndSynchronizer() {
|
||||||
return setupDatabase().then(() => {
|
await setupDatabase();
|
||||||
|
|
||||||
if (!synchronizer_) {
|
if (!synchronizer_) {
|
||||||
let fileDriver = new FileApiDriverMemory();
|
let fileDriver = new FileApiDriverMemory();
|
||||||
fileApi_ = new FileApi('/root', fileDriver);
|
fileApi_ = new FileApi('/root', fileDriver);
|
||||||
synchronizer_ = new Synchronizer(db(), fileApi);
|
synchronizer_ = new Synchronizer(db(), fileApi_);
|
||||||
}
|
}
|
||||||
done();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function db() {
|
function db() {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Log } from 'src/log.js';
|
import { Log } from 'src/log.js';
|
||||||
import { Database } from 'src/database.js';
|
import { Database } from 'src/database.js';
|
||||||
import { uuid } from 'src/uuid.js';
|
import { uuid } from 'src/uuid.js';
|
||||||
|
import { time } from 'src/time-utils.js';
|
||||||
|
|
||||||
class BaseModel {
|
class BaseModel {
|
||||||
|
|
||||||
@ -99,6 +100,7 @@ class BaseModel {
|
|||||||
}
|
}
|
||||||
if (!('trackChanges' in options)) options.trackChanges = true;
|
if (!('trackChanges' in options)) options.trackChanges = true;
|
||||||
if (!('isNew' in options)) options.isNew = 'auto';
|
if (!('isNew' in options)) options.isNew = 'auto';
|
||||||
|
if (!('autoTimestamp' in options)) options.autoTimestamp = true;
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,6 +139,7 @@ class BaseModel {
|
|||||||
static diffObjects(oldModel, newModel) {
|
static diffObjects(oldModel, newModel) {
|
||||||
let output = {};
|
let output = {};
|
||||||
for (let n in newModel) {
|
for (let n in newModel) {
|
||||||
|
if (n == 'type_') continue;
|
||||||
if (!newModel.hasOwnProperty(n)) continue;
|
if (!newModel.hasOwnProperty(n)) continue;
|
||||||
if (!(n in oldModel) || newModel[n] !== oldModel[n]) {
|
if (!(n in oldModel) || newModel[n] !== oldModel[n]) {
|
||||||
output[n] = newModel[n];
|
output[n] = newModel[n];
|
||||||
@ -145,9 +148,7 @@ class BaseModel {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
static saveQuery(o, isNew = 'auto') {
|
static saveQuery(o, options) {
|
||||||
if (isNew == 'auto') isNew = !o.id;
|
|
||||||
|
|
||||||
let temp = {}
|
let temp = {}
|
||||||
let fieldNames = this.fieldNames();
|
let fieldNames = this.fieldNames();
|
||||||
for (let i = 0; i < fieldNames.length; i++) {
|
for (let i = 0; i < fieldNames.length; i++) {
|
||||||
@ -156,22 +157,21 @@ class BaseModel {
|
|||||||
}
|
}
|
||||||
o = temp;
|
o = temp;
|
||||||
|
|
||||||
let query = '';
|
let query = {};
|
||||||
let itemId = o.id;
|
let itemId = o.id;
|
||||||
|
|
||||||
if (!o.updated_time && this.hasField('updated_time')) {
|
if (options.autoTimestamp && this.hasField('updated_time')) {
|
||||||
o.updated_time = Math.round((new Date()).getTime() / 1000);
|
o.updated_time = time.unix();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNew) {
|
if (options.isNew) {
|
||||||
if (this.useUuid() && !o.id) {
|
if (this.useUuid() && !o.id) {
|
||||||
//o = Object.assign({}, o);
|
|
||||||
itemId = uuid.create();
|
itemId = uuid.create();
|
||||||
o.id = itemId;
|
o.id = itemId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!o.created_time && this.hasField('created_time')) {
|
if (!o.created_time && this.hasField('created_time')) {
|
||||||
o.created_time = Math.round((new Date()).getTime() / 1000);
|
o.created_time = time.unix();
|
||||||
}
|
}
|
||||||
|
|
||||||
query = Database.insertQuery(this.tableName(), o);
|
query = Database.insertQuery(this.tableName(), o);
|
||||||
@ -192,10 +192,10 @@ class BaseModel {
|
|||||||
static save(o, options = null) {
|
static save(o, options = null) {
|
||||||
options = this.modOptions(options);
|
options = this.modOptions(options);
|
||||||
|
|
||||||
let isNew = options.isNew == 'auto' ? !o.id : options.isNew;
|
options.isNew = options.isNew == 'auto' ? !o.id : options.isNew;
|
||||||
|
|
||||||
let queries = [];
|
let queries = [];
|
||||||
let saveQuery = this.saveQuery(o, isNew);
|
let saveQuery = this.saveQuery(o, options);
|
||||||
let itemId = saveQuery.id;
|
let itemId = saveQuery.id;
|
||||||
|
|
||||||
queries.push(saveQuery);
|
queries.push(saveQuery);
|
||||||
|
@ -253,6 +253,8 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static insertQuery(tableName, data) {
|
static insertQuery(tableName, data) {
|
||||||
|
if (!data || !Object.keys(data).length) throw new Error('Data is empty');
|
||||||
|
|
||||||
let keySql= '';
|
let keySql= '';
|
||||||
let valueSql = '';
|
let valueSql = '';
|
||||||
let params = [];
|
let params = [];
|
||||||
@ -271,6 +273,8 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static updateQuery(tableName, data, where) {
|
static updateQuery(tableName, data, where) {
|
||||||
|
if (!data || !Object.keys(data).length) throw new Error('Data is empty');
|
||||||
|
|
||||||
let sql = '';
|
let sql = '';
|
||||||
let params = [];
|
let params = [];
|
||||||
for (let key in data) {
|
for (let key in data) {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { time } from 'src/time-utils.js';
|
||||||
|
|
||||||
class FileApiDriverMemory {
|
class FileApiDriverMemory {
|
||||||
|
|
||||||
constructor(baseDir) {
|
constructor(baseDir) {
|
||||||
@ -21,23 +23,23 @@ class FileApiDriverMemory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newItem(path, isDir = false) {
|
newItem(path, isDir = false) {
|
||||||
|
let now = time.unix();
|
||||||
return {
|
return {
|
||||||
path: path,
|
path: path,
|
||||||
isDir: isDir,
|
isDir: isDir,
|
||||||
updatedTime: this.currentTimestamp(),
|
updatedTime: now,
|
||||||
createdTime: this.currentTimestamp(),
|
createdTime: now,
|
||||||
content: '',
|
content: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
stat(path) {
|
stat(path) {
|
||||||
let item = this.itemIndexByPath(path);
|
let item = this.itemByPath(path);
|
||||||
if (!item) return Promise.reject(new Error('File not found: ' + path));
|
return Promise.resolve(item ? Object.assign({}, item) : null);
|
||||||
return Promise.resolve(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimestamp(path, timestamp) {
|
setTimestamp(path, timestamp) {
|
||||||
let item = this.itemIndexByPath(path);
|
let item = this.itemByPath(path);
|
||||||
if (!item) return Promise.reject(new Error('File not found: ' + path));
|
if (!item) return Promise.reject(new Error('File not found: ' + path));
|
||||||
item.updatedTime = timestamp;
|
item.updatedTime = timestamp;
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@ -53,7 +55,6 @@ class FileApiDriverMemory {
|
|||||||
let s = item.path.substr(path.length + 1);
|
let s = item.path.substr(path.length + 1);
|
||||||
if (s.split('/').length === 1) {
|
if (s.split('/').length === 1) {
|
||||||
let it = Object.assign({}, item);
|
let it = Object.assign({}, item);
|
||||||
it.path = it.path.substr(path.length + 1);
|
|
||||||
output.push(it);
|
output.push(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,7 +65,7 @@ class FileApiDriverMemory {
|
|||||||
|
|
||||||
get(path) {
|
get(path) {
|
||||||
let item = this.itemByPath(path);
|
let item = this.itemByPath(path);
|
||||||
if (!item) return Promise.reject(new Error('File not found: ' + path));
|
if (!item) return Promise.resolve(null);
|
||||||
if (item.isDir) return Promise.reject(new Error(path + ' is a directory, not a file'));
|
if (item.isDir) return Promise.reject(new Error(path + ' is a directory, not a file'));
|
||||||
return Promise.resolve(item.content);
|
return Promise.resolve(item.content);
|
||||||
}
|
}
|
||||||
@ -84,6 +85,7 @@ class FileApiDriverMemory {
|
|||||||
this.items_.push(item);
|
this.items_.push(item);
|
||||||
} else {
|
} else {
|
||||||
this.items_[index].content = content;
|
this.items_[index].content = content;
|
||||||
|
this.items_[index].updatedTime = time.unix();
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@ -37,34 +37,38 @@ class FileApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
list(path = '', recursive = false, context = null) {
|
list() {
|
||||||
let fullPath = this.fullPath_(path);
|
return this.driver_.list(this.baseDir_).then((items) => {
|
||||||
return this.driver_.list(fullPath).then((items) => {
|
return this.scopeItemsToBaseDir_(items);
|
||||||
items = this.scopeItemsToBaseDir_(items);
|
});
|
||||||
if (recursive) {
|
// let fullPath = this.fullPath_(path);
|
||||||
let chain = [];
|
// return this.driver_.list(fullPath).then((items) => {
|
||||||
for (let i = 0; i < items.length; i++) {
|
// return items;
|
||||||
let item = items[i];
|
// // items = this.scopeItemsToBaseDir_(items);
|
||||||
if (!item.isDir) continue;
|
// // if (recursive) {
|
||||||
|
// // let chain = [];
|
||||||
|
// // for (let i = 0; i < items.length; i++) {
|
||||||
|
// // let item = items[i];
|
||||||
|
// // if (!item.isDir) continue;
|
||||||
|
|
||||||
chain.push(() => {
|
// // chain.push(() => {
|
||||||
return this.list(item.path, true).then((children) => {
|
// // return this.list(item.path, true).then((children) => {
|
||||||
for (let j = 0; j < children.length; j++) {
|
// // for (let j = 0; j < children.length; j++) {
|
||||||
let md = children[j];
|
// // let md = children[j];
|
||||||
md.path = item.path + '/' + md.path;
|
// // md.path = item.path + '/' + md.path;
|
||||||
items.push(md);
|
// // items.push(md);
|
||||||
}
|
// // }
|
||||||
});
|
// // });
|
||||||
});
|
// // });
|
||||||
}
|
// // }
|
||||||
|
|
||||||
return promiseChain(chain).then(() => {
|
// // return promiseChain(chain).then(() => {
|
||||||
return items;
|
// // return items;
|
||||||
});
|
// // });
|
||||||
} else {
|
// // } else {
|
||||||
return items;
|
// // return items;
|
||||||
}
|
// // }
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimestamp(path, timestamp) {
|
setTimestamp(path, timestamp) {
|
||||||
@ -77,7 +81,7 @@ class FileApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stat(path) {
|
stat(path) {
|
||||||
console.info('stat ' + path);
|
//console.info('stat ' + path);
|
||||||
return this.driver_.stat(this.fullPath_(path)).then((output) => {
|
return this.driver_.stat(this.fullPath_(path)).then((output) => {
|
||||||
if (!output) return output;
|
if (!output) return output;
|
||||||
output.path = path;
|
output.path = path;
|
||||||
@ -86,12 +90,12 @@ class FileApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(path) {
|
get(path) {
|
||||||
console.info('get ' + path);
|
//console.info('get ' + path);
|
||||||
return this.driver_.get(this.fullPath_(path));
|
return this.driver_.get(this.fullPath_(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
put(path, content) {
|
put(path, content) {
|
||||||
console.info('put ' + path);
|
//console.info('put ' + path);
|
||||||
return this.driver_.put(this.fullPath_(path), content);
|
return this.driver_.put(this.fullPath_(path), content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,9 +74,20 @@ class Folder extends BaseItem {
|
|||||||
//return this.db().selectOne('SELECT * FROM notes WHERE `parent_id` = ? AND `' + field + '` = ?', [folderId, value]);
|
//return this.db().selectOne('SELECT * FROM notes WHERE `parent_id` = ? AND `' + field + '` = ?', [folderId, value]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static all() {
|
static async all(includeNotes = false) {
|
||||||
return this.modelSelectAll('SELECT * FROM folders');
|
let folders = await this.modelSelectAll('SELECT * FROM folders');
|
||||||
// return this.db().selectAll('SELECT * FROM folders');
|
if (!includeNotes) return folders;
|
||||||
|
|
||||||
|
let output = [];
|
||||||
|
for (let i = 0; i < folders.length; i++) {
|
||||||
|
let folder = folders[i];
|
||||||
|
let notes = await Note.all(folder.id);
|
||||||
|
output.push(folder);
|
||||||
|
output = output.concat(notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static save(o, options = null) {
|
static save(o, options = null) {
|
||||||
|
@ -69,6 +69,10 @@ class Note extends BaseItem {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static all(parentId) {
|
||||||
|
return this.modelSelectAll('SELECT * FROM notes WHERE parent_id = ?', [parentId]);
|
||||||
|
}
|
||||||
|
|
||||||
static save(o, options = null) {
|
static save(o, options = null) {
|
||||||
return super.save(o, options).then((result) => {
|
return super.save(o, options).then((result) => {
|
||||||
// 'result' could be a partial one at this point (if, for example, only one property of it was saved)
|
// 'result' could be a partial one at this point (if, for example, only one property of it was saved)
|
||||||
|
@ -11,7 +11,7 @@ import { Registry } from 'src/registry.js';
|
|||||||
|
|
||||||
class NoteFolderService extends BaseService {
|
class NoteFolderService extends BaseService {
|
||||||
|
|
||||||
static save(type, item, oldItem) {
|
static save(type, item, oldItem, options = null) {
|
||||||
let diff = null;
|
let diff = null;
|
||||||
if (oldItem) {
|
if (oldItem) {
|
||||||
diff = BaseModel.diffObjects(oldItem, item);
|
diff = BaseModel.diffObjects(oldItem, item);
|
||||||
@ -32,7 +32,9 @@ class NoteFolderService extends BaseService {
|
|||||||
toSave.id = item.id;
|
toSave.id = item.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ItemClass.save(toSave).then((savedItem) => {
|
console.info(toSave);
|
||||||
|
|
||||||
|
return ItemClass.save(toSave, options).then((savedItem) => {
|
||||||
output = Object.assign(item, savedItem);
|
output = Object.assign(item, savedItem);
|
||||||
if (isNew && type == 'note') return Note.updateGeolocation(output.id);
|
if (isNew && type == 'note') return Note.updateGeolocation(output.id);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
@ -161,22 +161,21 @@ class Synchronizer {
|
|||||||
|
|
||||||
if (!remote) {
|
if (!remote) {
|
||||||
if (local.syncTime) {
|
if (local.syncTime) {
|
||||||
// The item has been synced previously and now is no longer in the dest
|
|
||||||
// which means it has been deleted.
|
|
||||||
action.type = 'delete';
|
action.type = 'delete';
|
||||||
action.dest = 'local';
|
action.dest = 'local';
|
||||||
|
action.reason = 'Local item has been synced to remote previously, but remote no longer exist, which means it has been deleted';
|
||||||
} else {
|
} else {
|
||||||
// The item has never been synced and is not present in the dest
|
|
||||||
// which means it is new
|
|
||||||
action.type = 'create';
|
action.type = 'create';
|
||||||
action.dest = 'remote';
|
action.dest = 'remote';
|
||||||
|
action.reason = 'Local item has never been synced to remote, and remote does not exists, which means it is new';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.itemIsStrictlyOlderThan(local, local.syncTime)) continue;
|
if (this.itemIsStrictlyOlderThan(local, local.syncTime)) continue;
|
||||||
|
|
||||||
if (this.itemIsStrictlyOlderThan(remote, local.syncTime)) {
|
if (this.itemIsStrictlyOlderThan(remote, local.updatedTime)) {
|
||||||
action.type = 'update';
|
action.type = 'update';
|
||||||
action.dest = 'remote';
|
action.dest = 'remote';
|
||||||
|
action.reason = sprintf('Remote (%s) was modified after last sync of local (%s).', moment.unix(remote.updatedTime).toISOString(), moment.unix(local.syncTime).toISOString(),);
|
||||||
} else if (this.itemIsStrictlyNewerThan(remote, local.syncTime)) {
|
} else if (this.itemIsStrictlyNewerThan(remote, local.syncTime)) {
|
||||||
action.type = 'conflict';
|
action.type = 'conflict';
|
||||||
action.reason = sprintf('Both remote (%s) and local items (%s) were modified after the last sync (%s).',
|
action.reason = sprintf('Both remote (%s) and local items (%s) were modified after the last sync (%s).',
|
||||||
@ -186,10 +185,6 @@ class Synchronizer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (local.type == 'folder') {
|
if (local.type == 'folder') {
|
||||||
// For folders, currently we don't completely handle conflicts, we just
|
|
||||||
// we just update the local dir (.folder metadata file) with the remote
|
|
||||||
// version. It means the local version is lost but shouldn't be a big deal
|
|
||||||
// and should be rare (at worst, the folder name needs to renamed).
|
|
||||||
action.solution = [
|
action.solution = [
|
||||||
{ type: 'update', dest: 'local' },
|
{ type: 'update', dest: 'local' },
|
||||||
];
|
];
|
||||||
@ -230,10 +225,20 @@ class Synchronizer {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.itemIsStrictlyOlderThan(remote, local.syncTime)) continue; // Already have this version
|
if (this.itemIsStrictlyOlderThan(remote, local.syncTime)) continue; // Already have this version
|
||||||
|
|
||||||
// Note: no conflict is possible here since if the local item has been
|
// Note: no conflict is possible here since if the local item has been
|
||||||
// modified since the last sync, it's been processed in the previous loop.
|
// modified since the last sync, it's been processed in the previous loop.
|
||||||
|
// So throw an exception is this normally impossible condition happens anyway.
|
||||||
|
// It's handled at condition this.itemIsStrictlyNewerThan(remote, local.syncTime) in above loop
|
||||||
|
if (this.itemIsStrictlyNewerThan(remote, local.syncTime)) throw new Error('Remote item cannot be newer than last sync time.');
|
||||||
|
|
||||||
|
if (this.itemIsStrictlyNewerThan(remote, local.updatedTime)) {
|
||||||
action.type = 'update';
|
action.type = 'update';
|
||||||
action.dest = 'local';
|
action.dest = 'local';
|
||||||
|
action.reason = sprintf('Remote (%s) was modified after last sync of local (%s).', moment.unix(remote.updatedTime).toISOString(), moment.unix(local.syncTime).toISOString(),);;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output.push(action);
|
output.push(action);
|
||||||
@ -268,7 +273,7 @@ class Synchronizer {
|
|||||||
|
|
||||||
if (!action) return Promise.resolve();
|
if (!action) return Promise.resolve();
|
||||||
|
|
||||||
console.info('Sync action: ' + action.type + ' ' + action.dest);
|
console.info('Sync action: ' + action.type + ' ' + action.dest + ': ' + action.reason);
|
||||||
|
|
||||||
if (action.type == 'conflict') {
|
if (action.type == 'conflict') {
|
||||||
console.info(action);
|
console.info(action);
|
||||||
@ -293,10 +298,11 @@ class Synchronizer {
|
|||||||
} else {
|
} else {
|
||||||
let dbItem = syncItem.remoteItem.content;
|
let dbItem = syncItem.remoteItem.content;
|
||||||
dbItem.sync_time = time.unix();
|
dbItem.sync_time = time.unix();
|
||||||
|
dbItem.updated_time = dbItem.sync_time;
|
||||||
if (syncItem.type == 'folder') {
|
if (syncItem.type == 'folder') {
|
||||||
return Folder.save(dbItem, { isNew: true });
|
return Folder.save(dbItem, { isNew: true, autoTimestamp: false });
|
||||||
} else {
|
} else {
|
||||||
return Note.save(dbItem, { isNew: true });
|
return Note.save(dbItem, { isNew: true, autoTimestamp: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -317,7 +323,8 @@ class Synchronizer {
|
|||||||
} else {
|
} else {
|
||||||
let dbItem = syncItem.remoteItem.content;
|
let dbItem = syncItem.remoteItem.content;
|
||||||
dbItem.sync_time = time.unix();
|
dbItem.sync_time = time.unix();
|
||||||
return NoteFolderService.save(syncItem.type, dbItem, action.local.dbItem);
|
dbItem.updated_time = dbItem.sync_time;
|
||||||
|
return NoteFolderService.save(syncItem.type, dbItem, action.local.dbItem, { autoTimestamp: false });
|
||||||
// let dbItem = syncItem.remoteItem.content;
|
// let dbItem = syncItem.remoteItem.content;
|
||||||
// dbItem.sync_time = time.unix();
|
// dbItem.sync_time = time.unix();
|
||||||
// if (syncItem.type == 'folder') {
|
// if (syncItem.type == 'folder') {
|
||||||
@ -349,6 +356,7 @@ class Synchronizer {
|
|||||||
|
|
||||||
async processRemoteItem(remoteItem) {
|
async processRemoteItem(remoteItem) {
|
||||||
let content = await this.api().get(remoteItem.path);
|
let content = await this.api().get(remoteItem.path);
|
||||||
|
if (!content) throw new Error('Cannot get content for: ' + remoteItem.path);
|
||||||
remoteItem.content = Note.fromFriendlyString(content);
|
remoteItem.content = Note.fromFriendlyString(content);
|
||||||
let remoteSyncItem = this.remoteItemToSyncItem(remoteItem);
|
let remoteSyncItem = this.remoteItemToSyncItem(remoteItem);
|
||||||
|
|
||||||
@ -362,6 +370,7 @@ class Synchronizer {
|
|||||||
async processState_uploadChanges() {
|
async processState_uploadChanges() {
|
||||||
while (true) {
|
while (true) {
|
||||||
let result = await NoteFolderService.itemsThatNeedSync(50);
|
let result = await NoteFolderService.itemsThatNeedSync(50);
|
||||||
|
console.info('Items that need sync: ' + result.items.length);
|
||||||
for (let i = 0; i < result.items.length; i++) {
|
for (let i = 0; i < result.items.length; i++) {
|
||||||
let item = result.items[i];
|
let item = result.items[i];
|
||||||
await this.processLocalItem(item);
|
await this.processLocalItem(item);
|
||||||
@ -370,6 +379,8 @@ class Synchronizer {
|
|||||||
if (!result.hasMore) break;
|
if (!result.hasMore) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//console.info('DOWNLOAD CHANGE DISABLED'); return Promise.resolve();
|
||||||
|
|
||||||
return this.processState('downloadChanges');
|
return this.processState('downloadChanges');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,7 +407,10 @@ class Synchronizer {
|
|||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return this.processState('uploadChanges');
|
return this.processState('uploadChanges').catch((error) => {
|
||||||
|
console.info('Synchronizer error:', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user