@flusys/nestjs-storage 1.1.0-beta → 2.0.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/README.md +148 -6
- package/cjs/config/index.js +0 -1
- package/cjs/config/storage.constants.js +0 -17
- package/cjs/controllers/file-manager.controller.js +44 -1
- package/cjs/controllers/folder.controller.js +44 -1
- package/cjs/controllers/storage-config.controller.js +44 -1
- package/cjs/controllers/upload.controller.js +18 -29
- package/cjs/docs/storage-swagger.config.js +24 -136
- package/cjs/dtos/file-manager.dto.js +70 -34
- package/cjs/dtos/folder.dto.js +15 -9
- package/cjs/dtos/storage-config.dto.js +4 -85
- package/cjs/dtos/upload.dto.js +24 -17
- package/cjs/entities/file-manager-with-company.entity.js +3 -4
- package/cjs/entities/file-manager.entity.js +71 -3
- package/cjs/entities/folder-with-company.entity.js +3 -4
- package/cjs/entities/folder.entity.js +19 -3
- package/cjs/entities/index.js +9 -10
- package/cjs/entities/storage-config-with-company.entity.js +3 -4
- package/cjs/entities/storage-config.entity.js +74 -3
- package/cjs/interfaces/index.js +0 -1
- package/cjs/middlewares/file-serve.middleware.js +113 -100
- package/cjs/modules/storage.module.js +82 -136
- package/cjs/providers/azure-provider.optional.js +10 -38
- package/cjs/providers/local-provider.js +38 -31
- package/cjs/providers/s3-provider.optional.js +19 -40
- package/cjs/providers/storage-factory.service.js +54 -99
- package/cjs/providers/storage-provider.registry.js +8 -18
- package/cjs/services/file-manager.service.js +238 -323
- package/cjs/services/folder.service.js +8 -11
- package/cjs/services/index.js +1 -0
- package/cjs/{config → services}/storage-config.service.js +32 -76
- package/cjs/services/storage-datasource.provider.js +16 -26
- package/cjs/services/storage-provider-config.service.js +15 -37
- package/cjs/services/upload.service.js +72 -88
- package/cjs/utils/file-validator.util.js +458 -0
- package/cjs/utils/image-compressor.util.js +3 -8
- package/config/index.d.ts +0 -1
- package/config/storage.constants.d.ts +0 -8
- package/controllers/upload.controller.d.ts +3 -6
- package/dtos/file-manager.dto.d.ts +12 -5
- package/dtos/folder.dto.d.ts +5 -5
- package/dtos/storage-config.dto.d.ts +7 -13
- package/entities/file-manager-with-company.entity.d.ts +2 -2
- package/entities/file-manager.entity.d.ts +12 -2
- package/entities/folder-with-company.entity.d.ts +2 -2
- package/entities/folder.entity.d.ts +4 -2
- package/entities/index.d.ts +3 -4
- package/entities/storage-config-with-company.entity.d.ts +2 -2
- package/entities/storage-config.entity.d.ts +8 -2
- package/fesm/config/index.js +0 -1
- package/fesm/config/storage.constants.js +0 -8
- package/fesm/controllers/file-manager.controller.js +45 -2
- package/fesm/controllers/folder.controller.js +45 -2
- package/fesm/controllers/storage-config.controller.js +45 -2
- package/fesm/controllers/upload.controller.js +19 -30
- package/fesm/docs/storage-swagger.config.js +27 -142
- package/fesm/dtos/file-manager.dto.js +71 -35
- package/fesm/dtos/folder.dto.js +16 -10
- package/fesm/dtos/storage-config.dto.js +8 -95
- package/fesm/dtos/upload.dto.js +25 -19
- package/fesm/entities/file-manager-with-company.entity.js +3 -4
- package/fesm/entities/file-manager.entity.js +72 -4
- package/fesm/entities/folder-with-company.entity.js +3 -4
- package/fesm/entities/folder.entity.js +20 -4
- package/fesm/entities/index.js +5 -13
- package/fesm/entities/storage-config-with-company.entity.js +3 -4
- package/fesm/entities/storage-config.entity.js +75 -4
- package/fesm/interfaces/index.js +0 -1
- package/fesm/interfaces/storage-config.interface.js +1 -3
- package/fesm/middlewares/file-serve.middleware.js +114 -101
- package/fesm/modules/storage.module.js +83 -136
- package/fesm/providers/azure-provider.optional.js +11 -42
- package/fesm/providers/local-provider.js +38 -31
- package/fesm/providers/s3-provider.optional.js +20 -44
- package/fesm/providers/storage-factory.service.js +52 -97
- package/fesm/providers/storage-provider.registry.js +10 -20
- package/fesm/services/file-manager.service.js +237 -322
- package/fesm/services/folder.service.js +6 -9
- package/fesm/services/index.js +1 -0
- package/fesm/{config → services}/storage-config.service.js +32 -76
- package/fesm/services/storage-datasource.provider.js +16 -26
- package/fesm/services/storage-provider-config.service.js +13 -35
- package/fesm/services/upload.service.js +71 -87
- package/fesm/utils/file-validator.util.js +451 -0
- package/fesm/utils/image-compressor.util.js +3 -8
- package/interfaces/file-manager.interface.d.ts +7 -4
- package/interfaces/index.d.ts +0 -1
- package/interfaces/storage-config.interface.d.ts +0 -20
- package/interfaces/storage-module-options.interface.d.ts +0 -5
- package/middlewares/file-serve.middleware.d.ts +9 -1
- package/modules/storage.module.d.ts +1 -2
- package/package.json +6 -6
- package/providers/azure-provider.optional.d.ts +8 -6
- package/providers/local-provider.d.ts +2 -7
- package/providers/s3-provider.optional.d.ts +9 -7
- package/providers/storage-factory.service.d.ts +8 -9
- package/providers/storage-provider.registry.d.ts +4 -4
- package/services/file-manager.service.d.ts +23 -16
- package/services/folder.service.d.ts +4 -4
- package/services/index.d.ts +1 -0
- package/services/storage-config.service.d.ts +24 -0
- package/services/storage-datasource.provider.d.ts +3 -4
- package/services/storage-provider-config.service.d.ts +4 -4
- package/services/upload.service.d.ts +3 -2
- package/utils/file-validator.util.d.ts +19 -0
- package/cjs/entities/file-manager-base.entity.js +0 -115
- package/cjs/entities/folder-base.entity.js +0 -55
- package/cjs/entities/storage-config-base.entity.js +0 -93
- package/cjs/interfaces/file-upload-response.interface.js +0 -4
- package/config/storage-config.service.d.ts +0 -22
- package/entities/file-manager-base.entity.d.ts +0 -13
- package/entities/folder-base.entity.d.ts +0 -5
- package/entities/storage-config-base.entity.d.ts +0 -9
- package/fesm/entities/file-manager-base.entity.js +0 -108
- package/fesm/entities/folder-base.entity.js +0 -48
- package/fesm/entities/storage-config-base.entity.js +0 -83
- package/fesm/interfaces/file-upload-response.interface.js +0 -1
- package/interfaces/file-upload-response.interface.d.ts +0 -6
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
function _define_property(obj, key, value) {
|
|
2
|
+
if (key in obj) {
|
|
3
|
+
Object.defineProperty(obj, key, {
|
|
4
|
+
value: value,
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true
|
|
8
|
+
});
|
|
9
|
+
} else {
|
|
10
|
+
obj[key] = value;
|
|
11
|
+
}
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
14
|
+
import { Logger } from '@nestjs/common';
|
|
15
|
+
/**
|
|
16
|
+
* Magic byte signatures for common file types.
|
|
17
|
+
* Each entry maps a hex signature pattern to its MIME type.
|
|
18
|
+
*/ const MAGIC_BYTES = [
|
|
19
|
+
// Images
|
|
20
|
+
{
|
|
21
|
+
signature: [
|
|
22
|
+
0xff,
|
|
23
|
+
0xd8,
|
|
24
|
+
0xff
|
|
25
|
+
],
|
|
26
|
+
offset: 0,
|
|
27
|
+
mimeType: 'image/jpeg'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
signature: [
|
|
31
|
+
0x89,
|
|
32
|
+
0x50,
|
|
33
|
+
0x4e,
|
|
34
|
+
0x47,
|
|
35
|
+
0x0d,
|
|
36
|
+
0x0a,
|
|
37
|
+
0x1a,
|
|
38
|
+
0x0a
|
|
39
|
+
],
|
|
40
|
+
offset: 0,
|
|
41
|
+
mimeType: 'image/png'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
signature: [
|
|
45
|
+
0x47,
|
|
46
|
+
0x49,
|
|
47
|
+
0x46,
|
|
48
|
+
0x38,
|
|
49
|
+
0x37,
|
|
50
|
+
0x61
|
|
51
|
+
],
|
|
52
|
+
offset: 0,
|
|
53
|
+
mimeType: 'image/gif'
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
signature: [
|
|
57
|
+
0x47,
|
|
58
|
+
0x49,
|
|
59
|
+
0x46,
|
|
60
|
+
0x38,
|
|
61
|
+
0x39,
|
|
62
|
+
0x61
|
|
63
|
+
],
|
|
64
|
+
offset: 0,
|
|
65
|
+
mimeType: 'image/gif'
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
signature: [
|
|
69
|
+
0x42,
|
|
70
|
+
0x4d
|
|
71
|
+
],
|
|
72
|
+
offset: 0,
|
|
73
|
+
mimeType: 'image/bmp'
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
signature: [
|
|
77
|
+
0x52,
|
|
78
|
+
0x49,
|
|
79
|
+
0x46,
|
|
80
|
+
0x46
|
|
81
|
+
],
|
|
82
|
+
offset: 0,
|
|
83
|
+
mimeType: 'image/webp'
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
signature: [
|
|
87
|
+
0x00,
|
|
88
|
+
0x00,
|
|
89
|
+
0x01,
|
|
90
|
+
0x00
|
|
91
|
+
],
|
|
92
|
+
offset: 0,
|
|
93
|
+
mimeType: 'image/x-icon'
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
signature: [
|
|
97
|
+
0x00,
|
|
98
|
+
0x00,
|
|
99
|
+
0x02,
|
|
100
|
+
0x00
|
|
101
|
+
],
|
|
102
|
+
offset: 0,
|
|
103
|
+
mimeType: 'image/x-icon'
|
|
104
|
+
},
|
|
105
|
+
// Documents
|
|
106
|
+
{
|
|
107
|
+
signature: [
|
|
108
|
+
0x25,
|
|
109
|
+
0x50,
|
|
110
|
+
0x44,
|
|
111
|
+
0x46
|
|
112
|
+
],
|
|
113
|
+
offset: 0,
|
|
114
|
+
mimeType: 'application/pdf'
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
signature: [
|
|
118
|
+
0x50,
|
|
119
|
+
0x4b,
|
|
120
|
+
0x03,
|
|
121
|
+
0x04
|
|
122
|
+
],
|
|
123
|
+
offset: 0,
|
|
124
|
+
mimeType: 'application/zip'
|
|
125
|
+
},
|
|
126
|
+
// Audio
|
|
127
|
+
{
|
|
128
|
+
signature: [
|
|
129
|
+
0x49,
|
|
130
|
+
0x44,
|
|
131
|
+
0x33
|
|
132
|
+
],
|
|
133
|
+
offset: 0,
|
|
134
|
+
mimeType: 'audio/mpeg'
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
signature: [
|
|
138
|
+
0xff,
|
|
139
|
+
0xfb
|
|
140
|
+
],
|
|
141
|
+
offset: 0,
|
|
142
|
+
mimeType: 'audio/mpeg'
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
signature: [
|
|
146
|
+
0xff,
|
|
147
|
+
0xfa
|
|
148
|
+
],
|
|
149
|
+
offset: 0,
|
|
150
|
+
mimeType: 'audio/mpeg'
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
signature: [
|
|
154
|
+
0x4f,
|
|
155
|
+
0x67,
|
|
156
|
+
0x67,
|
|
157
|
+
0x53
|
|
158
|
+
],
|
|
159
|
+
offset: 0,
|
|
160
|
+
mimeType: 'audio/ogg'
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
signature: [
|
|
164
|
+
0x66,
|
|
165
|
+
0x4c,
|
|
166
|
+
0x61,
|
|
167
|
+
0x43
|
|
168
|
+
],
|
|
169
|
+
offset: 0,
|
|
170
|
+
mimeType: 'audio/flac'
|
|
171
|
+
},
|
|
172
|
+
// Video
|
|
173
|
+
{
|
|
174
|
+
signature: [
|
|
175
|
+
0x00,
|
|
176
|
+
0x00,
|
|
177
|
+
0x00,
|
|
178
|
+
0x1c,
|
|
179
|
+
0x66,
|
|
180
|
+
0x74,
|
|
181
|
+
0x79,
|
|
182
|
+
0x70
|
|
183
|
+
],
|
|
184
|
+
offset: 0,
|
|
185
|
+
mimeType: 'video/mp4'
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
signature: [
|
|
189
|
+
0x00,
|
|
190
|
+
0x00,
|
|
191
|
+
0x00,
|
|
192
|
+
0x20,
|
|
193
|
+
0x66,
|
|
194
|
+
0x74,
|
|
195
|
+
0x79,
|
|
196
|
+
0x70
|
|
197
|
+
],
|
|
198
|
+
offset: 0,
|
|
199
|
+
mimeType: 'video/mp4'
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
signature: [
|
|
203
|
+
0x1a,
|
|
204
|
+
0x45,
|
|
205
|
+
0xdf,
|
|
206
|
+
0xa3
|
|
207
|
+
],
|
|
208
|
+
offset: 0,
|
|
209
|
+
mimeType: 'video/webm'
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
signature: [
|
|
213
|
+
0x52,
|
|
214
|
+
0x49,
|
|
215
|
+
0x46,
|
|
216
|
+
0x46
|
|
217
|
+
],
|
|
218
|
+
offset: 0,
|
|
219
|
+
mimeType: 'video/avi'
|
|
220
|
+
},
|
|
221
|
+
// Archives
|
|
222
|
+
{
|
|
223
|
+
signature: [
|
|
224
|
+
0x1f,
|
|
225
|
+
0x8b
|
|
226
|
+
],
|
|
227
|
+
offset: 0,
|
|
228
|
+
mimeType: 'application/gzip'
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
signature: [
|
|
232
|
+
0x37,
|
|
233
|
+
0x7a,
|
|
234
|
+
0xbc,
|
|
235
|
+
0xaf,
|
|
236
|
+
0x27,
|
|
237
|
+
0x1c
|
|
238
|
+
],
|
|
239
|
+
offset: 0,
|
|
240
|
+
mimeType: 'application/x-7z-compressed'
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
signature: [
|
|
244
|
+
0x52,
|
|
245
|
+
0x61,
|
|
246
|
+
0x72,
|
|
247
|
+
0x21,
|
|
248
|
+
0x1a,
|
|
249
|
+
0x07
|
|
250
|
+
],
|
|
251
|
+
offset: 0,
|
|
252
|
+
mimeType: 'application/x-rar-compressed'
|
|
253
|
+
}
|
|
254
|
+
];
|
|
255
|
+
/**
|
|
256
|
+
* MIME type aliases - types that are equivalent.
|
|
257
|
+
*/ const MIME_ALIASES = {
|
|
258
|
+
'image/jpeg': [
|
|
259
|
+
'image/jpg'
|
|
260
|
+
],
|
|
261
|
+
'image/jpg': [
|
|
262
|
+
'image/jpeg'
|
|
263
|
+
],
|
|
264
|
+
'video/mp4': [
|
|
265
|
+
'video/quicktime'
|
|
266
|
+
],
|
|
267
|
+
'application/zip': [
|
|
268
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
269
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
270
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
|
271
|
+
]
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* File types that are text-based and don't have magic bytes.
|
|
275
|
+
* SECURITY NOTE: Dangerous types (HTML, JS, SVG) are excluded as they can contain scripts.
|
|
276
|
+
* These types require explicit allowlisting and additional content scanning.
|
|
277
|
+
*/ const TEXT_BASED_TYPES = [
|
|
278
|
+
'text/plain',
|
|
279
|
+
'text/csv',
|
|
280
|
+
'text/markdown',
|
|
281
|
+
'application/json',
|
|
282
|
+
'application/xml',
|
|
283
|
+
'application/typescript',
|
|
284
|
+
'text/css'
|
|
285
|
+
];
|
|
286
|
+
/**
|
|
287
|
+
* Dangerous text-based types that can execute scripts.
|
|
288
|
+
* These bypass magic-bytes validation but require explicit allowlisting.
|
|
289
|
+
*/ const DANGEROUS_TEXT_TYPES = [
|
|
290
|
+
'text/html',
|
|
291
|
+
'application/javascript',
|
|
292
|
+
'text/javascript',
|
|
293
|
+
'image/svg+xml',
|
|
294
|
+
'application/xhtml+xml'
|
|
295
|
+
];
|
|
296
|
+
/**
|
|
297
|
+
* ZIP-based format prefixes that are valid when detected as application/zip.
|
|
298
|
+
*/ const ZIP_VARIANT_PREFIXES = [
|
|
299
|
+
'application/vnd.openxmlformats-officedocument',
|
|
300
|
+
'application/x-zip',
|
|
301
|
+
'application/x-compressed'
|
|
302
|
+
];
|
|
303
|
+
/**
|
|
304
|
+
* Utility class for validating file content using magic bytes.
|
|
305
|
+
* Prevents file type spoofing by checking actual file content.
|
|
306
|
+
*/ export class FileValidator {
|
|
307
|
+
// Result Helpers
|
|
308
|
+
static failureResult(message, detectedType, declaredType) {
|
|
309
|
+
return {
|
|
310
|
+
valid: false,
|
|
311
|
+
detectedType,
|
|
312
|
+
declaredType,
|
|
313
|
+
message
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
static successResult(detectedType, declaredType) {
|
|
317
|
+
return {
|
|
318
|
+
valid: true,
|
|
319
|
+
detectedType,
|
|
320
|
+
declaredType
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Detect file type from buffer using magic bytes.
|
|
325
|
+
* @param buffer - File buffer to analyze
|
|
326
|
+
* @returns Detected MIME type or null if unknown
|
|
327
|
+
*/ static detectFileType(buffer) {
|
|
328
|
+
for (const { signature, offset, mimeType } of MAGIC_BYTES){
|
|
329
|
+
if (buffer.length < offset + signature.length) continue;
|
|
330
|
+
let matches = true;
|
|
331
|
+
for(let i = 0; i < signature.length; i++){
|
|
332
|
+
if (buffer[offset + i] !== signature[i]) {
|
|
333
|
+
matches = false;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (matches) {
|
|
338
|
+
return mimeType;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Check if a MIME type is text-based (doesn't have magic bytes).
|
|
345
|
+
*/ static isTextBasedType(mimeType) {
|
|
346
|
+
return TEXT_BASED_TYPES.some((t)=>mimeType.startsWith(t) || mimeType === t);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Check if a MIME type is a dangerous text type that can execute scripts.
|
|
350
|
+
*/ static isDangerousTextType(mimeType) {
|
|
351
|
+
return DANGEROUS_TEXT_TYPES.some((t)=>mimeType === t);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Check if two MIME types are compatible (exact match or aliases).
|
|
355
|
+
*/ static mimeTypesMatch(detected, declared) {
|
|
356
|
+
// Exact match
|
|
357
|
+
if (detected === declared) return true;
|
|
358
|
+
// Check aliases
|
|
359
|
+
const aliases = MIME_ALIASES[detected];
|
|
360
|
+
if (aliases?.includes(declared)) return true;
|
|
361
|
+
// Check reverse aliases
|
|
362
|
+
const reverseAliases = MIME_ALIASES[declared];
|
|
363
|
+
if (reverseAliases?.includes(detected)) return true;
|
|
364
|
+
// Check if both are in same category (e.g., both images)
|
|
365
|
+
const detectedCategory = detected.split('/')[0];
|
|
366
|
+
const declaredCategory = declared.split('/')[0];
|
|
367
|
+
// For ZIP-based formats, allow any ZIP-detected file if declared is a ZIP variant
|
|
368
|
+
if (detected === 'application/zip' && ZIP_VARIANT_PREFIXES.some((v)=>declared.startsWith(v))) {
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
return detectedCategory === declaredCategory;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Check if a MIME type is in the allowed list.
|
|
375
|
+
*/ static isTypeAllowed(mimeType, allowedTypes) {
|
|
376
|
+
// Wildcard allows all
|
|
377
|
+
if (allowedTypes.includes('*/*')) return true;
|
|
378
|
+
return allowedTypes.some((allowed)=>{
|
|
379
|
+
// Category wildcard (e.g., "image/*")
|
|
380
|
+
if (allowed.endsWith('/*')) {
|
|
381
|
+
const category = allowed.slice(0, -2);
|
|
382
|
+
return mimeType.startsWith(category);
|
|
383
|
+
}
|
|
384
|
+
return allowed === mimeType;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Validate file content matches declared MIME type using magic bytes.
|
|
389
|
+
* @param buffer - File buffer
|
|
390
|
+
* @param declaredMimeType - MIME type declared by client
|
|
391
|
+
* @param allowedTypes - List of allowed MIME types/patterns
|
|
392
|
+
* @returns Validation result
|
|
393
|
+
*/ static validateFileContent(buffer, declaredMimeType, allowedTypes = [
|
|
394
|
+
'*/*'
|
|
395
|
+
]) {
|
|
396
|
+
try {
|
|
397
|
+
const detectedType = this.detectFileType(buffer);
|
|
398
|
+
// No magic bytes detected - handle text-based types
|
|
399
|
+
if (!detectedType) {
|
|
400
|
+
return this.validateUndetectedType(declaredMimeType, allowedTypes);
|
|
401
|
+
}
|
|
402
|
+
// Verify detected type matches declared type
|
|
403
|
+
if (!this.mimeTypesMatch(detectedType, declaredMimeType)) {
|
|
404
|
+
this.logger.warn(`MIME type mismatch: declared=${declaredMimeType}, detected=${detectedType}`);
|
|
405
|
+
return this.failureResult(`File content does not match declared type. Detected: ${detectedType}, Declared: ${declaredMimeType}`, detectedType, declaredMimeType);
|
|
406
|
+
}
|
|
407
|
+
// Verify type is in allowed list
|
|
408
|
+
if (!this.isTypeAllowed(detectedType, allowedTypes)) {
|
|
409
|
+
return this.failureResult(`File type "${detectedType}" is not allowed`, detectedType, declaredMimeType);
|
|
410
|
+
}
|
|
411
|
+
return this.successResult(detectedType, declaredMimeType);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
this.logger.error('File validation error:', error);
|
|
414
|
+
return this.failureResult('File validation failed');
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Handle validation for files without detectable magic bytes.
|
|
419
|
+
*/ static validateUndetectedType(declaredMimeType, allowedTypes) {
|
|
420
|
+
// Check for dangerous text types first (HTML, JS, SVG)
|
|
421
|
+
if (this.isDangerousTextType(declaredMimeType)) {
|
|
422
|
+
const explicitlyAllowed = allowedTypes.some((t)=>t === declaredMimeType && t !== '*/*' && !t.endsWith('/*'));
|
|
423
|
+
if (!explicitlyAllowed) {
|
|
424
|
+
this.logger.warn(`Blocked dangerous file type: ${declaredMimeType} - requires explicit allowlisting`);
|
|
425
|
+
return this.failureResult(`File type "${declaredMimeType}" is potentially dangerous and not explicitly allowed`, declaredMimeType, declaredMimeType);
|
|
426
|
+
}
|
|
427
|
+
this.logger.warn(`Allowing explicitly permitted dangerous file type: ${declaredMimeType}`);
|
|
428
|
+
}
|
|
429
|
+
// Safe text-based files don't have magic bytes, trust declared type
|
|
430
|
+
if (this.isTextBasedType(declaredMimeType)) {
|
|
431
|
+
const isAllowed = this.isTypeAllowed(declaredMimeType, allowedTypes);
|
|
432
|
+
return isAllowed ? this.successResult(declaredMimeType, declaredMimeType) : this.failureResult(`File type "${declaredMimeType}" is not allowed`, declaredMimeType, declaredMimeType);
|
|
433
|
+
}
|
|
434
|
+
// Binary files without recognized signatures - be cautious
|
|
435
|
+
this.logger.warn(`Unable to detect file type for declared type: ${declaredMimeType}`);
|
|
436
|
+
return this.failureResult('Unable to verify file type. File may be corrupted or unsupported.', undefined, declaredMimeType);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Sanitize filename to prevent path traversal and special character issues.
|
|
440
|
+
* @param filename - Original filename
|
|
441
|
+
* @returns Sanitized filename
|
|
442
|
+
*/ static sanitizeFilename(filename) {
|
|
443
|
+
return filename// Remove path components (prevent traversal)
|
|
444
|
+
.replace(/^.*[\\\/]/, '')// Remove null bytes
|
|
445
|
+
.replace(/\0/g, '')// Replace multiple dots with single
|
|
446
|
+
.replace(/\.{2,}/g, '.')// Remove special characters except allowed ones
|
|
447
|
+
.replace(/[^a-zA-Z0-9._-]/g, '_')// Limit length
|
|
448
|
+
.substring(0, 255);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
_define_property(FileValidator, "logger", new Logger(FileValidator.name));
|
|
@@ -99,17 +99,12 @@ const sharp = sharpModule.default || sharpModule;
|
|
|
99
99
|
break;
|
|
100
100
|
}
|
|
101
101
|
try {
|
|
102
|
-
|
|
103
|
-
const { data, info } = await image.toBuffer({
|
|
104
|
-
resolveWithObject: true
|
|
105
|
-
});
|
|
102
|
+
const compressedBuffer = await image.toBuffer();
|
|
106
103
|
return {
|
|
107
|
-
buffer:
|
|
104
|
+
buffer: compressedBuffer,
|
|
108
105
|
format: `image/${targetFormat}`
|
|
109
106
|
};
|
|
110
|
-
} catch
|
|
111
|
-
// Fallback to original if processing fails
|
|
112
|
-
console.warn(`Image processing failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
107
|
+
} catch {
|
|
113
108
|
return {
|
|
114
109
|
buffer,
|
|
115
110
|
format: mimetype
|
|
@@ -3,12 +3,15 @@ export interface IFileManager extends IIdentity {
|
|
|
3
3
|
name?: string;
|
|
4
4
|
key?: string;
|
|
5
5
|
url?: string;
|
|
6
|
-
folder?:
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
folder?: {
|
|
7
|
+
id: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
} | null;
|
|
10
|
+
size?: string;
|
|
11
|
+
expiresAt?: number | null;
|
|
9
12
|
contentType?: string;
|
|
10
13
|
location?: string;
|
|
11
|
-
storageConfigId?: string;
|
|
14
|
+
storageConfigId?: string | null;
|
|
12
15
|
providerName?: string;
|
|
13
16
|
isPrivate?: boolean;
|
|
14
17
|
companyId?: string | null;
|
package/interfaces/index.d.ts
CHANGED
|
@@ -8,23 +8,3 @@ export interface IStorageConfig extends IIdentity {
|
|
|
8
8
|
isDefault: boolean;
|
|
9
9
|
companyId?: string | null;
|
|
10
10
|
}
|
|
11
|
-
export interface IAwsS3Config {
|
|
12
|
-
region: string;
|
|
13
|
-
bucket: string;
|
|
14
|
-
accessKeyId: string;
|
|
15
|
-
secretAccessKey: string;
|
|
16
|
-
endpoint?: string;
|
|
17
|
-
}
|
|
18
|
-
export interface IAzureBlobConfig {
|
|
19
|
-
accountName: string;
|
|
20
|
-
accountKey: string;
|
|
21
|
-
containerName: string;
|
|
22
|
-
}
|
|
23
|
-
export interface ISftpConfig {
|
|
24
|
-
host: string;
|
|
25
|
-
port: number;
|
|
26
|
-
username: string;
|
|
27
|
-
password?: string;
|
|
28
|
-
privateKey?: string;
|
|
29
|
-
basePath: string;
|
|
30
|
-
}
|
|
@@ -7,17 +7,12 @@ export interface IStorageModuleConfig extends IDataSourceServiceOptions {
|
|
|
7
7
|
localStoragePath?: string;
|
|
8
8
|
appUrl?: string;
|
|
9
9
|
}
|
|
10
|
-
export interface IStorageModuleConfigFull {
|
|
11
|
-
bootstrapAppConfig?: IBootstrapAppConfig;
|
|
12
|
-
config?: IStorageModuleConfig;
|
|
13
|
-
}
|
|
14
10
|
export interface StorageModuleOptions extends IDynamicModuleConfig {
|
|
15
11
|
bootstrapAppConfig?: IBootstrapAppConfig;
|
|
16
12
|
config?: IStorageModuleConfig;
|
|
17
13
|
}
|
|
18
14
|
export interface StorageOptionsFactory extends IModuleOptionsFactory<IStorageModuleConfig> {
|
|
19
15
|
createStorageOptions(): Promise<IStorageModuleConfig> | IStorageModuleConfig;
|
|
20
|
-
createOptions(): Promise<IStorageModuleConfig> | IStorageModuleConfig;
|
|
21
16
|
}
|
|
22
17
|
export interface StorageModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'>, IDynamicModuleConfig {
|
|
23
18
|
bootstrapAppConfig: IBootstrapAppConfig;
|
|
@@ -3,7 +3,15 @@ import { Request, Response, NextFunction } from 'express';
|
|
|
3
3
|
import { UploadService } from '../services/upload.service';
|
|
4
4
|
export declare class FileServeMiddleware implements NestMiddleware {
|
|
5
5
|
private uploadService;
|
|
6
|
-
private logger;
|
|
6
|
+
private readonly logger;
|
|
7
|
+
private readonly uploadDir;
|
|
7
8
|
constructor(uploadService: UploadService);
|
|
8
9
|
use(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
10
|
+
private extractFilePath;
|
|
11
|
+
private resolveFilePath;
|
|
12
|
+
private tryFallbackPaths;
|
|
13
|
+
private getMimeType;
|
|
14
|
+
private setResponseHeaders;
|
|
15
|
+
private streamFile;
|
|
16
|
+
private sendErrorResponse;
|
|
9
17
|
}
|
|
@@ -4,7 +4,6 @@ export declare class StorageModule implements NestModule {
|
|
|
4
4
|
configure(consumer: MiddlewareConsumer): void;
|
|
5
5
|
static forRoot(options: StorageModuleOptions): DynamicModule;
|
|
6
6
|
static forRootAsync(options: StorageModuleAsyncOptions): DynamicModule;
|
|
7
|
-
private static
|
|
8
|
-
private static getProviders;
|
|
7
|
+
private static buildProviders;
|
|
9
8
|
private static createAsyncProviders;
|
|
10
9
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-storage",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Modular storage package with optional AWS S3, Azure Blob, and SFTP providers",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "fesm/index.js",
|
|
@@ -94,15 +94,15 @@
|
|
|
94
94
|
"class-transformer": "^0.5.0",
|
|
95
95
|
"class-validator": "^0.14.0",
|
|
96
96
|
"typeorm": "^0.3.0",
|
|
97
|
-
"multer": "^
|
|
98
|
-
"sharp": "^0.
|
|
97
|
+
"multer": "^2.0.0",
|
|
98
|
+
"sharp": "^0.34.0",
|
|
99
99
|
"mime-types": "^2.1.0",
|
|
100
100
|
"uuid": "^9.0.0 || ^11.0.0",
|
|
101
101
|
"@aws-sdk/client-s3": "^3.400.0",
|
|
102
102
|
"@aws-sdk/lib-storage": "^3.400.0",
|
|
103
103
|
"@aws-sdk/s3-request-presigner": "^3.400.0",
|
|
104
104
|
"@azure/storage-blob": "^12.15.0",
|
|
105
|
-
"ssh2-sftp-client": "^
|
|
105
|
+
"ssh2-sftp-client": "^12.0.0"
|
|
106
106
|
},
|
|
107
107
|
"peerDependenciesMeta": {
|
|
108
108
|
"sharp": {
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
}
|
|
129
129
|
},
|
|
130
130
|
"dependencies": {
|
|
131
|
-
"@flusys/nestjs-core": "
|
|
132
|
-
"@flusys/nestjs-shared": "
|
|
131
|
+
"@flusys/nestjs-core": "2.0.0",
|
|
132
|
+
"@flusys/nestjs-shared": "2.0.0"
|
|
133
133
|
}
|
|
134
134
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { UploadOptionsDto } from '../dtos';
|
|
2
2
|
import { IStorageProvider, IUploadedFileInfo } from '../interfaces';
|
|
3
|
+
interface AzureConfig {
|
|
4
|
+
accountName: string;
|
|
5
|
+
accountKey: string;
|
|
6
|
+
containerName: string;
|
|
7
|
+
connectionString?: string;
|
|
8
|
+
}
|
|
3
9
|
export declare class AzureProvider implements IStorageProvider {
|
|
4
10
|
private logger;
|
|
5
11
|
private blobServiceClient;
|
|
6
12
|
private containerClient;
|
|
7
13
|
private containerName;
|
|
8
14
|
private accountName;
|
|
9
|
-
initialize(config:
|
|
10
|
-
accountName: string;
|
|
11
|
-
accountKey: string;
|
|
12
|
-
containerName: string;
|
|
13
|
-
connectionString?: string;
|
|
14
|
-
}): Promise<void>;
|
|
15
|
+
initialize(config: AzureConfig): Promise<void>;
|
|
15
16
|
uploadFile(file: Express.Multer.File, options: UploadOptionsDto): Promise<IUploadedFileInfo>;
|
|
16
17
|
uploadMultipleFiles(files: Express.Multer.File[], options: UploadOptionsDto): Promise<IUploadedFileInfo[]>;
|
|
17
18
|
deleteFile(key: string): Promise<void>;
|
|
@@ -19,3 +20,4 @@ export declare class AzureProvider implements IStorageProvider {
|
|
|
19
20
|
generatePresignedUrl(key: string, expiresInSeconds?: number): Promise<string>;
|
|
20
21
|
healthCheck(): Promise<boolean>;
|
|
21
22
|
}
|
|
23
|
+
export {};
|
|
@@ -5,6 +5,8 @@ export declare class LocalProvider implements IStorageProvider {
|
|
|
5
5
|
private basePath;
|
|
6
6
|
private baseUrl;
|
|
7
7
|
private relativeBasePath;
|
|
8
|
+
private validatePathWithinBase;
|
|
9
|
+
private validateKeyFormat;
|
|
8
10
|
initialize(config: {
|
|
9
11
|
basePath?: string;
|
|
10
12
|
baseUrl?: string;
|
|
@@ -15,11 +17,4 @@ export declare class LocalProvider implements IStorageProvider {
|
|
|
15
17
|
deleteMultipleFiles(keys: string[]): Promise<void>;
|
|
16
18
|
generatePresignedUrl(key: string, _expiresInSeconds?: number): Promise<string>;
|
|
17
19
|
healthCheck(): Promise<boolean>;
|
|
18
|
-
getAbsolutePath(key: string): string;
|
|
19
|
-
fileExists(key: string): Promise<boolean>;
|
|
20
|
-
getFileStats(key: string): Promise<{
|
|
21
|
-
size: number;
|
|
22
|
-
createdAt: Date;
|
|
23
|
-
modifiedAt: Date;
|
|
24
|
-
}>;
|
|
25
20
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { UploadOptionsDto } from '../dtos';
|
|
2
2
|
import { IStorageProvider, IUploadedFileInfo } from '../interfaces';
|
|
3
|
+
interface S3Config {
|
|
4
|
+
region: string;
|
|
5
|
+
bucket: string;
|
|
6
|
+
accessKeyId?: string;
|
|
7
|
+
secretAccessKey?: string;
|
|
8
|
+
endpoint?: string;
|
|
9
|
+
}
|
|
3
10
|
export declare class S3Provider implements IStorageProvider {
|
|
4
11
|
private logger;
|
|
5
12
|
private s3;
|
|
6
13
|
private bucketName;
|
|
7
14
|
private region;
|
|
8
|
-
initialize(config:
|
|
9
|
-
region: string;
|
|
10
|
-
bucket: string;
|
|
11
|
-
accessKeyId?: string;
|
|
12
|
-
secretAccessKey?: string;
|
|
13
|
-
endpoint?: string;
|
|
14
|
-
}): Promise<void>;
|
|
15
|
+
initialize(config: S3Config): Promise<void>;
|
|
15
16
|
uploadFile(file: Express.Multer.File, options: UploadOptionsDto): Promise<IUploadedFileInfo>;
|
|
16
17
|
uploadMultipleFiles(files: Express.Multer.File[], options: UploadOptionsDto): Promise<IUploadedFileInfo[]>;
|
|
17
18
|
deleteFile(key: string): Promise<void>;
|
|
@@ -19,3 +20,4 @@ export declare class S3Provider implements IStorageProvider {
|
|
|
19
20
|
generatePresignedUrl(key: string, expiresInSeconds?: number): Promise<string>;
|
|
20
21
|
healthCheck(): Promise<boolean>;
|
|
21
22
|
}
|
|
23
|
+
export {};
|