mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-01 15:49:00 +08:00
fix(webdav): speed up small fixed-length uploads
This commit is contained in:
@@ -392,17 +392,46 @@ describe('S3Service', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('uploads ReadableStream bodies through a presigned PUT without buffering', async () => {
|
||||
it('uploads small fixed-length ReadableStream bodies directly', async () => {
|
||||
mockSend.mockResolvedValueOnce({ $metadata: {} })
|
||||
const body = bytesStream(new Uint8Array([1, 2, 3]))
|
||||
|
||||
await expect(service.putObject(storage, 'notes/test.txt', body, 'text/plain', 3)).resolves.toBe(3)
|
||||
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
input: {
|
||||
Bucket: 'my-bucket',
|
||||
Key: 'notes/test.txt',
|
||||
Body: new Uint8Array([1, 2, 3]),
|
||||
ContentType: 'text/plain',
|
||||
ContentLength: 3,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects small fixed-length streams that do not match Content-Length', async () => {
|
||||
const body = bytesStream(new Uint8Array([1, 2, 3]))
|
||||
|
||||
await expect(service.putObject(storage, 'notes/test.txt', body, 'text/plain', 4)).rejects.toThrow(
|
||||
'Request body length does not match Content-Length',
|
||||
)
|
||||
})
|
||||
|
||||
it('uploads large ReadableStream bodies through a presigned PUT without buffering', async () => {
|
||||
mockSend.mockClear()
|
||||
const fetchMock = vi.fn().mockResolvedValueOnce(new Response(null, { status: 200 }))
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
const body = new ReadableStream()
|
||||
await expect(service.putObject(storage, 'videos/test.mp4', body, 'video/mp4', 1024)).resolves.toBe(1024)
|
||||
await expect(service.putObject(storage, 'videos/test.mp4', body, 'video/mp4', 1024 * 1024)).resolves.toBe(
|
||||
1024 * 1024,
|
||||
)
|
||||
expect(fetchMock).toHaveBeenCalledWith('https://signed-url.example.com', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'video/mp4',
|
||||
'Content-Length': '1024',
|
||||
'Content-Length': '1048576',
|
||||
},
|
||||
body,
|
||||
})
|
||||
@@ -413,7 +442,7 @@ describe('S3Service', () => {
|
||||
it('fails when presigned stream upload is rejected', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(new Response(null, { status: 403 })))
|
||||
await expect(
|
||||
service.putObject(storage, 'videos/test.mp4', new ReadableStream(), 'video/mp4', 1024),
|
||||
service.putObject(storage, 'videos/test.mp4', new ReadableStream(), 'video/mp4', 1024 * 1024),
|
||||
).rejects.toThrow('S3 stream upload failed: 403')
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
+11
-1
@@ -15,6 +15,7 @@ import type { Storage } from '../../shared/types'
|
||||
|
||||
const DEFAULT_EXPIRES_IN = 3600
|
||||
const MULTIPART_PART_SIZE = 5 * 1024 * 1024
|
||||
const SMALL_STREAM_PUT_BUFFER_SIZE = 256 * 1024
|
||||
|
||||
export class S3Service {
|
||||
createClient(storage: Storage): S3Client {
|
||||
@@ -141,6 +142,11 @@ export class S3Service {
|
||||
): Promise<number> {
|
||||
if (body instanceof ReadableStream) {
|
||||
if (contentLength === undefined) return this.putObjectMultipartStream(storage, key, body, contentType)
|
||||
if (contentLength <= SMALL_STREAM_PUT_BUFFER_SIZE) {
|
||||
const bytes = await streamToBytes(body)
|
||||
if (bytes.byteLength !== contentLength) throw new Error('Request body length does not match Content-Length')
|
||||
return this.putObject(storage, key, bytes, contentType)
|
||||
}
|
||||
await this.putObjectStream(storage, key, body, contentType, contentLength)
|
||||
return contentLength
|
||||
}
|
||||
@@ -279,7 +285,7 @@ export class S3Service {
|
||||
|
||||
async function bodyToBytes(body: unknown): Promise<Uint8Array> {
|
||||
if (body instanceof Uint8Array) return body
|
||||
if (body instanceof ReadableStream) return new Uint8Array(await new Response(body).arrayBuffer())
|
||||
if (body instanceof ReadableStream) return streamToBytes(body)
|
||||
|
||||
const streamBody = body as {
|
||||
transformToByteArray?: () => Promise<Uint8Array>
|
||||
@@ -291,6 +297,10 @@ async function bodyToBytes(body: unknown): Promise<Uint8Array> {
|
||||
throw new Error('Unsupported object body')
|
||||
}
|
||||
|
||||
async function streamToBytes(body: ReadableStream): Promise<Uint8Array> {
|
||||
return new Uint8Array(await new Response(body).arrayBuffer())
|
||||
}
|
||||
|
||||
function bodyToResponseBody(body: unknown): BodyInit {
|
||||
if (body instanceof Uint8Array)
|
||||
return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength) as ArrayBuffer
|
||||
|
||||
Reference in New Issue
Block a user