XHRUpload.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. const Plugin = require('./Plugin')
  2. const UppySocket = require('../core/UppySocket')
  3. const {
  4. emitSocketProgress,
  5. getSocketHost,
  6. settle
  7. } = require('../core/Utils')
  8. module.exports = class XHRUpload extends Plugin {
  9. constructor (core, opts) {
  10. super(core, opts)
  11. this.type = 'uploader'
  12. this.id = 'XHRUpload'
  13. this.title = 'XHRUpload'
  14. // Default options
  15. const defaultOptions = {
  16. formData: true,
  17. fieldName: 'files[]',
  18. method: 'post',
  19. metaFields: null,
  20. responseUrlFieldName: 'url',
  21. bundle: true,
  22. headers: {},
  23. getResponseData (xhr) {
  24. return JSON.parse(xhr.response)
  25. },
  26. getResponseError (xhr) {
  27. return new Error('Upload error')
  28. }
  29. }
  30. // Merge default options with the ones set by user
  31. this.opts = Object.assign({}, defaultOptions, opts)
  32. this.handleUpload = this.handleUpload.bind(this)
  33. }
  34. getOptions (file) {
  35. const opts = Object.assign({},
  36. this.opts,
  37. this.core.state.xhrUpload || {},
  38. file.xhrUpload || {}
  39. )
  40. opts.headers = {}
  41. Object.assign(opts.headers, this.opts.headers)
  42. if (this.core.state.xhrUpload) {
  43. Object.assign(opts.headers, this.core.state.xhrUpload.headers)
  44. }
  45. if (file.xhrUpload) {
  46. Object.assign(opts.headers, file.xhrUpload.headers)
  47. }
  48. return opts
  49. }
  50. createFormDataUpload (file, opts) {
  51. const formPost = new FormData()
  52. const metaFields = Array.isArray(opts.metaFields)
  53. ? opts.metaFields
  54. // Send along all fields by default.
  55. : Object.keys(file.meta)
  56. metaFields.forEach((item) => {
  57. formPost.append(item, file.meta[item])
  58. })
  59. formPost.append(opts.fieldName, file.data)
  60. return formPost
  61. }
  62. createBareUpload (file, opts) {
  63. return file.data
  64. }
  65. upload (file, current, total) {
  66. const opts = this.getOptions(file)
  67. this.core.log(`uploading ${current} of ${total}`)
  68. return new Promise((resolve, reject) => {
  69. const data = opts.formData
  70. ? this.createFormDataUpload(file, opts)
  71. : this.createBareUpload(file, opts)
  72. const xhr = new XMLHttpRequest()
  73. xhr.upload.addEventListener('progress', (ev) => {
  74. if (ev.lengthComputable) {
  75. this.core.emit('core:upload-progress', {
  76. uploader: this,
  77. id: file.id,
  78. bytesUploaded: ev.loaded,
  79. bytesTotal: ev.total
  80. })
  81. }
  82. })
  83. xhr.addEventListener('load', (ev) => {
  84. if (ev.target.status >= 200 && ev.target.status < 300) {
  85. const resp = opts.getResponseData(xhr)
  86. const uploadURL = resp[opts.responseUrlFieldName]
  87. this.core.emit('core:upload-success', file.id, resp, uploadURL)
  88. if (uploadURL) {
  89. this.core.log(`Download ${file.name} from ${file.uploadURL}`)
  90. }
  91. return resolve(file)
  92. } else {
  93. const error = opts.getResponseError(xhr) || new Error('Upload error')
  94. error.request = xhr
  95. this.core.emit('core:upload-error', file.id, error)
  96. return reject(error)
  97. }
  98. })
  99. xhr.addEventListener('error', (ev) => {
  100. const error = opts.getResponseError(xhr) || new Error('Upload error')
  101. this.core.emit('core:upload-error', file.id, error)
  102. return reject(new Error('Upload error'))
  103. })
  104. xhr.open(opts.method.toUpperCase(), opts.endpoint, true)
  105. Object.keys(opts.headers).forEach((header) => {
  106. xhr.setRequestHeader(header, opts.headers[header])
  107. })
  108. xhr.send(data)
  109. this.core.on('core:upload-cancel', (fileID) => {
  110. if (fileID === file.id) {
  111. xhr.abort()
  112. }
  113. })
  114. this.core.on('core:cancel-all', () => {
  115. // const files = this.core.getState().files
  116. // if (!files[file.id]) return
  117. xhr.abort()
  118. })
  119. this.core.emit('core:upload-started', file.id)
  120. })
  121. }
  122. uploadRemote (file, current, total) {
  123. const opts = this.getOptions(file)
  124. return new Promise((resolve, reject) => {
  125. this.core.emit('core:upload-started', file.id)
  126. const fields = {}
  127. const metaFields = Array.isArray(opts.metaFields)
  128. ? opts.metaFields
  129. // Send along all fields by default.
  130. : Object.keys(file.meta)
  131. metaFields.forEach((name) => {
  132. fields[name] = file.meta.name
  133. })
  134. fetch(file.remote.url, {
  135. method: 'post',
  136. credentials: 'include',
  137. headers: {
  138. 'Accept': 'application/json',
  139. 'Content-Type': 'application/json'
  140. },
  141. body: JSON.stringify(Object.assign({}, file.remote.body, {
  142. endpoint: opts.endpoint,
  143. size: file.data.size,
  144. fieldname: opts.fieldName,
  145. fields,
  146. headers: opts.headers
  147. }))
  148. })
  149. .then((res) => {
  150. if (res.status < 200 && res.status > 300) {
  151. return reject(res.statusText)
  152. }
  153. res.json().then((data) => {
  154. const token = data.token
  155. const host = getSocketHost(file.remote.host)
  156. const socket = new UppySocket({ target: `${host}/api/${token}` })
  157. socket.on('progress', (progressData) => emitSocketProgress(this, progressData, file))
  158. socket.on('success', (data) => {
  159. this.core.emit('core:upload-success', file.id, data, data.url)
  160. socket.close()
  161. return resolve()
  162. })
  163. })
  164. })
  165. })
  166. }
  167. uploadFiles (files) {
  168. const promises = files.map((file, i) => {
  169. const current = parseInt(i, 10) + 1
  170. const total = files.length
  171. if (file.isRemote) {
  172. return this.uploadRemote(file, current, total)
  173. } else {
  174. return this.upload(file, current, total)
  175. }
  176. })
  177. return settle(promises)
  178. }
  179. handleUpload (fileIDs) {
  180. if (fileIDs.length === 0) {
  181. this.core.log('[XHRUpload] No files to upload!')
  182. return Promise.resolve()
  183. }
  184. this.core.log('[XHRUpload] Uploading...')
  185. const files = fileIDs.map(getFile, this)
  186. function getFile (fileID) {
  187. return this.core.state.files[fileID]
  188. }
  189. return this.uploadFiles(files).then(() => null)
  190. }
  191. install () {
  192. this.core.addUploader(this.handleUpload)
  193. }
  194. uninstall () {
  195. this.core.removeUploader(this.handleUpload)
  196. }
  197. }