@gugananuvem/aws-local-simulator 1.0.12 → 1.0.14
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 +235 -11
- package/package.json +12 -2
- package/src/config/default-config.js +1 -0
- package/src/index.js +18 -2
- package/src/server.js +36 -32
- package/src/services/apigateway/index.js +5 -0
- package/src/services/apigateway/server.js +20 -0
- package/src/services/apigateway/simulator.js +13 -3
- package/src/services/athena/index.js +75 -0
- package/src/services/athena/server.js +101 -0
- package/src/services/athena/simulador.js +998 -0
- package/src/services/athena/simulator.js +346 -0
- package/src/services/cloudformation/index.js +106 -0
- package/src/services/cloudformation/server.js +417 -0
- package/src/services/cloudformation/simulador.js +1045 -0
- package/src/services/cloudtrail/index.js +84 -0
- package/src/services/cloudtrail/server.js +235 -0
- package/src/services/cloudtrail/simulador.js +719 -0
- package/src/services/cloudwatch/index.js +84 -0
- package/src/services/cloudwatch/server.js +366 -0
- package/src/services/cloudwatch/simulador.js +1173 -0
- package/src/services/cognito/index.js +5 -0
- package/src/services/cognito/simulator.js +4 -0
- package/src/services/config/index.js +96 -0
- package/src/services/config/server.js +215 -0
- package/src/services/config/simulador.js +1260 -0
- package/src/services/dynamodb/index.js +7 -3
- package/src/services/dynamodb/server.js +4 -2
- package/src/services/dynamodb/simulator.js +39 -29
- package/src/services/eventbridge/index.js +55 -51
- package/src/services/eventbridge/server.js +209 -0
- package/src/services/eventbridge/simulator.js +684 -0
- package/src/services/index.js +30 -4
- package/src/services/kms/index.js +75 -0
- package/src/services/kms/server.js +67 -0
- package/src/services/kms/simulator.js +324 -0
- package/src/services/lambda/index.js +5 -0
- package/src/services/lambda/simulator.js +48 -38
- package/src/services/parameter-store/index.js +80 -0
- package/src/services/parameter-store/server.js +50 -0
- package/src/services/parameter-store/simulator.js +201 -0
- package/src/services/s3/index.js +7 -3
- package/src/services/s3/server.js +20 -13
- package/src/services/s3/simulator.js +163 -407
- package/src/services/secret-manager/index.js +80 -0
- package/src/services/secret-manager/server.js +50 -0
- package/src/services/secret-manager/simulator.js +171 -0
- package/src/services/sns/index.js +55 -42
- package/src/services/sns/server.js +580 -0
- package/src/services/sns/simulator.js +1482 -0
- package/src/services/sqs/index.js +2 -4
- package/src/services/sqs/server.js +4 -2
- package/src/services/xray/index.js +83 -0
- package/src/services/xray/server.js +308 -0
- package/src/services/xray/simulador.js +994 -0
- package/src/utils/cloudtrail-audit.js +129 -0
- package/src/utils/local-store.js +18 -2
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const crypto = require("crypto");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
6
8
|
const LocalStore = require("../../utils/local-store");
|
|
7
9
|
const logger = require("../../utils/logger");
|
|
8
|
-
const
|
|
10
|
+
const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
|
|
9
11
|
|
|
10
12
|
class S3Simulator {
|
|
11
13
|
constructor(config) {
|
|
@@ -13,6 +15,7 @@ class S3Simulator {
|
|
|
13
15
|
this.dataDir = path.join(process.env.AWS_LOCAL_SIMULATOR_DATA_DIR, "s3");
|
|
14
16
|
this.store = new LocalStore(this.dataDir);
|
|
15
17
|
this.buckets = new Map();
|
|
18
|
+
this.audit = new CloudTrailAudit("s3.amazonaws.com");
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
async initialize() {
|
|
@@ -22,22 +25,31 @@ class S3Simulator {
|
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
loadBuckets() {
|
|
25
|
-
// Carrega buckets da configuração
|
|
26
28
|
if (this.config.s3?.buckets) {
|
|
27
29
|
for (const bucketName of this.config.s3.buckets) {
|
|
28
30
|
this.createBucket(bucketName);
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
// Carrega buckets existentes do disco
|
|
33
34
|
const savedBuckets = this.store.read("__buckets__");
|
|
34
|
-
if (savedBuckets) {
|
|
35
|
+
if (savedBuckets && typeof savedBuckets === "object" && !Array.isArray(savedBuckets)) {
|
|
35
36
|
for (const [name, data] of Object.entries(savedBuckets)) {
|
|
36
37
|
if (!this.buckets.has(name)) {
|
|
38
|
+
const objects = new Map();
|
|
39
|
+
for (const [key, meta] of Object.entries(data.objects || {})) {
|
|
40
|
+
objects.set(key, {
|
|
41
|
+
key: meta.key,
|
|
42
|
+
size: meta.size,
|
|
43
|
+
etag: meta.etag,
|
|
44
|
+
contentType: meta.contentType,
|
|
45
|
+
metadata: meta.metadata,
|
|
46
|
+
lastModified: new Date(meta.lastModified),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
37
49
|
this.buckets.set(name, {
|
|
38
50
|
name,
|
|
39
51
|
creationDate: new Date(data.creationDate),
|
|
40
|
-
objects
|
|
52
|
+
objects,
|
|
41
53
|
objectCount: data.objectCount || 0,
|
|
42
54
|
totalSize: data.totalSize || 0,
|
|
43
55
|
});
|
|
@@ -46,6 +58,39 @@ class S3Simulator {
|
|
|
46
58
|
}
|
|
47
59
|
}
|
|
48
60
|
|
|
61
|
+
// ─── Helpers de conteúdo em arquivo ───────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
_objectFilePath(bucketName, key) {
|
|
64
|
+
const safePath = key.split("/").map((part) =>
|
|
65
|
+
part.replace(/[<>:"|?*\\]/g, "_")
|
|
66
|
+
).join(path.sep);
|
|
67
|
+
return path.join(this.dataDir, bucketName, safePath);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_writeObjectContent(bucketName, key, content) {
|
|
71
|
+
const filePath = this._objectFilePath(bucketName, key);
|
|
72
|
+
const dir = path.dirname(filePath);
|
|
73
|
+
if (!fs.existsSync(dir)) {
|
|
74
|
+
require("mkdirp").sync(dir);
|
|
75
|
+
}
|
|
76
|
+
fs.writeFileSync(filePath, content);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_readObjectContent(bucketName, key) {
|
|
80
|
+
const filePath = this._objectFilePath(bucketName, key);
|
|
81
|
+
if (!fs.existsSync(filePath)) return null;
|
|
82
|
+
return fs.readFileSync(filePath);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_deleteObjectContent(bucketName, key) {
|
|
86
|
+
const filePath = this._objectFilePath(bucketName, key);
|
|
87
|
+
if (fs.existsSync(filePath)) {
|
|
88
|
+
fs.unlinkSync(filePath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Buckets ──────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
49
94
|
createBucket(bucketName) {
|
|
50
95
|
if (!this.isValidBucketName(bucketName)) {
|
|
51
96
|
return { error: { code: "InvalidBucketName", message: "Bucket name is invalid" }, status: 400 };
|
|
@@ -65,185 +110,27 @@ class S3Simulator {
|
|
|
65
110
|
|
|
66
111
|
this.buckets.set(bucketName, bucket);
|
|
67
112
|
this.persistBuckets();
|
|
68
|
-
|
|
69
113
|
logger.debug(`✅ Bucket S3 criado: ${bucketName}`);
|
|
70
|
-
|
|
114
|
+
this.audit.record({ eventName: "CreateBucket", readOnly: false, resources: [{ ARN: `arn:aws:s3:::${bucketName}`, type: "AWS::S3::Bucket" }], requestParameters: { bucketName } });
|
|
71
115
|
return { bucket };
|
|
72
116
|
}
|
|
73
117
|
|
|
74
118
|
deleteBucket(bucketName) {
|
|
75
119
|
const bucket = this.buckets.get(bucketName);
|
|
76
|
-
|
|
77
120
|
if (!bucket) {
|
|
78
121
|
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
79
122
|
}
|
|
80
|
-
|
|
81
123
|
if (bucket.objects.size > 0) {
|
|
82
124
|
return { error: { code: "BucketNotEmpty", message: "Bucket is not empty" }, status: 409 };
|
|
83
125
|
}
|
|
84
|
-
|
|
85
126
|
this.buckets.delete(bucketName);
|
|
86
127
|
this.store.delete(bucketName);
|
|
87
128
|
this.persistBuckets();
|
|
88
|
-
|
|
89
129
|
logger.debug(`🗑️ Bucket S3 deletado: ${bucketName}`);
|
|
90
|
-
|
|
91
|
-
return { success: true };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
putObject(bucketName, key, content, headers) {
|
|
95
|
-
const bucket = this.buckets.get(bucketName);
|
|
96
|
-
|
|
97
|
-
if (!bucket) {
|
|
98
|
-
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Normaliza o conteúdo
|
|
102
|
-
let body = content;
|
|
103
|
-
if (Buffer.isBuffer(content)) {
|
|
104
|
-
body = content;
|
|
105
|
-
} else if (typeof content === "object") {
|
|
106
|
-
body = Buffer.from(JSON.stringify(content));
|
|
107
|
-
} else if (typeof content === "string") {
|
|
108
|
-
body = Buffer.from(content);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const contentType = headers["content-type"] || "application/octet-stream";
|
|
112
|
-
const metadata = this.extractMetadata(headers);
|
|
113
|
-
const etag = crypto.createHash("md5").update(body).digest("hex");
|
|
114
|
-
|
|
115
|
-
const object = {
|
|
116
|
-
key,
|
|
117
|
-
size: body.length,
|
|
118
|
-
etag,
|
|
119
|
-
contentType,
|
|
120
|
-
metadata,
|
|
121
|
-
lastModified: new Date(),
|
|
122
|
-
content: body,
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
// Atualiza ou adiciona objeto
|
|
126
|
-
const oldObject = bucket.objects.get(key);
|
|
127
|
-
if (oldObject) {
|
|
128
|
-
bucket.totalSize -= oldObject.size;
|
|
129
|
-
} else {
|
|
130
|
-
bucket.objectCount++;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
bucket.objects.set(key, object);
|
|
134
|
-
bucket.totalSize += body.length;
|
|
135
|
-
|
|
136
|
-
this.persistBucket(bucketName);
|
|
137
|
-
|
|
138
|
-
logger.verboso(`📤 Upload S3: ${bucketName}/${key} (${body.length} bytes)`);
|
|
139
|
-
|
|
140
|
-
return { etag };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
getObject(bucketName, key, headers) {
|
|
144
|
-
const bucket = this.buckets.get(bucketName);
|
|
145
|
-
|
|
146
|
-
if (!bucket) {
|
|
147
|
-
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const object = bucket.objects.get(key);
|
|
151
|
-
if (!object) {
|
|
152
|
-
return { error: { code: "NoSuchKey", message: "The specified key does not exist" }, status: 404 };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
let content = object.content;
|
|
156
|
-
let start = 0;
|
|
157
|
-
let end = content.length - 1;
|
|
158
|
-
|
|
159
|
-
// Suporte a Range headers
|
|
160
|
-
if (headers.range) {
|
|
161
|
-
const range = headers.range.match(/bytes=(\d+)-(\d+)?/);
|
|
162
|
-
if (range) {
|
|
163
|
-
start = parseInt(range[1], 10);
|
|
164
|
-
end = range[2] ? parseInt(range[2], 10) : content.length - 1;
|
|
165
|
-
content = content.slice(start, end + 1);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
content,
|
|
171
|
-
etag: object.etag,
|
|
172
|
-
lastModified: object.lastModified.toUTCString(),
|
|
173
|
-
contentType: object.contentType,
|
|
174
|
-
size: content.length,
|
|
175
|
-
metadata: object.metadata,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
deleteObject(bucketName, key) {
|
|
180
|
-
const bucket = this.buckets.get(bucketName);
|
|
181
|
-
|
|
182
|
-
if (!bucket) {
|
|
183
|
-
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const object = bucket.objects.get(key);
|
|
187
|
-
if (object) {
|
|
188
|
-
bucket.objects.delete(key);
|
|
189
|
-
bucket.objectCount--;
|
|
190
|
-
bucket.totalSize -= object.size;
|
|
191
|
-
this.persistBucket(bucketName);
|
|
192
|
-
logger.verboso(`🗑️ Delete S3: ${bucketName}/${key}`);
|
|
193
|
-
}
|
|
194
|
-
|
|
130
|
+
this.audit.record({ eventName: "DeleteBucket", readOnly: false, resources: [{ ARN: `arn:aws:s3:::${bucketName}`, type: "AWS::S3::Bucket" }] });
|
|
195
131
|
return { success: true };
|
|
196
132
|
}
|
|
197
133
|
|
|
198
|
-
listObjects(bucketName, options = {}) {
|
|
199
|
-
const bucket = this.buckets.get(bucketName);
|
|
200
|
-
|
|
201
|
-
if (!bucket) {
|
|
202
|
-
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const { prefix = "", delimiter, maxKeys = 1000 } = options;
|
|
206
|
-
|
|
207
|
-
let objects = Array.from(bucket.objects.values())
|
|
208
|
-
.filter((obj) => obj.key.startsWith(prefix))
|
|
209
|
-
.sort((a, b) => a.key.localeCompare(b.key));
|
|
210
|
-
|
|
211
|
-
const commonPrefixes = new Set();
|
|
212
|
-
|
|
213
|
-
if (delimiter) {
|
|
214
|
-
const filteredObjects = [];
|
|
215
|
-
for (const obj of objects) {
|
|
216
|
-
const afterPrefix = obj.key.substring(prefix.length);
|
|
217
|
-
const delimiterIndex = afterPrefix.indexOf(delimiter);
|
|
218
|
-
|
|
219
|
-
if (delimiterIndex !== -1) {
|
|
220
|
-
const prefixPath = prefix + afterPrefix.substring(0, delimiterIndex + 1);
|
|
221
|
-
commonPrefixes.add(prefixPath);
|
|
222
|
-
} else {
|
|
223
|
-
filteredObjects.push(obj);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
objects = filteredObjects;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const contents = objects.slice(0, maxKeys).map((obj) => ({
|
|
230
|
-
Key: obj.key,
|
|
231
|
-
LastModified: obj.lastModified.toISOString(),
|
|
232
|
-
ETag: `"${obj.etag}"`,
|
|
233
|
-
Size: obj.size,
|
|
234
|
-
StorageClass: "STANDARD",
|
|
235
|
-
}));
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
name: bucketName,
|
|
239
|
-
prefix,
|
|
240
|
-
maxKeys,
|
|
241
|
-
isTruncated: objects.length > maxKeys,
|
|
242
|
-
contents,
|
|
243
|
-
commonPrefixes: Array.from(commonPrefixes),
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
134
|
listBuckets() {
|
|
248
135
|
return Array.from(this.buckets.values()).map((bucket) => ({
|
|
249
136
|
Name: bucket.name,
|
|
@@ -265,7 +152,6 @@ class S3Simulator {
|
|
|
265
152
|
if (!bucket) {
|
|
266
153
|
return { error: { code: "NoSuchBucket", message: "Bucket not found" } };
|
|
267
154
|
}
|
|
268
|
-
|
|
269
155
|
return {
|
|
270
156
|
name: bucket.name,
|
|
271
157
|
creationDate: bucket.creationDate,
|
|
@@ -282,78 +168,8 @@ class S3Simulator {
|
|
|
282
168
|
};
|
|
283
169
|
}
|
|
284
170
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (bucket) {
|
|
288
|
-
bucket.objects.clear();
|
|
289
|
-
bucket.objectCount = 0;
|
|
290
|
-
bucket.totalSize = 0;
|
|
291
|
-
this.persistBucket(bucketName);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
getBucketsCount() {
|
|
296
|
-
return this.buckets.size;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
isValidBucketName(bucketName) {
|
|
300
|
-
const regex = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
|
|
301
|
-
return regex.test(bucketName) && !bucketName.includes("..") && !bucketName.includes(".-") && !bucketName.includes("-.");
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
extractMetadata(headers) {
|
|
305
|
-
const metadata = {};
|
|
306
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
307
|
-
if (key.startsWith("x-amz-meta-")) {
|
|
308
|
-
const metaKey = key.replace("x-amz-meta-", "");
|
|
309
|
-
metadata[metaKey] = value;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return metadata;
|
|
313
|
-
}
|
|
314
|
-
persistBuckets() {
|
|
315
|
-
const bucketsObj = {};
|
|
316
|
-
for (const [name, bucket] of this.buckets.entries()) {
|
|
317
|
-
const objectsObj = {};
|
|
318
|
-
for (const [key, obj] of bucket.objects.entries()) {
|
|
319
|
-
objectsObj[key] = {
|
|
320
|
-
key: obj.key,
|
|
321
|
-
size: obj.size,
|
|
322
|
-
etag: obj.etag,
|
|
323
|
-
contentType: obj.contentType,
|
|
324
|
-
metadata: obj.metadata,
|
|
325
|
-
lastModified: obj.lastModified,
|
|
326
|
-
content: obj.content,
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
bucketsObj[name] = {
|
|
331
|
-
creationDate: bucket.creationDate.toISOString(),
|
|
332
|
-
objects: objectsObj,
|
|
333
|
-
objectCount: bucket.objectCount,
|
|
334
|
-
totalSize: bucket.totalSize,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
this.store.write("__buckets__", bucketsObj);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
isValidBucketName(bucketName) {
|
|
341
|
-
const regex = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
|
|
342
|
-
return regex.test(bucketName) && !bucketName.includes("..") && !bucketName.includes(".-") && !bucketName.includes("-.");
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async reset() {
|
|
346
|
-
for (const [name] of this.buckets) {
|
|
347
|
-
const bucket = this.buckets.get(name);
|
|
348
|
-
if (bucket) {
|
|
349
|
-
bucket.objects.clear();
|
|
350
|
-
bucket.objectCount = 0;
|
|
351
|
-
bucket.totalSize = 0;
|
|
352
|
-
this.store.write(name, {});
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
this.persistBuckets();
|
|
356
|
-
logger.debug("S3: Todos os dados resetados");
|
|
171
|
+
getBucket(bucketName) {
|
|
172
|
+
return this.buckets.get(bucketName);
|
|
357
173
|
}
|
|
358
174
|
|
|
359
175
|
getBucketsCount() {
|
|
@@ -368,81 +184,23 @@ class S3Simulator {
|
|
|
368
184
|
return total;
|
|
369
185
|
}
|
|
370
186
|
|
|
371
|
-
|
|
372
|
-
return this.buckets.get(bucketName);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
listBuckets() {
|
|
376
|
-
return Array.from(this.buckets.values()).map((bucket) => ({
|
|
377
|
-
Name: bucket.name,
|
|
378
|
-
CreationDate: bucket.creationDate.toISOString(),
|
|
379
|
-
}));
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
getBucketsInfo() {
|
|
383
|
-
return Array.from(this.buckets.values()).map((bucket) => ({
|
|
384
|
-
name: bucket.name,
|
|
385
|
-
creationDate: bucket.creationDate,
|
|
386
|
-
objectCount: bucket.objectCount,
|
|
387
|
-
totalSize: bucket.totalSize,
|
|
388
|
-
}));
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
getBucketInfo(bucketName) {
|
|
187
|
+
clearBucket(bucketName) {
|
|
392
188
|
const bucket = this.buckets.get(bucketName);
|
|
393
|
-
if (
|
|
394
|
-
|
|
189
|
+
if (bucket) {
|
|
190
|
+
for (const key of bucket.objects.keys()) {
|
|
191
|
+
this._deleteObjectContent(bucketName, key);
|
|
192
|
+
}
|
|
193
|
+
bucket.objects.clear();
|
|
194
|
+
bucket.objectCount = 0;
|
|
195
|
+
bucket.totalSize = 0;
|
|
196
|
+
this.persistBucket(bucketName);
|
|
395
197
|
}
|
|
396
|
-
|
|
397
|
-
return {
|
|
398
|
-
name: bucket.name,
|
|
399
|
-
creationDate: bucket.creationDate,
|
|
400
|
-
objectCount: bucket.objectCount,
|
|
401
|
-
totalSize: bucket.totalSize,
|
|
402
|
-
objects: Array.from(bucket.objects.values())
|
|
403
|
-
.slice(0, 20)
|
|
404
|
-
.map((obj) => ({
|
|
405
|
-
key: obj.key,
|
|
406
|
-
size: obj.size,
|
|
407
|
-
etag: obj.etag,
|
|
408
|
-
lastModified: obj.lastModified,
|
|
409
|
-
})),
|
|
410
|
-
};
|
|
411
198
|
}
|
|
412
199
|
|
|
413
|
-
|
|
414
|
-
const bucket = this.buckets.get(bucketName);
|
|
415
|
-
if (!bucket) return [];
|
|
416
|
-
return Array.from(bucket.objects.values()).map((obj) => ({
|
|
417
|
-
key: obj.key,
|
|
418
|
-
size: obj.size,
|
|
419
|
-
etag: obj.etag,
|
|
420
|
-
lastModified: obj.lastModified,
|
|
421
|
-
}));
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
headObject(bucketName, key) {
|
|
425
|
-
const bucket = this.buckets.get(bucketName);
|
|
426
|
-
if (!bucket) {
|
|
427
|
-
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const object = bucket.objects.get(key);
|
|
431
|
-
if (!object) {
|
|
432
|
-
return { error: { code: "NoSuchKey", message: "Key does not exist" }, status: 404 };
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return {
|
|
436
|
-
etag: object.etag,
|
|
437
|
-
lastModified: object.lastModified.toUTCString(),
|
|
438
|
-
contentType: object.contentType,
|
|
439
|
-
size: object.size,
|
|
440
|
-
};
|
|
441
|
-
}
|
|
200
|
+
// ─── Objects ──────────────────────────────────────────────────────────────
|
|
442
201
|
|
|
443
202
|
putObject(bucketName, key, content, headers) {
|
|
444
203
|
const bucket = this.buckets.get(bucketName);
|
|
445
|
-
|
|
446
204
|
if (!bucket) {
|
|
447
205
|
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
448
206
|
}
|
|
@@ -460,16 +218,6 @@ class S3Simulator {
|
|
|
460
218
|
const metadata = this.extractMetadata(headers);
|
|
461
219
|
const etag = crypto.createHash("md5").update(body).digest("hex");
|
|
462
220
|
|
|
463
|
-
const object = {
|
|
464
|
-
key,
|
|
465
|
-
size: body.length,
|
|
466
|
-
etag,
|
|
467
|
-
contentType,
|
|
468
|
-
metadata,
|
|
469
|
-
lastModified: new Date(),
|
|
470
|
-
content: body,
|
|
471
|
-
};
|
|
472
|
-
|
|
473
221
|
const oldObject = bucket.objects.get(key);
|
|
474
222
|
if (oldObject) {
|
|
475
223
|
bucket.totalSize -= oldObject.size;
|
|
@@ -477,19 +225,27 @@ class S3Simulator {
|
|
|
477
225
|
bucket.objectCount++;
|
|
478
226
|
}
|
|
479
227
|
|
|
480
|
-
|
|
228
|
+
// Apenas metadados no Map — conteúdo vai para arquivo
|
|
229
|
+
bucket.objects.set(key, {
|
|
230
|
+
key,
|
|
231
|
+
size: body.length,
|
|
232
|
+
etag,
|
|
233
|
+
contentType,
|
|
234
|
+
metadata,
|
|
235
|
+
lastModified: new Date(),
|
|
236
|
+
});
|
|
481
237
|
bucket.totalSize += body.length;
|
|
482
238
|
|
|
239
|
+
this._writeObjectContent(bucketName, key, body);
|
|
483
240
|
this.persistBucket(bucketName);
|
|
484
241
|
|
|
485
242
|
logger.verboso(`📤 Upload S3: ${bucketName}/${key} (${body.length} bytes)`);
|
|
486
|
-
|
|
243
|
+
this.audit.record({ eventName: "PutObject", readOnly: false, isDataEvent: true, resources: [{ ARN: `arn:aws:s3:::${bucketName}/${key}`, type: "AWS::S3::Object" }], requestParameters: { bucketName, key, contentType }, responseElements: { etag } });
|
|
487
244
|
return { etag };
|
|
488
245
|
}
|
|
489
246
|
|
|
490
247
|
getObject(bucketName, key, headers) {
|
|
491
248
|
const bucket = this.buckets.get(bucketName);
|
|
492
|
-
|
|
493
249
|
if (!bucket) {
|
|
494
250
|
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
495
251
|
}
|
|
@@ -499,20 +255,21 @@ class S3Simulator {
|
|
|
499
255
|
return { error: { code: "NoSuchKey", message: "The specified key does not exist" }, status: 404 };
|
|
500
256
|
}
|
|
501
257
|
|
|
502
|
-
let content =
|
|
503
|
-
|
|
504
|
-
|
|
258
|
+
let content = this._readObjectContent(bucketName, key);
|
|
259
|
+
if (!content) {
|
|
260
|
+
return { error: { code: "NoSuchKey", message: "Object content not found on disk" }, status: 404 };
|
|
261
|
+
}
|
|
505
262
|
|
|
506
|
-
if (headers.range) {
|
|
263
|
+
if (headers && headers.range) {
|
|
507
264
|
const range = headers.range.match(/bytes=(\d+)-(\d+)?/);
|
|
508
265
|
if (range) {
|
|
509
|
-
start = parseInt(range[1], 10);
|
|
510
|
-
end = range[2] ? parseInt(range[2], 10) : content.length - 1;
|
|
511
|
-
content = content.
|
|
266
|
+
const start = parseInt(range[1], 10);
|
|
267
|
+
const end = range[2] ? parseInt(range[2], 10) : content.length - 1;
|
|
268
|
+
content = content.subarray(start, end + 1);
|
|
512
269
|
}
|
|
513
270
|
}
|
|
514
271
|
|
|
515
|
-
|
|
272
|
+
const result = {
|
|
516
273
|
content,
|
|
517
274
|
etag: object.etag,
|
|
518
275
|
lastModified: object.lastModified.toUTCString(),
|
|
@@ -520,51 +277,64 @@ class S3Simulator {
|
|
|
520
277
|
size: content.length,
|
|
521
278
|
metadata: object.metadata,
|
|
522
279
|
};
|
|
280
|
+
this.audit.record({ eventName: "GetObject", readOnly: true, isDataEvent: true, resources: [{ ARN: `arn:aws:s3:::${bucketName}/${key}`, type: "AWS::S3::Object" }], requestParameters: { bucketName, key } });
|
|
281
|
+
return result;
|
|
523
282
|
}
|
|
524
283
|
|
|
525
|
-
|
|
284
|
+
headObject(bucketName, key) {
|
|
526
285
|
const bucket = this.buckets.get(bucketName);
|
|
527
|
-
|
|
528
286
|
if (!bucket) {
|
|
529
287
|
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
530
288
|
}
|
|
289
|
+
const object = bucket.objects.get(key);
|
|
290
|
+
if (!object) {
|
|
291
|
+
return { error: { code: "NoSuchKey", message: "Key does not exist" }, status: 404 };
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
etag: object.etag,
|
|
295
|
+
lastModified: object.lastModified.toUTCString(),
|
|
296
|
+
contentType: object.contentType,
|
|
297
|
+
size: object.size,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
531
300
|
|
|
301
|
+
deleteObject(bucketName, key) {
|
|
302
|
+
const bucket = this.buckets.get(bucketName);
|
|
303
|
+
if (!bucket) {
|
|
304
|
+
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
305
|
+
}
|
|
532
306
|
const object = bucket.objects.get(key);
|
|
533
307
|
if (object) {
|
|
534
308
|
bucket.objects.delete(key);
|
|
535
309
|
bucket.objectCount--;
|
|
536
310
|
bucket.totalSize -= object.size;
|
|
311
|
+
this._deleteObjectContent(bucketName, key);
|
|
537
312
|
this.persistBucket(bucketName);
|
|
538
313
|
logger.verboso(`🗑️ Delete S3: ${bucketName}/${key}`);
|
|
314
|
+
this.audit.record({ eventName: "DeleteObject", readOnly: false, isDataEvent: true, resources: [{ ARN: `arn:aws:s3:::${bucketName}/${key}`, type: "AWS::S3::Object" }], requestParameters: { bucketName, key } });
|
|
539
315
|
}
|
|
540
|
-
|
|
541
316
|
return { success: true };
|
|
542
317
|
}
|
|
543
318
|
|
|
544
319
|
listObjects(bucketName, options = {}) {
|
|
545
320
|
const bucket = this.buckets.get(bucketName);
|
|
546
|
-
|
|
547
321
|
if (!bucket) {
|
|
548
322
|
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
549
323
|
}
|
|
550
324
|
|
|
551
325
|
const { prefix = "", delimiter, maxKeys = 1000 } = options;
|
|
552
|
-
|
|
553
326
|
let objects = Array.from(bucket.objects.values())
|
|
554
327
|
.filter((obj) => obj.key.startsWith(prefix))
|
|
555
328
|
.sort((a, b) => a.key.localeCompare(b.key));
|
|
556
329
|
|
|
557
330
|
const commonPrefixes = new Set();
|
|
558
|
-
|
|
559
331
|
if (delimiter) {
|
|
560
332
|
const filteredObjects = [];
|
|
561
333
|
for (const obj of objects) {
|
|
562
334
|
const afterPrefix = obj.key.substring(prefix.length);
|
|
563
335
|
const delimiterIndex = afterPrefix.indexOf(delimiter);
|
|
564
|
-
|
|
565
336
|
if (delimiterIndex !== -1) {
|
|
566
|
-
|
|
567
|
-
commonPrefixes.add(prefixPath);
|
|
337
|
+
commonPrefixes.add(prefix + afterPrefix.substring(0, delimiterIndex + 1));
|
|
568
338
|
} else {
|
|
569
339
|
filteredObjects.push(obj);
|
|
570
340
|
}
|
|
@@ -590,20 +360,28 @@ class S3Simulator {
|
|
|
590
360
|
};
|
|
591
361
|
}
|
|
592
362
|
|
|
593
|
-
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
363
|
+
listAllObjects(bucketName) {
|
|
364
|
+
const bucket = this.buckets.get(bucketName);
|
|
365
|
+
if (!bucket) return [];
|
|
366
|
+
return Array.from(bucket.objects.values()).map((obj) => ({
|
|
367
|
+
key: obj.key,
|
|
368
|
+
size: obj.size,
|
|
369
|
+
etag: obj.etag,
|
|
370
|
+
lastModified: obj.lastModified,
|
|
371
|
+
}));
|
|
602
372
|
}
|
|
603
373
|
|
|
374
|
+
// ─── Persistência ─────────────────────────────────────────────────────────
|
|
375
|
+
|
|
604
376
|
persistBucket(bucketName) {
|
|
605
377
|
const bucket = this.buckets.get(bucketName);
|
|
606
|
-
if (bucket)
|
|
378
|
+
if (!bucket) return;
|
|
379
|
+
this.persistBuckets();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
persistBuckets() {
|
|
383
|
+
const bucketsObj = {};
|
|
384
|
+
for (const [name, bucket] of this.buckets.entries()) {
|
|
607
385
|
const objectsObj = {};
|
|
608
386
|
for (const [key, obj] of bucket.objects.entries()) {
|
|
609
387
|
objectsObj[key] = {
|
|
@@ -613,22 +391,47 @@ class S3Simulator {
|
|
|
613
391
|
contentType: obj.contentType,
|
|
614
392
|
metadata: obj.metadata,
|
|
615
393
|
lastModified: obj.lastModified,
|
|
616
|
-
content: obj.content,
|
|
617
394
|
};
|
|
618
395
|
}
|
|
619
|
-
|
|
620
|
-
|
|
396
|
+
bucketsObj[name] = {
|
|
397
|
+
creationDate: bucket.creationDate.toISOString(),
|
|
398
|
+
objects: objectsObj,
|
|
399
|
+
objectCount: bucket.objectCount,
|
|
400
|
+
totalSize: bucket.totalSize,
|
|
401
|
+
};
|
|
621
402
|
}
|
|
403
|
+
this.store.write("__buckets__", bucketsObj);
|
|
622
404
|
}
|
|
623
405
|
|
|
624
|
-
|
|
625
|
-
const bucket
|
|
626
|
-
|
|
406
|
+
async reset() {
|
|
407
|
+
for (const [name, bucket] of this.buckets) {
|
|
408
|
+
for (const key of bucket.objects.keys()) {
|
|
409
|
+
this._deleteObjectContent(name, key);
|
|
410
|
+
}
|
|
627
411
|
bucket.objects.clear();
|
|
628
412
|
bucket.objectCount = 0;
|
|
629
413
|
bucket.totalSize = 0;
|
|
630
|
-
this.
|
|
414
|
+
this.store.write(name, {});
|
|
415
|
+
}
|
|
416
|
+
this.persistBuckets();
|
|
417
|
+
logger.debug("S3: Todos os dados resetados");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ─── Utilitários ──────────────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
isValidBucketName(bucketName) {
|
|
423
|
+
const regex = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
|
|
424
|
+
return regex.test(bucketName) && !bucketName.includes("..") && !bucketName.includes(".-") && !bucketName.includes("-.");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
extractMetadata(headers) {
|
|
428
|
+
const metadata = {};
|
|
429
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
430
|
+
if (key.startsWith("x-amz-meta-")) {
|
|
431
|
+
metadata[key.replace("x-amz-meta-", "")] = value;
|
|
432
|
+
}
|
|
631
433
|
}
|
|
434
|
+
return metadata;
|
|
632
435
|
}
|
|
633
436
|
|
|
634
437
|
getStats() {
|
|
@@ -639,6 +442,8 @@ class S3Simulator {
|
|
|
639
442
|
};
|
|
640
443
|
}
|
|
641
444
|
|
|
445
|
+
// ─── XML Responses ────────────────────────────────────────────────────────
|
|
446
|
+
|
|
642
447
|
generateListBucketsResponse(buckets) {
|
|
643
448
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
644
449
|
<ListAllMyBucketsResult>
|
|
@@ -647,16 +452,7 @@ class S3Simulator {
|
|
|
647
452
|
<DisplayName>local-simulator</DisplayName>
|
|
648
453
|
</Owner>
|
|
649
454
|
<Buckets>
|
|
650
|
-
${buckets
|
|
651
|
-
.map(
|
|
652
|
-
(bucket) => `
|
|
653
|
-
<Bucket>
|
|
654
|
-
<Name>${bucket.Name}</Name>
|
|
655
|
-
<CreationDate>${bucket.CreationDate}</CreationDate>
|
|
656
|
-
</Bucket>
|
|
657
|
-
`,
|
|
658
|
-
)
|
|
659
|
-
.join("")}
|
|
455
|
+
${buckets.map((b) => `<Bucket><Name>${b.Name}</Name><CreationDate>${b.CreationDate}</CreationDate></Bucket>`).join("")}
|
|
660
456
|
</Buckets>
|
|
661
457
|
</ListAllMyBucketsResult>`;
|
|
662
458
|
}
|
|
@@ -668,28 +464,8 @@ class S3Simulator {
|
|
|
668
464
|
<Prefix>${data.prefix}</Prefix>
|
|
669
465
|
<MaxKeys>${data.maxKeys}</MaxKeys>
|
|
670
466
|
<IsTruncated>${data.isTruncated}</IsTruncated>
|
|
671
|
-
${data.contents
|
|
672
|
-
|
|
673
|
-
(obj) => `
|
|
674
|
-
<Contents>
|
|
675
|
-
<Key>${obj.Key}</Key>
|
|
676
|
-
<LastModified>${obj.LastModified}</LastModified>
|
|
677
|
-
<ETag>${obj.ETag}</ETag>
|
|
678
|
-
<Size>${obj.Size}</Size>
|
|
679
|
-
<StorageClass>${obj.StorageClass}</StorageClass>
|
|
680
|
-
</Contents>
|
|
681
|
-
`,
|
|
682
|
-
)
|
|
683
|
-
.join("")}
|
|
684
|
-
${data.commonPrefixes
|
|
685
|
-
.map(
|
|
686
|
-
(prefix) => `
|
|
687
|
-
<CommonPrefixes>
|
|
688
|
-
<Prefix>${prefix}</Prefix>
|
|
689
|
-
</CommonPrefixes>
|
|
690
|
-
`,
|
|
691
|
-
)
|
|
692
|
-
.join("")}
|
|
467
|
+
${data.contents.map((obj) => `<Contents><Key>${obj.Key}</Key><LastModified>${obj.LastModified}</LastModified><ETag>${obj.ETag}</ETag><Size>${obj.Size}</Size><StorageClass>${obj.StorageClass}</StorageClass></Contents>`).join("")}
|
|
468
|
+
${data.commonPrefixes.map((p) => `<CommonPrefixes><Prefix>${p}</Prefix></CommonPrefixes>`).join("")}
|
|
693
469
|
</ListBucketResult>`;
|
|
694
470
|
}
|
|
695
471
|
|
|
@@ -701,28 +477,8 @@ class S3Simulator {
|
|
|
701
477
|
<MaxKeys>${data.maxKeys}</MaxKeys>
|
|
702
478
|
<IsTruncated>${data.isTruncated}</IsTruncated>
|
|
703
479
|
<KeyCount>${data.contents.length}</KeyCount>
|
|
704
|
-
${data.contents
|
|
705
|
-
|
|
706
|
-
(obj) => `
|
|
707
|
-
<Contents>
|
|
708
|
-
<Key>${obj.Key}</Key>
|
|
709
|
-
<LastModified>${obj.LastModified}</LastModified>
|
|
710
|
-
<ETag>${obj.ETag}</ETag>
|
|
711
|
-
<Size>${obj.Size}</Size>
|
|
712
|
-
<StorageClass>${obj.StorageClass}</StorageClass>
|
|
713
|
-
</Contents>
|
|
714
|
-
`,
|
|
715
|
-
)
|
|
716
|
-
.join("")}
|
|
717
|
-
${data.commonPrefixes
|
|
718
|
-
.map(
|
|
719
|
-
(prefix) => `
|
|
720
|
-
<CommonPrefixes>
|
|
721
|
-
<Prefix>${prefix}</Prefix>
|
|
722
|
-
</CommonPrefixes>
|
|
723
|
-
`,
|
|
724
|
-
)
|
|
725
|
-
.join("")}
|
|
480
|
+
${data.contents.map((obj) => `<Contents><Key>${obj.Key}</Key><LastModified>${obj.LastModified}</LastModified><ETag>${obj.ETag}</ETag><Size>${obj.Size}</Size><StorageClass>${obj.StorageClass}</StorageClass></Contents>`).join("")}
|
|
481
|
+
${data.commonPrefixes.map((p) => `<CommonPrefixes><Prefix>${p}</Prefix></CommonPrefixes>`).join("")}
|
|
726
482
|
</ListBucketResult>`;
|
|
727
483
|
}
|
|
728
484
|
|