index.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. const BasePlugin = require('@uppy/core/lib/BasePlugin')
  2. const tus = require('tus-js-client')
  3. const { Provider, RequestClient, Socket } = require('@uppy/companion-client')
  4. const emitSocketProgress = require('@uppy/utils/lib/emitSocketProgress')
  5. const getSocketHost = require('@uppy/utils/lib/getSocketHost')
  6. const settle = require('@uppy/utils/lib/settle')
  7. const EventTracker = require('@uppy/utils/lib/EventTracker')
  8. const NetworkError = require('@uppy/utils/lib/NetworkError')
  9. const isNetworkError = require('@uppy/utils/lib/isNetworkError')
  10. const { RateLimitedQueue } = require('@uppy/utils/lib/RateLimitedQueue')
  11. const hasProperty = require('@uppy/utils/lib/hasProperty')
  12. const getFingerprint = require('./getFingerprint')
  13. /** @typedef {import('..').TusOptions} TusOptions */
  14. /** @typedef {import('tus-js-client').UploadOptions} RawTusOptions */
  15. /** @typedef {import('@uppy/core').Uppy} Uppy */
  16. /** @typedef {import('@uppy/core').UppyFile} UppyFile */
  17. /** @typedef {import('@uppy/core').FailedUppyFile<{}>} FailedUppyFile */
  18. /**
  19. * Extracted from https://github.com/tus/tus-js-client/blob/master/lib/upload.js#L13
  20. * excepted we removed 'fingerprint' key to avoid adding more dependencies
  21. *
  22. * @type {RawTusOptions}
  23. */
  24. const tusDefaultOptions = {
  25. endpoint: '',
  26. uploadUrl: null,
  27. metadata: {},
  28. uploadSize: null,
  29. onProgress: null,
  30. onChunkComplete: null,
  31. onSuccess: null,
  32. onError: null,
  33. overridePatchMethod: false,
  34. headers: {},
  35. addRequestId: false,
  36. chunkSize: Infinity,
  37. retryDelays: [0, 1000, 3000, 5000],
  38. parallelUploads: 1,
  39. removeFingerprintOnSuccess: false,
  40. uploadLengthDeferred: false,
  41. uploadDataDuringCreation: false,
  42. }
  43. /**
  44. * Tus resumable file uploader
  45. */
  46. module.exports = class Tus extends BasePlugin {
  47. static VERSION = require('../package.json').version
  48. /**
  49. * @param {Uppy} uppy
  50. * @param {TusOptions} opts
  51. */
  52. constructor (uppy, opts) {
  53. super(uppy, opts)
  54. this.type = 'uploader'
  55. this.id = this.opts.id || 'Tus'
  56. this.title = 'Tus'
  57. // set default options
  58. const defaultOptions = {
  59. useFastRemoteRetry: true,
  60. limit: 5,
  61. retryDelays: [0, 1000, 3000, 5000],
  62. withCredentials: false,
  63. }
  64. // merge default options with the ones set by user
  65. /** @type {import("..").TusOptions} */
  66. this.opts = { ...defaultOptions, ...opts }
  67. if ('autoRetry' in opts) {
  68. throw new Error('The `autoRetry` option was deprecated and has been removed.')
  69. }
  70. /**
  71. * Simultaneous upload limiting is shared across all uploads with this plugin.
  72. *
  73. * @type {RateLimitedQueue}
  74. */
  75. this.requests = new RateLimitedQueue(this.opts.limit)
  76. this.uploaders = Object.create(null)
  77. this.uploaderEvents = Object.create(null)
  78. this.uploaderSockets = Object.create(null)
  79. this.handleResetProgress = this.handleResetProgress.bind(this)
  80. this.handleUpload = this.handleUpload.bind(this)
  81. }
  82. handleResetProgress () {
  83. const files = { ...this.uppy.getState().files }
  84. Object.keys(files).forEach((fileID) => {
  85. // Only clone the file object if it has a Tus `uploadUrl` attached.
  86. if (files[fileID].tus && files[fileID].tus.uploadUrl) {
  87. const tusState = { ...files[fileID].tus }
  88. delete tusState.uploadUrl
  89. files[fileID] = { ...files[fileID], tus: tusState }
  90. }
  91. })
  92. this.uppy.setState({ files })
  93. }
  94. /**
  95. * Clean up all references for a file's upload: the tus.Upload instance,
  96. * any events related to the file, and the Companion WebSocket connection.
  97. *
  98. * @param {string} fileID
  99. */
  100. resetUploaderReferences (fileID, opts = {}) {
  101. if (this.uploaders[fileID]) {
  102. const uploader = this.uploaders[fileID]
  103. uploader.abort()
  104. if (opts.abort) {
  105. uploader.abort(true)
  106. }
  107. this.uploaders[fileID] = null
  108. }
  109. if (this.uploaderEvents[fileID]) {
  110. this.uploaderEvents[fileID].remove()
  111. this.uploaderEvents[fileID] = null
  112. }
  113. if (this.uploaderSockets[fileID]) {
  114. this.uploaderSockets[fileID].close()
  115. this.uploaderSockets[fileID] = null
  116. }
  117. }
  118. /**
  119. * Create a new Tus upload.
  120. *
  121. * A lot can happen during an upload, so this is quite hard to follow!
  122. * - First, the upload is started. If the file was already paused by the time the upload starts, nothing should happen.
  123. * If the `limit` option is used, the upload must be queued onto the `this.requests` queue.
  124. * When an upload starts, we store the tus.Upload instance, and an EventTracker instance that manages the event listeners
  125. * for pausing, cancellation, removal, etc.
  126. * - While the upload is in progress, it may be paused or cancelled.
  127. * Pausing aborts the underlying tus.Upload, and removes the upload from the `this.requests` queue. All other state is
  128. * maintained.
  129. * Cancelling removes the upload from the `this.requests` queue, and completely aborts the upload-- the `tus.Upload`
  130. * instance is aborted and discarded, the EventTracker instance is destroyed (removing all listeners).
  131. * Resuming the upload uses the `this.requests` queue as well, to prevent selectively pausing and resuming uploads from
  132. * bypassing the limit.
  133. * - After completing an upload, the tus.Upload and EventTracker instances are cleaned up, and the upload is marked as done
  134. * in the `this.requests` queue.
  135. * - When an upload completed with an error, the same happens as on successful completion, but the `upload()` promise is
  136. * rejected.
  137. *
  138. * When working on this function, keep in mind:
  139. * - When an upload is completed or cancelled for any reason, the tus.Upload and EventTracker instances need to be cleaned
  140. * up using this.resetUploaderReferences().
  141. * - When an upload is cancelled or paused, for any reason, it needs to be removed from the `this.requests` queue using
  142. * `queuedRequest.abort()`.
  143. * - When an upload is completed for any reason, including errors, it needs to be marked as such using
  144. * `queuedRequest.done()`.
  145. * - When an upload is started or resumed, it needs to go through the `this.requests` queue. The `queuedRequest` variable
  146. * must be updated so the other uses of it are valid.
  147. * - Before replacing the `queuedRequest` variable, the previous `queuedRequest` must be aborted, else it will keep taking
  148. * up a spot in the queue.
  149. *
  150. * @param {UppyFile} file for use with upload
  151. * @param {number} current file in a queue
  152. * @param {number} total number of files in a queue
  153. * @returns {Promise<void>}
  154. */
  155. upload (file) {
  156. this.resetUploaderReferences(file.id)
  157. // Create a new tus upload
  158. return new Promise((resolve, reject) => {
  159. this.uppy.emit('upload-started', file)
  160. const opts = {
  161. ...this.opts,
  162. ...(file.tus || {}),
  163. }
  164. if (typeof opts.headers === 'function') {
  165. opts.headers = opts.headers(file)
  166. }
  167. /** @type {RawTusOptions} */
  168. const uploadOptions = {
  169. ...tusDefaultOptions,
  170. ...opts,
  171. }
  172. // We override tus fingerprint to uppy’s `file.id`, since the `file.id`
  173. // now also includes `relativePath` for files added from folders.
  174. // This means you can add 2 identical files, if one is in folder a,
  175. // the other in folder b.
  176. uploadOptions.fingerprint = getFingerprint(file)
  177. uploadOptions.onBeforeRequest = (req) => {
  178. const xhr = req.getUnderlyingObject()
  179. xhr.withCredentials = !!opts.withCredentials
  180. if (typeof opts.onBeforeRequest === 'function') {
  181. opts.onBeforeRequest(req)
  182. }
  183. }
  184. uploadOptions.onError = (err) => {
  185. this.uppy.log(err)
  186. const xhr = err.originalRequest ? err.originalRequest.getUnderlyingObject() : null
  187. if (isNetworkError(xhr)) {
  188. err = new NetworkError(err, xhr)
  189. }
  190. this.resetUploaderReferences(file.id)
  191. queuedRequest.done()
  192. this.uppy.emit('upload-error', file, err)
  193. reject(err)
  194. }
  195. uploadOptions.onProgress = (bytesUploaded, bytesTotal) => {
  196. this.onReceiveUploadUrl(file, upload.url)
  197. this.uppy.emit('upload-progress', file, {
  198. uploader: this,
  199. bytesUploaded,
  200. bytesTotal,
  201. })
  202. }
  203. uploadOptions.onSuccess = () => {
  204. const uploadResp = {
  205. uploadURL: upload.url,
  206. }
  207. this.resetUploaderReferences(file.id)
  208. queuedRequest.done()
  209. this.uppy.emit('upload-success', file, uploadResp)
  210. if (upload.url) {
  211. this.uppy.log(`Download ${upload.file.name} from ${upload.url}`)
  212. }
  213. resolve(upload)
  214. }
  215. const copyProp = (obj, srcProp, destProp) => {
  216. if (hasProperty(obj, srcProp) && !hasProperty(obj, destProp)) {
  217. obj[destProp] = obj[srcProp]
  218. }
  219. }
  220. /** @type {Record<string, string>} */
  221. const meta = {}
  222. const metaFields = Array.isArray(opts.metaFields)
  223. ? opts.metaFields
  224. // Send along all fields by default.
  225. : Object.keys(file.meta)
  226. metaFields.forEach((item) => {
  227. meta[item] = file.meta[item]
  228. })
  229. // tusd uses metadata fields 'filetype' and 'filename'
  230. copyProp(meta, 'type', 'filetype')
  231. copyProp(meta, 'name', 'filename')
  232. uploadOptions.metadata = meta
  233. const upload = new tus.Upload(file.data, uploadOptions)
  234. this.uploaders[file.id] = upload
  235. this.uploaderEvents[file.id] = new EventTracker(this.uppy)
  236. upload.findPreviousUploads().then((previousUploads) => {
  237. const previousUpload = previousUploads[0]
  238. if (previousUpload) {
  239. this.uppy.log(`[Tus] Resuming upload of ${file.id} started at ${previousUpload.creationTime}`)
  240. upload.resumeFromPreviousUpload(previousUpload)
  241. }
  242. })
  243. let queuedRequest = this.requests.run(() => {
  244. if (!file.isPaused) {
  245. upload.start()
  246. }
  247. // Don't do anything here, the caller will take care of cancelling the upload itself
  248. // using resetUploaderReferences(). This is because resetUploaderReferences() has to be
  249. // called when this request is still in the queue, and has not been started yet, too. At
  250. // that point this cancellation function is not going to be called.
  251. // Also, we need to remove the request from the queue _without_ destroying everything
  252. // related to this upload to handle pauses.
  253. return () => {}
  254. })
  255. this.onFileRemove(file.id, (targetFileID) => {
  256. queuedRequest.abort()
  257. this.resetUploaderReferences(file.id, { abort: !!upload.url })
  258. resolve(`upload ${targetFileID} was removed`)
  259. })
  260. this.onPause(file.id, (isPaused) => {
  261. if (isPaused) {
  262. // Remove this file from the queue so another file can start in its place.
  263. queuedRequest.abort()
  264. upload.abort()
  265. } else {
  266. // Resuming an upload should be queued, else you could pause and then
  267. // resume a queued upload to make it skip the queue.
  268. queuedRequest.abort()
  269. queuedRequest = this.requests.run(() => {
  270. upload.start()
  271. return () => {}
  272. })
  273. }
  274. })
  275. this.onPauseAll(file.id, () => {
  276. queuedRequest.abort()
  277. upload.abort()
  278. })
  279. this.onCancelAll(file.id, () => {
  280. queuedRequest.abort()
  281. this.resetUploaderReferences(file.id, { abort: !!upload.url })
  282. resolve(`upload ${file.id} was canceled`)
  283. })
  284. this.onResumeAll(file.id, () => {
  285. queuedRequest.abort()
  286. if (file.error) {
  287. upload.abort()
  288. }
  289. queuedRequest = this.requests.run(() => {
  290. upload.start()
  291. return () => {}
  292. })
  293. })
  294. }).catch((err) => {
  295. this.uppy.emit('upload-error', file, err)
  296. throw err
  297. })
  298. }
  299. /**
  300. * @param {UppyFile} file for use with upload
  301. * @param {number} current file in a queue
  302. * @param {number} total number of files in a queue
  303. * @returns {Promise<void>}
  304. */
  305. uploadRemote (file) {
  306. this.resetUploaderReferences(file.id)
  307. const opts = { ...this.opts }
  308. if (file.tus) {
  309. // Install file-specific upload overrides.
  310. Object.assign(opts, file.tus)
  311. }
  312. this.uppy.emit('upload-started', file)
  313. this.uppy.log(file.remote.url)
  314. if (file.serverToken) {
  315. return this.connectToServerSocket(file)
  316. }
  317. return new Promise((resolve, reject) => {
  318. const Client = file.remote.providerOptions.provider ? Provider : RequestClient
  319. const client = new Client(this.uppy, file.remote.providerOptions)
  320. // !! cancellation is NOT supported at this stage yet
  321. client.post(file.remote.url, {
  322. ...file.remote.body,
  323. endpoint: opts.endpoint,
  324. uploadUrl: opts.uploadUrl,
  325. protocol: 'tus',
  326. size: file.data.size,
  327. headers: opts.headers,
  328. metadata: file.meta,
  329. }).then((res) => {
  330. this.uppy.setFileState(file.id, { serverToken: res.token })
  331. file = this.uppy.getFile(file.id)
  332. return this.connectToServerSocket(file)
  333. }).then(() => {
  334. resolve()
  335. }).catch((err) => {
  336. this.uppy.emit('upload-error', file, err)
  337. reject(err)
  338. })
  339. })
  340. }
  341. /**
  342. * See the comment on the upload() method.
  343. *
  344. * Additionally, when an upload is removed, completed, or cancelled, we need to close the WebSocket connection. This is
  345. * handled by the resetUploaderReferences() function, so the same guidelines apply as in upload().
  346. *
  347. * @param {UppyFile} file
  348. */
  349. connectToServerSocket (file) {
  350. return new Promise((resolve, reject) => {
  351. const token = file.serverToken
  352. const host = getSocketHost(file.remote.companionUrl)
  353. const socket = new Socket({ target: `${host}/api/${token}`, autoOpen: false })
  354. this.uploaderSockets[file.id] = socket
  355. this.uploaderEvents[file.id] = new EventTracker(this.uppy)
  356. this.onFileRemove(file.id, () => {
  357. queuedRequest.abort()
  358. socket.send('cancel', {})
  359. this.resetUploaderReferences(file.id)
  360. resolve(`upload ${file.id} was removed`)
  361. })
  362. this.onPause(file.id, (isPaused) => {
  363. if (isPaused) {
  364. // Remove this file from the queue so another file can start in its place.
  365. queuedRequest.abort()
  366. socket.send('pause', {})
  367. } else {
  368. // Resuming an upload should be queued, else you could pause and then
  369. // resume a queued upload to make it skip the queue.
  370. queuedRequest.abort()
  371. queuedRequest = this.requests.run(() => {
  372. socket.send('resume', {})
  373. return () => {}
  374. })
  375. }
  376. })
  377. this.onPauseAll(file.id, () => {
  378. queuedRequest.abort()
  379. socket.send('pause', {})
  380. })
  381. this.onCancelAll(file.id, () => {
  382. queuedRequest.abort()
  383. socket.send('cancel', {})
  384. this.resetUploaderReferences(file.id)
  385. resolve(`upload ${file.id} was canceled`)
  386. })
  387. this.onResumeAll(file.id, () => {
  388. queuedRequest.abort()
  389. if (file.error) {
  390. socket.send('pause', {})
  391. }
  392. queuedRequest = this.requests.run(() => {
  393. socket.send('resume', {})
  394. return () => {}
  395. })
  396. })
  397. this.onRetry(file.id, () => {
  398. // Only do the retry if the upload is actually in progress;
  399. // else we could try to send these messages when the upload is still queued.
  400. // We may need a better check for this since the socket may also be closed
  401. // for other reasons, like network failures.
  402. if (socket.isOpen) {
  403. socket.send('pause', {})
  404. socket.send('resume', {})
  405. }
  406. })
  407. this.onRetryAll(file.id, () => {
  408. // See the comment in the onRetry() call
  409. if (socket.isOpen) {
  410. socket.send('pause', {})
  411. socket.send('resume', {})
  412. }
  413. })
  414. socket.on('progress', (progressData) => emitSocketProgress(this, progressData, file))
  415. socket.on('error', (errData) => {
  416. const { message } = errData.error
  417. const error = Object.assign(new Error(message), { cause: errData.error })
  418. // If the remote retry optimisation should not be used,
  419. // close the socket—this will tell companion to clear state and delete the file.
  420. if (!this.opts.useFastRemoteRetry) {
  421. this.resetUploaderReferences(file.id)
  422. // Remove the serverToken so that a new one will be created for the retry.
  423. this.uppy.setFileState(file.id, {
  424. serverToken: null,
  425. })
  426. } else {
  427. socket.close()
  428. }
  429. this.uppy.emit('upload-error', file, error)
  430. queuedRequest.done()
  431. reject(error)
  432. })
  433. socket.on('success', (data) => {
  434. const uploadResp = {
  435. uploadURL: data.url,
  436. }
  437. this.uppy.emit('upload-success', file, uploadResp)
  438. this.resetUploaderReferences(file.id)
  439. queuedRequest.done()
  440. resolve()
  441. })
  442. let queuedRequest = this.requests.run(() => {
  443. socket.open()
  444. if (file.isPaused) {
  445. socket.send('pause', {})
  446. }
  447. // Don't do anything here, the caller will take care of cancelling the upload itself
  448. // using resetUploaderReferences(). This is because resetUploaderReferences() has to be
  449. // called when this request is still in the queue, and has not been started yet, too. At
  450. // that point this cancellation function is not going to be called.
  451. // Also, we need to remove the request from the queue _without_ destroying everything
  452. // related to this upload to handle pauses.
  453. return () => {}
  454. })
  455. })
  456. }
  457. /**
  458. * Store the uploadUrl on the file options, so that when Golden Retriever
  459. * restores state, we will continue uploading to the correct URL.
  460. *
  461. * @param {UppyFile} file
  462. * @param {string} uploadURL
  463. */
  464. onReceiveUploadUrl (file, uploadURL) {
  465. const currentFile = this.uppy.getFile(file.id)
  466. if (!currentFile) return
  467. // Only do the update if we didn't have an upload URL yet.
  468. if (!currentFile.tus || currentFile.tus.uploadUrl !== uploadURL) {
  469. this.uppy.log('[Tus] Storing upload url')
  470. this.uppy.setFileState(currentFile.id, {
  471. tus: { ...currentFile.tus, uploadUrl: uploadURL },
  472. })
  473. }
  474. }
  475. /**
  476. * @param {string} fileID
  477. * @param {function(string): void} cb
  478. */
  479. onFileRemove (fileID, cb) {
  480. this.uploaderEvents[fileID].on('file-removed', (file) => {
  481. if (fileID === file.id) cb(file.id)
  482. })
  483. }
  484. /**
  485. * @param {string} fileID
  486. * @param {function(boolean): void} cb
  487. */
  488. onPause (fileID, cb) {
  489. this.uploaderEvents[fileID].on('upload-pause', (targetFileID, isPaused) => {
  490. if (fileID === targetFileID) {
  491. // const isPaused = this.uppy.pauseResume(fileID)
  492. cb(isPaused)
  493. }
  494. })
  495. }
  496. /**
  497. * @param {string} fileID
  498. * @param {function(): void} cb
  499. */
  500. onRetry (fileID, cb) {
  501. this.uploaderEvents[fileID].on('upload-retry', (targetFileID) => {
  502. if (fileID === targetFileID) {
  503. cb()
  504. }
  505. })
  506. }
  507. /**
  508. * @param {string} fileID
  509. * @param {function(): void} cb
  510. */
  511. onRetryAll (fileID, cb) {
  512. this.uploaderEvents[fileID].on('retry-all', () => {
  513. if (!this.uppy.getFile(fileID)) return
  514. cb()
  515. })
  516. }
  517. /**
  518. * @param {string} fileID
  519. * @param {function(): void} cb
  520. */
  521. onPauseAll (fileID, cb) {
  522. this.uploaderEvents[fileID].on('pause-all', () => {
  523. if (!this.uppy.getFile(fileID)) return
  524. cb()
  525. })
  526. }
  527. /**
  528. * @param {string} fileID
  529. * @param {function(): void} cb
  530. */
  531. onCancelAll (fileID, cb) {
  532. this.uploaderEvents[fileID].on('cancel-all', () => {
  533. if (!this.uppy.getFile(fileID)) return
  534. cb()
  535. })
  536. }
  537. /**
  538. * @param {string} fileID
  539. * @param {function(): void} cb
  540. */
  541. onResumeAll (fileID, cb) {
  542. this.uploaderEvents[fileID].on('resume-all', () => {
  543. if (!this.uppy.getFile(fileID)) return
  544. cb()
  545. })
  546. }
  547. /**
  548. * @param {(UppyFile | FailedUppyFile)[]} files
  549. */
  550. uploadFiles (files) {
  551. const promises = files.map((file, i) => {
  552. const current = i + 1
  553. const total = files.length
  554. if ('error' in file && file.error) {
  555. return Promise.reject(new Error(file.error))
  556. } if (file.isRemote) {
  557. // We emit upload-started here, so that it's also emitted for files
  558. // that have to wait due to the `limit` option.
  559. // Don't double-emit upload-started for Golden Retriever-restored files that were already started
  560. if (!file.progress.uploadStarted || !file.isRestored) {
  561. this.uppy.emit('upload-started', file)
  562. }
  563. return this.uploadRemote(file, current, total)
  564. }
  565. // Don't double-emit upload-started for Golden Retriever-restored files that were already started
  566. if (!file.progress.uploadStarted || !file.isRestored) {
  567. this.uppy.emit('upload-started', file)
  568. }
  569. return this.upload(file, current, total)
  570. })
  571. return settle(promises)
  572. }
  573. /**
  574. * @param {string[]} fileIDs
  575. */
  576. handleUpload (fileIDs) {
  577. if (fileIDs.length === 0) {
  578. this.uppy.log('[Tus] No files to upload')
  579. return Promise.resolve()
  580. }
  581. if (this.opts.limit === 0) {
  582. this.uppy.log(
  583. '[Tus] When uploading multiple files at once, consider setting the `limit` option (to `10` for example), to limit the number of concurrent uploads, which helps prevent memory and network issues: https://uppy.io/docs/tus/#limit-0',
  584. 'warning'
  585. )
  586. }
  587. this.uppy.log('[Tus] Uploading...')
  588. const filesToUpload = fileIDs.map((fileID) => this.uppy.getFile(fileID))
  589. return this.uploadFiles(filesToUpload)
  590. .then(() => null)
  591. }
  592. install () {
  593. this.uppy.setState({
  594. capabilities: { ...this.uppy.getState().capabilities, resumableUploads: true },
  595. })
  596. this.uppy.addUploader(this.handleUpload)
  597. this.uppy.on('reset-progress', this.handleResetProgress)
  598. }
  599. uninstall () {
  600. this.uppy.setState({
  601. capabilities: { ...this.uppy.getState().capabilities, resumableUploads: false },
  602. })
  603. this.uppy.removeUploader(this.handleUpload)
  604. }
  605. }