zyket 1.0.31 → 1.0.33
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/index.js +3 -1
- package/package.json +2 -1
- package/src/extensions/interactive-storage/index.js +163 -0
- package/src/extensions/interactive-storage/middlewares/MulterMiddleware.js +31 -0
- package/src/extensions/interactive-storage/routes/browse.js +31 -0
- package/src/extensions/interactive-storage/routes/create-folder.js +37 -0
- package/src/extensions/interactive-storage/routes/delete-folder.js +57 -0
- package/src/extensions/interactive-storage/routes/delete.js +41 -0
- package/src/extensions/interactive-storage/routes/download.js +47 -0
- package/src/extensions/interactive-storage/routes/info.js +37 -0
- package/src/extensions/interactive-storage/routes/upload.js +46 -0
- package/src/services/database/index.js +18 -0
- package/src/services/express/Express.js +46 -0
package/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const Event = require("./src/services/events/Event");
|
|
|
9
9
|
const Worker = require("./src/services/bullmq/Worker");
|
|
10
10
|
const BullBoardExtension = require("./src/extensions/bullboard");
|
|
11
11
|
const Extension = require("./src/extensions/Extension");
|
|
12
|
+
const InteractiveStorageExtension = require("./src/extensions/interactive-storage");
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
module.exports = {
|
|
@@ -19,5 +20,6 @@ module.exports = {
|
|
|
19
20
|
Schedule, Event,
|
|
20
21
|
Worker,
|
|
21
22
|
EnvManager,
|
|
22
|
-
BullBoardExtension,
|
|
23
|
+
BullBoardExtension, InteractiveStorageExtension,
|
|
24
|
+
Extension
|
|
23
25
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zyket",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.33",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"fast-glob": "^3.3.3",
|
|
23
23
|
"mariadb": "^3.4.5",
|
|
24
24
|
"minio": "^8.0.6",
|
|
25
|
+
"multer": "^2.0.2",
|
|
25
26
|
"node-cron": "^4.2.1",
|
|
26
27
|
"node-dependency-injection": "^3.2.4",
|
|
27
28
|
"prompts": "^2.4.2",
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
const Extension = require('../Extension');
|
|
2
|
+
|
|
3
|
+
// Import route classes
|
|
4
|
+
const UploadRoute = require('./routes/upload');
|
|
5
|
+
const BrowseRoute = require('./routes/browse');
|
|
6
|
+
const DownloadRoute = require('./routes/download');
|
|
7
|
+
const InfoRoute = require('./routes/info');
|
|
8
|
+
const DeleteRoute = require('./routes/delete');
|
|
9
|
+
const CreateFolderRoute = require('./routes/create-folder');
|
|
10
|
+
const DeleteFolderRoute = require('./routes/delete-folder');
|
|
11
|
+
const MulterMiddleware = require('./middlewares/MulterMiddleware');
|
|
12
|
+
|
|
13
|
+
module.exports = class InteractiveStorageExtension extends Extension {
|
|
14
|
+
static bucketName;
|
|
15
|
+
path;
|
|
16
|
+
maxFileSize;
|
|
17
|
+
middlewares;
|
|
18
|
+
|
|
19
|
+
constructor({ path = '/storage', bucketName = 'dropbox', maxFileSize = 100 * 1024 * 1024, middlewares = [] } = {}) {
|
|
20
|
+
super("InteractiveStorageExtension");
|
|
21
|
+
this.path = path || '/storage';
|
|
22
|
+
InteractiveStorageExtension.bucketName = bucketName;
|
|
23
|
+
this.maxFileSize = maxFileSize;
|
|
24
|
+
this.middlewares = middlewares || [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
load(container) {
|
|
28
|
+
if (!container.get('s3')) return container.get('logger').warn('InteractiveStorageExtension: s3 service not found, skipping InteractiveStorage setup');
|
|
29
|
+
if (!container.get('express')) return container.get('logger').warn('InteractiveStorageExtension: express service not found, skipping InteractiveStorage setup');
|
|
30
|
+
|
|
31
|
+
const express = container.get('express');
|
|
32
|
+
const s3 = container.get('s3');
|
|
33
|
+
const logger = container.get('logger');
|
|
34
|
+
|
|
35
|
+
// Ensure bucket exists
|
|
36
|
+
this.#ensureBucket(s3, logger);
|
|
37
|
+
|
|
38
|
+
// Bind helper methods
|
|
39
|
+
const normalizePath = this.#normalizePath.bind(this);
|
|
40
|
+
const listFiles = this.#listFiles.bind(this);
|
|
41
|
+
const listFilesAndFolders = this.#listFilesAndFolders.bind(this);
|
|
42
|
+
const getFileStat = this.#getFileStat.bind(this);
|
|
43
|
+
|
|
44
|
+
// Create route instances
|
|
45
|
+
const routes = [
|
|
46
|
+
new UploadRoute(`${this.path}/upload`, s3, InteractiveStorageExtension.bucketName, normalizePath),
|
|
47
|
+
new BrowseRoute(`${this.path}/browse`, s3, InteractiveStorageExtension.bucketName, normalizePath, listFilesAndFolders),
|
|
48
|
+
new DownloadRoute(`${this.path}/download/:fileName`, s3, InteractiveStorageExtension.bucketName, getFileStat),
|
|
49
|
+
new InfoRoute(`${this.path}/info/:fileName`, s3, InteractiveStorageExtension.bucketName, getFileStat),
|
|
50
|
+
new DeleteRoute(`${this.path}/delete`, s3, InteractiveStorageExtension.bucketName),
|
|
51
|
+
new CreateFolderRoute(`${this.path}/create-folder`, s3, InteractiveStorageExtension.bucketName, normalizePath),
|
|
52
|
+
new DeleteFolderRoute(`${this.path}/delete-folder`, s3, InteractiveStorageExtension.bucketName, normalizePath, listFiles)
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// Add multer middleware to upload route
|
|
56
|
+
routes[0].middlewares = {
|
|
57
|
+
post: [new MulterMiddleware(this.maxFileSize)]
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Register routes using the express service pattern
|
|
61
|
+
express.registerRoutes(routes);
|
|
62
|
+
|
|
63
|
+
logger.info(`InteractiveStorage extension loaded at ${this.path}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async #ensureBucket(s3, logger) {
|
|
67
|
+
try {
|
|
68
|
+
const buckets = await s3.listBuckets();
|
|
69
|
+
const bucketExists = buckets.some(bucket => bucket.name === InteractiveStorageExtension.bucketName);
|
|
70
|
+
|
|
71
|
+
if (!bucketExists) {
|
|
72
|
+
await s3.createBucket(InteractiveStorageExtension.bucketName);
|
|
73
|
+
logger.info(`Created S3 bucket: ${InteractiveStorageExtension.bucketName}`);
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
logger.error(`Error ensuring bucket exists: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async #listFiles(s3, prefix = '') {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const files = [];
|
|
83
|
+
const stream = s3.client.listObjectsV2(InteractiveStorageExtension.bucketName, prefix, true);
|
|
84
|
+
|
|
85
|
+
stream.on('data', (obj) => {
|
|
86
|
+
files.push({
|
|
87
|
+
name: obj.name,
|
|
88
|
+
size: obj.size,
|
|
89
|
+
lastModified: obj.lastModified,
|
|
90
|
+
etag: obj.etag
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
stream.on('end', () => resolve(files));
|
|
95
|
+
stream.on('error', reject);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async #listFilesAndFolders(s3, prefix = '') {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const items = [];
|
|
102
|
+
const folders = new Set();
|
|
103
|
+
|
|
104
|
+
const stream = s3.client.listObjectsV2(InteractiveStorageExtension.bucketName, prefix, false);
|
|
105
|
+
|
|
106
|
+
stream.on('data', (obj) => {
|
|
107
|
+
if (obj.prefix) {
|
|
108
|
+
// This is a folder
|
|
109
|
+
const folderName = obj.prefix.slice(prefix.length).replace(/\/$/, '');
|
|
110
|
+
if (folderName && !folderName.includes('/')) {
|
|
111
|
+
folders.add(folderName);
|
|
112
|
+
}
|
|
113
|
+
} else if (obj.name) {
|
|
114
|
+
// This is a file
|
|
115
|
+
const fileName = obj.name.slice(prefix.length);
|
|
116
|
+
|
|
117
|
+
// Skip folder markers and files in subfolders
|
|
118
|
+
if (fileName === '.folder' || fileName.includes('/')) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
items.push({
|
|
123
|
+
type: 'file',
|
|
124
|
+
name: fileName,
|
|
125
|
+
fullPath: obj.name,
|
|
126
|
+
size: obj.size,
|
|
127
|
+
lastModified: obj.lastModified,
|
|
128
|
+
etag: obj.etag
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
stream.on('end', () => {
|
|
134
|
+
// Add folders to items
|
|
135
|
+
folders.forEach(folder => {
|
|
136
|
+
items.unshift({
|
|
137
|
+
type: 'folder',
|
|
138
|
+
name: folder,
|
|
139
|
+
fullPath: prefix + folder
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
resolve(items);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
stream.on('error', reject);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#normalizePath(path) {
|
|
151
|
+
// Remove leading/trailing slashes and normalize
|
|
152
|
+
return path.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async #getFileStat(s3, fileName) {
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
s3.client.statObject(InteractiveStorageExtension.bucketName, fileName, (err, stat) => {
|
|
158
|
+
if (err) return reject(err);
|
|
159
|
+
resolve(stat);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { Middleware } = require('../../../services/express');
|
|
2
|
+
const multer = require('multer');
|
|
3
|
+
|
|
4
|
+
module.exports = class MulterMiddleware extends Middleware {
|
|
5
|
+
upload;
|
|
6
|
+
|
|
7
|
+
constructor(maxFileSize = 100 * 1024 * 1024) {
|
|
8
|
+
super();
|
|
9
|
+
this.upload = multer({
|
|
10
|
+
storage: multer.memoryStorage(),
|
|
11
|
+
limits: {
|
|
12
|
+
fileSize: maxFileSize
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async handle({ container, request, response, next }) {
|
|
18
|
+
const uploadMiddleware = this.upload.array('files');
|
|
19
|
+
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
uploadMiddleware(request, response, (err) => {
|
|
22
|
+
if (err) {
|
|
23
|
+
container.get('logger').error(`Multer error: ${err.message}`);
|
|
24
|
+
return reject(err);
|
|
25
|
+
}
|
|
26
|
+
next();
|
|
27
|
+
resolve();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { Route } = require('../../../services/express');
|
|
2
|
+
|
|
3
|
+
module.exports = class BrowseRoute extends Route {
|
|
4
|
+
s3;
|
|
5
|
+
bucketName;
|
|
6
|
+
normalizePath;
|
|
7
|
+
listFilesAndFolders;
|
|
8
|
+
|
|
9
|
+
constructor(path, s3, bucketName, normalizePath, listFilesAndFolders) {
|
|
10
|
+
super(path);
|
|
11
|
+
this.s3 = s3;
|
|
12
|
+
this.bucketName = bucketName;
|
|
13
|
+
this.normalizePath = normalizePath;
|
|
14
|
+
this.listFilesAndFolders = listFilesAndFolders;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async get({ container, request }) {
|
|
18
|
+
const logger = container.get('logger');
|
|
19
|
+
const folder = request.query.folder || '';
|
|
20
|
+
const prefix = folder ? this.normalizePath(folder) + '/' : '';
|
|
21
|
+
|
|
22
|
+
const items = await this.listFilesAndFolders(this.s3, prefix);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
success: true,
|
|
26
|
+
bucket: this.bucketName,
|
|
27
|
+
currentPath: folder,
|
|
28
|
+
items: items
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { Route } = require('../../../services/express');
|
|
2
|
+
|
|
3
|
+
module.exports = class CreateFolderRoute extends Route {
|
|
4
|
+
s3;
|
|
5
|
+
bucketName;
|
|
6
|
+
normalizePath;
|
|
7
|
+
|
|
8
|
+
constructor(path, s3, bucketName, normalizePath) {
|
|
9
|
+
super(path);
|
|
10
|
+
this.s3 = s3;
|
|
11
|
+
this.bucketName = bucketName;
|
|
12
|
+
this.normalizePath = normalizePath;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async post({ container, request }) {
|
|
16
|
+
const logger = container.get('logger');
|
|
17
|
+
const { folderPath } = request.body;
|
|
18
|
+
|
|
19
|
+
if (!folderPath) {
|
|
20
|
+
return { success: false, message: 'Folder path is required', status: 400 };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const normalizedPath = this.normalizePath(folderPath);
|
|
24
|
+
const folderMarker = `${normalizedPath}/.folder`;
|
|
25
|
+
|
|
26
|
+
// Create an empty marker file to represent the folder
|
|
27
|
+
await this.s3.saveFile(this.bucketName, folderMarker, Buffer.from(''), 'text/plain');
|
|
28
|
+
|
|
29
|
+
logger.info(`Created folder: ${normalizedPath}`);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
success: true,
|
|
33
|
+
message: 'Folder created successfully',
|
|
34
|
+
folderPath: normalizedPath
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const { Route } = require('../../../services/express');
|
|
2
|
+
|
|
3
|
+
module.exports = class DeleteFolderRoute extends Route {
|
|
4
|
+
s3;
|
|
5
|
+
bucketName;
|
|
6
|
+
normalizePath;
|
|
7
|
+
listFiles;
|
|
8
|
+
|
|
9
|
+
constructor(path, s3, bucketName, normalizePath, listFiles) {
|
|
10
|
+
super(path);
|
|
11
|
+
this.s3 = s3;
|
|
12
|
+
this.bucketName = bucketName;
|
|
13
|
+
this.normalizePath = normalizePath;
|
|
14
|
+
this.listFiles = listFiles;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async delete({ container, request }) {
|
|
18
|
+
const logger = container.get('logger');
|
|
19
|
+
const { folderPath } = request.body;
|
|
20
|
+
|
|
21
|
+
if (!folderPath) {
|
|
22
|
+
return { success: false, message: 'Folder path is required', status: 400 };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const normalizedPath = this.normalizePath(folderPath);
|
|
26
|
+
const prefix = `${normalizedPath}/`;
|
|
27
|
+
|
|
28
|
+
// List all files in the folder
|
|
29
|
+
const files = await this.listFiles(this.s3, prefix);
|
|
30
|
+
|
|
31
|
+
if (files.length === 0) {
|
|
32
|
+
return {
|
|
33
|
+
success: true,
|
|
34
|
+
message: 'Folder is empty or does not exist',
|
|
35
|
+
deletedCount: 0
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Delete all files
|
|
40
|
+
const deletePromises = files.map(file =>
|
|
41
|
+
this.s3.removeFile(this.bucketName, file.name).catch(err => ({ error: err.message, fileName: file.name }))
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const results = await Promise.all(deletePromises);
|
|
45
|
+
const errors = results.filter(r => r && r.error);
|
|
46
|
+
const successCount = results.length - errors.length;
|
|
47
|
+
|
|
48
|
+
logger.info(`Deleted folder ${normalizedPath} with ${successCount} files`);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
success: errors.length === 0,
|
|
52
|
+
message: `Deleted folder and ${successCount} files`,
|
|
53
|
+
deletedCount: successCount,
|
|
54
|
+
errors: errors.length > 0 ? errors : undefined
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const { Route } = require('../../../services/express');
|
|
2
|
+
|
|
3
|
+
module.exports = class DeleteRoute extends Route {
|
|
4
|
+
s3;
|
|
5
|
+
bucketName;
|
|
6
|
+
|
|
7
|
+
constructor(path, s3, bucketName) {
|
|
8
|
+
super(path);
|
|
9
|
+
this.s3 = s3;
|
|
10
|
+
this.bucketName = bucketName;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async delete({ container, request }) {
|
|
14
|
+
const logger = container.get('logger');
|
|
15
|
+
const { fileName, fileNames } = request.body;
|
|
16
|
+
|
|
17
|
+
// Support both single file and multiple files
|
|
18
|
+
const filesToDelete = fileNames || (fileName ? [fileName] : []);
|
|
19
|
+
|
|
20
|
+
if (!Array.isArray(filesToDelete) || filesToDelete.length === 0) {
|
|
21
|
+
return { success: false, message: 'fileName or fileNames array is required', status: 400 };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const deletePromises = filesToDelete.map(file =>
|
|
25
|
+
this.s3.removeFile(this.bucketName, file).catch(err => ({ error: err.message, fileName: file }))
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const results = await Promise.all(deletePromises);
|
|
29
|
+
const errors = results.filter(r => r && r.error);
|
|
30
|
+
const successCount = results.length - errors.length;
|
|
31
|
+
|
|
32
|
+
logger.info(`Deleted ${successCount} file(s) from S3 dropbox`);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
success: errors.length === 0,
|
|
36
|
+
message: `Deleted ${successCount} of ${filesToDelete.length} file(s)`,
|
|
37
|
+
deletedCount: successCount,
|
|
38
|
+
errors: errors.length > 0 ? errors : undefined
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const { Route } = require('../../../services/express');
|
|
2
|
+
|
|
3
|
+
module.exports = class DownloadRoute extends Route {
|
|
4
|
+
s3;
|
|
5
|
+
bucketName;
|
|
6
|
+
getFileStat;
|
|
7
|
+
|
|
8
|
+
constructor(path, s3, bucketName, getFileStat) {
|
|
9
|
+
super(path);
|
|
10
|
+
this.s3 = s3;
|
|
11
|
+
this.bucketName = bucketName;
|
|
12
|
+
this.getFileStat = getFileStat;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async get({ container, request, response }) {
|
|
16
|
+
const logger = container.get('logger');
|
|
17
|
+
const { fileName } = request.params;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Get file info first
|
|
21
|
+
const stat = await this.getFileStat(this.s3, fileName);
|
|
22
|
+
|
|
23
|
+
// Set response headers
|
|
24
|
+
response.setHeader('Content-Type', stat.metaData?.['content-type'] || 'application/octet-stream');
|
|
25
|
+
response.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
|
26
|
+
response.setHeader('Content-Length', stat.size);
|
|
27
|
+
|
|
28
|
+
// Stream the file
|
|
29
|
+
await new Promise((resolve, reject) => {
|
|
30
|
+
this.s3.client.getObject(this.bucketName, fileName, (err, stream) => {
|
|
31
|
+
if (err) return reject(err);
|
|
32
|
+
stream.pipe(response);
|
|
33
|
+
stream.on('end', resolve);
|
|
34
|
+
stream.on('error', reject);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
logger.info(`Downloaded file: ${fileName}`);
|
|
39
|
+
|
|
40
|
+
// Return null to indicate response was handled manually
|
|
41
|
+
return null;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logger.error(`Error downloading file: ${error.message}`);
|
|
44
|
+
return { success: false, message: 'File not found', status: 404 };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { Route } = require('../../../services/express');
|
|
2
|
+
|
|
3
|
+
module.exports = class InfoRoute extends Route {
|
|
4
|
+
s3;
|
|
5
|
+
bucketName;
|
|
6
|
+
getFileStat;
|
|
7
|
+
|
|
8
|
+
constructor(path, s3, bucketName, getFileStat) {
|
|
9
|
+
super(path);
|
|
10
|
+
this.s3 = s3;
|
|
11
|
+
this.bucketName = bucketName;
|
|
12
|
+
this.getFileStat = getFileStat;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async get({ container, request }) {
|
|
16
|
+
const logger = container.get('logger');
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const { fileName } = request.params;
|
|
20
|
+
const stat = await this.getFileStat(this.s3, fileName);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
success: true,
|
|
24
|
+
file: {
|
|
25
|
+
name: fileName,
|
|
26
|
+
size: stat.size,
|
|
27
|
+
lastModified: stat.lastModified,
|
|
28
|
+
etag: stat.etag,
|
|
29
|
+
contentType: stat.metaData?.['content-type']
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logger.error(`Error getting file info: ${error.message}`);
|
|
34
|
+
return { success: false, message: 'File not found', status: 404 };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { Route } = require('../../../services/express');
|
|
2
|
+
|
|
3
|
+
module.exports = class UploadRoute extends Route {
|
|
4
|
+
s3;
|
|
5
|
+
bucketName;
|
|
6
|
+
normalizePath;
|
|
7
|
+
|
|
8
|
+
constructor(path, s3, bucketName, normalizePath) {
|
|
9
|
+
super(path);
|
|
10
|
+
this.s3 = s3;
|
|
11
|
+
this.bucketName = bucketName;
|
|
12
|
+
this.normalizePath = normalizePath;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async post({ container, request }) {
|
|
16
|
+
const logger = container.get('logger');
|
|
17
|
+
|
|
18
|
+
if (!request.files || request.files.length === 0) {
|
|
19
|
+
return { success: false, message: 'No files uploaded', status: 400 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const folder = request.body.folder || '';
|
|
23
|
+
const folderPath = folder ? this.normalizePath(folder) + '/' : '';
|
|
24
|
+
|
|
25
|
+
const uploadPromises = request.files.map(async (file) => {
|
|
26
|
+
const fileName = `${folderPath}${Date.now()}-${file.originalname}`;
|
|
27
|
+
await this.s3.saveFile(this.bucketName, fileName, file.buffer, file.mimetype);
|
|
28
|
+
return {
|
|
29
|
+
originalName: file.originalname,
|
|
30
|
+
fileName: fileName,
|
|
31
|
+
folder: folder,
|
|
32
|
+
size: file.size,
|
|
33
|
+
mimeType: file.mimetype
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const uploadedFiles = await Promise.all(uploadPromises);
|
|
38
|
+
logger.info(`Uploaded ${uploadedFiles.length} file(s) to S3 dropbox`);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
success: true,
|
|
42
|
+
message: 'Files uploaded successfully',
|
|
43
|
+
files: uploadedFiles
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -57,6 +57,24 @@ module.exports = class Database extends Service {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
async loadModel(model) {
|
|
61
|
+
if (typeof model !== 'function') {
|
|
62
|
+
this.#container.get('logger').error(`Model ${model} is not a function`);
|
|
63
|
+
throw new Error(`Model ${model} is not a function`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const modelInstance = model({sequelize: this.sequelize, container: this.#container, Sequelize});
|
|
67
|
+
if(this.models[modelInstance.name]) {
|
|
68
|
+
this.#container.get('logger').warn(`Model ${modelInstance.name} is already loaded, cannot load it again`);
|
|
69
|
+
throw new Error(`Model ${modelInstance.name} is already loaded, cannot load it again`);
|
|
70
|
+
}
|
|
71
|
+
this.models[modelInstance.name] = modelInstance;
|
|
72
|
+
|
|
73
|
+
if (modelInstance.associate) {
|
|
74
|
+
modelInstance.associate(this.models);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
#createModelsFolder() {
|
|
61
79
|
const path = 'src/models';
|
|
62
80
|
if (!fs.existsSync(path)) fs.mkdirSync(path);
|
|
@@ -94,11 +94,57 @@ module.exports = class Express extends Service {
|
|
|
94
94
|
}
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
// Attach Express to HTTP server - this allows dynamic route registration
|
|
98
|
+
this.#httpServer.removeAllListeners("request");
|
|
97
99
|
this.#httpServer.on("request", this.#app);
|
|
98
100
|
|
|
99
101
|
this.#container.get('logger').info(`Express is running on http://localhost:${httpServer.address().port}`);
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
async registerRoutes(routes) {
|
|
105
|
+
const methods = ['post', 'get', 'put', 'delete']
|
|
106
|
+
for (const route of routes) {
|
|
107
|
+
for (const methodName of methods) {
|
|
108
|
+
const method = route[methodName];
|
|
109
|
+
if(!method) continue;
|
|
110
|
+
this.#container.get('logger').debug(`Registering route: [${methodName}] ${route.path}`);
|
|
111
|
+
const middlewares = route?.middlewares?.[methodName] || [];
|
|
112
|
+
for (const mw of middlewares) {
|
|
113
|
+
if (!(mw instanceof Middleware)) {
|
|
114
|
+
throw new Error(`Middleware for route ${route.path} is not an instance of Middleware`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
this.#app[methodName](
|
|
118
|
+
route.path,
|
|
119
|
+
...middlewares.map(mw => async (req, res, next) => {
|
|
120
|
+
try {
|
|
121
|
+
await mw.handle({ container: this.#container, request: req, response: res, next })
|
|
122
|
+
} catch (error) {
|
|
123
|
+
this.#container.get('logger').error(`Error in middleware for route [${methodName}] ${route.path}: ${error.message}`);
|
|
124
|
+
return res.status(500).json({ success: false, message: error.message || 'Internal Server Error' });
|
|
125
|
+
}
|
|
126
|
+
}),
|
|
127
|
+
async (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const routeResponse = await route[methodName]({ container: this.#container, request: req, response: res });
|
|
130
|
+
const status = routeResponse?.status || 200;
|
|
131
|
+
return res.status(status).json({
|
|
132
|
+
...routeResponse,
|
|
133
|
+
success: routeResponse?.success !== false,
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
this.#container.get('logger').error(`Error in route [${methodName}] ${route.path}: ${error.message}`);
|
|
137
|
+
return res.status(500).json({ success: false, message: error.message || 'Internal Server Error' });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.#httpServer.removeAllListeners("request");
|
|
144
|
+
this.#httpServer.on("request", this.#app);
|
|
145
|
+
|
|
146
|
+
}
|
|
147
|
+
|
|
102
148
|
async #loadRoutesFromFolder(routesFolder) {
|
|
103
149
|
this.#createRoutesFolder(routesFolder);
|
|
104
150
|
const routes = (await fg('**/*.js', { cwd: routesFolder })).map((rt) => {
|