node-server-engine 1.2.9 → 1.3.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/.env.example +4 -1
- package/README.md +128 -107
- package/dist/entities/Endpoint/Endpoint.d.ts +2 -0
- package/dist/entities/Endpoint/Endpoint.js +11 -2
- package/dist/entities/Endpoint/Endpoint.js.map +1 -1
- package/dist/entities/Endpoint/Endpoint.types.d.ts +3 -0
- package/dist/entities/Endpoint/Endpoint.types.js.map +1 -1
- package/dist/middleware/fileUploader/fileUploader.js +45 -3
- package/dist/middleware/fileUploader/fileUploader.js.map +1 -1
- package/dist/middleware/multiPartFileUploader/index.d.ts +1 -0
- package/dist/middleware/multiPartFileUploader/index.js +11 -0
- package/dist/middleware/multiPartFileUploader/index.js.map +1 -1
- package/dist/middleware/multiPartFileUploader/multiPartFileUploader.d.ts +4 -3
- package/dist/middleware/multiPartFileUploader/multiPartFileUploader.js +169 -105
- package/dist/middleware/multiPartFileUploader/multiPartFileUploader.js.map +1 -1
- package/dist/middleware/multiPartFileUploader/multiPartFileUploader.types.d.ts +20 -0
- package/dist/middleware/multiPartFileUploader/multiPartFileUploader.types.js +2 -0
- package/dist/middleware/multiPartFileUploader/multiPartFileUploader.types.js.map +1 -0
- package/dist/middleware/multiPartFileUploader/validate/index.d.ts +1 -0
- package/dist/middleware/multiPartFileUploader/validate/index.js +17 -0
- package/dist/middleware/multiPartFileUploader/validate/index.js.map +1 -0
- package/dist/middleware/multiPartFileUploader/validate/validate.d.ts +4 -0
- package/dist/middleware/multiPartFileUploader/validate/validate.js +28 -0
- package/dist/middleware/multiPartFileUploader/validate/validate.js.map +1 -0
- package/package.json +3 -1
- package/src/entities/Endpoint/Endpoint.ts +22 -3
- package/src/entities/Endpoint/Endpoint.types.ts +3 -0
- package/src/middleware/fileUploader/fileUploader.ts +51 -2
- package/src/middleware/multiPartFileUploader/index.ts +1 -0
- package/src/middleware/multiPartFileUploader/multiPartFileUploader.ts +193 -128
- package/src/middleware/multiPartFileUploader/multiPartFileUploader.types.ts +23 -0
- package/src/middleware/multiPartFileUploader/validate/index.ts +1 -0
- package/src/middleware/multiPartFileUploader/validate/validate.ts +27 -0
|
@@ -1,184 +1,249 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
2
|
import multer from 'multer';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import { Schema } from 'express-validator';
|
|
5
6
|
import { reportDebug, reportError } from 'utils';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import { WebError } from 'entities/WebError';
|
|
8
|
+
import { validate } from './validate';
|
|
9
|
+
import { MultiPartFileUploaderConfig } from './multiPartFileUploader.types';
|
|
9
10
|
|
|
10
11
|
const namespace = 'engine:middleware:multiPartFileUploader';
|
|
11
12
|
|
|
13
|
+
const TEMP_DIR: string = path.join(
|
|
14
|
+
process.env.FILE_STORAGE_FOLDER || './uploads',
|
|
15
|
+
'temp_chunks'
|
|
16
|
+
);
|
|
17
|
+
const FILE_DIR: string = path.join(
|
|
18
|
+
process.env.FILE_STORAGE_FOLDER || './uploads',
|
|
19
|
+
'completed_files'
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
fs.ensureDirSync(FILE_DIR);
|
|
12
23
|
fs.ensureDirSync(TEMP_DIR);
|
|
13
24
|
|
|
14
25
|
const storage = multer.memoryStorage();
|
|
15
26
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
message: 'Missing required fields',
|
|
42
|
-
details: {
|
|
43
|
-
filename,
|
|
44
|
-
totalChunks,
|
|
45
|
-
chunkIndex,
|
|
46
|
-
uniqueID,
|
|
47
|
-
receivedChunk: !!req.file
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
reportDebug({
|
|
51
|
-
namespace,
|
|
52
|
-
message: `Missing required fields`,
|
|
53
|
-
data: {
|
|
54
|
-
status: 'failed',
|
|
55
|
-
details: {
|
|
56
|
-
filename,
|
|
57
|
-
totalChunks,
|
|
58
|
-
chunkIndex,
|
|
59
|
-
uniqueID
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
return next();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const sessionFolder = path.join(TEMP_DIR, uniqueID);
|
|
67
|
-
fs.ensureDirSync(sessionFolder);
|
|
68
|
-
|
|
69
|
-
const chunkPath = path.join(
|
|
70
|
-
sessionFolder,
|
|
71
|
-
`${filename}.part${chunkIndex}`
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
await fs.writeFile(chunkPath, req.file.buffer);
|
|
76
|
-
} catch (writeErr) {
|
|
77
|
-
reportError(
|
|
78
|
-
`Failed to write chunk ${chunkIndex}: ${(writeErr as Error).message}`
|
|
79
|
-
);
|
|
80
|
-
return next(writeErr);
|
|
81
|
-
}
|
|
27
|
+
export function multiPartFileUploader(
|
|
28
|
+
filesOption: MultiPartFileUploaderConfig,
|
|
29
|
+
validator: Schema
|
|
30
|
+
) {
|
|
31
|
+
return async (
|
|
32
|
+
req: Request,
|
|
33
|
+
res: Response,
|
|
34
|
+
next: NextFunction
|
|
35
|
+
): Promise<void> => {
|
|
36
|
+
try {
|
|
37
|
+
reportDebug({ namespace, message: 'Starting file upload middleware' });
|
|
38
|
+
|
|
39
|
+
const uploadSingle = multer({ storage }).single('file');
|
|
40
|
+
uploadSingle(req, res, async (err) => {
|
|
41
|
+
if (err) {
|
|
42
|
+
reportError('Error uploading chunk');
|
|
43
|
+
return next(
|
|
44
|
+
new WebError({
|
|
45
|
+
statusCode: 500,
|
|
46
|
+
errorCode: 'upload-failed',
|
|
47
|
+
message: 'Error uploading file chunk',
|
|
48
|
+
data: { error: err.message }
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
}
|
|
82
52
|
|
|
83
|
-
|
|
53
|
+
try {
|
|
54
|
+
reportDebug({ namespace, message: 'Validating request' });
|
|
55
|
+
await validate(req, validator);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
reportDebug({
|
|
58
|
+
namespace,
|
|
59
|
+
message: 'Validation failed',
|
|
60
|
+
data: { error }
|
|
61
|
+
});
|
|
62
|
+
return next(
|
|
63
|
+
new WebError({
|
|
64
|
+
statusCode: 400,
|
|
65
|
+
errorCode: 'validation-failed',
|
|
66
|
+
message: 'Validation failed for file upload',
|
|
67
|
+
data: { error: (error as Error).message }
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
}
|
|
84
71
|
|
|
85
|
-
|
|
72
|
+
const { filename, totalChunks, chunkIndex, uniqueID } = req.body;
|
|
86
73
|
reportDebug({
|
|
87
74
|
namespace,
|
|
88
|
-
message:
|
|
75
|
+
message: 'Received chunk data',
|
|
76
|
+
data: { filename, totalChunks, chunkIndex, uniqueID }
|
|
89
77
|
});
|
|
90
78
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
79
|
+
if (
|
|
80
|
+
!filename ||
|
|
81
|
+
!totalChunks ||
|
|
82
|
+
chunkIndex === undefined ||
|
|
83
|
+
!uniqueID ||
|
|
84
|
+
!req.file
|
|
85
|
+
) {
|
|
86
|
+
return next(
|
|
87
|
+
new WebError({
|
|
88
|
+
statusCode: 400,
|
|
89
|
+
errorCode: 'missing-field',
|
|
90
|
+
message: 'Missing required fields',
|
|
91
|
+
data: {
|
|
92
|
+
filename,
|
|
93
|
+
totalChunks,
|
|
94
|
+
chunkIndex,
|
|
95
|
+
uniqueID,
|
|
96
|
+
receivedChunk: !!req.file
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
filesOption?.maxSize &&
|
|
104
|
+
req.file.size > parseInt(filesOption?.maxSize, 10)
|
|
105
|
+
) {
|
|
106
|
+
return next(
|
|
107
|
+
new WebError({
|
|
108
|
+
statusCode: 400,
|
|
109
|
+
errorCode: 'chunk-too-large',
|
|
110
|
+
message: 'Uploaded chunk exceeds the maximum allowed size',
|
|
111
|
+
data: {
|
|
112
|
+
maxSize: filesOption.maxSize,
|
|
113
|
+
receivedSize: req.file.size
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
);
|
|
96
117
|
}
|
|
97
118
|
|
|
98
|
-
const
|
|
119
|
+
const sessionFolder = path.join(TEMP_DIR, uniqueID);
|
|
120
|
+
fs.ensureDirSync(sessionFolder);
|
|
121
|
+
const chunkPath = path.join(
|
|
122
|
+
sessionFolder,
|
|
123
|
+
`${filename}.part${chunkIndex}`
|
|
124
|
+
);
|
|
99
125
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
126
|
+
try {
|
|
127
|
+
reportDebug({
|
|
128
|
+
namespace,
|
|
129
|
+
message: 'Writing chunk to disk',
|
|
130
|
+
data: { chunkPath }
|
|
131
|
+
});
|
|
132
|
+
await fs.writeFile(chunkPath, req.file.buffer);
|
|
133
|
+
} catch (writeErr) {
|
|
134
|
+
return next(
|
|
135
|
+
new WebError({
|
|
136
|
+
statusCode: 500,
|
|
137
|
+
errorCode: 'chunk-write-failed',
|
|
138
|
+
message: 'Failed to write chunk',
|
|
139
|
+
data: { chunkIndex, error: (writeErr as Error).message }
|
|
140
|
+
})
|
|
141
|
+
);
|
|
103
142
|
}
|
|
104
143
|
|
|
105
|
-
|
|
106
|
-
status: 'completed',
|
|
107
|
-
message: 'File uploaded and merged successfully',
|
|
108
|
-
details: { filename, uniqueID }
|
|
109
|
-
};
|
|
144
|
+
const uploadedChunks = await fs.readdir(sessionFolder);
|
|
110
145
|
reportDebug({
|
|
111
146
|
namespace,
|
|
112
|
-
message:
|
|
113
|
-
data: {
|
|
114
|
-
status: 'completed',
|
|
115
|
-
details: { filename, uniqueID }
|
|
116
|
-
}
|
|
147
|
+
message: 'Uploaded chunk count',
|
|
148
|
+
data: { uploadedChunks: uploadedChunks.length, totalChunks }
|
|
117
149
|
});
|
|
118
150
|
|
|
119
|
-
|
|
120
|
-
|
|
151
|
+
if (uploadedChunks.length === parseInt(totalChunks, 10)) {
|
|
152
|
+
try {
|
|
153
|
+
const finalFilePath = path.join(
|
|
154
|
+
FILE_DIR,
|
|
155
|
+
`${uniqueID}_${filename}`
|
|
156
|
+
);
|
|
157
|
+
await mergeChunks(
|
|
158
|
+
filename,
|
|
159
|
+
totalChunks,
|
|
160
|
+
sessionFolder,
|
|
161
|
+
finalFilePath
|
|
162
|
+
);
|
|
163
|
+
req.multipartFile = {
|
|
164
|
+
isPending: false,
|
|
165
|
+
originalname: filename,
|
|
166
|
+
uniqueID,
|
|
167
|
+
filePath: finalFilePath
|
|
168
|
+
};
|
|
169
|
+
} catch (mergeErr) {
|
|
170
|
+
return next(
|
|
171
|
+
new WebError({
|
|
172
|
+
statusCode: 500,
|
|
173
|
+
errorCode: 'file-merge-failed',
|
|
174
|
+
message: 'Error merging file chunks',
|
|
175
|
+
data: { error: (mergeErr as Error).message }
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return next();
|
|
180
|
+
}
|
|
121
181
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
data: {
|
|
126
|
-
filename,
|
|
182
|
+
req.multipartFile = {
|
|
183
|
+
isPending: true,
|
|
184
|
+
originalname: filename,
|
|
127
185
|
uniqueID,
|
|
128
186
|
chunkIndex,
|
|
129
187
|
totalChunks
|
|
130
|
-
}
|
|
188
|
+
};
|
|
189
|
+
next();
|
|
131
190
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
`Failed to upload or process chunk: ${(error as Error).message}`
|
|
144
|
-
);
|
|
145
|
-
next(error);
|
|
146
|
-
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
next(
|
|
193
|
+
new WebError({
|
|
194
|
+
statusCode: 500,
|
|
195
|
+
errorCode: 'unexpected-error',
|
|
196
|
+
message: 'Unexpected error in file upload',
|
|
197
|
+
data: { error: (error as Error).message }
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
147
202
|
}
|
|
148
203
|
|
|
149
204
|
async function mergeChunks(
|
|
150
205
|
filename: string,
|
|
151
206
|
totalChunks: string | number,
|
|
152
|
-
sessionFolder: string
|
|
207
|
+
sessionFolder: string,
|
|
208
|
+
finalFilePath: string
|
|
153
209
|
): Promise<void> {
|
|
154
|
-
const finalFilePath = path.join(sessionFolder, filename);
|
|
155
210
|
const writeStream = fs.createWriteStream(finalFilePath);
|
|
211
|
+
reportDebug({
|
|
212
|
+
namespace,
|
|
213
|
+
message: 'Merging file chunks',
|
|
214
|
+
data: { finalFilePath }
|
|
215
|
+
});
|
|
156
216
|
|
|
157
217
|
for (let i = 0; i < parseInt(totalChunks as string, 10); i++) {
|
|
158
218
|
const partPath = path.join(sessionFolder, `${filename}.part${i}`);
|
|
159
|
-
|
|
160
219
|
try {
|
|
161
220
|
const chunkData = await fs.readFile(partPath);
|
|
162
221
|
writeStream.write(chunkData);
|
|
163
222
|
} catch (error) {
|
|
164
|
-
|
|
165
|
-
|
|
223
|
+
throw new WebError({
|
|
224
|
+
statusCode: 500,
|
|
225
|
+
errorCode: 'chunk-read-failed',
|
|
226
|
+
message: `Error reading chunk ${i}`,
|
|
227
|
+
data: { error: (error as Error).message }
|
|
228
|
+
});
|
|
166
229
|
}
|
|
167
|
-
|
|
168
230
|
fs.remove(partPath).catch((err: Error) =>
|
|
169
231
|
reportError(`Error removing part ${partPath}: ${err.message}`)
|
|
170
232
|
);
|
|
171
233
|
}
|
|
172
234
|
|
|
173
235
|
writeStream.end();
|
|
174
|
-
|
|
175
236
|
return new Promise<void>((resolve, reject) => {
|
|
176
237
|
writeStream.on('finish', () => resolve(undefined));
|
|
177
|
-
writeStream.on('error', (error) =>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
238
|
+
writeStream.on('error', (error) =>
|
|
239
|
+
reject(
|
|
240
|
+
new WebError({
|
|
241
|
+
statusCode: 500,
|
|
242
|
+
errorCode: 'file-merge-error',
|
|
243
|
+
message: 'Error merging file chunks',
|
|
244
|
+
data: { error: (error as Error).message }
|
|
245
|
+
})
|
|
246
|
+
)
|
|
247
|
+
);
|
|
181
248
|
});
|
|
182
249
|
}
|
|
183
|
-
|
|
184
|
-
export { multiPartFileUploader };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Extend Express request object to include `files`
|
|
2
|
+
declare global {
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
4
|
+
namespace Express {
|
|
5
|
+
interface Request {
|
|
6
|
+
/** Uploaded files mapped by field name */
|
|
7
|
+
multipartFile?: {
|
|
8
|
+
isPending: boolean;
|
|
9
|
+
originalname: string;
|
|
10
|
+
uniqueID: string;
|
|
11
|
+
filePath?: string;
|
|
12
|
+
chunkIndex?: number;
|
|
13
|
+
totalChunks?: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Configuration for Multi part file upload */
|
|
20
|
+
export interface MultiPartFileUploaderConfig {
|
|
21
|
+
required?: boolean;
|
|
22
|
+
maxSize?: string;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './validate';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Request } from 'express';
|
|
2
|
+
import { Schema } from 'express-validator';
|
|
3
|
+
import { validate as validateMiddleware } from 'middleware/validate';
|
|
4
|
+
|
|
5
|
+
/** Validate a schema against a request using our validation middleware */
|
|
6
|
+
export async function validate(
|
|
7
|
+
request: Request,
|
|
8
|
+
schema: Schema
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
// Generate a validation chain using our middleware
|
|
12
|
+
const validationChain = validateMiddleware(schema).flat();
|
|
13
|
+
if (!validationChain.length) resolve();
|
|
14
|
+
// Recursively go through each middleware by imitating the next function from express
|
|
15
|
+
let position = -1;
|
|
16
|
+
const next = (error?: unknown): void => {
|
|
17
|
+
if (error) reject(error);
|
|
18
|
+
position++;
|
|
19
|
+
if (position === validationChain.length) {
|
|
20
|
+
resolve();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
validationChain[position](request, undefined, next);
|
|
24
|
+
};
|
|
25
|
+
next();
|
|
26
|
+
});
|
|
27
|
+
}
|