diff --git a/CliClient/app/cmd.js b/CliClient/app/cmd.js deleted file mode 100644 index e4867aa46..000000000 --- a/CliClient/app/cmd.js +++ /dev/null @@ -1,657 +0,0 @@ -require('source-map-support').install(); -require('babel-plugin-transform-runtime'); - -import { FileApi } from 'src/file-api.js'; -import { FileApiDriverLocal } from 'src/file-api-driver-local.js'; -import { FileApiDriverMemory } from 'src/file-api-driver-memory.js'; -import { Database } from 'src/database.js'; -import { DatabaseDriverNode } from 'src/database-driver-node.js'; -import { BaseModel } from 'src/base-model.js'; -import { Folder } from 'src/models/folder.js'; -import { Note } from 'src/models/note.js'; -import { Setting } from 'src/models/setting.js'; -import { Synchronizer } from 'src/synchronizer.js'; -import { uuid } from 'src/uuid.js'; -import { sprintf } from 'sprintf-js'; -import { _ } from 'src/locale.js'; - -let db = new Database(new DatabaseDriverNode()); -let fileDriver = new FileApiDriverLocal(); -let fileApi = new FileApi('/home/laurent/Temp/TestImport', fileDriver); -let synchronizer = new Synchronizer(db, fileApi); -const vorpal = require('vorpal')(); - -async function main() { - await db.open({ name: '/home/laurent/Temp/test.sqlite3' }); - BaseModel.db_ = db; - await Setting.load(); - - let commands = []; - let currentFolder = null; - - function switchCurrentFolder(folder) { - currentFolder = folder; - updatePrompt(); - } - - function promptString() { - let path = '~'; - if (currentFolder) { - path += '/' + currentFolder.title; - } - return 'joplin:' + path + '$ '; - } - - function updatePrompt() { - vorpal.delimiter(promptString()); - } - - // For now, to go around this issue: https://github.com/dthree/vorpal/issues/114 - function quotePromptArg(s) { - if (s.indexOf(' ') >= 0) { - return '"' + s + '"'; - } - return s; - } - - function autocompleteFolders() { - return Folder.all().then((folders) => { - let output = []; - for (let i = 0; i < folders.length; i++) { - output.push(quotePromptArg(folders[i].title)); - } - output.push('..'); - output.push('.'); - return output; - }); - } - - function autocompleteItems() { - let promise = null; - if (!currentFolder) { - promise = Folder.all(); - } else { - promise = Note.previews(currentFolder.id); - } - - return promise.then((items) => { - let output = []; - for (let i = 0; i < items.length; i++) { - output.push(quotePromptArg(items[i].title)); - } - return output; - }); - } - - process.stdin.on('keypress', (_, key) => { - if (key && key.name === 'return') { - updatePrompt(); - } - - if (key.name === 'tab') { - vorpal.ui.imprint(); - vorpal.log(vorpal.ui.input()); - } - }); - - commands.push({ - usage: 'cd ', - description: 'Moved to [list-title] - all further operations will happen within this list. Use `cd ..` to go back one level.', - action: function (args, end) { - let folderTitle = args['list-title']; - - if (folderTitle == '..') { - switchCurrentFolder(null); - end(); - return; - } - - if (folderTitle == '.') { - end(); - return; - } - - Folder.loadByField('title', folderTitle).then((folder) => { - switchCurrentFolder(folder); - end(); - }); - }, - autocomplete: autocompleteFolders, - }); - - commands.push({ - usage: 'mklist ', - alias: 'mkdir', - description: 'Creates a new list', - action: function (args, end) { - Folder.save({ title: args['list-title'] }).catch((error) => { - this.log(error); - }).then((folder) => { - switchCurrentFolder(folder); - end(); - }); - }, - }); - - commands.push({ - usage: 'mknote ', - alias: 'touch', - description: 'Creates a new note', - action: function (args, end) { - if (!currentFolder) { - this.log('Notes can only be created within a list.'); - end(); - return; - } - - let note = { - title: args['note-title'], - parent_id: currentFolder.id, - }; - Note.save(note).catch((error) => { - this.log(error); - }).then((note) => { - end(); - }); - }, - }); - - commands.push({ - usage: 'set [prop-value]', - description: 'Sets the given of the given item.', - action: function (args, end) { - let promise = null; - let title = args['item-title']; - let propName = args['prop-name']; - let propValue = args['prop-value']; - if (!propValue) propValue = ''; - - if (!currentFolder) { - promise = Folder.loadByField('title', title); - } else { - promise = Folder.loadNoteByField(currentFolder.id, 'title', title); - } - - promise.then((item) => { - if (!item) { - this.log(_('No item with title "%s" found.', title)); - end(); - return; - } - - let newItem = { - id: item.id, - type_: item.type_, - }; - newItem[propName] = propValue; - let ItemClass = BaseItem.itemClass(); - return ItemClass.save(newItem); - }).catch((error) => { - this.log(error); - }).then(() => { - end(); - }); - }, - autocomplete: autocompleteItems, - }); - - commands.push({ - usage: 'cat ', - description: 'Displays the given item data.', - action: function (args, end) { - let title = args['item-title']; - - let promise = null; - if (!currentFolder) { - promise = Folder.loadByField('title', title); - } else { - promise = Folder.loadNoteByField(currentFolder.id, 'title', title); - } - - promise.then((item) => { - if (!item) { - this.log(_('No item with title "%s" found.', title)); - end(); - return; - } - - if (!currentFolder) { - this.log(Folder.serialize(item)); - } else { - this.log(Note.serialize(item)); - } - }).catch((error) => { - this.log(error); - }).then(() => { - end(); - }); - }, - autocomplete: autocompleteItems, - }); - - commands.push({ - usage: 'rm ', - description: 'Deletes the given item. For a list, all the notes within that list will be deleted.', - action: function (args, end) { - let title = args['item-title']; - - let promise = null; - let itemType = currentFolder ? 'note' : 'folder'; - if (itemType == 'folder') { - promise = Folder.loadByField('title', title); - } else { - promise = Folder.loadNoteByField(currentFolder.id, 'title', title); - } - - promise.then((item) => { - if (!item) { - this.log(_('No item with title "%s" found.', title)); - end(); - return; - } - - if (itemType == 'folder') { - return Folder.delete(item.id); - } else { - return Note.delete(item.id); - } - }).catch((error) => { - this.log(error); - }).then(() => { - end(); - }); - }, - autocomplete: autocompleteItems, - }); - - commands.push({ - usage: 'ls [list-title]', - alias: 'll', - description: 'Lists items in [list-title].', - action: function (args, end) { - let folderTitle = args['list-title']; - - let promise = null; - - if (folderTitle == '..') { - promise = Promise.resolve('root'); - } else if (folderTitle && folderTitle != '.') { - promise = Folder.loadByField('title', folderTitle); - } else if (currentFolder) { - promise = Promise.resolve(currentFolder); - } else { - promise = Promise.resolve('root'); - } - - promise.then((folder) => { - let p = null - let postfix = ''; - if (folder === 'root') { - p = Folder.all(); - postfix = '/'; - } else if (!folder) { - throw new Error(_('Unknown list: "%s"', folderTitle)); - } else { - p = Note.previews(folder.id); - } - - return p.then((previews) => { - for (let i = 0; i < previews.length; i++) { - this.log(previews[i].title + postfix); - } - }); - }).catch((error) => { - this.log(error); - }).then(() => { - end(); - }); - }, - autocomplete: autocompleteFolders, - }); - - commands.push({ - usage: 'sync', - description: 'Synchronizes with remote storage.', - action: function (args, end) { - synchronizer.start().catch((error) => { - console.error(error); - }).then(() => { - end(); - }); - }, - }); - - for (let i = 0; i < commands.length; i++) { - let c = commands[i]; - let o = vorpal.command(c.usage, c.description); - if (c.alias) { - o.alias(c.alias); - } - if (c.autocomplete) { - o.autocomplete({ - data: c.autocomplete, - }); - } - o.action(c.action); - } - - vorpal.delimiter(promptString()).show(); -} - -main(); - -// BaseModel.db_ = db; -// }).then(() => { -// return Setting.load(); -// }).then(() => { -// let commands = []; -// let currentFolder = null; - -// function switchCurrentFolder(folder) { -// currentFolder = folder; -// updatePrompt(); -// } - -// function promptString() { -// let path = '~'; -// if (currentFolder) { -// path += '/' + currentFolder.title; -// } -// return 'joplin:' + path + '$ '; -// } - -// function updatePrompt() { -// vorpal.delimiter(promptString()); -// } - -// // For now, to go around this issue: https://github.com/dthree/vorpal/issues/114 -// function quotePromptArg(s) { -// if (s.indexOf(' ') >= 0) { -// return '"' + s + '"'; -// } -// return s; -// } - -// function autocompleteFolders() { -// return Folder.all().then((folders) => { -// let output = []; -// for (let i = 0; i < folders.length; i++) { -// output.push(quotePromptArg(folders[i].title)); -// } -// output.push('..'); -// output.push('.'); -// return output; -// }); -// } - -// function autocompleteItems() { -// let promise = null; -// if (!currentFolder) { -// promise = Folder.all(); -// } else { -// promise = Note.previews(currentFolder.id); -// } - -// return promise.then((items) => { -// let output = []; -// for (let i = 0; i < items.length; i++) { -// output.push(quotePromptArg(items[i].title)); -// } -// return output; -// }); -// } - -// process.stdin.on('keypress', (_, key) => { -// if (key && key.name === 'return') { -// updatePrompt(); -// } - -// if (key.name === 'tab') { -// vorpal.ui.imprint(); -// vorpal.log(vorpal.ui.input()); -// } -// }); - -// commands.push({ -// usage: 'cd ', -// description: 'Moved to [list-title] - all further operations will happen within this list. Use `cd ..` to go back one level.', -// action: function (args, end) { -// let folderTitle = args['list-title']; - -// if (folderTitle == '..') { -// switchCurrentFolder(null); -// end(); -// return; -// } - -// if (folderTitle == '.') { -// end(); -// return; -// } - -// Folder.loadByField('title', folderTitle).then((folder) => { -// switchCurrentFolder(folder); -// end(); -// }); -// }, -// autocomplete: autocompleteFolders, -// }); - -// commands.push({ -// usage: 'mklist ', -// alias: 'mkdir', -// description: 'Creates a new list', -// action: function (args, end) { -// Folder.save({ title: args['list-title'] }).catch((error) => { -// this.log(error); -// }).then((folder) => { -// switchCurrentFolder(folder); -// end(); -// }); -// }, -// }); - -// commands.push({ -// usage: 'mknote ', -// alias: 'touch', -// description: 'Creates a new note', -// action: function (args, end) { -// if (!currentFolder) { -// this.log('Notes can only be created within a list.'); -// end(); -// return; -// } - -// let note = { -// title: args['note-title'], -// parent_id: currentFolder.id, -// }; -// Note.save(note).catch((error) => { -// this.log(error); -// }).then((note) => { -// end(); -// }); -// }, -// }); - -// commands.push({ -// usage: 'set [prop-value]', -// description: 'Sets the given of the given item.', -// action: function (args, end) { -// let promise = null; -// let title = args['item-title']; -// let propName = args['prop-name']; -// let propValue = args['prop-value']; -// if (!propValue) propValue = ''; - -// if (!currentFolder) { -// promise = Folder.loadByField('title', title); -// } else { -// promise = Folder.loadNoteByField(currentFolder.id, 'title', title); -// } - -// promise.then((item) => { -// if (!item) { -// this.log(_('No item with title "%s" found.', title)); -// end(); -// return; -// } - -// let newItem = { -// id: item.id, -// type_: item.type_, -// }; -// newItem[propName] = propValue; -// let ItemClass = BaseItem.itemClass(); -// return ItemClass.save(newItem); -// }).catch((error) => { -// this.log(error); -// }).then(() => { -// end(); -// }); -// }, -// autocomplete: autocompleteItems, -// }); - -// commands.push({ -// usage: 'cat ', -// description: 'Displays the given item data.', -// action: function (args, end) { -// let title = args['item-title']; - -// let promise = null; -// if (!currentFolder) { -// promise = Folder.loadByField('title', title); -// } else { -// promise = Folder.loadNoteByField(currentFolder.id, 'title', title); -// } - -// promise.then((item) => { -// if (!item) { -// this.log(_('No item with title "%s" found.', title)); -// end(); -// return; -// } - -// if (!currentFolder) { -// this.log(Folder.serialize(item)); -// } else { -// this.log(Note.serialize(item)); -// } -// }).catch((error) => { -// this.log(error); -// }).then(() => { -// end(); -// }); -// }, -// autocomplete: autocompleteItems, -// }); - -// commands.push({ -// usage: 'rm ', -// description: 'Deletes the given item. For a list, all the notes within that list will be deleted.', -// action: function (args, end) { -// let title = args['item-title']; - -// let promise = null; -// let itemType = currentFolder ? 'note' : 'folder'; -// if (itemType == 'folder') { -// promise = Folder.loadByField('title', title); -// } else { -// promise = Folder.loadNoteByField(currentFolder.id, 'title', title); -// } - -// promise.then((item) => { -// if (!item) { -// this.log(_('No item with title "%s" found.', title)); -// end(); -// return; -// } - -// if (itemType == 'folder') { -// return Folder.delete(item.id); -// } else { -// return Note.delete(item.id); -// } -// }).catch((error) => { -// this.log(error); -// }).then(() => { -// end(); -// }); -// }, -// autocomplete: autocompleteItems, -// }); - -// commands.push({ -// usage: 'ls [list-title]', -// alias: 'll', -// description: 'Lists items in [list-title].', -// action: function (args, end) { -// let folderTitle = args['list-title']; - -// let promise = null; - -// if (folderTitle == '..') { -// promise = Promise.resolve('root'); -// } else if (folderTitle && folderTitle != '.') { -// promise = Folder.loadByField('title', folderTitle); -// } else if (currentFolder) { -// promise = Promise.resolve(currentFolder); -// } else { -// promise = Promise.resolve('root'); -// } - -// promise.then((folder) => { -// let p = null -// let postfix = ''; -// if (folder === 'root') { -// p = Folder.all(); -// postfix = '/'; -// } else if (!folder) { -// throw new Error(_('Unknown list: "%s"', folderTitle)); -// } else { -// p = Note.previews(folder.id); -// } - -// return p.then((previews) => { -// for (let i = 0; i < previews.length; i++) { -// this.log(previews[i].title + postfix); -// } -// }); -// }).catch((error) => { -// this.log(error); -// }).then(() => { -// end(); -// }); -// }, -// autocomplete: autocompleteFolders, -// }); - -// commands.push({ -// usage: 'sync', -// description: 'Synchronizes with remote storage.', -// action: function (args, end) { -// synchronizer.start().catch((error) => { -// console.error(error); -// }).then(() => { -// end(); -// }); -// }, -// }); - -// for (let i = 0; i < commands.length; i++) { -// let c = commands[i]; -// let o = vorpal.command(c.usage, c.description); -// if (c.alias) { -// o.alias(c.alias); -// } -// if (c.autocomplete) { -// o.autocomplete({ -// data: c.autocomplete, -// }); -// } -// o.action(c.action); -// } - -// vorpal.delimiter(promptString()).show(); -// }); \ No newline at end of file diff --git a/CliClient/app/main.js b/CliClient/app/main.js new file mode 100644 index 000000000..a264d4b6f --- /dev/null +++ b/CliClient/app/main.js @@ -0,0 +1,384 @@ +require('source-map-support').install(); +require('babel-plugin-transform-runtime'); + +import { FileApi } from 'src/file-api.js'; +import { FileApiDriverOneDrive } from 'src/file-api-driver-onedrive.js'; +import { Database } from 'src/database.js'; +import { DatabaseDriverNode } from 'src/database-driver-node.js'; +import { BaseModel } from 'src/base-model.js'; +import { Folder } from 'src/models/folder.js'; +import { Note } from 'src/models/note.js'; +import { Setting } from 'src/models/setting.js'; +import { Synchronizer } from 'src/synchronizer.js'; +import { uuid } from 'src/uuid.js'; +import { sprintf } from 'sprintf-js'; +import { _ } from 'src/locale.js'; +import os from 'os'; +import fs from 'fs-extra'; + + +let db = new Database(new DatabaseDriverNode()); +let synchronizer_ = null; +const vorpal = require('vorpal')(); +const APPNAME = 'joplin'; + +async function main() { + let dataDir = os.homedir() + '/.local/share/' + APPNAME; + await fs.mkdirp(dataDir, 0o755); + + await db.open({ name: dataDir + '/database.sqlite' }); + BaseModel.db_ = db; + await Setting.load(); + + let commands = []; + let currentFolder = null; + + async function synchronizer(remoteBackend) { + if (synchronizer_) return synchronizer_; + + let fileApi = null; + + if (remoteBackend == 'onedrive') { + const CLIENT_ID = 'e09fc0de-c958-424f-83a2-e56a721d331b'; + const CLIENT_SECRET = 'JA3cwsqSGHFtjMwd5XoF5L5'; + + let driver = new FileApiDriverOneDrive(CLIENT_ID, CLIENT_SECRET); + let auth = Setting.value('sync.onedrive.auth'); + + if (auth) { + auth = JSON.parse(auth); + } else { + auth = await driver.api().oauthDance(); + Setting.setValue('sync.onedrive.auth', JSON.stringify(auth)); + } + + driver.api().setAuth(auth); + + let appDir = await driver.api().appDirectory(); + fileApi = new FileApi(appDir, driver); + } else { + throw new Error('Unknown backend: ' . remoteBackend); + } + + synchronizer_ = new Synchronizer(db, fileApi); + + return synchronizer_; + } + + let s = await synchronizer(); + return; + + function switchCurrentFolder(folder) { + currentFolder = folder; + updatePrompt(); + } + + function promptString() { + let path = '~'; + if (currentFolder) { + path += '/' + currentFolder.title; + } + return 'joplin:' + path + '$ '; + } + + function updatePrompt() { + vorpal.delimiter(promptString()); + } + + // For now, to go around this issue: https://github.com/dthree/vorpal/issues/114 + function quotePromptArg(s) { + if (s.indexOf(' ') >= 0) { + return '"' + s + '"'; + } + return s; + } + + function autocompleteFolders() { + return Folder.all().then((folders) => { + let output = []; + for (let i = 0; i < folders.length; i++) { + output.push(quotePromptArg(folders[i].title)); + } + output.push('..'); + output.push('.'); + return output; + }); + } + + function autocompleteItems() { + let promise = null; + if (!currentFolder) { + promise = Folder.all(); + } else { + promise = Note.previews(currentFolder.id); + } + + return promise.then((items) => { + let output = []; + for (let i = 0; i < items.length; i++) { + output.push(quotePromptArg(items[i].title)); + } + return output; + }); + } + + process.stdin.on('keypress', (_, key) => { + if (key && key.name === 'return') { + updatePrompt(); + } + + if (key.name === 'tab') { + vorpal.ui.imprint(); + vorpal.log(vorpal.ui.input()); + } + }); + + commands.push({ + usage: 'cd ', + description: 'Moved to [list-title] - all further operations will happen within this list. Use `cd ..` to go back one level.', + action: function (args, end) { + let folderTitle = args['list-title']; + + if (folderTitle == '..') { + switchCurrentFolder(null); + end(); + return; + } + + if (folderTitle == '.') { + end(); + return; + } + + Folder.loadByField('title', folderTitle).then((folder) => { + switchCurrentFolder(folder); + end(); + }); + }, + autocomplete: autocompleteFolders, + }); + + commands.push({ + usage: 'mklist ', + alias: 'mkdir', + description: 'Creates a new list', + action: function (args, end) { + Folder.save({ title: args['list-title'] }).catch((error) => { + this.log(error); + }).then((folder) => { + switchCurrentFolder(folder); + end(); + }); + }, + }); + + commands.push({ + usage: 'mknote ', + alias: 'touch', + description: 'Creates a new note', + action: function (args, end) { + if (!currentFolder) { + this.log('Notes can only be created within a list.'); + end(); + return; + } + + let note = { + title: args['note-title'], + parent_id: currentFolder.id, + }; + Note.save(note).catch((error) => { + this.log(error); + }).then((note) => { + end(); + }); + }, + }); + + commands.push({ + usage: 'set [prop-value]', + description: 'Sets the given of the given item.', + action: function (args, end) { + let promise = null; + let title = args['item-title']; + let propName = args['prop-name']; + let propValue = args['prop-value']; + if (!propValue) propValue = ''; + + if (!currentFolder) { + promise = Folder.loadByField('title', title); + } else { + promise = Folder.loadNoteByField(currentFolder.id, 'title', title); + } + + promise.then((item) => { + if (!item) { + this.log(_('No item with title "%s" found.', title)); + end(); + return; + } + + let newItem = { + id: item.id, + type_: item.type_, + }; + newItem[propName] = propValue; + let ItemClass = BaseItem.itemClass(); + return ItemClass.save(newItem); + }).catch((error) => { + this.log(error); + }).then(() => { + end(); + }); + }, + autocomplete: autocompleteItems, + }); + + commands.push({ + usage: 'cat ', + description: 'Displays the given item data.', + action: function (args, end) { + let title = args['item-title']; + + let promise = null; + if (!currentFolder) { + promise = Folder.loadByField('title', title); + } else { + promise = Folder.loadNoteByField(currentFolder.id, 'title', title); + } + + promise.then((item) => { + if (!item) { + this.log(_('No item with title "%s" found.', title)); + end(); + return; + } + + if (!currentFolder) { + this.log(Folder.serialize(item)); + } else { + this.log(Note.serialize(item)); + } + }).catch((error) => { + this.log(error); + }).then(() => { + end(); + }); + }, + autocomplete: autocompleteItems, + }); + + commands.push({ + usage: 'rm ', + description: 'Deletes the given item. For a list, all the notes within that list will be deleted.', + action: function (args, end) { + let title = args['item-title']; + + let promise = null; + let itemType = currentFolder ? 'note' : 'folder'; + if (itemType == 'folder') { + promise = Folder.loadByField('title', title); + } else { + promise = Folder.loadNoteByField(currentFolder.id, 'title', title); + } + + promise.then((item) => { + if (!item) { + this.log(_('No item with title "%s" found.', title)); + end(); + return; + } + + if (itemType == 'folder') { + return Folder.delete(item.id); + } else { + return Note.delete(item.id); + } + }).catch((error) => { + this.log(error); + }).then(() => { + end(); + }); + }, + autocomplete: autocompleteItems, + }); + + commands.push({ + usage: 'ls [list-title]', + alias: 'll', + description: 'Lists items in [list-title].', + action: function (args, end) { + let folderTitle = args['list-title']; + + let promise = null; + + if (folderTitle == '..') { + promise = Promise.resolve('root'); + } else if (folderTitle && folderTitle != '.') { + promise = Folder.loadByField('title', folderTitle); + } else if (currentFolder) { + promise = Promise.resolve(currentFolder); + } else { + promise = Promise.resolve('root'); + } + + promise.then((folder) => { + let p = null + let postfix = ''; + if (folder === 'root') { + p = Folder.all(); + postfix = '/'; + } else if (!folder) { + throw new Error(_('Unknown list: "%s"', folderTitle)); + } else { + p = Note.previews(folder.id); + } + + return p.then((previews) => { + for (let i = 0; i < previews.length; i++) { + this.log(previews[i].title + postfix); + } + }); + }).catch((error) => { + this.log(error); + }).then(() => { + end(); + }); + }, + autocomplete: autocompleteFolders, + }); + + commands.push({ + usage: 'sync', + description: 'Synchronizes with remote storage.', + action: function (args, end) { + synchronizer('onedrive').then((s) => { + return s.start(); + }).catch((error) => { + console.error(error); + }).then(() => { + end(); + }); + }, + }); + + for (let i = 0; i < commands.length; i++) { + let c = commands[i]; + let o = vorpal.command(c.usage, c.description); + if (c.alias) { + o.alias(c.alias); + } + if (c.autocomplete) { + o.autocomplete({ + data: c.autocomplete, + }); + } + o.action(c.action); + } + + vorpal.delimiter(promptString()).show(); +} + +main().catch((error) => { + console.error('Fatal error: ', error); +}); \ No newline at end of file diff --git a/CliClient/app/onedrive-server.js b/CliClient/app/onedrive-server.js index 2d6734cc6..eecbe25bf 100644 --- a/CliClient/app/onedrive-server.js +++ b/CliClient/app/onedrive-server.js @@ -1,5 +1,6 @@ require('source-map-support').install(); require('babel-plugin-transform-runtime'); + import { OneDriveApi } from 'src/onedrive-api.js'; const fetch = require('node-fetch'); diff --git a/CliClient/app/test-onedrive.js b/CliClient/app/test-onedrive.js index 5fd6cd354..29cef79d4 100644 --- a/CliClient/app/test-onedrive.js +++ b/CliClient/app/test-onedrive.js @@ -50,10 +50,19 @@ async function main() { - let driver = new FileApiDriverOneDrive(config.oneDriveToken); + let driver = new FileApiDriverOneDrive('e09fc0de-c958-424f-83a2-e56a721d331b', 'JA3cwsqSGHFtjMwd5XoF5L5'); + driver.api().setToken(config.oneDriveToken); + + //config.oneDriveToken); let api = new FileApi('/joplin', driver); - await api.delete('eraseme.txt'); + let appDir = await driver.api().execJson('GET', '/drive/special/approot'); + + console.info(appDir); + + // /drive/special/approot + + // await api.delete('eraseme.txt'); // let result = await api.list(); // console.info(result); @@ -63,8 +72,8 @@ async function main() { - let content = await api.get('aaaaaaaaaaaaaaaaa.txt'); - console.info(content); + // let content = await api.get('aaaaaaaaaaaaaaaaa.txt'); + // console.info(content); diff --git a/CliClient/package.json b/CliClient/package.json index 0575d9ffc..6776e8c54 100644 --- a/CliClient/package.json +++ b/CliClient/package.json @@ -18,6 +18,7 @@ "query-string": "4.3.4", "react": "16.0.0-alpha.6", "sax": "^1.2.2", + "server-destroy": "^1.0.1", "source-map-support": "^0.4.15", "sprintf-js": "^1.1.1", "sqlite3": "^3.1.8", diff --git a/CliClient/run.sh b/CliClient/run.sh index a2078326e..8cb91b812 100755 --- a/CliClient/run.sh +++ b/CliClient/run.sh @@ -4,6 +4,6 @@ CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" rm -f "$CLIENT_DIR/app/src" ln -s "$CLIENT_DIR/../ReactNativeClient/src" "$CLIENT_DIR/app" -npm run build && NODE_PATH="$CLIENT_DIR/build/" node build/cmd.js +npm run build && NODE_PATH="$CLIENT_DIR/build/" node build/main.js #npm run build && NODE_PATH="$CLIENT_DIR/build/" node build/test-onedrive.js #npm run build && NODE_PATH="$CLIENT_DIR/build/" node build/onedrive-server.js \ No newline at end of file diff --git a/ReactNativeClient/src/file-api-driver-onedrive.js b/ReactNativeClient/src/file-api-driver-onedrive.js index beb45971e..b5faad68e 100644 --- a/ReactNativeClient/src/file-api-driver-onedrive.js +++ b/ReactNativeClient/src/file-api-driver-onedrive.js @@ -4,9 +4,12 @@ import { OneDriveApi } from 'src/onedrive-api.js'; class FileApiDriverOneDrive { - constructor(token) { - this.api_ = new OneDriveApi('e09fc0de-c958-424f-83a2-e56a721d331b'); - this.api_.setToken(token); + constructor(clientId, clientSecret) { + this.api_ = new OneDriveApi(clientId, clientSecret); + } + + api() { + return this.api_; } listReturnsFullPath() { @@ -20,7 +23,7 @@ class FileApiDriverOneDrive { } makePath_(path) { - return '/drive/root:' + path; + return path; } makeItems_(odItems) { @@ -41,7 +44,12 @@ class FileApiDriverOneDrive { } async stat(path) { - let item = await this.api_.execJson('GET', this.makePath_(path), this.itemFilter_()); + try { + let item = await this.api_.execJson('GET', this.makePath_(path), this.itemFilter_()); + } catch (error) { + if (error.error.code == 'itemNotFound') return null; + throw error; + } return this.makeItem_(item); } @@ -59,8 +67,14 @@ class FileApiDriverOneDrive { return this.makeItems_(items.value); } - get(path) { - return this.api_.execText('GET', this.makePath_(path) + ':/content'); + async get(path) { + try { + let content = await this.api_.execText('GET', this.makePath_(path) + ':/content'); + } catch (error) { + if (error.error.code == 'itemNotFound') return null; + throw error; + } + return content; } mkdir(path) { diff --git a/ReactNativeClient/src/file-api.js b/ReactNativeClient/src/file-api.js index 144e83433..f941ce8a8 100644 --- a/ReactNativeClient/src/file-api.js +++ b/ReactNativeClient/src/file-api.js @@ -7,6 +7,10 @@ class FileApi { this.driver_ = driver; } + dlog(s) { + console.info('FileApi: ' + s); + } + fullPath_(path) { let output = this.baseDir_; if (path != '') output += '/' + path; @@ -31,32 +35,35 @@ class FileApi { return output; } - listDirectories() { - return this.driver_.list(this.fullPath_('')).then((items) => { - let output = []; - for (let i = 0; i < items.length; i++) { - if (items[i].isDir) output.push(this.scopeItemToBaseDir_(items[i])); - } - return output; - }); - } + // listDirectories() { + // return this.driver_.list(this.fullPath_('')).then((items) => { + // let output = []; + // for (let i = 0; i < items.length; i++) { + // if (items[i].isDir) output.push(this.scopeItemToBaseDir_(items[i])); + // } + // return output; + // }); + // } list() { + this.dlog('list'); return this.driver_.list(this.baseDir_).then((items) => { return this.scopeItemsToBaseDir_(items); }); } setTimestamp(path, timestamp) { + this.dlog('setTimestamp ' + path); return this.driver_.setTimestamp(this.fullPath_(path), timestamp); } - mkdir(path) { - console.info('mkdir ' + path); - return this.driver_.mkdir(this.fullPath_(path)); - } + // mkdir(path) { + // this.dlog('delete ' + path); + // return this.driver_.mkdir(this.fullPath_(path)); + // } stat(path) { + this.dlog('stat ' + path); return this.driver_.stat(this.fullPath_(path)).then((output) => { if (!output) return output; output.path = path; @@ -65,20 +72,24 @@ class FileApi { } get(path) { + this.dlog('get ' + path); return this.driver_.get(this.fullPath_(path)); } put(path, content) { + this.dlog('put ' + path); return this.driver_.put(this.fullPath_(path), content); } delete(path) { + this.dlog('delete ' + path); return this.driver_.delete(this.fullPath_(path)); } - move(oldPath, newPath) { - return this.driver_.move(this.fullPath_(oldPath), this.fullPath_(newPath)); - } + // move(oldPath, newPath) { + // this.dlog('move ' + path); + // return this.driver_.move(this.fullPath_(oldPath), this.fullPath_(newPath)); + // } format() { return this.driver_.format(); diff --git a/ReactNativeClient/src/models/setting.js b/ReactNativeClient/src/models/setting.js index 678be35d0..b35c23f5f 100644 --- a/ReactNativeClient/src/models/setting.js +++ b/ReactNativeClient/src/models/setting.js @@ -133,6 +133,7 @@ Setting.defaults_ = { 'sync.lastRevId': { value: 0, type: 'int' }, // DEPRECATED 'sync.lastUpdateTime': { value: 0, type: 'int' }, 'sync.conflictFolderId': { value: '', type: 'string' }, + 'sync.onedrive.auth': { value: '', type: 'string' }, }; export { Setting }; \ No newline at end of file diff --git a/ReactNativeClient/src/onedrive-api.js b/ReactNativeClient/src/onedrive-api.js index 08adc9cb5..ece92ebb1 100644 --- a/ReactNativeClient/src/onedrive-api.js +++ b/ReactNativeClient/src/onedrive-api.js @@ -1,4 +1,9 @@ const fetch = require('node-fetch'); +const tcpPortUsed = require('tcp-port-used'); +const http = require("http"); +const urlParser = require("url"); +const FormData = require('form-data'); +const enableServerDestroy = require('server-destroy'); import { stringify } from 'query-string'; class OneDriveApi { @@ -6,10 +11,19 @@ class OneDriveApi { constructor(clientId, clientSecret) { this.clientId_ = clientId; this.clientSecret_ = clientSecret; + this.auth_ = null; } - setToken(token) { - this.token_ = token; + tokenBaseUrl() { + return 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; + } + + setAuth(auth) { + this.auth_ = auth; + } + + token() { + return this.auth_ ? this.auth_.access_token : null; } clientId() { @@ -20,10 +34,15 @@ class OneDriveApi { return this.clientSecret_; } - possibleOAuthFlowPorts() { + possibleOAuthDancePorts() { return [1917, 9917, 8917]; } + async appDirectory() { + let r = await this.execJson('GET', '/drive/special/approot'); + return r.parentReference.path + '/' + r.name; + } + authCodeUrl(redirectUri) { let query = { client_id: this.clientId_, @@ -40,10 +59,6 @@ class OneDriveApi { if (!options) options = {}; if (!options.headers) options.headers = {}; - if (this.token_) { - options.headers['Authorization'] = 'bearer ' + this.token_; - } - if (method != 'GET') { options.method = method; } @@ -62,13 +77,23 @@ class OneDriveApi { console.info(method + ' ' + url); console.info(data); - let response = await fetch(url, options); - if (!response.ok) { - let error = await response.json(); - throw error; - } + while (true) { + options.headers['Authorization'] = 'bearer ' + this.token(); - return response; + let response = await fetch(url, options); + if (!response.ok) { + let error = await response.json(); + + if (error && error.error && error.error.code == 'InvalidAuthenticationToken') { + await this.refreshAccessToken(); + continue; + } else { + throw error; + } + } + + return response; + } } async execJson(method, path, query, data) { @@ -83,6 +108,114 @@ class OneDriveApi { return output; } + async refreshAccessToken() { + if (!this.auth_) throw new Error('Cannot refresh token: authentication data is missing'); + + let body = new FormData(); + body.append('client_id', this.clientId()); + body.append('client_secret', this.clientSecret()); + body.append('refresh_token', this.auth_.refresh_token); + body.append('redirect_uri', 'http://localhost:1917'); + body.append('grant_type', 'refresh_token'); + + let options = { + method: 'POST', + body: body, + }; + + this.auth_ = null; + + let response = await fetch(this.tokenBaseUrl(), options); + if (!response.ok) { + let msg = await response.text(); + throw new Error(msg); + } + + this.auth_ = await response.json(); + + // POST https://login.microsoftonline.com/common/oauth2/v2.0/token + // Content-Type: application/x-www-form-urlencoded + + // client_id={client_id}&redirect_uri={redirect_uri}&client_secret={client_secret} + // &refresh_token={refresh_token}&grant_type=refresh_token + } + + async oauthDance() { + this.auth_ = null; + + let ports = this.possibleOAuthDancePorts(); + let port = null; + for (let i = 0; i < ports.length; i++) { + let inUse = await tcpPortUsed.check(ports[i]); + if (!inUse) { + port = ports[i]; + break; + } + } + + if (!port) throw new Error('All potential ports are in use - please report the issue at https://github.com/laurent22/joplin'); + + let authCodeUrl = this.authCodeUrl('http://localhost:' + port); + + return new Promise((resolve, reject) => { + let server = http.createServer(); + let errorMessage = null; + + server.on('request', (request, response) => { + const query = urlParser.parse(request.url, true).query; + + function writeResponse(code, message) { + response.writeHead(code, {"Content-Type": "text/html"}); + response.write(message); + response.end(); + } + + if (!query.code) return writeResponse(400, '"code" query parameter is missing'); + + let body = new FormData(); + body.append('client_id', this.clientId()); + body.append('client_secret', this.clientSecret()); + body.append('code', query.code ? query.code : ''); + body.append('redirect_uri', 'http://localhost:' + port.toString()); + body.append('grant_type', 'authorization_code'); + + let options = { + method: 'POST', + body: body, + }; + + fetch(this.tokenBaseUrl(), options).then((r) => { + if (!r.ok) { + errorMessage = 'Could not retrieve auth code: ' + r.status + ': ' + r.statusText; + writeResponse(400, errorMessage); + server.destroy(); + return; + } + + return r.json().then((json) => { + this.auth_ = json; + writeResponse(200, 'The application has been authorised - you may now close this browser tab.'); + server.destroy(); + }); + }); + }); + + server.on('close', () => { + if (errorMessage) { + reject(new Error(errorMessage)); + } else { + resolve(this.auth_); + } + }); + + server.listen(port); + + enableServerDestroy(server); + + console.info('Please open this URL in your browser to authentify the application: ' + authCodeUrl); + }); + } + } export { OneDriveApi }; \ No newline at end of file