@stacksjs/ts-cloud 0.1.2 → 0.1.5
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 +98 -13
- package/dist/aws/acm.d.ts +129 -0
- package/dist/aws/application-autoscaling.d.ts +282 -0
- package/dist/aws/bedrock.d.ts +2292 -0
- package/dist/aws/client.d.ts +79 -0
- package/dist/aws/cloudformation.d.ts +105 -0
- package/dist/aws/cloudfront.d.ts +265 -0
- package/dist/aws/cloudwatch-logs.d.ts +48 -0
- package/dist/aws/comprehend.d.ts +505 -0
- package/dist/aws/connect.d.ts +377 -0
- package/dist/aws/deploy-imap.d.ts +14 -0
- package/dist/aws/dynamodb.d.ts +176 -0
- package/dist/aws/ec2.d.ts +272 -0
- package/dist/aws/ecr.d.ts +149 -0
- package/dist/aws/ecs.d.ts +162 -0
- package/dist/aws/elasticache.d.ts +71 -0
- package/dist/aws/elbv2.d.ts +248 -0
- package/dist/aws/email.d.ts +175 -0
- package/dist/aws/eventbridge.d.ts +142 -0
- package/dist/aws/iam.d.ts +638 -0
- package/dist/aws/imap-server.d.ts +119 -0
- package/{src/aws/index.ts → dist/aws/index.d.ts} +62 -83
- package/{src/aws/kendra.ts → dist/aws/kendra.d.ts} +71 -386
- package/dist/aws/lambda.d.ts +232 -0
- package/dist/aws/opensearch.d.ts +87 -0
- package/dist/aws/personalize.d.ts +516 -0
- package/dist/aws/polly.d.ts +214 -0
- package/dist/aws/rds.d.ts +240 -0
- package/dist/aws/rekognition.d.ts +543 -0
- package/dist/aws/route53-domains.d.ts +113 -0
- package/dist/aws/route53.d.ts +215 -0
- package/dist/aws/s3.d.ts +212 -0
- package/dist/aws/scheduler.d.ts +140 -0
- package/dist/aws/secrets-manager.d.ts +170 -0
- package/dist/aws/ses.d.ts +288 -0
- package/dist/aws/setup-phone.d.ts +0 -0
- package/dist/aws/setup-sms.d.ts +115 -0
- package/dist/aws/sms.d.ts +304 -0
- package/dist/aws/smtp-server.d.ts +61 -0
- package/dist/aws/sns.d.ts +117 -0
- package/dist/aws/sqs.d.ts +65 -0
- package/dist/aws/ssm.d.ts +179 -0
- package/dist/aws/sts.d.ts +15 -0
- package/dist/aws/support.d.ts +104 -0
- package/dist/aws/test-imap.d.ts +0 -0
- package/dist/aws/textract.d.ts +403 -0
- package/dist/aws/transcribe.d.ts +60 -0
- package/dist/aws/translate.d.ts +358 -0
- package/dist/aws/voice.d.ts +219 -0
- package/dist/bin/cli.js +1724 -0
- package/dist/config.d.ts +7 -0
- package/dist/deploy/index.d.ts +2 -0
- package/dist/deploy/static-site-external-dns.d.ts +51 -0
- package/dist/deploy/static-site.d.ts +71 -0
- package/dist/dns/cloudflare.d.ts +52 -0
- package/dist/dns/godaddy.d.ts +38 -0
- package/dist/dns/index.d.ts +45 -0
- package/dist/dns/porkbun.d.ts +18 -0
- package/dist/dns/route53-adapter.d.ts +38 -0
- package/{src/dns/types.ts → dist/dns/types.d.ts} +26 -63
- package/dist/dns/validator.d.ts +78 -0
- package/dist/generators/index.d.ts +1 -0
- package/dist/generators/infrastructure.d.ts +30 -0
- package/{src/index.ts → dist/index.d.ts} +70 -93
- package/dist/index.js +7881 -0
- package/dist/push/apns.d.ts +60 -0
- package/dist/push/fcm.d.ts +117 -0
- package/dist/push/index.d.ts +14 -0
- package/dist/security/pre-deploy-scanner.d.ts +69 -0
- package/dist/ssl/acme-client.d.ts +67 -0
- package/dist/ssl/index.d.ts +2 -0
- package/dist/ssl/letsencrypt.d.ts +48 -0
- package/dist/types.d.ts +1 -0
- package/dist/utils/cli.d.ts +123 -0
- package/dist/validation/index.d.ts +1 -0
- package/dist/validation/template.d.ts +23 -0
- package/package.json +8 -8
- package/bin/cli.ts +0 -133
- package/bin/commands/analytics.ts +0 -328
- package/bin/commands/api.ts +0 -379
- package/bin/commands/assets.ts +0 -221
- package/bin/commands/audit.ts +0 -501
- package/bin/commands/backup.ts +0 -682
- package/bin/commands/cache.ts +0 -294
- package/bin/commands/cdn.ts +0 -281
- package/bin/commands/config.ts +0 -202
- package/bin/commands/container.ts +0 -105
- package/bin/commands/cost.ts +0 -208
- package/bin/commands/database.ts +0 -401
- package/bin/commands/deploy.ts +0 -674
- package/bin/commands/domain.ts +0 -397
- package/bin/commands/email.ts +0 -423
- package/bin/commands/environment.ts +0 -285
- package/bin/commands/events.ts +0 -424
- package/bin/commands/firewall.ts +0 -145
- package/bin/commands/function.ts +0 -116
- package/bin/commands/generate.ts +0 -280
- package/bin/commands/git.ts +0 -139
- package/bin/commands/iam.ts +0 -464
- package/bin/commands/index.ts +0 -48
- package/bin/commands/init.ts +0 -120
- package/bin/commands/logs.ts +0 -148
- package/bin/commands/network.ts +0 -579
- package/bin/commands/notify.ts +0 -489
- package/bin/commands/queue.ts +0 -407
- package/bin/commands/scheduler.ts +0 -370
- package/bin/commands/secrets.ts +0 -54
- package/bin/commands/server.ts +0 -629
- package/bin/commands/shared.ts +0 -97
- package/bin/commands/ssl.ts +0 -138
- package/bin/commands/stack.ts +0 -325
- package/bin/commands/status.ts +0 -385
- package/bin/commands/storage.ts +0 -450
- package/bin/commands/team.ts +0 -96
- package/bin/commands/tunnel.ts +0 -489
- package/bin/commands/utils.ts +0 -202
- package/build.ts +0 -15
- package/cloud +0 -2
- package/src/aws/acm.ts +0 -768
- package/src/aws/application-autoscaling.ts +0 -845
- package/src/aws/bedrock.ts +0 -4074
- package/src/aws/client.ts +0 -878
- package/src/aws/cloudformation.ts +0 -896
- package/src/aws/cloudfront.ts +0 -1531
- package/src/aws/cloudwatch-logs.ts +0 -154
- package/src/aws/comprehend.ts +0 -839
- package/src/aws/connect.ts +0 -1056
- package/src/aws/deploy-imap.ts +0 -384
- package/src/aws/dynamodb.ts +0 -340
- package/src/aws/ec2.ts +0 -1385
- package/src/aws/ecr.ts +0 -621
- package/src/aws/ecs.ts +0 -615
- package/src/aws/elasticache.ts +0 -301
- package/src/aws/elbv2.ts +0 -942
- package/src/aws/email.ts +0 -928
- package/src/aws/eventbridge.ts +0 -248
- package/src/aws/iam.ts +0 -1689
- package/src/aws/imap-server.ts +0 -2100
- package/src/aws/lambda.ts +0 -786
- package/src/aws/opensearch.ts +0 -158
- package/src/aws/personalize.ts +0 -977
- package/src/aws/polly.ts +0 -559
- package/src/aws/rds.ts +0 -888
- package/src/aws/rekognition.ts +0 -846
- package/src/aws/route53-domains.ts +0 -359
- package/src/aws/route53.ts +0 -1046
- package/src/aws/s3.ts +0 -2318
- package/src/aws/scheduler.ts +0 -571
- package/src/aws/secrets-manager.ts +0 -769
- package/src/aws/ses.ts +0 -1081
- package/src/aws/setup-phone.ts +0 -104
- package/src/aws/setup-sms.ts +0 -580
- package/src/aws/sms.ts +0 -1735
- package/src/aws/smtp-server.ts +0 -531
- package/src/aws/sns.ts +0 -758
- package/src/aws/sqs.ts +0 -382
- package/src/aws/ssm.ts +0 -807
- package/src/aws/sts.ts +0 -92
- package/src/aws/support.ts +0 -391
- package/src/aws/test-imap.ts +0 -86
- package/src/aws/textract.ts +0 -780
- package/src/aws/transcribe.ts +0 -108
- package/src/aws/translate.ts +0 -641
- package/src/aws/voice.ts +0 -1379
- package/src/config.ts +0 -35
- package/src/deploy/index.ts +0 -7
- package/src/deploy/static-site-external-dns.ts +0 -906
- package/src/deploy/static-site.ts +0 -1125
- package/src/dns/godaddy.ts +0 -412
- package/src/dns/index.ts +0 -183
- package/src/dns/porkbun.ts +0 -362
- package/src/dns/route53-adapter.ts +0 -414
- package/src/dns/validator.ts +0 -369
- package/src/generators/index.ts +0 -5
- package/src/generators/infrastructure.ts +0 -1660
- package/src/push/apns.ts +0 -452
- package/src/push/fcm.ts +0 -506
- package/src/push/index.ts +0 -58
- package/src/ssl/acme-client.ts +0 -478
- package/src/ssl/index.ts +0 -7
- package/src/ssl/letsencrypt.ts +0 -747
- package/src/types.ts +0 -2
- package/src/utils/cli.ts +0 -398
- package/src/validation/index.ts +0 -5
- package/src/validation/template.ts +0 -405
- package/test/index.test.ts +0 -128
- package/tsconfig.json +0 -18
package/src/aws/s3.ts
DELETED
|
@@ -1,2318 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AWS S3 Operations
|
|
3
|
-
* Direct API calls without AWS CLI dependency
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import * as crypto from 'node:crypto'
|
|
7
|
-
import { AWSClient } from './client'
|
|
8
|
-
import { readdir, stat } from 'node:fs/promises'
|
|
9
|
-
import { join } from 'node:path'
|
|
10
|
-
import { readFileSync } from 'node:fs'
|
|
11
|
-
|
|
12
|
-
export interface S3SyncOptions {
|
|
13
|
-
source: string
|
|
14
|
-
bucket: string
|
|
15
|
-
prefix?: string
|
|
16
|
-
delete?: boolean
|
|
17
|
-
acl?: 'private' | 'public-read' | 'public-read-write' | 'authenticated-read'
|
|
18
|
-
cacheControl?: string
|
|
19
|
-
contentType?: string
|
|
20
|
-
metadata?: Record<string, string>
|
|
21
|
-
exclude?: string[]
|
|
22
|
-
include?: string[]
|
|
23
|
-
dryRun?: boolean
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface S3CopyOptions {
|
|
27
|
-
source: string
|
|
28
|
-
bucket: string
|
|
29
|
-
key: string
|
|
30
|
-
acl?: 'private' | 'public-read' | 'public-read-write' | 'authenticated-read'
|
|
31
|
-
cacheControl?: string
|
|
32
|
-
contentType?: string
|
|
33
|
-
metadata?: Record<string, string>
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface S3ListOptions {
|
|
37
|
-
bucket: string
|
|
38
|
-
prefix?: string
|
|
39
|
-
maxKeys?: number
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface S3Object {
|
|
43
|
-
Key: string
|
|
44
|
-
LastModified: string
|
|
45
|
-
Size: number
|
|
46
|
-
ETag?: string
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* S3 client using direct API calls
|
|
51
|
-
*/
|
|
52
|
-
export class S3Client {
|
|
53
|
-
private client: AWSClient
|
|
54
|
-
private region: string
|
|
55
|
-
|
|
56
|
-
constructor(region: string = 'us-east-1', profile?: string) {
|
|
57
|
-
this.region = region
|
|
58
|
-
this.client = new AWSClient()
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Get AWS credentials from environment or credentials file
|
|
63
|
-
*/
|
|
64
|
-
private getCredentials(): { accessKeyId: string, secretAccessKey: string, sessionToken?: string } {
|
|
65
|
-
// 1. Check environment variables first
|
|
66
|
-
const envAccessKey = process.env.AWS_ACCESS_KEY_ID
|
|
67
|
-
const envSecretKey = process.env.AWS_SECRET_ACCESS_KEY
|
|
68
|
-
const envSessionToken = process.env.AWS_SESSION_TOKEN
|
|
69
|
-
|
|
70
|
-
if (envAccessKey && envSecretKey) {
|
|
71
|
-
return { accessKeyId: envAccessKey, secretAccessKey: envSecretKey, sessionToken: envSessionToken }
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// 2. Try to load from ~/.aws/credentials file
|
|
75
|
-
const fileCreds = this.loadCredentialsFromFile()
|
|
76
|
-
if (fileCreds) {
|
|
77
|
-
return fileCreds
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
throw new Error('AWS credentials not found. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables, or configure ~/.aws/credentials.')
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Load credentials from ~/.aws/credentials file
|
|
85
|
-
*/
|
|
86
|
-
private loadCredentialsFromFile(): { accessKeyId: string, secretAccessKey: string, sessionToken?: string } | null {
|
|
87
|
-
try {
|
|
88
|
-
const { existsSync, readFileSync } = require('node:fs')
|
|
89
|
-
const { homedir } = require('node:os')
|
|
90
|
-
const { join: pathJoin } = require('node:path')
|
|
91
|
-
|
|
92
|
-
const profile = process.env.AWS_PROFILE || 'default'
|
|
93
|
-
const credentialsPath = process.env.AWS_SHARED_CREDENTIALS_FILE || pathJoin(homedir(), '.aws', 'credentials')
|
|
94
|
-
|
|
95
|
-
if (!existsSync(credentialsPath)) {
|
|
96
|
-
return null
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const content = readFileSync(credentialsPath, 'utf-8')
|
|
100
|
-
const lines = content.split('\n')
|
|
101
|
-
let currentProfile = ''
|
|
102
|
-
let accessKeyId = ''
|
|
103
|
-
let secretAccessKey = ''
|
|
104
|
-
let sessionToken: string | undefined
|
|
105
|
-
|
|
106
|
-
for (const line of lines) {
|
|
107
|
-
const trimmed = line.trim()
|
|
108
|
-
if (!trimmed || trimmed.startsWith('#')) continue
|
|
109
|
-
|
|
110
|
-
const profileMatch = trimmed.match(/^\[([^\]]+)\]$/)
|
|
111
|
-
if (profileMatch) {
|
|
112
|
-
currentProfile = profileMatch[1]
|
|
113
|
-
continue
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (currentProfile === profile) {
|
|
117
|
-
const [key, ...valueParts] = trimmed.split('=')
|
|
118
|
-
const value = valueParts.join('=').trim()
|
|
119
|
-
|
|
120
|
-
if (key.trim() === 'aws_access_key_id') {
|
|
121
|
-
accessKeyId = value
|
|
122
|
-
} else if (key.trim() === 'aws_secret_access_key') {
|
|
123
|
-
secretAccessKey = value
|
|
124
|
-
} else if (key.trim() === 'aws_session_token') {
|
|
125
|
-
sessionToken = value
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (accessKeyId && secretAccessKey) {
|
|
131
|
-
return { accessKeyId, secretAccessKey, sessionToken }
|
|
132
|
-
}
|
|
133
|
-
} catch {
|
|
134
|
-
// Failed to read credentials file
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return null
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* List all S3 buckets in the account
|
|
142
|
-
*/
|
|
143
|
-
async listBuckets(): Promise<{ Buckets: Array<{ Name: string, CreationDate?: string }> }> {
|
|
144
|
-
const result = await this.client.request({
|
|
145
|
-
service: 's3',
|
|
146
|
-
region: this.region,
|
|
147
|
-
method: 'GET',
|
|
148
|
-
path: '/',
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
const buckets: Array<{ Name: string, CreationDate?: string }> = []
|
|
152
|
-
const bucketList = result?.ListAllMyBucketsResult?.Buckets?.Bucket
|
|
153
|
-
|
|
154
|
-
if (bucketList) {
|
|
155
|
-
const list = Array.isArray(bucketList) ? bucketList : [bucketList]
|
|
156
|
-
for (const b of list) {
|
|
157
|
-
buckets.push({
|
|
158
|
-
Name: b.Name,
|
|
159
|
-
CreationDate: b.CreationDate,
|
|
160
|
-
})
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return { Buckets: buckets }
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Create an S3 bucket
|
|
169
|
-
*/
|
|
170
|
-
async createBucket(bucket: string, options?: { acl?: string }): Promise<void> {
|
|
171
|
-
const headers: Record<string, string> = {}
|
|
172
|
-
if (options?.acl) {
|
|
173
|
-
headers['x-amz-acl'] = options.acl
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// For us-east-1, don't include LocationConstraint
|
|
177
|
-
// For other regions, include it in the body
|
|
178
|
-
let body: string | undefined
|
|
179
|
-
if (this.region !== 'us-east-1') {
|
|
180
|
-
body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
181
|
-
<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
182
|
-
<LocationConstraint>${this.region}</LocationConstraint>
|
|
183
|
-
</CreateBucketConfiguration>`
|
|
184
|
-
headers['Content-Type'] = 'application/xml'
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
await this.client.request({
|
|
188
|
-
service: 's3',
|
|
189
|
-
region: this.region,
|
|
190
|
-
method: 'PUT',
|
|
191
|
-
path: `/${bucket}`,
|
|
192
|
-
headers,
|
|
193
|
-
body,
|
|
194
|
-
})
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Delete an S3 bucket (must be empty)
|
|
199
|
-
*/
|
|
200
|
-
async deleteBucket(bucket: string): Promise<void> {
|
|
201
|
-
await this.client.request({
|
|
202
|
-
service: 's3',
|
|
203
|
-
region: this.region,
|
|
204
|
-
method: 'DELETE',
|
|
205
|
-
path: `/${bucket}`,
|
|
206
|
-
})
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Empty and delete an S3 bucket
|
|
211
|
-
*/
|
|
212
|
-
async emptyAndDeleteBucket(bucket: string): Promise<void> {
|
|
213
|
-
// First list and delete all objects
|
|
214
|
-
let hasMore = true
|
|
215
|
-
while (hasMore) {
|
|
216
|
-
const objects = await this.listAllObjects({ bucket })
|
|
217
|
-
if (objects.length === 0) {
|
|
218
|
-
hasMore = false
|
|
219
|
-
break
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Delete in batches of 1000 (S3 limit)
|
|
223
|
-
const keys = objects.map(obj => obj.Key)
|
|
224
|
-
for (let i = 0; i < keys.length; i += 1000) {
|
|
225
|
-
const batch = keys.slice(i, i + 1000)
|
|
226
|
-
await this.deleteObjects(bucket, batch)
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Now delete the bucket
|
|
231
|
-
await this.deleteBucket(bucket)
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* List all objects in a bucket (handles pagination)
|
|
236
|
-
*/
|
|
237
|
-
async listAllObjects(options: S3ListOptions): Promise<S3Object[]> {
|
|
238
|
-
const allObjects: S3Object[] = []
|
|
239
|
-
let continuationToken: string | undefined
|
|
240
|
-
|
|
241
|
-
do {
|
|
242
|
-
const params: Record<string, any> = {
|
|
243
|
-
'list-type': '2',
|
|
244
|
-
'max-keys': '1000',
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (options.prefix) {
|
|
248
|
-
params.prefix = options.prefix
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (continuationToken) {
|
|
252
|
-
params['continuation-token'] = continuationToken
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const result = await this.client.request({
|
|
256
|
-
service: 's3',
|
|
257
|
-
region: this.region,
|
|
258
|
-
method: 'GET',
|
|
259
|
-
path: `/${options.bucket}`,
|
|
260
|
-
queryParams: params,
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
const contents = result?.ListBucketResult?.Contents
|
|
264
|
-
if (contents) {
|
|
265
|
-
const list = Array.isArray(contents) ? contents : [contents]
|
|
266
|
-
for (const obj of list) {
|
|
267
|
-
allObjects.push({
|
|
268
|
-
Key: obj.Key,
|
|
269
|
-
LastModified: obj.LastModified || '',
|
|
270
|
-
Size: Number.parseInt(obj.Size || '0'),
|
|
271
|
-
ETag: obj.ETag,
|
|
272
|
-
})
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Check for more results
|
|
277
|
-
const isTruncated = result?.ListBucketResult?.IsTruncated
|
|
278
|
-
continuationToken = isTruncated === 'true' || isTruncated === true
|
|
279
|
-
? result?.ListBucketResult?.NextContinuationToken
|
|
280
|
-
: undefined
|
|
281
|
-
|
|
282
|
-
} while (continuationToken)
|
|
283
|
-
|
|
284
|
-
return allObjects
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* List objects in S3 bucket
|
|
289
|
-
*/
|
|
290
|
-
async list(options: S3ListOptions): Promise<S3Object[]> {
|
|
291
|
-
// Use path-style URL without query params for simpler signing
|
|
292
|
-
const result = await this.client.request({
|
|
293
|
-
service: 's3',
|
|
294
|
-
region: this.region,
|
|
295
|
-
method: 'GET',
|
|
296
|
-
path: `/${options.bucket}`,
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
// Parse S3 XML response
|
|
300
|
-
const objects: S3Object[] = []
|
|
301
|
-
|
|
302
|
-
// Handle ListBucketResult structure from XML parsing
|
|
303
|
-
const contents = result?.ListBucketResult?.Contents
|
|
304
|
-
if (contents) {
|
|
305
|
-
const items = Array.isArray(contents) ? contents : [contents]
|
|
306
|
-
for (const item of items) {
|
|
307
|
-
// Filter by prefix if specified
|
|
308
|
-
if (options.prefix && !item.Key?.startsWith(options.prefix)) {
|
|
309
|
-
continue
|
|
310
|
-
}
|
|
311
|
-
objects.push({
|
|
312
|
-
Key: item.Key || '',
|
|
313
|
-
LastModified: item.LastModified || '',
|
|
314
|
-
Size: Number.parseInt(item.Size || '0'),
|
|
315
|
-
ETag: item.ETag,
|
|
316
|
-
})
|
|
317
|
-
// Respect maxKeys
|
|
318
|
-
if (options.maxKeys && objects.length >= options.maxKeys) {
|
|
319
|
-
break
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return objects
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Put object to S3 bucket
|
|
329
|
-
*/
|
|
330
|
-
async putObject(options: {
|
|
331
|
-
bucket: string
|
|
332
|
-
key: string
|
|
333
|
-
body: string | Buffer | Uint8Array
|
|
334
|
-
acl?: string
|
|
335
|
-
cacheControl?: string
|
|
336
|
-
contentType?: string
|
|
337
|
-
metadata?: Record<string, string>
|
|
338
|
-
}): Promise<void> {
|
|
339
|
-
const headers: Record<string, string> = {}
|
|
340
|
-
|
|
341
|
-
if (options.acl) {
|
|
342
|
-
headers['x-amz-acl'] = options.acl
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (options.cacheControl) {
|
|
346
|
-
headers['Cache-Control'] = options.cacheControl
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (options.contentType) {
|
|
350
|
-
headers['Content-Type'] = options.contentType
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (options.metadata) {
|
|
354
|
-
for (const [key, value] of Object.entries(options.metadata)) {
|
|
355
|
-
headers[`x-amz-meta-${key}`] = value
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Normalize body to Buffer for binary data
|
|
360
|
-
// Uint8Array needs to be converted to Buffer for proper handling
|
|
361
|
-
const normalizedBody = options.body instanceof Uint8Array && !Buffer.isBuffer(options.body)
|
|
362
|
-
? Buffer.from(options.body)
|
|
363
|
-
: options.body
|
|
364
|
-
|
|
365
|
-
// For binary data (Buffer/Uint8Array), use direct binary upload
|
|
366
|
-
if (Buffer.isBuffer(normalizedBody) || (normalizedBody as any) instanceof Uint8Array) {
|
|
367
|
-
const binaryBody = Buffer.isBuffer(normalizedBody) ? normalizedBody : Buffer.from(normalizedBody)
|
|
368
|
-
// Actually, for S3 we need to send raw binary, not base64
|
|
369
|
-
// Let's use Bun's fetch which handles Buffer natively
|
|
370
|
-
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials()
|
|
371
|
-
const host = `${options.bucket}.s3.${this.region}.amazonaws.com`
|
|
372
|
-
const url = `https://${host}/${options.key}`
|
|
373
|
-
|
|
374
|
-
const now = new Date()
|
|
375
|
-
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
|
|
376
|
-
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '')
|
|
377
|
-
|
|
378
|
-
const payloadHash = crypto.createHash('sha256').update(binaryBody).digest('hex')
|
|
379
|
-
|
|
380
|
-
const requestHeaders: Record<string, string> = {
|
|
381
|
-
'host': host,
|
|
382
|
-
'x-amz-date': amzDate,
|
|
383
|
-
'x-amz-content-sha256': payloadHash,
|
|
384
|
-
...headers,
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (sessionToken) {
|
|
388
|
-
requestHeaders['x-amz-security-token'] = sessionToken
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Create canonical request
|
|
392
|
-
const canonicalHeaders = Object.keys(requestHeaders)
|
|
393
|
-
.sort()
|
|
394
|
-
.map(key => `${key.toLowerCase()}:${requestHeaders[key].trim()}\n`)
|
|
395
|
-
.join('')
|
|
396
|
-
|
|
397
|
-
const signedHeaders = Object.keys(requestHeaders)
|
|
398
|
-
.sort()
|
|
399
|
-
.map(key => key.toLowerCase())
|
|
400
|
-
.join(';')
|
|
401
|
-
|
|
402
|
-
const canonicalRequest = [
|
|
403
|
-
'PUT',
|
|
404
|
-
`/${options.key}`,
|
|
405
|
-
'',
|
|
406
|
-
canonicalHeaders,
|
|
407
|
-
signedHeaders,
|
|
408
|
-
payloadHash,
|
|
409
|
-
].join('\n')
|
|
410
|
-
|
|
411
|
-
// Create string to sign
|
|
412
|
-
const algorithm = 'AWS4-HMAC-SHA256'
|
|
413
|
-
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`
|
|
414
|
-
const stringToSign = [
|
|
415
|
-
algorithm,
|
|
416
|
-
amzDate,
|
|
417
|
-
credentialScope,
|
|
418
|
-
crypto.createHash('sha256').update(canonicalRequest).digest('hex'),
|
|
419
|
-
].join('\n')
|
|
420
|
-
|
|
421
|
-
// Calculate signature
|
|
422
|
-
const kDate = crypto.createHmac('sha256', `AWS4${secretAccessKey}`).update(dateStamp).digest()
|
|
423
|
-
const kRegion = crypto.createHmac('sha256', kDate).update(this.region).digest()
|
|
424
|
-
const kService = crypto.createHmac('sha256', kRegion).update('s3').digest()
|
|
425
|
-
const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
|
|
426
|
-
const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex')
|
|
427
|
-
|
|
428
|
-
const authorizationHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
|
|
429
|
-
|
|
430
|
-
const response = await fetch(url, {
|
|
431
|
-
method: 'PUT',
|
|
432
|
-
headers: {
|
|
433
|
-
...requestHeaders,
|
|
434
|
-
'Authorization': authorizationHeader,
|
|
435
|
-
},
|
|
436
|
-
body: binaryBody,
|
|
437
|
-
})
|
|
438
|
-
|
|
439
|
-
if (!response.ok) {
|
|
440
|
-
const errorText = await response.text()
|
|
441
|
-
throw new Error(`S3 PUT failed: ${response.status} ${errorText}`)
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
await this.client.request({
|
|
448
|
-
service: 's3',
|
|
449
|
-
region: this.region,
|
|
450
|
-
method: 'PUT',
|
|
451
|
-
path: `/${options.key}`,
|
|
452
|
-
bucket: options.bucket, // Use virtual-hosted style
|
|
453
|
-
headers,
|
|
454
|
-
body: options.body as string,
|
|
455
|
-
})
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Get object from S3 bucket
|
|
460
|
-
* Returns raw content as string (not parsed as XML)
|
|
461
|
-
*/
|
|
462
|
-
async getObject(bucket: string, key: string): Promise<string> {
|
|
463
|
-
const result = await this.client.request({
|
|
464
|
-
service: 's3',
|
|
465
|
-
region: this.region,
|
|
466
|
-
method: 'GET',
|
|
467
|
-
path: `/${bucket}/${key}`,
|
|
468
|
-
rawResponse: true,
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
return result
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Copy object within S3 (server-side copy)
|
|
476
|
-
*/
|
|
477
|
-
async copyObject(options: {
|
|
478
|
-
sourceBucket: string
|
|
479
|
-
sourceKey: string
|
|
480
|
-
destinationBucket: string
|
|
481
|
-
destinationKey: string
|
|
482
|
-
contentType?: string
|
|
483
|
-
metadata?: Record<string, string>
|
|
484
|
-
metadataDirective?: 'COPY' | 'REPLACE'
|
|
485
|
-
}): Promise<void> {
|
|
486
|
-
const headers: Record<string, string> = {
|
|
487
|
-
'x-amz-copy-source': `/${options.sourceBucket}/${options.sourceKey}`,
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (options.metadataDirective) {
|
|
491
|
-
headers['x-amz-metadata-directive'] = options.metadataDirective
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (options.contentType) {
|
|
495
|
-
headers['Content-Type'] = options.contentType
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (options.metadata) {
|
|
499
|
-
for (const [key, value] of Object.entries(options.metadata)) {
|
|
500
|
-
headers[`x-amz-meta-${key}`] = value
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
await this.client.request({
|
|
505
|
-
service: 's3',
|
|
506
|
-
region: this.region,
|
|
507
|
-
method: 'PUT',
|
|
508
|
-
path: `/${options.destinationBucket}/${options.destinationKey}`,
|
|
509
|
-
headers,
|
|
510
|
-
})
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Delete object from S3
|
|
515
|
-
*/
|
|
516
|
-
async deleteObject(bucket: string, key: string): Promise<void> {
|
|
517
|
-
await this.client.request({
|
|
518
|
-
service: 's3',
|
|
519
|
-
region: this.region,
|
|
520
|
-
method: 'DELETE',
|
|
521
|
-
path: `/${bucket}/${key}`,
|
|
522
|
-
})
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Delete multiple objects from S3
|
|
527
|
-
*/
|
|
528
|
-
async deleteObjects(bucket: string, keys: string[]): Promise<void> {
|
|
529
|
-
const deleteXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
530
|
-
<Delete>
|
|
531
|
-
${keys.map(key => `<Object><Key>${key}</Key></Object>`).join('\n ')}
|
|
532
|
-
</Delete>`
|
|
533
|
-
|
|
534
|
-
// S3 DeleteObjects requires Content-MD5 header
|
|
535
|
-
const contentMd5 = crypto.createHash('md5').update(deleteXml).digest('base64')
|
|
536
|
-
|
|
537
|
-
await this.client.request({
|
|
538
|
-
service: 's3',
|
|
539
|
-
region: this.region,
|
|
540
|
-
method: 'POST',
|
|
541
|
-
path: `/${bucket}`,
|
|
542
|
-
queryParams: { delete: '' },
|
|
543
|
-
body: deleteXml,
|
|
544
|
-
headers: {
|
|
545
|
-
'Content-Type': 'application/xml',
|
|
546
|
-
'Content-MD5': contentMd5,
|
|
547
|
-
},
|
|
548
|
-
})
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Check if bucket exists
|
|
553
|
-
*/
|
|
554
|
-
async bucketExists(bucket: string): Promise<boolean> {
|
|
555
|
-
try {
|
|
556
|
-
await this.client.request({
|
|
557
|
-
service: 's3',
|
|
558
|
-
region: this.region,
|
|
559
|
-
method: 'HEAD',
|
|
560
|
-
path: `/${bucket}`,
|
|
561
|
-
})
|
|
562
|
-
return true
|
|
563
|
-
}
|
|
564
|
-
catch {
|
|
565
|
-
return false
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Copy file to S3
|
|
571
|
-
*/
|
|
572
|
-
async copy(options: S3CopyOptions): Promise<void> {
|
|
573
|
-
// Read file and upload
|
|
574
|
-
const fileContent = readFileSync(options.source)
|
|
575
|
-
|
|
576
|
-
await this.putObject({
|
|
577
|
-
bucket: options.bucket,
|
|
578
|
-
key: options.key,
|
|
579
|
-
body: fileContent,
|
|
580
|
-
acl: options.acl,
|
|
581
|
-
cacheControl: options.cacheControl,
|
|
582
|
-
contentType: options.contentType,
|
|
583
|
-
metadata: options.metadata,
|
|
584
|
-
})
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* Sync local directory to S3 bucket
|
|
589
|
-
* Note: This is a simplified version. For production use, implement proper sync logic
|
|
590
|
-
*/
|
|
591
|
-
async sync(options: S3SyncOptions): Promise<void> {
|
|
592
|
-
const files = await this.listFilesRecursive(options.source)
|
|
593
|
-
|
|
594
|
-
for (const file of files) {
|
|
595
|
-
// Skip excluded files
|
|
596
|
-
if (options.exclude && options.exclude.some(pattern => file.includes(pattern))) {
|
|
597
|
-
continue
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// Check included files
|
|
601
|
-
if (options.include && !options.include.some(pattern => file.includes(pattern))) {
|
|
602
|
-
continue
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
const relativePath = file.substring(options.source.length + 1)
|
|
606
|
-
const s3Key = options.prefix ? `${options.prefix}/${relativePath}` : relativePath
|
|
607
|
-
|
|
608
|
-
if (!options.dryRun) {
|
|
609
|
-
const fileContent = readFileSync(file)
|
|
610
|
-
|
|
611
|
-
await this.putObject({
|
|
612
|
-
bucket: options.bucket,
|
|
613
|
-
key: s3Key,
|
|
614
|
-
body: fileContent,
|
|
615
|
-
acl: options.acl,
|
|
616
|
-
cacheControl: options.cacheControl,
|
|
617
|
-
contentType: options.contentType,
|
|
618
|
-
metadata: options.metadata,
|
|
619
|
-
})
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Delete object from S3 (alias for deleteObject)
|
|
626
|
-
*/
|
|
627
|
-
async delete(bucket: string, key: string): Promise<void> {
|
|
628
|
-
await this.deleteObject(bucket, key)
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Delete all objects in a prefix
|
|
633
|
-
*/
|
|
634
|
-
async deletePrefix(bucket: string, prefix: string): Promise<void> {
|
|
635
|
-
const objects = await this.list({ bucket, prefix })
|
|
636
|
-
const keys = objects.map(obj => obj.Key)
|
|
637
|
-
|
|
638
|
-
if (keys.length > 0) {
|
|
639
|
-
await this.deleteObjects(bucket, keys)
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Get bucket size
|
|
645
|
-
*/
|
|
646
|
-
async getBucketSize(bucket: string, prefix?: string): Promise<number> {
|
|
647
|
-
const objects = await this.list({ bucket, prefix })
|
|
648
|
-
return objects.reduce((total, obj) => total + obj.Size, 0)
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* List files recursively in a directory
|
|
653
|
-
*/
|
|
654
|
-
private async listFilesRecursive(dir: string): Promise<string[]> {
|
|
655
|
-
const files: string[] = []
|
|
656
|
-
const entries = await readdir(dir, { withFileTypes: true })
|
|
657
|
-
|
|
658
|
-
for (const entry of entries) {
|
|
659
|
-
const fullPath = join(dir, entry.name)
|
|
660
|
-
|
|
661
|
-
if (entry.isDirectory()) {
|
|
662
|
-
const subFiles = await this.listFilesRecursive(fullPath)
|
|
663
|
-
files.push(...subFiles)
|
|
664
|
-
}
|
|
665
|
-
else {
|
|
666
|
-
files.push(fullPath)
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
return files
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Put bucket policy for an S3 bucket
|
|
675
|
-
* Uses path-style URLs to avoid redirect issues
|
|
676
|
-
*/
|
|
677
|
-
async putBucketPolicy(bucket: string, policy: object | string): Promise<void> {
|
|
678
|
-
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials()
|
|
679
|
-
const host = `s3.${this.region}.amazonaws.com`
|
|
680
|
-
const policyString = typeof policy === 'string' ? policy : JSON.stringify(policy)
|
|
681
|
-
|
|
682
|
-
const now = new Date()
|
|
683
|
-
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
|
|
684
|
-
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '')
|
|
685
|
-
|
|
686
|
-
const payloadHash = crypto.createHash('sha256').update(policyString).digest('hex')
|
|
687
|
-
|
|
688
|
-
// Use path-style URL: s3.region.amazonaws.com/bucket?policy
|
|
689
|
-
const canonicalUri = '/' + bucket
|
|
690
|
-
const canonicalQuerystring = 'policy='
|
|
691
|
-
|
|
692
|
-
const requestHeaders: Record<string, string> = {
|
|
693
|
-
'host': host,
|
|
694
|
-
'x-amz-date': amzDate,
|
|
695
|
-
'x-amz-content-sha256': payloadHash,
|
|
696
|
-
'content-type': 'application/json',
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
if (sessionToken) {
|
|
700
|
-
requestHeaders['x-amz-security-token'] = sessionToken
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const canonicalHeaders = Object.keys(requestHeaders)
|
|
704
|
-
.sort()
|
|
705
|
-
.map(key => `${key.toLowerCase()}:${requestHeaders[key].trim()}\n`)
|
|
706
|
-
.join('')
|
|
707
|
-
|
|
708
|
-
const signedHeaders = Object.keys(requestHeaders)
|
|
709
|
-
.sort()
|
|
710
|
-
.map(key => key.toLowerCase())
|
|
711
|
-
.join(';')
|
|
712
|
-
|
|
713
|
-
const canonicalRequest = [
|
|
714
|
-
'PUT',
|
|
715
|
-
canonicalUri,
|
|
716
|
-
canonicalQuerystring,
|
|
717
|
-
canonicalHeaders,
|
|
718
|
-
signedHeaders,
|
|
719
|
-
payloadHash,
|
|
720
|
-
].join('\n')
|
|
721
|
-
|
|
722
|
-
const algorithm = 'AWS4-HMAC-SHA256'
|
|
723
|
-
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`
|
|
724
|
-
const stringToSign = [
|
|
725
|
-
algorithm,
|
|
726
|
-
amzDate,
|
|
727
|
-
credentialScope,
|
|
728
|
-
crypto.createHash('sha256').update(canonicalRequest).digest('hex'),
|
|
729
|
-
].join('\n')
|
|
730
|
-
|
|
731
|
-
const kDate = crypto.createHmac('sha256', 'AWS4' + secretAccessKey).update(dateStamp).digest()
|
|
732
|
-
const kRegion = crypto.createHmac('sha256', kDate).update(this.region).digest()
|
|
733
|
-
const kService = crypto.createHmac('sha256', kRegion).update('s3').digest()
|
|
734
|
-
const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
|
|
735
|
-
const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex')
|
|
736
|
-
|
|
737
|
-
const authHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
|
|
738
|
-
|
|
739
|
-
const url = `https://${host}${canonicalUri}?${canonicalQuerystring}`
|
|
740
|
-
|
|
741
|
-
const response = await fetch(url, {
|
|
742
|
-
method: 'PUT',
|
|
743
|
-
headers: {
|
|
744
|
-
...requestHeaders,
|
|
745
|
-
'Authorization': authHeader,
|
|
746
|
-
},
|
|
747
|
-
body: policyString,
|
|
748
|
-
})
|
|
749
|
-
|
|
750
|
-
if (!response.ok) {
|
|
751
|
-
const text = await response.text()
|
|
752
|
-
throw new Error(`Failed to put bucket policy: ${response.status} ${text}`)
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
/**
|
|
757
|
-
* Get bucket policy for an S3 bucket
|
|
758
|
-
* Uses path-style URLs to avoid redirect issues
|
|
759
|
-
*/
|
|
760
|
-
async getBucketPolicy(bucket: string): Promise<object | null> {
|
|
761
|
-
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials()
|
|
762
|
-
const host = `s3.${this.region}.amazonaws.com`
|
|
763
|
-
|
|
764
|
-
const now = new Date()
|
|
765
|
-
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
|
|
766
|
-
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '')
|
|
767
|
-
|
|
768
|
-
const payloadHash = crypto.createHash('sha256').update('').digest('hex')
|
|
769
|
-
|
|
770
|
-
// Use path-style URL: s3.region.amazonaws.com/bucket?policy
|
|
771
|
-
const canonicalUri = '/' + bucket
|
|
772
|
-
const canonicalQuerystring = 'policy='
|
|
773
|
-
|
|
774
|
-
const requestHeaders: Record<string, string> = {
|
|
775
|
-
'host': host,
|
|
776
|
-
'x-amz-date': amzDate,
|
|
777
|
-
'x-amz-content-sha256': payloadHash,
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
if (sessionToken) {
|
|
781
|
-
requestHeaders['x-amz-security-token'] = sessionToken
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
const canonicalHeaders = Object.keys(requestHeaders)
|
|
785
|
-
.sort()
|
|
786
|
-
.map(key => `${key.toLowerCase()}:${requestHeaders[key].trim()}\n`)
|
|
787
|
-
.join('')
|
|
788
|
-
|
|
789
|
-
const signedHeaders = Object.keys(requestHeaders)
|
|
790
|
-
.sort()
|
|
791
|
-
.map(key => key.toLowerCase())
|
|
792
|
-
.join(';')
|
|
793
|
-
|
|
794
|
-
const canonicalRequest = [
|
|
795
|
-
'GET',
|
|
796
|
-
canonicalUri,
|
|
797
|
-
canonicalQuerystring,
|
|
798
|
-
canonicalHeaders,
|
|
799
|
-
signedHeaders,
|
|
800
|
-
payloadHash,
|
|
801
|
-
].join('\n')
|
|
802
|
-
|
|
803
|
-
const algorithm = 'AWS4-HMAC-SHA256'
|
|
804
|
-
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`
|
|
805
|
-
const stringToSign = [
|
|
806
|
-
algorithm,
|
|
807
|
-
amzDate,
|
|
808
|
-
credentialScope,
|
|
809
|
-
crypto.createHash('sha256').update(canonicalRequest).digest('hex'),
|
|
810
|
-
].join('\n')
|
|
811
|
-
|
|
812
|
-
const kDate = crypto.createHmac('sha256', 'AWS4' + secretAccessKey).update(dateStamp).digest()
|
|
813
|
-
const kRegion = crypto.createHmac('sha256', kDate).update(this.region).digest()
|
|
814
|
-
const kService = crypto.createHmac('sha256', kRegion).update('s3').digest()
|
|
815
|
-
const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
|
|
816
|
-
const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex')
|
|
817
|
-
|
|
818
|
-
const authHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
|
|
819
|
-
|
|
820
|
-
const url = `https://${host}${canonicalUri}?${canonicalQuerystring}`
|
|
821
|
-
|
|
822
|
-
const response = await fetch(url, {
|
|
823
|
-
method: 'GET',
|
|
824
|
-
headers: {
|
|
825
|
-
...requestHeaders,
|
|
826
|
-
'Authorization': authHeader,
|
|
827
|
-
},
|
|
828
|
-
})
|
|
829
|
-
|
|
830
|
-
if (response.status === 404) {
|
|
831
|
-
return null
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
if (!response.ok) {
|
|
835
|
-
const text = await response.text()
|
|
836
|
-
throw new Error(`Failed to get bucket policy: ${response.status} ${text}`)
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
const text = await response.text()
|
|
840
|
-
return JSON.parse(text)
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
/**
|
|
844
|
-
* Delete bucket policy
|
|
845
|
-
*/
|
|
846
|
-
async deleteBucketPolicy(bucket: string): Promise<void> {
|
|
847
|
-
await this.client.request({
|
|
848
|
-
service: 's3',
|
|
849
|
-
region: this.region,
|
|
850
|
-
method: 'DELETE',
|
|
851
|
-
path: `/${bucket}`,
|
|
852
|
-
queryParams: { policy: '' },
|
|
853
|
-
})
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
/**
|
|
857
|
-
* Head bucket - check if bucket exists and you have access
|
|
858
|
-
*/
|
|
859
|
-
async headBucket(bucket: string): Promise<{ exists: boolean; region?: string }> {
|
|
860
|
-
try {
|
|
861
|
-
const result = await this.client.request({
|
|
862
|
-
service: 's3',
|
|
863
|
-
region: this.region,
|
|
864
|
-
method: 'HEAD',
|
|
865
|
-
path: `/${bucket}`,
|
|
866
|
-
returnHeaders: true,
|
|
867
|
-
})
|
|
868
|
-
return { exists: true, region: result?.headers?.['x-amz-bucket-region'] }
|
|
869
|
-
} catch (e: any) {
|
|
870
|
-
if (e.statusCode === 404) {
|
|
871
|
-
return { exists: false }
|
|
872
|
-
}
|
|
873
|
-
throw e
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
/**
|
|
878
|
-
* Head object - get object metadata without downloading
|
|
879
|
-
*/
|
|
880
|
-
async headObject(bucket: string, key: string): Promise<{
|
|
881
|
-
ContentLength?: number
|
|
882
|
-
ContentType?: string
|
|
883
|
-
ETag?: string
|
|
884
|
-
LastModified?: string
|
|
885
|
-
Metadata?: Record<string, string>
|
|
886
|
-
} | null> {
|
|
887
|
-
try {
|
|
888
|
-
const result = await this.client.request({
|
|
889
|
-
service: 's3',
|
|
890
|
-
region: this.region,
|
|
891
|
-
method: 'HEAD',
|
|
892
|
-
path: `/${bucket}/${key}`,
|
|
893
|
-
returnHeaders: true,
|
|
894
|
-
})
|
|
895
|
-
return {
|
|
896
|
-
ContentLength: result?.headers?.['content-length'] ? parseInt(result.headers['content-length']) : undefined,
|
|
897
|
-
ContentType: result?.headers?.['content-type'],
|
|
898
|
-
ETag: result?.headers?.['etag'],
|
|
899
|
-
LastModified: result?.headers?.['last-modified'],
|
|
900
|
-
}
|
|
901
|
-
} catch (e: any) {
|
|
902
|
-
if (e.statusCode === 404) {
|
|
903
|
-
return null
|
|
904
|
-
}
|
|
905
|
-
throw e
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
/**
|
|
910
|
-
* Get object as Buffer
|
|
911
|
-
*/
|
|
912
|
-
async getObjectBuffer(bucket: string, key: string): Promise<Buffer> {
|
|
913
|
-
const content = await this.getObject(bucket, key)
|
|
914
|
-
return Buffer.from(content)
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
/**
|
|
918
|
-
* Get object as JSON
|
|
919
|
-
*/
|
|
920
|
-
async getObjectJson<T = any>(bucket: string, key: string): Promise<T> {
|
|
921
|
-
const content = await this.getObject(bucket, key)
|
|
922
|
-
return JSON.parse(content)
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
/**
|
|
926
|
-
* Put JSON object
|
|
927
|
-
*/
|
|
928
|
-
async putObjectJson(bucket: string, key: string, data: any, options?: {
|
|
929
|
-
acl?: string
|
|
930
|
-
cacheControl?: string
|
|
931
|
-
metadata?: Record<string, string>
|
|
932
|
-
}): Promise<void> {
|
|
933
|
-
await this.putObject({
|
|
934
|
-
bucket,
|
|
935
|
-
key,
|
|
936
|
-
body: JSON.stringify(data),
|
|
937
|
-
contentType: 'application/json',
|
|
938
|
-
...options,
|
|
939
|
-
})
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
/**
|
|
943
|
-
* Get bucket versioning configuration
|
|
944
|
-
*/
|
|
945
|
-
async getBucketVersioning(bucket: string): Promise<{ Status?: 'Enabled' | 'Suspended' }> {
|
|
946
|
-
const result = await this.client.request({
|
|
947
|
-
service: 's3',
|
|
948
|
-
region: this.region,
|
|
949
|
-
method: 'GET',
|
|
950
|
-
path: `/${bucket}`,
|
|
951
|
-
queryParams: { versioning: '' },
|
|
952
|
-
})
|
|
953
|
-
return {
|
|
954
|
-
Status: result?.VersioningConfiguration?.Status,
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
/**
|
|
959
|
-
* Put bucket versioning configuration
|
|
960
|
-
*/
|
|
961
|
-
async putBucketVersioning(bucket: string, status: 'Enabled' | 'Suspended'): Promise<void> {
|
|
962
|
-
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
963
|
-
<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
964
|
-
<Status>${status}</Status>
|
|
965
|
-
</VersioningConfiguration>`
|
|
966
|
-
|
|
967
|
-
await this.client.request({
|
|
968
|
-
service: 's3',
|
|
969
|
-
region: this.region,
|
|
970
|
-
method: 'PUT',
|
|
971
|
-
path: `/${bucket}`,
|
|
972
|
-
queryParams: { versioning: '' },
|
|
973
|
-
headers: { 'Content-Type': 'application/xml' },
|
|
974
|
-
body,
|
|
975
|
-
})
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
/**
|
|
979
|
-
* Get bucket lifecycle configuration
|
|
980
|
-
*/
|
|
981
|
-
async getBucketLifecycleConfiguration(bucket: string): Promise<any> {
|
|
982
|
-
try {
|
|
983
|
-
const result = await this.client.request({
|
|
984
|
-
service: 's3',
|
|
985
|
-
region: this.region,
|
|
986
|
-
method: 'GET',
|
|
987
|
-
path: `/${bucket}`,
|
|
988
|
-
queryParams: { lifecycle: '' },
|
|
989
|
-
})
|
|
990
|
-
return result?.LifecycleConfiguration
|
|
991
|
-
} catch (e: any) {
|
|
992
|
-
if (e.statusCode === 404) {
|
|
993
|
-
return null
|
|
994
|
-
}
|
|
995
|
-
throw e
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
/**
|
|
1000
|
-
* Put bucket lifecycle configuration
|
|
1001
|
-
*/
|
|
1002
|
-
async putBucketLifecycleConfiguration(bucket: string, rules: Array<{
|
|
1003
|
-
ID: string
|
|
1004
|
-
Status: 'Enabled' | 'Disabled'
|
|
1005
|
-
Filter?: { Prefix?: string }
|
|
1006
|
-
Expiration?: { Days?: number; Date?: string }
|
|
1007
|
-
Transitions?: Array<{ Days?: number; StorageClass: string }>
|
|
1008
|
-
NoncurrentVersionExpiration?: { NoncurrentDays: number }
|
|
1009
|
-
}>): Promise<void> {
|
|
1010
|
-
const rulesXml = rules.map(rule => {
|
|
1011
|
-
let ruleXml = `<Rule><ID>${rule.ID}</ID><Status>${rule.Status}</Status>`
|
|
1012
|
-
|
|
1013
|
-
if (rule.Filter) {
|
|
1014
|
-
ruleXml += `<Filter><Prefix>${rule.Filter.Prefix || ''}</Prefix></Filter>`
|
|
1015
|
-
} else {
|
|
1016
|
-
ruleXml += '<Filter><Prefix></Prefix></Filter>'
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
if (rule.Expiration) {
|
|
1020
|
-
if (rule.Expiration.Days) {
|
|
1021
|
-
ruleXml += `<Expiration><Days>${rule.Expiration.Days}</Days></Expiration>`
|
|
1022
|
-
} else if (rule.Expiration.Date) {
|
|
1023
|
-
ruleXml += `<Expiration><Date>${rule.Expiration.Date}</Date></Expiration>`
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
if (rule.Transitions) {
|
|
1028
|
-
for (const t of rule.Transitions) {
|
|
1029
|
-
ruleXml += `<Transition><Days>${t.Days}</Days><StorageClass>${t.StorageClass}</StorageClass></Transition>`
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
if (rule.NoncurrentVersionExpiration) {
|
|
1034
|
-
ruleXml += `<NoncurrentVersionExpiration><NoncurrentDays>${rule.NoncurrentVersionExpiration.NoncurrentDays}</NoncurrentDays></NoncurrentVersionExpiration>`
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
ruleXml += '</Rule>'
|
|
1038
|
-
return ruleXml
|
|
1039
|
-
}).join('')
|
|
1040
|
-
|
|
1041
|
-
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1042
|
-
<LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
1043
|
-
${rulesXml}
|
|
1044
|
-
</LifecycleConfiguration>`
|
|
1045
|
-
|
|
1046
|
-
await this.client.request({
|
|
1047
|
-
service: 's3',
|
|
1048
|
-
region: this.region,
|
|
1049
|
-
method: 'PUT',
|
|
1050
|
-
path: `/${bucket}`,
|
|
1051
|
-
queryParams: { lifecycle: '' },
|
|
1052
|
-
headers: { 'Content-Type': 'application/xml' },
|
|
1053
|
-
body,
|
|
1054
|
-
})
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
/**
|
|
1058
|
-
* Delete bucket lifecycle configuration
|
|
1059
|
-
*/
|
|
1060
|
-
async deleteBucketLifecycleConfiguration(bucket: string): Promise<void> {
|
|
1061
|
-
await this.client.request({
|
|
1062
|
-
service: 's3',
|
|
1063
|
-
region: this.region,
|
|
1064
|
-
method: 'DELETE',
|
|
1065
|
-
path: `/${bucket}`,
|
|
1066
|
-
queryParams: { lifecycle: '' },
|
|
1067
|
-
})
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
/**
|
|
1071
|
-
* Get bucket CORS configuration
|
|
1072
|
-
*/
|
|
1073
|
-
async getBucketCors(bucket: string): Promise<any> {
|
|
1074
|
-
try {
|
|
1075
|
-
const result = await this.client.request({
|
|
1076
|
-
service: 's3',
|
|
1077
|
-
region: this.region,
|
|
1078
|
-
method: 'GET',
|
|
1079
|
-
path: `/${bucket}`,
|
|
1080
|
-
queryParams: { cors: '' },
|
|
1081
|
-
})
|
|
1082
|
-
return result?.CORSConfiguration
|
|
1083
|
-
} catch (e: any) {
|
|
1084
|
-
if (e.statusCode === 404) {
|
|
1085
|
-
return null
|
|
1086
|
-
}
|
|
1087
|
-
throw e
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
/**
|
|
1092
|
-
* Put bucket CORS configuration
|
|
1093
|
-
*/
|
|
1094
|
-
async putBucketCors(bucket: string, rules: Array<{
|
|
1095
|
-
AllowedOrigins: string[]
|
|
1096
|
-
AllowedMethods: string[]
|
|
1097
|
-
AllowedHeaders?: string[]
|
|
1098
|
-
ExposeHeaders?: string[]
|
|
1099
|
-
MaxAgeSeconds?: number
|
|
1100
|
-
}>): Promise<void> {
|
|
1101
|
-
const rulesXml = rules.map(rule => {
|
|
1102
|
-
let ruleXml = '<CORSRule>'
|
|
1103
|
-
for (const origin of rule.AllowedOrigins) {
|
|
1104
|
-
ruleXml += `<AllowedOrigin>${origin}</AllowedOrigin>`
|
|
1105
|
-
}
|
|
1106
|
-
for (const method of rule.AllowedMethods) {
|
|
1107
|
-
ruleXml += `<AllowedMethod>${method}</AllowedMethod>`
|
|
1108
|
-
}
|
|
1109
|
-
if (rule.AllowedHeaders) {
|
|
1110
|
-
for (const header of rule.AllowedHeaders) {
|
|
1111
|
-
ruleXml += `<AllowedHeader>${header}</AllowedHeader>`
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
if (rule.ExposeHeaders) {
|
|
1115
|
-
for (const header of rule.ExposeHeaders) {
|
|
1116
|
-
ruleXml += `<ExposeHeader>${header}</ExposeHeader>`
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
if (rule.MaxAgeSeconds) {
|
|
1120
|
-
ruleXml += `<MaxAgeSeconds>${rule.MaxAgeSeconds}</MaxAgeSeconds>`
|
|
1121
|
-
}
|
|
1122
|
-
ruleXml += '</CORSRule>'
|
|
1123
|
-
return ruleXml
|
|
1124
|
-
}).join('')
|
|
1125
|
-
|
|
1126
|
-
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1127
|
-
<CORSConfiguration>
|
|
1128
|
-
${rulesXml}
|
|
1129
|
-
</CORSConfiguration>`
|
|
1130
|
-
|
|
1131
|
-
await this.client.request({
|
|
1132
|
-
service: 's3',
|
|
1133
|
-
region: this.region,
|
|
1134
|
-
method: 'PUT',
|
|
1135
|
-
path: `/${bucket}`,
|
|
1136
|
-
queryParams: { cors: '' },
|
|
1137
|
-
headers: { 'Content-Type': 'application/xml' },
|
|
1138
|
-
body,
|
|
1139
|
-
})
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
/**
|
|
1143
|
-
* Delete bucket CORS configuration
|
|
1144
|
-
*/
|
|
1145
|
-
async deleteBucketCors(bucket: string): Promise<void> {
|
|
1146
|
-
await this.client.request({
|
|
1147
|
-
service: 's3',
|
|
1148
|
-
region: this.region,
|
|
1149
|
-
method: 'DELETE',
|
|
1150
|
-
path: `/${bucket}`,
|
|
1151
|
-
queryParams: { cors: '' },
|
|
1152
|
-
})
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
/**
|
|
1156
|
-
* Get bucket encryption configuration
|
|
1157
|
-
*/
|
|
1158
|
-
async getBucketEncryption(bucket: string): Promise<any> {
|
|
1159
|
-
try {
|
|
1160
|
-
const result = await this.client.request({
|
|
1161
|
-
service: 's3',
|
|
1162
|
-
region: this.region,
|
|
1163
|
-
method: 'GET',
|
|
1164
|
-
path: `/${bucket}`,
|
|
1165
|
-
queryParams: { encryption: '' },
|
|
1166
|
-
})
|
|
1167
|
-
return result?.ServerSideEncryptionConfiguration
|
|
1168
|
-
} catch (e: any) {
|
|
1169
|
-
if (e.statusCode === 404) {
|
|
1170
|
-
return null
|
|
1171
|
-
}
|
|
1172
|
-
throw e
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
/**
|
|
1177
|
-
* Put bucket encryption configuration
|
|
1178
|
-
*/
|
|
1179
|
-
async putBucketEncryption(bucket: string, sseAlgorithm: 'AES256' | 'aws:kms', kmsKeyId?: string): Promise<void> {
|
|
1180
|
-
let ruleXml = `<ApplyServerSideEncryptionByDefault><SSEAlgorithm>${sseAlgorithm}</SSEAlgorithm>`
|
|
1181
|
-
if (kmsKeyId) {
|
|
1182
|
-
ruleXml += `<KMSMasterKeyID>${kmsKeyId}</KMSMasterKeyID>`
|
|
1183
|
-
}
|
|
1184
|
-
ruleXml += '</ApplyServerSideEncryptionByDefault>'
|
|
1185
|
-
|
|
1186
|
-
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1187
|
-
<ServerSideEncryptionConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
1188
|
-
<Rule>${ruleXml}</Rule>
|
|
1189
|
-
</ServerSideEncryptionConfiguration>`
|
|
1190
|
-
|
|
1191
|
-
await this.client.request({
|
|
1192
|
-
service: 's3',
|
|
1193
|
-
region: this.region,
|
|
1194
|
-
method: 'PUT',
|
|
1195
|
-
path: `/${bucket}`,
|
|
1196
|
-
queryParams: { encryption: '' },
|
|
1197
|
-
headers: { 'Content-Type': 'application/xml' },
|
|
1198
|
-
body,
|
|
1199
|
-
})
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
/**
|
|
1203
|
-
* Delete bucket encryption configuration
|
|
1204
|
-
*/
|
|
1205
|
-
async deleteBucketEncryption(bucket: string): Promise<void> {
|
|
1206
|
-
await this.client.request({
|
|
1207
|
-
service: 's3',
|
|
1208
|
-
region: this.region,
|
|
1209
|
-
method: 'DELETE',
|
|
1210
|
-
path: `/${bucket}`,
|
|
1211
|
-
queryParams: { encryption: '' },
|
|
1212
|
-
})
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
/**
|
|
1216
|
-
* Get bucket tagging
|
|
1217
|
-
*/
|
|
1218
|
-
async getBucketTagging(bucket: string): Promise<Array<{ Key: string; Value: string }>> {
|
|
1219
|
-
try {
|
|
1220
|
-
const result = await this.client.request({
|
|
1221
|
-
service: 's3',
|
|
1222
|
-
region: this.region,
|
|
1223
|
-
method: 'GET',
|
|
1224
|
-
path: `/${bucket}`,
|
|
1225
|
-
queryParams: { tagging: '' },
|
|
1226
|
-
})
|
|
1227
|
-
const tagSet = result?.Tagging?.TagSet?.Tag
|
|
1228
|
-
if (!tagSet) return []
|
|
1229
|
-
return Array.isArray(tagSet) ? tagSet : [tagSet]
|
|
1230
|
-
} catch (e: any) {
|
|
1231
|
-
if (e.statusCode === 404) {
|
|
1232
|
-
return []
|
|
1233
|
-
}
|
|
1234
|
-
throw e
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
/**
|
|
1239
|
-
* Put bucket tagging
|
|
1240
|
-
*/
|
|
1241
|
-
async putBucketTagging(bucket: string, tags: Array<{ Key: string; Value: string }>): Promise<void> {
|
|
1242
|
-
const tagsXml = tags.map(t => `<Tag><Key>${t.Key}</Key><Value>${t.Value}</Value></Tag>`).join('')
|
|
1243
|
-
|
|
1244
|
-
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1245
|
-
<Tagging>
|
|
1246
|
-
<TagSet>${tagsXml}</TagSet>
|
|
1247
|
-
</Tagging>`
|
|
1248
|
-
|
|
1249
|
-
await this.client.request({
|
|
1250
|
-
service: 's3',
|
|
1251
|
-
region: this.region,
|
|
1252
|
-
method: 'PUT',
|
|
1253
|
-
path: `/${bucket}`,
|
|
1254
|
-
queryParams: { tagging: '' },
|
|
1255
|
-
headers: { 'Content-Type': 'application/xml' },
|
|
1256
|
-
body,
|
|
1257
|
-
})
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
/**
|
|
1261
|
-
* Delete bucket tagging
|
|
1262
|
-
*/
|
|
1263
|
-
async deleteBucketTagging(bucket: string): Promise<void> {
|
|
1264
|
-
await this.client.request({
|
|
1265
|
-
service: 's3',
|
|
1266
|
-
region: this.region,
|
|
1267
|
-
method: 'DELETE',
|
|
1268
|
-
path: `/${bucket}`,
|
|
1269
|
-
queryParams: { tagging: '' },
|
|
1270
|
-
})
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
/**
|
|
1274
|
-
* Get object tagging
|
|
1275
|
-
*/
|
|
1276
|
-
async getObjectTagging(bucket: string, key: string): Promise<Array<{ Key: string; Value: string }>> {
|
|
1277
|
-
try {
|
|
1278
|
-
const result = await this.client.request({
|
|
1279
|
-
service: 's3',
|
|
1280
|
-
region: this.region,
|
|
1281
|
-
method: 'GET',
|
|
1282
|
-
path: `/${bucket}/${key}`,
|
|
1283
|
-
queryParams: { tagging: '' },
|
|
1284
|
-
})
|
|
1285
|
-
const tagSet = result?.Tagging?.TagSet?.Tag
|
|
1286
|
-
if (!tagSet) return []
|
|
1287
|
-
return Array.isArray(tagSet) ? tagSet : [tagSet]
|
|
1288
|
-
} catch (e: any) {
|
|
1289
|
-
if (e.statusCode === 404) {
|
|
1290
|
-
return []
|
|
1291
|
-
}
|
|
1292
|
-
throw e
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
/**
|
|
1297
|
-
* Put object tagging
|
|
1298
|
-
*/
|
|
1299
|
-
async putObjectTagging(bucket: string, key: string, tags: Array<{ Key: string; Value: string }>): Promise<void> {
|
|
1300
|
-
const tagsXml = tags.map(t => `<Tag><Key>${t.Key}</Key><Value>${t.Value}</Value></Tag>`).join('')
|
|
1301
|
-
|
|
1302
|
-
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1303
|
-
<Tagging>
|
|
1304
|
-
<TagSet>${tagsXml}</TagSet>
|
|
1305
|
-
</Tagging>`
|
|
1306
|
-
|
|
1307
|
-
await this.client.request({
|
|
1308
|
-
service: 's3',
|
|
1309
|
-
region: this.region,
|
|
1310
|
-
method: 'PUT',
|
|
1311
|
-
path: `/${bucket}/${key}`,
|
|
1312
|
-
queryParams: { tagging: '' },
|
|
1313
|
-
headers: { 'Content-Type': 'application/xml' },
|
|
1314
|
-
body,
|
|
1315
|
-
})
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
/**
|
|
1319
|
-
* Delete object tagging
|
|
1320
|
-
*/
|
|
1321
|
-
async deleteObjectTagging(bucket: string, key: string): Promise<void> {
|
|
1322
|
-
await this.client.request({
|
|
1323
|
-
service: 's3',
|
|
1324
|
-
region: this.region,
|
|
1325
|
-
method: 'DELETE',
|
|
1326
|
-
path: `/${bucket}/${key}`,
|
|
1327
|
-
queryParams: { tagging: '' },
|
|
1328
|
-
})
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
/**
|
|
1332
|
-
* Get bucket ACL
|
|
1333
|
-
*/
|
|
1334
|
-
async getBucketAcl(bucket: string): Promise<any> {
|
|
1335
|
-
const result = await this.client.request({
|
|
1336
|
-
service: 's3',
|
|
1337
|
-
region: this.region,
|
|
1338
|
-
method: 'GET',
|
|
1339
|
-
path: `/${bucket}`,
|
|
1340
|
-
queryParams: { acl: '' },
|
|
1341
|
-
})
|
|
1342
|
-
return result?.AccessControlPolicy
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
/**
|
|
1346
|
-
* Put bucket ACL (canned ACL)
|
|
1347
|
-
*/
|
|
1348
|
-
async putBucketAcl(bucket: string, acl: 'private' | 'public-read' | 'public-read-write' | 'authenticated-read'): Promise<void> {
|
|
1349
|
-
await this.client.request({
|
|
1350
|
-
service: 's3',
|
|
1351
|
-
region: this.region,
|
|
1352
|
-
method: 'PUT',
|
|
1353
|
-
path: `/${bucket}`,
|
|
1354
|
-
queryParams: { acl: '' },
|
|
1355
|
-
headers: { 'x-amz-acl': acl },
|
|
1356
|
-
})
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
/**
|
|
1360
|
-
* Get object ACL
|
|
1361
|
-
*/
|
|
1362
|
-
async getObjectAcl(bucket: string, key: string): Promise<any> {
|
|
1363
|
-
const result = await this.client.request({
|
|
1364
|
-
service: 's3',
|
|
1365
|
-
region: this.region,
|
|
1366
|
-
method: 'GET',
|
|
1367
|
-
path: `/${bucket}/${key}`,
|
|
1368
|
-
queryParams: { acl: '' },
|
|
1369
|
-
})
|
|
1370
|
-
return result?.AccessControlPolicy
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
/**
|
|
1374
|
-
* Put object ACL (canned ACL)
|
|
1375
|
-
*/
|
|
1376
|
-
async putObjectAcl(bucket: string, key: string, acl: 'private' | 'public-read' | 'public-read-write' | 'authenticated-read'): Promise<void> {
|
|
1377
|
-
await this.client.request({
|
|
1378
|
-
service: 's3',
|
|
1379
|
-
region: this.region,
|
|
1380
|
-
method: 'PUT',
|
|
1381
|
-
path: `/${bucket}/${key}`,
|
|
1382
|
-
queryParams: { acl: '' },
|
|
1383
|
-
headers: { 'x-amz-acl': acl },
|
|
1384
|
-
})
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
/**
|
|
1388
|
-
* Get bucket location
|
|
1389
|
-
*/
|
|
1390
|
-
async getBucketLocation(bucket: string): Promise<string> {
|
|
1391
|
-
const result = await this.client.request({
|
|
1392
|
-
service: 's3',
|
|
1393
|
-
region: this.region,
|
|
1394
|
-
method: 'GET',
|
|
1395
|
-
path: `/${bucket}`,
|
|
1396
|
-
queryParams: { location: '' },
|
|
1397
|
-
})
|
|
1398
|
-
// Empty string means us-east-1
|
|
1399
|
-
return result?.LocationConstraint || 'us-east-1'
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
/**
|
|
1403
|
-
* Get bucket logging configuration
|
|
1404
|
-
*/
|
|
1405
|
-
async getBucketLogging(bucket: string): Promise<any> {
|
|
1406
|
-
const result = await this.client.request({
|
|
1407
|
-
service: 's3',
|
|
1408
|
-
region: this.region,
|
|
1409
|
-
method: 'GET',
|
|
1410
|
-
path: `/${bucket}`,
|
|
1411
|
-
queryParams: { logging: '' },
|
|
1412
|
-
})
|
|
1413
|
-
return result?.BucketLoggingStatus
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
/**
|
|
1417
|
-
* Put bucket logging configuration
|
|
1418
|
-
*/
|
|
1419
|
-
async putBucketLogging(bucket: string, targetBucket: string, targetPrefix: string): Promise<void> {
|
|
1420
|
-
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1421
|
-
<BucketLoggingStatus xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
1422
|
-
<LoggingEnabled>
|
|
1423
|
-
<TargetBucket>${targetBucket}</TargetBucket>
|
|
1424
|
-
<TargetPrefix>${targetPrefix}</TargetPrefix>
|
|
1425
|
-
</LoggingEnabled>
|
|
1426
|
-
</BucketLoggingStatus>`
|
|
1427
|
-
|
|
1428
|
-
await this.client.request({
|
|
1429
|
-
service: 's3',
|
|
1430
|
-
region: this.region,
|
|
1431
|
-
method: 'PUT',
|
|
1432
|
-
path: `/${bucket}`,
|
|
1433
|
-
queryParams: { logging: '' },
|
|
1434
|
-
headers: { 'Content-Type': 'application/xml' },
|
|
1435
|
-
body,
|
|
1436
|
-
})
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
/**
|
|
1440
|
-
* Get bucket notification configuration
|
|
1441
|
-
*/
|
|
1442
|
-
async getBucketNotificationConfiguration(bucket: string): Promise<any> {
|
|
1443
|
-
const result = await this.client.request({
|
|
1444
|
-
service: 's3',
|
|
1445
|
-
region: this.region,
|
|
1446
|
-
method: 'GET',
|
|
1447
|
-
path: `/${bucket}`,
|
|
1448
|
-
queryParams: { notification: '' },
|
|
1449
|
-
})
|
|
1450
|
-
return result?.NotificationConfiguration
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
/**
|
|
1454
|
-
* Put bucket notification configuration
|
|
1455
|
-
*/
|
|
1456
|
-
async putBucketNotificationConfiguration(bucket: string, config: {
|
|
1457
|
-
LambdaFunctionConfigurations?: Array<{
|
|
1458
|
-
Id?: string
|
|
1459
|
-
LambdaFunctionArn: string
|
|
1460
|
-
Events: string[]
|
|
1461
|
-
Filter?: { Key?: { FilterRules: Array<{ Name: string; Value: string }> } }
|
|
1462
|
-
}>
|
|
1463
|
-
TopicConfigurations?: Array<{
|
|
1464
|
-
Id?: string
|
|
1465
|
-
TopicArn: string
|
|
1466
|
-
Events: string[]
|
|
1467
|
-
Filter?: { Key?: { FilterRules: Array<{ Name: string; Value: string }> } }
|
|
1468
|
-
}>
|
|
1469
|
-
QueueConfigurations?: Array<{
|
|
1470
|
-
Id?: string
|
|
1471
|
-
QueueArn: string
|
|
1472
|
-
Events: string[]
|
|
1473
|
-
Filter?: { Key?: { FilterRules: Array<{ Name: string; Value: string }> } }
|
|
1474
|
-
}>
|
|
1475
|
-
}): Promise<void> {
|
|
1476
|
-
let configXml = ''
|
|
1477
|
-
|
|
1478
|
-
if (config.LambdaFunctionConfigurations) {
|
|
1479
|
-
for (const c of config.LambdaFunctionConfigurations) {
|
|
1480
|
-
configXml += '<CloudFunctionConfiguration>'
|
|
1481
|
-
if (c.Id) configXml += `<Id>${c.Id}</Id>`
|
|
1482
|
-
configXml += `<CloudFunction>${c.LambdaFunctionArn}</CloudFunction>`
|
|
1483
|
-
for (const event of c.Events) {
|
|
1484
|
-
configXml += `<Event>${event}</Event>`
|
|
1485
|
-
}
|
|
1486
|
-
if (c.Filter?.Key?.FilterRules) {
|
|
1487
|
-
configXml += '<Filter><S3Key>'
|
|
1488
|
-
for (const rule of c.Filter.Key.FilterRules) {
|
|
1489
|
-
configXml += `<FilterRule><Name>${rule.Name}</Name><Value>${rule.Value}</Value></FilterRule>`
|
|
1490
|
-
}
|
|
1491
|
-
configXml += '</S3Key></Filter>'
|
|
1492
|
-
}
|
|
1493
|
-
configXml += '</CloudFunctionConfiguration>'
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
if (config.TopicConfigurations) {
|
|
1498
|
-
for (const c of config.TopicConfigurations) {
|
|
1499
|
-
configXml += '<TopicConfiguration>'
|
|
1500
|
-
if (c.Id) configXml += `<Id>${c.Id}</Id>`
|
|
1501
|
-
configXml += `<Topic>${c.TopicArn}</Topic>`
|
|
1502
|
-
for (const event of c.Events) {
|
|
1503
|
-
configXml += `<Event>${event}</Event>`
|
|
1504
|
-
}
|
|
1505
|
-
configXml += '</TopicConfiguration>'
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
if (config.QueueConfigurations) {
|
|
1510
|
-
for (const c of config.QueueConfigurations) {
|
|
1511
|
-
configXml += '<QueueConfiguration>'
|
|
1512
|
-
if (c.Id) configXml += `<Id>${c.Id}</Id>`
|
|
1513
|
-
configXml += `<Queue>${c.QueueArn}</Queue>`
|
|
1514
|
-
for (const event of c.Events) {
|
|
1515
|
-
configXml += `<Event>${event}</Event>`
|
|
1516
|
-
}
|
|
1517
|
-
configXml += '</QueueConfiguration>'
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1522
|
-
<NotificationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
1523
|
-
${configXml}
|
|
1524
|
-
</NotificationConfiguration>`
|
|
1525
|
-
|
|
1526
|
-
await this.client.request({
|
|
1527
|
-
service: 's3',
|
|
1528
|
-
region: this.region,
|
|
1529
|
-
method: 'PUT',
|
|
1530
|
-
path: `/${bucket}`,
|
|
1531
|
-
queryParams: { notification: '' },
|
|
1532
|
-
headers: { 'Content-Type': 'application/xml' },
|
|
1533
|
-
body,
|
|
1534
|
-
})
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
/**
|
|
1538
|
-
* Get bucket website configuration
|
|
1539
|
-
*/
|
|
1540
|
-
async getBucketWebsite(bucket: string): Promise<any> {
|
|
1541
|
-
try {
|
|
1542
|
-
const result = await this.client.request({
|
|
1543
|
-
service: 's3',
|
|
1544
|
-
region: this.region,
|
|
1545
|
-
method: 'GET',
|
|
1546
|
-
path: `/${bucket}`,
|
|
1547
|
-
queryParams: { website: '' },
|
|
1548
|
-
})
|
|
1549
|
-
return result?.WebsiteConfiguration
|
|
1550
|
-
} catch (e: any) {
|
|
1551
|
-
if (e.statusCode === 404) {
|
|
1552
|
-
return null
|
|
1553
|
-
}
|
|
1554
|
-
throw e
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
/**
|
|
1559
|
-
* Put bucket website configuration
|
|
1560
|
-
*/
|
|
1561
|
-
async putBucketWebsite(bucket: string, config: {
|
|
1562
|
-
IndexDocument: string
|
|
1563
|
-
ErrorDocument?: string
|
|
1564
|
-
RedirectAllRequestsTo?: { HostName: string; Protocol?: string }
|
|
1565
|
-
}): Promise<void> {
|
|
1566
|
-
let configXml = ''
|
|
1567
|
-
|
|
1568
|
-
if (config.RedirectAllRequestsTo) {
|
|
1569
|
-
configXml = `<RedirectAllRequestsTo>
|
|
1570
|
-
<HostName>${config.RedirectAllRequestsTo.HostName}</HostName>
|
|
1571
|
-
${config.RedirectAllRequestsTo.Protocol ? `<Protocol>${config.RedirectAllRequestsTo.Protocol}</Protocol>` : ''}
|
|
1572
|
-
</RedirectAllRequestsTo>`
|
|
1573
|
-
} else {
|
|
1574
|
-
configXml = `<IndexDocument><Suffix>${config.IndexDocument}</Suffix></IndexDocument>`
|
|
1575
|
-
if (config.ErrorDocument) {
|
|
1576
|
-
configXml += `<ErrorDocument><Key>${config.ErrorDocument}</Key></ErrorDocument>`
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1581
|
-
<WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
1582
|
-
${configXml}
|
|
1583
|
-
</WebsiteConfiguration>`
|
|
1584
|
-
|
|
1585
|
-
await this.client.request({
|
|
1586
|
-
service: 's3',
|
|
1587
|
-
region: this.region,
|
|
1588
|
-
method: 'PUT',
|
|
1589
|
-
path: `/${bucket}`,
|
|
1590
|
-
queryParams: { website: '' },
|
|
1591
|
-
headers: { 'Content-Type': 'application/xml' },
|
|
1592
|
-
body,
|
|
1593
|
-
})
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
/**
|
|
1597
|
-
* Delete bucket website configuration
|
|
1598
|
-
*/
|
|
1599
|
-
async deleteBucketWebsite(bucket: string): Promise<void> {
|
|
1600
|
-
await this.client.request({
|
|
1601
|
-
service: 's3',
|
|
1602
|
-
region: this.region,
|
|
1603
|
-
method: 'DELETE',
|
|
1604
|
-
path: `/${bucket}`,
|
|
1605
|
-
queryParams: { website: '' },
|
|
1606
|
-
})
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
/**
|
|
1610
|
-
* Get bucket replication configuration
|
|
1611
|
-
*/
|
|
1612
|
-
async getBucketReplication(bucket: string): Promise<any> {
|
|
1613
|
-
try {
|
|
1614
|
-
const result = await this.client.request({
|
|
1615
|
-
service: 's3',
|
|
1616
|
-
region: this.region,
|
|
1617
|
-
method: 'GET',
|
|
1618
|
-
path: `/${bucket}`,
|
|
1619
|
-
queryParams: { replication: '' },
|
|
1620
|
-
})
|
|
1621
|
-
return result?.ReplicationConfiguration
|
|
1622
|
-
} catch (e: any) {
|
|
1623
|
-
if (e.statusCode === 404) {
|
|
1624
|
-
return null
|
|
1625
|
-
}
|
|
1626
|
-
throw e
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
/**
|
|
1631
|
-
* Delete bucket replication configuration
|
|
1632
|
-
*/
|
|
1633
|
-
async deleteBucketReplication(bucket: string): Promise<void> {
|
|
1634
|
-
await this.client.request({
|
|
1635
|
-
service: 's3',
|
|
1636
|
-
region: this.region,
|
|
1637
|
-
method: 'DELETE',
|
|
1638
|
-
path: `/${bucket}`,
|
|
1639
|
-
queryParams: { replication: '' },
|
|
1640
|
-
})
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
/**
|
|
1644
|
-
* Get public access block configuration
|
|
1645
|
-
*/
|
|
1646
|
-
async getPublicAccessBlock(bucket: string): Promise<any> {
|
|
1647
|
-
try {
|
|
1648
|
-
const result = await this.client.request({
|
|
1649
|
-
service: 's3',
|
|
1650
|
-
region: this.region,
|
|
1651
|
-
method: 'GET',
|
|
1652
|
-
path: `/${bucket}`,
|
|
1653
|
-
queryParams: { publicAccessBlock: '' },
|
|
1654
|
-
})
|
|
1655
|
-
return result?.PublicAccessBlockConfiguration
|
|
1656
|
-
} catch (e: any) {
|
|
1657
|
-
if (e.statusCode === 404) {
|
|
1658
|
-
return null
|
|
1659
|
-
}
|
|
1660
|
-
throw e
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
/**
|
|
1665
|
-
* Put public access block configuration
|
|
1666
|
-
*/
|
|
1667
|
-
async putPublicAccessBlock(bucket: string, config: {
|
|
1668
|
-
BlockPublicAcls?: boolean
|
|
1669
|
-
IgnorePublicAcls?: boolean
|
|
1670
|
-
BlockPublicPolicy?: boolean
|
|
1671
|
-
RestrictPublicBuckets?: boolean
|
|
1672
|
-
}): Promise<void> {
|
|
1673
|
-
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1674
|
-
<PublicAccessBlockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
1675
|
-
<BlockPublicAcls>${config.BlockPublicAcls ?? true}</BlockPublicAcls>
|
|
1676
|
-
<IgnorePublicAcls>${config.IgnorePublicAcls ?? true}</IgnorePublicAcls>
|
|
1677
|
-
<BlockPublicPolicy>${config.BlockPublicPolicy ?? true}</BlockPublicPolicy>
|
|
1678
|
-
<RestrictPublicBuckets>${config.RestrictPublicBuckets ?? true}</RestrictPublicBuckets>
|
|
1679
|
-
</PublicAccessBlockConfiguration>`
|
|
1680
|
-
|
|
1681
|
-
await this.client.request({
|
|
1682
|
-
service: 's3',
|
|
1683
|
-
region: this.region,
|
|
1684
|
-
method: 'PUT',
|
|
1685
|
-
path: `/${bucket}`,
|
|
1686
|
-
queryParams: { publicAccessBlock: '' },
|
|
1687
|
-
headers: { 'Content-Type': 'application/xml' },
|
|
1688
|
-
body,
|
|
1689
|
-
})
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
/**
|
|
1693
|
-
* Delete public access block configuration
|
|
1694
|
-
*/
|
|
1695
|
-
async deletePublicAccessBlock(bucket: string): Promise<void> {
|
|
1696
|
-
await this.client.request({
|
|
1697
|
-
service: 's3',
|
|
1698
|
-
region: this.region,
|
|
1699
|
-
method: 'DELETE',
|
|
1700
|
-
path: `/${bucket}`,
|
|
1701
|
-
queryParams: { publicAccessBlock: '' },
|
|
1702
|
-
})
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
/**
|
|
1706
|
-
* Generate a presigned URL for GET
|
|
1707
|
-
*/
|
|
1708
|
-
generatePresignedGetUrl(bucket: string, key: string, expiresInSeconds: number = 3600): string {
|
|
1709
|
-
const { accessKeyId, secretAccessKey } = this.getCredentials()
|
|
1710
|
-
const host = `${bucket}.s3.${this.region}.amazonaws.com`
|
|
1711
|
-
const now = new Date()
|
|
1712
|
-
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
|
|
1713
|
-
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '')
|
|
1714
|
-
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`
|
|
1715
|
-
const credential = `${accessKeyId}/${credentialScope}`
|
|
1716
|
-
|
|
1717
|
-
const queryParams = new URLSearchParams({
|
|
1718
|
-
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
|
1719
|
-
'X-Amz-Credential': credential,
|
|
1720
|
-
'X-Amz-Date': amzDate,
|
|
1721
|
-
'X-Amz-Expires': expiresInSeconds.toString(),
|
|
1722
|
-
'X-Amz-SignedHeaders': 'host',
|
|
1723
|
-
})
|
|
1724
|
-
|
|
1725
|
-
const canonicalRequest = [
|
|
1726
|
-
'GET',
|
|
1727
|
-
`/${key}`,
|
|
1728
|
-
queryParams.toString(),
|
|
1729
|
-
`host:${host}\n`,
|
|
1730
|
-
'host',
|
|
1731
|
-
'UNSIGNED-PAYLOAD',
|
|
1732
|
-
].join('\n')
|
|
1733
|
-
|
|
1734
|
-
const stringToSign = [
|
|
1735
|
-
'AWS4-HMAC-SHA256',
|
|
1736
|
-
amzDate,
|
|
1737
|
-
credentialScope,
|
|
1738
|
-
crypto.createHash('sha256').update(canonicalRequest).digest('hex'),
|
|
1739
|
-
].join('\n')
|
|
1740
|
-
|
|
1741
|
-
const kDate = crypto.createHmac('sha256', `AWS4${secretAccessKey}`).update(dateStamp).digest()
|
|
1742
|
-
const kRegion = crypto.createHmac('sha256', kDate).update(this.region).digest()
|
|
1743
|
-
const kService = crypto.createHmac('sha256', kRegion).update('s3').digest()
|
|
1744
|
-
const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
|
|
1745
|
-
const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex')
|
|
1746
|
-
|
|
1747
|
-
queryParams.append('X-Amz-Signature', signature)
|
|
1748
|
-
|
|
1749
|
-
return `https://${host}/${key}?${queryParams.toString()}`
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
/**
|
|
1753
|
-
* Generate a presigned URL for PUT
|
|
1754
|
-
*/
|
|
1755
|
-
generatePresignedPutUrl(bucket: string, key: string, contentType: string, expiresInSeconds: number = 3600): string {
|
|
1756
|
-
const { accessKeyId, secretAccessKey } = this.getCredentials()
|
|
1757
|
-
const host = `${bucket}.s3.${this.region}.amazonaws.com`
|
|
1758
|
-
const now = new Date()
|
|
1759
|
-
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
|
|
1760
|
-
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '')
|
|
1761
|
-
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`
|
|
1762
|
-
const credential = `${accessKeyId}/${credentialScope}`
|
|
1763
|
-
|
|
1764
|
-
const queryParams = new URLSearchParams({
|
|
1765
|
-
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
|
1766
|
-
'X-Amz-Credential': credential,
|
|
1767
|
-
'X-Amz-Date': amzDate,
|
|
1768
|
-
'X-Amz-Expires': expiresInSeconds.toString(),
|
|
1769
|
-
'X-Amz-SignedHeaders': 'content-type;host',
|
|
1770
|
-
})
|
|
1771
|
-
|
|
1772
|
-
const canonicalRequest = [
|
|
1773
|
-
'PUT',
|
|
1774
|
-
`/${key}`,
|
|
1775
|
-
queryParams.toString(),
|
|
1776
|
-
`content-type:${contentType}\nhost:${host}\n`,
|
|
1777
|
-
'content-type;host',
|
|
1778
|
-
'UNSIGNED-PAYLOAD',
|
|
1779
|
-
].join('\n')
|
|
1780
|
-
|
|
1781
|
-
const stringToSign = [
|
|
1782
|
-
'AWS4-HMAC-SHA256',
|
|
1783
|
-
amzDate,
|
|
1784
|
-
credentialScope,
|
|
1785
|
-
crypto.createHash('sha256').update(canonicalRequest).digest('hex'),
|
|
1786
|
-
].join('\n')
|
|
1787
|
-
|
|
1788
|
-
const kDate = crypto.createHmac('sha256', `AWS4${secretAccessKey}`).update(dateStamp).digest()
|
|
1789
|
-
const kRegion = crypto.createHmac('sha256', kDate).update(this.region).digest()
|
|
1790
|
-
const kService = crypto.createHmac('sha256', kRegion).update('s3').digest()
|
|
1791
|
-
const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
|
|
1792
|
-
const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex')
|
|
1793
|
-
|
|
1794
|
-
queryParams.append('X-Amz-Signature', signature)
|
|
1795
|
-
|
|
1796
|
-
return `https://${host}/${key}?${queryParams.toString()}`
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
/**
|
|
1800
|
-
* Initiate multipart upload
|
|
1801
|
-
*/
|
|
1802
|
-
async createMultipartUpload(bucket: string, key: string, options?: {
|
|
1803
|
-
contentType?: string
|
|
1804
|
-
metadata?: Record<string, string>
|
|
1805
|
-
}): Promise<{ UploadId: string }> {
|
|
1806
|
-
const headers: Record<string, string> = {}
|
|
1807
|
-
if (options?.contentType) {
|
|
1808
|
-
headers['Content-Type'] = options.contentType
|
|
1809
|
-
}
|
|
1810
|
-
if (options?.metadata) {
|
|
1811
|
-
for (const [k, v] of Object.entries(options.metadata)) {
|
|
1812
|
-
headers[`x-amz-meta-${k}`] = v
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
const result = await this.client.request({
|
|
1817
|
-
service: 's3',
|
|
1818
|
-
region: this.region,
|
|
1819
|
-
method: 'POST',
|
|
1820
|
-
path: `/${bucket}/${key}`,
|
|
1821
|
-
queryParams: { uploads: '' },
|
|
1822
|
-
headers,
|
|
1823
|
-
})
|
|
1824
|
-
|
|
1825
|
-
return { UploadId: result?.InitiateMultipartUploadResult?.UploadId }
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
/**
|
|
1829
|
-
* Upload a part in multipart upload
|
|
1830
|
-
*/
|
|
1831
|
-
async uploadPart(bucket: string, key: string, uploadId: string, partNumber: number, body: Buffer): Promise<{ ETag: string }> {
|
|
1832
|
-
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials()
|
|
1833
|
-
const host = `${bucket}.s3.${this.region}.amazonaws.com`
|
|
1834
|
-
const url = `https://${host}/${key}?partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`
|
|
1835
|
-
|
|
1836
|
-
const now = new Date()
|
|
1837
|
-
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
|
|
1838
|
-
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '')
|
|
1839
|
-
|
|
1840
|
-
const payloadHash = crypto.createHash('sha256').update(body).digest('hex')
|
|
1841
|
-
|
|
1842
|
-
const requestHeaders: Record<string, string> = {
|
|
1843
|
-
'host': host,
|
|
1844
|
-
'x-amz-date': amzDate,
|
|
1845
|
-
'x-amz-content-sha256': payloadHash,
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
if (sessionToken) {
|
|
1849
|
-
requestHeaders['x-amz-security-token'] = sessionToken
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
const canonicalHeaders = Object.keys(requestHeaders)
|
|
1853
|
-
.sort()
|
|
1854
|
-
.map(k => `${k.toLowerCase()}:${requestHeaders[k].trim()}\n`)
|
|
1855
|
-
.join('')
|
|
1856
|
-
|
|
1857
|
-
const signedHeaders = Object.keys(requestHeaders)
|
|
1858
|
-
.sort()
|
|
1859
|
-
.map(k => k.toLowerCase())
|
|
1860
|
-
.join(';')
|
|
1861
|
-
|
|
1862
|
-
const canonicalRequest = [
|
|
1863
|
-
'PUT',
|
|
1864
|
-
`/${key}`,
|
|
1865
|
-
`partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`,
|
|
1866
|
-
canonicalHeaders,
|
|
1867
|
-
signedHeaders,
|
|
1868
|
-
payloadHash,
|
|
1869
|
-
].join('\n')
|
|
1870
|
-
|
|
1871
|
-
const algorithm = 'AWS4-HMAC-SHA256'
|
|
1872
|
-
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`
|
|
1873
|
-
const stringToSign = [
|
|
1874
|
-
algorithm,
|
|
1875
|
-
amzDate,
|
|
1876
|
-
credentialScope,
|
|
1877
|
-
crypto.createHash('sha256').update(canonicalRequest).digest('hex'),
|
|
1878
|
-
].join('\n')
|
|
1879
|
-
|
|
1880
|
-
const kDate = crypto.createHmac('sha256', `AWS4${secretAccessKey}`).update(dateStamp).digest()
|
|
1881
|
-
const kRegion = crypto.createHmac('sha256', kDate).update(this.region).digest()
|
|
1882
|
-
const kService = crypto.createHmac('sha256', kRegion).update('s3').digest()
|
|
1883
|
-
const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
|
|
1884
|
-
const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex')
|
|
1885
|
-
|
|
1886
|
-
const authHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
|
|
1887
|
-
|
|
1888
|
-
const response = await fetch(url, {
|
|
1889
|
-
method: 'PUT',
|
|
1890
|
-
headers: {
|
|
1891
|
-
...requestHeaders,
|
|
1892
|
-
'Authorization': authHeader,
|
|
1893
|
-
},
|
|
1894
|
-
body,
|
|
1895
|
-
})
|
|
1896
|
-
|
|
1897
|
-
if (!response.ok) {
|
|
1898
|
-
const text = await response.text()
|
|
1899
|
-
throw new Error(`Upload part failed: ${response.status} ${text}`)
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
return { ETag: response.headers.get('etag') || '' }
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
/**
|
|
1906
|
-
* Complete multipart upload
|
|
1907
|
-
*/
|
|
1908
|
-
async completeMultipartUpload(bucket: string, key: string, uploadId: string, parts: Array<{ PartNumber: number; ETag: string }>): Promise<void> {
|
|
1909
|
-
const partsXml = parts
|
|
1910
|
-
.sort((a, b) => a.PartNumber - b.PartNumber)
|
|
1911
|
-
.map(p => `<Part><PartNumber>${p.PartNumber}</PartNumber><ETag>${p.ETag}</ETag></Part>`)
|
|
1912
|
-
.join('')
|
|
1913
|
-
|
|
1914
|
-
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1915
|
-
<CompleteMultipartUpload>${partsXml}</CompleteMultipartUpload>`
|
|
1916
|
-
|
|
1917
|
-
await this.client.request({
|
|
1918
|
-
service: 's3',
|
|
1919
|
-
region: this.region,
|
|
1920
|
-
method: 'POST',
|
|
1921
|
-
path: `/${bucket}/${key}`,
|
|
1922
|
-
queryParams: { uploadId },
|
|
1923
|
-
headers: { 'Content-Type': 'application/xml' },
|
|
1924
|
-
body,
|
|
1925
|
-
})
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
/**
|
|
1929
|
-
* Abort multipart upload
|
|
1930
|
-
*/
|
|
1931
|
-
async abortMultipartUpload(bucket: string, key: string, uploadId: string): Promise<void> {
|
|
1932
|
-
await this.client.request({
|
|
1933
|
-
service: 's3',
|
|
1934
|
-
region: this.region,
|
|
1935
|
-
method: 'DELETE',
|
|
1936
|
-
path: `/${bucket}/${key}`,
|
|
1937
|
-
queryParams: { uploadId },
|
|
1938
|
-
})
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
/**
|
|
1942
|
-
* List multipart uploads
|
|
1943
|
-
*/
|
|
1944
|
-
async listMultipartUploads(bucket: string): Promise<Array<{ Key: string; UploadId: string; Initiated: string }>> {
|
|
1945
|
-
const result = await this.client.request({
|
|
1946
|
-
service: 's3',
|
|
1947
|
-
region: this.region,
|
|
1948
|
-
method: 'GET',
|
|
1949
|
-
path: `/${bucket}`,
|
|
1950
|
-
queryParams: { uploads: '' },
|
|
1951
|
-
})
|
|
1952
|
-
|
|
1953
|
-
const uploads = result?.ListMultipartUploadsResult?.Upload
|
|
1954
|
-
if (!uploads) return []
|
|
1955
|
-
const list = Array.isArray(uploads) ? uploads : [uploads]
|
|
1956
|
-
return list.map((u: any) => ({
|
|
1957
|
-
Key: u.Key,
|
|
1958
|
-
UploadId: u.UploadId,
|
|
1959
|
-
Initiated: u.Initiated,
|
|
1960
|
-
}))
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
/**
|
|
1964
|
-
* Restore object from Glacier
|
|
1965
|
-
*/
|
|
1966
|
-
async restoreObject(bucket: string, key: string, days: number, tier: 'Standard' | 'Bulk' | 'Expedited' = 'Standard'): Promise<void> {
|
|
1967
|
-
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1968
|
-
<RestoreRequest>
|
|
1969
|
-
<Days>${days}</Days>
|
|
1970
|
-
<GlacierJobParameters>
|
|
1971
|
-
<Tier>${tier}</Tier>
|
|
1972
|
-
</GlacierJobParameters>
|
|
1973
|
-
</RestoreRequest>`
|
|
1974
|
-
|
|
1975
|
-
await this.client.request({
|
|
1976
|
-
service: 's3',
|
|
1977
|
-
region: this.region,
|
|
1978
|
-
method: 'POST',
|
|
1979
|
-
path: `/${bucket}/${key}`,
|
|
1980
|
-
queryParams: { restore: '' },
|
|
1981
|
-
headers: { 'Content-Type': 'application/xml' },
|
|
1982
|
-
body,
|
|
1983
|
-
})
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
/**
|
|
1987
|
-
* Select object content (S3 Select)
|
|
1988
|
-
*/
|
|
1989
|
-
async selectObjectContent(bucket: string, key: string, expression: string, inputFormat: 'CSV' | 'JSON' | 'Parquet', outputFormat: 'CSV' | 'JSON' = 'JSON'): Promise<string> {
|
|
1990
|
-
let inputSerialization = ''
|
|
1991
|
-
if (inputFormat === 'CSV') {
|
|
1992
|
-
inputSerialization = '<CSV><FileHeaderInfo>USE</FileHeaderInfo></CSV>'
|
|
1993
|
-
} else if (inputFormat === 'JSON') {
|
|
1994
|
-
inputSerialization = '<JSON><Type>DOCUMENT</Type></JSON>'
|
|
1995
|
-
} else {
|
|
1996
|
-
inputSerialization = '<Parquet/>'
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
let outputSerialization = ''
|
|
2000
|
-
if (outputFormat === 'CSV') {
|
|
2001
|
-
outputSerialization = '<CSV/>'
|
|
2002
|
-
} else {
|
|
2003
|
-
outputSerialization = '<JSON/>'
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2007
|
-
<SelectObjectContentRequest>
|
|
2008
|
-
<Expression>${expression}</Expression>
|
|
2009
|
-
<ExpressionType>SQL</ExpressionType>
|
|
2010
|
-
<InputSerialization>${inputSerialization}</InputSerialization>
|
|
2011
|
-
<OutputSerialization>${outputSerialization}</OutputSerialization>
|
|
2012
|
-
</SelectObjectContentRequest>`
|
|
2013
|
-
|
|
2014
|
-
const result = await this.client.request({
|
|
2015
|
-
service: 's3',
|
|
2016
|
-
region: this.region,
|
|
2017
|
-
method: 'POST',
|
|
2018
|
-
path: `/${bucket}/${key}`,
|
|
2019
|
-
queryParams: { select: '', 'select-type': '2' },
|
|
2020
|
-
headers: { 'Content-Type': 'application/xml' },
|
|
2021
|
-
body,
|
|
2022
|
-
rawResponse: true,
|
|
2023
|
-
})
|
|
2024
|
-
|
|
2025
|
-
return result
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
/**
|
|
2029
|
-
* Generate a presigned URL for S3 object access
|
|
2030
|
-
* Allows temporary access to private objects without authentication
|
|
2031
|
-
*/
|
|
2032
|
-
async getSignedUrl(options: {
|
|
2033
|
-
bucket: string
|
|
2034
|
-
key: string
|
|
2035
|
-
expiresIn?: number
|
|
2036
|
-
operation?: 'getObject' | 'putObject'
|
|
2037
|
-
}): Promise<string> {
|
|
2038
|
-
const { bucket, key, expiresIn = 3600, operation = 'getObject' } = options
|
|
2039
|
-
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials()
|
|
2040
|
-
|
|
2041
|
-
const now = new Date()
|
|
2042
|
-
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
|
|
2043
|
-
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '')
|
|
2044
|
-
|
|
2045
|
-
const host = `${bucket}.s3.${this.region}.amazonaws.com`
|
|
2046
|
-
const method = operation === 'putObject' ? 'PUT' : 'GET'
|
|
2047
|
-
const algorithm = 'AWS4-HMAC-SHA256'
|
|
2048
|
-
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`
|
|
2049
|
-
const credential = `${accessKeyId}/${credentialScope}`
|
|
2050
|
-
|
|
2051
|
-
// Build query parameters
|
|
2052
|
-
const queryParams: Record<string, string> = {
|
|
2053
|
-
'X-Amz-Algorithm': algorithm,
|
|
2054
|
-
'X-Amz-Credential': credential,
|
|
2055
|
-
'X-Amz-Date': amzDate,
|
|
2056
|
-
'X-Amz-Expires': expiresIn.toString(),
|
|
2057
|
-
'X-Amz-SignedHeaders': 'host',
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
if (sessionToken) {
|
|
2061
|
-
queryParams['X-Amz-Security-Token'] = sessionToken
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
// Sort and encode query string
|
|
2065
|
-
const sortedParams = Object.keys(queryParams).sort()
|
|
2066
|
-
const canonicalQuerystring = sortedParams
|
|
2067
|
-
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(queryParams[k])}`)
|
|
2068
|
-
.join('&')
|
|
2069
|
-
|
|
2070
|
-
// Canonical request
|
|
2071
|
-
const canonicalUri = '/' + key
|
|
2072
|
-
const canonicalHeaders = `host:${host}\n`
|
|
2073
|
-
const signedHeaders = 'host'
|
|
2074
|
-
const payloadHash = 'UNSIGNED-PAYLOAD'
|
|
2075
|
-
|
|
2076
|
-
const canonicalRequest = [
|
|
2077
|
-
method,
|
|
2078
|
-
canonicalUri,
|
|
2079
|
-
canonicalQuerystring,
|
|
2080
|
-
canonicalHeaders,
|
|
2081
|
-
signedHeaders,
|
|
2082
|
-
payloadHash,
|
|
2083
|
-
].join('\n')
|
|
2084
|
-
|
|
2085
|
-
// String to sign
|
|
2086
|
-
const stringToSign = [
|
|
2087
|
-
algorithm,
|
|
2088
|
-
amzDate,
|
|
2089
|
-
credentialScope,
|
|
2090
|
-
crypto.createHash('sha256').update(canonicalRequest).digest('hex'),
|
|
2091
|
-
].join('\n')
|
|
2092
|
-
|
|
2093
|
-
// Calculate signature
|
|
2094
|
-
const kDate = crypto.createHmac('sha256', `AWS4${secretAccessKey}`).update(dateStamp).digest()
|
|
2095
|
-
const kRegion = crypto.createHmac('sha256', kDate).update(this.region).digest()
|
|
2096
|
-
const kService = crypto.createHmac('sha256', kRegion).update('s3').digest()
|
|
2097
|
-
const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
|
|
2098
|
-
const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex')
|
|
2099
|
-
|
|
2100
|
-
// Build presigned URL
|
|
2101
|
-
const presignedUrl = `https://${host}${canonicalUri}?${canonicalQuerystring}&X-Amz-Signature=${signature}`
|
|
2102
|
-
|
|
2103
|
-
return presignedUrl
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
/**
|
|
2107
|
-
* List objects in a bucket with pagination support
|
|
2108
|
-
*/
|
|
2109
|
-
async listObjects(options: {
|
|
2110
|
-
bucket: string
|
|
2111
|
-
prefix?: string
|
|
2112
|
-
maxKeys?: number
|
|
2113
|
-
continuationToken?: string
|
|
2114
|
-
}): Promise<{
|
|
2115
|
-
objects: S3Object[]
|
|
2116
|
-
nextContinuationToken?: string
|
|
2117
|
-
}> {
|
|
2118
|
-
const { bucket, prefix, maxKeys = 1000, continuationToken } = options
|
|
2119
|
-
|
|
2120
|
-
// Build query parameters for ListObjectsV2
|
|
2121
|
-
const queryParams: Record<string, string> = {
|
|
2122
|
-
'list-type': '2',
|
|
2123
|
-
'max-keys': maxKeys.toString(),
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
|
-
if (prefix) queryParams.prefix = prefix
|
|
2127
|
-
if (continuationToken) queryParams['continuation-token'] = continuationToken
|
|
2128
|
-
|
|
2129
|
-
const result = await this.client.request({
|
|
2130
|
-
service: 's3',
|
|
2131
|
-
region: this.region,
|
|
2132
|
-
method: 'GET',
|
|
2133
|
-
path: `/${bucket}`,
|
|
2134
|
-
queryParams,
|
|
2135
|
-
})
|
|
2136
|
-
|
|
2137
|
-
// Parse S3 XML response
|
|
2138
|
-
const objects: S3Object[] = []
|
|
2139
|
-
const listResult = result?.ListBucketResult
|
|
2140
|
-
|
|
2141
|
-
if (listResult?.Contents) {
|
|
2142
|
-
const items = Array.isArray(listResult.Contents) ? listResult.Contents : [listResult.Contents]
|
|
2143
|
-
for (const item of items) {
|
|
2144
|
-
objects.push({
|
|
2145
|
-
Key: item.Key || '',
|
|
2146
|
-
LastModified: item.LastModified || '',
|
|
2147
|
-
Size: Number.parseInt(item.Size || '0'),
|
|
2148
|
-
ETag: item.ETag,
|
|
2149
|
-
})
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
return {
|
|
2154
|
-
objects,
|
|
2155
|
-
nextContinuationToken: listResult?.NextContinuationToken,
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
/**
|
|
2160
|
-
* Empty a bucket by deleting all objects (required before bucket deletion)
|
|
2161
|
-
*/
|
|
2162
|
-
async emptyBucket(bucket: string): Promise<{ deletedCount: number }> {
|
|
2163
|
-
let deletedCount = 0
|
|
2164
|
-
|
|
2165
|
-
// List all objects in the bucket (handles pagination internally)
|
|
2166
|
-
const objects = await this.listAllObjects({ bucket })
|
|
2167
|
-
|
|
2168
|
-
if (objects.length > 0) {
|
|
2169
|
-
// Delete objects in batches of 1000
|
|
2170
|
-
for (let i = 0; i < objects.length; i += 1000) {
|
|
2171
|
-
const batch = objects.slice(i, i + 1000)
|
|
2172
|
-
const keys = batch.map((obj: S3Object) => obj.Key)
|
|
2173
|
-
await this.deleteObjects(bucket, keys)
|
|
2174
|
-
deletedCount += keys.length
|
|
2175
|
-
}
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
// Also delete any object versions if versioning is enabled
|
|
2179
|
-
try {
|
|
2180
|
-
let keyMarker: string | undefined
|
|
2181
|
-
let versionIdMarker: string | undefined
|
|
2182
|
-
|
|
2183
|
-
do {
|
|
2184
|
-
const versionsResult = await this.listObjectVersions({
|
|
2185
|
-
bucket,
|
|
2186
|
-
keyMarker,
|
|
2187
|
-
versionIdMarker,
|
|
2188
|
-
maxKeys: 1000,
|
|
2189
|
-
})
|
|
2190
|
-
|
|
2191
|
-
const versionsToDelete: Array<{ Key: string; VersionId?: string }> = []
|
|
2192
|
-
|
|
2193
|
-
if (versionsResult.versions) {
|
|
2194
|
-
for (const version of versionsResult.versions) {
|
|
2195
|
-
versionsToDelete.push({ Key: version.Key, VersionId: version.VersionId })
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
if (versionsResult.deleteMarkers) {
|
|
2200
|
-
for (const marker of versionsResult.deleteMarkers) {
|
|
2201
|
-
versionsToDelete.push({ Key: marker.Key, VersionId: marker.VersionId })
|
|
2202
|
-
}
|
|
2203
|
-
}
|
|
2204
|
-
|
|
2205
|
-
if (versionsToDelete.length > 0) {
|
|
2206
|
-
await this.deleteObjectVersions(bucket, versionsToDelete)
|
|
2207
|
-
deletedCount += versionsToDelete.length
|
|
2208
|
-
}
|
|
2209
|
-
|
|
2210
|
-
keyMarker = versionsResult.nextKeyMarker
|
|
2211
|
-
versionIdMarker = versionsResult.nextVersionIdMarker
|
|
2212
|
-
} while (keyMarker)
|
|
2213
|
-
}
|
|
2214
|
-
catch {
|
|
2215
|
-
// Versioning might not be enabled, ignore errors
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2218
|
-
return { deletedCount }
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
/**
|
|
2222
|
-
* List object versions in a bucket
|
|
2223
|
-
*/
|
|
2224
|
-
async listObjectVersions(options: {
|
|
2225
|
-
bucket: string
|
|
2226
|
-
prefix?: string
|
|
2227
|
-
keyMarker?: string
|
|
2228
|
-
versionIdMarker?: string
|
|
2229
|
-
maxKeys?: number
|
|
2230
|
-
}): Promise<{
|
|
2231
|
-
versions: Array<{ Key: string; VersionId: string; IsLatest: boolean }>
|
|
2232
|
-
deleteMarkers: Array<{ Key: string; VersionId: string; IsLatest: boolean }>
|
|
2233
|
-
nextKeyMarker?: string
|
|
2234
|
-
nextVersionIdMarker?: string
|
|
2235
|
-
}> {
|
|
2236
|
-
const { bucket, prefix, keyMarker, versionIdMarker, maxKeys = 1000 } = options
|
|
2237
|
-
|
|
2238
|
-
const queryParams: Record<string, string> = {
|
|
2239
|
-
versions: '',
|
|
2240
|
-
'max-keys': maxKeys.toString(),
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
if (prefix) queryParams.prefix = prefix
|
|
2244
|
-
if (keyMarker) queryParams['key-marker'] = keyMarker
|
|
2245
|
-
if (versionIdMarker) queryParams['version-id-marker'] = versionIdMarker
|
|
2246
|
-
|
|
2247
|
-
const result = await this.client.request({
|
|
2248
|
-
service: 's3',
|
|
2249
|
-
region: this.region,
|
|
2250
|
-
method: 'GET',
|
|
2251
|
-
path: `/${bucket}`,
|
|
2252
|
-
queryParams,
|
|
2253
|
-
})
|
|
2254
|
-
|
|
2255
|
-
const versions: Array<{ Key: string; VersionId: string; IsLatest: boolean }> = []
|
|
2256
|
-
const deleteMarkers: Array<{ Key: string; VersionId: string; IsLatest: boolean }> = []
|
|
2257
|
-
|
|
2258
|
-
// Parse versions
|
|
2259
|
-
if (result.Version) {
|
|
2260
|
-
const versionList = Array.isArray(result.Version) ? result.Version : [result.Version]
|
|
2261
|
-
for (const v of versionList) {
|
|
2262
|
-
versions.push({
|
|
2263
|
-
Key: v.Key,
|
|
2264
|
-
VersionId: v.VersionId,
|
|
2265
|
-
IsLatest: v.IsLatest === 'true',
|
|
2266
|
-
})
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
// Parse delete markers
|
|
2271
|
-
if (result.DeleteMarker) {
|
|
2272
|
-
const markerList = Array.isArray(result.DeleteMarker) ? result.DeleteMarker : [result.DeleteMarker]
|
|
2273
|
-
for (const m of markerList) {
|
|
2274
|
-
deleteMarkers.push({
|
|
2275
|
-
Key: m.Key,
|
|
2276
|
-
VersionId: m.VersionId,
|
|
2277
|
-
IsLatest: m.IsLatest === 'true',
|
|
2278
|
-
})
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
return {
|
|
2283
|
-
versions,
|
|
2284
|
-
deleteMarkers,
|
|
2285
|
-
nextKeyMarker: result.NextKeyMarker,
|
|
2286
|
-
nextVersionIdMarker: result.NextVersionIdMarker,
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
/**
|
|
2291
|
-
* Delete specific object versions
|
|
2292
|
-
*/
|
|
2293
|
-
async deleteObjectVersions(
|
|
2294
|
-
bucket: string,
|
|
2295
|
-
objects: Array<{ Key: string; VersionId?: string }>,
|
|
2296
|
-
): Promise<void> {
|
|
2297
|
-
const deleteXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2298
|
-
<Delete>
|
|
2299
|
-
<Quiet>true</Quiet>
|
|
2300
|
-
${objects.map(obj => `<Object><Key>${obj.Key}</Key>${obj.VersionId ? `<VersionId>${obj.VersionId}</VersionId>` : ''}</Object>`).join('\n ')}
|
|
2301
|
-
</Delete>`
|
|
2302
|
-
|
|
2303
|
-
const contentMd5 = crypto.createHash('md5').update(deleteXml).digest('base64')
|
|
2304
|
-
|
|
2305
|
-
await this.client.request({
|
|
2306
|
-
service: 's3',
|
|
2307
|
-
region: this.region,
|
|
2308
|
-
method: 'POST',
|
|
2309
|
-
path: `/${bucket}`,
|
|
2310
|
-
queryParams: { delete: '' },
|
|
2311
|
-
body: deleteXml,
|
|
2312
|
-
headers: {
|
|
2313
|
-
'Content-Type': 'application/xml',
|
|
2314
|
-
'Content-MD5': contentMd5,
|
|
2315
|
-
},
|
|
2316
|
-
})
|
|
2317
|
-
}
|
|
2318
|
-
}
|