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:
parent
01fc71d732
commit
9060ed489c
@ -50,19 +50,8 @@ function createNoteId(note) {
|
||||
}
|
||||
|
||||
async function fuzzyMatch(note) {
|
||||
let notes = await Note.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0 AND created_time = ?', [note.created_time]);
|
||||
if (!notes.length) return null;
|
||||
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;
|
||||
let notes = await Note.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0 AND created_time = ? AND title = ?', [note.created_time, note.title]);
|
||||
return notes.length !== 1 ? null : notes[0];
|
||||
}
|
||||
|
||||
async function saveNoteResources(note) {
|
||||
|
@ -42,21 +42,6 @@ let logger = new Logger();
|
||||
let dbLogger = 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({
|
||||
usage: 'version',
|
||||
description: 'Displays version information',
|
||||
@ -433,7 +418,7 @@ function commandByName(name) {
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
let c = commands[i];
|
||||
let n = c.usage.split(' ');
|
||||
n = n[0];
|
||||
n = n[0].trim();
|
||||
if (n == name) 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) {
|
||||
if (synchronizers_[syncTarget]) return synchronizers_[syncTarget];
|
||||
|
||||
@ -580,53 +617,49 @@ function cmdPromptConfirm(commandInstance, message) {
|
||||
});
|
||||
}
|
||||
|
||||
// Handles the initial arguments passed to main script and
|
||||
// route them to the "root" command.
|
||||
function handleStartArgs(argv) {
|
||||
return new Promise((resolve, reject) => {
|
||||
while (true) {
|
||||
// Handles the initial flags passed to main script and
|
||||
// returns the remaining args.
|
||||
async function handleStartFlags(argv) {
|
||||
argv = argv.slice(0);
|
||||
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);
|
||||
|
||||
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();
|
||||
// }
|
||||
|
||||
continue;
|
||||
}
|
||||
// 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 (arg.length && arg[0] == '-') {
|
||||
throw new Error(_('Unknown flag: %s', arg));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return argv;
|
||||
}
|
||||
|
||||
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) => {
|
||||
@ -645,7 +678,6 @@ const vorpal = require('vorpal')();
|
||||
async function main() {
|
||||
for (let commandIndex = 0; commandIndex < commands.length; commandIndex++) {
|
||||
let c = commands[commandIndex];
|
||||
if (c.usage == 'root') continue;
|
||||
let o = vorpal.command(c.usage, c.description);
|
||||
if (c.options) {
|
||||
for (let i = 0; i < c.options.length; i++) {
|
||||
@ -669,7 +701,8 @@ async function main() {
|
||||
|
||||
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 resourceDir = profileDir + '/resources';
|
||||
@ -704,10 +737,19 @@ async function main() {
|
||||
if (!activeFolder) activeFolder = await Folder.defaultFolder();
|
||||
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.'));
|
||||
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();
|
||||
|
||||
// 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) => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joplin-cli",
|
||||
"version": "0.8.11",
|
||||
"version": "0.8.14",
|
||||
"bin": {
|
||||
"joplin": "./main.sh"
|
||||
},
|
||||
|
@ -1,4 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
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
|
@ -20,6 +20,7 @@ async function allItems() {
|
||||
async function localItemsSameAsRemote(locals, expect) {
|
||||
try {
|
||||
let files = await fileApi().list();
|
||||
files = files.items;
|
||||
expect(locals.length).toBe(files.length);
|
||||
|
||||
for (let i = 0; i < locals.length; i++) {
|
||||
@ -116,7 +117,6 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start();
|
||||
|
||||
let all = await allItems();
|
||||
let files = await fileApi().list();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
|
||||
@ -227,6 +227,7 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start();
|
||||
|
||||
let files = await fileApi().list();
|
||||
files = files.items;
|
||||
|
||||
expect(files.length).toBe(1);
|
||||
expect(files[0].path).toBe(Folder.systemPath(folder1));
|
||||
|
@ -53,7 +53,7 @@ class FileApiDriverLocal {
|
||||
});
|
||||
}
|
||||
|
||||
list(path) {
|
||||
list(path, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readdir(path, (error, items) => {
|
||||
if (error) {
|
||||
@ -75,7 +75,11 @@ class FileApiDriverLocal {
|
||||
|
||||
return promiseChain(chain).then((results) => {
|
||||
if (!results) results = [];
|
||||
resolve(results);
|
||||
resolve({
|
||||
items: results
|
||||
hasMore: false,
|
||||
context: null,
|
||||
});
|
||||
}).catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
@ -41,7 +41,7 @@ class FileApiDriverMemory {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
list(path) {
|
||||
list(path, options) {
|
||||
let output = [];
|
||||
|
||||
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) {
|
||||
|
@ -45,7 +45,7 @@ class FileApiDriverOneDrive {
|
||||
try {
|
||||
item = await this.api_.execJson('GET', this.makePath_(path), this.itemFilter_());
|
||||
} catch (error) {
|
||||
if (error.error.code == 'itemNotFound') return null;
|
||||
if (error.code == 'itemNotFound') return null;
|
||||
throw error;
|
||||
}
|
||||
return item;
|
||||
@ -67,9 +67,22 @@ class FileApiDriverOneDrive {
|
||||
return this.makeItem_(item);
|
||||
}
|
||||
|
||||
async list(path) {
|
||||
let items = await this.api_.execJson('GET', this.makePath_(path) + ':/children', this.itemFilter_());
|
||||
return this.makeItems_(items.value);
|
||||
async list(path, options = null) {
|
||||
let query = this.itemFilter_();
|
||||
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) {
|
||||
@ -77,7 +90,7 @@ class FileApiDriverOneDrive {
|
||||
try {
|
||||
content = await this.api_.execText('GET', this.makePath_(path) + ':/content');
|
||||
} catch (error) {
|
||||
if (error.error.code == 'itemNotFound') return null;
|
||||
if (error.code == 'itemNotFound') return null;
|
||||
throw error;
|
||||
}
|
||||
return content;
|
||||
|
@ -27,17 +27,19 @@ class FileApi {
|
||||
list(path = '', options = null) {
|
||||
if (!options) options = {};
|
||||
if (!('includeHidden' in options)) options.includeHidden = false;
|
||||
if (!('context' in options)) options.context = null;
|
||||
|
||||
this.logger().debug('list');
|
||||
return this.driver_.list(this.baseDir_).then((items) => {
|
||||
this.logger().debug('list ' + this.baseDir_);
|
||||
|
||||
return this.driver_.list(this.baseDir_, options).then((result) => {
|
||||
if (!options.includeHidden) {
|
||||
let temp = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (!isHidden(items[i].path)) temp.push(items[i]);
|
||||
for (let i = 0; i < result.items.length; i++) {
|
||||
if (!isHidden(result.items[i].path)) temp.push(result.items[i]);
|
||||
}
|
||||
items = temp;
|
||||
result.items = temp;
|
||||
}
|
||||
return items;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -67,6 +67,20 @@ class OneDriveApi {
|
||||
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) {
|
||||
method = method.toUpperCase();
|
||||
|
||||
@ -82,26 +96,42 @@ class OneDriveApi {
|
||||
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;
|
||||
|
||||
// console.info(method + ' ' + url);
|
||||
// console.info(data);
|
||||
// Rare error (one Google hit) - maybe repeat the request when it happens?
|
||||
|
||||
// { 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++) {
|
||||
options.headers['Authorization'] = 'bearer ' + this.token();
|
||||
|
||||
let response = await fetch(url, options);
|
||||
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();
|
||||
continue;
|
||||
} else {
|
||||
error.request = method + ' ' + url + ' ' + JSON.stringify(query) + ' ' + JSON.stringify(data) + ' ' + JSON.stringify(options);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@ -104,194 +104,204 @@ class Synchronizer {
|
||||
noteConflict: 0,
|
||||
};
|
||||
|
||||
await this.createWorkDir();
|
||||
try {
|
||||
await this.createWorkDir();
|
||||
|
||||
let donePaths = [];
|
||||
while (true) {
|
||||
let result = await BaseItem.itemsThatNeedSync();
|
||||
let locals = result.items;
|
||||
let donePaths = [];
|
||||
while (true) {
|
||||
let result = await BaseItem.itemsThatNeedSync();
|
||||
let locals = result.items;
|
||||
|
||||
for (let i = 0; i < locals.length; i++) {
|
||||
let local = locals[i];
|
||||
let ItemClass = BaseItem.itemClass(local);
|
||||
let path = BaseItem.systemPath(local);
|
||||
for (let i = 0; i < locals.length; i++) {
|
||||
let local = locals[i];
|
||||
let ItemClass = BaseItem.itemClass(local);
|
||||
let path = BaseItem.systemPath(local);
|
||||
|
||||
// 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));
|
||||
// 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));
|
||||
|
||||
let remote = await this.api().stat(path);
|
||||
let content = ItemClass.serialize(local);
|
||||
let action = null;
|
||||
let updateSyncTimeOnly = true;
|
||||
let reason = '';
|
||||
let remote = await this.api().stat(path);
|
||||
let content = ItemClass.serialize(local);
|
||||
let action = null;
|
||||
let updateSyncTimeOnly = true;
|
||||
let reason = '';
|
||||
|
||||
if (!remote) {
|
||||
if (!local.sync_time) {
|
||||
action = 'createRemote';
|
||||
reason = 'remote does not exist, and local is new and has never been synced';
|
||||
if (!remote) {
|
||||
if (!local.sync_time) {
|
||||
action = 'createRemote';
|
||||
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 {
|
||||
// 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';
|
||||
if (remote.updated_time > local.sync_time) {
|
||||
// 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
|
||||
// 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) {
|
||||
// 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
|
||||
// modified and so there's a conflict.
|
||||
action = local.type_ == BaseModel.MODEL_TYPE_NOTE ? 'noteConflict' : 'folderConflict';
|
||||
reason = 'both remote and local have changes';
|
||||
|
||||
this.logSyncOperation(action, local, remote, reason);
|
||||
|
||||
if (action == 'createRemote' || action == 'updateRemote') {
|
||||
|
||||
// Make the operation atomic by doing the work on a copy of the file
|
||||
// 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 {
|
||||
action = 'updateRemote';
|
||||
reason = 'local has changes';
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// and then copying it back to the original location.
|
||||
let tempPath = this.syncDirName_ + '/' + path + '_' + time.unixMs();
|
||||
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);
|
||||
|
||||
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 });
|
||||
this.logSyncOperation(action, local, content, reason);
|
||||
} 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 });
|
||||
this.logSyncOperation(action, local, remote, reason);
|
||||
}
|
||||
|
||||
report[action]++;
|
||||
}
|
||||
|
||||
report[action]++;
|
||||
|
||||
donePaths.push(path);
|
||||
if (!listResult.hasMore) break;
|
||||
context = listResult.context;
|
||||
}
|
||||
|
||||
if (!result.hasMore) break;
|
||||
}
|
||||
// ------------------------------------------------------------------------
|
||||
// Search, among the local IDs, those that don't exist remotely, which
|
||||
// means the item has been deleted.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 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 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)
|
||||
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']++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!action) continue;
|
||||
|
||||
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']++;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger().error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger().info('Synchronization complete [' + synchronizationId + ']:');
|
||||
await this.logSyncSummary(report);
|
||||
|
||||
this.state_ = 'idle';
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user