millas 0.2.15 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -2
- package/src/container/Application.js +2 -0
- package/src/core/foundation.js +7 -1
- package/src/core/http.js +4 -0
- package/src/http/RequestContext.js +4 -1
- package/src/http/UploadedFile.js +412 -0
- package/src/http/middleware/UploadMiddleware.js +341 -0
- package/src/router/Router.js +80 -2
- package/src/storage/Storage.js +4 -0
- package/src/storage/drivers/S3Driver.js +471 -0
- package/src/validation/types.js +25 -4
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UploadMiddleware
|
|
5
|
+
*
|
|
6
|
+
* Built-in multipart/form-data handler for Millas.
|
|
7
|
+
* Zero config required — just declare file() fields in your shape or
|
|
8
|
+
* pass options to the constructor and the framework takes care of the rest.
|
|
9
|
+
*
|
|
10
|
+
* Powered by multer under the hood (lazy-required, so it only needs to be
|
|
11
|
+
* installed if you actually handle uploads — multer is listed as an optional
|
|
12
|
+
* peer dependency).
|
|
13
|
+
*
|
|
14
|
+
* ── Usage patterns ───────────────────────────────────────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* 1. No shape, no config — just add 'upload' to middleware:
|
|
17
|
+
*
|
|
18
|
+
* Route.post('/media/upload', ['auth', 'upload'], MediaController, 'upload');
|
|
19
|
+
*
|
|
20
|
+
* async upload({ file, files, user }) {
|
|
21
|
+
* // file — first uploaded file (any field name), UploadedFile instance
|
|
22
|
+
* // files — all files keyed by field name
|
|
23
|
+
* const path = await file.store('media');
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* 2. Parameterized alias — configure field/count inline:
|
|
27
|
+
*
|
|
28
|
+
* 'upload' → any field, 50 MB limit (default)
|
|
29
|
+
* 'upload:avatar' → single field named 'avatar'
|
|
30
|
+
* 'upload:photos,5' → field 'photos', up to 5 files
|
|
31
|
+
*
|
|
32
|
+
* Route.post('/avatar', ['auth', 'upload:avatar'], UserController, 'updateAvatar');
|
|
33
|
+
*
|
|
34
|
+
* 3. With a shape — Router auto-injects UploadMiddleware, zero manual config:
|
|
35
|
+
*
|
|
36
|
+
* async upload({ body, file, user }) {
|
|
37
|
+
* // file is a multer file object: { buffer, mimetype, size, originalname }
|
|
38
|
+
* // body has the rest of the form fields
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* ── Manual usage ──────────────────────────────────────────────────────────────
|
|
42
|
+
*
|
|
43
|
+
* const { UploadMiddleware } = require('millas/core/http');
|
|
44
|
+
*
|
|
45
|
+
* // Single named field
|
|
46
|
+
* Route.post('/avatar', ['auth', new UploadMiddleware({ field: 'avatar' })], UserController, 'updateAvatar');
|
|
47
|
+
*
|
|
48
|
+
* // Multiple named fields
|
|
49
|
+
* Route.post('/listing', ['auth', new UploadMiddleware({ fields: [{ name: 'photo', maxCount: 5 }] })], ListingController, 'store');
|
|
50
|
+
*
|
|
51
|
+
* // Any field name (multer .any())
|
|
52
|
+
* Route.post('/upload', [new UploadMiddleware({ any: true })], UploadController, 'store');
|
|
53
|
+
*
|
|
54
|
+
* ── Options ───────────────────────────────────────────────────────────────────
|
|
55
|
+
*
|
|
56
|
+
* {
|
|
57
|
+
* field: string // single field name (default: 'file')
|
|
58
|
+
* fields: [{name, maxCount}] // multiple named fields
|
|
59
|
+
* any: boolean // accept any field name
|
|
60
|
+
* maxSize: number|string // max file size bytes or '10mb' (default: 50mb)
|
|
61
|
+
* storage: 'memory'|'disk' // multer storage (default: 'memory')
|
|
62
|
+
* dest: string // disk storage destination (only when storage: 'disk')
|
|
63
|
+
* filter: Function // custom multer fileFilter(req, file, cb)
|
|
64
|
+
* }
|
|
65
|
+
*
|
|
66
|
+
* ── Handler context ───────────────────────────────────────────────────────────
|
|
67
|
+
*
|
|
68
|
+
* After this middleware runs, RequestContext exposes:
|
|
69
|
+
*
|
|
70
|
+
* ctx.file — single uploaded file (multer file object)
|
|
71
|
+
* ctx.files — multiple uploaded files (object or array)
|
|
72
|
+
* ctx.body — non-file form fields (strings)
|
|
73
|
+
*
|
|
74
|
+
* Multer file object shape:
|
|
75
|
+
* {
|
|
76
|
+
* fieldname: 'file',
|
|
77
|
+
* originalname: 'photo.jpg',
|
|
78
|
+
* mimetype: 'image/jpeg',
|
|
79
|
+
* size: 204800,
|
|
80
|
+
* buffer: <Buffer ...> // memory storage only
|
|
81
|
+
* path: '/tmp/...' // disk storage only
|
|
82
|
+
* }
|
|
83
|
+
*/
|
|
84
|
+
class UploadMiddleware {
|
|
85
|
+
/**
|
|
86
|
+
* @param {object} options
|
|
87
|
+
* @param {string} [options.field='file'] single field name
|
|
88
|
+
* @param {Array<{name,maxCount}>} [options.fields] multiple named fields
|
|
89
|
+
* @param {boolean} [options.any=false] accept any field
|
|
90
|
+
* @param {number|string} [options.maxSize] max file size
|
|
91
|
+
* @param {'memory'|'disk'} [options.storage='memory']
|
|
92
|
+
* @param {string} [options.dest] disk dest (storage:'disk')
|
|
93
|
+
* @param {Function} [options.filter] multer fileFilter
|
|
94
|
+
*/
|
|
95
|
+
constructor(options = {}) {
|
|
96
|
+
this._options = {
|
|
97
|
+
field: options.field || 'file',
|
|
98
|
+
_explicitField: !!options.field, // true only when caller set field explicitly
|
|
99
|
+
fields: options.fields || null,
|
|
100
|
+
any: options.any || false,
|
|
101
|
+
maxSize: options.maxSize !== undefined ? _parseSize(options.maxSize) : 50 * 1024 * 1024,
|
|
102
|
+
storage: options.storage || 'memory',
|
|
103
|
+
dest: options.dest || null,
|
|
104
|
+
filter: options.filter || null,
|
|
105
|
+
};
|
|
106
|
+
this._multerInstance = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Millas Middleware protocol ─────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build and cache the multer middleware on first request, then delegate.
|
|
113
|
+
* Runs as a standard Millas middleware — ctx + next.
|
|
114
|
+
*
|
|
115
|
+
* Because multer is Express middleware internally, we reach into ctx.req.raw
|
|
116
|
+
* to get the underlying Express req/res and call multer directly.
|
|
117
|
+
*
|
|
118
|
+
* Skips gracefully when the request is not multipart/form-data so that
|
|
119
|
+
* non-upload requests to the same route (or misconfigured clients) get a
|
|
120
|
+
* clear 400 rather than a multer internal error.
|
|
121
|
+
*/
|
|
122
|
+
async handle(ctx, next) {
|
|
123
|
+
const expressReq = ctx.req.raw;
|
|
124
|
+
const contentType = expressReq.headers?.['content-type'] || '';
|
|
125
|
+
|
|
126
|
+
// Skip multer entirely for non-multipart requests.
|
|
127
|
+
// ctx.file / ctx.files stay null/{} — the handler or shape validation
|
|
128
|
+
// will surface a missing-required-field error if needed.
|
|
129
|
+
if (!contentType.includes('multipart/form-data')) {
|
|
130
|
+
return next();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const multerMw = this._getMulterMiddleware();
|
|
134
|
+
|
|
135
|
+
await new Promise((resolve, reject) => {
|
|
136
|
+
// multer needs a real Express res object; we synthesize a minimal one
|
|
137
|
+
// that satisfies multer's internal usage (it only calls res.end /
|
|
138
|
+
// statusCode on hard errors, which we convert to thrown exceptions).
|
|
139
|
+
const fakeRes = _buildFakeRes(reject);
|
|
140
|
+
multerMw(expressReq, fakeRes, (err) => {
|
|
141
|
+
if (err) return reject(err);
|
|
142
|
+
resolve();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Wrap raw multer file objects in UploadedFile instances so handlers
|
|
147
|
+
// receive rich, manipulable file objects instead of plain multer blobs.
|
|
148
|
+
const UploadedFile = require('../UploadedFile');
|
|
149
|
+
const { file, files } = UploadedFile.wrapRequest(expressReq);
|
|
150
|
+
|
|
151
|
+
// Sync into RequestContext (which was constructed before multer ran).
|
|
152
|
+
ctx.file = file;
|
|
153
|
+
ctx.files = files;
|
|
154
|
+
|
|
155
|
+
// Also mirror back onto the raw express req so Router._buildShapeMiddleware
|
|
156
|
+
// can pick them up when it merges file inputs for validation.
|
|
157
|
+
expressReq._millaFile = file;
|
|
158
|
+
expressReq._millaFiles = files;
|
|
159
|
+
|
|
160
|
+
// Also update ctx.body with any non-file multipart fields multer parsed.
|
|
161
|
+
// req.body is mutated in-place by multer, but ctx.body was built at
|
|
162
|
+
// RequestContext construction time. Reassign so the handler sees the fields.
|
|
163
|
+
if (expressReq.body && typeof expressReq.body === 'object') {
|
|
164
|
+
// Merge multer-parsed text fields into the existing ctx.body object
|
|
165
|
+
// without breaking the .validate() / .only() / .except() helpers
|
|
166
|
+
// that _buildBody attached as non-enumerable properties.
|
|
167
|
+
Object.assign(ctx.body, expressReq.body);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return next();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Static factory helpers ──────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Build an UploadMiddleware from parameterized alias string parts.
|
|
177
|
+
* Called by MiddlewareRegistry when the alias includes parameters.
|
|
178
|
+
*
|
|
179
|
+
* 'upload' → any field, 50 MB limit (default)
|
|
180
|
+
* 'upload:avatar' → single field named 'avatar'
|
|
181
|
+
* 'upload:photos,5' → field 'photos', up to 5 files
|
|
182
|
+
* 'upload:*' → any field (explicit)
|
|
183
|
+
*
|
|
184
|
+
* @param {string[]} params Parts after the colon, split by comma
|
|
185
|
+
* @returns {UploadMiddleware}
|
|
186
|
+
*/
|
|
187
|
+
static fromParams(params) {
|
|
188
|
+
if (!params || !params.length || params[0] === '*') {
|
|
189
|
+
return new UploadMiddleware({ any: true });
|
|
190
|
+
}
|
|
191
|
+
const field = params[0];
|
|
192
|
+
const maxCount = params[1] ? parseInt(params[1], 10) : 1;
|
|
193
|
+
if (maxCount > 1) {
|
|
194
|
+
return new UploadMiddleware({ fields: [{ name: field, maxCount }] });
|
|
195
|
+
}
|
|
196
|
+
return new UploadMiddleware({ field });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Build an UploadMiddleware from a shape definition.
|
|
201
|
+
* Called internally by the Router when auto-injecting.
|
|
202
|
+
*
|
|
203
|
+
* @param {import('../Shape').ShapeDefinition} shape
|
|
204
|
+
* @returns {UploadMiddleware}
|
|
205
|
+
*/
|
|
206
|
+
static fromShape(shape) {
|
|
207
|
+
const opts = { storage: 'memory' };
|
|
208
|
+
|
|
209
|
+
// Collect file field names from the shape's "in" validators
|
|
210
|
+
const fileFields = _fileFieldsFromShape(shape);
|
|
211
|
+
|
|
212
|
+
if (fileFields.length === 1) {
|
|
213
|
+
opts.field = fileFields[0].name;
|
|
214
|
+
} else if (fileFields.length > 1) {
|
|
215
|
+
opts.fields = fileFields; // [{name, maxCount}]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Honour explicit maxSize from first file validator found
|
|
219
|
+
const firstFileValidator = _firstFileValidator(shape);
|
|
220
|
+
if (firstFileValidator?._maxSizeBytes) {
|
|
221
|
+
opts.maxSize = firstFileValidator._maxSizeBytes;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Build a combined mimeType filter from all file validators
|
|
225
|
+
const allMimes = _allMimeTypes(shape);
|
|
226
|
+
if (allMimes.length) {
|
|
227
|
+
opts.filter = (req, file, cb) => {
|
|
228
|
+
cb(null, allMimes.includes(file.mimetype));
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return new UploadMiddleware(opts);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Internal ───────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
_getMulterMiddleware() {
|
|
238
|
+
if (this._multerInstance) return this._multerInstance;
|
|
239
|
+
|
|
240
|
+
let multer;
|
|
241
|
+
try {
|
|
242
|
+
multer = require('multer');
|
|
243
|
+
} catch {
|
|
244
|
+
throw new Error(
|
|
245
|
+
'[Millas UploadMiddleware] multer is not installed.\n' +
|
|
246
|
+
'Run: npm install multer\n' +
|
|
247
|
+
'multer is required for multipart/file upload support.'
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const { field, fields, any, maxSize, storage, dest, filter } = this._options;
|
|
252
|
+
|
|
253
|
+
// Storage engine
|
|
254
|
+
const storageEngine = storage === 'disk' && dest
|
|
255
|
+
? multer.diskStorage({ destination: dest, filename: (_req, file, cb) => cb(null, `${Date.now()}_${file.originalname}`) })
|
|
256
|
+
: multer.memoryStorage();
|
|
257
|
+
|
|
258
|
+
const multerConfig = { storage: storageEngine, limits: { fileSize: maxSize } };
|
|
259
|
+
if (filter) multerConfig.fileFilter = filter;
|
|
260
|
+
|
|
261
|
+
const upload = multer(multerConfig);
|
|
262
|
+
|
|
263
|
+
// Pick the right multer handler based on options.
|
|
264
|
+
// When used without a shape (e.g. just 'upload' alias), default to .any()
|
|
265
|
+
// so the handler receives whatever field the client sends, regardless of name.
|
|
266
|
+
if (any || (!fields && this._options.field === 'file' && !this._options._explicitField)) {
|
|
267
|
+
this._multerInstance = upload.any();
|
|
268
|
+
} else if (fields) {
|
|
269
|
+
this._multerInstance = upload.fields(fields);
|
|
270
|
+
} else {
|
|
271
|
+
this._multerInstance = upload.single(field);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return this._multerInstance;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Parse a human-readable size string to bytes.
|
|
282
|
+
* '5mb' → 5242880, '500kb' → 512000, 1024 → 1024
|
|
283
|
+
*/
|
|
284
|
+
function _parseSize(size) {
|
|
285
|
+
if (typeof size === 'number') return size;
|
|
286
|
+
const s = String(size).trim().toLowerCase();
|
|
287
|
+
const n = parseFloat(s);
|
|
288
|
+
if (s.endsWith('gb')) return n * 1024 * 1024 * 1024;
|
|
289
|
+
if (s.endsWith('mb')) return n * 1024 * 1024;
|
|
290
|
+
if (s.endsWith('kb')) return n * 1024;
|
|
291
|
+
return n;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Scan a shape's "in" schema for FileValidator instances.
|
|
296
|
+
* Returns [{name, maxCount: 1}] for each file field found.
|
|
297
|
+
*/
|
|
298
|
+
function _fileFieldsFromShape(shape) {
|
|
299
|
+
if (!shape?.in) return [];
|
|
300
|
+
const fields = [];
|
|
301
|
+
for (const [name, validator] of Object.entries(shape.in)) {
|
|
302
|
+
if (validator?._type === 'file') {
|
|
303
|
+
fields.push({ name, maxCount: 1 });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return fields;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function _firstFileValidator(shape) {
|
|
310
|
+
if (!shape?.in) return null;
|
|
311
|
+
for (const validator of Object.values(shape.in)) {
|
|
312
|
+
if (validator?._type === 'file') return validator;
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function _allMimeTypes(shape) {
|
|
318
|
+
if (!shape?.in) return [];
|
|
319
|
+
const mimes = [];
|
|
320
|
+
for (const validator of Object.values(shape.in)) {
|
|
321
|
+
if (validator?._type === 'file' && Array.isArray(validator._mimeTypes)) {
|
|
322
|
+
mimes.push(...validator._mimeTypes);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return [...new Set(mimes)];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Build a minimal fake Express response object for multer.
|
|
330
|
+
* Multer only interacts with res in error cases — we map those to rejections.
|
|
331
|
+
*/
|
|
332
|
+
function _buildFakeRes(reject) {
|
|
333
|
+
return {
|
|
334
|
+
statusCode: 200,
|
|
335
|
+
end: (msg) => reject(new Error(msg || 'Upload error')),
|
|
336
|
+
setHeader: () => {},
|
|
337
|
+
getHeader: () => null,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
module.exports = UploadMiddleware;
|
package/src/router/Router.js
CHANGED
|
@@ -73,6 +73,15 @@ class Router {
|
|
|
73
73
|
// validation middleware that runs BEFORE the handler.
|
|
74
74
|
// On failure → 422 immediately, handler never runs.
|
|
75
75
|
// On success → ctx.body is replaced with coerced, validated output.
|
|
76
|
+
//
|
|
77
|
+
// When the shape declares encoding:'multipart' or contains any file()
|
|
78
|
+
// field, UploadMiddleware is injected first (before validation) so that
|
|
79
|
+
// multer parses the multipart body and populates req.file / req.files /
|
|
80
|
+
// req.body before the validation step reads them.
|
|
81
|
+
const uploadMiddlewares = route.shape && this._shapeNeedsUpload(route.shape)
|
|
82
|
+
? this._buildUploadMiddleware(route.shape)
|
|
83
|
+
: [];
|
|
84
|
+
|
|
76
85
|
const shapeMiddlewares = route.shape
|
|
77
86
|
? this._buildShapeMiddleware(route.shape)
|
|
78
87
|
: [];
|
|
@@ -87,29 +96,98 @@ class Router {
|
|
|
87
96
|
|
|
88
97
|
this._adapter.mountRoute(route.verb, route.path, [
|
|
89
98
|
...middlewareHandlers,
|
|
99
|
+
...uploadMiddlewares,
|
|
90
100
|
...shapeMiddlewares,
|
|
91
101
|
terminalHandler,
|
|
92
102
|
]);
|
|
93
103
|
}
|
|
94
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Returns true if the shape requires multipart parsing —
|
|
107
|
+
* either because encoding is explicitly set to 'multipart', or because
|
|
108
|
+
* the "in" schema contains at least one file() validator.
|
|
109
|
+
*
|
|
110
|
+
* @param {import('../http/Shape').ShapeDefinition} shape
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
_shapeNeedsUpload(shape) {
|
|
114
|
+
if (shape.encoding === 'multipart') return true;
|
|
115
|
+
if (!shape.in) return false;
|
|
116
|
+
return Object.values(shape.in).some(v => v?._type === 'file');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build an Express middleware array that runs UploadMiddleware before
|
|
121
|
+
* the shape validation step. The UploadMiddleware is configured from
|
|
122
|
+
* the shape (field names, maxSize, mimeTypes) so developers don't have
|
|
123
|
+
* to configure it separately.
|
|
124
|
+
*
|
|
125
|
+
* @param {import('../http/Shape').ShapeDefinition} shape
|
|
126
|
+
* @returns {Function[]}
|
|
127
|
+
*/
|
|
128
|
+
_buildUploadMiddleware(shape) {
|
|
129
|
+
const UploadMiddleware = require('../http/middleware/UploadMiddleware');
|
|
130
|
+
const instance = UploadMiddleware.fromShape(shape);
|
|
131
|
+
// Wrap it through the adapter so it becomes an Express (req,res,next) fn
|
|
132
|
+
return [this._adapter.wrapMiddleware(instance, this._container)];
|
|
133
|
+
}
|
|
134
|
+
|
|
95
135
|
/**
|
|
96
136
|
* Build Express middleware functions from a shape definition.
|
|
97
137
|
* Returns an array of 0, 1, or 2 middleware functions
|
|
98
138
|
* (one for body/in, one for query) depending on what the shape declares.
|
|
99
139
|
*
|
|
140
|
+
* For multipart routes, file fields from req.file / req.files are merged
|
|
141
|
+
* into the validation input so file() validators in the schema run correctly.
|
|
142
|
+
*
|
|
100
143
|
* @param {import('../http/Shape').ShapeDefinition} shape
|
|
101
144
|
* @returns {Function[]}
|
|
102
145
|
*/
|
|
103
146
|
_buildShapeMiddleware(shape) {
|
|
104
147
|
const { Validator } = require('../validation/Validator');
|
|
105
148
|
const middlewares = [];
|
|
149
|
+
const isMultipart = this._shapeNeedsUpload(shape);
|
|
106
150
|
|
|
107
151
|
// ── Body / in validation ───────────────────────────────────────────────
|
|
108
152
|
if (shape.in && Object.keys(shape.in).length) {
|
|
109
153
|
middlewares.push(async (req, res, next) => {
|
|
110
154
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
155
|
+
// For multipart requests, merge uploaded files into the validation
|
|
156
|
+
// input so file() validators in the shape's "in" schema can run.
|
|
157
|
+
// req.file → single file (multer .single())
|
|
158
|
+
// req.files → multiple files (multer .fields() or .any())
|
|
159
|
+
let rawBody = req.body || {};
|
|
160
|
+
if (isMultipart) {
|
|
161
|
+
const fileInputs = {};
|
|
162
|
+
// Prefer the already-wrapped UploadedFile instances written by
|
|
163
|
+
// UploadMiddleware so FileValidator receives the same UploadedFile
|
|
164
|
+
// objects that the handler will destructure.
|
|
165
|
+
if (req._millaFile) {
|
|
166
|
+
fileInputs[req._millaFile.fieldName] = req._millaFile;
|
|
167
|
+
}
|
|
168
|
+
if (req._millaFiles) {
|
|
169
|
+
for (const [fieldname, f] of Object.entries(req._millaFiles)) {
|
|
170
|
+
fileInputs[fieldname] = f;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Fallback to raw multer objects if UploadMiddleware hasn't run
|
|
174
|
+
// (e.g. manual upload middleware setup without UploadMiddleware)
|
|
175
|
+
if (!Object.keys(fileInputs).length) {
|
|
176
|
+
if (req.file) fileInputs[req.file.fieldname] = req.file;
|
|
177
|
+
if (req.files) {
|
|
178
|
+
if (Array.isArray(req.files)) {
|
|
179
|
+
for (const f of req.files) fileInputs[f.fieldname] = f;
|
|
180
|
+
} else {
|
|
181
|
+
for (const [fieldname, arr] of Object.entries(req.files)) {
|
|
182
|
+
fileInputs[fieldname] = arr.length === 1 ? arr[0] : arr;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
rawBody = { ...rawBody, ...fileInputs };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const clean = await Validator.validate(rawBody, shape.in);
|
|
113
191
|
// Replace req.body with the coerced, validated subset so the
|
|
114
192
|
// handler's { body } destructure gets clean data automatically.
|
|
115
193
|
req.body = clean;
|
package/src/storage/Storage.js
CHANGED
|
@@ -129,6 +129,10 @@ class Storage {
|
|
|
129
129
|
const driverName = diskConf.driver || name || 'local';
|
|
130
130
|
|
|
131
131
|
switch (driverName) {
|
|
132
|
+
case 's3': {
|
|
133
|
+
const S3Driver = require('./drivers/S3Driver');
|
|
134
|
+
return new S3Driver(diskConf);
|
|
135
|
+
}
|
|
132
136
|
case 'local':
|
|
133
137
|
default: {
|
|
134
138
|
const LocalDriver = require('./drivers/LocalDriver');
|