mirror of
https://github.com/transloadit/uppy.git
synced 2026-01-23 02:25:07 +00:00
fixes #6024. ### Problem - `getPlugin()` defaults to `UnknownPlugin`, so methods like `openModal` are not visible , since core is not aware of that plugin type ### Proposed change - Introduce a types-only registry in core: - `export interface PluginTypeRegistry<M extends Meta, B extends Body> {}` - Overload `getPlugin` to return a precise type when the id is a known key of the registry. - add `Dashboard` to PluginTypeRegistry through module augmentation: - `'Dashboard': Dashboard<M, B>`. - When a project imports from `@uppy/dashboard`, its module augmentation extends PluginTypeRegistry, adding the correct type into it - I've added Tests , kept them in a separate file so it's easier to review , once this approach gets approved I'll add them to `Uppy.test.ts` Once this PR gets a positive review I'll add this for other plugins , currently only added for `@uppy/dashboard` **Build with Local tarball can be checked here** https://stackblitz.com/~/github.com/qxprakash/uppy-type-test?file=type_test.ts
641 lines
21 KiB
TypeScript
641 lines
21 KiB
TypeScript
import type { RequestClient } from '@uppy/companion-client'
|
||
import type {
|
||
Body,
|
||
DefinePluginOpts,
|
||
Meta,
|
||
PluginOpts,
|
||
Uppy,
|
||
UppyFile,
|
||
} from '@uppy/core'
|
||
import { BasePlugin, EventManager } from '@uppy/core'
|
||
import {
|
||
filterFilesToEmitUploadStarted,
|
||
filterNonFailedFiles,
|
||
getAllowedMetaFields,
|
||
hasProperty,
|
||
isNetworkError,
|
||
type LocalUppyFile,
|
||
NetworkError,
|
||
RateLimitedQueue,
|
||
} from '@uppy/utils'
|
||
import * as tus from 'tus-js-client'
|
||
import packageJson from '../package.json' with { type: 'json' }
|
||
import getFingerprint from './getFingerprint.js'
|
||
|
||
type RestTusUploadOptions = Omit<
|
||
tus.UploadOptions,
|
||
'onShouldRetry' | 'onBeforeRequest' | 'headers'
|
||
>
|
||
|
||
export type TusDetailedError = tus.DetailedError
|
||
|
||
export type TusBody = { xhr: XMLHttpRequest }
|
||
|
||
export interface TusOpts<M extends Meta, B extends Body>
|
||
extends PluginOpts,
|
||
RestTusUploadOptions {
|
||
endpoint?: string
|
||
headers?:
|
||
| Record<string, string>
|
||
| ((file: UppyFile<M, B>) => Record<string, string>)
|
||
limit?: number
|
||
chunkSize?: number
|
||
onBeforeRequest?: (
|
||
req: tus.HttpRequest,
|
||
file: UppyFile<M, B>,
|
||
) => void | Promise<void>
|
||
onShouldRetry?: (
|
||
err: tus.DetailedError,
|
||
retryAttempt: number,
|
||
options: TusOpts<M, B>,
|
||
next: (e: tus.DetailedError) => boolean,
|
||
) => boolean
|
||
retryDelays?: number[]
|
||
withCredentials?: boolean
|
||
allowedMetaFields?: boolean | string[]
|
||
rateLimitedQueue?: RateLimitedQueue
|
||
}
|
||
export type { TusOpts as TusOptions }
|
||
|
||
/**
|
||
* Extracted from https://github.com/tus/tus-js-client/blob/master/lib/upload.js#L13
|
||
* excepted we removed 'fingerprint' key to avoid adding more dependencies
|
||
*/
|
||
const tusDefaultOptions = {
|
||
endpoint: '',
|
||
|
||
uploadUrl: null,
|
||
metadata: {},
|
||
uploadSize: null,
|
||
|
||
onProgress: null,
|
||
onChunkComplete: null,
|
||
onSuccess: null,
|
||
onError: null,
|
||
|
||
overridePatchMethod: false,
|
||
headers: {},
|
||
addRequestId: false,
|
||
|
||
chunkSize: Infinity,
|
||
retryDelays: [100, 1000, 3000, 5000],
|
||
parallelUploads: 1,
|
||
removeFingerprintOnSuccess: false,
|
||
uploadLengthDeferred: false,
|
||
uploadDataDuringCreation: false,
|
||
} satisfies tus.UploadOptions
|
||
|
||
const defaultOptions = {
|
||
limit: 20,
|
||
retryDelays: tusDefaultOptions.retryDelays,
|
||
withCredentials: false,
|
||
allowedMetaFields: true,
|
||
} satisfies Partial<TusOpts<any, any>>
|
||
|
||
type Opts<M extends Meta, B extends Body> = DefinePluginOpts<
|
||
TusOpts<M, B>,
|
||
keyof typeof defaultOptions
|
||
>
|
||
|
||
declare module '@uppy/utils' {
|
||
export interface LocalUppyFile<M extends Meta, B extends Body> {
|
||
tus?: TusOpts<M, B>
|
||
}
|
||
export interface RemoteUppyFile<M extends Meta, B extends Body> {
|
||
tus?: TusOpts<M, B>
|
||
}
|
||
}
|
||
|
||
declare module '@uppy/core' {
|
||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||
Tus: Tus<M, B>
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Tus resumable file uploader
|
||
*/
|
||
export default class Tus<M extends Meta, B extends Body> extends BasePlugin<
|
||
Opts<M, B>,
|
||
M,
|
||
B
|
||
> {
|
||
static VERSION = packageJson.version
|
||
|
||
#retryDelayIterator
|
||
|
||
requests: RateLimitedQueue
|
||
|
||
uploaders: Record<string, tus.Upload | null>
|
||
|
||
uploaderEvents: Record<string, EventManager<M, B> | null>
|
||
|
||
constructor(uppy: Uppy<M, B>, opts: TusOpts<M, B>) {
|
||
super(uppy, { ...defaultOptions, ...opts })
|
||
this.type = 'uploader'
|
||
this.id = this.opts.id || 'Tus'
|
||
|
||
if (opts?.allowedMetaFields === undefined && 'metaFields' in this.opts) {
|
||
throw new Error(
|
||
'The `metaFields` option has been renamed to `allowedMetaFields`.',
|
||
)
|
||
}
|
||
|
||
if ('autoRetry' in opts) {
|
||
throw new Error(
|
||
'The `autoRetry` option was deprecated and has been removed.',
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Simultaneous upload limiting is shared across all uploads with this plugin.
|
||
*
|
||
* @type {RateLimitedQueue}
|
||
*/
|
||
this.requests =
|
||
this.opts.rateLimitedQueue ?? new RateLimitedQueue(this.opts.limit)
|
||
this.#retryDelayIterator = this.opts.retryDelays?.values()
|
||
|
||
this.uploaders = Object.create(null)
|
||
this.uploaderEvents = Object.create(null)
|
||
}
|
||
|
||
/**
|
||
* Clean up all references for a file's upload: the tus.Upload instance,
|
||
* any events related to the file, and the Companion WebSocket connection.
|
||
*/
|
||
resetUploaderReferences(fileID: string, opts?: { abort: boolean }): void {
|
||
const uploader = this.uploaders[fileID]
|
||
if (uploader) {
|
||
uploader.abort()
|
||
|
||
if (opts?.abort) {
|
||
uploader.abort(true)
|
||
}
|
||
|
||
this.uploaders[fileID] = null
|
||
}
|
||
if (this.uploaderEvents[fileID]) {
|
||
this.uploaderEvents[fileID]!.remove()
|
||
this.uploaderEvents[fileID] = null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create a new Tus upload.
|
||
*
|
||
* A lot can happen during an upload, so this is quite hard to follow!
|
||
* - First, the upload is started. If the file was already paused by the time the upload starts, nothing should happen.
|
||
* If the `limit` option is used, the upload must be queued onto the `this.requests` queue.
|
||
* When an upload starts, we store the tus.Upload instance, and an EventManager instance that manages the event listeners
|
||
* for pausing, cancellation, removal, etc.
|
||
* - While the upload is in progress, it may be paused or cancelled.
|
||
* Pausing aborts the underlying tus.Upload, and removes the upload from the `this.requests` queue. All other state is
|
||
* maintained.
|
||
* Cancelling removes the upload from the `this.requests` queue, and completely aborts the upload-- the `tus.Upload`
|
||
* instance is aborted and discarded, the EventManager instance is destroyed (removing all listeners).
|
||
* Resuming the upload uses the `this.requests` queue as well, to prevent selectively pausing and resuming uploads from
|
||
* bypassing the limit.
|
||
* - After completing an upload, the tus.Upload and EventManager instances are cleaned up, and the upload is marked as done
|
||
* in the `this.requests` queue.
|
||
* - When an upload completed with an error, the same happens as on successful completion, but the `upload()` promise is
|
||
* rejected.
|
||
*
|
||
* When working on this function, keep in mind:
|
||
* - When an upload is completed or cancelled for any reason, the tus.Upload and EventManager instances need to be cleaned
|
||
* up using this.resetUploaderReferences().
|
||
* - When an upload is cancelled or paused, for any reason, it needs to be removed from the `this.requests` queue using
|
||
* `queuedRequest.abort()`.
|
||
* - When an upload is completed for any reason, including errors, it needs to be marked as such using
|
||
* `queuedRequest.done()`.
|
||
* - When an upload is started or resumed, it needs to go through the `this.requests` queue. The `queuedRequest` variable
|
||
* must be updated so the other uses of it are valid.
|
||
* - Before replacing the `queuedRequest` variable, the previous `queuedRequest` must be aborted, else it will keep taking
|
||
* up a spot in the queue.
|
||
*
|
||
*/
|
||
async #uploadLocalFile(
|
||
file: LocalUppyFile<M, B>,
|
||
): Promise<tus.Upload | string> {
|
||
this.resetUploaderReferences(file.id)
|
||
|
||
// Create a new tus upload
|
||
return new Promise<tus.Upload | string>((resolve, reject) => {
|
||
let queuedRequest: ReturnType<RateLimitedQueue['run']>
|
||
// biome-ignore lint/style/useConst: ...
|
||
let qRequest: () => () => void
|
||
// biome-ignore lint/style/useConst: ...
|
||
let upload: tus.Upload
|
||
|
||
const opts = {
|
||
...this.opts,
|
||
...(file.tus || {}),
|
||
}
|
||
|
||
if (typeof opts.headers === 'function') {
|
||
opts.headers = opts.headers(file)
|
||
}
|
||
|
||
const { onShouldRetry, onBeforeRequest, ...commonOpts } = opts
|
||
|
||
const uploadOptions: tus.UploadOptions = {
|
||
...tusDefaultOptions,
|
||
...commonOpts,
|
||
}
|
||
|
||
// We override tus fingerprint to uppy’s `file.id`, since the `file.id`
|
||
// now also includes `relativePath` for files added from folders.
|
||
// This means you can add 2 identical files, if one is in folder a,
|
||
// the other in folder b.
|
||
uploadOptions.fingerprint = getFingerprint(file)
|
||
|
||
uploadOptions.onBeforeRequest = async (req) => {
|
||
const xhr = req.getUnderlyingObject()
|
||
xhr.withCredentials = !!opts.withCredentials
|
||
|
||
let userProvidedPromise: Promise<void> | void
|
||
if (typeof onBeforeRequest === 'function') {
|
||
userProvidedPromise = onBeforeRequest(req, file)
|
||
}
|
||
|
||
if (hasProperty(queuedRequest, 'shouldBeRequeued')) {
|
||
if (!queuedRequest.shouldBeRequeued) return Promise.reject()
|
||
// TODO: switch to `Promise.withResolvers` on the next major if available.
|
||
let done: () => void
|
||
const p = new Promise<void>((res) => {
|
||
done = res
|
||
})
|
||
queuedRequest = this.requests.run(() => {
|
||
if (file.isPaused) {
|
||
queuedRequest.abort()
|
||
}
|
||
done()
|
||
return () => {}
|
||
})
|
||
// If the request has been requeued because it was rate limited by the
|
||
// remote server, we want to wait for `RateLimitedQueue` to dispatch
|
||
// the re-try request.
|
||
// Therefore we create a promise that the queue will resolve when
|
||
// enough time has elapsed to expect not to be rate-limited again.
|
||
// This means we can hold the Tus retry here with a `Promise.all`,
|
||
// together with the returned value of the user provided
|
||
// `onBeforeRequest` option callback (in case it returns a promise).
|
||
// @ts-expect-error it's fine
|
||
await Promise.all([p, userProvidedPromise])
|
||
return undefined
|
||
}
|
||
// @ts-expect-error it's fine
|
||
return userProvidedPromise
|
||
}
|
||
|
||
uploadOptions.onError = (err) => {
|
||
this.uppy.log(err)
|
||
|
||
const xhr =
|
||
(err as tus.DetailedError).originalRequest != null
|
||
? (err as tus.DetailedError).originalRequest.getUnderlyingObject()
|
||
: null
|
||
if (isNetworkError(xhr)) {
|
||
err = new NetworkError(err, xhr)
|
||
}
|
||
|
||
this.resetUploaderReferences(file.id)
|
||
queuedRequest?.abort()
|
||
|
||
if (typeof opts.onError === 'function') {
|
||
opts.onError(err)
|
||
}
|
||
reject(err)
|
||
}
|
||
|
||
uploadOptions.onProgress = (bytesUploaded, bytesTotal) => {
|
||
this.onReceiveUploadUrl(file, upload.url)
|
||
if (typeof opts.onProgress === 'function') {
|
||
opts.onProgress(bytesUploaded, bytesTotal)
|
||
}
|
||
const latestFile = this.uppy.getFile(file.id)
|
||
this.uppy.emit('upload-progress', latestFile, {
|
||
uploadStarted: latestFile.progress.uploadStarted ?? 0,
|
||
bytesUploaded,
|
||
bytesTotal,
|
||
})
|
||
}
|
||
|
||
uploadOptions.onSuccess = (payload) => {
|
||
const uploadResp: UppyFile<M, B>['response'] = {
|
||
uploadURL: upload.url ?? undefined,
|
||
status: 200,
|
||
body: {
|
||
// We have to put `as XMLHttpRequest` because tus-js-client
|
||
// returns `any`, as the type differs in Node.js and the browser.
|
||
// In the browser it's always `XMLHttpRequest`.
|
||
xhr: payload.lastResponse.getUnderlyingObject() as XMLHttpRequest,
|
||
// Body extends Record<string, unknown> and thus `xhr` is not known
|
||
// but we export the `TusBody` type, which people pass as a generic into the Uppy class,
|
||
// so on the implementer side it works as expected.
|
||
} as unknown as B,
|
||
}
|
||
|
||
this.uppy.emit('upload-success', this.uppy.getFile(file.id), uploadResp)
|
||
|
||
this.resetUploaderReferences(file.id)
|
||
queuedRequest.done()
|
||
|
||
if (upload.url) {
|
||
// @ts-expect-error not typed in tus-js-client
|
||
const { name } = upload.file
|
||
this.uppy.log(`Download ${name} from ${upload.url}`)
|
||
}
|
||
if (typeof opts.onSuccess === 'function') {
|
||
opts.onSuccess(payload)
|
||
}
|
||
|
||
resolve(upload)
|
||
}
|
||
|
||
const defaultOnShouldRetry = (err: tus.DetailedError) => {
|
||
const status = err?.originalResponse?.getStatus()
|
||
|
||
if (status === 429) {
|
||
// HTTP 429 Too Many Requests => to avoid the whole download to fail, pause all requests.
|
||
if (!this.requests.isPaused) {
|
||
const next = this.#retryDelayIterator?.next()
|
||
if (next == null || next.done) {
|
||
return false
|
||
}
|
||
this.requests.rateLimit(next.value)
|
||
}
|
||
} else if (
|
||
status != null &&
|
||
status >= 400 &&
|
||
status < 500 &&
|
||
status !== 409 &&
|
||
status !== 423
|
||
) {
|
||
// HTTP 4xx, the server won't send anything, it's doesn't make sense to retry
|
||
// HTTP 409 Conflict (happens if the Upload-Offset header does not match the one on the server)
|
||
// HTTP 423 Locked (happens when a paused download is resumed too quickly)
|
||
return false
|
||
} else if (
|
||
typeof navigator !== 'undefined' &&
|
||
navigator.onLine === false
|
||
) {
|
||
// The navigator is offline, let's wait for it to come back online.
|
||
if (!this.requests.isPaused) {
|
||
this.requests.pause()
|
||
window.addEventListener(
|
||
'online',
|
||
() => {
|
||
this.requests.resume()
|
||
},
|
||
{ once: true },
|
||
)
|
||
}
|
||
}
|
||
queuedRequest.abort()
|
||
queuedRequest = {
|
||
shouldBeRequeued: true,
|
||
abort() {
|
||
this.shouldBeRequeued = false
|
||
},
|
||
done() {
|
||
throw new Error(
|
||
'Cannot mark a queued request as done: this indicates a bug',
|
||
)
|
||
},
|
||
fn() {
|
||
throw new Error('Cannot run a queued request: this indicates a bug')
|
||
},
|
||
}
|
||
return true
|
||
}
|
||
|
||
if (onShouldRetry != null) {
|
||
uploadOptions.onShouldRetry = (
|
||
error: tus.DetailedError,
|
||
retryAttempt: number,
|
||
) => onShouldRetry(error, retryAttempt, opts, defaultOnShouldRetry)
|
||
} else {
|
||
uploadOptions.onShouldRetry = defaultOnShouldRetry
|
||
}
|
||
|
||
const copyProp = (
|
||
obj: Record<string, unknown>,
|
||
srcProp: string,
|
||
destProp: string,
|
||
) => {
|
||
if (hasProperty(obj, srcProp) && !hasProperty(obj, destProp)) {
|
||
obj[destProp] = obj[srcProp]
|
||
}
|
||
}
|
||
|
||
// We can't use `allowedMetaFields` to index generic M
|
||
// and we also don't care about the type specifically here,
|
||
// we just want to pass the meta fields along.
|
||
const meta: Record<string, string> = {}
|
||
const allowedMetaFields = getAllowedMetaFields(
|
||
opts.allowedMetaFields,
|
||
file.meta,
|
||
)
|
||
allowedMetaFields.forEach((item) => {
|
||
// tus type definition for metadata only accepts `Record<string, string>`
|
||
// but in reality (at runtime) it accepts `Record<string, unknown>`
|
||
// tus internally converts everything into a string, but let's do it here instead to be explicit.
|
||
// because Uppy can have anything inside meta values, (for example relativePath: null is often sent by uppy)
|
||
meta[item] = String(file.meta[item])
|
||
})
|
||
|
||
// tusd uses metadata fields 'filetype' and 'filename'
|
||
copyProp(meta, 'type', 'filetype')
|
||
copyProp(meta, 'name', 'filename')
|
||
|
||
uploadOptions.metadata = meta
|
||
|
||
if (file.data == null) throw new Error('File data is empty')
|
||
upload = new tus.Upload(file.data, uploadOptions)
|
||
this.uploaders[file.id] = upload
|
||
const eventManager = new EventManager(this.uppy)
|
||
this.uploaderEvents[file.id] = eventManager
|
||
|
||
qRequest = () => {
|
||
if (!file.isPaused) {
|
||
upload.start()
|
||
}
|
||
// Don't do anything here, the caller will take care of cancelling the upload itself
|
||
// using resetUploaderReferences(). This is because resetUploaderReferences() has to be
|
||
// called when this request is still in the queue, and has not been started yet, too. At
|
||
// that point this cancellation function is not going to be called.
|
||
// Also, we need to remove the request from the queue _without_ destroying everything
|
||
// related to this upload to handle pauses.
|
||
return () => {}
|
||
}
|
||
|
||
upload.findPreviousUploads().then((previousUploads) => {
|
||
const previousUpload = previousUploads[0]
|
||
if (previousUpload) {
|
||
this.uppy.log(
|
||
`[Tus] Resuming upload of ${file.id} started at ${previousUpload.creationTime}`,
|
||
)
|
||
upload.resumeFromPreviousUpload(previousUpload)
|
||
}
|
||
queuedRequest = this.requests.run(qRequest)
|
||
})
|
||
|
||
eventManager.onFileRemove(file.id, (targetFileID) => {
|
||
queuedRequest.abort()
|
||
this.resetUploaderReferences(file.id, { abort: !!upload.url })
|
||
resolve(`upload ${targetFileID} was removed`)
|
||
})
|
||
|
||
eventManager.onPause(file.id, (isPaused) => {
|
||
queuedRequest.abort()
|
||
if (isPaused) {
|
||
// Remove this file from the queue so another file can start in its place.
|
||
upload.abort()
|
||
} else {
|
||
// Resuming an upload should be queued, else you could pause and then
|
||
// resume a queued upload to make it skip the queue.
|
||
queuedRequest = this.requests.run(qRequest)
|
||
}
|
||
})
|
||
|
||
eventManager.onPauseAll(file.id, () => {
|
||
queuedRequest.abort()
|
||
upload.abort()
|
||
})
|
||
|
||
eventManager.onCancelAll(file.id, () => {
|
||
queuedRequest.abort()
|
||
this.resetUploaderReferences(file.id, { abort: !!upload.url })
|
||
resolve(`upload ${file.id} was canceled`)
|
||
})
|
||
|
||
eventManager.onResumeAll(file.id, () => {
|
||
queuedRequest.abort()
|
||
if (file.error) {
|
||
upload.abort()
|
||
}
|
||
queuedRequest = this.requests.run(qRequest)
|
||
})
|
||
}).catch((err) => {
|
||
this.uppy.emit('upload-error', file, err)
|
||
throw err
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Store the uploadUrl on the file options, so that when Golden Retriever
|
||
* restores state, we will continue uploading to the correct URL.
|
||
*/
|
||
onReceiveUploadUrl(file: UppyFile<M, B>, uploadURL: string | null): void {
|
||
const currentFile = this.uppy.getFile(file.id)
|
||
if (!currentFile) return
|
||
// Only do the update if we didn't have an upload URL yet.
|
||
if (!currentFile.tus || currentFile.tus.uploadUrl !== uploadURL) {
|
||
this.uppy.log('[Tus] Storing upload url')
|
||
this.uppy.setFileState(currentFile.id, {
|
||
tus: { ...currentFile.tus, uploadUrl: uploadURL },
|
||
})
|
||
}
|
||
}
|
||
|
||
#getCompanionClientArgs(file: UppyFile<M, B>) {
|
||
const opts = { ...this.opts }
|
||
|
||
if (file.tus) {
|
||
// Install file-specific upload overrides.
|
||
Object.assign(opts, file.tus)
|
||
}
|
||
|
||
if (typeof opts.headers === 'function') {
|
||
opts.headers = opts.headers(file)
|
||
}
|
||
|
||
return {
|
||
...('remote' in file && file.remote.body),
|
||
endpoint: opts.endpoint,
|
||
uploadUrl: opts.uploadUrl,
|
||
protocol: 'tus',
|
||
size: file.data!.size,
|
||
headers: opts.headers,
|
||
metadata: file.meta,
|
||
}
|
||
}
|
||
|
||
async #uploadFiles(files: UppyFile<M, B>[]) {
|
||
const filesFiltered = filterNonFailedFiles(files)
|
||
const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered)
|
||
this.uppy.emit('upload-start', filesToEmit)
|
||
|
||
await Promise.allSettled(
|
||
filesFiltered.map((file) => {
|
||
if (file.isRemote) {
|
||
const getQueue = () => this.requests
|
||
const controller = new AbortController()
|
||
|
||
const removedHandler = (removedFile: UppyFile<M, B>) => {
|
||
if (removedFile.id === file.id) controller.abort()
|
||
}
|
||
this.uppy.on('file-removed', removedHandler)
|
||
|
||
const uploadPromise = this.uppy
|
||
.getRequestClientForFile<RequestClient<M, B>>(file)
|
||
.uploadRemoteFile(file, this.#getCompanionClientArgs(file), {
|
||
signal: controller.signal,
|
||
getQueue,
|
||
})
|
||
|
||
this.requests.wrapSyncFunction(
|
||
() => {
|
||
this.uppy.off('file-removed', removedHandler)
|
||
},
|
||
{ priority: -1 },
|
||
)()
|
||
|
||
return uploadPromise
|
||
}
|
||
|
||
return this.#uploadLocalFile(file)
|
||
}),
|
||
)
|
||
}
|
||
|
||
#handleUpload = async (fileIDs: string[]) => {
|
||
if (fileIDs.length === 0) {
|
||
this.uppy.log('[Tus] No files to upload')
|
||
return
|
||
}
|
||
|
||
if (this.opts.limit === 0) {
|
||
this.uppy.log(
|
||
'[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',
|
||
'warning',
|
||
)
|
||
}
|
||
|
||
this.uppy.log('[Tus] Uploading...')
|
||
const filesToUpload = this.uppy.getFilesByIds(fileIDs)
|
||
|
||
await this.#uploadFiles(filesToUpload)
|
||
}
|
||
|
||
install(): void {
|
||
this.uppy.setState({
|
||
capabilities: {
|
||
...this.uppy.getState().capabilities,
|
||
resumableUploads: true,
|
||
},
|
||
})
|
||
this.uppy.addUploader(this.#handleUpload)
|
||
}
|
||
|
||
uninstall(): void {
|
||
this.uppy.setState({
|
||
capabilities: {
|
||
...this.uppy.getState().capabilities,
|
||
resumableUploads: false,
|
||
},
|
||
})
|
||
this.uppy.removeUploader(this.#handleUpload)
|
||
}
|
||
}
|