update imports after merge remove stale files

This commit is contained in:
prakash 2026-07-07 18:40:40 +05:30
parent e51c1ad99b
commit 0c7f02aef2
No known key found for this signature in database
6 changed files with 7 additions and 716 deletions

View file

@ -1,444 +0,0 @@
import type { Body, Meta, UppyFile } from '@uppy/core'
import type {
RateLimitedQueue,
WrapPromiseFunctionType,
} from '@uppy/core/utils'
import type AwsS3Multipart from './index.js'
import type {
AwsS3MultipartOptions,
AwsS3UploadParameters,
uploadPartBytes,
} from './index.js'
import { type Chunk, pausingUploadReason } from './MultipartUploader.js'
import type { UploadPartBytesResult, UploadResult } from './utils.js'
import { throwIfAborted } from './utils.js'
function removeMetadataFromURL(urlString: string) {
const urlObject = new URL(urlString)
urlObject.search = ''
urlObject.hash = ''
return urlObject.href
}
export class HTTPCommunicationQueue<M extends Meta, B extends Body> {
#abortMultipartUpload!: WrapPromiseFunctionType<
AwsS3Multipart<M, B>['abortMultipartUpload']
>
#cache = new WeakMap()
#createMultipartUpload!: WrapPromiseFunctionType<
AwsS3Multipart<M, B>['createMultipartUpload']
>
#fetchSignature!: WrapPromiseFunctionType<AwsS3Multipart<M, B>['signPart']>
#getUploadParameters!: WrapPromiseFunctionType<
AwsS3Multipart<M, B>['getUploadParameters']
>
#listParts!: WrapPromiseFunctionType<AwsS3Multipart<M, B>['listParts']>
#previousRetryDelay!: number
#requests
#retryDelays!: { values: () => Iterator<number> }
#sendCompletionRequest!: WrapPromiseFunctionType<
AwsS3Multipart<M, B>['completeMultipartUpload']
>
#setS3MultipartState
#uploadPartBytes!: WrapPromiseFunctionType<uploadPartBytes>
#getFile
constructor(
requests: RateLimitedQueue,
options: AwsS3MultipartOptions<M, B>,
setS3MultipartState: (file: UppyFile<M, B>, result: UploadResult) => void,
getFile: (file: UppyFile<M, B>) => UppyFile<M, B>,
) {
this.#requests = requests
this.#setS3MultipartState = setS3MultipartState
this.#getFile = getFile
this.setOptions(options)
}
setOptions(options: Partial<AwsS3MultipartOptions<M, B>>): void {
const requests = this.#requests
if ('abortMultipartUpload' in options) {
this.#abortMultipartUpload = requests.wrapPromiseFunction(
options.abortMultipartUpload as any,
{ priority: 1 },
)
}
if ('createMultipartUpload' in options) {
this.#createMultipartUpload = requests.wrapPromiseFunction(
options.createMultipartUpload as any,
{ priority: -1 },
)
}
if ('signPart' in options) {
this.#fetchSignature = requests.wrapPromiseFunction(
options.signPart as any,
)
}
if ('listParts' in options) {
this.#listParts = requests.wrapPromiseFunction(options.listParts as any)
}
if ('completeMultipartUpload' in options) {
this.#sendCompletionRequest = requests.wrapPromiseFunction(
options.completeMultipartUpload as any,
{ priority: 1 },
)
}
if ('retryDelays' in options) {
this.#retryDelays = options.retryDelays ?? []
}
if ('uploadPartBytes' in options) {
this.#uploadPartBytes = requests.wrapPromiseFunction(
options.uploadPartBytes as any,
{ priority: Infinity },
)
}
if ('getUploadParameters' in options) {
this.#getUploadParameters = requests.wrapPromiseFunction(
options.getUploadParameters as any,
)
}
}
async #shouldRetry(err: any, retryDelayIterator: Iterator<number>) {
const requests = this.#requests
const status = err?.source?.status
// TODO: this retry logic is taken out of Tus. We should have a centralized place for retrying,
// perhaps the rate limited queue, and dedupe all plugins with that.
if (status == null) {
return false
}
if (status === 403 && err.message === 'Request has expired') {
if (!requests.isPaused) {
// We don't want to exhaust the retryDelayIterator as long as there are
// more than one request in parallel, to give slower connection a chance
// to catch up with the expiry set in Companion.
if (requests.limit === 1 || this.#previousRetryDelay == null) {
const next = retryDelayIterator.next()
if (next == null || next.done) {
return false
}
// If there are more than 1 request done in parallel, the RLQ limit is
// decreased and the failed request is requeued after waiting for a bit.
// If there is only one request in parallel, the limit can't be
// decreased, so we iterate over `retryDelayIterator` as we do for
// other failures.
// `#previousRetryDelay` caches the value so we can re-use it next time.
this.#previousRetryDelay = next.value
}
// No need to stop the other requests, we just want to lower the limit.
requests.rateLimit(0)
await new Promise((resolve) =>
setTimeout(resolve, this.#previousRetryDelay),
)
}
} else if (status === 429) {
// HTTP 429 Too Many Requests => to avoid the whole download to fail, pause all requests.
if (!requests.isPaused) {
const next = retryDelayIterator.next()
if (next == null || next.done) {
return false
}
requests.rateLimit(next.value)
}
} else if (status > 400 && status < 500 && status !== 409) {
// HTTP 4xx, the server won't send anything, it's doesn't make sense to retry
return false
} else if (typeof navigator !== 'undefined' && navigator.onLine === false) {
// The navigator is offline, let's wait for it to come back online.
if (!requests.isPaused) {
requests.pause()
window.addEventListener(
'online',
() => {
requests.resume()
},
{ once: true },
)
}
} else {
// Other error code means the request can be retried later.
const next = retryDelayIterator.next()
if (next == null || next.done) {
return false
}
await new Promise((resolve) => setTimeout(resolve, next.value))
}
return true
}
async getUploadId(
file: UppyFile<M, B>,
signal: AbortSignal,
): Promise<UploadResult> {
let cachedResult: Promise<UploadResult> | UploadResult | undefined
// As the cache is updated asynchronously, there could be a race condition
// where we just miss a new result so we loop here until we get nothing back,
// at which point it's out turn to create a new cache entry.
for (;;) {
if (file.data == null) throw new Error('File data is empty')
cachedResult = this.#cache.get(file.data)
if (cachedResult == null) break
try {
return await cachedResult
} catch {
// In case of failure, we want to ignore the cached error.
// At this point, either there's a new cached value, or we'll exit the loop a create a new one.
}
}
const promise = this.#createMultipartUpload(this.#getFile(file), signal)
const abortPromise = () => {
if (file.data == null) throw new Error('File data is empty')
promise.abort(signal.reason)
this.#cache.delete(file.data)
}
signal.addEventListener('abort', abortPromise, { once: true })
this.#cache.set(file.data, promise)
promise.then(
async (result) => {
signal.removeEventListener('abort', abortPromise)
this.#setS3MultipartState(file, result)
if (file.data == null) throw new Error('File data is empty')
this.#cache.set(file.data, result)
},
() => {
signal.removeEventListener('abort', abortPromise)
if (file.data == null) throw new Error('File data is empty')
this.#cache.delete(file.data)
},
)
return promise
}
async abortFileUpload(file: UppyFile<M, B>): Promise<void> {
if (file.data == null) throw new Error('File data is empty')
const result = this.#cache.get(file.data)
if (result == null) {
// If the createMultipartUpload request never was made, we don't
// need to send the abortMultipartUpload request.
return
}
// Remove the cache entry right away for follow-up requests do not try to
// use the soon-to-be aborted cached values.
this.#cache.delete(file.data)
this.#setS3MultipartState(file, Object.create(null))
let awaitedResult: UploadResult
try {
awaitedResult = await result
} catch {
// If the cached result rejects, there's nothing to abort.
return
}
await this.#abortMultipartUpload(this.#getFile(file), awaitedResult)
}
async #nonMultipartUpload(
file: UppyFile<M, B>,
chunk: Chunk,
signal?: AbortSignal,
) {
const {
method = 'POST',
url,
fields,
headers,
} = await this.#getUploadParameters(this.#getFile(file), {
signal,
}).abortOn(signal)
let body: FormData | Blob
const data = chunk.getData()
if (method.toUpperCase() === 'POST') {
const formData = new FormData()
Object.entries(fields!).forEach(([key, value]) => {
formData.set(key, value)
})
formData.set('file', data)
body = formData
} else {
body = data
}
const { onProgress, onComplete } = chunk
const result = (await this.#uploadPartBytes({
signature: { url, headers, method } as any,
body,
size: data.size,
onProgress,
onComplete,
signal,
}).abortOn(signal)) as unknown as B // todo this doesn't make sense
// Note: `fields.key` is not returned by old Companion versions.
// See https://github.com/transloadit/uppy/pull/5602
const key = fields?.key
this.#setS3MultipartState(file, { key: key! })
return {
...result,
location:
(result.location as string | undefined) ?? removeMetadataFromURL(url),
bucket: fields?.bucket,
key,
}
}
async uploadFile(
file: UppyFile<M, B>,
chunks: Chunk[],
signal: AbortSignal,
): Promise<B & Partial<UploadPartBytesResult>> {
throwIfAborted(signal)
if (chunks.length === 1 && !chunks[0].shouldUseMultipart) {
return this.#nonMultipartUpload(file, chunks[0], signal)
}
const { uploadId, key } = await this.getUploadId(file, signal)
throwIfAborted(signal)
try {
const parts = await Promise.all(
chunks.map((chunk, i) => this.uploadChunk(file, i + 1, chunk, signal)),
)
throwIfAborted(signal)
return await this.#sendCompletionRequest(
this.#getFile(file),
{ key, uploadId, parts, signal },
signal,
).abortOn(signal)
} catch (err) {
if (err?.cause !== pausingUploadReason && err?.name !== 'AbortError') {
// We purposefully don't wait for the promise and ignore its status,
// because we want the error `err` to bubble up ASAP to report it to the
// user. A failure to abort is not that big of a deal anyway.
this.abortFileUpload(file)
}
throw err
}
}
restoreUploadFile(file: UppyFile<M, B>, uploadIdAndKey: UploadResult): void {
if (file.data == null) throw new Error('File data is empty')
this.#cache.set(file.data, uploadIdAndKey)
}
async resumeUploadFile(
file: UppyFile<M, B>,
chunks: Array<Chunk | null>,
signal: AbortSignal,
): Promise<B> {
throwIfAborted(signal)
if (
chunks.length === 1 &&
chunks[0] != null &&
!chunks[0].shouldUseMultipart
) {
return this.#nonMultipartUpload(file, chunks[0], signal)
}
const { uploadId, key } = await this.getUploadId(file, signal)
throwIfAborted(signal)
const alreadyUploadedParts = await this.#listParts(
this.#getFile(file),
{ uploadId, key, signal },
signal,
).abortOn(signal)
throwIfAborted(signal)
const parts = await Promise.all(
chunks.map((chunk, i) => {
const partNumber = i + 1
const alreadyUploadedInfo = alreadyUploadedParts.find(
({ PartNumber }) => PartNumber === partNumber,
)
if (alreadyUploadedInfo == null) {
return this.uploadChunk(file, partNumber, chunk!, signal)
}
// Already uploaded chunks are set to null. If we are restoring the upload, we need to mark it as already uploaded.
chunk?.setAsUploaded?.()
return { PartNumber: partNumber, ETag: alreadyUploadedInfo.ETag }
}),
)
throwIfAborted(signal)
return this.#sendCompletionRequest(
this.#getFile(file),
{ key, uploadId, parts, signal },
signal,
).abortOn(signal)
}
async uploadChunk(
file: UppyFile<M, B>,
partNumber: number,
chunk: Chunk,
signal: AbortSignal,
): Promise<UploadPartBytesResult & { PartNumber: number }> {
throwIfAborted(signal)
const { uploadId, key } = await this.getUploadId(file, signal)
const signatureRetryIterator = this.#retryDelays.values()
const chunkRetryIterator = this.#retryDelays.values()
const shouldRetrySignature = () => {
const next = signatureRetryIterator.next()
if (next == null || next.done) {
return null
}
return next.value
}
for (;;) {
throwIfAborted(signal)
const chunkData = chunk.getData()
const { onProgress, onComplete } = chunk
let signature: AwsS3UploadParameters
try {
signature = await this.#fetchSignature(this.#getFile(file), {
// Always defined for multipart uploads
uploadId: uploadId!,
key,
partNumber,
body: chunkData,
signal,
}).abortOn(signal)
} catch (err) {
const timeout = shouldRetrySignature()
if (timeout == null || signal.aborted) {
throw err
}
await new Promise((resolve) => setTimeout(resolve, timeout))
continue
}
throwIfAborted(signal)
try {
return {
PartNumber: partNumber,
...(await this.#uploadPartBytes({
signature,
body: chunkData,
size: chunkData.size,
onProgress,
onComplete,
signal,
}).abortOn(signal)),
}
} catch (err) {
if (!(await this.#shouldRetry(err, chunkRetryIterator))) throw err
}
}
}
}

View file

@ -1,265 +0,0 @@
import type { Uppy } from '@uppy/core'
import type { Body, Meta, UppyFile } from '@uppy/core/utils'
import { AbortController } from '@uppy/core/utils'
import type { HTTPCommunicationQueue } from './HTTPCommunicationQueue.js'
const MB = 1024 * 1024
interface MultipartUploaderOptions<M extends Meta, B extends Body> {
getChunkSize?: (file: { size: number }) => number
onProgress?: (bytesUploaded: number, bytesTotal: number) => void
onPartComplete?: (part: { PartNumber: number; ETag: string }) => void
shouldUseMultipart?: boolean | ((file: UppyFile<M, B>) => boolean)
onSuccess?: (result: B) => void
onError?: (err: unknown) => void
companionComm: HTTPCommunicationQueue<M, B>
file: UppyFile<M, B>
log: Uppy<M, B>['log']
uploadId?: string
key: string
}
const defaultOptions = {
getChunkSize(file: { size: number }) {
return Math.ceil(file.size / 10000)
},
onProgress() {},
onPartComplete() {},
onSuccess() {},
onError(err: unknown) {
throw err
},
} satisfies Partial<MultipartUploaderOptions<any, any>>
export interface Chunk {
getData: () => Blob
onProgress: (ev: ProgressEvent) => void
onComplete: (etag: string) => void
shouldUseMultipart: boolean
setAsUploaded?: () => void
}
function ensureInt<T>(value: T): T extends number | string ? number : never {
if (typeof value === 'string') {
// @ts-expect-error TS is not able to recognize it's fine.
return parseInt(value, 10)
}
if (typeof value === 'number') {
// @ts-expect-error TS is not able to recognize it's fine.
return value
}
throw new TypeError('Expected a number')
}
export const pausingUploadReason = Symbol('pausing upload, not an actual error')
/**
* A MultipartUploader instance is used per file upload to determine whether a
* upload should be done as multipart or as a regular S3 upload
* (based on the user-provided `shouldUseMultipart` option value) and to manage
* the chunk splitting.
*/
class MultipartUploader<M extends Meta, B extends Body> {
options: MultipartUploaderOptions<M, B> &
Required<Pick<MultipartUploaderOptions<M, B>, keyof typeof defaultOptions>>
#abortController = new AbortController()
#chunks: Array<Chunk | null> = []
#chunkState: { uploaded: number; etag?: string; done?: boolean }[] = []
/**
* The (un-chunked) data to upload.
*/
#data: Blob
#file: UppyFile<M, B>
#uploadHasStarted = false
#onError: (err: unknown) => void
#onSuccess: (result: B) => void
#shouldUseMultipart: MultipartUploaderOptions<M, B>['shouldUseMultipart']
#isRestoring: boolean
#onReject = (err: unknown) =>
(err as any)?.cause === pausingUploadReason ? null : this.#onError(err)
#maxMultipartParts = 10_000
#minPartSize = 5 * MB
constructor(data: Blob, options: MultipartUploaderOptions<M, B>) {
this.options = {
...defaultOptions,
...options,
}
// Use default `getChunkSize` if it was null or something
this.options.getChunkSize ??= defaultOptions.getChunkSize
this.#data = data
this.#file = options.file
this.#onSuccess = this.options.onSuccess
this.#onError = this.options.onError
this.#shouldUseMultipart = this.options.shouldUseMultipart
// When we are restoring an upload, we already have an UploadId and a Key. Otherwise
// we need to call `createMultipartUpload` to get an `uploadId` and a `key`.
// Non-multipart uploads are not restorable.
this.#isRestoring = (options.uploadId && options.key) as any as boolean
this.#initChunks()
}
// initChunks checks the user preference for using multipart uploads (opts.shouldUseMultipart)
// and calculates the optimal part size. When using multipart part uploads every part except for the last has
// to be at least 5 MB and there can be no more than 10K parts.
// This means we sometimes need to change the preferred part size from the user in order to meet these requirements.
#initChunks() {
const fileSize = this.#data.size
const shouldUseMultipart =
typeof this.#shouldUseMultipart === 'function'
? this.#shouldUseMultipart(this.#file)
: Boolean(this.#shouldUseMultipart)
if (shouldUseMultipart && fileSize > this.#minPartSize) {
// At least 5MB per request:
let chunkSize = Math.max(
this.options.getChunkSize(this.#data) as number, // Math.max can take undefined but TS does not think so
this.#minPartSize,
)
let arraySize = Math.floor(fileSize / chunkSize)
// At most 10k requests per file:
if (arraySize > this.#maxMultipartParts) {
arraySize = this.#maxMultipartParts
chunkSize = fileSize / this.#maxMultipartParts
}
this.#chunks = Array(arraySize)
for (let offset = 0, j = 0; offset < fileSize; offset += chunkSize, j++) {
const end = Math.min(fileSize, offset + chunkSize)
// Defer data fetching/slicing until we actually need the data, because it's slow if we have a lot of files
const getData = () => {
const i2 = offset
return this.#data.slice(i2, end)
}
this.#chunks[j] = {
getData,
onProgress: this.#onPartProgress(j),
onComplete: this.#onPartComplete(j),
shouldUseMultipart,
}
if (this.#isRestoring) {
const size =
offset + chunkSize > fileSize ? fileSize - offset : chunkSize
// setAsUploaded is called by listPart, to keep up-to-date the
// quantity of data that is left to actually upload.
this.#chunks[j]!.setAsUploaded = () => {
this.#chunks[j] = null
this.#chunkState[j].uploaded = size
}
}
}
} else {
this.#chunks = [
{
getData: () => this.#data,
onProgress: this.#onPartProgress(0),
onComplete: this.#onPartComplete(0),
shouldUseMultipart,
},
]
}
this.#chunkState = this.#chunks.map(() => ({ uploaded: 0 }))
}
#createUpload() {
this.options.companionComm
.uploadFile(
this.#file,
this.#chunks as Chunk[],
this.#abortController.signal,
)
.then(this.#onSuccess, this.#onReject)
this.#uploadHasStarted = true
}
#resumeUpload() {
this.options.companionComm
.resumeUploadFile(this.#file, this.#chunks, this.#abortController.signal)
.then(this.#onSuccess, this.#onReject)
}
#onPartProgress = (index: number) => (ev: ProgressEvent) => {
if (!ev.lengthComputable) return
this.#chunkState[index].uploaded = ensureInt(ev.loaded)
const totalUploaded = this.#chunkState.reduce((n, c) => n + c.uploaded, 0)
this.options.onProgress(totalUploaded, this.#data.size)
}
#onPartComplete = (index: number) => (etag: string) => {
// This avoids the net::ERR_OUT_OF_MEMORY in Chromium Browsers.
this.#chunks[index] = null
this.#chunkState[index].etag = etag
this.#chunkState[index].done = true
const part = {
PartNumber: index + 1,
ETag: etag,
}
this.options.onPartComplete(part)
}
#abortUpload() {
this.#abortController.abort()
this.options.companionComm
.abortFileUpload(this.#file)
.catch((err: unknown) => this.options.log(err as Error))
}
start(): void {
if (this.#uploadHasStarted) {
if (!this.#abortController.signal.aborted)
this.#abortController.abort(pausingUploadReason)
this.#abortController = new AbortController()
this.#resumeUpload()
} else if (this.#isRestoring) {
this.options.companionComm.restoreUploadFile(this.#file, {
uploadId: this.options.uploadId,
key: this.options.key,
})
this.#resumeUpload()
} else {
this.#createUpload()
}
}
pause(): void {
this.#abortController.abort(pausingUploadReason)
// Swap it out for a new controller, because this instance may be resumed later.
this.#abortController = new AbortController()
}
abort(opts?: { really?: boolean }): void {
if (opts?.really) this.#abortUpload()
else this.pause()
}
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: accessed via Symbol in tests
private [Symbol.for('uppy test: getChunkState')]() {
return this.#chunkState
}
}
export default MultipartUploader

View file

@ -1,5 +1,5 @@
import { EventManager, type Uppy } from '@uppy/core'
import type { Body, LocalUppyFile, Meta } from '@uppy/utils'
import type { Body, LocalUppyFile, Meta } from '@uppy/core/utils'
import type S3Client from './s3-client/S3Client.js'
/** Persisted S3 multipart state for Golden Retriever resume support */
@ -8,7 +8,7 @@ interface S3MultipartState {
key: string
}
declare module '@uppy/utils' {
declare module '@uppy/core/utils' {
export interface LocalUppyFile<M extends Meta, B extends Body> {
s3Multipart?: S3MultipartState
}

View file

@ -1,4 +1,4 @@
import type { RequestClient } from '@uppy/companion-client'
import type { RequestClient } from '@uppy/core/companion-client'
import {
BasePlugin,
type DefinePluginOpts,
@ -11,13 +11,13 @@ import type {
Meta,
RemoteUppyFile,
UppyFile,
} from '@uppy/utils'
} from '@uppy/core/utils'
import {
filterFilesToEmitUploadStarted,
filterFilesToUpload,
getAllowedMetaFields,
TaskQueue,
} from '@uppy/utils'
} from '@uppy/core/utils'
import packageJson from '../package.json' with { type: 'json' }
import S3Uploader, { type UploadResult } from './S3Uploader.js'
import S3Companion from './s3-client/CompanionS3.js'

View file

@ -1,4 +1,4 @@
import { fetcher } from '@uppy/utils'
import { fetcher } from '@uppy/core/utils'
import * as C from './consts.js'
import type * as IT from './types.js'

View file

@ -280,7 +280,7 @@ class S3mini extends S3Client {
}
/**
* Core XHR upload implementation using @uppy/utils/fetcher.
* Core XHR upload implementation using @uppy/core/utils fetcher.
*
* Features:
* - Automatic retry with exponential backoff (3 attempts)