Skip to content

Commit

Permalink
feat: support custom pathToRegexpModule (#66)
Browse files Browse the repository at this point in the history
part of eggjs/core#290

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Dependencies**
    - Updated `egg-path-matching` to version 1.2.0
    - Added `path-to-regexp-v8` dependency

- **Middleware**
- Enhanced multipart file upload middleware with additional application
context configuration

- **Testing**
    - Improved file upload test cases
    - Updated error message assertions for more flexible matching
    - Added new test scenarios for file upload endpoints

- **Configuration**
    - Updated file upload handling configuration
    - Added new routing and controller logic for file uploads

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
fengmk2 authored Jan 22, 2025
1 parent b2a9662 commit d474eec
Show file tree
Hide file tree
Showing 14 changed files with 242 additions and 13 deletions.
9 changes: 5 additions & 4 deletions app/middleware/multipart.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use strict';

const pathMatching = require('egg-path-matching');

module.exports = options => {
module.exports = (options, app) => {
// normalize
const matchFn = options.fileModeMatch && pathMatching({ match: options.fileModeMatch });
const matchFn = options.fileModeMatch && pathMatching({
match: options.fileModeMatch,
pathToRegexpModule: app.options.pathToRegexpModule,
});

return async function multipart(ctx, next) {
if (!ctx.is('multipart')) return next();
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"dependencies": {
"co-busboy": "^2.0.0",
"dayjs": "^1.11.5",
"egg-path-matching": "^1.0.1",
"egg-path-matching": "^1.2.0",
"humanize-bytes": "^1.0.1"
},
"devDependencies": {
Expand All @@ -62,6 +62,7 @@
"is-type-of": "^1.2.1",
"typescript": "5",
"urllib": "3",
"stream-wormhole": "^2.0.1"
"stream-wormhole": "^2.0.1",
"path-to-regexp-v8": "npm:path-to-regexp@8"
}
}
2 changes: 1 addition & 1 deletion test/dynamic-option.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ describe('test/dynamic-option.test.js', () => {
});

assert(res.status === 413);
assert(res.data.toString().includes('Request_fileSize_limitError: Reach fileSize limit'));
assert.match(res.data.toString(), /Error: Reach fileSize limit/);
});
});
135 changes: 135 additions & 0 deletions test/enable-pathToRegexpModule.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
const assert = require('assert');
const formstream = require('formstream');
const urllib = require('urllib');
const path = require('path');
const mock = require('egg-mock');
const fs = require('fs').promises;

describe('test/enable-pathToRegexpModule.test.js', () => {
let app;
let server;
let host;
before(() => {
app = mock.app({
baseDir: 'apps/fileModeMatch-glob-with-pathToRegexpModule',
pathToRegexpModule: require.resolve('path-to-regexp-v8'),
});
return app.ready();
});
before(() => {
server = app.listen();
host = 'http://127.0.0.1:' + server.address().port;
});
after(() => {
return fs.rm(app.config.multipart.tmpdir, { force: true, recursive: true });
});
after(() => app.close());
after(() => server.close());
beforeEach(() => app.mockCsrf());
afterEach(mock.restore);

it('should upload match file mode work on /upload_file', async () => {
const form = formstream();
form.field('foo', 'fengmk2').field('love', 'egg');
form.file('file1', __filename, 'foooooooo.js');
form.file('file2', __filename);
// will ignore empty file
form.buffer('file3', Buffer.from(''), '', 'application/octet-stream');
form.file('bigfile', path.join(__dirname, 'fixtures', 'bigfile.js'));
// other form fields
form.field('work', 'with Node.js');

const headers = form.headers();
const res = await urllib.request(host + '/upload_file', {
method: 'POST',
headers,
stream: form,
});

assert(res.status === 200);
const data = JSON.parse(res.data);
assert.deepStrictEqual(data.body, { foo: 'fengmk2', love: 'egg', work: 'with Node.js' });
assert(data.files.length === 3);
assert(data.files[0].field === 'file1');
assert(data.files[0].filename === 'foooooooo.js');
assert(data.files[0].encoding === '7bit');
assert(data.files[0].mime === 'application/javascript');
assert(data.files[0].filepath.startsWith(app.config.multipart.tmpdir));

assert(data.files[1].field === 'file2');
assert(data.files[1].filename === 'enable-pathToRegexpModule.test.js');
assert(data.files[1].encoding === '7bit');
assert(data.files[1].mime === 'application/javascript');
assert(data.files[1].filepath.startsWith(app.config.multipart.tmpdir));

assert(data.files[2].field === 'bigfile');
assert(data.files[2].filename === 'bigfile.js');
assert(data.files[2].encoding === '7bit');
assert(data.files[2].mime === 'application/javascript');
assert(data.files[2].filepath.startsWith(app.config.multipart.tmpdir));
});

it('should upload match file mode work on /upload_file/*', async () => {
const form = formstream();
form.field('foo', 'fengmk2').field('love', 'egg');
form.file('file1', __filename, 'foooooooo.js');
form.file('file2', __filename);
// will ignore empty file
form.buffer('file3', Buffer.from(''), '', 'application/octet-stream');
form.file('bigfile', path.join(__dirname, 'fixtures', 'bigfile.js'));
// other form fields
form.field('work', 'with Node.js');

const headers = form.headers();
const res = await urllib.request(host + '/upload_file/foo', {
method: 'POST',
headers,
stream: form,
});

assert.equal(res.status, 200);
const data = JSON.parse(res.data);
assert.deepStrictEqual(data.body, { foo: 'fengmk2', love: 'egg', work: 'with Node.js' });
assert(data.files.length === 3);
assert(data.files[0].field === 'file1');
assert(data.files[0].filename === 'foooooooo.js');
assert(data.files[0].encoding === '7bit');
assert(data.files[0].mime === 'application/javascript');
assert(data.files[0].filepath.startsWith(app.config.multipart.tmpdir));

assert(data.files[1].field === 'file2');
assert(data.files[1].filename === 'enable-pathToRegexpModule.test.js');
assert(data.files[1].encoding === '7bit');
assert(data.files[1].mime === 'application/javascript');
assert(data.files[1].filepath.startsWith(app.config.multipart.tmpdir));

assert(data.files[2].field === 'bigfile');
assert(data.files[2].filename === 'bigfile.js');
assert(data.files[2].encoding === '7bit');
assert(data.files[2].mime === 'application/javascript');
assert(data.files[2].filepath.startsWith(app.config.multipart.tmpdir));
});

it('should upload not match file mode', async () => {
const form = formstream();
form.field('foo', 'fengmk2').field('love', 'egg');
form.file('file1', __filename, 'foooooooo.js');
form.file('file2', __filename);
// will ignore empty file
form.buffer('file3', Buffer.from(''), '', 'application/octet-stream');
form.file('bigfile', path.join(__dirname, 'fixtures', 'bigfile.js'));
// other form fields
form.field('work', 'with Node.js');

const headers = form.headers();
const res = await urllib.request(host + '/upload', {
method: 'POST',
headers,
stream: form,
});

assert(res.status === 200);
const data = JSON.parse(res.data);
assert.deepStrictEqual(data, { body: {} });
});
});
10 changes: 5 additions & 5 deletions test/file-mode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ describe('test/file-mode.test.js', () => {
});

assert(res.status === 413);
assert(res.data.toString().includes('Request_fields_limitError: Reach fields limit'));
assert.match(res.data.toString(), /Error: Reach fields limit/);
});

it('should throw error when request files limit', async () => {
Expand All @@ -205,7 +205,7 @@ describe('test/file-mode.test.js', () => {
});

assert(res.status === 413);
assert(res.data.toString().includes('Request_files_limitError: Reach files limit'));
assert.match(res.data.toString(), /Error: Reach files limit/);
});

it('should throw error when request field size limit', async () => {
Expand All @@ -220,7 +220,7 @@ describe('test/file-mode.test.js', () => {
});

assert(res.status === 413);
assert(res.data.toString().includes('Request_fieldSize_limitError: Reach fieldSize limit'));
assert.match(res.data.toString(), /Error: Reach fieldSize limit/);
});

// fieldNameSize is TODO on busboy
Expand All @@ -237,7 +237,7 @@ describe('test/file-mode.test.js', () => {
});

assert(res.status === 413);
assert(res.data.toString().includes('Request_fieldSize_limitError: Reach fieldSize limit'));
assert.match(res.data.toString(), /Error: Reach fieldSize limit/);
});

it('should throw error when request file size limit', async () => {
Expand All @@ -256,7 +256,7 @@ describe('test/file-mode.test.js', () => {
});

assert(res.status === 413);
assert(res.data.toString().includes('Request_fileSize_limitError: Reach fileSize limit'));
assert.match(res.data.toString(), /Error: Reach fileSize limit/);
});

it('should throw error when file name invalid', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

module.exports = async ctx => {
await ctx.saveRequestFiles();
ctx.body = {
body: ctx.request.body,
files: ctx.request.files,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';

module.exports = async ctx => {
ctx.body = {
body: ctx.request.body,
files: ctx.request.files,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';

module.exports = app => {
app.post('/upload', app.controller.upload);
app.post('/upload_file', app.controller.upload);
app.post('/upload_file/foo', app.controller.upload);
app.post('/save', app.controller.save);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<form method="POST" action="/upload_file?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
title: <input name="title" />
file1: <input name="file1" type="file" />
file2: <input name="file2" type="file" />
file3: <input name="file3" type="file" />
other: <input name="other" />
<button type="submit">上传</button>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
exports.multipart = {
mode: 'stream',
fileModeMatch: '/upload_file{/:paths}'
};

exports.keys = 'multipart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';

exports.logger = {
consoleLevel: 'NONE',
coreLogger: {
// consoleLevel: 'DEBUG',
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "fileModeMatch-glob-with-pathToRegexpModule"
}
1 change: 1 addition & 0 deletions test/fixtures/apps/fileModeMatch-glob/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
module.exports = app => {
app.post('/upload', app.controller.upload);
app.post('/upload_file', app.controller.upload);
app.post('/upload_file/foo', app.controller.upload);
app.post('/save', app.controller.save);
};
43 changes: 42 additions & 1 deletion test/stream-mode-with-filematch-glob.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('test/stream-mode-with-filematch-glob.test.js', () => {
beforeEach(() => app.mockCsrf());
afterEach(mock.restore);

it('should upload match file mode', async () => {
it('should upload match file mode work on /upload_file', async () => {
const form = formstream();
form.field('foo', 'fengmk2').field('love', 'egg');
form.file('file1', __filename, 'foooooooo.js');
Expand Down Expand Up @@ -70,6 +70,47 @@ describe('test/stream-mode-with-filematch-glob.test.js', () => {
assert(data.files[2].filepath.startsWith(app.config.multipart.tmpdir));
});

it('should upload match file mode work on /upload_file/*', async () => {
const form = formstream();
form.field('foo', 'fengmk2').field('love', 'egg');
form.file('file1', __filename, 'foooooooo.js');
form.file('file2', __filename);
// will ignore empty file
form.buffer('file3', Buffer.from(''), '', 'application/octet-stream');
form.file('bigfile', path.join(__dirname, 'fixtures', 'bigfile.js'));
// other form fields
form.field('work', 'with Node.js');

const headers = form.headers();
const res = await urllib.request(host + '/upload_file/foo', {
method: 'POST',
headers,
stream: form,
});

assert.equal(res.status, 200);
const data = JSON.parse(res.data);
assert.deepStrictEqual(data.body, { foo: 'fengmk2', love: 'egg', work: 'with Node.js' });
assert(data.files.length === 3);
assert(data.files[0].field === 'file1');
assert(data.files[0].filename === 'foooooooo.js');
assert(data.files[0].encoding === '7bit');
assert(data.files[0].mime === 'application/javascript');
assert(data.files[0].filepath.startsWith(app.config.multipart.tmpdir));

assert(data.files[1].field === 'file2');
assert(data.files[1].filename === 'stream-mode-with-filematch-glob.test.js');
assert(data.files[1].encoding === '7bit');
assert(data.files[1].mime === 'application/javascript');
assert(data.files[1].filepath.startsWith(app.config.multipart.tmpdir));

assert(data.files[2].field === 'bigfile');
assert(data.files[2].filename === 'bigfile.js');
assert(data.files[2].encoding === '7bit');
assert(data.files[2].mime === 'application/javascript');
assert(data.files[2].filepath.startsWith(app.config.multipart.tmpdir));
});

it('should upload not match file mode', async () => {
const form = formstream();
form.field('foo', 'fengmk2').field('love', 'egg');
Expand Down

0 comments on commit d474eec

Please sign in to comment.