index.js 25 KB

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