index.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. const Provider = require('../Provider')
  2. const request = require('request')
  3. const purest = require('purest')({ request })
  4. const logger = require('../../logger')
  5. const adapter = require('./adapter')
  6. const { ProviderApiError, ProviderAuthError } = require('../error')
  7. // From https://www.dropbox.com/developers/reference/json-encoding:
  8. //
  9. // This function is simple and has OK performance compared to more
  10. // complicated ones: http://jsperf.com/json-escape-unicode/4
  11. const charsToEncode = /[\u007f-\uffff]/g
  12. function httpHeaderSafeJson (v) {
  13. return JSON.stringify(v).replace(charsToEncode,
  14. function (c) {
  15. return '\\u' + ('000' + c.charCodeAt(0).toString(16)).slice(-4)
  16. }
  17. )
  18. }
  19. /**
  20. * Adapter for API https://www.dropbox.com/developers/documentation/http/documentation
  21. */
  22. class DropBox extends Provider {
  23. constructor (options) {
  24. super(options)
  25. this.authProvider = options.provider = DropBox.authProvider
  26. this.client = purest(options)
  27. // needed for the thumbnails fetched via companion
  28. this.needsCookieAuth = true
  29. }
  30. static get authProvider () {
  31. return 'dropbox'
  32. }
  33. _userInfo ({ token }, done) {
  34. this.client
  35. .post('users/get_current_account')
  36. .options({ version: '2' })
  37. .auth(token)
  38. .request(done)
  39. }
  40. /**
  41. * Makes 2 requests in parallel - 1. to get files, 2. to get user email
  42. * it then waits till both requests are done before proceeding with the callback
  43. *
  44. * @param {object} options
  45. * @param {function} done
  46. */
  47. list (options, done) {
  48. let userInfoDone = false
  49. let statsDone = false
  50. let userInfo
  51. let stats
  52. let reqErr
  53. const finishReq = () => {
  54. if (reqErr || stats.statusCode !== 200) {
  55. const err = this._error(reqErr, stats)
  56. logger.error(err, 'provider.dropbox.list.error')
  57. done(err)
  58. } else {
  59. stats.body.user_email = userInfo.body.email
  60. done(null, this.adaptData(stats.body, options.companion))
  61. }
  62. }
  63. this.stats(options, (err, resp) => {
  64. statsDone = true
  65. stats = resp
  66. reqErr = reqErr || err
  67. if (userInfoDone) {
  68. finishReq()
  69. }
  70. })
  71. this._userInfo(options, (err, resp) => {
  72. userInfoDone = true
  73. userInfo = resp
  74. reqErr = reqErr || err
  75. if (statsDone) {
  76. finishReq()
  77. }
  78. })
  79. }
  80. stats ({ directory, query, token }, done) {
  81. if (query.cursor) {
  82. this.client
  83. .post('files/list_folder/continue')
  84. .options({ version: '2' })
  85. .auth(token)
  86. .json({
  87. cursor: query.cursor
  88. })
  89. .request(done)
  90. return
  91. }
  92. this.client
  93. .post('files/list_folder')
  94. .options({ version: '2' })
  95. .qs(query)
  96. .auth(token)
  97. .json({
  98. path: `${directory || ''}`,
  99. include_non_downloadable_files: false
  100. })
  101. .request(done)
  102. }
  103. download ({ id, token }, onData) {
  104. return this.client
  105. .post('https://content.dropboxapi.com/2/files/download')
  106. .options({
  107. version: '2',
  108. headers: {
  109. 'Dropbox-API-Arg': httpHeaderSafeJson({ path: `${id}` })
  110. }
  111. })
  112. .auth(token)
  113. .request()
  114. .on('response', (resp) => {
  115. if (resp.statusCode !== 200) {
  116. onData(this._error(null, resp))
  117. } else {
  118. resp.on('data', (chunk) => onData(null, chunk))
  119. }
  120. })
  121. .on('end', () => onData(null, null))
  122. .on('error', (err) => {
  123. logger.error(err, 'provider.dropbox.download.error')
  124. onData(err)
  125. })
  126. }
  127. thumbnail ({ id, token }, done) {
  128. return this.client
  129. .post('https://content.dropboxapi.com/2/files/get_thumbnail')
  130. .options({
  131. version: '2',
  132. headers: {
  133. 'Dropbox-API-Arg': httpHeaderSafeJson({ path: `${id}`, size: 'w256h256' })
  134. }
  135. })
  136. .auth(token)
  137. .request()
  138. .on('response', (resp) => {
  139. if (resp.statusCode !== 200) {
  140. const err = this._error(null, resp)
  141. logger.error(err, 'provider.dropbox.thumbnail.error')
  142. return done(err)
  143. }
  144. done(null, resp)
  145. })
  146. .on('error', (err) => {
  147. logger.error(err, 'provider.dropbox.thumbnail.error')
  148. })
  149. }
  150. size ({ id, token }, done) {
  151. return this.client
  152. .post('files/get_metadata')
  153. .options({ version: '2' })
  154. .auth(token)
  155. .json({ path: id })
  156. .request((err, resp, body) => {
  157. if (err || resp.statusCode !== 200) {
  158. err = this._error(err, resp)
  159. logger.error(err, 'provider.dropbox.size.error')
  160. return done(err)
  161. }
  162. done(null, parseInt(body.size))
  163. })
  164. }
  165. logout ({ token }, done) {
  166. return this.client
  167. .post('auth/token/revoke')
  168. .options({ version: '2' })
  169. .auth(token)
  170. .request((err, resp) => {
  171. if (err || resp.statusCode !== 200) {
  172. logger.error(err, 'provider.dropbox.size.error')
  173. done(this._error(err, resp))
  174. return
  175. }
  176. done(null, { revoked: true })
  177. })
  178. }
  179. adaptData (res, companion) {
  180. const data = { username: adapter.getUsername(res), items: [] }
  181. const items = adapter.getItemSubList(res)
  182. items.forEach((item) => {
  183. data.items.push({
  184. isFolder: adapter.isFolder(item),
  185. icon: adapter.getItemIcon(item),
  186. name: adapter.getItemName(item),
  187. mimeType: adapter.getMimeType(item),
  188. id: adapter.getItemId(item),
  189. thumbnail: companion.buildURL(adapter.getItemThumbnailUrl(item), true),
  190. requestPath: adapter.getItemRequestPath(item),
  191. modifiedDate: adapter.getItemModifiedDate(item),
  192. size: adapter.getItemSize(item)
  193. })
  194. })
  195. data.nextPagePath = adapter.getNextPagePath(res)
  196. return data
  197. }
  198. _error (err, resp) {
  199. if (resp) {
  200. const fallbackMessage = `request to ${this.authProvider} returned ${resp.statusCode}`
  201. const errMsg = (resp.body || {}).error_summary ? resp.body.error_summary : fallbackMessage
  202. return resp.statusCode === 401 ? new ProviderAuthError() : new ProviderApiError(errMsg, resp.statusCode)
  203. }
  204. return err
  205. }
  206. }
  207. module.exports = DropBox