@geek-fun/serverlessinsight 0.3.3 → 0.4.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 +209 -21
- package/README.zh-CN.md +232 -0
- package/dist/package.json +33 -33
- package/dist/src/commands/index.js +3 -1
- package/dist/src/commands/local.js +6 -3
- package/dist/src/common/constants.js +2 -1
- package/dist/src/common/context.js +35 -17
- package/dist/src/common/iacHelper.js +39 -4
- package/dist/src/common/index.d.ts +1 -1
- package/dist/src/common/index.js +1 -1
- package/dist/src/common/requestHelper.js +16 -0
- package/dist/src/parser/eventParser.js +1 -1
- package/dist/src/parser/index.d.ts +2 -1
- package/dist/src/parser/index.js +32 -1
- package/dist/src/stack/localStack/aliyunFc.js +145 -0
- package/dist/src/stack/localStack/bucket.js +226 -0
- package/dist/src/stack/localStack/event.js +133 -26
- package/dist/src/stack/localStack/function.js +120 -0
- package/dist/src/stack/localStack/functionRunner.js +270 -0
- package/dist/src/stack/localStack/index.d.ts +4 -1
- package/dist/src/stack/localStack/index.js +14 -4
- package/dist/src/stack/localStack/localServer.js +111 -0
- package/dist/src/stack/localStack/utils.js +36 -0
- package/dist/src/stack/rosStack/bootstrap.js +1 -3
- package/dist/src/types/localStack/index.d.ts +81 -0
- package/dist/src/types/localStack/index.js +10 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +33 -33
- package/dist/src/common/domainHelper.js +0 -10
|
@@ -10,28 +10,46 @@ const providerEnum_1 = require("./providerEnum");
|
|
|
10
10
|
const node_async_hooks_1 = require("node:async_hooks");
|
|
11
11
|
const imsClient_1 = require("./imsClient");
|
|
12
12
|
const asyncLocalStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
13
|
+
const DEFAULT_IAC_FILES = [
|
|
14
|
+
'serverlessinsight.yml',
|
|
15
|
+
'serverlessInsight.yml',
|
|
16
|
+
'ServerlessInsight.yml',
|
|
17
|
+
'serverless-insight.yml',
|
|
18
|
+
];
|
|
13
19
|
const getIacLocation = (location) => {
|
|
14
20
|
const projectRoot = node_path_1.default.resolve(process.cwd());
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
const searchTargets = location ? [location] : DEFAULT_IAC_FILES;
|
|
22
|
+
const attempted = new Set();
|
|
23
|
+
const toAbsolutePath = (target) => node_path_1.default.isAbsolute(target) ? target : node_path_1.default.resolve(projectRoot, target);
|
|
24
|
+
const tryResolveCandidate = (target) => {
|
|
25
|
+
const resolved = toAbsolutePath(target);
|
|
26
|
+
attempted.add(resolved);
|
|
27
|
+
if (!node_fs_1.default.existsSync(resolved)) {
|
|
28
|
+
return undefined;
|
|
19
29
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const stats = node_fs_1.default.statSync(resolved);
|
|
31
|
+
if (stats.isDirectory()) {
|
|
32
|
+
for (const fileName of DEFAULT_IAC_FILES) {
|
|
33
|
+
const nested = node_path_1.default.join(resolved, fileName);
|
|
34
|
+
attempted.add(nested);
|
|
35
|
+
if (node_fs_1.default.existsSync(nested) && node_fs_1.default.statSync(nested).isFile()) {
|
|
36
|
+
return nested;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return resolved;
|
|
42
|
+
};
|
|
43
|
+
for (const candidate of searchTargets) {
|
|
44
|
+
const match = tryResolveCandidate(candidate);
|
|
45
|
+
if (match) {
|
|
46
|
+
return match;
|
|
32
47
|
}
|
|
33
48
|
}
|
|
34
|
-
|
|
49
|
+
const attemptedList = Array.from(attempted)
|
|
50
|
+
.map((n) => `'${n}'`)
|
|
51
|
+
.join(', ');
|
|
52
|
+
throw new Error(`No IaC file found. Tried: ${attemptedList}`);
|
|
35
53
|
};
|
|
36
54
|
exports.getIacLocation = getIacLocation;
|
|
37
55
|
const setContext = async (config, reaValToken = false) => {
|
|
@@ -36,13 +36,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.formatRosId = exports.calcValue = exports.calcRefs = exports.getFileSource = exports.readCodeSize = exports.resolveCode = void 0;
|
|
39
|
+
exports.splitDomain = exports.formatRosId = exports.getIacDefinition = exports.calcValue = exports.calcRefs = exports.getFileSource = exports.readCodeSize = exports.resolveCode = void 0;
|
|
40
40
|
const node_path_1 = __importDefault(require("node:path"));
|
|
41
41
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
42
42
|
const ros = __importStar(require("@alicloud/ros-cdk-core"));
|
|
43
43
|
const ossDeployment = __importStar(require("@alicloud/ros-cdk-ossdeployment"));
|
|
44
44
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
45
45
|
const lodash_1 = require("lodash");
|
|
46
|
+
const parser_1 = require("../parser");
|
|
47
|
+
const logger_1 = require("./logger");
|
|
46
48
|
const resolveCode = (location) => {
|
|
47
49
|
const filePath = node_path_1.default.resolve(process.cwd(), location);
|
|
48
50
|
const fileContent = node_fs_1.default.readFileSync(filePath);
|
|
@@ -104,7 +106,7 @@ exports.calcRefs = calcRefs;
|
|
|
104
106
|
const getParam = (key, records) => {
|
|
105
107
|
return records?.find((param) => param.key === key)?.value;
|
|
106
108
|
};
|
|
107
|
-
const calcValue = (rawValue, ctx) => {
|
|
109
|
+
const calcValue = (rawValue, ctx, iacVars) => {
|
|
108
110
|
const containsStage = rawValue.match(/\$\{ctx.stage}/);
|
|
109
111
|
const containsVar = rawValue.match(/\$\{vars.\w+}/);
|
|
110
112
|
const containsMap = rawValue.match(/\$\{stages\.(\w+)}/);
|
|
@@ -113,14 +115,40 @@ const calcValue = (rawValue, ctx) => {
|
|
|
113
115
|
value = rawValue.replace(/\$\{ctx.stage}/g, ctx.stage);
|
|
114
116
|
}
|
|
115
117
|
if (containsVar?.length) {
|
|
116
|
-
|
|
118
|
+
// Use provided iacVars or parse from file
|
|
119
|
+
const vars = iacVars ?? (0, parser_1.parseYaml)(ctx.iacLocation).vars;
|
|
120
|
+
const mergedParams = Array.from(new Map([
|
|
121
|
+
...Object.entries(vars ?? {}).map(([key, value]) => [key, String(value)]),
|
|
122
|
+
...(ctx.parameters ?? []).map(({ key, value }) => [key, value]),
|
|
123
|
+
].filter(([, v]) => v !== undefined)).entries()).map(([key, value]) => ({ key, value }));
|
|
124
|
+
value = value.replace(/\$\{vars\.(\w+)}/g, (_, key) => {
|
|
125
|
+
const paramValue = getParam(key, mergedParams);
|
|
126
|
+
if (!paramValue) {
|
|
127
|
+
logger_1.logger.warn(`Variable '${key}' not found in vars or parameters, using empty string`);
|
|
128
|
+
}
|
|
129
|
+
return paramValue || '';
|
|
130
|
+
});
|
|
117
131
|
}
|
|
118
132
|
if (containsMap?.length) {
|
|
119
|
-
value = value.replace(/\$\{stages\.(\w+)}/g, (_, key) =>
|
|
133
|
+
value = value.replace(/\$\{stages\.(\w+)}/g, (_, key) => {
|
|
134
|
+
const stageValue = getParam(key, (0, lodash_1.get)(ctx.stages, `${ctx.stage}`));
|
|
135
|
+
if (!stageValue) {
|
|
136
|
+
logger_1.logger.warn(`Stage variable '${key}' not found in stage '${ctx.stage}', using empty string`);
|
|
137
|
+
}
|
|
138
|
+
return stageValue || '';
|
|
139
|
+
});
|
|
120
140
|
}
|
|
121
141
|
return value;
|
|
122
142
|
};
|
|
123
143
|
exports.calcValue = calcValue;
|
|
144
|
+
const getIacDefinition = (iac, rawValue) => {
|
|
145
|
+
const matchFn = rawValue.match(/^\$\{functions\.(\w+(\.\w+)?)}$/);
|
|
146
|
+
if (matchFn?.length) {
|
|
147
|
+
return iac.functions?.find((fc) => fc.key === matchFn[1]);
|
|
148
|
+
}
|
|
149
|
+
return iac.functions?.find((fc) => fc.key === rawValue);
|
|
150
|
+
};
|
|
151
|
+
exports.getIacDefinition = getIacDefinition;
|
|
124
152
|
const formatRosId = (id) => {
|
|
125
153
|
// Insert underscore before uppercase letters, but only when they follow a lowercase letter
|
|
126
154
|
let result = id.replace(/([a-z])([A-Z])/g, '$1_$2');
|
|
@@ -135,3 +163,10 @@ const formatRosId = (id) => {
|
|
|
135
163
|
return result;
|
|
136
164
|
};
|
|
137
165
|
exports.formatRosId = formatRosId;
|
|
166
|
+
const splitDomain = (domain) => {
|
|
167
|
+
const parts = domain.split('.');
|
|
168
|
+
const rr = parts.length > 2 ? parts[0] : '@';
|
|
169
|
+
const domainName = parts.length > 2 ? parts.slice(1).join('.') : domain;
|
|
170
|
+
return { rr, domainName };
|
|
171
|
+
};
|
|
172
|
+
exports.splitDomain = splitDomain;
|
package/dist/src/common/index.js
CHANGED
|
@@ -24,4 +24,4 @@ __exportStar(require("./constants"), exports);
|
|
|
24
24
|
__exportStar(require("./imsClient"), exports);
|
|
25
25
|
__exportStar(require("./base64"), exports);
|
|
26
26
|
__exportStar(require("./rosAssets"), exports);
|
|
27
|
-
__exportStar(require("./
|
|
27
|
+
__exportStar(require("./requestHelper"), exports);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readRequestBody = void 0;
|
|
4
|
+
const readRequestBody = (req) => {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
let body = '';
|
|
7
|
+
req.on('data', (chunk) => {
|
|
8
|
+
body += chunk.toString();
|
|
9
|
+
});
|
|
10
|
+
req.on('end', () => {
|
|
11
|
+
resolve(body);
|
|
12
|
+
});
|
|
13
|
+
req.on('error', reject);
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
exports.readRequestBody = readRequestBody;
|
package/dist/src/parser/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseYaml = void 0;
|
|
3
|
+
exports.revalYaml = exports.parseYaml = void 0;
|
|
4
4
|
const node_fs_1 = require("node:fs");
|
|
5
5
|
const functionParser_1 = require("./functionParser");
|
|
6
6
|
const eventParser_1 = require("./eventParser");
|
|
@@ -10,6 +10,7 @@ const yaml_1 = require("yaml");
|
|
|
10
10
|
const validator_1 = require("../validator");
|
|
11
11
|
const bucketParser_1 = require("./bucketParser");
|
|
12
12
|
const tableParser_1 = require("./tableParser");
|
|
13
|
+
const common_1 = require("../common");
|
|
13
14
|
const validateExistence = (path) => {
|
|
14
15
|
if (!(0, node_fs_1.existsSync)(path)) {
|
|
15
16
|
throw new Error(`File does not exist at path: ${path}`);
|
|
@@ -38,3 +39,33 @@ const parseYaml = (iacLocation) => {
|
|
|
38
39
|
return transformYaml(iacJson);
|
|
39
40
|
};
|
|
40
41
|
exports.parseYaml = parseYaml;
|
|
42
|
+
const evaluateObject = (obj, ctx, iacVars) => {
|
|
43
|
+
if (typeof obj === 'string') {
|
|
44
|
+
return (0, common_1.calcValue)(obj, ctx, iacVars);
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(obj)) {
|
|
47
|
+
return obj.map((item) => evaluateObject(item, ctx, iacVars));
|
|
48
|
+
}
|
|
49
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
50
|
+
return Object.fromEntries(Object.entries(obj).map(([key, val]) => [key, evaluateObject(val, ctx, iacVars)]));
|
|
51
|
+
}
|
|
52
|
+
return obj;
|
|
53
|
+
};
|
|
54
|
+
const revalYaml = (iacLocation, ctx) => {
|
|
55
|
+
validateExistence(iacLocation);
|
|
56
|
+
const yamlContent = (0, node_fs_1.readFileSync)(iacLocation, 'utf8');
|
|
57
|
+
const iacJson = (0, yaml_1.parse)(yamlContent);
|
|
58
|
+
(0, validator_1.validateYaml)(iacJson);
|
|
59
|
+
const evaluatedIacJson = evaluateObject(iacJson, ctx, iacJson.vars);
|
|
60
|
+
const iac = transformYaml(evaluatedIacJson);
|
|
61
|
+
// Set default values for optional fields in functions
|
|
62
|
+
if (iac.functions) {
|
|
63
|
+
iac.functions = iac.functions.map((fn) => ({
|
|
64
|
+
...fn,
|
|
65
|
+
memory: fn.memory || 128,
|
|
66
|
+
timeout: fn.timeout || 3,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
return iac;
|
|
70
|
+
};
|
|
71
|
+
exports.revalYaml = revalYaml;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateRequestId = exports.logApiGatewayRequest = exports.transformFCResponse = exports.addFCHeaders = exports.createAliyunContext = exports.createAliyunContextSerializable = exports.transformToAliyunEvent = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const common_1 = require("../../common");
|
|
6
|
+
const createFCLogger = (requestId) => {
|
|
7
|
+
const formatLog = (level, message) => {
|
|
8
|
+
const timestamp = new Date().toISOString();
|
|
9
|
+
console.log(`${timestamp} ${requestId} [${level}] ${message}`);
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
debug: (message) => formatLog('DEBUG', message),
|
|
13
|
+
info: (message) => formatLog('INFO', message),
|
|
14
|
+
warn: (message) => formatLog('WARNING', message),
|
|
15
|
+
error: (message) => formatLog('ERROR', message),
|
|
16
|
+
log: (message) => formatLog('INFO', message),
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
const transformToAliyunEvent = async (req, url, query) => {
|
|
20
|
+
const rawBody = await (0, common_1.readRequestBody)(req);
|
|
21
|
+
const pathParameters = {};
|
|
22
|
+
const event = {
|
|
23
|
+
path: url,
|
|
24
|
+
httpMethod: req.method || 'GET',
|
|
25
|
+
headers: req.headers,
|
|
26
|
+
queryParameters: query,
|
|
27
|
+
pathParameters,
|
|
28
|
+
body: rawBody || undefined,
|
|
29
|
+
isBase64Encoded: false,
|
|
30
|
+
};
|
|
31
|
+
const eventBuffer = Buffer.from(JSON.stringify(event));
|
|
32
|
+
return { event: eventBuffer, headers: req.headers };
|
|
33
|
+
};
|
|
34
|
+
exports.transformToAliyunEvent = transformToAliyunEvent;
|
|
35
|
+
const createAliyunContextSerializable = (iac, functionName, handler, memory, timeout, requestId) => {
|
|
36
|
+
return {
|
|
37
|
+
requestId,
|
|
38
|
+
region: iac.provider.region || 'cn-hangzhou',
|
|
39
|
+
accountId: process.env.ALIYUN_ACCOUNT_ID || '000000000000',
|
|
40
|
+
credentials: {
|
|
41
|
+
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || 'mock-access-key-id',
|
|
42
|
+
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || 'mock-access-key-secret',
|
|
43
|
+
securityToken: process.env.ALIYUN_SECURITY_TOKEN || '',
|
|
44
|
+
},
|
|
45
|
+
function: {
|
|
46
|
+
name: functionName,
|
|
47
|
+
handler,
|
|
48
|
+
memory,
|
|
49
|
+
timeout,
|
|
50
|
+
initializer: '',
|
|
51
|
+
},
|
|
52
|
+
service: {
|
|
53
|
+
name: iac.service || 'default-service',
|
|
54
|
+
logProject: `${iac.service}-log-project`,
|
|
55
|
+
logStore: `${iac.service}-log-store`,
|
|
56
|
+
qualifier: 'LATEST',
|
|
57
|
+
versionId: '1',
|
|
58
|
+
},
|
|
59
|
+
tracing: {
|
|
60
|
+
spanContext: '',
|
|
61
|
+
jaegerEndpoint: '',
|
|
62
|
+
spanBaggages: {},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
exports.createAliyunContextSerializable = createAliyunContextSerializable;
|
|
67
|
+
const createAliyunContext = (iac, functionName, handler, memory, timeout, requestId) => {
|
|
68
|
+
const baseContext = (0, exports.createAliyunContextSerializable)(iac, functionName, handler, memory, timeout, requestId);
|
|
69
|
+
return {
|
|
70
|
+
...baseContext,
|
|
71
|
+
tracing: {
|
|
72
|
+
...baseContext.tracing,
|
|
73
|
+
parseOpenTracingBaggages: () => ({}),
|
|
74
|
+
},
|
|
75
|
+
logger: createFCLogger(requestId),
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
exports.createAliyunContext = createAliyunContext;
|
|
79
|
+
const addFCHeaders = (context, headers) => {
|
|
80
|
+
return {
|
|
81
|
+
...headers,
|
|
82
|
+
'x-fc-request-id': context.requestId,
|
|
83
|
+
'x-fc-access-key-id': context.credentials.accessKeyId,
|
|
84
|
+
'x-fc-access-key-secret': context.credentials.accessKeySecret,
|
|
85
|
+
'x-fc-security-token': context.credentials.securityToken,
|
|
86
|
+
'x-fc-function-handler': context.function.handler,
|
|
87
|
+
'x-fc-function-memory': String(context.function.memory),
|
|
88
|
+
'x-fc-region': context.region,
|
|
89
|
+
'x-fc-account-id': context.accountId,
|
|
90
|
+
'x-fc-qualifier': context.service.qualifier,
|
|
91
|
+
'x-fc-version-id': context.service.versionId,
|
|
92
|
+
'x-fc-function-name': context.function.name,
|
|
93
|
+
'x-fc-service-logproject': context.service.logProject,
|
|
94
|
+
'x-fc-service-logstore': context.service.logStore,
|
|
95
|
+
'x-fc-control-path': '/http-invoke',
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
exports.addFCHeaders = addFCHeaders;
|
|
99
|
+
const transformFCResponse = (result) => {
|
|
100
|
+
if (result && typeof result === 'object' && 'statusCode' in result && 'body' in result) {
|
|
101
|
+
const { statusCode: rawStatus = 200, body: rawBody, isBase64Encoded, headers = {}, } = result;
|
|
102
|
+
const parsedStatus = typeof rawStatus === 'string' ? parseInt(rawStatus, 10) : rawStatus;
|
|
103
|
+
const statusCode = isNaN(parsedStatus) ? 200 : parsedStatus;
|
|
104
|
+
let body = rawBody;
|
|
105
|
+
if (isBase64Encoded && typeof body === 'string') {
|
|
106
|
+
body = Buffer.from(body, 'base64').toString('utf-8');
|
|
107
|
+
}
|
|
108
|
+
if (typeof body === 'string') {
|
|
109
|
+
try {
|
|
110
|
+
body = JSON.parse(body);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// If parsing fails, keep as string
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { statusCode, headers, body };
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
statusCode: 200,
|
|
120
|
+
headers: { 'Content-Type': 'application/json' },
|
|
121
|
+
body: result,
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
exports.transformFCResponse = transformFCResponse;
|
|
125
|
+
const logApiGatewayRequest = (requestId, apiPath, statusCode, startTime, endTime, sourceIp) => {
|
|
126
|
+
const duration = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(1);
|
|
127
|
+
const startTimeStr = formatDateTime(startTime);
|
|
128
|
+
const endTimeStr = formatDateTime(endTime);
|
|
129
|
+
const timestamp = formatDateTime(new Date());
|
|
130
|
+
console.log(`${timestamp} | ${requestId} | ${apiPath} | Sync Call | local-app | Development | local-project | ${statusCode} | ${startTimeStr} | ${endTimeStr} | ${duration}s | - | ${sourceIp}`);
|
|
131
|
+
};
|
|
132
|
+
exports.logApiGatewayRequest = logApiGatewayRequest;
|
|
133
|
+
const formatDateTime = (date) => {
|
|
134
|
+
const year = date.getFullYear();
|
|
135
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
136
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
137
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
138
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
139
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
140
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
141
|
+
};
|
|
142
|
+
const generateRequestId = () => {
|
|
143
|
+
return (0, crypto_1.randomUUID)().replace(/-/g, '');
|
|
144
|
+
};
|
|
145
|
+
exports.generateRequestId = generateRequestId;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.bucketsHandler = void 0;
|
|
7
|
+
const common_1 = require("../../common");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const TEXT_MIME_TYPES = new Set([
|
|
11
|
+
'text/plain',
|
|
12
|
+
'text/html',
|
|
13
|
+
'text/css',
|
|
14
|
+
'text/javascript',
|
|
15
|
+
'text/markdown',
|
|
16
|
+
'text/xml',
|
|
17
|
+
'application/json',
|
|
18
|
+
'application/javascript',
|
|
19
|
+
'application/xml',
|
|
20
|
+
]);
|
|
21
|
+
const getMimeType = (filename) => {
|
|
22
|
+
const ext = node_path_1.default.extname(filename).toLowerCase();
|
|
23
|
+
const mimeTypes = {
|
|
24
|
+
'.html': 'text/html',
|
|
25
|
+
'.htm': 'text/html',
|
|
26
|
+
'.css': 'text/css',
|
|
27
|
+
'.js': 'application/javascript',
|
|
28
|
+
'.json': 'application/json',
|
|
29
|
+
'.xml': 'application/xml',
|
|
30
|
+
'.txt': 'text/plain',
|
|
31
|
+
'.md': 'text/markdown',
|
|
32
|
+
'.jpg': 'image/jpeg',
|
|
33
|
+
'.jpeg': 'image/jpeg',
|
|
34
|
+
'.png': 'image/png',
|
|
35
|
+
'.gif': 'image/gif',
|
|
36
|
+
'.svg': 'image/svg+xml',
|
|
37
|
+
'.ico': 'image/x-icon',
|
|
38
|
+
'.pdf': 'application/pdf',
|
|
39
|
+
'.zip': 'application/zip',
|
|
40
|
+
'.tar': 'application/x-tar',
|
|
41
|
+
'.gz': 'application/gzip',
|
|
42
|
+
};
|
|
43
|
+
return mimeTypes[ext] || 'application/octet-stream';
|
|
44
|
+
};
|
|
45
|
+
const listDirectory = (dirPath, bucketPath) => {
|
|
46
|
+
try {
|
|
47
|
+
const entries = node_fs_1.default.readdirSync(dirPath, { withFileTypes: true });
|
|
48
|
+
return entries.map((entry) => {
|
|
49
|
+
const fullPath = node_path_1.default.join(dirPath, entry.name);
|
|
50
|
+
const relativePath = node_path_1.default.relative(bucketPath, fullPath);
|
|
51
|
+
const stats = node_fs_1.default.statSync(fullPath);
|
|
52
|
+
return {
|
|
53
|
+
name: entry.name,
|
|
54
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
55
|
+
size: entry.isDirectory() ? 0 : stats.size,
|
|
56
|
+
path: relativePath.replace(/\\/g, '/'), // Normalize path separators
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
common_1.logger.error(`Error listing directory: ${error}`);
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const getAllFiles = (dirPath, bucketPath, fileList = []) => {
|
|
66
|
+
try {
|
|
67
|
+
const entries = node_fs_1.default.readdirSync(dirPath, { withFileTypes: true });
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const fullPath = node_path_1.default.join(dirPath, entry.name);
|
|
70
|
+
const relativePath = node_path_1.default.relative(bucketPath, fullPath);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
// Recursively list files in subdirectories
|
|
73
|
+
getAllFiles(fullPath, bucketPath, fileList);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const stats = node_fs_1.default.statSync(fullPath);
|
|
77
|
+
fileList.push({
|
|
78
|
+
name: entry.name,
|
|
79
|
+
type: 'file',
|
|
80
|
+
size: stats.size,
|
|
81
|
+
path: relativePath.replace(/\\/g, '/'),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return fileList;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
common_1.logger.error(`Error getting all files: ${error}`);
|
|
89
|
+
return fileList;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const bucketsHandler = async (req, parsed, iac) => {
|
|
93
|
+
common_1.logger.info(`Bucket request received by local server -> ${req.method} ${parsed.identifier ?? '/'} ${parsed.url}`);
|
|
94
|
+
// Find the bucket definition
|
|
95
|
+
const bucketDef = iac.buckets?.find((bucket) => bucket.key === parsed.identifier);
|
|
96
|
+
if (!bucketDef) {
|
|
97
|
+
return {
|
|
98
|
+
statusCode: 404,
|
|
99
|
+
body: { error: 'Bucket not found', bucketKey: parsed.identifier },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Determine the bucket path - use website.code if available, otherwise use bucket name
|
|
103
|
+
let bucketBasePath;
|
|
104
|
+
if (bucketDef.website?.code) {
|
|
105
|
+
bucketBasePath = node_path_1.default.resolve(process.cwd(), bucketDef.website.code);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Fallback: create a directory based on bucket name (for non-website buckets)
|
|
109
|
+
bucketBasePath = node_path_1.default.resolve(process.cwd(), bucketDef.name);
|
|
110
|
+
}
|
|
111
|
+
// Check if bucket path exists
|
|
112
|
+
if (!node_fs_1.default.existsSync(bucketBasePath)) {
|
|
113
|
+
return {
|
|
114
|
+
statusCode: 404,
|
|
115
|
+
body: {
|
|
116
|
+
error: 'Bucket directory not found',
|
|
117
|
+
bucketKey: bucketDef.key,
|
|
118
|
+
bucketName: bucketDef.name,
|
|
119
|
+
expectedPath: bucketBasePath,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// Root path lists all files in bucket
|
|
124
|
+
if (parsed.url === '/') {
|
|
125
|
+
try {
|
|
126
|
+
const files = getAllFiles(bucketBasePath, bucketBasePath);
|
|
127
|
+
return {
|
|
128
|
+
statusCode: 200,
|
|
129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
130
|
+
body: {
|
|
131
|
+
bucket: bucketDef.name,
|
|
132
|
+
bucketKey: bucketDef.key,
|
|
133
|
+
files,
|
|
134
|
+
count: files.length,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
common_1.logger.error(`Error listing bucket files: ${error}`);
|
|
140
|
+
return {
|
|
141
|
+
statusCode: 500,
|
|
142
|
+
body: {
|
|
143
|
+
error: 'Failed to list bucket files',
|
|
144
|
+
message: error instanceof Error ? error.message : String(error),
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Otherwise, serve the requested file
|
|
150
|
+
const requestedPath = parsed.url.startsWith('/') ? parsed.url.slice(1) : parsed.url;
|
|
151
|
+
const filePath = node_path_1.default.join(bucketBasePath, requestedPath);
|
|
152
|
+
// Security check: ensure the requested file is within the bucket directory
|
|
153
|
+
const normalizedFilePath = node_path_1.default.normalize(filePath);
|
|
154
|
+
const normalizedBucketPath = node_path_1.default.normalize(bucketBasePath);
|
|
155
|
+
if (!normalizedFilePath.startsWith(normalizedBucketPath)) {
|
|
156
|
+
return {
|
|
157
|
+
statusCode: 403,
|
|
158
|
+
body: { error: 'Access denied: Path traversal attempt detected' },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// Check if file exists
|
|
162
|
+
if (!node_fs_1.default.existsSync(filePath)) {
|
|
163
|
+
return {
|
|
164
|
+
statusCode: 404,
|
|
165
|
+
body: {
|
|
166
|
+
error: 'File not found',
|
|
167
|
+
path: requestedPath,
|
|
168
|
+
bucketKey: bucketDef.key,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// If it's a directory, list its contents
|
|
173
|
+
const stats = node_fs_1.default.statSync(filePath);
|
|
174
|
+
if (stats.isDirectory()) {
|
|
175
|
+
try {
|
|
176
|
+
const files = listDirectory(filePath, bucketBasePath);
|
|
177
|
+
return {
|
|
178
|
+
statusCode: 200,
|
|
179
|
+
headers: { 'Content-Type': 'application/json' },
|
|
180
|
+
body: {
|
|
181
|
+
bucket: bucketDef.name,
|
|
182
|
+
bucketKey: bucketDef.key,
|
|
183
|
+
directory: requestedPath,
|
|
184
|
+
files,
|
|
185
|
+
count: files.length,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
common_1.logger.error(`Error listing directory: ${error}`);
|
|
191
|
+
return {
|
|
192
|
+
statusCode: 500,
|
|
193
|
+
body: {
|
|
194
|
+
error: 'Failed to list directory',
|
|
195
|
+
message: error instanceof Error ? error.message : String(error),
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Serve the file
|
|
201
|
+
try {
|
|
202
|
+
const fileContent = node_fs_1.default.readFileSync(filePath);
|
|
203
|
+
const mimeType = getMimeType(filePath);
|
|
204
|
+
// For text files, return as string; for binary files, return as base64
|
|
205
|
+
const isTextFile = TEXT_MIME_TYPES.has(mimeType);
|
|
206
|
+
return {
|
|
207
|
+
statusCode: 200,
|
|
208
|
+
headers: {
|
|
209
|
+
'Content-Type': mimeType,
|
|
210
|
+
'Content-Length': String(fileContent.length),
|
|
211
|
+
},
|
|
212
|
+
body: isTextFile ? fileContent.toString('utf-8') : fileContent.toString('base64'),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
common_1.logger.error(`Error reading file: ${error}`);
|
|
217
|
+
return {
|
|
218
|
+
statusCode: 500,
|
|
219
|
+
body: {
|
|
220
|
+
error: 'Failed to read file',
|
|
221
|
+
message: error instanceof Error ? error.message : String(error),
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
exports.bucketsHandler = bucketsHandler;
|