diff --git a/CliClient/app/cmd.js b/CliClient/app/cmd.js index f7572b040f..e4867aa460 100644 --- a/CliClient/app/cmd.js +++ b/CliClient/app/cmd.js @@ -14,111 +14,332 @@ import { Synchronizer } from 'src/synchronizer.js'; import { uuid } from 'src/uuid.js'; import { sprintf } from 'sprintf-js'; import { _ } from 'src/locale.js'; -import { NoteFolderService } from 'src/services/note-folder-service.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(); - - -// import { ItemSyncTime } from 'src/models/item-sync-time.js'; - -// const vorpal = require('vorpal')(); - -// let db = new Database(new DatabaseDriverNode()); -// db.setDebugEnabled(false); -// db.open({ name: '/home/laurent/Temp/test.sqlite3' }).then(() => { -// BaseModel.db_ = db; - -// return ItemSyncTime.setTime(123, 789); - -// }).then((r) => { -// console.info(r); -// }).catch((error) => { -// console.error(error); -// }); - -// // let fileDriver = new FileApiDriverLocal(); -// // let fileApi = new FileApi('/home/laurent/Temp/TestImport', fileDriver); -// // let synchronizer = new Synchronizer(db, fileApi); - - -// let fileDriver = new FileApiDriverMemory(); -// let fileApi = new FileApi('/root', fileDriver); -// let synchronizer = new Synchronizer(db, fileApi); - - -// fileApi.mkdir('test').then(() => { -// return fileApi.mkdir('test2'); -// }).then(() => { -// return fileApi.put('test/un', 'abcd1111').then(fileApi.put('test/deux', 'abcd2222')); -// }).then(() => { -// return fileApi.list(); -// }).then((items) => { -// //console.info(items); -// }).then(() => { -// return fileApi.delete('test/un'); -// }).then(() => { -// return fileApi.get('test/deux').then((content) => { console.info(content); }); -// }).then(() => { -// return fileApi.list('test', true); -// }).then((items) => { -// console.info(items); -// }).catch((error) => { -// console.error(error); -// }).then(() => { -// process.exit(); -// }); - - - - - - - -// db.open({ name: '/home/laurent/Temp/test.sqlite3' }).then(() => { // BaseModel.db_ = db; // }).then(() => { // return Setting.load(); @@ -221,7 +442,7 @@ let synchronizer = new Synchronizer(db, fileApi); // alias: 'mkdir', // description: 'Creates a new list', // action: function (args, end) { -// NoteFolderService.save('folder', { title: args['list-title'] }).catch((error) => { +// Folder.save({ title: args['list-title'] }).catch((error) => { // this.log(error); // }).then((folder) => { // switchCurrentFolder(folder); @@ -245,7 +466,7 @@ let synchronizer = new Synchronizer(db, fileApi); // title: args['note-title'], // parent_id: currentFolder.id, // }; -// NoteFolderService.save('note', note).catch((error) => { +// Note.save(note).catch((error) => { // this.log(error); // }).then((note) => { // end(); @@ -276,10 +497,13 @@ let synchronizer = new Synchronizer(db, fileApi); // return; // } -// let newItem = Object.assign({}, item); +// let newItem = { +// id: item.id, +// type_: item.type_, +// }; // newItem[propName] = propValue; -// let itemType = currentFolder ? 'note' : 'folder'; -// return NoteFolderService.save(itemType, newItem, item); +// let ItemClass = BaseItem.itemClass(); +// return ItemClass.save(newItem); // }).catch((error) => { // this.log(error); // }).then(() => { diff --git a/CliClient/app/onedrive-server.js b/CliClient/app/onedrive-server.js new file mode 100644 index 0000000000..2d6734cc6d --- /dev/null +++ b/CliClient/app/onedrive-server.js @@ -0,0 +1,70 @@ +require('source-map-support').install(); +require('babel-plugin-transform-runtime'); +import { OneDriveApi } from 'src/onedrive-api.js'; + +const fetch = require('node-fetch'); +const tcpPortUsed = require('tcp-port-used'); +const http = require("http"); +const urlParser = require("url"); +const FormData = require('form-data'); + +async function main() { + let api = new OneDriveApi('e09fc0de-c958-424f-83a2-e56a721d331b', 'FAPky27RNWYuXWwThgkQE47'); + + let ports = api.possibleOAuthFlowPorts(); + 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 = api.authCodeUrl('http://localhost:' + port); + + let server = http.createServer((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 url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; + let body = new FormData(); + body.append('client_id', api.clientId()); + body.append('client_secret', api.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(url, options).then((r) => { + if (!r.ok) { + let msg = 'Could not retrieve auth code: ' + r.status + ': ' + r.statusText; + console.info(msg); + return writeResponse(400, msg); + } + return r.json().then((json) => { + console.info(json); + return writeResponse(200, 'The application has been authorised - you may now close this browser tab.'); + }); + }); + }); + + server.listen(port); + + console.info(authCodeUrl); +} + +main(); \ No newline at end of file diff --git a/CliClient/app/test-onedrive.js b/CliClient/app/test-onedrive.js index 0b307eb406..5fd6cd354f 100644 --- a/CliClient/app/test-onedrive.js +++ b/CliClient/app/test-onedrive.js @@ -1,9 +1,15 @@ require('source-map-support').install(); require('babel-plugin-transform-runtime'); + +import { OneDriveApi } from 'src/onedrive-api.js'; + const MicrosoftGraph = require("@microsoft/microsoft-graph-client"); const fs = require('fs-extra'); const path = require('path'); +import { FileApiDriverOneDrive } from 'src/file-api-driver-onedrive.js'; +import { FileApi } from 'src/file-api.js'; + function configContent() { const configFilePath = path.dirname(__dirname) + '/config.json'; return fs.readFile(configFilePath, 'utf8').then((content) => { @@ -13,18 +19,84 @@ function configContent() { async function main() { - let config = await configContent(); + let config = await configContent(); - var token = ''; - var client = MicrosoftGraph.Client.init({ - authProvider: (done) => { - done(null, config.oneDriveToken); - } - }); + const fetch = require('node-fetch'); + + let options = { + headers: { 'Authorization': 'bearer ' + config.oneDriveToken }, + }; + + // let api = new OneDriveApi('a'); + // api.setToken(config.oneDriveToken); + // let r = await api.execText('GET', '/drive/root:/joplin/aaaaaaaaaaaaaaaaa.txt:/content'); + // console.info(r); + + + + + //console.info(options); + + // let response = await fetch('https://graph.microsoft.com/v1.0/drive/root:/joplin/aaaaaaaaaaaaaaaaa.txt:/content', options); + + // console.info(response.ok); + // console.info(response.status); + // console.info(response.statusText); + // console.info(response.headers.get('Location')); + + // let responseText = await response.text(); + // console.info(responseText); + + + + + let driver = new FileApiDriverOneDrive(config.oneDriveToken); + let api = new FileApi('/joplin', driver); + + await api.delete('eraseme.txt'); + + // let result = await api.list(); + // console.info(result); + + //await api.put('aaaaaaaaaaaaaaaaa.txt', 'AAAAAAAAAAAA MOD'); + //onsole.info(content); + + + + let content = await api.get('aaaaaaaaaaaaaaaaa.txt'); + console.info(content); + + + + // let r = await api.setTimestamp('aaaaaaaaaaaaaaaaa.txt', 1498061000000); + // console.info(r); + + + // console.info('=============='); + + // let stat = await api.stat('aaaaaaaaaaaaaaaaa.txt'); + // console.info(stat); + + // console.info(content); + + +// // const fetch = require('node-fetch'); +// let content = await api.get('aaaaaaaaaaaaaaaaa.txt'); +// console.info('CONTENT', content); + + // var token = ''; + // var client = MicrosoftGraph.Client.init({ + // authProvider: (done) => { + // done(null, config.oneDriveToken); + // } + // }); // LIST ITEMS - // client.api('/drive/items/9ADA0EADFA073D0A%21109/children').get((err, res) => { + //client.api('/drive/items/9ADA0EADFA073D0A%21109/children').get((err, res) => { + //client.api('/drive/items/9ADA0EADFA073D0A%21109/children').get((err, res) => { + //client.api('/drive/root:/joplin:/children').get((err, res) => { + // client.api('/drive/root:/.:/children').get((err, res) => { // console.log(err, res); // }); @@ -48,9 +120,9 @@ async function main() { // GET ITEM METADATA - client.api('/drive/items/9ADA0EADFA073D0A%21110?select=name,lastModifiedDateTime').get((err, res) => { - console.log(err, res); - }); + // client.api('/drive/items/9ADA0EADFA073D0A%21110?select=name,lastModifiedDateTime').get((err, res) => { + // console.log(err, res); + // }); } main().catch((error) => { diff --git a/CliClient/package.json b/CliClient/package.json index c6d27517a8..0575d9ffcc 100644 --- a/CliClient/package.json +++ b/CliClient/package.json @@ -22,6 +22,7 @@ "sprintf-js": "^1.1.1", "sqlite3": "^3.1.8", "string-to-stream": "^1.1.0", + "tcp-port-used": "^0.1.2", "uuid": "^3.0.1", "vorpal": "^1.12.0" }, diff --git a/CliClient/run.sh b/CliClient/run.sh index a6c9d5cfc4..a2078326eb 100755 --- a/CliClient/run.sh +++ b/CliClient/run.sh @@ -4,5 +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/test-onedrive.js \ No newline at end of file +npm run build && NODE_PATH="$CLIENT_DIR/build/" node build/cmd.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-memory.js b/ReactNativeClient/src/file-api-driver-memory.js index 4a6b66f773..c267c70e7a 100644 --- a/ReactNativeClient/src/file-api-driver-memory.js +++ b/ReactNativeClient/src/file-api-driver-memory.js @@ -6,8 +6,8 @@ class FileApiDriverMemory { this.items_ = []; } - currentTimestamp() { - return Math.round((new Date()).getTime() / 1000); + listReturnsFullPath() { + return true; } itemIndexByPath(path) { diff --git a/ReactNativeClient/src/file-api-driver-onedrive.js b/ReactNativeClient/src/file-api-driver-onedrive.js new file mode 100644 index 0000000000..beb45971e3 --- /dev/null +++ b/ReactNativeClient/src/file-api-driver-onedrive.js @@ -0,0 +1,91 @@ +import moment from 'moment'; +import { time } from 'src/time-utils.js'; +import { OneDriveApi } from 'src/onedrive-api.js'; + +class FileApiDriverOneDrive { + + constructor(token) { + this.api_ = new OneDriveApi('e09fc0de-c958-424f-83a2-e56a721d331b'); + this.api_.setToken(token); + } + + listReturnsFullPath() { + return false; + } + + itemFilter_() { + return { + select: 'name,file,folder,fileSystemInfo', + } + } + + makePath_(path) { + return '/drive/root:' + path; + } + + makeItems_(odItems) { + let output = []; + for (let i = 0; i < odItems.length; i++) { + output.push(this.makeItem_(odItems[i])); + } + return output; + } + + makeItem_(odItem) { + return { + path: odItem.name, + isDir: ('folder' in odItem), + created_time: moment(odItem.fileSystemInfo.createdDateTime, 'YYYY-MM-DDTHH:mm:ss.SSSZ').format('x'), + updated_time: moment(odItem.fileSystemInfo.lastModifiedDateTime, 'YYYY-MM-DDTHH:mm:ss.SSSZ').format('x'), + }; + } + + async stat(path) { + let item = await this.api_.execJson('GET', this.makePath_(path), this.itemFilter_()); + return this.makeItem_(item); + } + + async setTimestamp(path, timestamp) { + let body = { + fileSystemInfo: { + lastModifiedDateTime: moment.unix(timestamp / 1000).utc().format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z', + } + }; + await this.api_.exec('PATCH', this.makePath_(path), null, body); + } + + async list(path) { + let items = await this.api_.execJson('GET', this.makePath_(path) + ':/children', this.itemFilter_()); + return this.makeItems_(items.value); + } + + get(path) { + return this.api_.execText('GET', this.makePath_(path) + ':/content'); + } + + mkdir(path) { + throw new Error('Not implemented'); + } + + put(path, content) { + let options = { + headers: { 'Content-Type': 'text/plain' }, + }; + return this.api_.exec('PUT', this.makePath_(path) + ':/content', null, content, options); + } + + delete(path) { + return this.api_.exec('DELETE', this.makePath_(path)); + } + + move(oldPath, newPath) { + throw new Error('Not implemented'); + } + + format() { + throw new Error('Not implemented'); + } + +} + +export { FileApiDriverOneDrive }; \ No newline at end of file diff --git a/ReactNativeClient/src/file-api.js b/ReactNativeClient/src/file-api.js index 8ed5007159..144e83433a 100644 --- a/ReactNativeClient/src/file-api.js +++ b/ReactNativeClient/src/file-api.js @@ -14,12 +14,16 @@ class FileApi { } scopeItemToBaseDir_(item) { + if (!this.driver_.listReturnsFullPath()) return item; + let output = Object.assign({}, item); output.path = item.path.substr(this.baseDir_.length + 1); return output; } scopeItemsToBaseDir_(items) { + if (!this.driver_.listReturnsFullPath()) return items; + let output = []; for (let i = 0; i < items.length; i++) { output.push(this.scopeItemToBaseDir_(items[i])); diff --git a/ReactNativeClient/src/onedrive-api.js b/ReactNativeClient/src/onedrive-api.js new file mode 100644 index 0000000000..08adc9cb50 --- /dev/null +++ b/ReactNativeClient/src/onedrive-api.js @@ -0,0 +1,88 @@ +const fetch = require('node-fetch'); +import { stringify } from 'query-string'; + +class OneDriveApi { + + constructor(clientId, clientSecret) { + this.clientId_ = clientId; + this.clientSecret_ = clientSecret; + } + + setToken(token) { + this.token_ = token; + } + + clientId() { + return this.clientId_; + } + + clientSecret() { + return this.clientSecret_; + } + + possibleOAuthFlowPorts() { + return [1917, 9917, 8917]; + } + + authCodeUrl(redirectUri) { + let query = { + client_id: this.clientId_, + scope: 'files.readwrite offline_access', + response_type: 'code', + redirect_uri: redirectUri, + }; + return 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?' + stringify(query); + } + + async exec(method, path, query = null, data = null, options = null) { + method = method.toUpperCase(); + + if (!options) options = {}; + if (!options.headers) options.headers = {}; + + if (this.token_) { + options.headers['Authorization'] = 'bearer ' + this.token_; + } + + if (method != 'GET') { + options.method = method; + } + + if (method == 'PATCH') { + options.headers['Content-Type'] = 'application/json'; + if (data) data = JSON.stringify(data); + } + + let url = 'https://graph.microsoft.com/v1.0' + path; + + if (query) url += '?' + stringify(query); + + if (data) options.body = data; + + console.info(method + ' ' + url); + console.info(data); + + let response = await fetch(url, options); + if (!response.ok) { + let error = await response.json(); + throw error; + } + + return response; + } + + async execJson(method, path, query, data) { + let response = await this.exec(method, path, query, data); + let output = await response.json(); + return output; + } + + async execText(method, path, query, data) { + let response = await this.exec(method, path, query, data); + let output = await response.text(); + return output; + } + +} + +export { OneDriveApi }; \ No newline at end of file