diff --git a/src/index.ts b/src/index.ts index ded47ff3..aceb3d52 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,6 +68,7 @@ export { } from './IncomingHttpHeaders.js'; export * from './HttpClientError.js'; export { FetchFactory, fetch } from './fetch.js'; +export { FormData as WebFormData } from './FormData.js'; export default { request, diff --git a/test/fixtures/BufferStream.ts b/test/fixtures/BufferStream.ts new file mode 100644 index 00000000..df60f8b3 --- /dev/null +++ b/test/fixtures/BufferStream.ts @@ -0,0 +1,72 @@ +import { Transform } from 'node:stream'; + +const BUF_SIZE = 1024 * 1024; + +export class BufferStream extends Transform { + private buf: Buffer; + private offset: number; + + constructor(options?: any) { + super(options); + this.realloc(); + } + + realloc() { + this.buf = Buffer.alloc(BUF_SIZE); + this.offset = 0; + } + + _transform(chunk: Buffer, _: any, callback: any) { + const currentLength = this.offset; + const chunkSize = chunk.length; + const newSize = currentLength + chunkSize; + // 缓冲区未满 + // - 向缓冲区写入 + if (newSize < BUF_SIZE) { + chunk.copy(this.buf, currentLength); + this.offset += chunkSize; + return callback(); + } + + // 缓冲区正好满 + // - 拷贝到缓冲区以后, 将 chunk 返回 + // - 刷新缓冲区 + if (newSize === BUF_SIZE) { + chunk.copy(this.buf, currentLength); + const writeChunk = this.buf; + this.realloc(); + return callback(null, writeChunk); + } + + // 超过缓冲区大小 + // - 拷贝到缓冲区以后, 将 chunk 返回 + // - 刷新缓冲区 + // - 将超出的部分拷贝到新的缓冲区中 + const copyLength = BUF_SIZE - currentLength; + const remainLength = chunkSize - copyLength; + chunk.copy(this.buf, currentLength, 0, copyLength); + const writeChunk = this.buf; + this.push(writeChunk); + this.realloc(); + + if (remainLength > BUF_SIZE) { + // 特殊情况: 给了一个超大 chunk + // 直接将这个 chunk 返回,没必要缓冲了 + this.push(chunk.slice(copyLength)); + } else { + chunk.copy(this.buf, 0, copyLength); + this.offset = remainLength; + } + return callback(null); + } + + _flush(callback: any) { + if (this.offset) { + const chunk = Buffer.alloc(this.offset); + this.buf.copy(chunk); + this.push(chunk); + this.offset = 0; + } + callback(); + } +} diff --git a/test/formData-with-BufferStream.test.ts b/test/formData-with-BufferStream.test.ts new file mode 100644 index 00000000..b2fb94e0 --- /dev/null +++ b/test/formData-with-BufferStream.test.ts @@ -0,0 +1,43 @@ +import { strict as assert } from 'node:assert'; +import { createReadStream } from 'node:fs'; +import { basename } from 'node:path'; +import { describe, it, beforeAll, afterAll } from 'vitest'; +import { HttpClient, WebFormData } from '../src/index.js'; +import { startServer } from './fixtures/server.js'; +import { BufferStream } from './fixtures/BufferStream.js'; + +describe('formData-with-BufferStream.test.ts', () => { + let close: any; + let _url: string; + beforeAll(async () => { + const { closeServer, url } = await startServer(); + close = closeServer; + _url = url; + }); + + afterAll(async () => { + await close(); + }); + + it('should post with BufferStream', async () => { + const fileStream = createReadStream(__filename); + const bufferStream = new BufferStream(); + fileStream.pipe(bufferStream); + const formData = new WebFormData(); + const fileName = basename(__filename); + formData.append('fileBufferStream', bufferStream, fileName); + formData.append('foo', 'bar'); + + const httpClient = new HttpClient(); + const response = await httpClient.request(`${_url}multipart`, { + method: 'POST', + content: formData, + headers: formData.getHeaders(), + dataType: 'json', + }); + assert.equal(response.status, 200); + // console.log(response.data); + assert.equal(response.data.files.fileBufferStream.filename, 'formData-with-BufferStream.test.ts'); + assert.deepEqual(response.data.form, { foo: 'bar' }); + }); +});