put signal into missing methods

and make all methods have single arg params
This commit is contained in:
Mikael Finstad 2026-07-16 21:03:05 +02:00
parent de7d9c2302
commit 260716b9d8
No known key found for this signature in database
GPG key ID: 25AB36E3E81CBC26
6 changed files with 295 additions and 212 deletions

View file

@ -214,7 +214,7 @@ export default class S3Uploader<M extends Meta, B extends Body> {
throw new Error('Missing S3 object key for aborting upload')
}
this.#options.s3Client
.abortMultipartUpload(this.#key, this.#uploadId)
.abortMultipartUpload({ key: this.#key, uploadId: this.#uploadId })
.catch((abortErr) => {
this.#options.log?.(abortErr, 'warning')
})
@ -238,10 +238,11 @@ export default class S3Uploader<M extends Meta, B extends Body> {
if (!this.#key) {
throw new Error('Missing S3 object key for resuming upload')
}
const existingParts = await this.#options.s3Client.listParts(
this.#uploadId,
this.#key,
)
const existingParts = await this.#options.s3Client.listParts({
uploadId: this.#uploadId,
key: this.#key,
signal: this.#abortController?.signal,
})
// Sync local state with S3 - mark already-uploaded parts
for (const part of existingParts) {
const chunkIndex = part.partNumber - 1
@ -259,17 +260,17 @@ export default class S3Uploader<M extends Meta, B extends Body> {
const signal = this.#abortController?.signal
signal?.throwIfAborted()
const { location, key } = await this.#options.s3Client.putObject(
this.#options.key,
this.#data,
this.#options.file.type || 'application/octet-stream',
this.#options.metadata,
(bytesUploaded: number) => {
const { location, key } = await this.#options.s3Client.putObject({
key: this.#options.key,
data: this.#data,
fileType: this.#options.file.type || 'application/octet-stream',
metadata: this.#options.metadata,
onProgress: (bytesUploaded: number) => {
this.#chunkState[0].uploaded = bytesUploaded
this.#onProgress()
},
signal,
)
})
this.#onSuccess({
location,
@ -281,12 +282,15 @@ export default class S3Uploader<M extends Meta, B extends Body> {
const signal = this.#abortController?.signal
signal?.throwIfAborted()
// We deliberately don't pass the abort signal into the create request: if
// it were cancelled mid-flight, S3 might still create the upload while we
// never receive the uploadId — an orphan we couldn't clean up (see below).
const { uploadId, key } =
await this.#options.s3Client.createMultipartUpload(
this.#options.key,
this.#options.file.type || 'application/octet-stream',
this.#options.metadata,
)
await this.#options.s3Client.createMultipartUpload({
key: this.#options.key,
fileType: this.#options.file.type || 'application/octet-stream',
metadata: this.#options.metadata,
})
if (key == null) {
throw new Error(
'S3 client did not return object key for multipart upload',
@ -330,17 +334,17 @@ export default class S3Uploader<M extends Meta, B extends Body> {
if (this.#key == null) {
throw new Error('Missing S3 object key for uploading part')
}
const { etag } = await this.#options.s3Client.uploadPart(
this.#key,
this.#uploadId!,
chunkData,
const { etag } = await this.#options.s3Client.uploadPart({
key: this.#key,
uploadId: this.#uploadId!,
data: chunkData,
partNumber,
(bytesUploaded: number) => {
onProgress: (bytesUploaded: number) => {
this.#chunkState[chunkIndex].uploaded = bytesUploaded
this.#onProgress()
},
signal,
)
})
// after part finished uploading, update chunk state
this.#chunkState[i].uploaded = chunk.size
@ -366,11 +370,12 @@ export default class S3Uploader<M extends Meta, B extends Body> {
}
const { location, key } =
await this.#options.s3Client.completeMultipartUpload(
this.#key,
this.#uploadId!,
await this.#options.s3Client.completeMultipartUpload({
key: this.#key,
uploadId: this.#uploadId!,
parts,
)
signal,
})
this.#onSuccess({
location,

View file

@ -46,14 +46,14 @@ class S3Companion extends S3Client {
return this.xhr({ url, method, data, onProgress, signal, contentType })
}
public override async putObject(
keyIn: string,
data: Blob | File,
fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE,
metadata: Record<string, unknown>,
onProgress?: IT.OnProgressFn,
signal?: AbortSignal,
) {
public override async putObject({
key: keyIn,
data,
fileType = C.DEFAULT_STREAM_CONTENT_TYPE,
metadata = {},
onProgress,
signal,
}: IT.PutObjectParams) {
const response = await this._fetch(
`/params?${new URLSearchParams({ filename: keyIn, type: fileType, ...Object.fromEntries(Object.entries(metadata).map(([k, v]) => [`metadata[${k}]`, String(v)])) })}`,
)
@ -81,11 +81,11 @@ class S3Companion extends S3Client {
}
}
public override async createMultipartUpload(
keyIn: string,
fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE,
metadata: Record<string, unknown>,
) {
public override async createMultipartUpload({
key: keyIn,
fileType = C.DEFAULT_STREAM_CONTENT_TYPE,
metadata,
}: IT.CreateMultipartUploadParams) {
if (typeof fileType !== 'string') {
throw new TypeError(`${C.ERROR_PREFIX}fileType must be a string`)
}
@ -107,14 +107,14 @@ class S3Companion extends S3Client {
return { uploadId, key }
}
public override async uploadPart(
key: string,
uploadId: string,
data: XMLHttpRequestBodyInit,
partNumber: number,
onProgress?: IT.OnProgressFn,
signal?: AbortSignal,
) {
public override async uploadPart({
key,
uploadId,
data,
partNumber,
onProgress,
signal,
}: IT.UploadPartParams) {
const response = await this._fetch(
`/multipart/${encodeURIComponent(uploadId)}/${encodeURIComponent(partNumber)}?${new URLSearchParams({ key })}`,
{
@ -142,10 +142,10 @@ class S3Companion extends S3Client {
return { etag }
}
public override async listParts(
uploadId: string,
key: string,
): Promise<IT.UploadPart[]> {
public override async listParts({
uploadId,
key,
}: IT.ListPartsParams): Promise<IT.UploadPart[]> {
if (!uploadId) {
throw new TypeError(C.ERROR_UPLOAD_ID_REQUIRED)
}
@ -165,11 +165,11 @@ class S3Companion extends S3Client {
}))
}
public override async completeMultipartUpload(
key: string,
uploadId: string,
parts: Array<IT.UploadPart>,
) {
public override async completeMultipartUpload({
key,
uploadId,
parts,
}: IT.CompleteMultipartUploadParams) {
const response = await this._fetch(
`/multipart/${encodeURIComponent(uploadId)}/complete?${new URLSearchParams({ key })}`,
{
@ -201,7 +201,10 @@ class S3Companion extends S3Client {
}
}
public override async abortMultipartUpload(key: string, uploadId: string) {
public override async abortMultipartUpload({
key,
uploadId,
}: IT.AbortMultipartUploadParams) {
await this._fetch(
`/multipart/${encodeURIComponent(uploadId)}?${new URLSearchParams({ key })}`,
{

View file

@ -1,5 +1,4 @@
import { fetcher } from '@uppy/core/utils'
import * as C from './consts.js'
import type * as IT from './types.js'
class S3Client {
@ -115,14 +114,7 @@ class S3Client {
})
}
public async putObject(
key: string,
data: XMLHttpRequestBodyInit,
fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE,
metadata: Record<string, unknown>,
onProgress?: IT.OnProgressFn,
signal?: AbortSignal,
): Promise<{
public async putObject(params: IT.PutObjectParams): Promise<{
location: string
key: string
etag: string | undefined
@ -131,10 +123,7 @@ class S3Client {
}
public async createMultipartUpload(
key: string,
fileType?: string,
// @ts-expect-error unused
metadata: Record<string, unknown>,
params: IT.CreateMultipartUploadParams,
): Promise<{
uploadId: string
key: string
@ -142,30 +131,18 @@ class S3Client {
throw new Error('Not implemented')
}
public async uploadPart(
key: string,
uploadId: string,
data: XMLHttpRequestBodyInit,
partNumber: number,
onProgress?: IT.OnProgressFn,
signal?: AbortSignal,
): Promise<{
public async uploadPart(params: IT.UploadPartParams): Promise<{
etag: string
}> {
throw new Error('Not implemented')
}
public async listParts(
uploadId: string,
key: string,
): Promise<IT.UploadPart[]> {
public async listParts(params: IT.ListPartsParams): Promise<IT.UploadPart[]> {
throw new Error('Not implemented')
}
public async completeMultipartUpload(
key: string,
uploadId: string,
parts: IT.UploadPart[],
params: IT.CompleteMultipartUploadParams,
): Promise<{
location: string
bucket: string | undefined
@ -176,13 +153,12 @@ class S3Client {
}
public async abortMultipartUpload(
key: string,
uploadId: string,
params: IT.AbortMultipartUploadParams,
): Promise<void> {
throw new Error('Not implemented')
}
public async deleteObject(key: string): Promise<void> {
public async deleteObject(params: IT.DeleteObjectParams): Promise<void> {
throw new Error('Not implemented')
}
}

View file

@ -34,7 +34,7 @@ import * as U from './utils.js'
* },
* });
*
* await s3.putObject('file.txt', 'Hello, World!');
* await s3.putObject({ key: 'file.txt', data: 'Hello, World!' });
*/
class S3mini extends S3Client {
readonly endpoint?: URL
@ -175,20 +175,14 @@ class S3mini extends S3Client {
/**
* Uploads an object to S3 using XHR for progress tracking.
* @param key - Object key
* @param data - Data to upload (Blob, ArrayBuffer, Uint8Array, or string)
* @param fileType - Content type
* @param onProgress - Optional progress callback
* @param signal - Optional abort signal
*/
public override async putObject(
key: string,
data: XMLHttpRequestBodyInit,
fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE,
metadata?: Record<string, unknown>,
onProgress?: IT.OnProgressFn,
signal?: AbortSignal,
) {
public override async putObject({
key,
data,
fileType = C.DEFAULT_STREAM_CONTENT_TYPE,
onProgress,
signal,
}: IT.PutObjectParams) {
this._checkKey(key)
const { xhr, url } = await this.request({
@ -207,11 +201,12 @@ class S3mini extends S3Client {
}
/** Initiates a multipart upload and returns the upload ID. */
public override async createMultipartUpload(
key: string,
fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE,
metadata?: Record<string, unknown>, // todo support metadata here too?
) {
public override async createMultipartUpload({
key,
fileType = C.DEFAULT_STREAM_CONTENT_TYPE,
signal,
}: IT.CreateMultipartUploadParams) {
// todo support metadata here too?
this._checkKey(key)
if (typeof fileType !== 'string') {
throw new TypeError(`${C.ERROR_PREFIX}fileType must be a string`)
@ -220,6 +215,7 @@ class S3mini extends S3Client {
const { xhr } = await this.request({
request: { method: 'POST', key },
contentType: fileType,
signal,
})
const parsed = U.parseXml(xhr.responseText) as Record<string, unknown>
@ -247,14 +243,14 @@ class S3mini extends S3Client {
)
}
public override async uploadPart(
key: string,
uploadId: string,
data: XMLHttpRequestBodyInit,
partNumber: number,
onProgress?: IT.OnProgressFn,
signal?: AbortSignal,
) {
public override async uploadPart({
key,
uploadId,
data,
partNumber,
onProgress,
signal,
}: IT.UploadPartParams) {
this._validateUploadPartParams(key, uploadId, partNumber)
const { xhr } = await this.request({
@ -379,16 +375,18 @@ class S3mini extends S3Client {
}
/** Lists uploaded parts for a multipart upload. */
public override async listParts(
uploadId: string,
key: string,
): Promise<IT.UploadPart[]> {
public override async listParts({
uploadId,
key,
signal,
}: IT.ListPartsParams): Promise<IT.UploadPart[]> {
this._checkKey(key)
if (!uploadId) {
throw new TypeError(C.ERROR_UPLOAD_ID_REQUIRED)
}
const { xhr } = await this.request({
request: { method: 'GET', key, uploadId },
signal,
})
const parsed = U.parseXml(xhr.responseText) as Record<string, unknown>
@ -417,17 +415,19 @@ class S3mini extends S3Client {
}
/** Completes a multipart upload by combining all uploaded parts. */
public override async completeMultipartUpload(
key: string,
uploadId: string,
parts: Array<IT.UploadPart>,
) {
public override async completeMultipartUpload({
key,
uploadId,
parts,
signal,
}: IT.CompleteMultipartUploadParams) {
const xmlBody = this._buildCompleteMultipartUploadXml(parts)
const { xhr } = await this.request({
request: { method: 'POST', key, uploadId },
contentType: C.XML_CONTENT_TYPE,
data: xmlBody,
signal,
})
const parsed = U.parseXml(xhr.responseText)
@ -473,7 +473,11 @@ class S3mini extends S3Client {
}
/** Aborts a multipart upload and removes all uploaded parts. */
public override async abortMultipartUpload(key: string, uploadId: string) {
public override async abortMultipartUpload({
key,
uploadId,
signal,
}: IT.AbortMultipartUploadParams) {
this._checkKey(key)
if (!uploadId) {
throw new TypeError(C.ERROR_UPLOAD_ID_REQUIRED)
@ -481,6 +485,7 @@ class S3mini extends S3Client {
const { xhr } = await this.request({
request: { method: 'DELETE', key, uploadId },
signal,
})
const parsed = U.parseXml(xhr.responseText) as Record<string, unknown>
@ -511,9 +516,10 @@ class S3mini extends S3Client {
}
/** Deletes an object from the bucket. Returns true on success. */
public override async deleteObject(key: string) {
public override async deleteObject({ key, signal }: IT.DeleteObjectParams) {
const { xhr } = await this.request({
request: { method: 'DELETE', key },
signal,
})
if (xhr.status !== 200 && xhr.status !== 204) {

View file

@ -110,6 +110,62 @@ export interface UploadPart {
etag: string
}
/** Parameters for {@link S3Client.putObject}. */
export interface PutObjectParams {
key: string
data: Blob | File
fileType?: string
metadata?: Record<string, unknown>
onProgress?: OnProgressFn
signal?: AbortSignal
}
/** Parameters for {@link S3Client.createMultipartUpload}. */
export interface CreateMultipartUploadParams {
key: string
fileType?: string
metadata?: Record<string, unknown>
signal?: AbortSignal
}
/** Parameters for {@link S3Client.uploadPart}. */
export interface UploadPartParams {
key: string
uploadId: string
data: XMLHttpRequestBodyInit
partNumber: number
onProgress?: OnProgressFn
signal?: AbortSignal
}
/** Parameters for {@link S3Client.listParts}. */
export interface ListPartsParams {
uploadId: string
key: string
signal?: AbortSignal
}
/** Parameters for {@link S3Client.completeMultipartUpload}. */
export interface CompleteMultipartUploadParams {
key: string
uploadId: string
parts: UploadPart[]
signal?: AbortSignal
}
/** Parameters for {@link S3Client.abortMultipartUpload}. */
export interface AbortMultipartUploadParams {
key: string
uploadId: string
signal?: AbortSignal
}
/** Parameters for {@link S3Client.deleteObject}. */
export interface DeleteObjectParams {
key: string
signal?: AbortSignal
}
export interface ErrorWithCode {
code?: string
cause?: { code?: string }

View file

@ -39,7 +39,7 @@ if (config) {
beforeAll(async () => {
for (const key of FILE_KEYS) {
try {
await s3client.deleteObject(key)
await s3client.deleteObject({ key })
} catch {
// intentionally ignore the errors as the objects don't exists. still feels hacky tbh
}
@ -85,7 +85,11 @@ if (config) {
'Hello from pre-signed URL test.',
)
const result = await s3client.putObject(key, fileContents, 'text/plain')
const result = await s3client.putObject({
key,
data: fileContents,
fileType: 'text/plain',
})
expect(result.location).toBeDefined()
expect(result.location).toContain(key)
@ -99,30 +103,32 @@ if (config) {
const partSize = 5 * 1024 * 1024 // 5MB
const part = randomBytes(partSize)
const { uploadId } = await s3client.createMultipartUpload(
const { uploadId } = await s3client.createMultipartUpload({
key,
'application/octet-stream',
)
fileType: 'application/octet-stream',
})
expect(uploadId).toBeDefined()
const partNumber = 1
const { etag } = await s3client.uploadPart(
const { etag } = await s3client.uploadPart({
key,
uploadId,
part,
data: part,
partNumber,
)
})
expect(etag).toBeDefined()
const result = await s3client.completeMultipartUpload(key, uploadId, [
{ etag, partNumber },
])
const result = await s3client.completeMultipartUpload({
key,
uploadId,
parts: [{ etag, partNumber }],
})
expect(result.etag).toBeDefined()
expect(result.location).toBeDefined()
expect(result.location).toContain(key)
expect(result.key).toBe(key)
await s3client.deleteObject(key)
await s3client.deleteObject({ key })
})
// ===== getCredentials tests (STS) =====
@ -158,13 +164,17 @@ if (config) {
})
try {
const result = await s3.putObject(testKey, 'Hello STS!', 'text/plain')
const result = await s3.putObject({
key: testKey,
data: 'Hello STS!',
fileType: 'text/plain',
})
expect(result.location).toBeDefined()
expect(result.location).toContain(testKey)
expect(result.location).not.toContain('X-Amz-Signature')
expect(result.location).not.toContain('?')
} finally {
await s3.deleteObject(testKey)
await s3.deleteObject({ key: testKey })
}
})
@ -196,16 +206,16 @@ if (config) {
},
})
await s3.putObject(
`sts-cache-1-${Date.now()}.txt`,
'File 1',
'text/plain',
)
await s3.putObject(
`sts-cache-2-${Date.now()}.txt`,
'File 2',
'text/plain',
)
await s3.putObject({
key: `sts-cache-1-${Date.now()}.txt`,
data: 'File 1',
fileType: 'text/plain',
})
await s3.putObject({
key: `sts-cache-2-${Date.now()}.txt`,
data: 'File 2',
fileType: 'text/plain',
})
expect(fetchCount).toBe(1)
})
@ -238,25 +248,32 @@ if (config) {
const partSize = 5 * 1024 * 1024 // 5MB
const part = randomBytes(partSize)
const { uploadId } = await s3.createMultipartUpload(
const { uploadId } = await s3.createMultipartUpload({
key,
'application/octet-stream',
)
fileType: 'application/octet-stream',
})
expect(uploadId).toBeDefined()
const partNumber = 1
const { etag } = await s3.uploadPart(key, uploadId, part, partNumber)
const { etag } = await s3.uploadPart({
key,
uploadId,
data: part,
partNumber,
})
expect(etag).toBeDefined()
const result = await s3.completeMultipartUpload(key, uploadId, [
{ etag, partNumber },
])
const result = await s3.completeMultipartUpload({
key,
uploadId,
parts: [{ etag, partNumber }],
})
expect(result.etag).toBeDefined()
expect(result.location).toBeDefined()
expect(result.location).toContain(key)
expect(result.key).toBe(key)
await s3.deleteObject(key)
await s3.deleteObject({ key })
})
})
@ -266,41 +283,45 @@ if (config) {
// we don't need an explicit eTag method as we already get eTag in the putOject response
it('putObject uploads successfully and returns ETag', async () => {
const response = await s3client.putObject(key, content, 'text/plain')
const response = await s3client.putObject({
key,
data: content,
fileType: 'text/plain',
})
expect(response.etag).toBeDefined()
await s3client.deleteObject(key)
await s3client.deleteObject({ key })
})
it('putObject handles binary data', async () => {
const binaryData = new Uint8Array(6).fill(0xff)
const response = await s3client.putObject(
const response = await s3client.putObject({
key,
binaryData,
'application/octet-stream',
)
data: binaryData,
fileType: 'application/octet-stream',
})
expect(response).toBeDefined()
expect(response.etag).toBeDefined()
// cleanup
await s3client.deleteObject(key)
await s3client.deleteObject({ key })
})
// test createMultipartUpload
it('createMultipartUpload returns a valid uploadId', async () => {
const { uploadId } = await s3client.createMultipartUpload(
key_bin,
'application/octet-stream',
)
const { uploadId } = await s3client.createMultipartUpload({
key: key_bin,
fileType: 'application/octet-stream',
})
expect(uploadId).toBeDefined()
expect(typeof uploadId).toBe('string')
expect(uploadId.length).toBeGreaterThan(0)
// cleanup
await s3client.abortMultipartUpload(key, uploadId)
await s3client.abortMultipartUpload({ key, uploadId })
})
// test uploadPart
@ -308,18 +329,18 @@ if (config) {
it('uploadPart returns partNumber and Etag', async () => {
const partData = randomBytes(EIGHT_MB)
const { uploadId } = await s3client.createMultipartUpload(
key_bin,
'application/octet-stream',
)
const { uploadId } = await s3client.createMultipartUpload({
key: key_bin,
fileType: 'application/octet-stream',
})
// upload part
const partResult = await s3client.uploadPart(
key_bin,
const partResult = await s3client.uploadPart({
key: key_bin,
uploadId,
partData,
1,
)
data: partData,
partNumber: 1,
})
expect(partResult).toBeDefined()
expect(partResult.etag).toBeDefined()
@ -327,7 +348,7 @@ if (config) {
expect(partResult.etag.length).toBe(32)
// cleanup
await s3client.abortMultipartUpload(key, uploadId)
await s3client.abortMultipartUpload({ key, uploadId })
})
// end to end multipart flow
@ -337,10 +358,10 @@ if (config) {
const partSize = EIGHT_MB
const totalParts = Math.ceil(large_buffer.byteLength / partSize)
const { uploadId } = await s3client.createMultipartUpload(
key_bin,
'application/octet-stream',
)
const { uploadId } = await s3client.createMultipartUpload({
key: key_bin,
fileType: 'application/octet-stream',
})
expect(uploadId).toBeDefined()
// upload all parts
@ -351,7 +372,12 @@ if (config) {
(i + 1) * partSize,
)
uploadPromises.push(
s3client.uploadPart(key_bin, uploadId, partBuffer, i + 1),
s3client.uploadPart({
key: key_bin,
uploadId,
data: partBuffer,
partNumber: i + 1,
}),
)
}
const uploadResponses = await Promise.all(uploadPromises)
@ -371,11 +397,11 @@ if (config) {
etag: response.etag,
}))
const completeResponse = await s3client.completeMultipartUpload(
key_bin,
const completeResponse = await s3client.completeMultipartUpload({
key: key_bin,
uploadId,
parts,
)
})
expect(completeResponse).toBeDefined()
expect(typeof completeResponse).toBe('object')
@ -385,51 +411,62 @@ if (config) {
// cleanup
await s3client.deleteObject(key_bin)
await s3client.deleteObject({ key: key_bin })
})
it('abortMultipartUpload cancels upload successfully', async () => {
// start upload
const { uploadId } = await s3client.createMultipartUpload(
key_abort_multipart,
'application/octet-stream',
)
const { uploadId } = await s3client.createMultipartUpload({
key: key_abort_multipart,
fileType: 'application/octet-stream',
})
expect(uploadId).toBeDefined()
const partData = randomBytes(EIGHT_MB)
await s3client.uploadPart(key_abort_multipart, uploadId, partData, 1)
await s3client.uploadPart({
key: key_abort_multipart,
uploadId,
data: partData,
partNumber: 1,
})
// abort
await s3client.abortMultipartUpload(key_abort_multipart, uploadId)
await s3client.abortMultipartUpload({
key: key_abort_multipart,
uploadId,
})
})
it('listParts returns uploaded parts correctly', async () => {
const partSize = EIGHT_MB
const { uploadId } = await s3client.createMultipartUpload(
key_list_parts,
'application/octet-stream',
)
const { uploadId } = await s3client.createMultipartUpload({
key: key_list_parts,
fileType: 'application/octet-stream',
})
expect(uploadId).toBeDefined()
const part1Data = randomBytes(partSize)
const part2Data = randomBytes(partSize)
const part1Result = await s3client.uploadPart(
key_list_parts,
const part1Result = await s3client.uploadPart({
key: key_list_parts,
uploadId,
part1Data,
1,
)
const part2Result = await s3client.uploadPart(
key_list_parts,
data: part1Data,
partNumber: 1,
})
const part2Result = await s3client.uploadPart({
key: key_list_parts,
uploadId,
part2Data,
2,
)
data: part2Data,
partNumber: 2,
})
const parts = await s3client.listParts(uploadId, key_list_parts)
const parts = await s3client.listParts({
uploadId,
key: key_list_parts,
})
expect(parts).toBeInstanceOf(Array)
expect(parts.length).toBe(2)
@ -443,7 +480,7 @@ if (config) {
expect(parts[1].etag).toBe(part2Result.etag)
// cleanup abort upload
await s3client.abortMultipartUpload(key, uploadId)
await s3client.abortMultipartUpload({ key, uploadId })
})
})
} else {