index.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. const Plugin = require('../../core/Plugin')
  2. const Translator = require('../../core/Translator')
  3. const { limitPromises } = require('../../core/Utils')
  4. const XHRUpload = require('../XHRUpload')
  5. function isXml (xhr) {
  6. const contentType = xhr.headers ? xhr.headers['content-type'] : xhr.getResponseHeader('Content-Type')
  7. return typeof contentType === 'string' && contentType.toLowerCase() === 'application/xml'
  8. }
  9. module.exports = class AwsS3 extends Plugin {
  10. constructor (uppy, opts) {
  11. super(uppy, opts)
  12. this.type = 'uploader'
  13. this.id = 'AwsS3'
  14. this.title = 'AWS S3'
  15. const defaultLocale = {
  16. strings: {
  17. preparingUpload: 'Preparing upload...'
  18. }
  19. }
  20. const defaultOptions = {
  21. timeout: 30 * 1000,
  22. limit: 0,
  23. getUploadParameters: this.getUploadParameters.bind(this),
  24. locale: defaultLocale
  25. }
  26. this.opts = Object.assign({}, defaultOptions, opts)
  27. this.locale = Object.assign({}, defaultLocale, this.opts.locale)
  28. this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)
  29. this.translator = new Translator({ locale: this.locale })
  30. this.i18n = this.translator.translate.bind(this.translator)
  31. this.prepareUpload = this.prepareUpload.bind(this)
  32. if (typeof this.opts.limit === 'number' && this.opts.limit !== 0) {
  33. this.limitRequests = limitPromises(this.opts.limit)
  34. } else {
  35. this.limitRequests = (fn) => fn
  36. }
  37. }
  38. getUploadParameters (file) {
  39. if (!this.opts.host) {
  40. throw new Error('Expected a `host` option containing an uppy-server address.')
  41. }
  42. const filename = encodeURIComponent(file.name)
  43. const type = encodeURIComponent(file.type)
  44. return fetch(`${this.opts.host}/s3/params?filename=${filename}&type=${type}`, {
  45. method: 'get',
  46. headers: { accept: 'application/json' }
  47. }).then((response) => response.json())
  48. }
  49. validateParameters (file, params) {
  50. const valid = typeof params === 'object' && params &&
  51. typeof params.url === 'string' &&
  52. (typeof params.fields === 'object' || params.fields == null) &&
  53. (params.method == null || /^(put|post)$/i.test(params.method))
  54. if (!valid) {
  55. const err = new TypeError(`AwsS3: got incorrect result from 'getUploadParameters()' for file '${file.name}', expected an object '{ url, method, fields }'.\nSee https://uppy.io/docs/aws-s3/#getUploadParameters-file for more on the expected format.`)
  56. console.error(err)
  57. throw err
  58. }
  59. return params
  60. }
  61. prepareUpload (fileIDs) {
  62. fileIDs.forEach((id) => {
  63. const file = this.uppy.getFile(id)
  64. this.uppy.emit('preprocess-progress', file, {
  65. mode: 'determinate',
  66. message: this.i18n('preparingUpload'),
  67. value: 0
  68. })
  69. })
  70. const getUploadParameters = this.limitRequests(this.opts.getUploadParameters)
  71. return Promise.all(
  72. fileIDs.map((id) => {
  73. const file = this.uppy.getFile(id)
  74. const paramsPromise = Promise.resolve()
  75. .then(() => getUploadParameters(file))
  76. return paramsPromise.then((params) => {
  77. return this.validateParameters(file, params)
  78. }).then((params) => {
  79. this.uppy.emit('preprocess-progress', file, {
  80. mode: 'determinate',
  81. message: this.i18n('preparingUpload'),
  82. value: 1
  83. })
  84. return params
  85. }).catch((error) => {
  86. this.uppy.emit('upload-error', file, error)
  87. })
  88. })
  89. ).then((responses) => {
  90. const updatedFiles = {}
  91. fileIDs.forEach((id, index) => {
  92. const file = this.uppy.getFile(id)
  93. if (file.error) {
  94. return
  95. }
  96. const {
  97. method = 'post',
  98. url,
  99. fields,
  100. headers
  101. } = responses[index]
  102. const xhrOpts = {
  103. method,
  104. formData: method.toLowerCase() === 'post',
  105. endpoint: url,
  106. metaFields: Object.keys(fields)
  107. }
  108. if (headers) {
  109. xhrOpts.headers = headers
  110. }
  111. const updatedFile = Object.assign({}, file, {
  112. meta: Object.assign({}, file.meta, fields),
  113. xhrUpload: xhrOpts
  114. })
  115. updatedFiles[id] = updatedFile
  116. })
  117. this.uppy.setState({
  118. files: Object.assign({}, this.uppy.getState().files, updatedFiles)
  119. })
  120. fileIDs.forEach((id) => {
  121. const file = this.uppy.getFile(id)
  122. this.uppy.emit('preprocess-complete', file)
  123. })
  124. })
  125. }
  126. install () {
  127. this.uppy.addPreProcessor(this.prepareUpload)
  128. this.uppy.use(XHRUpload, {
  129. fieldName: 'file',
  130. responseUrlFieldName: 'location',
  131. timeout: this.opts.timeout,
  132. limit: this.opts.limit,
  133. getResponseData (content, xhr) {
  134. // If no response, we've hopefully done a PUT request to the file
  135. // in the bucket on its full URL.
  136. if (!isXml(xhr)) {
  137. return { location: xhr.responseURL }
  138. }
  139. let getValue = () => ''
  140. if (xhr.responseXML) {
  141. getValue = (key) => {
  142. const el = xhr.responseXML.querySelector(key)
  143. return el ? el.textContent : ''
  144. }
  145. }
  146. if (xhr.responseText) {
  147. getValue = (key) => {
  148. const start = xhr.responseText.indexOf(`<${key}>`)
  149. const end = xhr.responseText.indexOf(`</${key}>`)
  150. return start !== -1 && end !== -1
  151. ? xhr.responseText.slice(start + key.length + 2, end)
  152. : ''
  153. }
  154. }
  155. return {
  156. location: getValue('Location'),
  157. bucket: getValue('Bucket'),
  158. key: getValue('Key'),
  159. etag: getValue('ETag')
  160. }
  161. },
  162. getResponseError (content, xhr) {
  163. // If no response, we don't have a specific error message, use the default.
  164. if (!isXml(xhr)) {
  165. return
  166. }
  167. const error = xhr.responseXML.querySelector('Error > Message')
  168. return new Error(error.textContent)
  169. }
  170. })
  171. }
  172. uninstall () {
  173. const uploader = this.uppy.getPlugin('XHRUpload')
  174. this.uppy.removePlugin(uploader)
  175. this.uppy.removePreProcessor(this.prepareUpload)
  176. }
  177. }