Tus.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. const Plugin = require('./Plugin')
  2. const tus = require('tus-js-client')
  3. const UppySocket = require('../core/UppySocket')
  4. const {
  5. emitSocketProgress,
  6. getSocketHost,
  7. settle
  8. } = require('../core/Utils')
  9. require('whatwg-fetch')
  10. // Extracted from https://github.com/tus/tus-js-client/blob/master/lib/upload.js#L13
  11. // excepted we removed 'fingerprint' key to avoid adding more dependencies
  12. const tusDefaultOptions = {
  13. endpoint: '',
  14. resume: true,
  15. onProgress: null,
  16. onChunkComplete: null,
  17. onSuccess: null,
  18. onError: null,
  19. headers: {},
  20. chunkSize: Infinity,
  21. withCredentials: false,
  22. uploadUrl: null,
  23. uploadSize: null,
  24. overridePatchMethod: false,
  25. retryDelays: null
  26. }
  27. /**
  28. * Create a wrapper around an event emitter with a `remove` method to remove
  29. * all events that were added using the wrapped emitter.
  30. */
  31. function createEventTracker (emitter) {
  32. const events = []
  33. return {
  34. on (event, fn) {
  35. events.push([ event, fn ])
  36. return emitter.on(event, fn)
  37. },
  38. remove () {
  39. events.forEach(([ event, fn ]) => {
  40. emitter.off(event, fn)
  41. })
  42. }
  43. }
  44. }
  45. /**
  46. * Tus resumable file uploader
  47. *
  48. */
  49. module.exports = class Tus10 extends Plugin {
  50. constructor (core, opts) {
  51. super(core, opts)
  52. this.type = 'uploader'
  53. this.id = 'Tus'
  54. this.title = 'Tus'
  55. // set default options
  56. const defaultOptions = {
  57. resume: true,
  58. autoRetry: true,
  59. retryDelays: [0, 1000, 3000, 5000]
  60. }
  61. // merge default options with the ones set by user
  62. this.opts = Object.assign({}, defaultOptions, opts)
  63. this.uploaders = Object.create(null)
  64. this.uploaderEvents = Object.create(null)
  65. this.uploaderSockets = Object.create(null)
  66. this.handleResetProgress = this.handleResetProgress.bind(this)
  67. this.handleUpload = this.handleUpload.bind(this)
  68. }
  69. handleResetProgress () {
  70. const files = Object.assign({}, this.core.state.files)
  71. Object.keys(files).forEach((fileID) => {
  72. // Only clone the file object if it has a Tus `uploadUrl` attached.
  73. if (files[fileID].tus && files[fileID].tus.uploadUrl) {
  74. const tusState = Object.assign({}, files[fileID].tus)
  75. delete tusState.uploadUrl
  76. files[fileID] = Object.assign({}, files[fileID], { tus: tusState })
  77. }
  78. })
  79. this.core.setState({ files })
  80. }
  81. /**
  82. * Clean up all references for a file's upload: the tus.Upload instance,
  83. * any events related to the file, and the uppy-server WebSocket connection.
  84. */
  85. resetUploaderReferences (fileID) {
  86. if (this.uploaders[fileID]) {
  87. this.uploaders[fileID].abort()
  88. this.uploaders[fileID] = null
  89. }
  90. if (this.uploaderEvents[fileID]) {
  91. this.uploaderEvents[fileID].remove()
  92. this.uploaderEvents[fileID] = null
  93. }
  94. if (this.uploaderSockets[fileID]) {
  95. this.uploaderSockets[fileID].close()
  96. this.uploaderSockets[fileID] = null
  97. }
  98. }
  99. /**
  100. * Create a new Tus upload
  101. *
  102. * @param {object} file for use with upload
  103. * @param {integer} current file in a queue
  104. * @param {integer} total number of files in a queue
  105. * @returns {Promise}
  106. */
  107. upload (file, current, total) {
  108. this.core.log(`uploading ${current} of ${total}`)
  109. this.resetUploaderReferences(file.id)
  110. // Create a new tus upload
  111. return new Promise((resolve, reject) => {
  112. const optsTus = Object.assign(
  113. {},
  114. tusDefaultOptions,
  115. this.opts,
  116. // Install file-specific upload overrides.
  117. file.tus || {}
  118. )
  119. optsTus.onError = (err) => {
  120. this.core.log(err)
  121. this.core.emit('core:upload-error', file.id, err)
  122. err.message = `Failed because: ${err.message}`
  123. this.resetUploaderReferences(file.id)
  124. reject(err)
  125. }
  126. optsTus.onProgress = (bytesUploaded, bytesTotal) => {
  127. this.onReceiveUploadUrl(file, upload.url)
  128. this.core.emit('core:upload-progress', {
  129. uploader: this,
  130. id: file.id,
  131. bytesUploaded: bytesUploaded,
  132. bytesTotal: bytesTotal
  133. })
  134. }
  135. optsTus.onSuccess = () => {
  136. this.core.emit('core:upload-success', file.id, upload, upload.url)
  137. if (upload.url) {
  138. this.core.log('Download ' + upload.file.name + ' from ' + upload.url)
  139. }
  140. this.resetUploaderReferences(file.id)
  141. resolve(upload)
  142. }
  143. optsTus.metadata = file.meta
  144. const upload = new tus.Upload(file.data, optsTus)
  145. this.uploaders[file.id] = upload
  146. this.uploaderEvents[file.id] = createEventTracker(this.core)
  147. this.onFileRemove(file.id, (targetFileID) => {
  148. this.resetUploaderReferences(file.id)
  149. resolve(`upload ${targetFileID} was removed`)
  150. })
  151. this.onPause(file.id, (isPaused) => {
  152. isPaused ? upload.abort() : upload.start()
  153. })
  154. this.onPauseAll(file.id, () => {
  155. upload.abort()
  156. })
  157. this.onCancelAll(file.id, () => {
  158. this.resetUploaderReferences(file.id)
  159. })
  160. this.onResumeAll(file.id, () => {
  161. if (file.error) {
  162. upload.abort()
  163. }
  164. upload.start()
  165. })
  166. upload.start()
  167. this.core.emit('core:upload-started', file.id, upload)
  168. })
  169. }
  170. uploadRemote (file, current, total) {
  171. this.resetUploaderReferences(file.id)
  172. return new Promise((resolve, reject) => {
  173. this.core.log(file.remote.url)
  174. if (file.serverToken) {
  175. this.connectToServerSocket(file)
  176. } else {
  177. let endpoint = this.opts.endpoint
  178. if (file.tus && file.tus.endpoint) {
  179. endpoint = file.tus.endpoint
  180. }
  181. this.core.emitter.emit('core:upload-started', file.id)
  182. fetch(file.remote.url, {
  183. method: 'post',
  184. credentials: 'include',
  185. headers: {
  186. 'Accept': 'application/json',
  187. 'Content-Type': 'application/json'
  188. },
  189. body: JSON.stringify(Object.assign({}, file.remote.body, {
  190. endpoint,
  191. protocol: 'tus',
  192. size: file.data.size,
  193. metadata: file.meta
  194. }))
  195. })
  196. .then((res) => {
  197. if (res.status < 200 && res.status > 300) {
  198. return reject(res.statusText)
  199. }
  200. res.json().then((data) => {
  201. const token = data.token
  202. file = this.getFile(file.id)
  203. file.serverToken = token
  204. this.updateFile(file)
  205. this.connectToServerSocket(file)
  206. resolve()
  207. })
  208. })
  209. }
  210. })
  211. }
  212. connectToServerSocket (file) {
  213. const token = file.serverToken
  214. const host = getSocketHost(file.remote.host)
  215. const socket = new UppySocket({ target: `${host}/api/${token}` })
  216. this.uploaderSockets[file.id] = socket
  217. this.uploaderEvents[file.id] = createEventTracker(this.core)
  218. this.onFileRemove(file.id, () => socket.send('pause', {}))
  219. this.onPause(file.id, (isPaused) => {
  220. isPaused ? socket.send('pause', {}) : socket.send('resume', {})
  221. })
  222. this.onPauseAll(file.id, () => socket.send('pause', {}))
  223. this.onCancelAll(file.id, () => socket.send('pause', {}))
  224. this.onResumeAll(file.id, () => {
  225. if (file.error) {
  226. socket.send('pause', {})
  227. }
  228. socket.send('resume', {})
  229. })
  230. this.onRetry(file.id, () => {
  231. socket.send('pause', {})
  232. socket.send('resume', {})
  233. })
  234. this.onRetryAll(file.id, () => {
  235. socket.send('pause', {})
  236. socket.send('resume', {})
  237. })
  238. socket.on('progress', (progressData) => emitSocketProgress(this, progressData, file))
  239. socket.on('success', (data) => {
  240. this.core.emitter.emit('core:upload-success', file.id, data, data.url)
  241. this.resetUploaderReferences(file.id)
  242. })
  243. }
  244. getFile (fileID) {
  245. return this.core.state.files[fileID]
  246. }
  247. updateFile (file) {
  248. const files = Object.assign({}, this.core.state.files, {
  249. [file.id]: file
  250. })
  251. this.core.setState({ files })
  252. }
  253. onReceiveUploadUrl (file, uploadURL) {
  254. const currentFile = this.getFile(file.id)
  255. if (!currentFile) return
  256. // Only do the update if we didn't have an upload URL yet.
  257. if (!currentFile.tus || currentFile.tus.uploadUrl !== uploadURL) {
  258. const newFile = Object.assign({}, currentFile, {
  259. tus: Object.assign({}, currentFile.tus, {
  260. uploadUrl: uploadURL
  261. })
  262. })
  263. this.updateFile(newFile)
  264. }
  265. }
  266. onFileRemove (fileID, cb) {
  267. this.uploaderEvents[fileID].on('core:file-removed', (targetFileID) => {
  268. if (fileID === targetFileID) cb(targetFileID)
  269. })
  270. }
  271. onPause (fileID, cb) {
  272. this.uploaderEvents[fileID].on('core:upload-pause', (targetFileID, isPaused) => {
  273. if (fileID === targetFileID) {
  274. // const isPaused = this.core.pauseResume(fileID)
  275. cb(isPaused)
  276. }
  277. })
  278. }
  279. onRetry (fileID, cb) {
  280. this.uploaderEvents[fileID].on('core:upload-retry', (targetFileID) => {
  281. if (fileID === targetFileID) {
  282. cb()
  283. }
  284. })
  285. }
  286. onRetryAll (fileID, cb) {
  287. this.uploaderEvents[fileID].on('core:retry-all', (filesToRetry) => {
  288. if (!this.core.getFile(fileID)) return
  289. cb()
  290. })
  291. }
  292. onPauseAll (fileID, cb) {
  293. this.uploaderEvents[fileID].on('core:pause-all', () => {
  294. if (!this.core.getFile(fileID)) return
  295. cb()
  296. })
  297. }
  298. onCancelAll (fileID, cb) {
  299. this.uploaderEvents[fileID].on('core:cancel-all', () => {
  300. if (!this.core.getFile(fileID)) return
  301. cb()
  302. })
  303. }
  304. onResumeAll (fileID, cb) {
  305. this.uploaderEvents[fileID].on('core:resume-all', () => {
  306. if (!this.core.getFile(fileID)) return
  307. cb()
  308. })
  309. }
  310. uploadFiles (files) {
  311. const promises = files.map((file, index) => {
  312. const current = parseInt(index, 10) + 1
  313. const total = files.length
  314. if (!file.isRemote) {
  315. return this.upload(file, current, total)
  316. } else {
  317. return this.uploadRemote(file, current, total)
  318. }
  319. })
  320. return settle(promises)
  321. }
  322. handleUpload (fileIDs) {
  323. if (fileIDs.length === 0) {
  324. this.core.log('Tus: no files to upload!')
  325. return Promise.resolve()
  326. }
  327. this.core.log('Tus is uploading...')
  328. const filesToUpload = fileIDs.map((fileID) => this.core.getFile(fileID))
  329. return this.uploadFiles(filesToUpload)
  330. }
  331. addResumableUploadsCapabilityFlag () {
  332. const newCapabilities = Object.assign({}, this.core.getState().capabilities)
  333. newCapabilities.resumableUploads = true
  334. this.core.setState({
  335. capabilities: newCapabilities
  336. })
  337. }
  338. install () {
  339. this.addResumableUploadsCapabilityFlag()
  340. this.core.addUploader(this.handleUpload)
  341. this.core.on('core:reset-progress', this.handleResetProgress)
  342. if (this.opts.autoRetry) {
  343. this.core.on('back-online', this.core.retryAll)
  344. }
  345. }
  346. uninstall () {
  347. this.core.removeUploader(this.handleUpload)
  348. if (this.opts.autoRetry) {
  349. this.core.off('back-online', this.core.retryAll)
  350. }
  351. }
  352. }