@stacksjs/ts-cloud 0.7.4 → 0.7.6
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/dist/aws/index.js +259 -0
- package/dist/bin/cli.js +316 -316
- package/dist/chunk-0wxyppza.js +146 -0
- package/dist/chunk-3knnr7wh.js +601 -0
- package/dist/chunk-3rsfns7x.js +10427 -0
- package/dist/chunk-93hjhs78.js +1752 -0
- package/dist/chunk-arsh1g5h.js +1749 -0
- package/dist/chunk-b82pbxyp.js +1572 -0
- package/dist/chunk-c6rgvg1j.js +46124 -0
- package/dist/chunk-d2p5n2aq.js +23 -0
- package/dist/chunk-d7p84vz5.js +1699 -0
- package/dist/chunk-dpkk640m.js +4392 -0
- package/dist/chunk-eq08r166.js +8 -0
- package/dist/chunk-hsk6fe6x.js +371 -0
- package/dist/chunk-mj1tmhte.js +13 -0
- package/dist/chunk-p6309384.js +12828 -0
- package/dist/chunk-pegd7rmf.js +10 -0
- package/dist/chunk-pqfzdg68.js +12 -0
- package/dist/chunk-qpj3edwz.js +601 -0
- package/dist/chunk-tjjgajbh.js +1032 -0
- package/dist/chunk-tnztxpcb.js +630 -0
- package/dist/chunk-tt4kxske.js +1788 -0
- package/dist/chunk-v0bahtg2.js +4 -0
- package/dist/chunk-vd87cpvn.js +1754 -0
- package/dist/chunk-zn0nxxa8.js +1779 -0
- package/dist/deploy/index.js +87 -0
- package/dist/dns/index.js +31 -0
- package/dist/drivers/hetzner/provision.d.ts +53 -0
- package/dist/drivers/index.d.ts +5 -1
- package/dist/drivers/index.js +98 -0
- package/dist/drivers/shared/remote-exec.d.ts +47 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4383 -91358
- package/dist/push/index.js +509 -0
- package/dist/ui/index.html +3 -3
- package/dist/ui/server/actions.html +3 -3
- package/dist/ui/server/backups.html +3 -3
- package/dist/ui/server/database.html +3 -3
- package/dist/ui/server/deployments.html +3 -3
- package/dist/ui/server/firewall.html +3 -3
- package/dist/ui/server/logs.html +3 -3
- package/dist/ui/server/services.html +3 -3
- package/dist/ui/server/sites.html +3 -3
- package/dist/ui/server/ssh-keys.html +3 -3
- package/dist/ui/server/terminal.html +3 -3
- package/dist/ui/server/workers.html +3 -3
- package/dist/ui/serverless/alarms.html +3 -3
- package/dist/ui/serverless/assets.html +3 -3
- package/dist/ui/serverless/data.html +3 -3
- package/dist/ui/serverless/deployments.html +3 -3
- package/dist/ui/serverless/functions.html +3 -3
- package/dist/ui/serverless/logs.html +3 -3
- package/dist/ui/serverless/queues.html +3 -3
- package/dist/ui/serverless/scheduler.html +3 -3
- package/dist/ui/serverless/secrets.html +3 -3
- package/dist/ui/serverless/traces.html +3 -3
- package/dist/ui/serverless.html +3 -3
- package/package.json +3 -3
|
@@ -0,0 +1,1754 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AWSClient
|
|
3
|
+
} from "./chunk-arsh1g5h.js";
|
|
4
|
+
import {
|
|
5
|
+
__require
|
|
6
|
+
} from "./chunk-v0bahtg2.js";
|
|
7
|
+
|
|
8
|
+
// src/aws/s3.ts
|
|
9
|
+
import * as crypto from "node:crypto";
|
|
10
|
+
|
|
11
|
+
// src/aws/credentials.ts
|
|
12
|
+
function resolveCredentials(profile) {
|
|
13
|
+
if (profile) {
|
|
14
|
+
const creds = loadProfileFromFile(profile);
|
|
15
|
+
if (!creds) {
|
|
16
|
+
throw new Error(`AWS profile '${profile}' not found in ~/.aws/credentials`);
|
|
17
|
+
}
|
|
18
|
+
return creds;
|
|
19
|
+
}
|
|
20
|
+
const envAccessKey = process.env.AWS_ACCESS_KEY_ID;
|
|
21
|
+
const envSecretKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
22
|
+
if (envAccessKey && envSecretKey) {
|
|
23
|
+
return {
|
|
24
|
+
accessKeyId: envAccessKey,
|
|
25
|
+
secretAccessKey: envSecretKey,
|
|
26
|
+
sessionToken: process.env.AWS_SESSION_TOKEN
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return loadProfileFromFile(process.env.AWS_PROFILE || "default") ?? { accessKeyId: "", secretAccessKey: "" };
|
|
30
|
+
}
|
|
31
|
+
function loadProfileFromFile(profile) {
|
|
32
|
+
const { existsSync, readFileSync } = __require("node:fs");
|
|
33
|
+
const { homedir } = __require("node:os");
|
|
34
|
+
const { join } = __require("node:path");
|
|
35
|
+
const credentialsPath = process.env.AWS_SHARED_CREDENTIALS_FILE || join(homedir(), ".aws", "credentials");
|
|
36
|
+
if (!existsSync(credentialsPath))
|
|
37
|
+
return null;
|
|
38
|
+
const content = readFileSync(credentialsPath, "utf-8");
|
|
39
|
+
let currentProfile = null;
|
|
40
|
+
let accessKeyId;
|
|
41
|
+
let secretAccessKey;
|
|
42
|
+
let sessionToken;
|
|
43
|
+
for (const line of content.split(`
|
|
44
|
+
`)) {
|
|
45
|
+
const trimmed = line.trim();
|
|
46
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";"))
|
|
47
|
+
continue;
|
|
48
|
+
const profileMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
49
|
+
if (profileMatch) {
|
|
50
|
+
if (currentProfile === profile && accessKeyId && secretAccessKey) {
|
|
51
|
+
return { accessKeyId, secretAccessKey, sessionToken };
|
|
52
|
+
}
|
|
53
|
+
currentProfile = profileMatch[1];
|
|
54
|
+
accessKeyId = undefined;
|
|
55
|
+
secretAccessKey = undefined;
|
|
56
|
+
sessionToken = undefined;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (currentProfile === profile) {
|
|
60
|
+
const [key, ...valueParts] = trimmed.split("=");
|
|
61
|
+
const value = valueParts.join("=").trim();
|
|
62
|
+
switch (key.trim().toLowerCase()) {
|
|
63
|
+
case "aws_access_key_id":
|
|
64
|
+
accessKeyId = value;
|
|
65
|
+
break;
|
|
66
|
+
case "aws_secret_access_key":
|
|
67
|
+
secretAccessKey = value;
|
|
68
|
+
break;
|
|
69
|
+
case "aws_session_token":
|
|
70
|
+
sessionToken = value;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (currentProfile === profile && accessKeyId && secretAccessKey) {
|
|
76
|
+
return { accessKeyId, secretAccessKey, sessionToken };
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/aws/s3.ts
|
|
82
|
+
import { readdir } from "node:fs/promises";
|
|
83
|
+
import { join } from "node:path";
|
|
84
|
+
import { readFileSync } from "node:fs";
|
|
85
|
+
function toFetchBody(data) {
|
|
86
|
+
const { buffer, byteOffset, byteLength } = data;
|
|
87
|
+
if (buffer instanceof ArrayBuffer) {
|
|
88
|
+
return buffer.slice(byteOffset, byteOffset + byteLength);
|
|
89
|
+
}
|
|
90
|
+
const copy = new ArrayBuffer(byteLength);
|
|
91
|
+
new Uint8Array(copy).set(new Uint8Array(data));
|
|
92
|
+
return copy;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class S3Client {
|
|
96
|
+
client;
|
|
97
|
+
region;
|
|
98
|
+
explicitProfile;
|
|
99
|
+
endpoint;
|
|
100
|
+
forcePathStyle;
|
|
101
|
+
explicitCredentials;
|
|
102
|
+
constructor(region = "us-east-1", profile, options) {
|
|
103
|
+
this.region = region;
|
|
104
|
+
this.explicitProfile = profile;
|
|
105
|
+
this.endpoint = options?.endpoint;
|
|
106
|
+
this.forcePathStyle = options?.forcePathStyle;
|
|
107
|
+
this.explicitCredentials = options?.credentials;
|
|
108
|
+
this.client = new AWSClient(options?.credentials, { profile, endpoint: options?.endpoint, forcePathStyle: options?.forcePathStyle });
|
|
109
|
+
}
|
|
110
|
+
getCredentials() {
|
|
111
|
+
if (this.explicitCredentials?.accessKeyId && this.explicitCredentials.secretAccessKey) {
|
|
112
|
+
return this.explicitCredentials;
|
|
113
|
+
}
|
|
114
|
+
const creds = resolveCredentials(this.explicitProfile);
|
|
115
|
+
if (creds.accessKeyId && creds.secretAccessKey) {
|
|
116
|
+
return creds;
|
|
117
|
+
}
|
|
118
|
+
throw new Error("S3 credentials not found. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY (or pass explicit credentials/profile), or configure ~/.aws/credentials.");
|
|
119
|
+
}
|
|
120
|
+
s3BaseHost() {
|
|
121
|
+
return this.endpoint || `s3.${this.region}.amazonaws.com`;
|
|
122
|
+
}
|
|
123
|
+
s3VirtualHost(bucket) {
|
|
124
|
+
return this.forcePathStyle ? this.s3BaseHost() : `${bucket}.${this.s3BaseHost()}`;
|
|
125
|
+
}
|
|
126
|
+
async listBuckets() {
|
|
127
|
+
const result = await this.client.request({
|
|
128
|
+
service: "s3",
|
|
129
|
+
region: this.region,
|
|
130
|
+
method: "GET",
|
|
131
|
+
path: "/"
|
|
132
|
+
});
|
|
133
|
+
const buckets = [];
|
|
134
|
+
const root = result?.ListAllMyBucketsResult ?? result;
|
|
135
|
+
const bucketList = root?.Buckets?.Bucket;
|
|
136
|
+
if (bucketList) {
|
|
137
|
+
const list = Array.isArray(bucketList) ? bucketList : [bucketList];
|
|
138
|
+
for (const b of list) {
|
|
139
|
+
buckets.push({
|
|
140
|
+
Name: b.Name,
|
|
141
|
+
CreationDate: b.CreationDate
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { Buckets: buckets };
|
|
146
|
+
}
|
|
147
|
+
async createBucket(bucket, options) {
|
|
148
|
+
const headers = {};
|
|
149
|
+
if (options?.acl) {
|
|
150
|
+
headers["x-amz-acl"] = options.acl;
|
|
151
|
+
}
|
|
152
|
+
let body;
|
|
153
|
+
if (this.region !== "us-east-1" && !this.endpoint) {
|
|
154
|
+
body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
155
|
+
<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
156
|
+
<LocationConstraint>${this.region}</LocationConstraint>
|
|
157
|
+
</CreateBucketConfiguration>`;
|
|
158
|
+
headers["Content-Type"] = "application/xml";
|
|
159
|
+
}
|
|
160
|
+
await this.client.request({
|
|
161
|
+
service: "s3",
|
|
162
|
+
region: this.region,
|
|
163
|
+
method: "PUT",
|
|
164
|
+
path: `/${bucket}`,
|
|
165
|
+
headers,
|
|
166
|
+
body
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async deleteBucket(bucket) {
|
|
170
|
+
await this.client.request({
|
|
171
|
+
service: "s3",
|
|
172
|
+
region: this.region,
|
|
173
|
+
method: "DELETE",
|
|
174
|
+
path: `/${bucket}`
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async emptyAndDeleteBucket(bucket) {
|
|
178
|
+
let hasMore = true;
|
|
179
|
+
while (hasMore) {
|
|
180
|
+
const objects = await this.listAllObjects({ bucket });
|
|
181
|
+
if (objects.length === 0) {
|
|
182
|
+
hasMore = false;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
const keys = objects.map((obj) => obj.Key);
|
|
186
|
+
for (let i = 0;i < keys.length; i += 1000) {
|
|
187
|
+
const batch = keys.slice(i, i + 1000);
|
|
188
|
+
await this.deleteObjects(bucket, batch);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
await this.deleteBucket(bucket);
|
|
192
|
+
}
|
|
193
|
+
async listAllObjects(options) {
|
|
194
|
+
const allObjects = [];
|
|
195
|
+
let continuationToken;
|
|
196
|
+
do {
|
|
197
|
+
const params = {
|
|
198
|
+
"list-type": "2",
|
|
199
|
+
"max-keys": "1000"
|
|
200
|
+
};
|
|
201
|
+
if (options.prefix) {
|
|
202
|
+
params.prefix = options.prefix;
|
|
203
|
+
}
|
|
204
|
+
if (continuationToken) {
|
|
205
|
+
params["continuation-token"] = continuationToken;
|
|
206
|
+
}
|
|
207
|
+
const result = await this.client.request({
|
|
208
|
+
service: "s3",
|
|
209
|
+
region: this.region,
|
|
210
|
+
method: "GET",
|
|
211
|
+
path: `/${options.bucket}`,
|
|
212
|
+
queryParams: params
|
|
213
|
+
});
|
|
214
|
+
const root = result?.ListBucketResult ?? result;
|
|
215
|
+
const contents = root?.Contents;
|
|
216
|
+
if (contents) {
|
|
217
|
+
const list = Array.isArray(contents) ? contents : [contents];
|
|
218
|
+
for (const obj of list) {
|
|
219
|
+
allObjects.push({
|
|
220
|
+
Key: obj.Key,
|
|
221
|
+
LastModified: obj.LastModified || "",
|
|
222
|
+
Size: Number.parseInt(obj.Size || "0"),
|
|
223
|
+
ETag: obj.ETag
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const isTruncated = root?.IsTruncated;
|
|
228
|
+
continuationToken = isTruncated === "true" || isTruncated === true ? root?.NextContinuationToken : undefined;
|
|
229
|
+
} while (continuationToken);
|
|
230
|
+
return allObjects;
|
|
231
|
+
}
|
|
232
|
+
async list(options) {
|
|
233
|
+
const result = await this.client.request({
|
|
234
|
+
service: "s3",
|
|
235
|
+
region: this.region,
|
|
236
|
+
method: "GET",
|
|
237
|
+
path: `/${options.bucket}`
|
|
238
|
+
});
|
|
239
|
+
const objects = [];
|
|
240
|
+
const root = result?.ListBucketResult ?? result;
|
|
241
|
+
const contents = root?.Contents;
|
|
242
|
+
if (contents) {
|
|
243
|
+
const items = Array.isArray(contents) ? contents : [contents];
|
|
244
|
+
for (const item of items) {
|
|
245
|
+
if (options.prefix && !item.Key?.startsWith(options.prefix)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
objects.push({
|
|
249
|
+
Key: item.Key || "",
|
|
250
|
+
LastModified: item.LastModified || "",
|
|
251
|
+
Size: Number.parseInt(item.Size || "0"),
|
|
252
|
+
ETag: item.ETag
|
|
253
|
+
});
|
|
254
|
+
if (options.maxKeys && objects.length >= options.maxKeys) {
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return objects;
|
|
260
|
+
}
|
|
261
|
+
async putObject(options) {
|
|
262
|
+
const headers = {};
|
|
263
|
+
if (options.acl) {
|
|
264
|
+
headers["x-amz-acl"] = options.acl;
|
|
265
|
+
}
|
|
266
|
+
if (options.cacheControl) {
|
|
267
|
+
headers["Cache-Control"] = options.cacheControl;
|
|
268
|
+
}
|
|
269
|
+
if (options.contentType) {
|
|
270
|
+
headers["Content-Type"] = options.contentType;
|
|
271
|
+
}
|
|
272
|
+
if (options.metadata) {
|
|
273
|
+
for (const [key, value] of Object.entries(options.metadata)) {
|
|
274
|
+
headers[`x-amz-meta-${key}`] = value;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const encodedKey = options.key.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
278
|
+
const normalizedBody = options.body instanceof Uint8Array && !Buffer.isBuffer(options.body) ? Buffer.from(options.body) : options.body;
|
|
279
|
+
if (Buffer.isBuffer(normalizedBody) || normalizedBody instanceof Uint8Array) {
|
|
280
|
+
const binaryBody = Buffer.isBuffer(normalizedBody) ? normalizedBody : Buffer.from(normalizedBody);
|
|
281
|
+
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
|
|
282
|
+
const host = this.s3VirtualHost(options.bucket);
|
|
283
|
+
const url = `https://${host}/${encodedKey}`;
|
|
284
|
+
const now = new Date;
|
|
285
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
286
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
287
|
+
const payloadHash = crypto.createHash("sha256").update(binaryBody).digest("hex");
|
|
288
|
+
const requestHeaders = {
|
|
289
|
+
host,
|
|
290
|
+
"x-amz-date": amzDate,
|
|
291
|
+
"x-amz-content-sha256": payloadHash,
|
|
292
|
+
...headers
|
|
293
|
+
};
|
|
294
|
+
if (sessionToken) {
|
|
295
|
+
requestHeaders["x-amz-security-token"] = sessionToken;
|
|
296
|
+
}
|
|
297
|
+
const canonicalHeaders = Object.keys(requestHeaders).sort().map((key) => `${key.toLowerCase()}:${requestHeaders[key].trim()}
|
|
298
|
+
`).join("");
|
|
299
|
+
const signedHeaders = Object.keys(requestHeaders).sort().map((key) => key.toLowerCase()).join(";");
|
|
300
|
+
const canonicalRequest = [
|
|
301
|
+
"PUT",
|
|
302
|
+
`/${encodedKey}`,
|
|
303
|
+
"",
|
|
304
|
+
canonicalHeaders,
|
|
305
|
+
signedHeaders,
|
|
306
|
+
payloadHash
|
|
307
|
+
].join(`
|
|
308
|
+
`);
|
|
309
|
+
const algorithm = "AWS4-HMAC-SHA256";
|
|
310
|
+
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
|
|
311
|
+
const stringToSign = [
|
|
312
|
+
algorithm,
|
|
313
|
+
amzDate,
|
|
314
|
+
credentialScope,
|
|
315
|
+
crypto.createHash("sha256").update(canonicalRequest).digest("hex")
|
|
316
|
+
].join(`
|
|
317
|
+
`);
|
|
318
|
+
const kDate = crypto.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
|
|
319
|
+
const kRegion = crypto.createHmac("sha256", kDate).update(this.region).digest();
|
|
320
|
+
const kService = crypto.createHmac("sha256", kRegion).update("s3").digest();
|
|
321
|
+
const kSigning = crypto.createHmac("sha256", kService).update("aws4_request").digest();
|
|
322
|
+
const signature = crypto.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
323
|
+
const authorizationHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
324
|
+
const response = await fetch(url, {
|
|
325
|
+
method: "PUT",
|
|
326
|
+
headers: {
|
|
327
|
+
...requestHeaders,
|
|
328
|
+
Authorization: authorizationHeader
|
|
329
|
+
},
|
|
330
|
+
body: toFetchBody(binaryBody)
|
|
331
|
+
});
|
|
332
|
+
if (!response.ok) {
|
|
333
|
+
const errorText = await response.text();
|
|
334
|
+
throw new Error(`S3 PUT failed: ${response.status} ${errorText}`);
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
await this.client.request({
|
|
339
|
+
service: "s3",
|
|
340
|
+
region: this.region,
|
|
341
|
+
method: "PUT",
|
|
342
|
+
path: `/${encodedKey}`,
|
|
343
|
+
bucket: options.bucket,
|
|
344
|
+
headers,
|
|
345
|
+
body: options.body
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
async getObject(bucket, key) {
|
|
349
|
+
const encodedKey = key.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
350
|
+
const result = await this.client.request({
|
|
351
|
+
service: "s3",
|
|
352
|
+
region: this.region,
|
|
353
|
+
method: "GET",
|
|
354
|
+
path: `/${bucket}/${encodedKey}`,
|
|
355
|
+
rawResponse: true
|
|
356
|
+
});
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
async getObjectBytes(bucket, key) {
|
|
360
|
+
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
|
|
361
|
+
const host = this.s3VirtualHost(bucket);
|
|
362
|
+
const encodedKey = key.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
363
|
+
const canonicalUri = this.forcePathStyle ? `/${bucket}/${encodedKey}` : `/${encodedKey}`;
|
|
364
|
+
const url = `https://${host}${canonicalUri}`;
|
|
365
|
+
const now = new Date;
|
|
366
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
367
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
368
|
+
const payloadHash = crypto.createHash("sha256").update("").digest("hex");
|
|
369
|
+
const requestHeaders = {
|
|
370
|
+
host,
|
|
371
|
+
"x-amz-date": amzDate,
|
|
372
|
+
"x-amz-content-sha256": payloadHash
|
|
373
|
+
};
|
|
374
|
+
if (sessionToken) {
|
|
375
|
+
requestHeaders["x-amz-security-token"] = sessionToken;
|
|
376
|
+
}
|
|
377
|
+
const canonicalHeaders = Object.keys(requestHeaders).sort().map((k) => `${k.toLowerCase()}:${requestHeaders[k].trim()}
|
|
378
|
+
`).join("");
|
|
379
|
+
const signedHeaders = Object.keys(requestHeaders).sort().map((k) => k.toLowerCase()).join(";");
|
|
380
|
+
const canonicalRequest = [
|
|
381
|
+
"GET",
|
|
382
|
+
canonicalUri,
|
|
383
|
+
"",
|
|
384
|
+
canonicalHeaders,
|
|
385
|
+
signedHeaders,
|
|
386
|
+
payloadHash
|
|
387
|
+
].join(`
|
|
388
|
+
`);
|
|
389
|
+
const algorithm = "AWS4-HMAC-SHA256";
|
|
390
|
+
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
|
|
391
|
+
const stringToSign = [
|
|
392
|
+
algorithm,
|
|
393
|
+
amzDate,
|
|
394
|
+
credentialScope,
|
|
395
|
+
crypto.createHash("sha256").update(canonicalRequest).digest("hex")
|
|
396
|
+
].join(`
|
|
397
|
+
`);
|
|
398
|
+
const kDate = crypto.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
|
|
399
|
+
const kRegion = crypto.createHmac("sha256", kDate).update(this.region).digest();
|
|
400
|
+
const kService = crypto.createHmac("sha256", kRegion).update("s3").digest();
|
|
401
|
+
const kSigning = crypto.createHmac("sha256", kService).update("aws4_request").digest();
|
|
402
|
+
const signature = crypto.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
403
|
+
const authorizationHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
404
|
+
const response = await fetch(url, {
|
|
405
|
+
method: "GET",
|
|
406
|
+
headers: { ...requestHeaders, Authorization: authorizationHeader }
|
|
407
|
+
});
|
|
408
|
+
if (!response.ok) {
|
|
409
|
+
const errorText = await response.text();
|
|
410
|
+
throw new Error(`S3 GET failed: ${response.status} ${errorText}`);
|
|
411
|
+
}
|
|
412
|
+
const buffer = await response.arrayBuffer();
|
|
413
|
+
const contentLengthHeader = response.headers.get("content-length");
|
|
414
|
+
return {
|
|
415
|
+
body: new Uint8Array(buffer),
|
|
416
|
+
contentType: response.headers.get("content-type") ?? undefined,
|
|
417
|
+
contentLength: contentLengthHeader ? Number.parseInt(contentLengthHeader, 10) : undefined
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
async copyObject(options) {
|
|
421
|
+
const headers = {
|
|
422
|
+
"x-amz-copy-source": `/${options.sourceBucket}/${options.sourceKey}`
|
|
423
|
+
};
|
|
424
|
+
if (options.metadataDirective) {
|
|
425
|
+
headers["x-amz-metadata-directive"] = options.metadataDirective;
|
|
426
|
+
}
|
|
427
|
+
if (options.contentType) {
|
|
428
|
+
headers["Content-Type"] = options.contentType;
|
|
429
|
+
}
|
|
430
|
+
if (options.metadata) {
|
|
431
|
+
for (const [key, value] of Object.entries(options.metadata)) {
|
|
432
|
+
headers[`x-amz-meta-${key}`] = value;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
await this.client.request({
|
|
436
|
+
service: "s3",
|
|
437
|
+
region: this.region,
|
|
438
|
+
method: "PUT",
|
|
439
|
+
path: `/${options.destinationBucket}/${options.destinationKey}`,
|
|
440
|
+
headers
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
async deleteObject(bucket, key) {
|
|
444
|
+
const encodedKey = key.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
445
|
+
await this.client.request({
|
|
446
|
+
service: "s3",
|
|
447
|
+
region: this.region,
|
|
448
|
+
method: "DELETE",
|
|
449
|
+
path: `/${bucket}/${encodedKey}`
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
async deleteObjects(bucket, keys) {
|
|
453
|
+
const deleteXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
454
|
+
<Delete>
|
|
455
|
+
${keys.map((key) => `<Object><Key>${key}</Key></Object>`).join(`
|
|
456
|
+
`)}
|
|
457
|
+
</Delete>`;
|
|
458
|
+
const contentMd5 = crypto.createHash("md5").update(deleteXml).digest("base64");
|
|
459
|
+
await this.client.request({
|
|
460
|
+
service: "s3",
|
|
461
|
+
region: this.region,
|
|
462
|
+
method: "POST",
|
|
463
|
+
path: `/${bucket}`,
|
|
464
|
+
queryParams: { delete: "" },
|
|
465
|
+
body: deleteXml,
|
|
466
|
+
headers: {
|
|
467
|
+
"Content-Type": "application/xml",
|
|
468
|
+
"Content-MD5": contentMd5
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
async bucketExists(bucket) {
|
|
473
|
+
try {
|
|
474
|
+
await this.client.request({
|
|
475
|
+
service: "s3",
|
|
476
|
+
region: this.region,
|
|
477
|
+
method: "HEAD",
|
|
478
|
+
path: `/${bucket}`
|
|
479
|
+
});
|
|
480
|
+
return true;
|
|
481
|
+
} catch {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async copy(options) {
|
|
486
|
+
const fileContent = readFileSync(options.source);
|
|
487
|
+
await this.putObject({
|
|
488
|
+
bucket: options.bucket,
|
|
489
|
+
key: options.key,
|
|
490
|
+
body: fileContent,
|
|
491
|
+
acl: options.acl,
|
|
492
|
+
cacheControl: options.cacheControl,
|
|
493
|
+
contentType: options.contentType,
|
|
494
|
+
metadata: options.metadata
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
async sync(options) {
|
|
498
|
+
const files = await this.listFilesRecursive(options.source);
|
|
499
|
+
for (const file of files) {
|
|
500
|
+
if (options.exclude && options.exclude.some((pattern) => file.includes(pattern))) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (options.include && !options.include.some((pattern) => file.includes(pattern))) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
const relativePath = file.substring(options.source.length + 1);
|
|
507
|
+
const s3Key = options.prefix ? `${options.prefix}/${relativePath}` : relativePath;
|
|
508
|
+
if (!options.dryRun) {
|
|
509
|
+
const fileContent = readFileSync(file);
|
|
510
|
+
await this.putObject({
|
|
511
|
+
bucket: options.bucket,
|
|
512
|
+
key: s3Key,
|
|
513
|
+
body: fileContent,
|
|
514
|
+
acl: options.acl,
|
|
515
|
+
cacheControl: options.cacheControl,
|
|
516
|
+
contentType: options.contentType,
|
|
517
|
+
metadata: options.metadata
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async delete(bucket, key) {
|
|
523
|
+
await this.deleteObject(bucket, key);
|
|
524
|
+
}
|
|
525
|
+
async deletePrefix(bucket, prefix) {
|
|
526
|
+
const objects = await this.list({ bucket, prefix });
|
|
527
|
+
const keys = objects.map((obj) => obj.Key);
|
|
528
|
+
if (keys.length > 0) {
|
|
529
|
+
await this.deleteObjects(bucket, keys);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async getBucketSize(bucket, prefix) {
|
|
533
|
+
const objects = await this.list({ bucket, prefix });
|
|
534
|
+
return objects.reduce((total, obj) => total + obj.Size, 0);
|
|
535
|
+
}
|
|
536
|
+
async listFilesRecursive(dir) {
|
|
537
|
+
const files = [];
|
|
538
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
539
|
+
for (const entry of entries) {
|
|
540
|
+
const fullPath = join(dir, entry.name);
|
|
541
|
+
if (entry.isDirectory()) {
|
|
542
|
+
const subFiles = await this.listFilesRecursive(fullPath);
|
|
543
|
+
files.push(...subFiles);
|
|
544
|
+
} else {
|
|
545
|
+
files.push(fullPath);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return files;
|
|
549
|
+
}
|
|
550
|
+
async putBucketPolicy(bucket, policy) {
|
|
551
|
+
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
|
|
552
|
+
const host = this.s3BaseHost();
|
|
553
|
+
const policyString = typeof policy === "string" ? policy : JSON.stringify(policy);
|
|
554
|
+
const now = new Date;
|
|
555
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
556
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
557
|
+
const payloadHash = crypto.createHash("sha256").update(policyString).digest("hex");
|
|
558
|
+
const canonicalUri = "/" + bucket;
|
|
559
|
+
const canonicalQuerystring = "policy=";
|
|
560
|
+
const requestHeaders = {
|
|
561
|
+
host,
|
|
562
|
+
"x-amz-date": amzDate,
|
|
563
|
+
"x-amz-content-sha256": payloadHash,
|
|
564
|
+
"content-type": "application/json"
|
|
565
|
+
};
|
|
566
|
+
if (sessionToken) {
|
|
567
|
+
requestHeaders["x-amz-security-token"] = sessionToken;
|
|
568
|
+
}
|
|
569
|
+
const canonicalHeaders = Object.keys(requestHeaders).sort().map((key) => `${key.toLowerCase()}:${requestHeaders[key].trim()}
|
|
570
|
+
`).join("");
|
|
571
|
+
const signedHeaders = Object.keys(requestHeaders).sort().map((key) => key.toLowerCase()).join(";");
|
|
572
|
+
const canonicalRequest = [
|
|
573
|
+
"PUT",
|
|
574
|
+
canonicalUri,
|
|
575
|
+
canonicalQuerystring,
|
|
576
|
+
canonicalHeaders,
|
|
577
|
+
signedHeaders,
|
|
578
|
+
payloadHash
|
|
579
|
+
].join(`
|
|
580
|
+
`);
|
|
581
|
+
const algorithm = "AWS4-HMAC-SHA256";
|
|
582
|
+
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
|
|
583
|
+
const stringToSign = [
|
|
584
|
+
algorithm,
|
|
585
|
+
amzDate,
|
|
586
|
+
credentialScope,
|
|
587
|
+
crypto.createHash("sha256").update(canonicalRequest).digest("hex")
|
|
588
|
+
].join(`
|
|
589
|
+
`);
|
|
590
|
+
const kDate = crypto.createHmac("sha256", "AWS4" + secretAccessKey).update(dateStamp).digest();
|
|
591
|
+
const kRegion = crypto.createHmac("sha256", kDate).update(this.region).digest();
|
|
592
|
+
const kService = crypto.createHmac("sha256", kRegion).update("s3").digest();
|
|
593
|
+
const kSigning = crypto.createHmac("sha256", kService).update("aws4_request").digest();
|
|
594
|
+
const signature = crypto.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
595
|
+
const authHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
596
|
+
const url = `https://${host}${canonicalUri}?${canonicalQuerystring}`;
|
|
597
|
+
const response = await fetch(url, {
|
|
598
|
+
method: "PUT",
|
|
599
|
+
headers: {
|
|
600
|
+
...requestHeaders,
|
|
601
|
+
Authorization: authHeader
|
|
602
|
+
},
|
|
603
|
+
body: policyString
|
|
604
|
+
});
|
|
605
|
+
if (!response.ok) {
|
|
606
|
+
const text = await response.text();
|
|
607
|
+
throw new Error(`Failed to put bucket policy: ${response.status} ${text}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async getBucketPolicy(bucket) {
|
|
611
|
+
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
|
|
612
|
+
const host = this.s3BaseHost();
|
|
613
|
+
const now = new Date;
|
|
614
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
615
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
616
|
+
const payloadHash = crypto.createHash("sha256").update("").digest("hex");
|
|
617
|
+
const canonicalUri = "/" + bucket;
|
|
618
|
+
const canonicalQuerystring = "policy=";
|
|
619
|
+
const requestHeaders = {
|
|
620
|
+
host,
|
|
621
|
+
"x-amz-date": amzDate,
|
|
622
|
+
"x-amz-content-sha256": payloadHash
|
|
623
|
+
};
|
|
624
|
+
if (sessionToken) {
|
|
625
|
+
requestHeaders["x-amz-security-token"] = sessionToken;
|
|
626
|
+
}
|
|
627
|
+
const canonicalHeaders = Object.keys(requestHeaders).sort().map((key) => `${key.toLowerCase()}:${requestHeaders[key].trim()}
|
|
628
|
+
`).join("");
|
|
629
|
+
const signedHeaders = Object.keys(requestHeaders).sort().map((key) => key.toLowerCase()).join(";");
|
|
630
|
+
const canonicalRequest = [
|
|
631
|
+
"GET",
|
|
632
|
+
canonicalUri,
|
|
633
|
+
canonicalQuerystring,
|
|
634
|
+
canonicalHeaders,
|
|
635
|
+
signedHeaders,
|
|
636
|
+
payloadHash
|
|
637
|
+
].join(`
|
|
638
|
+
`);
|
|
639
|
+
const algorithm = "AWS4-HMAC-SHA256";
|
|
640
|
+
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
|
|
641
|
+
const stringToSign = [
|
|
642
|
+
algorithm,
|
|
643
|
+
amzDate,
|
|
644
|
+
credentialScope,
|
|
645
|
+
crypto.createHash("sha256").update(canonicalRequest).digest("hex")
|
|
646
|
+
].join(`
|
|
647
|
+
`);
|
|
648
|
+
const kDate = crypto.createHmac("sha256", "AWS4" + secretAccessKey).update(dateStamp).digest();
|
|
649
|
+
const kRegion = crypto.createHmac("sha256", kDate).update(this.region).digest();
|
|
650
|
+
const kService = crypto.createHmac("sha256", kRegion).update("s3").digest();
|
|
651
|
+
const kSigning = crypto.createHmac("sha256", kService).update("aws4_request").digest();
|
|
652
|
+
const signature = crypto.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
653
|
+
const authHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
654
|
+
const url = `https://${host}${canonicalUri}?${canonicalQuerystring}`;
|
|
655
|
+
const response = await fetch(url, {
|
|
656
|
+
method: "GET",
|
|
657
|
+
headers: {
|
|
658
|
+
...requestHeaders,
|
|
659
|
+
Authorization: authHeader
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
if (response.status === 404) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
if (!response.ok) {
|
|
666
|
+
const text2 = await response.text();
|
|
667
|
+
throw new Error(`Failed to get bucket policy: ${response.status} ${text2}`);
|
|
668
|
+
}
|
|
669
|
+
const text = await response.text();
|
|
670
|
+
return JSON.parse(text);
|
|
671
|
+
}
|
|
672
|
+
async deleteBucketPolicy(bucket) {
|
|
673
|
+
await this.client.request({
|
|
674
|
+
service: "s3",
|
|
675
|
+
region: this.region,
|
|
676
|
+
method: "DELETE",
|
|
677
|
+
path: `/${bucket}`,
|
|
678
|
+
queryParams: { policy: "" }
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
async headBucket(bucket) {
|
|
682
|
+
try {
|
|
683
|
+
const result = await this.client.request({
|
|
684
|
+
service: "s3",
|
|
685
|
+
region: this.region,
|
|
686
|
+
method: "HEAD",
|
|
687
|
+
path: `/${bucket}`,
|
|
688
|
+
returnHeaders: true
|
|
689
|
+
});
|
|
690
|
+
return { exists: true, region: result?.headers?.["x-amz-bucket-region"] };
|
|
691
|
+
} catch (e) {
|
|
692
|
+
if (e.statusCode === 404) {
|
|
693
|
+
return { exists: false };
|
|
694
|
+
}
|
|
695
|
+
throw e;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
async headObject(bucket, key) {
|
|
699
|
+
try {
|
|
700
|
+
const result = await this.client.request({
|
|
701
|
+
service: "s3",
|
|
702
|
+
region: this.region,
|
|
703
|
+
method: "HEAD",
|
|
704
|
+
path: `/${bucket}/${key}`,
|
|
705
|
+
returnHeaders: true
|
|
706
|
+
});
|
|
707
|
+
return {
|
|
708
|
+
ContentLength: result?.headers?.["content-length"] ? parseInt(result.headers["content-length"]) : undefined,
|
|
709
|
+
ContentType: result?.headers?.["content-type"],
|
|
710
|
+
ETag: result?.headers?.["etag"],
|
|
711
|
+
LastModified: result?.headers?.["last-modified"]
|
|
712
|
+
};
|
|
713
|
+
} catch (e) {
|
|
714
|
+
if (e.statusCode === 404) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
throw e;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async getObjectBuffer(bucket, key) {
|
|
721
|
+
const content = await this.getObject(bucket, key);
|
|
722
|
+
return Buffer.from(content);
|
|
723
|
+
}
|
|
724
|
+
async getObjectJson(bucket, key) {
|
|
725
|
+
const content = await this.getObject(bucket, key);
|
|
726
|
+
return JSON.parse(content);
|
|
727
|
+
}
|
|
728
|
+
async putObjectJson(bucket, key, data, options) {
|
|
729
|
+
await this.putObject({
|
|
730
|
+
bucket,
|
|
731
|
+
key,
|
|
732
|
+
body: JSON.stringify(data),
|
|
733
|
+
contentType: "application/json",
|
|
734
|
+
...options
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
async getBucketVersioning(bucket) {
|
|
738
|
+
const result = await this.client.request({
|
|
739
|
+
service: "s3",
|
|
740
|
+
region: this.region,
|
|
741
|
+
method: "GET",
|
|
742
|
+
path: `/${bucket}`,
|
|
743
|
+
queryParams: { versioning: "" }
|
|
744
|
+
});
|
|
745
|
+
return {
|
|
746
|
+
Status: result?.VersioningConfiguration?.Status
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
async putBucketVersioning(bucket, status) {
|
|
750
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
751
|
+
<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
752
|
+
<Status>${status}</Status>
|
|
753
|
+
</VersioningConfiguration>`;
|
|
754
|
+
await this.client.request({
|
|
755
|
+
service: "s3",
|
|
756
|
+
region: this.region,
|
|
757
|
+
method: "PUT",
|
|
758
|
+
path: `/${bucket}`,
|
|
759
|
+
queryParams: { versioning: "" },
|
|
760
|
+
headers: { "Content-Type": "application/xml" },
|
|
761
|
+
body
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
async getBucketLifecycleConfiguration(bucket) {
|
|
765
|
+
try {
|
|
766
|
+
const result = await this.client.request({
|
|
767
|
+
service: "s3",
|
|
768
|
+
region: this.region,
|
|
769
|
+
method: "GET",
|
|
770
|
+
path: `/${bucket}`,
|
|
771
|
+
queryParams: { lifecycle: "" }
|
|
772
|
+
});
|
|
773
|
+
return result?.LifecycleConfiguration;
|
|
774
|
+
} catch (e) {
|
|
775
|
+
if (e.statusCode === 404) {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
throw e;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
async putBucketLifecycleConfiguration(bucket, rules) {
|
|
782
|
+
const rulesXml = rules.map((rule) => {
|
|
783
|
+
let ruleXml = `<Rule><ID>${rule.ID}</ID><Status>${rule.Status}</Status>`;
|
|
784
|
+
if (rule.Filter) {
|
|
785
|
+
ruleXml += `<Filter><Prefix>${rule.Filter.Prefix || ""}</Prefix></Filter>`;
|
|
786
|
+
} else {
|
|
787
|
+
ruleXml += "<Filter><Prefix></Prefix></Filter>";
|
|
788
|
+
}
|
|
789
|
+
if (rule.Expiration) {
|
|
790
|
+
if (rule.Expiration.Days) {
|
|
791
|
+
ruleXml += `<Expiration><Days>${rule.Expiration.Days}</Days></Expiration>`;
|
|
792
|
+
} else if (rule.Expiration.Date) {
|
|
793
|
+
ruleXml += `<Expiration><Date>${rule.Expiration.Date}</Date></Expiration>`;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (rule.Transitions) {
|
|
797
|
+
for (const t of rule.Transitions) {
|
|
798
|
+
ruleXml += `<Transition><Days>${t.Days}</Days><StorageClass>${t.StorageClass}</StorageClass></Transition>`;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (rule.NoncurrentVersionExpiration) {
|
|
802
|
+
ruleXml += `<NoncurrentVersionExpiration><NoncurrentDays>${rule.NoncurrentVersionExpiration.NoncurrentDays}</NoncurrentDays></NoncurrentVersionExpiration>`;
|
|
803
|
+
}
|
|
804
|
+
ruleXml += "</Rule>";
|
|
805
|
+
return ruleXml;
|
|
806
|
+
}).join("");
|
|
807
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
808
|
+
<LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
809
|
+
${rulesXml}
|
|
810
|
+
</LifecycleConfiguration>`;
|
|
811
|
+
await this.client.request({
|
|
812
|
+
service: "s3",
|
|
813
|
+
region: this.region,
|
|
814
|
+
method: "PUT",
|
|
815
|
+
path: `/${bucket}`,
|
|
816
|
+
queryParams: { lifecycle: "" },
|
|
817
|
+
headers: { "Content-Type": "application/xml" },
|
|
818
|
+
body
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
async deleteBucketLifecycleConfiguration(bucket) {
|
|
822
|
+
await this.client.request({
|
|
823
|
+
service: "s3",
|
|
824
|
+
region: this.region,
|
|
825
|
+
method: "DELETE",
|
|
826
|
+
path: `/${bucket}`,
|
|
827
|
+
queryParams: { lifecycle: "" }
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
async getBucketCors(bucket) {
|
|
831
|
+
try {
|
|
832
|
+
const result = await this.client.request({
|
|
833
|
+
service: "s3",
|
|
834
|
+
region: this.region,
|
|
835
|
+
method: "GET",
|
|
836
|
+
path: `/${bucket}`,
|
|
837
|
+
queryParams: { cors: "" }
|
|
838
|
+
});
|
|
839
|
+
return result?.CORSConfiguration;
|
|
840
|
+
} catch (e) {
|
|
841
|
+
if (e.statusCode === 404) {
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
throw e;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async putBucketCors(bucket, rules) {
|
|
848
|
+
const rulesXml = rules.map((rule) => {
|
|
849
|
+
let ruleXml = "<CORSRule>";
|
|
850
|
+
for (const origin of rule.AllowedOrigins) {
|
|
851
|
+
ruleXml += `<AllowedOrigin>${origin}</AllowedOrigin>`;
|
|
852
|
+
}
|
|
853
|
+
for (const method of rule.AllowedMethods) {
|
|
854
|
+
ruleXml += `<AllowedMethod>${method}</AllowedMethod>`;
|
|
855
|
+
}
|
|
856
|
+
if (rule.AllowedHeaders) {
|
|
857
|
+
for (const header of rule.AllowedHeaders) {
|
|
858
|
+
ruleXml += `<AllowedHeader>${header}</AllowedHeader>`;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (rule.ExposeHeaders) {
|
|
862
|
+
for (const header of rule.ExposeHeaders) {
|
|
863
|
+
ruleXml += `<ExposeHeader>${header}</ExposeHeader>`;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (rule.MaxAgeSeconds) {
|
|
867
|
+
ruleXml += `<MaxAgeSeconds>${rule.MaxAgeSeconds}</MaxAgeSeconds>`;
|
|
868
|
+
}
|
|
869
|
+
ruleXml += "</CORSRule>";
|
|
870
|
+
return ruleXml;
|
|
871
|
+
}).join("");
|
|
872
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
873
|
+
<CORSConfiguration>
|
|
874
|
+
${rulesXml}
|
|
875
|
+
</CORSConfiguration>`;
|
|
876
|
+
await this.client.request({
|
|
877
|
+
service: "s3",
|
|
878
|
+
region: this.region,
|
|
879
|
+
method: "PUT",
|
|
880
|
+
path: `/${bucket}`,
|
|
881
|
+
queryParams: { cors: "" },
|
|
882
|
+
headers: { "Content-Type": "application/xml" },
|
|
883
|
+
body
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
async deleteBucketCors(bucket) {
|
|
887
|
+
await this.client.request({
|
|
888
|
+
service: "s3",
|
|
889
|
+
region: this.region,
|
|
890
|
+
method: "DELETE",
|
|
891
|
+
path: `/${bucket}`,
|
|
892
|
+
queryParams: { cors: "" }
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
async getBucketEncryption(bucket) {
|
|
896
|
+
try {
|
|
897
|
+
const result = await this.client.request({
|
|
898
|
+
service: "s3",
|
|
899
|
+
region: this.region,
|
|
900
|
+
method: "GET",
|
|
901
|
+
path: `/${bucket}`,
|
|
902
|
+
queryParams: { encryption: "" }
|
|
903
|
+
});
|
|
904
|
+
return result?.ServerSideEncryptionConfiguration;
|
|
905
|
+
} catch (e) {
|
|
906
|
+
if (e.statusCode === 404) {
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
throw e;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
async putBucketEncryption(bucket, sseAlgorithm, kmsKeyId) {
|
|
913
|
+
let ruleXml = `<ApplyServerSideEncryptionByDefault><SSEAlgorithm>${sseAlgorithm}</SSEAlgorithm>`;
|
|
914
|
+
if (kmsKeyId) {
|
|
915
|
+
ruleXml += `<KMSMasterKeyID>${kmsKeyId}</KMSMasterKeyID>`;
|
|
916
|
+
}
|
|
917
|
+
ruleXml += "</ApplyServerSideEncryptionByDefault>";
|
|
918
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
919
|
+
<ServerSideEncryptionConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
920
|
+
<Rule>${ruleXml}</Rule>
|
|
921
|
+
</ServerSideEncryptionConfiguration>`;
|
|
922
|
+
await this.client.request({
|
|
923
|
+
service: "s3",
|
|
924
|
+
region: this.region,
|
|
925
|
+
method: "PUT",
|
|
926
|
+
path: `/${bucket}`,
|
|
927
|
+
queryParams: { encryption: "" },
|
|
928
|
+
headers: { "Content-Type": "application/xml" },
|
|
929
|
+
body
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
async deleteBucketEncryption(bucket) {
|
|
933
|
+
await this.client.request({
|
|
934
|
+
service: "s3",
|
|
935
|
+
region: this.region,
|
|
936
|
+
method: "DELETE",
|
|
937
|
+
path: `/${bucket}`,
|
|
938
|
+
queryParams: { encryption: "" }
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
async getBucketTagging(bucket) {
|
|
942
|
+
try {
|
|
943
|
+
const result = await this.client.request({
|
|
944
|
+
service: "s3",
|
|
945
|
+
region: this.region,
|
|
946
|
+
method: "GET",
|
|
947
|
+
path: `/${bucket}`,
|
|
948
|
+
queryParams: { tagging: "" }
|
|
949
|
+
});
|
|
950
|
+
const tagSet = result?.Tagging?.TagSet?.Tag;
|
|
951
|
+
if (!tagSet)
|
|
952
|
+
return [];
|
|
953
|
+
return Array.isArray(tagSet) ? tagSet : [tagSet];
|
|
954
|
+
} catch (e) {
|
|
955
|
+
if (e.statusCode === 404) {
|
|
956
|
+
return [];
|
|
957
|
+
}
|
|
958
|
+
throw e;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async putBucketTagging(bucket, tags) {
|
|
962
|
+
const tagsXml = tags.map((t) => `<Tag><Key>${t.Key}</Key><Value>${t.Value}</Value></Tag>`).join("");
|
|
963
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
964
|
+
<Tagging>
|
|
965
|
+
<TagSet>${tagsXml}</TagSet>
|
|
966
|
+
</Tagging>`;
|
|
967
|
+
await this.client.request({
|
|
968
|
+
service: "s3",
|
|
969
|
+
region: this.region,
|
|
970
|
+
method: "PUT",
|
|
971
|
+
path: `/${bucket}`,
|
|
972
|
+
queryParams: { tagging: "" },
|
|
973
|
+
headers: { "Content-Type": "application/xml" },
|
|
974
|
+
body
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
async deleteBucketTagging(bucket) {
|
|
978
|
+
await this.client.request({
|
|
979
|
+
service: "s3",
|
|
980
|
+
region: this.region,
|
|
981
|
+
method: "DELETE",
|
|
982
|
+
path: `/${bucket}`,
|
|
983
|
+
queryParams: { tagging: "" }
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
async getObjectTagging(bucket, key) {
|
|
987
|
+
try {
|
|
988
|
+
const result = await this.client.request({
|
|
989
|
+
service: "s3",
|
|
990
|
+
region: this.region,
|
|
991
|
+
method: "GET",
|
|
992
|
+
path: `/${bucket}/${key}`,
|
|
993
|
+
queryParams: { tagging: "" }
|
|
994
|
+
});
|
|
995
|
+
const tagSet = result?.Tagging?.TagSet?.Tag;
|
|
996
|
+
if (!tagSet)
|
|
997
|
+
return [];
|
|
998
|
+
return Array.isArray(tagSet) ? tagSet : [tagSet];
|
|
999
|
+
} catch (e) {
|
|
1000
|
+
if (e.statusCode === 404) {
|
|
1001
|
+
return [];
|
|
1002
|
+
}
|
|
1003
|
+
throw e;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
async putObjectTagging(bucket, key, tags) {
|
|
1007
|
+
const tagsXml = tags.map((t) => `<Tag><Key>${t.Key}</Key><Value>${t.Value}</Value></Tag>`).join("");
|
|
1008
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1009
|
+
<Tagging>
|
|
1010
|
+
<TagSet>${tagsXml}</TagSet>
|
|
1011
|
+
</Tagging>`;
|
|
1012
|
+
await this.client.request({
|
|
1013
|
+
service: "s3",
|
|
1014
|
+
region: this.region,
|
|
1015
|
+
method: "PUT",
|
|
1016
|
+
path: `/${bucket}/${key}`,
|
|
1017
|
+
queryParams: { tagging: "" },
|
|
1018
|
+
headers: { "Content-Type": "application/xml" },
|
|
1019
|
+
body
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
async deleteObjectTagging(bucket, key) {
|
|
1023
|
+
await this.client.request({
|
|
1024
|
+
service: "s3",
|
|
1025
|
+
region: this.region,
|
|
1026
|
+
method: "DELETE",
|
|
1027
|
+
path: `/${bucket}/${key}`,
|
|
1028
|
+
queryParams: { tagging: "" }
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
async getBucketAcl(bucket) {
|
|
1032
|
+
const result = await this.client.request({
|
|
1033
|
+
service: "s3",
|
|
1034
|
+
region: this.region,
|
|
1035
|
+
method: "GET",
|
|
1036
|
+
path: `/${bucket}`,
|
|
1037
|
+
queryParams: { acl: "" }
|
|
1038
|
+
});
|
|
1039
|
+
return result?.AccessControlPolicy;
|
|
1040
|
+
}
|
|
1041
|
+
async putBucketAcl(bucket, acl) {
|
|
1042
|
+
await this.client.request({
|
|
1043
|
+
service: "s3",
|
|
1044
|
+
region: this.region,
|
|
1045
|
+
method: "PUT",
|
|
1046
|
+
path: `/${bucket}`,
|
|
1047
|
+
queryParams: { acl: "" },
|
|
1048
|
+
headers: { "x-amz-acl": acl }
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
async getObjectAcl(bucket, key) {
|
|
1052
|
+
const result = await this.client.request({
|
|
1053
|
+
service: "s3",
|
|
1054
|
+
region: this.region,
|
|
1055
|
+
method: "GET",
|
|
1056
|
+
path: `/${bucket}/${key}`,
|
|
1057
|
+
queryParams: { acl: "" }
|
|
1058
|
+
});
|
|
1059
|
+
return result?.AccessControlPolicy;
|
|
1060
|
+
}
|
|
1061
|
+
async putObjectAcl(bucket, key, acl) {
|
|
1062
|
+
await this.client.request({
|
|
1063
|
+
service: "s3",
|
|
1064
|
+
region: this.region,
|
|
1065
|
+
method: "PUT",
|
|
1066
|
+
path: `/${bucket}/${key}`,
|
|
1067
|
+
queryParams: { acl: "" },
|
|
1068
|
+
headers: { "x-amz-acl": acl }
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
async getBucketLocation(bucket) {
|
|
1072
|
+
const result = await this.client.request({
|
|
1073
|
+
service: "s3",
|
|
1074
|
+
region: this.region,
|
|
1075
|
+
method: "GET",
|
|
1076
|
+
path: `/${bucket}`,
|
|
1077
|
+
queryParams: { location: "" }
|
|
1078
|
+
});
|
|
1079
|
+
return result?.LocationConstraint || "us-east-1";
|
|
1080
|
+
}
|
|
1081
|
+
async getBucketLogging(bucket) {
|
|
1082
|
+
const result = await this.client.request({
|
|
1083
|
+
service: "s3",
|
|
1084
|
+
region: this.region,
|
|
1085
|
+
method: "GET",
|
|
1086
|
+
path: `/${bucket}`,
|
|
1087
|
+
queryParams: { logging: "" }
|
|
1088
|
+
});
|
|
1089
|
+
return result?.BucketLoggingStatus;
|
|
1090
|
+
}
|
|
1091
|
+
async putBucketLogging(bucket, targetBucket, targetPrefix) {
|
|
1092
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1093
|
+
<BucketLoggingStatus xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
1094
|
+
<LoggingEnabled>
|
|
1095
|
+
<TargetBucket>${targetBucket}</TargetBucket>
|
|
1096
|
+
<TargetPrefix>${targetPrefix}</TargetPrefix>
|
|
1097
|
+
</LoggingEnabled>
|
|
1098
|
+
</BucketLoggingStatus>`;
|
|
1099
|
+
await this.client.request({
|
|
1100
|
+
service: "s3",
|
|
1101
|
+
region: this.region,
|
|
1102
|
+
method: "PUT",
|
|
1103
|
+
path: `/${bucket}`,
|
|
1104
|
+
queryParams: { logging: "" },
|
|
1105
|
+
headers: { "Content-Type": "application/xml" },
|
|
1106
|
+
body
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
async getBucketNotificationConfiguration(bucket) {
|
|
1110
|
+
const result = await this.client.request({
|
|
1111
|
+
service: "s3",
|
|
1112
|
+
region: this.region,
|
|
1113
|
+
method: "GET",
|
|
1114
|
+
path: `/${bucket}`,
|
|
1115
|
+
queryParams: { notification: "" }
|
|
1116
|
+
});
|
|
1117
|
+
return result?.NotificationConfiguration;
|
|
1118
|
+
}
|
|
1119
|
+
async putBucketNotificationConfiguration(bucket, config) {
|
|
1120
|
+
let configXml = "";
|
|
1121
|
+
if (config.LambdaFunctionConfigurations) {
|
|
1122
|
+
for (const c of config.LambdaFunctionConfigurations) {
|
|
1123
|
+
configXml += "<CloudFunctionConfiguration>";
|
|
1124
|
+
if (c.Id)
|
|
1125
|
+
configXml += `<Id>${c.Id}</Id>`;
|
|
1126
|
+
configXml += `<CloudFunction>${c.LambdaFunctionArn}</CloudFunction>`;
|
|
1127
|
+
for (const event of c.Events) {
|
|
1128
|
+
configXml += `<Event>${event}</Event>`;
|
|
1129
|
+
}
|
|
1130
|
+
if (c.Filter?.Key?.FilterRules) {
|
|
1131
|
+
configXml += "<Filter><S3Key>";
|
|
1132
|
+
for (const rule of c.Filter.Key.FilterRules) {
|
|
1133
|
+
configXml += `<FilterRule><Name>${rule.Name}</Name><Value>${rule.Value}</Value></FilterRule>`;
|
|
1134
|
+
}
|
|
1135
|
+
configXml += "</S3Key></Filter>";
|
|
1136
|
+
}
|
|
1137
|
+
configXml += "</CloudFunctionConfiguration>";
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (config.TopicConfigurations) {
|
|
1141
|
+
for (const c of config.TopicConfigurations) {
|
|
1142
|
+
configXml += "<TopicConfiguration>";
|
|
1143
|
+
if (c.Id)
|
|
1144
|
+
configXml += `<Id>${c.Id}</Id>`;
|
|
1145
|
+
configXml += `<Topic>${c.TopicArn}</Topic>`;
|
|
1146
|
+
for (const event of c.Events) {
|
|
1147
|
+
configXml += `<Event>${event}</Event>`;
|
|
1148
|
+
}
|
|
1149
|
+
configXml += "</TopicConfiguration>";
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (config.QueueConfigurations) {
|
|
1153
|
+
for (const c of config.QueueConfigurations) {
|
|
1154
|
+
configXml += "<QueueConfiguration>";
|
|
1155
|
+
if (c.Id)
|
|
1156
|
+
configXml += `<Id>${c.Id}</Id>`;
|
|
1157
|
+
configXml += `<Queue>${c.QueueArn}</Queue>`;
|
|
1158
|
+
for (const event of c.Events) {
|
|
1159
|
+
configXml += `<Event>${event}</Event>`;
|
|
1160
|
+
}
|
|
1161
|
+
configXml += "</QueueConfiguration>";
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1165
|
+
<NotificationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
1166
|
+
${configXml}
|
|
1167
|
+
</NotificationConfiguration>`;
|
|
1168
|
+
await this.client.request({
|
|
1169
|
+
service: "s3",
|
|
1170
|
+
region: this.region,
|
|
1171
|
+
method: "PUT",
|
|
1172
|
+
path: `/${bucket}`,
|
|
1173
|
+
queryParams: { notification: "" },
|
|
1174
|
+
headers: { "Content-Type": "application/xml" },
|
|
1175
|
+
body
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
async getBucketWebsite(bucket) {
|
|
1179
|
+
try {
|
|
1180
|
+
const result = await this.client.request({
|
|
1181
|
+
service: "s3",
|
|
1182
|
+
region: this.region,
|
|
1183
|
+
method: "GET",
|
|
1184
|
+
path: `/${bucket}`,
|
|
1185
|
+
queryParams: { website: "" }
|
|
1186
|
+
});
|
|
1187
|
+
return result?.WebsiteConfiguration;
|
|
1188
|
+
} catch (e) {
|
|
1189
|
+
if (e.statusCode === 404) {
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
throw e;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
async putBucketWebsite(bucket, config) {
|
|
1196
|
+
let configXml = "";
|
|
1197
|
+
if (config.RedirectAllRequestsTo) {
|
|
1198
|
+
configXml = `<RedirectAllRequestsTo>
|
|
1199
|
+
<HostName>${config.RedirectAllRequestsTo.HostName}</HostName>
|
|
1200
|
+
${config.RedirectAllRequestsTo.Protocol ? `<Protocol>${config.RedirectAllRequestsTo.Protocol}</Protocol>` : ""}
|
|
1201
|
+
</RedirectAllRequestsTo>`;
|
|
1202
|
+
} else {
|
|
1203
|
+
configXml = `<IndexDocument><Suffix>${config.IndexDocument}</Suffix></IndexDocument>`;
|
|
1204
|
+
if (config.ErrorDocument) {
|
|
1205
|
+
configXml += `<ErrorDocument><Key>${config.ErrorDocument}</Key></ErrorDocument>`;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1209
|
+
<WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
1210
|
+
${configXml}
|
|
1211
|
+
</WebsiteConfiguration>`;
|
|
1212
|
+
await this.client.request({
|
|
1213
|
+
service: "s3",
|
|
1214
|
+
region: this.region,
|
|
1215
|
+
method: "PUT",
|
|
1216
|
+
path: `/${bucket}`,
|
|
1217
|
+
queryParams: { website: "" },
|
|
1218
|
+
headers: { "Content-Type": "application/xml" },
|
|
1219
|
+
body
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
async deleteBucketWebsite(bucket) {
|
|
1223
|
+
await this.client.request({
|
|
1224
|
+
service: "s3",
|
|
1225
|
+
region: this.region,
|
|
1226
|
+
method: "DELETE",
|
|
1227
|
+
path: `/${bucket}`,
|
|
1228
|
+
queryParams: { website: "" }
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
async getBucketReplication(bucket) {
|
|
1232
|
+
try {
|
|
1233
|
+
const result = await this.client.request({
|
|
1234
|
+
service: "s3",
|
|
1235
|
+
region: this.region,
|
|
1236
|
+
method: "GET",
|
|
1237
|
+
path: `/${bucket}`,
|
|
1238
|
+
queryParams: { replication: "" }
|
|
1239
|
+
});
|
|
1240
|
+
return result?.ReplicationConfiguration;
|
|
1241
|
+
} catch (e) {
|
|
1242
|
+
if (e.statusCode === 404) {
|
|
1243
|
+
return null;
|
|
1244
|
+
}
|
|
1245
|
+
throw e;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
async deleteBucketReplication(bucket) {
|
|
1249
|
+
await this.client.request({
|
|
1250
|
+
service: "s3",
|
|
1251
|
+
region: this.region,
|
|
1252
|
+
method: "DELETE",
|
|
1253
|
+
path: `/${bucket}`,
|
|
1254
|
+
queryParams: { replication: "" }
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
async getPublicAccessBlock(bucket) {
|
|
1258
|
+
try {
|
|
1259
|
+
const result = await this.client.request({
|
|
1260
|
+
service: "s3",
|
|
1261
|
+
region: this.region,
|
|
1262
|
+
method: "GET",
|
|
1263
|
+
path: `/${bucket}`,
|
|
1264
|
+
queryParams: { publicAccessBlock: "" }
|
|
1265
|
+
});
|
|
1266
|
+
return result?.PublicAccessBlockConfiguration;
|
|
1267
|
+
} catch (e) {
|
|
1268
|
+
if (e.statusCode === 404) {
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1271
|
+
throw e;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
async putPublicAccessBlock(bucket, config) {
|
|
1275
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1276
|
+
<PublicAccessBlockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
1277
|
+
<BlockPublicAcls>${config.BlockPublicAcls ?? true}</BlockPublicAcls>
|
|
1278
|
+
<IgnorePublicAcls>${config.IgnorePublicAcls ?? true}</IgnorePublicAcls>
|
|
1279
|
+
<BlockPublicPolicy>${config.BlockPublicPolicy ?? true}</BlockPublicPolicy>
|
|
1280
|
+
<RestrictPublicBuckets>${config.RestrictPublicBuckets ?? true}</RestrictPublicBuckets>
|
|
1281
|
+
</PublicAccessBlockConfiguration>`;
|
|
1282
|
+
await this.client.request({
|
|
1283
|
+
service: "s3",
|
|
1284
|
+
region: this.region,
|
|
1285
|
+
method: "PUT",
|
|
1286
|
+
path: `/${bucket}`,
|
|
1287
|
+
queryParams: { publicAccessBlock: "" },
|
|
1288
|
+
headers: { "Content-Type": "application/xml" },
|
|
1289
|
+
body
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
async deletePublicAccessBlock(bucket) {
|
|
1293
|
+
await this.client.request({
|
|
1294
|
+
service: "s3",
|
|
1295
|
+
region: this.region,
|
|
1296
|
+
method: "DELETE",
|
|
1297
|
+
path: `/${bucket}`,
|
|
1298
|
+
queryParams: { publicAccessBlock: "" }
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
generatePresignedGetUrl(bucket, key, expiresInSeconds = 3600) {
|
|
1302
|
+
const { accessKeyId, secretAccessKey } = this.getCredentials();
|
|
1303
|
+
const host = this.s3VirtualHost(bucket);
|
|
1304
|
+
const now = new Date;
|
|
1305
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
1306
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
1307
|
+
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
|
|
1308
|
+
const credential = `${accessKeyId}/${credentialScope}`;
|
|
1309
|
+
const encodedKey = key.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
1310
|
+
const queryParams = new URLSearchParams({
|
|
1311
|
+
"X-Amz-Algorithm": "AWS4-HMAC-SHA256",
|
|
1312
|
+
"X-Amz-Credential": credential,
|
|
1313
|
+
"X-Amz-Date": amzDate,
|
|
1314
|
+
"X-Amz-Expires": expiresInSeconds.toString(),
|
|
1315
|
+
"X-Amz-SignedHeaders": "host"
|
|
1316
|
+
});
|
|
1317
|
+
const canonicalRequest = [
|
|
1318
|
+
"GET",
|
|
1319
|
+
`/${encodedKey}`,
|
|
1320
|
+
queryParams.toString(),
|
|
1321
|
+
`host:${host}
|
|
1322
|
+
`,
|
|
1323
|
+
"host",
|
|
1324
|
+
"UNSIGNED-PAYLOAD"
|
|
1325
|
+
].join(`
|
|
1326
|
+
`);
|
|
1327
|
+
const stringToSign = [
|
|
1328
|
+
"AWS4-HMAC-SHA256",
|
|
1329
|
+
amzDate,
|
|
1330
|
+
credentialScope,
|
|
1331
|
+
crypto.createHash("sha256").update(canonicalRequest).digest("hex")
|
|
1332
|
+
].join(`
|
|
1333
|
+
`);
|
|
1334
|
+
const kDate = crypto.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
|
|
1335
|
+
const kRegion = crypto.createHmac("sha256", kDate).update(this.region).digest();
|
|
1336
|
+
const kService = crypto.createHmac("sha256", kRegion).update("s3").digest();
|
|
1337
|
+
const kSigning = crypto.createHmac("sha256", kService).update("aws4_request").digest();
|
|
1338
|
+
const signature = crypto.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
1339
|
+
queryParams.append("X-Amz-Signature", signature);
|
|
1340
|
+
return `https://${host}/${encodedKey}?${queryParams.toString()}`;
|
|
1341
|
+
}
|
|
1342
|
+
generatePresignedPutUrl(bucket, key, contentType, expiresInSeconds = 3600) {
|
|
1343
|
+
const { accessKeyId, secretAccessKey } = this.getCredentials();
|
|
1344
|
+
const host = this.s3VirtualHost(bucket);
|
|
1345
|
+
const now = new Date;
|
|
1346
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
1347
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
1348
|
+
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
|
|
1349
|
+
const credential = `${accessKeyId}/${credentialScope}`;
|
|
1350
|
+
const encodedKey = key.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
1351
|
+
const queryParams = new URLSearchParams({
|
|
1352
|
+
"X-Amz-Algorithm": "AWS4-HMAC-SHA256",
|
|
1353
|
+
"X-Amz-Credential": credential,
|
|
1354
|
+
"X-Amz-Date": amzDate,
|
|
1355
|
+
"X-Amz-Expires": expiresInSeconds.toString(),
|
|
1356
|
+
"X-Amz-SignedHeaders": "content-type;host"
|
|
1357
|
+
});
|
|
1358
|
+
const canonicalRequest = [
|
|
1359
|
+
"PUT",
|
|
1360
|
+
`/${encodedKey}`,
|
|
1361
|
+
queryParams.toString(),
|
|
1362
|
+
`content-type:${contentType}
|
|
1363
|
+
host:${host}
|
|
1364
|
+
`,
|
|
1365
|
+
"content-type;host",
|
|
1366
|
+
"UNSIGNED-PAYLOAD"
|
|
1367
|
+
].join(`
|
|
1368
|
+
`);
|
|
1369
|
+
const stringToSign = [
|
|
1370
|
+
"AWS4-HMAC-SHA256",
|
|
1371
|
+
amzDate,
|
|
1372
|
+
credentialScope,
|
|
1373
|
+
crypto.createHash("sha256").update(canonicalRequest).digest("hex")
|
|
1374
|
+
].join(`
|
|
1375
|
+
`);
|
|
1376
|
+
const kDate = crypto.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
|
|
1377
|
+
const kRegion = crypto.createHmac("sha256", kDate).update(this.region).digest();
|
|
1378
|
+
const kService = crypto.createHmac("sha256", kRegion).update("s3").digest();
|
|
1379
|
+
const kSigning = crypto.createHmac("sha256", kService).update("aws4_request").digest();
|
|
1380
|
+
const signature = crypto.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
1381
|
+
queryParams.append("X-Amz-Signature", signature);
|
|
1382
|
+
return `https://${host}/${encodedKey}?${queryParams.toString()}`;
|
|
1383
|
+
}
|
|
1384
|
+
async createMultipartUpload(bucket, key, options) {
|
|
1385
|
+
const headers = {};
|
|
1386
|
+
if (options?.contentType) {
|
|
1387
|
+
headers["Content-Type"] = options.contentType;
|
|
1388
|
+
}
|
|
1389
|
+
if (options?.metadata) {
|
|
1390
|
+
for (const [k, v] of Object.entries(options.metadata)) {
|
|
1391
|
+
headers[`x-amz-meta-${k}`] = v;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
const result = await this.client.request({
|
|
1395
|
+
service: "s3",
|
|
1396
|
+
region: this.region,
|
|
1397
|
+
method: "POST",
|
|
1398
|
+
path: `/${bucket}/${key}`,
|
|
1399
|
+
queryParams: { uploads: "" },
|
|
1400
|
+
headers
|
|
1401
|
+
});
|
|
1402
|
+
return { UploadId: result?.InitiateMultipartUploadResult?.UploadId };
|
|
1403
|
+
}
|
|
1404
|
+
async uploadPart(bucket, key, uploadId, partNumber, body) {
|
|
1405
|
+
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
|
|
1406
|
+
const host = this.s3VirtualHost(bucket);
|
|
1407
|
+
const url = `https://${host}/${key}?partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`;
|
|
1408
|
+
const now = new Date;
|
|
1409
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
1410
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
1411
|
+
const payloadHash = crypto.createHash("sha256").update(body).digest("hex");
|
|
1412
|
+
const requestHeaders = {
|
|
1413
|
+
host,
|
|
1414
|
+
"x-amz-date": amzDate,
|
|
1415
|
+
"x-amz-content-sha256": payloadHash
|
|
1416
|
+
};
|
|
1417
|
+
if (sessionToken) {
|
|
1418
|
+
requestHeaders["x-amz-security-token"] = sessionToken;
|
|
1419
|
+
}
|
|
1420
|
+
const canonicalHeaders = Object.keys(requestHeaders).sort().map((k) => `${k.toLowerCase()}:${requestHeaders[k].trim()}
|
|
1421
|
+
`).join("");
|
|
1422
|
+
const signedHeaders = Object.keys(requestHeaders).sort().map((k) => k.toLowerCase()).join(";");
|
|
1423
|
+
const canonicalRequest = [
|
|
1424
|
+
"PUT",
|
|
1425
|
+
`/${key}`,
|
|
1426
|
+
`partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`,
|
|
1427
|
+
canonicalHeaders,
|
|
1428
|
+
signedHeaders,
|
|
1429
|
+
payloadHash
|
|
1430
|
+
].join(`
|
|
1431
|
+
`);
|
|
1432
|
+
const algorithm = "AWS4-HMAC-SHA256";
|
|
1433
|
+
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
|
|
1434
|
+
const stringToSign = [
|
|
1435
|
+
algorithm,
|
|
1436
|
+
amzDate,
|
|
1437
|
+
credentialScope,
|
|
1438
|
+
crypto.createHash("sha256").update(canonicalRequest).digest("hex")
|
|
1439
|
+
].join(`
|
|
1440
|
+
`);
|
|
1441
|
+
const kDate = crypto.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
|
|
1442
|
+
const kRegion = crypto.createHmac("sha256", kDate).update(this.region).digest();
|
|
1443
|
+
const kService = crypto.createHmac("sha256", kRegion).update("s3").digest();
|
|
1444
|
+
const kSigning = crypto.createHmac("sha256", kService).update("aws4_request").digest();
|
|
1445
|
+
const signature = crypto.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
1446
|
+
const authHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
1447
|
+
const response = await fetch(url, {
|
|
1448
|
+
method: "PUT",
|
|
1449
|
+
headers: {
|
|
1450
|
+
...requestHeaders,
|
|
1451
|
+
Authorization: authHeader
|
|
1452
|
+
},
|
|
1453
|
+
body: toFetchBody(body)
|
|
1454
|
+
});
|
|
1455
|
+
if (!response.ok) {
|
|
1456
|
+
const text = await response.text();
|
|
1457
|
+
throw new Error(`Upload part failed: ${response.status} ${text}`);
|
|
1458
|
+
}
|
|
1459
|
+
return { ETag: response.headers.get("etag") || "" };
|
|
1460
|
+
}
|
|
1461
|
+
async completeMultipartUpload(bucket, key, uploadId, parts) {
|
|
1462
|
+
const partsXml = parts.sort((a, b) => a.PartNumber - b.PartNumber).map((p) => `<Part><PartNumber>${p.PartNumber}</PartNumber><ETag>${p.ETag}</ETag></Part>`).join("");
|
|
1463
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1464
|
+
<CompleteMultipartUpload>${partsXml}</CompleteMultipartUpload>`;
|
|
1465
|
+
await this.client.request({
|
|
1466
|
+
service: "s3",
|
|
1467
|
+
region: this.region,
|
|
1468
|
+
method: "POST",
|
|
1469
|
+
path: `/${bucket}/${key}`,
|
|
1470
|
+
queryParams: { uploadId },
|
|
1471
|
+
headers: { "Content-Type": "application/xml" },
|
|
1472
|
+
body
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
async abortMultipartUpload(bucket, key, uploadId) {
|
|
1476
|
+
await this.client.request({
|
|
1477
|
+
service: "s3",
|
|
1478
|
+
region: this.region,
|
|
1479
|
+
method: "DELETE",
|
|
1480
|
+
path: `/${bucket}/${key}`,
|
|
1481
|
+
queryParams: { uploadId }
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
async listMultipartUploads(bucket) {
|
|
1485
|
+
const result = await this.client.request({
|
|
1486
|
+
service: "s3",
|
|
1487
|
+
region: this.region,
|
|
1488
|
+
method: "GET",
|
|
1489
|
+
path: `/${bucket}`,
|
|
1490
|
+
queryParams: { uploads: "" }
|
|
1491
|
+
});
|
|
1492
|
+
const uploads = result?.ListMultipartUploadsResult?.Upload;
|
|
1493
|
+
if (!uploads)
|
|
1494
|
+
return [];
|
|
1495
|
+
const list = Array.isArray(uploads) ? uploads : [uploads];
|
|
1496
|
+
return list.map((u) => ({
|
|
1497
|
+
Key: u.Key,
|
|
1498
|
+
UploadId: u.UploadId,
|
|
1499
|
+
Initiated: u.Initiated
|
|
1500
|
+
}));
|
|
1501
|
+
}
|
|
1502
|
+
async restoreObject(bucket, key, days, tier = "Standard") {
|
|
1503
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1504
|
+
<RestoreRequest>
|
|
1505
|
+
<Days>${days}</Days>
|
|
1506
|
+
<GlacierJobParameters>
|
|
1507
|
+
<Tier>${tier}</Tier>
|
|
1508
|
+
</GlacierJobParameters>
|
|
1509
|
+
</RestoreRequest>`;
|
|
1510
|
+
await this.client.request({
|
|
1511
|
+
service: "s3",
|
|
1512
|
+
region: this.region,
|
|
1513
|
+
method: "POST",
|
|
1514
|
+
path: `/${bucket}/${key}`,
|
|
1515
|
+
queryParams: { restore: "" },
|
|
1516
|
+
headers: { "Content-Type": "application/xml" },
|
|
1517
|
+
body
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
async selectObjectContent(bucket, key, expression, inputFormat, outputFormat = "JSON") {
|
|
1521
|
+
let inputSerialization = "";
|
|
1522
|
+
if (inputFormat === "CSV") {
|
|
1523
|
+
inputSerialization = "<CSV><FileHeaderInfo>USE</FileHeaderInfo></CSV>";
|
|
1524
|
+
} else if (inputFormat === "JSON") {
|
|
1525
|
+
inputSerialization = "<JSON><Type>DOCUMENT</Type></JSON>";
|
|
1526
|
+
} else {
|
|
1527
|
+
inputSerialization = "<Parquet/>";
|
|
1528
|
+
}
|
|
1529
|
+
let outputSerialization = "";
|
|
1530
|
+
if (outputFormat === "CSV") {
|
|
1531
|
+
outputSerialization = "<CSV/>";
|
|
1532
|
+
} else {
|
|
1533
|
+
outputSerialization = "<JSON/>";
|
|
1534
|
+
}
|
|
1535
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1536
|
+
<SelectObjectContentRequest>
|
|
1537
|
+
<Expression>${expression}</Expression>
|
|
1538
|
+
<ExpressionType>SQL</ExpressionType>
|
|
1539
|
+
<InputSerialization>${inputSerialization}</InputSerialization>
|
|
1540
|
+
<OutputSerialization>${outputSerialization}</OutputSerialization>
|
|
1541
|
+
</SelectObjectContentRequest>`;
|
|
1542
|
+
const result = await this.client.request({
|
|
1543
|
+
service: "s3",
|
|
1544
|
+
region: this.region,
|
|
1545
|
+
method: "POST",
|
|
1546
|
+
path: `/${bucket}/${key}`,
|
|
1547
|
+
queryParams: { select: "", "select-type": "2" },
|
|
1548
|
+
headers: { "Content-Type": "application/xml" },
|
|
1549
|
+
body,
|
|
1550
|
+
rawResponse: true
|
|
1551
|
+
});
|
|
1552
|
+
return result;
|
|
1553
|
+
}
|
|
1554
|
+
async getSignedUrl(options) {
|
|
1555
|
+
const { bucket, key, expiresIn = 3600, operation = "getObject" } = options;
|
|
1556
|
+
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
|
|
1557
|
+
const now = new Date;
|
|
1558
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
1559
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
1560
|
+
const host = this.s3VirtualHost(bucket);
|
|
1561
|
+
const method = operation === "putObject" ? "PUT" : "GET";
|
|
1562
|
+
const algorithm = "AWS4-HMAC-SHA256";
|
|
1563
|
+
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
|
|
1564
|
+
const credential = `${accessKeyId}/${credentialScope}`;
|
|
1565
|
+
const queryParams = {
|
|
1566
|
+
"X-Amz-Algorithm": algorithm,
|
|
1567
|
+
"X-Amz-Credential": credential,
|
|
1568
|
+
"X-Amz-Date": amzDate,
|
|
1569
|
+
"X-Amz-Expires": expiresIn.toString(),
|
|
1570
|
+
"X-Amz-SignedHeaders": "host"
|
|
1571
|
+
};
|
|
1572
|
+
if (sessionToken) {
|
|
1573
|
+
queryParams["X-Amz-Security-Token"] = sessionToken;
|
|
1574
|
+
}
|
|
1575
|
+
const sortedParams = Object.keys(queryParams).sort();
|
|
1576
|
+
const canonicalQuerystring = sortedParams.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(queryParams[k])}`).join("&");
|
|
1577
|
+
const canonicalUri = "/" + key.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
1578
|
+
const canonicalHeaders = `host:${host}
|
|
1579
|
+
`;
|
|
1580
|
+
const signedHeaders = "host";
|
|
1581
|
+
const payloadHash = "UNSIGNED-PAYLOAD";
|
|
1582
|
+
const canonicalRequest = [
|
|
1583
|
+
method,
|
|
1584
|
+
canonicalUri,
|
|
1585
|
+
canonicalQuerystring,
|
|
1586
|
+
canonicalHeaders,
|
|
1587
|
+
signedHeaders,
|
|
1588
|
+
payloadHash
|
|
1589
|
+
].join(`
|
|
1590
|
+
`);
|
|
1591
|
+
const stringToSign = [
|
|
1592
|
+
algorithm,
|
|
1593
|
+
amzDate,
|
|
1594
|
+
credentialScope,
|
|
1595
|
+
crypto.createHash("sha256").update(canonicalRequest).digest("hex")
|
|
1596
|
+
].join(`
|
|
1597
|
+
`);
|
|
1598
|
+
const kDate = crypto.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
|
|
1599
|
+
const kRegion = crypto.createHmac("sha256", kDate).update(this.region).digest();
|
|
1600
|
+
const kService = crypto.createHmac("sha256", kRegion).update("s3").digest();
|
|
1601
|
+
const kSigning = crypto.createHmac("sha256", kService).update("aws4_request").digest();
|
|
1602
|
+
const signature = crypto.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
1603
|
+
const presignedUrl = `https://${host}${canonicalUri}?${canonicalQuerystring}&X-Amz-Signature=${signature}`;
|
|
1604
|
+
return presignedUrl;
|
|
1605
|
+
}
|
|
1606
|
+
async listObjects(options) {
|
|
1607
|
+
const { bucket, prefix, maxKeys = 1000, continuationToken } = options;
|
|
1608
|
+
const queryParams = {
|
|
1609
|
+
"list-type": "2",
|
|
1610
|
+
"max-keys": maxKeys.toString()
|
|
1611
|
+
};
|
|
1612
|
+
if (prefix)
|
|
1613
|
+
queryParams.prefix = prefix;
|
|
1614
|
+
if (continuationToken)
|
|
1615
|
+
queryParams["continuation-token"] = continuationToken;
|
|
1616
|
+
const result = await this.client.request({
|
|
1617
|
+
service: "s3",
|
|
1618
|
+
region: this.region,
|
|
1619
|
+
method: "GET",
|
|
1620
|
+
path: `/${bucket}`,
|
|
1621
|
+
queryParams
|
|
1622
|
+
});
|
|
1623
|
+
const objects = [];
|
|
1624
|
+
const listResult = result?.ListBucketResult ?? result;
|
|
1625
|
+
if (listResult?.Contents) {
|
|
1626
|
+
const items = Array.isArray(listResult.Contents) ? listResult.Contents : [listResult.Contents];
|
|
1627
|
+
for (const item of items) {
|
|
1628
|
+
objects.push({
|
|
1629
|
+
Key: item.Key || "",
|
|
1630
|
+
LastModified: item.LastModified || "",
|
|
1631
|
+
Size: Number.parseInt(item.Size || "0"),
|
|
1632
|
+
ETag: item.ETag
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return {
|
|
1637
|
+
objects,
|
|
1638
|
+
nextContinuationToken: listResult?.NextContinuationToken
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
async emptyBucket(bucket) {
|
|
1642
|
+
let deletedCount = 0;
|
|
1643
|
+
const objects = await this.listAllObjects({ bucket });
|
|
1644
|
+
if (objects.length > 0) {
|
|
1645
|
+
for (let i = 0;i < objects.length; i += 1000) {
|
|
1646
|
+
const batch = objects.slice(i, i + 1000);
|
|
1647
|
+
const keys = batch.map((obj) => obj.Key);
|
|
1648
|
+
await this.deleteObjects(bucket, keys);
|
|
1649
|
+
deletedCount += keys.length;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
try {
|
|
1653
|
+
let keyMarker;
|
|
1654
|
+
let versionIdMarker;
|
|
1655
|
+
do {
|
|
1656
|
+
const versionsResult = await this.listObjectVersions({
|
|
1657
|
+
bucket,
|
|
1658
|
+
keyMarker,
|
|
1659
|
+
versionIdMarker,
|
|
1660
|
+
maxKeys: 1000
|
|
1661
|
+
});
|
|
1662
|
+
const versionsToDelete = [];
|
|
1663
|
+
if (versionsResult.versions) {
|
|
1664
|
+
for (const version of versionsResult.versions) {
|
|
1665
|
+
versionsToDelete.push({ Key: version.Key, VersionId: version.VersionId });
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
if (versionsResult.deleteMarkers) {
|
|
1669
|
+
for (const marker of versionsResult.deleteMarkers) {
|
|
1670
|
+
versionsToDelete.push({ Key: marker.Key, VersionId: marker.VersionId });
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
if (versionsToDelete.length > 0) {
|
|
1674
|
+
await this.deleteObjectVersions(bucket, versionsToDelete);
|
|
1675
|
+
deletedCount += versionsToDelete.length;
|
|
1676
|
+
}
|
|
1677
|
+
keyMarker = versionsResult.nextKeyMarker;
|
|
1678
|
+
versionIdMarker = versionsResult.nextVersionIdMarker;
|
|
1679
|
+
} while (keyMarker);
|
|
1680
|
+
} catch {}
|
|
1681
|
+
return { deletedCount };
|
|
1682
|
+
}
|
|
1683
|
+
async listObjectVersions(options) {
|
|
1684
|
+
const { bucket, prefix, keyMarker, versionIdMarker, maxKeys = 1000 } = options;
|
|
1685
|
+
const queryParams = {
|
|
1686
|
+
versions: "",
|
|
1687
|
+
"max-keys": maxKeys.toString()
|
|
1688
|
+
};
|
|
1689
|
+
if (prefix)
|
|
1690
|
+
queryParams.prefix = prefix;
|
|
1691
|
+
if (keyMarker)
|
|
1692
|
+
queryParams["key-marker"] = keyMarker;
|
|
1693
|
+
if (versionIdMarker)
|
|
1694
|
+
queryParams["version-id-marker"] = versionIdMarker;
|
|
1695
|
+
const result = await this.client.request({
|
|
1696
|
+
service: "s3",
|
|
1697
|
+
region: this.region,
|
|
1698
|
+
method: "GET",
|
|
1699
|
+
path: `/${bucket}`,
|
|
1700
|
+
queryParams
|
|
1701
|
+
});
|
|
1702
|
+
const versions = [];
|
|
1703
|
+
const deleteMarkers = [];
|
|
1704
|
+
if (result.Version) {
|
|
1705
|
+
const versionList = Array.isArray(result.Version) ? result.Version : [result.Version];
|
|
1706
|
+
for (const v of versionList) {
|
|
1707
|
+
versions.push({
|
|
1708
|
+
Key: v.Key,
|
|
1709
|
+
VersionId: v.VersionId,
|
|
1710
|
+
IsLatest: v.IsLatest === "true"
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
if (result.DeleteMarker) {
|
|
1715
|
+
const markerList = Array.isArray(result.DeleteMarker) ? result.DeleteMarker : [result.DeleteMarker];
|
|
1716
|
+
for (const m of markerList) {
|
|
1717
|
+
deleteMarkers.push({
|
|
1718
|
+
Key: m.Key,
|
|
1719
|
+
VersionId: m.VersionId,
|
|
1720
|
+
IsLatest: m.IsLatest === "true"
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return {
|
|
1725
|
+
versions,
|
|
1726
|
+
deleteMarkers,
|
|
1727
|
+
nextKeyMarker: result.NextKeyMarker,
|
|
1728
|
+
nextVersionIdMarker: result.NextVersionIdMarker
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
async deleteObjectVersions(bucket, objects) {
|
|
1732
|
+
const deleteXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1733
|
+
<Delete>
|
|
1734
|
+
<Quiet>true</Quiet>
|
|
1735
|
+
${objects.map((obj) => `<Object><Key>${obj.Key}</Key>${obj.VersionId ? `<VersionId>${obj.VersionId}</VersionId>` : ""}</Object>`).join(`
|
|
1736
|
+
`)}
|
|
1737
|
+
</Delete>`;
|
|
1738
|
+
const contentMd5 = crypto.createHash("md5").update(deleteXml).digest("base64");
|
|
1739
|
+
await this.client.request({
|
|
1740
|
+
service: "s3",
|
|
1741
|
+
region: this.region,
|
|
1742
|
+
method: "POST",
|
|
1743
|
+
path: `/${bucket}`,
|
|
1744
|
+
queryParams: { delete: "" },
|
|
1745
|
+
body: deleteXml,
|
|
1746
|
+
headers: {
|
|
1747
|
+
"Content-Type": "application/xml",
|
|
1748
|
+
"Content-MD5": contentMd5
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
export { resolveCredentials, S3Client };
|