1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-30 10:36:35 +02:00
joplin/packages/lib/DropboxApi.js

214 lines
6.1 KiB
JavaScript
Raw Normal View History

2020-11-05 18:58:23 +02:00
const Logger = require('./Logger').default;
const shim = require('./shim').default;
const JoplinError = require('./JoplinError');
const time = require('./time').default;
const EventDispatcher = require('./EventDispatcher');
2018-03-24 21:35:10 +02:00
class DropboxApi {
constructor(options) {
this.logger_ = new Logger();
this.options_ = options;
this.authToken_ = null;
this.dispatcher_ = new EventDispatcher();
2018-03-24 21:35:10 +02:00
}
2018-03-26 19:33:55 +02:00
clientId() {
return this.options_.id;
}
clientSecret() {
return this.options_.secret;
}
2018-03-24 21:35:10 +02:00
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
authToken() {
2018-03-26 19:33:55 +02:00
return this.authToken_; // Without the "Bearer " prefix
2018-03-24 21:35:10 +02:00
}
on(eventName, callback) {
return this.dispatcher_.on(eventName, callback);
}
2018-03-24 21:35:10 +02:00
setAuthToken(v) {
this.authToken_ = v;
this.dispatcher_.dispatch('authRefreshed', this.authToken());
2018-03-24 21:35:10 +02:00
}
2018-03-26 19:33:55 +02:00
loginUrl() {
2019-09-19 23:51:18 +02:00
return `https://www.dropbox.com/oauth2/authorize?response_type=code&client_id=${this.clientId()}`;
2018-03-26 19:33:55 +02:00
}
2018-03-24 21:35:10 +02:00
baseUrl(endPointFormat) {
2019-09-19 23:51:18 +02:00
if (['content', 'api'].indexOf(endPointFormat) < 0) throw new Error(`Invalid end point format: ${endPointFormat}`);
return `https://${endPointFormat}.dropboxapi.com/2`;
2018-03-24 21:35:10 +02:00
}
requestToCurl_(url, options) {
const output = [];
2018-03-24 21:35:10 +02:00
output.push('curl');
2019-09-19 23:51:18 +02:00
if (options.method) output.push(`-X ${options.method}`);
2018-03-24 21:35:10 +02:00
if (options.headers) {
for (const n in options.headers) {
2018-03-24 21:35:10 +02:00
if (!options.headers.hasOwnProperty(n)) continue;
2019-09-19 23:51:18 +02:00
output.push(`${'-H ' + '\''}${n}: ${options.headers[n]}'`);
2018-03-24 21:35:10 +02:00
}
}
2019-09-19 23:51:18 +02:00
if (options.body) output.push(`${'--data ' + '"'}${options.body}"`);
2018-03-24 21:35:10 +02:00
output.push(url);
2019-07-29 15:43:53 +02:00
return output.join(' ');
2018-03-24 21:35:10 +02:00
}
2018-03-26 19:33:55 +02:00
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]);
2019-09-19 23:51:18 +02:00
formBody.push(`${encodedKey}=${encodedValue}`);
2018-03-26 19:33:55 +02:00
}
2019-07-29 15:43:53 +02:00
formBody = formBody.join('&');
2018-03-26 19:33:55 +02:00
const response = await shim.fetch('https://api.dropboxapi.com/oauth2/token', {
method: 'POST',
headers: {
2019-07-29 15:43:53 +02:00
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
2018-03-26 19:33:55 +02:00
},
2019-07-29 15:43:53 +02:00
body: formBody,
2018-03-26 19:33:55 +02:00
});
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;
}
2018-03-24 21:35:10 +02:00
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();
2019-09-19 23:51:18 +02:00
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
2018-03-24 21:35:10 +02:00
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;
2019-09-19 23:51:18 +02:00
const url = path.indexOf('https://') === 0 ? path : `${this.baseUrl(endPointFormat)}/${path}`;
2018-03-24 21:35:10 +02:00
let tryCount = 0;
while (true) {
try {
let response = null;
// console.info(this.requestToCurl_(url, fetchOptions));
// console.info(method + ' ' + url);
2018-03-24 21:35:10 +02:00
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);
2019-07-29 15:43:53 +02:00
} else {
// file
2018-03-24 21:35:10 +02:00
response = await shim.fetchBlob(url, fetchOptions);
}
const responseText = await response.text();
2019-07-29 15:43:53 +02:00
// console.info('Response: ' + responseText);
2018-03-24 21:35:10 +02:00
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_;
2019-07-29 15:43:53 +02:00
};
2018-03-24 21:35:10 +02:00
// 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 => {
2018-03-24 21:35:10 +02:00
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.
2019-09-19 23:51:18 +02:00
const shortResponseText = (`${responseText}`).substr(0, 1024);
const error = new JoplinError(`${method} ${path}: ${message} (${response.status}): ${shortResponseText}`, code);
error.httpStatus = response.status;
return error;
2019-07-29 15:43:53 +02:00
};
2018-03-24 21:35:10 +02:00
if (!response.ok) {
if (this.isTokenError(response.status, responseText)) {
this.setAuthToken(null);
}
2018-03-24 21:35:10 +02:00
// 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) {
2019-09-19 23:51:18 +02:00
this.logger().warn(`too_many_write_operations ${tryCount}`);
2018-03-24 21:35:10 +02:00
if (tryCount >= 3) {
throw error;
}
await time.sleep(tryCount * 2);
} else {
throw error;
}
}
}
}
}
module.exports = DropboxApi;