2017-05-18 21:58:01 +02:00
|
|
|
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';
|
2017-05-20 00:16:50 +02:00
|
|
|
import { Note } from 'src/models/note.js';
|
|
|
|
import { BaseModel } from 'src/base-model.js';
|
2017-05-19 21:12:09 +02:00
|
|
|
import { promiseChain } from 'src/promise-chain.js';
|
2017-06-11 23:11:14 +02:00
|
|
|
import moment from 'moment';
|
|
|
|
|
|
|
|
const fs = require('fs');
|
|
|
|
const path = require('path');
|
2017-05-18 21:58:01 +02:00
|
|
|
|
|
|
|
class Synchronizer {
|
|
|
|
|
2017-05-19 21:12:09 +02:00
|
|
|
constructor(db, api) {
|
2017-05-18 21:58:01 +02:00
|
|
|
this.state_ = 'idle';
|
2017-05-19 21:12:09 +02:00
|
|
|
this.db_ = db;
|
|
|
|
this.api_ = api;
|
2017-05-18 21:58:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
state() {
|
|
|
|
return this.state_;
|
|
|
|
}
|
|
|
|
|
|
|
|
db() {
|
2017-05-19 21:12:09 +02:00
|
|
|
return this.db_;
|
2017-05-18 21:58:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
api() {
|
2017-05-19 21:12:09 +02:00
|
|
|
return this.api_;
|
2017-05-18 21:58:01 +02:00
|
|
|
}
|
|
|
|
|
2017-06-11 23:11:14 +02:00
|
|
|
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 });
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-12 23:56:27 +02:00
|
|
|
remoteFileByPath(remoteFiles, path) {
|
2017-06-11 23:11:14 +02:00
|
|
|
for (let i = 0; i < remoteFiles.length; i++) {
|
2017-06-12 23:56:27 +02:00
|
|
|
if (remoteFiles[i].path == path) return remoteFiles[i];
|
2017-06-11 23:11:14 +02:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
conflictDir(remoteFiles) {
|
2017-06-12 23:56:27 +02:00
|
|
|
let d = this.remoteFileByPath('Conflicts');
|
2017-06-11 23:11:14 +02:00
|
|
|
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) => {
|
2017-06-12 23:56:27 +02:00
|
|
|
let p = path.basename(item.path).split('.');
|
2017-06-11 23:11:14 +02:00
|
|
|
let pos = item.isDir ? p.length - 1 : p.length - 2;
|
|
|
|
p.splice(pos, 0, moment().format('YYYYMMDDThhmmss'));
|
2017-06-12 23:56:27 +02:00
|
|
|
let newPath = p.join('.');
|
|
|
|
return this.api().move(item.path, conflictDirPath + '/' + newPath);
|
2017-06-11 23:11:14 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-06-13 22:58:17 +02:00
|
|
|
// isNewerThan(date1, date2) {
|
|
|
|
// return date1 > date2;
|
|
|
|
// }
|
|
|
|
|
|
|
|
itemByPath(items, path) {
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
|
if (items[i].path == path) return items[i];
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
syncAction(actionType, where, item, isConflict) {
|
|
|
|
return {
|
|
|
|
type: actionType,
|
|
|
|
where: where,
|
|
|
|
item: item,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-06-14 00:39:45 +02:00
|
|
|
itemIsSameDate(item, date) {
|
|
|
|
return Math.abs(item.updatedTime - date) <= 1;
|
|
|
|
}
|
|
|
|
|
2017-06-13 22:58:17 +02:00
|
|
|
itemIsNewerThan(item, date) {
|
2017-06-14 00:39:45 +02:00
|
|
|
if (this.itemIsSameDate(item, date)) return false;
|
2017-06-13 22:58:17 +02:00
|
|
|
return item.updatedTime > date;
|
|
|
|
}
|
|
|
|
|
|
|
|
itemIsOlderThan(item, date) {
|
2017-06-14 00:39:45 +02:00
|
|
|
if (this.itemIsSameDate(item, date)) return false;
|
|
|
|
return item.updatedTime < date;
|
2017-06-13 22:58:17 +02:00
|
|
|
}
|
|
|
|
|
2017-06-14 00:39:45 +02:00
|
|
|
// Assumption: it's not possible to, for example, have a directory one the dest
|
|
|
|
// and a file with the same name on the source. It's not possible because the
|
|
|
|
// file and directory names are UUID so should be unique.
|
|
|
|
syncActions(localItems, remoteItems, deletedLocalPaths) {
|
2017-06-13 22:58:17 +02:00
|
|
|
let output = [];
|
2017-06-14 00:39:45 +02:00
|
|
|
let donePaths = [];
|
2017-06-13 22:58:17 +02:00
|
|
|
|
|
|
|
for (let i = 0; i < localItems.length; i++) {
|
2017-06-14 00:39:45 +02:00
|
|
|
let local = localItems[i];
|
|
|
|
let remote = this.itemByPath(remoteItems, local.path);
|
|
|
|
|
2017-06-13 22:58:17 +02:00
|
|
|
let action = {
|
2017-06-14 00:39:45 +02:00
|
|
|
local: local,
|
|
|
|
remote: remote,
|
2017-06-13 22:58:17 +02:00
|
|
|
};
|
2017-06-14 00:39:45 +02:00
|
|
|
|
|
|
|
if (!remote) {
|
|
|
|
if (local.lastSyncTime) {
|
|
|
|
// The item has been synced previously and now is no longer in the dest
|
|
|
|
// which means it has been deleted.
|
|
|
|
action.type = 'delete';
|
|
|
|
action.dest = 'local';
|
|
|
|
} else {
|
|
|
|
// The item has never been synced and is not present in the dest
|
|
|
|
// which means it is new
|
|
|
|
action.type = 'create';
|
|
|
|
action.dest = 'remote';
|
|
|
|
}
|
2017-06-13 22:58:17 +02:00
|
|
|
} else {
|
2017-06-14 00:39:45 +02:00
|
|
|
if (this.itemIsOlderThan(local, local.lastSyncTime)) continue;
|
|
|
|
|
|
|
|
if (this.itemIsOlderThan(remote, local.lastSyncTime)) {
|
2017-06-13 22:58:17 +02:00
|
|
|
action.type = 'update';
|
2017-06-14 00:39:45 +02:00
|
|
|
action.dest = 'remote';
|
|
|
|
} else {
|
|
|
|
action.type = 'conflict';
|
|
|
|
if (local.isDir) {
|
|
|
|
// 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 = [
|
|
|
|
{ type: 'update', dest: 'local' },
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
action.solution = [
|
|
|
|
{ type: 'copy-to-remote-conflict-dir', dest: 'local' },
|
|
|
|
{ type: 'copy-to-local-conflict-dir', dest: 'local' },
|
|
|
|
{ type: 'update', dest: 'local' },
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
donePaths.push(local.path);
|
|
|
|
|
|
|
|
output.push(action);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < remoteItems.length; i++) {
|
|
|
|
let remote = remoteItems[i];
|
|
|
|
if (donePaths.indexOf(remote.path) >= 0) continue; // Already handled in the previous loop
|
|
|
|
let local = this.itemByPath(localItems, remote.path);
|
|
|
|
|
|
|
|
let action = {
|
|
|
|
local: local,
|
|
|
|
remote: remote,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!local) {
|
|
|
|
if (deletedLocalPaths.indexOf(remote.path) >= 0) {
|
|
|
|
action.type = 'delete';
|
|
|
|
action.dest = 'remote';
|
2017-06-13 22:58:17 +02:00
|
|
|
} else {
|
2017-06-14 00:39:45 +02:00
|
|
|
action.type = 'create';
|
|
|
|
action.dest = 'local';
|
2017-06-13 22:58:17 +02:00
|
|
|
}
|
2017-06-14 00:39:45 +02:00
|
|
|
} else {
|
|
|
|
if (this.itemIsOlderThan(remote, local.lastSyncTime)) continue; // Already have this version
|
|
|
|
// 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.
|
|
|
|
action.type = 'update';
|
|
|
|
action.dest = 'local';
|
2017-06-13 22:58:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
output.push(action);
|
|
|
|
}
|
|
|
|
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
2017-06-03 18:20:17 +02:00
|
|
|
processState_uploadChanges() {
|
2017-06-11 23:11:14 +02:00
|
|
|
let remoteFiles = [];
|
|
|
|
let processedChangeIds = [];
|
|
|
|
return this.api().list('', true).then((items) => {
|
|
|
|
remoteFiles = items;
|
|
|
|
return Change.all();
|
|
|
|
}).then((changes) => {
|
2017-06-03 18:20:17 +02:00
|
|
|
let mergedChanges = Change.mergeChanges(changes);
|
|
|
|
let chain = [];
|
2017-06-12 23:56:27 +02:00
|
|
|
const lastSyncTime = Setting.value('sync.lastUpdateTime');
|
2017-06-03 18:20:17 +02:00
|
|
|
for (let i = 0; i < mergedChanges.length; i++) {
|
|
|
|
let c = mergedChanges[i];
|
|
|
|
chain.push(() => {
|
|
|
|
let p = null;
|
2017-05-20 00:16:50 +02:00
|
|
|
|
|
|
|
let ItemClass = null;
|
2017-06-03 18:20:17 +02:00
|
|
|
if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) {
|
2017-05-20 00:16:50 +02:00
|
|
|
ItemClass = Folder;
|
2017-06-03 18:20:17 +02:00
|
|
|
} else if (c.item_type == BaseModel.ITEM_TYPE_NOTE) {
|
2017-05-20 00:16:50 +02:00
|
|
|
ItemClass = Note;
|
|
|
|
}
|
2017-05-19 21:12:09 +02:00
|
|
|
|
2017-06-03 18:20:17 +02:00
|
|
|
if (c.type == Change.TYPE_NOOP) {
|
|
|
|
p = Promise.resolve();
|
|
|
|
} else if (c.type == Change.TYPE_CREATE) {
|
2017-06-12 23:56:27 +02:00
|
|
|
p = this.loadParentAndItem(c).then((result) => {
|
|
|
|
let item = result.item;
|
|
|
|
let parent = result.parent;
|
|
|
|
if (!item) return; // Change refers to an object that doesn't exist (has probably been deleted directly in the database)
|
|
|
|
|
|
|
|
let path = ItemClass.systemPath(parent, item);
|
|
|
|
|
|
|
|
let remoteFile = this.remoteFileByPath(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).then(() => {
|
|
|
|
return this.api().put(Folder.systemMetadataPath(parent, item), Folder.toFriendlyString(item));
|
|
|
|
}).then(() => {
|
2017-06-13 22:12:08 +02:00
|
|
|
return this.api().setTimestamp(Folder.systemMetadataPath(parent, item), item.updated_time);
|
2017-06-12 23:56:27 +02:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return this.api().put(path, Note.toFriendlyString(item)).then(() => {
|
2017-06-13 22:12:08 +02:00
|
|
|
return this.api().setTimestamp(path, item.updated_time);
|
2017-06-12 23:56:27 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} else if (c.type == Change.TYPE_UPDATE) {
|
2017-06-11 23:11:14 +02:00
|
|
|
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);
|
|
|
|
|
2017-06-12 23:56:27 +02:00
|
|
|
let remoteFile = this.remoteFileByPath(remoteFiles, path);
|
2017-06-11 23:11:14 +02:00
|
|
|
let p = null;
|
2017-06-12 23:56:27 +02:00
|
|
|
if (remoteFile && remoteFile.updatedTime > lastSyncTime) {
|
|
|
|
console.info('CONFLICT:', lastSyncTime, remoteFile);
|
|
|
|
//console.info(moment.unix(remoteFile.updatedTime), moment.unix(result.item.updated_time));
|
2017-06-11 23:11:14 +02:00
|
|
|
p = this.moveConflict(remoteFile);
|
|
|
|
} else {
|
|
|
|
p = Promise.resolve();
|
|
|
|
}
|
|
|
|
|
2017-06-12 23:56:27 +02:00
|
|
|
console.info('Uploading change:', JSON.stringify(result.item));
|
|
|
|
|
2017-06-11 23:11:14 +02:00
|
|
|
return p.then(() => {
|
|
|
|
if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) {
|
2017-06-12 23:56:27 +02:00
|
|
|
return this.api().put(Folder.systemMetadataPath(result.parent, result.item), Folder.toFriendlyString(result.item));
|
2017-06-11 23:11:14 +02:00
|
|
|
} else {
|
|
|
|
return this.api().put(path, Note.toFriendlyString(result.item));
|
|
|
|
}
|
|
|
|
});
|
2017-05-20 00:16:50 +02:00
|
|
|
});
|
|
|
|
}
|
2017-05-19 21:32:49 +02:00
|
|
|
|
2017-06-11 23:11:14 +02:00
|
|
|
// TODO: handle DELETE
|
|
|
|
|
2017-06-03 18:20:17 +02:00
|
|
|
return p.then(() => {
|
|
|
|
processedChangeIds = processedChangeIds.concat(c.ids);
|
|
|
|
}).catch((error) => {
|
2017-06-11 23:11:14 +02:00
|
|
|
Log.warn('Failed applying changes', c.ids, error);
|
2017-06-03 18:20:17 +02:00
|
|
|
// This is fine - trying to apply changes to an object that has been deleted
|
2017-06-11 23:11:14 +02:00
|
|
|
// if (error.type == 'NotFoundException') {
|
|
|
|
// processedChangeIds = processedChangeIds.concat(c.ids);
|
|
|
|
// } else {
|
|
|
|
// throw error;
|
|
|
|
// }
|
2017-06-03 18:20:17 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-06-11 23:11:14 +02:00
|
|
|
return promiseChain(chain);
|
2017-06-12 23:56:27 +02:00
|
|
|
// }).then(() => {
|
|
|
|
// console.info(remoteFiles);
|
|
|
|
// for (let i = 0; i < remoteFiles.length; i++) {
|
|
|
|
// const remoteFile = remoteFiles[i];
|
|
|
|
|
|
|
|
// }
|
2017-06-11 23:11:14 +02:00
|
|
|
}).catch((error) => {
|
2017-06-12 23:56:27 +02:00
|
|
|
Log.error('Synchronization was interrupted due to an error:', error);
|
2017-06-11 23:11:14 +02:00
|
|
|
}).then(() => {
|
2017-06-12 23:56:27 +02:00
|
|
|
//Log.info('IDs to delete: ', processedChangeIds);
|
|
|
|
//return Change.deleteMultiple(processedChangeIds);
|
2017-06-03 18:20:17 +02:00
|
|
|
}).then(() => {
|
|
|
|
this.processState('downloadChanges');
|
|
|
|
});
|
2017-06-11 23:11:14 +02:00
|
|
|
|
|
|
|
|
|
|
|
// }).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');
|
|
|
|
// });
|
|
|
|
// });
|
2017-06-03 18:20:17 +02:00
|
|
|
}
|
2017-05-20 00:16:50 +02:00
|
|
|
|
2017-06-03 18:20:17 +02:00
|
|
|
processState_downloadChanges() {
|
2017-06-12 23:56:27 +02:00
|
|
|
// return this.api().list('', true).then((items) => {
|
|
|
|
// remoteFiles = items;
|
|
|
|
// return Change.all();
|
2017-05-19 21:12:09 +02:00
|
|
|
|
2017-06-12 23:56:27 +02:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
// });
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
// });
|
2017-06-03 18:20:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
processState(state) {
|
|
|
|
Log.info('Sync: processing: ' + state);
|
|
|
|
this.state_ = state;
|
|
|
|
|
|
|
|
if (state == 'uploadChanges') {
|
2017-06-11 23:11:14 +02:00
|
|
|
return this.processState_uploadChanges();
|
2017-06-03 18:20:17 +02:00
|
|
|
} else if (state == 'downloadChanges') {
|
2017-06-11 23:11:14 +02:00
|
|
|
return this.processState('idle');
|
|
|
|
//this.processState_downloadChanges();
|
2017-06-03 18:20:17 +02:00
|
|
|
} else if (state == 'idle') {
|
|
|
|
// Nothing
|
|
|
|
} else {
|
|
|
|
throw new Error('Invalid state: ' . state);
|
2017-05-18 21:58:01 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
start() {
|
2017-05-19 21:12:09 +02:00
|
|
|
Log.info('Sync: start');
|
2017-05-18 21:58:01 +02:00
|
|
|
|
|
|
|
if (this.state() != 'idle') {
|
|
|
|
Log.info("Sync: cannot start synchronizer because synchronization already in progress. State: " + this.state());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-06-11 23:11:14 +02:00
|
|
|
// if (!this.api().session()) {
|
|
|
|
// Log.info("Sync: cannot start synchronizer because user is not logged in.");
|
|
|
|
// return;
|
|
|
|
// }
|
2017-05-18 21:58:01 +02:00
|
|
|
|
2017-06-11 23:11:14 +02:00
|
|
|
return this.processState('uploadChanges');
|
2017-05-18 21:58:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
export { Synchronizer };
|