mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-27 08:21:03 +02:00
0765cf5955
- Joplin Server: Adds support for sharing a notebook - Desktop: Adds support for sharing a notebook with Joplin Server - Mobile: Adds support for reading and writing to a shared notebook (not possible to share a notebook) - Cli: Adds support for reading and writing to a shared notebook (not possible to share a notebook)
214 lines
6.1 KiB
JavaScript
214 lines
6.1 KiB
JavaScript
const Logger = require('./Logger').default;
|
|
const shim = require('./shim').default;
|
|
const JoplinError = require('./JoplinError').default;
|
|
const time = require('./time').default;
|
|
const EventDispatcher = require('./EventDispatcher');
|
|
|
|
class DropboxApi {
|
|
constructor(options) {
|
|
this.logger_ = new Logger();
|
|
this.options_ = options;
|
|
this.authToken_ = null;
|
|
this.dispatcher_ = new EventDispatcher();
|
|
}
|
|
|
|
clientId() {
|
|
return this.options_.id;
|
|
}
|
|
|
|
clientSecret() {
|
|
return this.options_.secret;
|
|
}
|
|
|
|
setLogger(l) {
|
|
this.logger_ = l;
|
|
}
|
|
|
|
logger() {
|
|
return this.logger_;
|
|
}
|
|
|
|
authToken() {
|
|
return this.authToken_; // Without the "Bearer " prefix
|
|
}
|
|
|
|
on(eventName, callback) {
|
|
return this.dispatcher_.on(eventName, callback);
|
|
}
|
|
|
|
setAuthToken(v) {
|
|
this.authToken_ = v;
|
|
this.dispatcher_.dispatch('authRefreshed', this.authToken());
|
|
}
|
|
|
|
loginUrl() {
|
|
return `https://www.dropbox.com/oauth2/authorize?response_type=code&client_id=${this.clientId()}`;
|
|
}
|
|
|
|
baseUrl(endPointFormat) {
|
|
if (['content', 'api'].indexOf(endPointFormat) < 0) throw new Error(`Invalid end point format: ${endPointFormat}`);
|
|
return `https://${endPointFormat}.dropboxapi.com/2`;
|
|
}
|
|
|
|
requestToCurl_(url, options) {
|
|
const output = [];
|
|
output.push('curl');
|
|
if (options.method) output.push(`-X ${options.method}`);
|
|
if (options.headers) {
|
|
for (const n in options.headers) {
|
|
if (!options.headers.hasOwnProperty(n)) continue;
|
|
output.push(`${'-H ' + '\''}${n}: ${options.headers[n]}'`);
|
|
}
|
|
}
|
|
if (options.body) output.push(`${'--data ' + '"'}${options.body}"`);
|
|
output.push(url);
|
|
|
|
return output.join(' ');
|
|
}
|
|
|
|
async execAuthToken(authCode) {
|
|
const postData = {
|
|
code: authCode,
|
|
grant_type: 'authorization_code',
|
|
client_id: this.clientId(),
|
|
client_secret: this.clientSecret(),
|
|
};
|
|
|
|
let formBody = [];
|
|
for (const property in postData) {
|
|
const encodedKey = encodeURIComponent(property);
|
|
const encodedValue = encodeURIComponent(postData[property]);
|
|
formBody.push(`${encodedKey}=${encodedValue}`);
|
|
}
|
|
formBody = formBody.join('&');
|
|
|
|
const response = await shim.fetch('https://api.dropboxapi.com/oauth2/token', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
},
|
|
body: formBody,
|
|
});
|
|
|
|
const responseText = await response.text();
|
|
if (!response.ok) throw new Error(responseText);
|
|
return JSON.parse(responseText);
|
|
}
|
|
|
|
isTokenError(status, responseText) {
|
|
if (status === 401) return true;
|
|
if (responseText.indexOf('OAuth 2 access token is malformed') >= 0) return true;
|
|
// eg. Error: POST files/create_folder_v2: Error (400): Error in call to API function "files/create_folder_v2": Must provide HTTP header "Authorization" or URL parameter "authorization".
|
|
if (responseText.indexOf('Must provide HTTP header "Authorization"') >= 0) return true;
|
|
return false;
|
|
}
|
|
|
|
async exec(method, path = '', body = null, headers = null, options = null) {
|
|
if (headers === null) headers = {};
|
|
if (options === null) options = {};
|
|
if (!options.target) options.target = 'string';
|
|
|
|
const authToken = this.authToken();
|
|
|
|
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
|
|
|
|
const endPointFormat = ['files/upload', 'files/download'].indexOf(path) >= 0 ? 'content' : 'api';
|
|
|
|
if (endPointFormat === 'api') {
|
|
headers['Content-Type'] = 'application/json';
|
|
if (body && typeof body === 'object') body = JSON.stringify(body);
|
|
} else {
|
|
headers['Content-Type'] = 'application/octet-stream';
|
|
}
|
|
|
|
const fetchOptions = {};
|
|
fetchOptions.headers = headers;
|
|
fetchOptions.method = method;
|
|
if (options.path) fetchOptions.path = options.path;
|
|
if (body) fetchOptions.body = body;
|
|
|
|
const url = path.indexOf('https://') === 0 ? path : `${this.baseUrl(endPointFormat)}/${path}`;
|
|
|
|
let tryCount = 0;
|
|
|
|
while (true) {
|
|
try {
|
|
let response = null;
|
|
|
|
// console.info(this.requestToCurl_(url, fetchOptions));
|
|
|
|
// console.info(method + ' ' + url);
|
|
|
|
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
|
|
response = await shim.uploadBlob(url, fetchOptions);
|
|
} else if (options.target == 'string') {
|
|
response = await shim.fetch(url, fetchOptions);
|
|
} else {
|
|
// file
|
|
response = await shim.fetchBlob(url, fetchOptions);
|
|
}
|
|
|
|
const responseText = await response.text();
|
|
|
|
// console.info('Response: ' + responseText);
|
|
|
|
let responseJson_ = null;
|
|
const loadResponseJson = () => {
|
|
if (!responseText) return null;
|
|
if (responseJson_) return responseJson_;
|
|
try {
|
|
responseJson_ = JSON.parse(responseText);
|
|
} catch (error) {
|
|
return { error: responseText };
|
|
}
|
|
return responseJson_;
|
|
};
|
|
|
|
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
|
|
const newError = message => {
|
|
const json = loadResponseJson();
|
|
let code = '';
|
|
if (json && json.error_summary) {
|
|
code = json.error_summary;
|
|
}
|
|
|
|
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
|
|
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
|
|
const shortResponseText = (`${responseText}`).substr(0, 1024);
|
|
const error = new JoplinError(`${method} ${path}: ${message} (${response.status}): ${shortResponseText}`, code);
|
|
error.httpStatus = response.status;
|
|
return error;
|
|
};
|
|
|
|
if (!response.ok) {
|
|
if (this.isTokenError(response.status, responseText)) {
|
|
this.setAuthToken(null);
|
|
}
|
|
|
|
// When using fetchBlob we only get a string (not xml or json) back
|
|
if (options.target === 'file') throw newError('fetchBlob error');
|
|
|
|
throw newError('Error');
|
|
}
|
|
|
|
if (options.responseFormat === 'text') return responseText;
|
|
|
|
return loadResponseJson();
|
|
} catch (error) {
|
|
tryCount++;
|
|
if (error && typeof error.code === 'string' && error.code.indexOf('too_many_write_operations') >= 0) {
|
|
this.logger().warn(`too_many_write_operations ${tryCount}`);
|
|
if (tryCount >= 3) {
|
|
throw error;
|
|
}
|
|
await time.sleep(tryCount * 2);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = DropboxApi;
|