node-server-engine 1.2.9 → 1.3.1

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.
Files changed (40) hide show
  1. package/.env.example +4 -1
  2. package/README.md +128 -107
  3. package/dist/entities/Endpoint/Endpoint.d.ts +2 -0
  4. package/dist/entities/Endpoint/Endpoint.js +11 -2
  5. package/dist/entities/Endpoint/Endpoint.js.map +1 -1
  6. package/dist/entities/Endpoint/Endpoint.types.d.ts +3 -0
  7. package/dist/entities/Endpoint/Endpoint.types.js.map +1 -1
  8. package/dist/entities/Server/Server.js +1 -1
  9. package/dist/entities/Server/Server.js.map +1 -1
  10. package/dist/middleware/fileUploader/fileUploader.js +45 -3
  11. package/dist/middleware/fileUploader/fileUploader.js.map +1 -1
  12. package/dist/middleware/multiPartFileUploader/index.d.ts +1 -0
  13. package/dist/middleware/multiPartFileUploader/index.js +11 -0
  14. package/dist/middleware/multiPartFileUploader/index.js.map +1 -1
  15. package/dist/middleware/multiPartFileUploader/multiPartFileUploader.d.ts +4 -3
  16. package/dist/middleware/multiPartFileUploader/multiPartFileUploader.js +169 -105
  17. package/dist/middleware/multiPartFileUploader/multiPartFileUploader.js.map +1 -1
  18. package/dist/middleware/multiPartFileUploader/multiPartFileUploader.types.d.ts +20 -0
  19. package/dist/middleware/multiPartFileUploader/multiPartFileUploader.types.js +2 -0
  20. package/dist/middleware/multiPartFileUploader/multiPartFileUploader.types.js.map +1 -0
  21. package/dist/middleware/multiPartFileUploader/validate/index.d.ts +1 -0
  22. package/dist/middleware/multiPartFileUploader/validate/index.js +17 -0
  23. package/dist/middleware/multiPartFileUploader/validate/index.js.map +1 -0
  24. package/dist/middleware/multiPartFileUploader/validate/validate.d.ts +4 -0
  25. package/dist/middleware/multiPartFileUploader/validate/validate.js +28 -0
  26. package/dist/middleware/multiPartFileUploader/validate/validate.js.map +1 -0
  27. package/dist/utils/index.d.ts +1 -0
  28. package/dist/utils/index.js +11 -11
  29. package/dist/utils/index.js.map +1 -1
  30. package/package.json +3 -1
  31. package/src/entities/Endpoint/Endpoint.ts +22 -3
  32. package/src/entities/Endpoint/Endpoint.types.ts +3 -0
  33. package/src/entities/Server/Server.ts +0 -2
  34. package/src/middleware/fileUploader/fileUploader.ts +51 -2
  35. package/src/middleware/multiPartFileUploader/index.ts +1 -0
  36. package/src/middleware/multiPartFileUploader/multiPartFileUploader.ts +193 -128
  37. package/src/middleware/multiPartFileUploader/multiPartFileUploader.types.ts +23 -0
  38. package/src/middleware/multiPartFileUploader/validate/index.ts +1 -0
  39. package/src/middleware/multiPartFileUploader/validate/validate.ts +27 -0
  40. package/src/utils/index.ts +1 -0
@@ -1,184 +1,249 @@
1
- import express from 'express';
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
- const TEMP_DIR: string =
8
- process.env.TEMP_CHUNK_FOLDER || './uploads/temp_chunks';
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
- async function multiPartFileUploader(
17
- req: express.Request,
18
- res: express.Response,
19
- next: express.NextFunction
20
- ): Promise<void> {
21
- try {
22
- const uploadSingle = multer({ storage }).single('file');
23
-
24
- uploadSingle(req, res, async (err) => {
25
- if (err) {
26
- reportError('Error uploading chunk');
27
- return next(err);
28
- }
29
-
30
- const { filename, totalChunks, chunkIndex, uniqueID } = req.body;
31
-
32
- if (
33
- !filename ||
34
- !totalChunks ||
35
- chunkIndex === undefined ||
36
- !uniqueID ||
37
- !req.file
38
- ) {
39
- req.body.uploadDetails = {
40
- status: 'failed',
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
- const uploadedChunks = await fs.readdir(sessionFolder);
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
- if (uploadedChunks.length === parseInt(totalChunks, 10)) {
72
+ const { filename, totalChunks, chunkIndex, uniqueID } = req.body;
86
73
  reportDebug({
87
74
  namespace,
88
- message: `All chunks uploaded. Merging file...`
75
+ message: 'Received chunk data',
76
+ data: { filename, totalChunks, chunkIndex, uniqueID }
89
77
  });
90
78
 
91
- try {
92
- await mergeChunks(filename, totalChunks, sessionFolder);
93
- } catch (mergeErr) {
94
- reportError(`File merging failed: ${(mergeErr as Error).message}`);
95
- return next(mergeErr);
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 finalFilePath = path.join(sessionFolder, filename);
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
- if (!fs.existsSync(finalFilePath)) {
101
- reportError('File merging failed: File does not exist');
102
- return next(new Error('File merging failed'));
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
- req.body.uploadDetails = {
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: `File merged successfully`,
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
- return next();
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
- reportDebug({
123
- namespace,
124
- message: `Chunk uploaded successfully`,
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
- req.body.uploadDetails = {
134
- status: 'pending',
135
- message: 'Chunk uploaded successfully',
136
- details: { filename, uniqueID, chunkIndex, totalChunks }
137
- };
138
-
139
- return next();
140
- });
141
- } catch (error) {
142
- reportError(
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
- reportError(`Error reading chunk ${i}: ${(error as Error).message}`);
165
- throw error;
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
- reportError(`Error merging chunks: ${(error as Error).message}`);
179
- reject(error);
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
+ }
@@ -12,6 +12,7 @@ export * from './envAssert';
12
12
  export * from './createSortedObject';
13
13
  export * from './jwt';
14
14
  export * from './hmac';
15
+ export * from './tlsConfig';
15
16
 
16
17
  // Networking
17
18
  export * from './createHttpServer';