Skip to content

Commit

Permalink
feat: support file mode on default stream mode (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
fengmk2 authored and dead-horse committed May 16, 2019
1 parent ed2d003 commit 7eb534f
Show file tree
Hide file tree
Showing 22 changed files with 249 additions and 11 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ language: node_js
node_js:
- '8'
- '10'
- '12'
install:
- npm i npminstall && npminstall
script:
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,20 @@ module.exports = class extends Controller {
};
```

### Support `file` and `stream` mode in the same time

If the default `mode` is `stream`, use the `fileModeMatch` options to match the request urls switch to `file` mode.

```js
config.multipart = {
mode: 'stream',
// let POST /upload_file request use the file mode, other requests use the stream mode.
fileModeMatch: /^\/upload_file$/,
};
```

NOTICE: `fileModeMatch` options only work on `stream` mode.

## License

[MIT](https://github.com/eggjs/egg-multipart/blob/master/LICENSE)
10 changes: 9 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use strict';

const bytes = require('humanize-bytes');
const assert = require('assert');
const path = require('path');
const bytes = require('humanize-bytes');

module.exports = app => {
const options = app.config.multipart;
Expand Down Expand Up @@ -87,6 +88,13 @@ module.exports = app => {

app.coreLogger.info('[egg-multipart] %s mode enable', options.mode);
if (options.mode === 'file') {
if (options.fileModeMatch) throw new TypeError('`fileModeMatch` options only work on stream mode, please remove it');
app.coreLogger.info('[egg-multipart] will save temporary files to %j, cleanup job cron: %j',
options.tmpdir, options.cleanSchedule.cron);
// enable multipart middleware
app.config.coreMiddleware.push('multipart');
} else if (options.fileModeMatch) {
assert(options.fileModeMatch instanceof RegExp, '`fileModeMatch` options should be an instance of RegExp');
app.coreLogger.info('[egg-multipart] will save temporary files to %j, cleanup job cron: %j',
options.tmpdir, options.cleanSchedule.cron);
// enable multipart middleware
Expand Down
6 changes: 3 additions & 3 deletions app/extend/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const HAS_CONSUMED = Symbol('Context#multipartHasConsumed');
module.exports = {
/**
* clean up request tmp files helper
* @method Context#cleanupRequestFiles
* @function Context#cleanupRequestFiles
* @param {Array<String>} [files] - file paths need to clenup, default is `ctx.request.files`.
*/
async cleanupRequestFiles(files) {
Expand All @@ -37,7 +37,7 @@ module.exports = {

/**
* create multipart.parts instance, to get separated files.
* @method Context#multipart
* @function Context#multipart
* @param {Object} [options] - override default multipart configurations
* - {Boolean} options.autoFields
* - {String} options.defCharset
Expand Down Expand Up @@ -71,7 +71,7 @@ module.exports = {
* // get other fields
* console.log(stream.fields);
* ```
* @method Context#getFileStream
* @function Context#getFileStream
* @param {Object} options
* - {Boolean} options.requireFile - required file submit, default is true
* - {String} options.defCharset
Expand Down
1 change: 1 addition & 0 deletions app/middleware/multipart.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module.exports = options => {

return async function multipart(ctx, next) {
if (!ctx.is('multipart')) return next();
if (options.fileModeMatch && !options.fileModeMatch.test(ctx.path)) return next();

let storedir;

Expand Down
4 changes: 2 additions & 2 deletions app/schedule/clean_tmpdir.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ module.exports = app => {
type: 'worker',
cron: app.config.multipart.cleanSchedule.cron,
immediate: false,
// disable on stream mode
disable: app.config.multipart.mode === 'stream',
// disable on stream mode and not set fileModeMatch
disable: app.config.multipart.mode === 'stream' && !app.config.multipart.fileModeMatch,
};
}

Expand Down
4 changes: 3 additions & 1 deletion azure-pipelines.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ jobs:
node_version: 8
node_10:
node_version: 10
maxParallel: 2
node_12:
node_version: 12
maxParallel: 3
steps:
- task: NodeTool@0
inputs:
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"command": {
"azure-pipelines": "ci-windows"
},
"version": "8, 10",
"version": "8, 10, 12",
"license": {
"year": 2017
}
Expand All @@ -77,7 +77,6 @@
"formstream": "^1.1.0",
"is-type-of": "^1.0.0",
"urllib": "^2.30.0",
"typescript": "^3.2.2",
"webstorm-disable-index": "^1.2.0"
"typescript": "^3.2.2"
}
}
7 changes: 7 additions & 0 deletions test/file-mode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,13 @@ describe('test/file-mode.test.js', () => {
});

describe('schedule/clean_tmpdir', () => {
it('should register clean_tmpdir schedule', () => {
// [egg-schedule]: register schedule /hello/egg-multipart/app/schedule/clean_tmpdir.js
const logger = app.loggers.scheduleLogger;
const content = fs.readFileSync(logger.options.file, 'utf8');
assert(/\[egg-schedule\]: register schedule .+clean_tmpdir\.js/.test(content));
});

it('should remove nothing', async () => {
app.mockLog();
await app.runSchedule(path.join(__dirname, '../app/schedule/clean_tmpdir'));
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/apps/fileModeMatch/app/controller/upload.js
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,
};
};
6 changes: 6 additions & 0 deletions test/fixtures/apps/fileModeMatch/app/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict';

module.exports = app => {
app.post('/upload', app.controller.upload);
app.post('/upload_file', app.controller.upload);
};
8 changes: 8 additions & 0 deletions test/fixtures/apps/fileModeMatch/app/views/home.html
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>
8 changes: 8 additions & 0 deletions test/fixtures/apps/fileModeMatch/config/config.default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';

exports.multipart = {
mode: 'stream',
fileModeMatch: /^\/upload_file$/
};

exports.keys = 'multipart';
8 changes: 8 additions & 0 deletions test/fixtures/apps/fileModeMatch/config/config.unittest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';

exports.logger = {
consoleLevel: 'NONE',
coreLogger: {
// consoleLevel: 'DEBUG',
},
};
3 changes: 3 additions & 0 deletions test/fixtures/apps/fileModeMatch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "multipart-file-mode-demo"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';

exports.multipart = {
mode: 'stream',
fileModeMatch: 'foobar',
};

exports.keys = 'multipart';
3 changes: 3 additions & 0 deletions test/fixtures/apps/wrong-fileModeMatch-value/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "multipart-wrong-mode-demo"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';

exports.multipart = {
mode: 'file',
fileModeMatch: /^\/upload$/,
};

exports.keys = 'multipart';
3 changes: 3 additions & 0 deletions test/fixtures/apps/wrong-fileModeMatch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "multipart-wrong-mode-demo"
}
15 changes: 15 additions & 0 deletions test/multipart.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ describe('test/multipart.test.js', () => {
beforeEach(() => app.mockCsrf());
afterEach(mock.restore);

it('should not has clean_tmpdir schedule', async () => {
try {
await app.runSchedule('clean_tmpdir');
throw new Error('should not run this');
} catch (err) {
assert(err.message === '[egg-schedule] Cannot find schedule clean_tmpdir');
}
});

it('should not register clean_tmpdir schedule', () => {
const logger = app.loggers.scheduleLogger;
const content = fs.readFileSync(logger.options.file, 'utf8');
assert(!/\[egg-schedule\]: register schedule .+clean_tmpdir\.js/.test(content));
});

it('should upload with csrf', function* () {
const form = formstream();
// form.file('file', filepath, filename);
Expand Down
103 changes: 103 additions & 0 deletions test/stream-mode-with-filematch.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
'use strict';

const assert = require('assert');
const formstream = require('formstream');
const urllib = require('urllib');
const path = require('path');
const fs = require('fs');
const mock = require('egg-mock');
const rimraf = require('mz-modules/rimraf');

describe('test/stream-mode-with-filematch.test.js', () => {
let app;
let server;
let host;
before(() => {
app = mock.app({
baseDir: 'apps/fileModeMatch',
});
return app.ready();
});
before(() => {
server = app.listen();
host = 'http://127.0.0.1:' + server.address().port;
});
after(() => {
return rimraf(app.config.multipart.tmpdir);
});
after(() => app.close());
after(() => server.close());
beforeEach(() => app.mockCsrf());
afterEach(mock.restore);

it('should upload 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_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 === 'stream-mode-with-filematch.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: {} });
});

it('should register clean_tmpdir schedule', () => {
// [egg-schedule]: register schedule /hello/egg-multipart/app/schedule/clean_tmpdir.js
const logger = app.loggers.scheduleLogger;
const content = fs.readFileSync(logger.options.file, 'utf8');
assert(/\[egg-schedule\]: register schedule .+clean_tmpdir\.js/.test(content));
});
});
27 changes: 26 additions & 1 deletion test/wrong-mode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const assert = require('assert');
const mock = require('egg-mock');

describe('test/wrong-mode.test.js', () => {
it('should start fail', () => {
it('should start fail when mode=foo', () => {
const app = mock.app({
baseDir: 'apps/wrong-mode',
});
Expand All @@ -16,4 +16,29 @@ describe('test/wrong-mode.test.js', () => {
assert(err.message === 'Expect mode to be \'stream\' or \'file\', but got \'foo\'');
});
});

it('should start fail when using options.fileModeMatch on file mode', () => {
const app = mock.app({
baseDir: 'apps/wrong-fileModeMatch',
});
return app.ready()
.then(() => {
throw new Error('should not run this');
}, err => {
assert(err.name === 'TypeError');
assert(err.message === '`fileModeMatch` options only work on stream mode, please remove it');
});
});

it('should start fail when using options.fileModeMatch is not RegExp', () => {
const app = mock.app({
baseDir: 'apps/wrong-fileModeMatch-value',
});
return app.ready()
.then(() => {
throw new Error('should not run this');
}, err => {
assert(err.message === '`fileModeMatch` options should be an instance of RegExp');
});
});
});

0 comments on commit 7eb534f

Please sign in to comment.