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) {
|
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) {
|
||||||
|
@ -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) => {
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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
|
@ -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));
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user