mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-15 09:04:04 +02:00
217 lines
6.2 KiB
JavaScript
217 lines
6.2 KiB
JavaScript
const { Logger } = require('lib/logger.js');
|
|
const { shim } = require('lib/shim.js');
|
|
const JoplinError = require('lib/JoplinError');
|
|
const URL = require('url-parse');
|
|
const { time } = require('lib/time-utils');
|
|
const EventDispatcher = require('lib/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) {
|
|
let output = [];
|
|
output.push('curl');
|
|
if (options.method) output.push('-X ' + options.method);
|
|
if (options.headers) {
|
|
for (let 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(),
|
|
};
|
|
|
|
var formBody = [];
|
|
for (var property in postData) {
|
|
var encodedKey = encodeURIComponent(property);
|
|
var 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) {
|
|
const json = loadResponseJson();
|
|
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;
|