index.js 7.4 KB

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