1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-03-26 21:12:59 +02:00

OneDrive support

This commit is contained in:
Laurent Cozic 2017-06-22 20:44:38 +01:00
parent 1d3a1dabab
commit 219f43bbaa
9 changed files with 631 additions and 80 deletions

View File

@ -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 <list-title>',
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 <list-title>',
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 <note-title>',
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 <item-title> <prop-name> [prop-value]',
description: 'Sets the given <prop-name> 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 <item-title>',
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 <item-title>',
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(() => {

View File

@ -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();

View File

@ -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) => {

View File

@ -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"
},

View File

@ -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
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

View File

@ -6,8 +6,8 @@ class FileApiDriverMemory {
this.items_ = [];
}
currentTimestamp() {
return Math.round((new Date()).getTime() / 1000);
listReturnsFullPath() {
return true;
}
itemIndexByPath(path) {

View File

@ -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 };

View File

@ -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]));

View File

@ -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 };