1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Fixed onedrive sync issue

This commit is contained in:
Laurent Cozic 2017-06-29 18:03:16 +00:00
parent 01fc71d732
commit 9060ed489c
11 changed files with 358 additions and 261 deletions

View File

@ -50,19 +50,8 @@ function createNoteId(note) {
} }
async function fuzzyMatch(note) { async function fuzzyMatch(note) {
let notes = await Note.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0 AND created_time = ?', [note.created_time]); let notes = await Note.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0 AND created_time = ? AND title = ?', [note.created_time, note.title]);
if (!notes.length) return null; return notes.length !== 1 ? null : notes[0];
if (notes.length === 1) return notes[0];
for (let i = 0; i < notes.length; i++) {
if (notes[i].title == note.title && note.title.trim() != '') return notes[i];
}
for (let i = 0; i < notes.length; i++) {
if (notes[i].body == note.body && note.body.trim() != '') return notes[i];
}
return null;
} }
async function saveNoteResources(note) { async function saveNoteResources(note) {

View File

@ -42,21 +42,6 @@ let logger = new Logger();
let dbLogger = new Logger(); let dbLogger = new Logger();
let syncLogger = new Logger(); let syncLogger = new Logger();
// commands.push({
// usage: 'root',
// options: [
// ['--profile <filePath>', 'Sets the profile path directory.'],
// ],
// action: function(args, end) {
// if (args.profile) {
// initArgs.profileDir = args.profile;
// args.splice(0, 2);
// }
// end(args);
// },
// });
commands.push({ commands.push({
usage: 'version', usage: 'version',
description: 'Displays version information', description: 'Displays version information',
@ -433,7 +418,7 @@ function commandByName(name) {
for (let i = 0; i < commands.length; i++) { for (let i = 0; i < commands.length; i++) {
let c = commands[i]; let c = commands[i];
let n = c.usage.split(' '); let n = c.usage.split(' ');
n = n[0]; n = n[0].trim();
if (n == name) return c; if (n == name) return c;
if (c.aliases && c.aliases.indexOf(name) >= 0) return c; if (c.aliases && c.aliases.indexOf(name) >= 0) return c;
} }
@ -453,6 +438,58 @@ function execCommand(name, args) {
}); });
} }
// async function execCommand(args) {
// var parseArgs = require('minimist');
// let results = parseArgs(args);
// //var results = vorpal.parse(args, { use: 'minimist' });
// if (!results['_'].length) throw new Error(_('Invalid command: %s', args));
// console.info(results);
// let commandName = results['_'].splice(0, 1);
// let cmd = commandByName(commandName);
// if (!cmd) throw new Error(_('Unknown command: %s', args));
// let usage = cmd.usage.split(' ');
// let commandArgs = [];
// usage.splice(0, 1);
// for (let i = 0; i < usage.length; i++) {
// let u = usage[i].trim();
// if (u == '') continue;
// let required = false;
// if (u.length >= 3 && u[0] == '<' && u[u.length - 1] == '>') {
// required = true;
// u = u.substr(1, u.length - 2);
// }
// if (u.length >= 3 && u[0] == '[' && u[u.length - 1] == ']') {
// u = u.substr(1, u.length - 2);
// }
// if (required && !results['_'].length) throw new Error(_('Missing argument: %s', args));
// if (!results['_'].length) break;
// console.info(u);
// commandArgs[u] = results['_'].splice(0, 1);
// }
// console.info(commandArgs);
// // usage: 'import-enex <file> [notebook]',
// // description: _('Imports en Evernote notebook file (.enex file).'),
// // options: [
// // ['--fuzzy-matching', 'For debugging purposes. Do not use.'],
// // ],
// }
async function synchronizer(syncTarget) { async function synchronizer(syncTarget) {
if (synchronizers_[syncTarget]) return synchronizers_[syncTarget]; if (synchronizers_[syncTarget]) return synchronizers_[syncTarget];
@ -580,53 +617,49 @@ function cmdPromptConfirm(commandInstance, message) {
}); });
} }
// Handles the initial arguments passed to main script and // Handles the initial flags passed to main script and
// route them to the "root" command. // returns the remaining args.
function handleStartArgs(argv) { async function handleStartFlags(argv) {
return new Promise((resolve, reject) => { argv = argv.slice(0);
while (true) { argv.splice(0, 2); // First arguments are the node executable, and the node JS file
while (argv.length) {
let arg = argv[0];
let nextArg = argv.length >= 2 ? argv[1] : null;
if (arg == '--profile') {
if (!nextArg) {
throw new Error(_('Usage: --profile <dir-path>'));
}
initArgs.profileDir = nextArg;
argv.splice(0, 2); argv.splice(0, 2);
continue;
if (argv[0] == '--profile') {
argv.splice(0, 1);
if (!argv.length) throw new Error(_('Profile path is missing'));
initArgs.profileDir = argv[0];
argv.splice(0, 1);
} else if (argv[0][0] === '-') {
throw new Error(_('Unknown flag: "%s"', argv[0]));
}
if (!argv.length || argv[0][0] != '-') {
resolve(argv);
}
return;
// if (argv && argv.length >= 3 && argv[2][0] == '-') {
// const startParams = vorpal.parse(argv, { use: 'minimist' });
// const cmd = commandByName('root');
// cmd.action(startParams, (newArgs) => {
// console.info(newArgs);
// resolve();
// });
// } else {
// console.info(argv);
// resolve();
// }
} }
// if (argv && argv.length >= 3 && argv[2][0] == '-') {
// const startParams = vorpal.parse(argv, { use: 'minimist' }); if (arg.length && arg[0] == '-') {
// const cmd = commandByName('root'); throw new Error(_('Unknown flag: %s', arg));
// cmd.action(startParams, (newArgs) => { } else {
// console.info(newArgs); break;
// resolve(); }
// }); }
// } else {
// console.info(argv); return argv;
// resolve(); }
// }
}); function escapeShellArg(arg) {
if (arg.indexOf('"') >= 0 && arg.indexOf("'") >= 0) throw new Error(_('Command line argument "%s" contains both quotes and double-quotes - aborting.', arg)); // Hopeless case
let quote = '"';
if (arg.indexOf('"') >= 0) quote = "'";
if (arg.indexOf(' ') >= 0 || arg.indexOf("\t") >= 0) return quote + arg + quote;
return arg;
}
function shellArgsToString(args) {
let output = [];
for (let i = 0; i < args.length; i++) {
output.push(escapeShellArg(args[i]));
}
return output.join(' ');
} }
process.stdin.on('keypress', (_, key) => { process.stdin.on('keypress', (_, key) => {
@ -645,7 +678,6 @@ const vorpal = require('vorpal')();
async function main() { async function main() {
for (let commandIndex = 0; commandIndex < commands.length; commandIndex++) { for (let commandIndex = 0; commandIndex < commands.length; commandIndex++) {
let c = commands[commandIndex]; let c = commands[commandIndex];
if (c.usage == 'root') continue;
let o = vorpal.command(c.usage, c.description); let o = vorpal.command(c.usage, c.description);
if (c.options) { if (c.options) {
for (let i = 0; i < c.options.length; i++) { for (let i = 0; i < c.options.length; i++) {
@ -669,7 +701,8 @@ async function main() {
vorpal.history('net.cozic.joplin'); // Enables persistent history vorpal.history('net.cozic.joplin'); // Enables persistent history
await handleStartArgs(process.argv); let argv = process.argv;
argv = await handleStartFlags(argv);
const profileDir = initArgs.profileDir ? initArgs.profileDir : os.homedir() + '/.config/' + Setting.value('appName'); const profileDir = initArgs.profileDir ? initArgs.profileDir : os.homedir() + '/.config/' + Setting.value('appName');
const resourceDir = profileDir + '/resources'; const resourceDir = profileDir + '/resources';
@ -704,10 +737,19 @@ async function main() {
if (!activeFolder) activeFolder = await Folder.defaultFolder(); if (!activeFolder) activeFolder = await Folder.defaultFolder();
if (!activeFolder) activeFolder = await Folder.createDefaultFolder(); if (!activeFolder) activeFolder = await Folder.createDefaultFolder();
if (!activeFolder) throw new Error(_('No default notebook is defined and could not create a new one. The database might be corrupted, please delete it and try again.')); if (!activeFolder) throw new Error(_('No default notebook is defined and could not create a new one. The database might be corrupted, please delete it and try again.'));
Setting.setValue('activeFolderId', activeFolder.id);
if (activeFolder) await execCommand('cd', { 'notebook': activeFolder.title }); // Use execCommand() so that no history entry is created await execCommand('cd', { 'notebook': activeFolder.title }); // Use execCommand() so that no history entry is created
vorpal.delimiter(promptString()).show(); vorpal.delimiter(promptString()).show();
// If we still have arguments, pass it to Vorpal and exit
if (argv.length) {
let cmd = shellArgsToString(argv);
vorpal.log(_('Executing: %s', cmd));
await vorpal.exec(cmd);
await vorpal.exec('exit');
return;
}
} }
main().catch((error) => { main().catch((error) => {

View File

@ -1,6 +1,6 @@
{ {
"name": "joplin-cli", "name": "joplin-cli",
"version": "0.8.11", "version": "0.8.14",
"bin": { "bin": {
"joplin": "./main.sh" "joplin": "./main.sh"
}, },

View File

@ -1,4 +1,6 @@
#!/bin/bash #!/bin/bash
set -e set -e
CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
bash $CLIENT_DIR/build.sh && NODE_PATH="$CLIENT_DIR/build/" node build/main.js --profile ~/Temp/TestNotes import-enex --fuzzy-matching /home/laurent/Downloads/desktop/afaire.enex afaire bash $CLIENT_DIR/build.sh && NODE_PATH="$CLIENT_DIR/build/" node build/main.js --profile ~/Temp/TestNotes
#bash $CLIENT_DIR/build.sh && NODE_PATH="$CLIENT_DIR/build/" node build/main.js --profile ~/Temp/TestNotes import-enex --fuzzy-matching /home/laurent/Desktop/afaire.enex afaire
#bash $CLIENT_DIR/build.sh && NODE_PATH="$CLIENT_DIR/build/" node build/main.js --profile ~/Temp/TestNotes import-enex --fuzzy-matching /home/laurent/Desktop/Laurent.enex laurent

View File

@ -20,6 +20,7 @@ async function allItems() {
async function localItemsSameAsRemote(locals, expect) { async function localItemsSameAsRemote(locals, expect) {
try { try {
let files = await fileApi().list(); let files = await fileApi().list();
files = files.items;
expect(locals.length).toBe(files.length); expect(locals.length).toBe(files.length);
for (let i = 0; i < locals.length; i++) { for (let i = 0; i < locals.length; i++) {
@ -116,7 +117,6 @@ describe('Synchronizer', function() {
await synchronizer().start(); await synchronizer().start();
let all = await allItems(); let all = await allItems();
let files = await fileApi().list();
await localItemsSameAsRemote(all, expect); await localItemsSameAsRemote(all, expect);
@ -227,6 +227,7 @@ describe('Synchronizer', function() {
await synchronizer().start(); await synchronizer().start();
let files = await fileApi().list(); let files = await fileApi().list();
files = files.items;
expect(files.length).toBe(1); expect(files.length).toBe(1);
expect(files[0].path).toBe(Folder.systemPath(folder1)); expect(files[0].path).toBe(Folder.systemPath(folder1));

View File

@ -53,7 +53,7 @@ class FileApiDriverLocal {
}); });
} }
list(path) { list(path, options) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.readdir(path, (error, items) => { fs.readdir(path, (error, items) => {
if (error) { if (error) {
@ -75,7 +75,11 @@ class FileApiDriverLocal {
return promiseChain(chain).then((results) => { return promiseChain(chain).then((results) => {
if (!results) results = []; if (!results) results = [];
resolve(results); resolve({
items: results
hasMore: false,
context: null,
});
}).catch((error) => { }).catch((error) => {
reject(error); reject(error);
}); });

View File

@ -41,7 +41,7 @@ class FileApiDriverMemory {
return Promise.resolve(); return Promise.resolve();
} }
list(path) { list(path, options) {
let output = []; let output = [];
for (let i = 0; i < this.items_.length; i++) { for (let i = 0; i < this.items_.length; i++) {
@ -57,7 +57,11 @@ class FileApiDriverMemory {
} }
} }
return Promise.resolve(output); return Promise.resolve({
items: output,
hasMore: false,
context: null,
});
} }
get(path) { get(path) {

View File

@ -45,7 +45,7 @@ class FileApiDriverOneDrive {
try { try {
item = await this.api_.execJson('GET', this.makePath_(path), this.itemFilter_()); item = await this.api_.execJson('GET', this.makePath_(path), this.itemFilter_());
} catch (error) { } catch (error) {
if (error.error.code == 'itemNotFound') return null; if (error.code == 'itemNotFound') return null;
throw error; throw error;
} }
return item; return item;
@ -67,9 +67,22 @@ class FileApiDriverOneDrive {
return this.makeItem_(item); return this.makeItem_(item);
} }
async list(path) { async list(path, options = null) {
let items = await this.api_.execJson('GET', this.makePath_(path) + ':/children', this.itemFilter_()); let query = this.itemFilter_();
return this.makeItems_(items.value); let url = this.makePath_(path) + ':/children';
if (options.context) {
query = null;
url = options.context;
}
let r = await this.api_.execJson('GET', url, query);
return {
hasMore: !!r['@odata.nextLink'],
items: this.makeItems_(r.value),
context: r["@odata.nextLink"],
}
} }
async get(path) { async get(path) {
@ -77,7 +90,7 @@ class FileApiDriverOneDrive {
try { try {
content = await this.api_.execText('GET', this.makePath_(path) + ':/content'); content = await this.api_.execText('GET', this.makePath_(path) + ':/content');
} catch (error) { } catch (error) {
if (error.error.code == 'itemNotFound') return null; if (error.code == 'itemNotFound') return null;
throw error; throw error;
} }
return content; return content;

View File

@ -27,17 +27,19 @@ class FileApi {
list(path = '', options = null) { list(path = '', options = null) {
if (!options) options = {}; if (!options) options = {};
if (!('includeHidden' in options)) options.includeHidden = false; if (!('includeHidden' in options)) options.includeHidden = false;
if (!('context' in options)) options.context = null;
this.logger().debug('list'); this.logger().debug('list ' + this.baseDir_);
return this.driver_.list(this.baseDir_).then((items) => {
return this.driver_.list(this.baseDir_, options).then((result) => {
if (!options.includeHidden) { if (!options.includeHidden) {
let temp = []; let temp = [];
for (let i = 0; i < items.length; i++) { for (let i = 0; i < result.items.length; i++) {
if (!isHidden(items[i].path)) temp.push(items[i]); if (!isHidden(result.items[i].path)) temp.push(result.items[i]);
} }
items = temp; result.items = temp;
} }
return items; return result;
}); });
} }

View File

@ -67,6 +67,20 @@ class OneDriveApi {
return 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?' + stringify(query); return 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?' + stringify(query);
} }
oneDriveErrorResponseToError(errorResponse) {
if (!errorResponse) return new Error('Undefined error');
if (errorResponse.error) {
let e = errorResponse.error;
let output = new Error(e.message);
if (e.code) output.code = e.code;
if (e.innerError) output.innerError = e.innerError;
return output;
} else {
return new Error(JSON.stringify(errorResponse));
}
}
async exec(method, path, query = null, data = null, options = null) { async exec(method, path, query = null, data = null, options = null) {
method = method.toUpperCase(); method = method.toUpperCase();
@ -82,26 +96,42 @@ class OneDriveApi {
if (data) data = JSON.stringify(data); if (data) data = JSON.stringify(data);
} }
let url = 'https://graph.microsoft.com/v1.0' + path; let url = path;
if (query) url += '?' + stringify(query); // In general, `path` contains a path relative to the base URL, but in some
// cases the full URL is provided (for example, when it's a URL that was
// retrieved from the API).
if (url.indexOf('https://') !== 0) url = 'https://graph.microsoft.com/v1.0' + path;
if (query) {
url += url.indexOf('?') < 0 ? '?' : '&';
url += stringify(query);
}
if (data) options.body = data; if (data) options.body = data;
// console.info(method + ' ' + url); // Rare error (one Google hit) - maybe repeat the request when it happens?
// console.info(data);
// { error:
// { code: 'generalException',
// message: 'An error occurred in the data store.',
// innerError:
// { 'request-id': 'b4310552-c18a-45b1-bde1-68e2c2345eef',
// date: '2017-06-29T00:15:50' } } }
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
options.headers['Authorization'] = 'bearer ' + this.token(); options.headers['Authorization'] = 'bearer ' + this.token();
let response = await fetch(url, options); let response = await fetch(url, options);
if (!response.ok) { if (!response.ok) {
let error = await response.json(); let errorResponse = await response.json();
let error = this.oneDriveErrorResponseToError(errorResponse);
if (error && error.error && error.error.code == 'InvalidAuthenticationToken') { if (error.code == 'InvalidAuthenticationToken') {
await this.refreshAccessToken(); await this.refreshAccessToken();
continue; continue;
} else { } else {
error.request = method + ' ' + url + ' ' + JSON.stringify(query) + ' ' + JSON.stringify(data) + ' ' + JSON.stringify(options);
throw error; throw error;
} }
} }

View File

@ -104,194 +104,204 @@ class Synchronizer {
noteConflict: 0, noteConflict: 0,
}; };
await this.createWorkDir(); try {
await this.createWorkDir();
let donePaths = []; let donePaths = [];
while (true) { while (true) {
let result = await BaseItem.itemsThatNeedSync(); let result = await BaseItem.itemsThatNeedSync();
let locals = result.items; let locals = result.items;
for (let i = 0; i < locals.length; i++) { for (let i = 0; i < locals.length; i++) {
let local = locals[i]; let local = locals[i];
let ItemClass = BaseItem.itemClass(local); let ItemClass = BaseItem.itemClass(local);
let path = BaseItem.systemPath(local); let path = BaseItem.systemPath(local);
// Safety check to avoid infinite loops: // 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)); if (donePaths.indexOf(path) > 0) throw new Error(sprintf('Processing a path that has already been done: %s. sync_time was not updated?', path));
let remote = await this.api().stat(path); let remote = await this.api().stat(path);
let content = ItemClass.serialize(local); let content = ItemClass.serialize(local);
let action = null; let action = null;
let updateSyncTimeOnly = true; let updateSyncTimeOnly = true;
let reason = ''; let reason = '';
if (!remote) { if (!remote) {
if (!local.sync_time) { if (!local.sync_time) {
action = 'createRemote'; action = 'createRemote';
reason = 'remote does not exist, and local is new and has never been synced'; reason = 'remote does not exist, and local is new and has never been synced';
} else {
// Note or folder was modified after having been deleted remotely
action = local.type_ == BaseModel.MODEL_TYPE_NOTE ? 'noteConflict' : 'folderConflict';
reason = 'remote has been deleted, but local has changes';
}
} else { } else {
// Note or folder was modified after having been deleted remotely if (remote.updated_time > local.sync_time) {
action = local.type_ == BaseModel.MODEL_TYPE_NOTE ? 'noteConflict' : 'folderConflict'; // Since, in this loop, we are only dealing with notes that require sync, if the
reason = 'remote has been deleted, but local has changes'; // remote has been modified after the sync time, it means both notes have been
// modified and so there's a conflict.
action = local.type_ == BaseModel.MODEL_TYPE_NOTE ? 'noteConflict' : 'folderConflict';
reason = 'both remote and local have changes';
} else {
action = 'updateRemote';
reason = 'local has changes';
}
} }
} else {
if (remote.updated_time > local.sync_time) { this.logSyncOperation(action, local, remote, reason);
// Since, in this loop, we are only dealing with notes that require sync, if the
// remote has been modified after the sync time, it means both notes have been if (action == 'createRemote' || action == 'updateRemote') {
// modified and so there's a conflict.
action = local.type_ == BaseModel.MODEL_TYPE_NOTE ? 'noteConflict' : 'folderConflict'; // Make the operation atomic by doing the work on a copy of the file
reason = 'both remote and local have changes'; // and then copying it back to the original location.
let tempPath = this.syncDirName_ + '/' + path + '_' + time.unixMs();
await this.api().put(tempPath, content);
await this.api().setTimestamp(tempPath, local.updated_time);
await this.api().move(tempPath, path);
await ItemClass.save({ id: local.id, sync_time: time.unixMs(), type_: local.type_ }, { autoTimestamp: false });
} else if (action == 'folderConflict') {
if (remote) {
let remoteContent = await this.api().get(path);
local = BaseItem.unserialize(remoteContent);
local.sync_time = time.unixMs();
await ItemClass.save(local, { autoTimestamp: false });
} else {
await ItemClass.delete(local.id);
}
} 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 conflictedNote = Object.assign({}, local);
delete conflictedNote.id;
conflictedNote.is_conflict = 1;
await Note.save(conflictedNote, { autoTimestamp: false });
if (remote) {
let remoteContent = await this.api().get(path);
local = BaseItem.unserialize(remoteContent);
local.sync_time = time.unixMs();
await ItemClass.save(local, { autoTimestamp: false });
}
}
report[action]++;
donePaths.push(path);
}
if (!result.hasMore) break;
}
// ------------------------------------------------------------------------
// 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)
this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
await this.api().delete(path);
await BaseModel.remoteDeletedItem(item.item_id);
report['deleteRemote']++;
}
// ------------------------------------------------------------------------
// Loop through all the remote items, find those that
// have been updated, and apply the changes to local.
// ------------------------------------------------------------------------
// 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.
let remoteIds = [];
let context = null;
while (true) {
let listResult = await this.api().list('', { context: context });
let remotes = listResult.items;
for (let i = 0; i < remotes.length; i++) {
let remote = remotes[i];
let path = remote.path;
remoteIds.push(BaseItem.pathToId(path));
if (donePaths.indexOf(path) > 0) continue;
let action = null;
let reason = '';
let local = await BaseItem.loadItemByPath(path);
if (!local) {
action = 'createLocal';
reason = 'remote exists but local does not';
} else { } else {
action = 'updateRemote'; if (remote.updated_time > local.updated_time) {
reason = 'local has changes'; action = 'updateLocal';
reason = sprintf('remote is more recent than local'); // , time.unixMsToIso(remote.updated_time), time.unixMsToIso(local.updated_time)
}
} }
}
this.logSyncOperation(action, local, remote, reason); if (!action) continue;
if (action == 'createRemote' || action == 'updateRemote') { if (action == 'createLocal' || action == 'updateLocal') {
let content = await this.api().get(path);
if (content === null) {
this.logger().warn('Remote has been deleted between now and the list() call? In that case it will be handled during the next sync: ' + path);
continue;
}
content = BaseItem.unserialize(content);
let ItemClass = BaseItem.itemClass(content);
// Make the operation atomic by doing the work on a copy of the file let newContent = Object.assign({}, content);
// and then copying it back to the original location. newContent.sync_time = time.unixMs();
let tempPath = this.syncDirName_ + '/' + path + '_' + time.unixMs(); let options = { autoTimestamp: false };
if (action == 'createLocal') options.isNew = true;
await ItemClass.save(newContent, options);
await this.api().put(tempPath, content); this.logSyncOperation(action, local, content, reason);
await this.api().setTimestamp(tempPath, local.updated_time);
await this.api().move(tempPath, path);
await ItemClass.save({ id: local.id, sync_time: time.unixMs(), type_: local.type_ }, { autoTimestamp: false });
} else if (action == 'folderConflict') {
if (remote) {
let remoteContent = await this.api().get(path);
local = BaseItem.unserialize(remoteContent);
local.sync_time = time.unixMs();
await ItemClass.save(local, { autoTimestamp: false });
} else { } else {
await ItemClass.delete(local.id); this.logSyncOperation(action, local, remote, reason);
}
} 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 conflictedNote = Object.assign({}, local);
delete conflictedNote.id;
conflictedNote.is_conflict = 1;
await Note.save(conflictedNote, { autoTimestamp: false });
if (remote) {
let remoteContent = await this.api().get(path);
local = BaseItem.unserialize(remoteContent);
local.sync_time = time.unixMs();
await ItemClass.save(local, { autoTimestamp: false });
} }
report[action]++;
} }
report[action]++; if (!listResult.hasMore) break;
context = listResult.context;
donePaths.push(path);
} }
if (!result.hasMore) break; // ------------------------------------------------------------------------
} // Search, among the local IDs, those that don't exist remotely, which
// means the item has been deleted.
// ------------------------------------------------------------------------
// ------------------------------------------------------------------------ let noteIds = await Folder.syncedNoteIds();
// Delete the remote items that have been deleted locally. for (let i = 0; i < noteIds.length; i++) {
// ------------------------------------------------------------------------ let noteId = noteIds[i];
if (remoteIds.indexOf(noteId) < 0) {
let deletedItems = await BaseModel.deletedItems(); this.logSyncOperation('deleteLocal', { id: noteId }, null, 'remote has been deleted');
for (let i = 0; i < deletedItems.length; i++) { await Note.delete(noteId, { trackDeleted: false });
let item = deletedItems[i]; report['deleteLocal']++;
let path = BaseItem.systemPath(item.item_id)
this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
await this.api().delete(path);
await BaseModel.remoteDeletedItem(item.item_id);
report['deleteRemote']++;
}
// ------------------------------------------------------------------------
// Loop through all the remote items, find those that
// have been updated, and apply the changes to local.
// ------------------------------------------------------------------------
// 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.
let remoteIds = [];
let remotes = await this.api().list();
for (let i = 0; i < remotes.length; i++) {
let remote = remotes[i];
let path = remote.path;
remoteIds.push(BaseItem.pathToId(path));
if (donePaths.indexOf(path) > 0) continue;
let action = null;
let reason = '';
let local = await BaseItem.loadItemByPath(path);
if (!local) {
action = 'createLocal';
reason = 'remote exists but local does not';
} else {
if (remote.updated_time > local.updated_time) {
action = 'updateLocal';
reason = sprintf('remote is more recent than local'); // , time.unixMsToIso(remote.updated_time), time.unixMsToIso(local.updated_time)
} }
} }
} catch (error) {
if (!action) continue; this.logger().error(error);
throw error;
if (action == 'createLocal' || action == 'updateLocal') {
let content = await this.api().get(path);
if (content === null) {
this.logger().warn('Remote has been deleted between now and the list() call? In that case it will be handled during the next sync: ' + path);
continue;
}
content = BaseItem.unserialize(content);
let ItemClass = BaseItem.itemClass(content);
let newContent = Object.assign({}, content);
newContent.sync_time = time.unixMs();
let options = { autoTimestamp: false };
if (action == 'createLocal') options.isNew = true;
await ItemClass.save(newContent, options);
this.logSyncOperation(action, local, content, reason);
} else {
this.logSyncOperation(action, local, remote, reason);
}
report[action]++;
}
// ------------------------------------------------------------------------
// Search, among the local IDs, those that don't exist remotely, which
// means the item has been deleted.
// ------------------------------------------------------------------------
let noteIds = await Folder.syncedNoteIds();
for (let i = 0; i < noteIds.length; i++) {
let noteId = noteIds[i];
if (remoteIds.indexOf(noteId) < 0) {
this.logSyncOperation('deleteLocal', { id: noteId }, null, 'remote has been deleted');
await Note.delete(noteId, { trackDeleted: false });
report['deleteLocal']++;
}
} }
this.logger().info('Synchronization complete [' + synchronizationId + ']:'); this.logger().info('Synchronization complete [' + synchronizationId + ']:');
await this.logSyncSummary(report); await this.logSyncSummary(report);
this.state_ = 'idle'; this.state_ = 'idle';
return Promise.resolve();
} }
} }