2018-03-24 21:35:10 +02:00
const { Logger } = require ( 'lib/logger.js' ) ;
const { shim } = require ( 'lib/shim.js' ) ;
const JoplinError = require ( 'lib/JoplinError' ) ;
const { time } = require ( 'lib/time-utils' ) ;
2018-03-27 01:05:39 +02:00
const EventDispatcher = require ( 'lib/EventDispatcher' ) ;
2018-03-24 21:35:10 +02:00
class DropboxApi {
constructor ( options ) {
this . logger _ = new Logger ( ) ;
this . options _ = options ;
this . authToken _ = null ;
2018-03-27 01:05:39 +02:00
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
}
2018-03-27 01:05:39 +02:00
on ( eventName , callback ) {
return this . dispatcher _ . on ( eventName , callback ) ;
}
2018-03-24 21:35:10 +02:00
setAuthToken ( v ) {
this . authToken _ = v ;
2018-03-27 01:05:39 +02:00
this . dispatcher _ . dispatch ( 'authRefreshed' , this . authToken ( ) ) ;
2018-03-24 21:35:10 +02:00
}
2018-03-26 19:33:55 +02:00
loginUrl ( ) {
return 'https://www.dropbox.com/oauth2/authorize?response_type=code&client_id=' + this . clientId ( ) ;
}
2018-03-24 21:35:10 +02:00
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 ;
2019-07-30 09:35:42 +02:00
output . push ( '-H ' + '\'' + n + ': ' + options . headers [ n ] + '\'' ) ;
2018-03-24 21:35:10 +02:00
}
}
if ( options . body ) output . push ( '--data ' + '"' + options . body + '"' ) ;
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 ( ) ,
} ;
var formBody = [ ] ;
for ( var property in postData ) {
var encodedKey = encodeURIComponent ( property ) ;
var encodedValue = encodeURIComponent ( postData [ property ] ) ;
2019-07-29 15:43:53 +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 ) ;
}
2018-03-27 01:05:39 +02:00
isTokenError ( status , responseText ) {
if ( status === 401 ) return true ;
if ( responseText . indexOf ( 'OAuth 2 access token is malformed' ) >= 0 ) return true ;
2018-05-22 16:03:55 +02:00
// 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 ;
2018-03-27 01:05:39 +02:00
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 ( ) ;
2018-03-26 19:33:55 +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 ;
2018-03-26 19:33:55 +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));
2018-03-27 01:05:39 +02:00
// 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
2019-07-29 15:43:53 +02:00
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.
const shortResponseText = ( responseText + '' ) . substr ( 0 , 1024 ) ;
2018-03-27 01:05:39 +02:00
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 ) {
2018-03-27 01:05:39 +02:00
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 ++ ;
2018-06-11 01:24:29 +02:00
if ( error && typeof error . code === 'string' && error . code . indexOf ( 'too_many_write_operations' ) >= 0 ) {
2018-03-24 21:35:10 +02:00
this . logger ( ) . warn ( 'too_many_write_operations ' + tryCount ) ;
if ( tryCount >= 3 ) {
throw error ;
}
await time . sleep ( tryCount * 2 ) ;
} else {
throw error ;
}
}
}
}
}
2018-05-22 16:03:55 +02:00
module . exports = DropboxApi ;