@reldens/server-utils 0.25.0 → 0.26.0
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/lib/uploader-factory.js +71 -24
- package/package.json +1 -1
package/lib/uploader-factory.js
CHANGED
|
@@ -25,10 +25,17 @@ class UploaderFactory
|
|
|
25
25
|
this.maxFilenameLength = props.maxFilenameLength || 255;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
setError(message, additionalData = {})
|
|
29
|
+
{
|
|
30
|
+
if(!this.error.message){
|
|
31
|
+
this.error = {message, ...additionalData};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
createUploader(fields, buckets, allowedFileTypes)
|
|
29
36
|
{
|
|
30
37
|
if(!this.validateInputs(fields, buckets, allowedFileTypes)){
|
|
31
|
-
this.
|
|
38
|
+
this.setError('Invalid uploader configuration: ' + this.error.message);
|
|
32
39
|
return false;
|
|
33
40
|
}
|
|
34
41
|
let storage = multer.diskStorage({
|
|
@@ -36,17 +43,23 @@ class UploaderFactory
|
|
|
36
43
|
try{
|
|
37
44
|
let dest = buckets[file.fieldname];
|
|
38
45
|
if(!FileHandler.isValidPath(dest)){
|
|
46
|
+
this.setError('Invalid destination path', {dest, fieldname: file.fieldname});
|
|
39
47
|
return cb(new Error('Invalid destination path'));
|
|
40
48
|
}
|
|
41
|
-
FileHandler.createFolder(dest);
|
|
49
|
+
let folderCreated = FileHandler.createFolder(dest);
|
|
50
|
+
if(!folderCreated){
|
|
51
|
+
this.setError('Cannot create destination folder', {dest, fileHandlerError: FileHandler.error});
|
|
52
|
+
return cb(new Error('Cannot create destination folder'));
|
|
53
|
+
}
|
|
42
54
|
cb(null, dest);
|
|
43
55
|
} catch(error){
|
|
44
|
-
this.
|
|
56
|
+
this.setError('Cannot prepare destination.', {error: error});
|
|
45
57
|
cb(error);
|
|
46
58
|
}
|
|
47
59
|
},
|
|
48
60
|
filename: (req, file, cb) => {
|
|
49
61
|
if(!this.validateFilenameSecurity(file.originalname)){
|
|
62
|
+
this.setError('Invalid filename', {originalname: file.originalname});
|
|
50
63
|
return cb(new Error('Invalid filename'));
|
|
51
64
|
}
|
|
52
65
|
if(!this.applySecureFileNames) {
|
|
@@ -80,6 +93,7 @@ class UploaderFactory
|
|
|
80
93
|
if(multerError instanceof multer.MulterError){
|
|
81
94
|
if('LIMIT_FILE_SIZE' === multerError.code){
|
|
82
95
|
let messageFile = 'File too large.';
|
|
96
|
+
this.setError(messageFile, {multerError});
|
|
83
97
|
if('function' === typeof this.processErrorResponse){
|
|
84
98
|
return this.processErrorResponse(413, messageFile, req, res);
|
|
85
99
|
}
|
|
@@ -87,18 +101,21 @@ class UploaderFactory
|
|
|
87
101
|
}
|
|
88
102
|
if('LIMIT_FILE_COUNT' === multerError.code){
|
|
89
103
|
let messageTooMany = 'Too many files.';
|
|
104
|
+
this.setError(messageTooMany, {multerError});
|
|
90
105
|
if('function' === typeof this.processErrorResponse){
|
|
91
106
|
return this.processErrorResponse(413, messageTooMany, req, res);
|
|
92
107
|
}
|
|
93
108
|
return res.status(413).send(messageTooMany);
|
|
94
109
|
}
|
|
95
110
|
let messageUpload = 'File upload error.';
|
|
111
|
+
this.setError(messageUpload, {multerError});
|
|
96
112
|
if('function' === typeof this.processErrorResponse){
|
|
97
113
|
return this.processErrorResponse(400, messageUpload, multerError, req, res);
|
|
98
114
|
}
|
|
99
115
|
return res.status(400).send(messageUpload);
|
|
100
116
|
}
|
|
101
|
-
let messageServer = this.error
|
|
117
|
+
let messageServer = this.error?.message ? this.error.message : 'Server error during file upload.';
|
|
118
|
+
this.setError(messageServer, {multerError});
|
|
102
119
|
if('function' === typeof this.processErrorResponse){
|
|
103
120
|
return this.processErrorResponse(415, messageServer, req, res);
|
|
104
121
|
}
|
|
@@ -109,8 +126,9 @@ class UploaderFactory
|
|
|
109
126
|
}
|
|
110
127
|
let validationResult = await this.validateAllUploadedFiles(req, allowedFileTypes);
|
|
111
128
|
if(!validationResult){
|
|
112
|
-
|
|
113
|
-
|
|
129
|
+
let filePaths = Object.values(req.files).flat().map(file => file.path);
|
|
130
|
+
FileHandler.removeMultiple(filePaths);
|
|
131
|
+
let messageContents = this.error?.message ? this.error.message : 'File validation failed.';
|
|
114
132
|
if('function' === typeof this.processErrorResponse){
|
|
115
133
|
return this.processErrorResponse(415, messageContents, req, res);
|
|
116
134
|
}
|
|
@@ -126,7 +144,8 @@ class UploaderFactory
|
|
|
126
144
|
try {
|
|
127
145
|
for(let fieldName in req.files){
|
|
128
146
|
for(let file of req.files[fieldName]){
|
|
129
|
-
|
|
147
|
+
let validationResult = await this.validateFileContents(file, allowedFileTypes[fieldName]);
|
|
148
|
+
if(!validationResult){
|
|
130
149
|
FileHandler.remove(file.path);
|
|
131
150
|
return false;
|
|
132
151
|
}
|
|
@@ -134,7 +153,7 @@ class UploaderFactory
|
|
|
134
153
|
}
|
|
135
154
|
return true;
|
|
136
155
|
} catch(error){
|
|
137
|
-
this.
|
|
156
|
+
this.setError('Error processing uploaded files.', {error});
|
|
138
157
|
return false;
|
|
139
158
|
}
|
|
140
159
|
}
|
|
@@ -163,28 +182,28 @@ class UploaderFactory
|
|
|
163
182
|
validateInputs(fields, buckets, allowedFileTypes)
|
|
164
183
|
{
|
|
165
184
|
if(!Array.isArray(fields)){
|
|
166
|
-
this.
|
|
185
|
+
this.setError('Fields must be an array');
|
|
167
186
|
return false;
|
|
168
187
|
}
|
|
169
188
|
if(!buckets || 'object' !== typeof buckets){
|
|
170
|
-
this.
|
|
189
|
+
this.setError('Buckets must be an object');
|
|
171
190
|
return false;
|
|
172
191
|
}
|
|
173
192
|
if(!allowedFileTypes || 'object' !== typeof allowedFileTypes){
|
|
174
|
-
this.
|
|
193
|
+
this.setError('AllowedFileTypes must be an object');
|
|
175
194
|
return false;
|
|
176
195
|
}
|
|
177
196
|
for(let field of fields){
|
|
178
197
|
if(!field.name || 'string' !== typeof field.name){
|
|
179
|
-
this.
|
|
198
|
+
this.setError('Field name is invalid');
|
|
180
199
|
return false;
|
|
181
200
|
}
|
|
182
201
|
if(!buckets[field.name]){
|
|
183
|
-
this.
|
|
202
|
+
this.setError('Missing bucket for field: ' + field.name);
|
|
184
203
|
return false;
|
|
185
204
|
}
|
|
186
205
|
if(!allowedFileTypes[field.name]){
|
|
187
|
-
this.
|
|
206
|
+
this.setError('Missing allowedFileType for field: ' + field.name);
|
|
188
207
|
return false;
|
|
189
208
|
}
|
|
190
209
|
}
|
|
@@ -197,23 +216,33 @@ class UploaderFactory
|
|
|
197
216
|
return cb(null, true);
|
|
198
217
|
}
|
|
199
218
|
if(!this.validateFilenameSecurity(file.originalname)){
|
|
200
|
-
this.
|
|
219
|
+
this.setError('Insecure filename: '+file.originalname, {originalname: file.originalname});
|
|
201
220
|
return cb(new Error('Insecure filename: '+file.originalname));
|
|
202
221
|
}
|
|
203
222
|
let fileExtension = FileHandler.extension(file.originalname).toLowerCase();
|
|
204
223
|
let allowedExtensions = this.allowedExtensions && this.allowedExtensions[allowedFileType];
|
|
205
224
|
if(allowedExtensions && !allowedExtensions.includes(fileExtension)){
|
|
206
|
-
this.
|
|
225
|
+
this.setError('Invalid file extension: '+fileExtension, {
|
|
226
|
+
extension: fileExtension,
|
|
227
|
+
allowedExtensions,
|
|
228
|
+
allowedFileType,
|
|
229
|
+
filename: file.originalname
|
|
230
|
+
});
|
|
207
231
|
return cb(new Error('Invalid file extension: '+fileExtension));
|
|
208
232
|
}
|
|
209
233
|
let allowedFileTypeRegex = this.convertToRegex(allowedFileType);
|
|
210
234
|
if(!allowedFileTypeRegex){
|
|
211
|
-
this.
|
|
235
|
+
this.setError('File type could not be converted to regex.', {allowedFileType});
|
|
212
236
|
return cb(new Error('File type could not be converted to regex'));
|
|
213
237
|
}
|
|
214
238
|
let mimeTypeValid = allowedFileTypeRegex.test(file.mimetype);
|
|
215
239
|
if(!mimeTypeValid){
|
|
216
|
-
this.
|
|
240
|
+
this.setError('Invalid MIME type: '+file.mimetype, {
|
|
241
|
+
mimetype: file.mimetype,
|
|
242
|
+
allowedFileType,
|
|
243
|
+
filename: file.originalname,
|
|
244
|
+
regex: allowedFileTypeRegex
|
|
245
|
+
});
|
|
217
246
|
return cb(new Error('Invalid MIME type: '+file.mimetype));
|
|
218
247
|
}
|
|
219
248
|
return cb(null, true);
|
|
@@ -222,21 +251,39 @@ class UploaderFactory
|
|
|
222
251
|
async validateFileContents(file, allowedFileType)
|
|
223
252
|
{
|
|
224
253
|
if(!FileHandler.isFile(file.path)){
|
|
225
|
-
this.
|
|
254
|
+
this.setError('File path must be provided.', {file});
|
|
226
255
|
return false;
|
|
227
256
|
}
|
|
228
257
|
let detectedType = FileHandler.detectFileType(file.path);
|
|
229
258
|
if(detectedType && 'application/octet-stream' !== detectedType){
|
|
230
259
|
let expectedMimeTypes = this.mimeTypes[allowedFileType] || [];
|
|
231
260
|
if(0 < expectedMimeTypes.length && -1 === expectedMimeTypes.indexOf(detectedType)){
|
|
232
|
-
this.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
261
|
+
this.setError('File content type mismatch.', {
|
|
262
|
+
detected: detectedType,
|
|
263
|
+
expected: expectedMimeTypes,
|
|
264
|
+
filename: file.filename,
|
|
265
|
+
path: file.path,
|
|
266
|
+
allowedFileType
|
|
267
|
+
});
|
|
236
268
|
return false;
|
|
237
269
|
}
|
|
238
270
|
}
|
|
239
|
-
|
|
271
|
+
let typeValidationResult = FileHandler.validateFileType(
|
|
272
|
+
file.path,
|
|
273
|
+
allowedFileType,
|
|
274
|
+
this.allowedExtensions,
|
|
275
|
+
this.maxFileSize
|
|
276
|
+
);
|
|
277
|
+
if(!typeValidationResult){
|
|
278
|
+
this.setError('File type validation failed.', {
|
|
279
|
+
fileHandlerError: FileHandler.error,
|
|
280
|
+
filename: file.filename,
|
|
281
|
+
path: file.path,
|
|
282
|
+
allowedFileType
|
|
283
|
+
});
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
240
287
|
}
|
|
241
288
|
|
|
242
289
|
convertToRegex(key)
|