Skip to content

Commit 2d70e43

Browse files
authored
Added multiple file support to attach helper (#204)
- The helper was working well, but only allowed one file at a time - We have usecases for multiple files, so we'll extend the attach helper to support multiple files
1 parent 1b6e988 commit 2d70e43

File tree

6 files changed

+141
-5
lines changed

6 files changed

+141
-5
lines changed

packages/express-test/example/app.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,11 @@ app.post('/api/upload/', upload.single('image'), async (req, res) => {
7676
return res.json(req.file);
7777
});
7878

79+
app.post('/api/upload-multiple/', upload.fields([
80+
{name: 'image', maxCount: 1},
81+
{name: 'document', maxCount: 1}
82+
]), async (req, res) => {
83+
return res.json(req.files);
84+
});
85+
7986
module.exports = app;

packages/express-test/lib/Request.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@ class Request {
2222
this.app = app;
2323
this.reqOptions = reqOptions instanceof RequestOptions ? reqOptions : new RequestOptions(reqOptions);
2424
this.cookieJar = cookieJar;
25+
this._formData = null; // Track FormData instance for multiple attachments
2526
}
2627

2728
attach(name, filePath) {
28-
const formData = attachFile(name, filePath);
29-
return this.body(formData);
29+
// Use the utility to create or append to FormData
30+
this._formData = attachFile(name, filePath, this._formData);
31+
32+
// Set the body to the FormData instance
33+
return this.body(this._formData);
3034
}
3135

3236
/**

packages/express-test/lib/utils.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ module.exports.normalizeURL = function normalizeURL(toNormalize) {
2121
return normalized;
2222
};
2323

24-
module.exports.attachFile = function attachFile(name, filePath) {
25-
const formData = new FormData();
24+
module.exports.attachFile = function attachFile(name, filePath, existingFormData = null) {
25+
const formData = existingFormData || new FormData();
2626
const fileContent = fs.readFileSync(filePath);
2727
const filename = path.basename(filePath);
2828
const contentType = mime.lookup(filePath) || 'application/octet-stream';

packages/express-test/test/Request.test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,61 @@ describe('Request', function () {
339339
assert.equal(request.reqOptions.headers.foo, 'bar');
340340
});
341341

342+
it('attach() sets body correctly with single file', async function () {
343+
const fn = () => {};
344+
const jar = {};
345+
const opts = new RequestOptions();
346+
const request = new Request(fn, jar, opts);
347+
348+
const path = require('path');
349+
const filePath = path.join(__dirname, 'fixtures/ghost-favicon.png');
350+
351+
request.attach('image', filePath);
352+
353+
assert.equal(request._formData instanceof FormData, true);
354+
assert.equal(request.reqOptions.body, undefined);
355+
356+
// Verify the FormData contains the file
357+
const buffer = request._formData.getBuffer();
358+
const content = buffer.toString();
359+
assert.match(content, /Content-Disposition: form-data; name="image"/);
360+
assert.match(content, /filename="ghost-favicon.png"/);
361+
});
362+
363+
it('attach() sets body correctly with multiple files', async function () {
364+
const fn = () => {};
365+
const jar = {};
366+
const opts = new RequestOptions();
367+
const request = new Request(fn, jar, opts);
368+
369+
const path = require('path');
370+
const fs = require('fs');
371+
const imagePath = path.join(__dirname, 'fixtures/ghost-favicon.png');
372+
const textPath = path.join(__dirname, 'fixtures/test-multi.txt');
373+
374+
// Create a test text file
375+
fs.writeFileSync(textPath, 'test content');
376+
377+
try {
378+
request
379+
.attach('image', imagePath)
380+
.attach('document', textPath);
381+
382+
assert.equal(request._formData instanceof FormData, true);
383+
384+
// Verify the FormData contains both files
385+
const buffer = request._formData.getBuffer();
386+
const content = buffer.toString();
387+
assert.match(content, /Content-Disposition: form-data; name="image"/);
388+
assert.match(content, /filename="ghost-favicon.png"/);
389+
assert.match(content, /Content-Disposition: form-data; name="document"/);
390+
assert.match(content, /filename="test-multi.txt"/);
391+
} finally {
392+
// Clean up
393+
fs.unlinkSync(textPath);
394+
}
395+
});
396+
342397
it('class is thenable [public api]', async function () {
343398
const fn = (req, res) => {
344399
// This is how reqresnext works

packages/express-test/test/example-app.test.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const {assert} = require('./utils');
2+
const path = require('path');
23

34
const Agent = require('../'); // we require the root file
45
const app = require('../example/app');
@@ -365,7 +366,7 @@ describe('Example App', function () {
365366

366367
const {body} = await agent
367368
.post('/api/upload/')
368-
.attach('image', __dirname + '/fixtures/ghost-favicon.png')
369+
.attach('image', path.join(__dirname, '/fixtures/ghost-favicon.png'))
369370
.expectStatus(200);
370371

371372
assert.equal(body.originalname, 'ghost-favicon.png');
@@ -385,6 +386,41 @@ describe('Example App', function () {
385386
}
386387
});
387388

389+
it('can upload multiple files using multiple attach calls', async function () {
390+
// Create a text file for testing
391+
const textFilePath = path.join(__dirname, '/fixtures/test-upload.txt');
392+
await fs.writeFile(textFilePath, 'test content for multiple files');
393+
394+
const imageContents = await fs.readFile(__dirname + '/fixtures/ghost-favicon.png');
395+
const textContents = await fs.readFile(textFilePath);
396+
397+
const {body} = await agent
398+
.post('/api/upload-multiple/')
399+
.attach('image', path.join(__dirname, '/fixtures/ghost-favicon.png'))
400+
.attach('document', textFilePath)
401+
.expectStatus(200);
402+
403+
// Verify both files were uploaded
404+
assert.equal(body.image[0].originalname, 'ghost-favicon.png');
405+
assert.equal(body.image[0].mimetype, 'image/png');
406+
assert.equal(body.image[0].size, imageContents.length);
407+
assert.equal(body.image[0].fieldname, 'image');
408+
409+
assert.equal(body.document[0].originalname, 'test-upload.txt');
410+
assert.equal(body.document[0].mimetype, 'text/plain');
411+
assert.equal(body.document[0].size, textContents.length);
412+
assert.equal(body.document[0].fieldname, 'document');
413+
414+
// Clean up uploaded files
415+
try {
416+
await fs.unlink(body.image[0].path);
417+
await fs.unlink(body.document[0].path);
418+
await fs.unlink(textFilePath);
419+
} catch (e) {
420+
// ignore if this fails
421+
}
422+
});
423+
388424
it('can stream body', async function () {
389425
const stat = await fs.stat(__dirname + '/fixtures/long-json-body.json');
390426
const stream = createReadStream(__dirname + '/fixtures/long-json-body.json');

packages/express-test/test/utils.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,5 +117,39 @@ describe('Utils', function () {
117117
// Check that the form data contains the file content
118118
assert.match(content, /test content for file upload/);
119119
});
120+
121+
it('can append to existing FormData', function () {
122+
const filePath1 = path.join(__dirname, 'fixtures/test-file.txt');
123+
const filePath2 = path.join(__dirname, 'fixtures/ghost-favicon.png');
124+
125+
// Create FormData with first file
126+
const formData = attachFile('file1', filePath1);
127+
128+
// Append second file to same FormData
129+
const updatedFormData = attachFile('file2', filePath2, formData);
130+
131+
// Should be the same instance
132+
assert.equal(formData, updatedFormData);
133+
134+
// Verify both files are in the FormData
135+
const buffer = formData.getBuffer();
136+
const content = buffer.toString();
137+
138+
assert.match(content, /Content-Disposition: form-data; name="file1"/);
139+
assert.match(content, /filename="test-file.txt"/);
140+
assert.match(content, /Content-Disposition: form-data; name="file2"/);
141+
assert.match(content, /filename="ghost-favicon.png"/);
142+
});
143+
144+
it('creates new FormData when existingFormData is null', function () {
145+
const filePath = path.join(__dirname, 'fixtures/test-file.txt');
146+
const formData = attachFile('testfile', filePath, null);
147+
148+
assert.equal(formData instanceof FormData, true);
149+
150+
const buffer = formData.getBuffer();
151+
const content = buffer.toString();
152+
assert.match(content, /Content-Disposition: form-data; name="testfile"/);
153+
});
120154
});
121155
});

0 commit comments

Comments
 (0)