2017-06-15 22:46:53 +01:00
|
|
|
require('babel-plugin-transform-runtime');
|
|
|
|
|
2017-06-15 19:18:48 +01:00
|
|
|
import { BaseItem } from 'src/models/base-item.js';
|
2017-06-15 23:12:00 +01:00
|
|
|
import { sprintf } from 'sprintf-js';
|
2017-06-18 23:06:10 +01:00
|
|
|
import { time } from 'src/time-utils.js';
|
|
|
|
import { Log } from 'src/log.js'
|
2017-05-18 19:58:01 +00:00
|
|
|
|
|
|
|
class Synchronizer {
|
|
|
|
|
2017-05-19 19:12:09 +00:00
|
|
|
constructor(db, api) {
|
|
|
|
this.db_ = db;
|
|
|
|
this.api_ = api;
|
2017-05-18 19:58:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
db() {
|
2017-05-19 19:12:09 +00:00
|
|
|
return this.db_;
|
2017-05-18 19:58:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
api() {
|
2017-05-19 19:12:09 +00:00
|
|
|
return this.api_;
|
2017-05-18 19:58:01 +00:00
|
|
|
}
|
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
async start() {
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// First, find all the items that have been changed since the
|
|
|
|
// last sync and apply the changes to remote.
|
|
|
|
// ------------------------------------------------------------------------
|
2017-06-15 19:18:48 +01:00
|
|
|
|
2017-06-13 23:39:45 +01:00
|
|
|
let donePaths = [];
|
2017-06-18 23:06:10 +01:00
|
|
|
while (true) {
|
|
|
|
let result = await BaseItem.itemsThatNeedSync();
|
|
|
|
let locals = result.items;
|
2017-06-13 20:58:17 +00:00
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
for (let i = 0; i < locals.length; i++) {
|
|
|
|
let local = locals[i];
|
|
|
|
let ItemClass = BaseItem.itemClass(local);
|
|
|
|
let path = BaseItem.systemPath(local);
|
2017-06-15 19:18:48 +01:00
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
// Safety check to avoid infinite loops:
|
|
|
|
if (donePaths.indexOf(path) > 0) throw new Error(sprintf('Processing a path that has already been done: %s. sync_time was not updated?', path));
|
2017-06-13 23:39:45 +01:00
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
let remote = await this.api().stat(path);
|
|
|
|
let content = ItemClass.serialize(local);
|
|
|
|
let action = null;
|
2017-06-13 23:39:45 +01:00
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
if (!remote) {
|
|
|
|
action = 'createRemote';
|
2017-06-13 23:39:45 +01:00
|
|
|
} else {
|
2017-06-18 23:06:10 +01:00
|
|
|
if (remote.updated_time > local.updated_time) {
|
|
|
|
action = 'conflict';
|
2017-06-13 23:39:45 +01:00
|
|
|
} else {
|
2017-06-18 23:06:10 +01:00
|
|
|
action = 'updateRemote';
|
2017-06-13 23:39:45 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
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);
|
2017-06-13 20:58:17 +00:00
|
|
|
}
|
2017-06-18 00:49:52 +01:00
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
let newLocal = { id: local.id, sync_time: time.unixMs(), type_: local.type_ };
|
|
|
|
await ItemClass.save(newLocal, { autoTimestamp: false });
|
2017-06-03 17:20:17 +01:00
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
donePaths.push(path);
|
|
|
|
}
|
2017-06-03 17:20:17 +01:00
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
if (!result.hasMore) break;
|
2017-05-18 19:58:01 +00:00
|
|
|
}
|
2017-06-15 23:12:00 +01:00
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// Then, loop through all the remote items, find those that
|
|
|
|
// have been updated, and apply the changes to local.
|
|
|
|
// ------------------------------------------------------------------------
|
2017-06-15 00:14:15 +01:00
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
// At this point all the local items that have changed have been pushed to remote
|
|
|
|
// or handled as conflicts, so no conflict is possible after this.
|
2017-06-15 00:14:15 +01:00
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
let remotes = await this.api().list();
|
|
|
|
for (let i = 0; i < remotes.length; i++) {
|
|
|
|
let remote = remotes[i];
|
|
|
|
let path = remote.path;
|
|
|
|
if (donePaths.indexOf(path) > 0) continue;
|
2017-06-15 19:18:48 +01:00
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
let action = null;
|
|
|
|
let local = await BaseItem.loadItemByPath(path);
|
|
|
|
if (!local) {
|
|
|
|
action = 'createLocal';
|
|
|
|
} else {
|
|
|
|
if (remote.updated_time > local.updated_time) {
|
|
|
|
action = 'updateLocal';
|
2017-06-15 19:18:48 +01:00
|
|
|
}
|
|
|
|
}
|
2017-06-14 20:59:46 +01:00
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
if (!action) continue;
|
2017-06-15 00:14:15 +01:00
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
if (action == 'createLocal' || action == 'updateLocal') {
|
|
|
|
let content = await this.api().get(path);
|
|
|
|
if (!content) {
|
|
|
|
Log.warn('Remote item has been deleted between now and the list() call? In that case it will handled during the next sync: ' + path);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
content = BaseItem.unserialize(content);
|
|
|
|
let ItemClass = BaseItem.itemClass(content);
|
2017-06-15 19:18:48 +01:00
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
content.sync_time = time.unixMs();
|
|
|
|
let options = { autoTimestamp: false };
|
|
|
|
if (action == 'createLocal') options.isNew = true;
|
|
|
|
await ItemClass.save(content, options);
|
2017-06-15 19:18:48 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-18 19:58:01 +00:00
|
|
|
|
2017-06-18 23:06:10 +01:00
|
|
|
return Promise.resolve();
|
2017-05-18 19:58:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
export { Synchronizer };
|