@hanzo/s3 0.6.3 → 8.0.7

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.
Files changed (119) hide show
  1. package/LICENSE +202 -0
  2. package/MAINTAINERS.md +62 -0
  3. package/README.md +262 -0
  4. package/README_zh_CN.md +192 -0
  5. package/dist/esm/AssumeRoleProvider.d.mts +86 -0
  6. package/dist/esm/AssumeRoleProvider.mjs +183 -0
  7. package/dist/esm/CredentialProvider.d.mts +22 -0
  8. package/dist/esm/CredentialProvider.mjs +48 -0
  9. package/dist/esm/Credentials.d.mts +22 -0
  10. package/dist/esm/Credentials.mjs +38 -0
  11. package/dist/esm/IamAwsProvider.d.mts +27 -0
  12. package/dist/esm/IamAwsProvider.mjs +189 -0
  13. package/dist/esm/errors.d.mts +82 -0
  14. package/dist/esm/errors.mjs +117 -0
  15. package/dist/esm/helpers.d.mts +156 -0
  16. package/dist/esm/helpers.mjs +218 -0
  17. package/dist/esm/internal/async.d.mts +9 -0
  18. package/dist/esm/internal/async.mjs +14 -0
  19. package/dist/esm/internal/callbackify.d.mts +1 -0
  20. package/dist/esm/internal/callbackify.mjs +15 -0
  21. package/dist/esm/internal/client.d.mts +394 -0
  22. package/dist/esm/internal/client.mjs +3007 -0
  23. package/dist/esm/internal/copy-conditions.d.mts +10 -0
  24. package/dist/esm/internal/copy-conditions.mjs +25 -0
  25. package/dist/esm/internal/extensions.d.mts +18 -0
  26. package/dist/esm/internal/extensions.mjs +114 -0
  27. package/dist/esm/internal/helper.d.mts +177 -0
  28. package/dist/esm/internal/helper.mjs +552 -0
  29. package/dist/esm/internal/join-host-port.d.mts +11 -0
  30. package/dist/esm/internal/join-host-port.mjs +23 -0
  31. package/dist/esm/internal/post-policy.d.mts +17 -0
  32. package/dist/esm/internal/post-policy.mjs +98 -0
  33. package/dist/esm/internal/request.d.mts +11 -0
  34. package/dist/esm/internal/request.mjs +75 -0
  35. package/dist/esm/internal/response.d.mts +8 -0
  36. package/dist/esm/internal/response.mjs +16 -0
  37. package/dist/esm/internal/s3-endpoints.d.mts +38 -0
  38. package/dist/esm/internal/s3-endpoints.mjs +68 -0
  39. package/dist/esm/internal/type.d.mts +482 -0
  40. package/dist/esm/internal/type.mjs +30 -0
  41. package/dist/esm/internal/xml-parser.d.mts +93 -0
  42. package/dist/esm/internal/xml-parser.mjs +819 -0
  43. package/dist/esm/notification.d.mts +58 -0
  44. package/dist/esm/notification.mjs +209 -0
  45. package/dist/esm/s3.d.mts +40 -0
  46. package/dist/esm/s3.mjs +86 -0
  47. package/dist/esm/signing.d.mts +5 -0
  48. package/dist/esm/signing.mjs +258 -0
  49. package/dist/main/AssumeRoleProvider.d.ts +86 -0
  50. package/dist/main/AssumeRoleProvider.js +191 -0
  51. package/dist/main/CredentialProvider.d.ts +22 -0
  52. package/dist/main/CredentialProvider.js +55 -0
  53. package/dist/main/Credentials.d.ts +22 -0
  54. package/dist/main/Credentials.js +45 -0
  55. package/dist/main/IamAwsProvider.d.ts +27 -0
  56. package/dist/main/IamAwsProvider.js +198 -0
  57. package/dist/main/errors.d.ts +82 -0
  58. package/dist/main/errors.js +138 -0
  59. package/dist/main/helpers.d.ts +156 -0
  60. package/dist/main/helpers.js +233 -0
  61. package/dist/main/internal/async.d.ts +9 -0
  62. package/dist/main/internal/async.js +24 -0
  63. package/dist/main/internal/callbackify.d.ts +1 -0
  64. package/dist/main/internal/callbackify.js +21 -0
  65. package/dist/main/internal/client.d.ts +394 -0
  66. package/dist/main/internal/client.js +3014 -0
  67. package/dist/main/internal/copy-conditions.d.ts +10 -0
  68. package/dist/main/internal/copy-conditions.js +31 -0
  69. package/dist/main/internal/extensions.d.ts +18 -0
  70. package/dist/main/internal/extensions.js +122 -0
  71. package/dist/main/internal/helper.d.ts +177 -0
  72. package/dist/main/internal/helper.js +608 -0
  73. package/dist/main/internal/join-host-port.d.ts +11 -0
  74. package/dist/main/internal/join-host-port.js +29 -0
  75. package/dist/main/internal/post-policy.d.ts +17 -0
  76. package/dist/main/internal/post-policy.js +107 -0
  77. package/dist/main/internal/request.d.ts +11 -0
  78. package/dist/main/internal/request.js +83 -0
  79. package/dist/main/internal/response.d.ts +8 -0
  80. package/dist/main/internal/response.js +24 -0
  81. package/dist/main/internal/s3-endpoints.d.ts +38 -0
  82. package/dist/main/internal/s3-endpoints.js +73 -0
  83. package/dist/main/internal/type.d.ts +482 -0
  84. package/dist/main/internal/type.js +42 -0
  85. package/dist/main/internal/xml-parser.d.ts +93 -0
  86. package/dist/main/internal/xml-parser.js +849 -0
  87. package/dist/main/notification.d.ts +58 -0
  88. package/dist/main/notification.js +230 -0
  89. package/dist/main/s3.d.ts +40 -0
  90. package/dist/main/s3.js +117 -0
  91. package/dist/main/signing.d.ts +5 -0
  92. package/dist/main/signing.js +269 -0
  93. package/package.json +146 -39
  94. package/src/AssumeRoleProvider.ts +262 -0
  95. package/src/CredentialProvider.ts +54 -0
  96. package/src/Credentials.ts +44 -0
  97. package/src/IamAwsProvider.ts +234 -0
  98. package/src/errors.ts +120 -0
  99. package/src/helpers.ts +354 -0
  100. package/src/internal/async.ts +14 -0
  101. package/src/internal/callbackify.ts +19 -0
  102. package/src/internal/client.ts +3412 -0
  103. package/src/internal/copy-conditions.ts +30 -0
  104. package/src/internal/extensions.ts +140 -0
  105. package/src/internal/helper.ts +606 -0
  106. package/src/internal/join-host-port.ts +23 -0
  107. package/src/internal/post-policy.ts +99 -0
  108. package/src/internal/request.ts +102 -0
  109. package/src/internal/response.ts +26 -0
  110. package/src/internal/s3-endpoints.ts +70 -0
  111. package/src/internal/type.ts +577 -0
  112. package/src/internal/xml-parser.ts +871 -0
  113. package/src/notification.ts +254 -0
  114. package/src/s3.ts +155 -0
  115. package/src/signing.ts +325 -0
  116. package/lib/index.js +0 -450
  117. package/lib/index.js.map +0 -7
  118. package/lib/perfTest.js +0 -91
  119. package/lib/perfTest.js.map +0 -7
@@ -0,0 +1,871 @@
1
+ import type * as http from 'node:http'
2
+ import type stream from 'node:stream'
3
+
4
+ import crc32 from 'buffer-crc32'
5
+ import { XMLParser } from 'fast-xml-parser'
6
+
7
+ import * as errors from '../errors.ts'
8
+ import { SelectResults } from '../helpers.ts'
9
+ import { isObject, parseXml, readableStream, sanitizeETag, sanitizeObjectKey, sanitizeSize, toArray } from './helper.ts'
10
+ import { readAsString } from './response.ts'
11
+ import type {
12
+ BucketItemFromList,
13
+ BucketItemWithMetadata,
14
+ CloudFunctionConfigEntry,
15
+ CommonPrefix,
16
+ CopyObjectResultV1,
17
+ ListBucketResultV1,
18
+ ListObjectV2Res,
19
+ NotificationConfigResult,
20
+ ObjectInfo,
21
+ ObjectLockInfo,
22
+ ObjectRowEntry,
23
+ QueueConfigEntry,
24
+ ReplicationConfig,
25
+ Tag,
26
+ Tags,
27
+ TopicConfigEntry,
28
+ } from './type.ts'
29
+ import { RETENTION_VALIDITY_UNITS } from './type.ts'
30
+
31
+ // parse XML response for bucket region
32
+ export function parseBucketRegion(xml: string): string {
33
+ // return region information
34
+ return parseXml(xml).LocationConstraint
35
+ }
36
+
37
+ const fxp = new XMLParser()
38
+
39
+ const fxpWithoutNumParser = new XMLParser({
40
+ // @ts-ignore
41
+ numberParseOptions: {
42
+ skipLike: /./,
43
+ },
44
+ })
45
+
46
+ // Parse XML and return information as Javascript types
47
+ // parse error XML response
48
+ export function parseError(xml: string, headerInfo: Record<string, unknown>) {
49
+ let xmlErr = {}
50
+ const xmlObj = fxp.parse(xml)
51
+ if (xmlObj.Error) {
52
+ xmlErr = xmlObj.Error
53
+ }
54
+ const e = new errors.S3Error() as unknown as Record<string, unknown>
55
+ Object.entries(xmlErr).forEach(([key, value]) => {
56
+ e[key.toLowerCase()] = value
57
+ })
58
+ Object.entries(headerInfo).forEach(([key, value]) => {
59
+ e[key] = value
60
+ })
61
+ return e
62
+ }
63
+
64
+ // Generates an Error object depending on http statusCode and XML body
65
+ export async function parseResponseError(response: http.IncomingMessage): Promise<Record<string, string>> {
66
+ const statusCode = response.statusCode
67
+ let code = '',
68
+ message = ''
69
+ if (statusCode === 301) {
70
+ code = 'MovedPermanently'
71
+ message = 'Moved Permanently'
72
+ } else if (statusCode === 307) {
73
+ code = 'TemporaryRedirect'
74
+ message = 'Are you using the correct endpoint URL?'
75
+ } else if (statusCode === 403) {
76
+ code = 'AccessDenied'
77
+ message = 'Valid and authorized credentials required'
78
+ } else if (statusCode === 404) {
79
+ code = 'NotFound'
80
+ message = 'Not Found'
81
+ } else if (statusCode === 405) {
82
+ code = 'MethodNotAllowed'
83
+ message = 'Method Not Allowed'
84
+ } else if (statusCode === 501) {
85
+ code = 'MethodNotAllowed'
86
+ message = 'Method Not Allowed'
87
+ } else if (statusCode === 503) {
88
+ code = 'SlowDown'
89
+ message = 'Please reduce your request rate.'
90
+ } else {
91
+ // Server-side error headers (protocol-level, sent by S3-compatible servers)
92
+ const hErrCode = (response.headers['x-s3-error-code'] ?? response.headers['x-minio-error-code']) as string
93
+ const hErrDesc = (response.headers['x-s3-error-desc'] ?? response.headers['x-minio-error-desc']) as string
94
+
95
+ if (hErrCode && hErrDesc) {
96
+ code = hErrCode
97
+ message = hErrDesc
98
+ }
99
+ }
100
+ const headerInfo: Record<string, string | undefined | null> = {}
101
+ // A value created by S3 compatible server that uniquely identifies the request.
102
+ headerInfo.amzRequestid = response.headers['x-amz-request-id'] as string | undefined
103
+ // A special token that helps troubleshoot API replies and issues.
104
+ headerInfo.amzId2 = response.headers['x-amz-id-2'] as string | undefined
105
+
106
+ // Region where the bucket is located. This header is returned only
107
+ // in HEAD bucket and ListObjects response.
108
+ headerInfo.amzBucketRegion = response.headers['x-amz-bucket-region'] as string | undefined
109
+
110
+ const xmlString = await readAsString(response)
111
+
112
+ if (xmlString) {
113
+ throw parseError(xmlString, headerInfo)
114
+ }
115
+
116
+ // Message should be instantiated for each S3Errors.
117
+ const e = new errors.S3Error(message, { cause: headerInfo })
118
+ // S3 Error code.
119
+ e.code = code
120
+ Object.entries(headerInfo).forEach(([key, value]) => {
121
+ // @ts-expect-error force set error properties
122
+ e[key] = value
123
+ })
124
+
125
+ throw e
126
+ }
127
+
128
+ /**
129
+ * parse XML response for list objects v2 with metadata in a bucket
130
+ */
131
+ export function parseListObjectsV2WithMetadata(xml: string) {
132
+ const result: {
133
+ objects: Array<BucketItemWithMetadata>
134
+ isTruncated: boolean
135
+ nextContinuationToken: string
136
+ } = {
137
+ objects: [],
138
+ isTruncated: false,
139
+ nextContinuationToken: '',
140
+ }
141
+
142
+ let xmlobj = parseXml(xml)
143
+ if (!xmlobj.ListBucketResult) {
144
+ throw new errors.InvalidXMLError('Missing tag: "ListBucketResult"')
145
+ }
146
+ xmlobj = xmlobj.ListBucketResult
147
+ if (xmlobj.IsTruncated) {
148
+ result.isTruncated = xmlobj.IsTruncated
149
+ }
150
+ if (xmlobj.NextContinuationToken) {
151
+ result.nextContinuationToken = xmlobj.NextContinuationToken
152
+ }
153
+
154
+ if (xmlobj.Contents) {
155
+ toArray(xmlobj.Contents).forEach((content) => {
156
+ const name = sanitizeObjectKey(content.Key)
157
+ const lastModified = new Date(content.LastModified)
158
+ const etag = sanitizeETag(content.ETag)
159
+ const size = content.Size
160
+
161
+ let tags: Tags = {}
162
+ if (content.UserTags != null) {
163
+ toArray(content.UserTags.split('&')).forEach((tag) => {
164
+ const [key, value] = tag.split('=')
165
+ tags[key] = value
166
+ })
167
+ } else {
168
+ tags = {}
169
+ }
170
+
171
+ let metadata
172
+ if (content.UserMetadata != null) {
173
+ metadata = toArray(content.UserMetadata)[0]
174
+ } else {
175
+ metadata = null
176
+ }
177
+ result.objects.push({ name, lastModified, etag, size, metadata, tags })
178
+ })
179
+ }
180
+
181
+ if (xmlobj.CommonPrefixes) {
182
+ toArray(xmlobj.CommonPrefixes).forEach((commonPrefix) => {
183
+ result.objects.push({ prefix: sanitizeObjectKey(toArray(commonPrefix.Prefix)[0]), size: 0 })
184
+ })
185
+ }
186
+ return result
187
+ }
188
+
189
+ export function parseListObjectsV2(xml: string): ListObjectV2Res {
190
+ const result: ListObjectV2Res = {
191
+ objects: [],
192
+ isTruncated: false,
193
+ nextContinuationToken: '',
194
+ }
195
+
196
+ let xmlobj = parseXml(xml)
197
+ if (!xmlobj.ListBucketResult) {
198
+ throw new errors.InvalidXMLError('Missing tag: "ListBucketResult"')
199
+ }
200
+ xmlobj = xmlobj.ListBucketResult
201
+ if (xmlobj.IsTruncated) {
202
+ result.isTruncated = xmlobj.IsTruncated
203
+ }
204
+ if (xmlobj.NextContinuationToken) {
205
+ result.nextContinuationToken = xmlobj.NextContinuationToken
206
+ }
207
+ if (xmlobj.Contents) {
208
+ toArray(xmlobj.Contents).forEach((content) => {
209
+ const name = sanitizeObjectKey(toArray(content.Key)[0])
210
+ const lastModified = new Date(content.LastModified)
211
+ const etag = sanitizeETag(content.ETag)
212
+ const size = content.Size
213
+ result.objects.push({ name, lastModified, etag, size })
214
+ })
215
+ }
216
+ if (xmlobj.CommonPrefixes) {
217
+ toArray(xmlobj.CommonPrefixes).forEach((commonPrefix) => {
218
+ result.objects.push({ prefix: sanitizeObjectKey(toArray(commonPrefix.Prefix)[0]), size: 0 })
219
+ })
220
+ }
221
+ return result
222
+ }
223
+
224
+ export function parseBucketNotification(xml: string): NotificationConfigResult {
225
+ const result: NotificationConfigResult = {
226
+ TopicConfiguration: [],
227
+ QueueConfiguration: [],
228
+ CloudFunctionConfiguration: [],
229
+ }
230
+
231
+ const genEvents = (events: unknown): string[] => {
232
+ if (!events) {
233
+ return []
234
+ }
235
+ return toArray(events) as string[]
236
+ }
237
+
238
+ const genFilterRules = (filters: unknown): { Name: string; Value: string }[] => {
239
+ const rules: { Name: string; Value: string }[] = []
240
+ if (!filters) {
241
+ return rules
242
+ }
243
+ const filterArr = toArray(filters) as Record<string, unknown>[]
244
+ if (filterArr[0]?.S3Key) {
245
+ const s3KeyArr = toArray((filterArr[0] as Record<string, unknown>).S3Key) as Record<string, unknown>[]
246
+ if (s3KeyArr[0]?.FilterRule) {
247
+ toArray(s3KeyArr[0].FilterRule).forEach((rule: unknown) => {
248
+ const r = rule as Record<string, unknown>
249
+ const Name = toArray(r.Name)[0] as string
250
+ const Value = toArray(r.Value)[0] as string
251
+ rules.push({ Name, Value })
252
+ })
253
+ }
254
+ }
255
+ return rules
256
+ }
257
+
258
+ let xmlobj = parseXml(xml)
259
+ xmlobj = xmlobj.NotificationConfiguration
260
+
261
+ if (xmlobj.TopicConfiguration) {
262
+ toArray(xmlobj.TopicConfiguration).forEach((config: Record<string, unknown>) => {
263
+ const Id = toArray(config.Id)[0] as string
264
+ const Topic = toArray(config.Topic)[0] as string
265
+ const Event = genEvents(config.Event)
266
+ const Filter = genFilterRules(config.Filter)
267
+ result.TopicConfiguration.push({ Id, Topic, Event, Filter } as TopicConfigEntry)
268
+ })
269
+ }
270
+ if (xmlobj.QueueConfiguration) {
271
+ toArray(xmlobj.QueueConfiguration).forEach((config: Record<string, unknown>) => {
272
+ const Id = toArray(config.Id)[0] as string
273
+ const Queue = toArray(config.Queue)[0] as string
274
+ const Event = genEvents(config.Event)
275
+ const Filter = genFilterRules(config.Filter)
276
+ result.QueueConfiguration.push({ Id, Queue, Event, Filter } as QueueConfigEntry)
277
+ })
278
+ }
279
+ if (xmlobj.CloudFunctionConfiguration) {
280
+ toArray(xmlobj.CloudFunctionConfiguration).forEach((config: Record<string, unknown>) => {
281
+ const Id = toArray(config.Id)[0] as string
282
+ const CloudFunction = toArray(config.CloudFunction)[0] as string
283
+ const Event = genEvents(config.Event)
284
+ const Filter = genFilterRules(config.Filter)
285
+ result.CloudFunctionConfiguration.push({ Id, CloudFunction, Event, Filter } as CloudFunctionConfigEntry)
286
+ })
287
+ }
288
+
289
+ return result
290
+ }
291
+
292
+ export type UploadedPart = {
293
+ part: number
294
+ lastModified?: Date
295
+ etag: string
296
+ size: number
297
+ }
298
+
299
+ // parse XML response for list parts of an in progress multipart upload
300
+ export function parseListParts(xml: string): {
301
+ isTruncated: boolean
302
+ marker: number
303
+ parts: UploadedPart[]
304
+ } {
305
+ let xmlobj = parseXml(xml)
306
+ const result: {
307
+ isTruncated: boolean
308
+ marker: number
309
+ parts: UploadedPart[]
310
+ } = {
311
+ isTruncated: false,
312
+ parts: [],
313
+ marker: 0,
314
+ }
315
+ if (!xmlobj.ListPartsResult) {
316
+ throw new errors.InvalidXMLError('Missing tag: "ListPartsResult"')
317
+ }
318
+ xmlobj = xmlobj.ListPartsResult
319
+ if (xmlobj.IsTruncated) {
320
+ result.isTruncated = xmlobj.IsTruncated
321
+ }
322
+ if (xmlobj.NextPartNumberMarker) {
323
+ result.marker = toArray(xmlobj.NextPartNumberMarker)[0] || ''
324
+ }
325
+ if (xmlobj.Part) {
326
+ toArray(xmlobj.Part).forEach((p) => {
327
+ const part = parseInt(toArray(p.PartNumber)[0], 10)
328
+ const lastModified = new Date(p.LastModified)
329
+ const etag = p.ETag.replace(/^"/g, '')
330
+ .replace(/"$/g, '')
331
+ .replace(/^&quot;/g, '')
332
+ .replace(/&quot;$/g, '')
333
+ .replace(/^&#34;/g, '')
334
+ .replace(/&#34;$/g, '')
335
+ result.parts.push({ part, lastModified, etag, size: parseInt(p.Size, 10) })
336
+ })
337
+ }
338
+ return result
339
+ }
340
+
341
+ export function parseListBucket(xml: string): BucketItemFromList[] {
342
+ let result: BucketItemFromList[] = []
343
+ const listBucketResultParser = new XMLParser({
344
+ parseTagValue: true, // Enable parsing of values
345
+ numberParseOptions: {
346
+ leadingZeros: false, // Disable number parsing for values with leading zeros
347
+ hex: false, // Disable hex number parsing - Invalid bucket name
348
+ skipLike: /^[0-9]+$/, // Skip number parsing if the value consists entirely of digits
349
+ },
350
+ tagValueProcessor: (tagName, tagValue = '') => {
351
+ // Ensure that the Name tag is always treated as a string
352
+ if (tagName === 'Name') {
353
+ return tagValue.toString()
354
+ }
355
+ return tagValue
356
+ },
357
+ ignoreAttributes: false, // Ensure that all attributes are parsed
358
+ })
359
+
360
+ const parsedXmlRes = listBucketResultParser.parse(xml)
361
+
362
+ if (!parsedXmlRes.ListAllMyBucketsResult) {
363
+ throw new errors.InvalidXMLError('Missing tag: "ListAllMyBucketsResult"')
364
+ }
365
+
366
+ const { ListAllMyBucketsResult: { Buckets = {} } = {} } = parsedXmlRes
367
+
368
+ if (Buckets.Bucket) {
369
+ result = toArray(Buckets.Bucket).map((bucket = {}) => {
370
+ const { Name: bucketName, CreationDate } = bucket
371
+ const creationDate = new Date(CreationDate)
372
+
373
+ return { name: bucketName, creationDate }
374
+ })
375
+ }
376
+
377
+ return result
378
+ }
379
+
380
+ export function parseInitiateMultipart(xml: string): string {
381
+ let xmlobj = parseXml(xml)
382
+
383
+ if (!xmlobj.InitiateMultipartUploadResult) {
384
+ throw new errors.InvalidXMLError('Missing tag: "InitiateMultipartUploadResult"')
385
+ }
386
+ xmlobj = xmlobj.InitiateMultipartUploadResult
387
+
388
+ if (xmlobj.UploadId) {
389
+ return xmlobj.UploadId
390
+ }
391
+ throw new errors.InvalidXMLError('Missing tag: "UploadId"')
392
+ }
393
+
394
+ export function parseReplicationConfig(xml: string): ReplicationConfig {
395
+ const xmlObj = parseXml(xml)
396
+ const { Role, Rule } = xmlObj.ReplicationConfiguration
397
+ return {
398
+ ReplicationConfiguration: {
399
+ role: Role,
400
+ rules: toArray(Rule),
401
+ },
402
+ }
403
+ }
404
+
405
+ export function parseObjectLegalHoldConfig(xml: string) {
406
+ const xmlObj = parseXml(xml)
407
+ return xmlObj.LegalHold
408
+ }
409
+
410
+ export function parseTagging(xml: string) {
411
+ const xmlObj = parseXml(xml)
412
+ let result: Tag[] = []
413
+ if (xmlObj.Tagging && xmlObj.Tagging.TagSet && xmlObj.Tagging.TagSet.Tag) {
414
+ const tagResult: Tag = xmlObj.Tagging.TagSet.Tag
415
+ // if it is a single tag convert into an array so that the return value is always an array.
416
+ if (Array.isArray(tagResult)) {
417
+ result = [...tagResult]
418
+ } else {
419
+ result.push(tagResult)
420
+ }
421
+ }
422
+ return result
423
+ }
424
+
425
+ // parse XML response when a multipart upload is completed
426
+ export function parseCompleteMultipart(xml: string) {
427
+ const xmlobj = parseXml(xml).CompleteMultipartUploadResult
428
+ if (xmlobj.Location) {
429
+ const location = toArray(xmlobj.Location)[0]
430
+ const bucket = toArray(xmlobj.Bucket)[0]
431
+ const key = xmlobj.Key
432
+ const etag = xmlobj.ETag.replace(/^"/g, '')
433
+ .replace(/"$/g, '')
434
+ .replace(/^&quot;/g, '')
435
+ .replace(/&quot;$/g, '')
436
+ .replace(/^&#34;/g, '')
437
+ .replace(/&#34;$/g, '')
438
+
439
+ return { location, bucket, key, etag }
440
+ }
441
+ // Complete Multipart can return XML Error after a 200 OK response
442
+ if (xmlobj.Code && xmlobj.Message) {
443
+ const errCode = toArray(xmlobj.Code)[0]
444
+ const errMessage = toArray(xmlobj.Message)[0]
445
+ return { errCode, errMessage }
446
+ }
447
+ }
448
+
449
+ type UploadID = string
450
+
451
+ export type ListMultipartResult = {
452
+ uploads: {
453
+ key: string
454
+ uploadId: UploadID
455
+ initiator?: { id: string; displayName: string }
456
+ owner?: { id: string; displayName: string }
457
+ storageClass: unknown
458
+ initiated: Date
459
+ }[]
460
+ prefixes: {
461
+ prefix: string
462
+ }[]
463
+ isTruncated: boolean
464
+ nextKeyMarker: string
465
+ nextUploadIdMarker: string
466
+ }
467
+
468
+ // parse XML response for listing in-progress multipart uploads
469
+ export function parseListMultipart(xml: string): ListMultipartResult {
470
+ const result: ListMultipartResult = {
471
+ prefixes: [],
472
+ uploads: [],
473
+ isTruncated: false,
474
+ nextKeyMarker: '',
475
+ nextUploadIdMarker: '',
476
+ }
477
+
478
+ let xmlobj = parseXml(xml)
479
+
480
+ if (!xmlobj.ListMultipartUploadsResult) {
481
+ throw new errors.InvalidXMLError('Missing tag: "ListMultipartUploadsResult"')
482
+ }
483
+ xmlobj = xmlobj.ListMultipartUploadsResult
484
+ if (xmlobj.IsTruncated) {
485
+ result.isTruncated = xmlobj.IsTruncated
486
+ }
487
+ if (xmlobj.NextKeyMarker) {
488
+ result.nextKeyMarker = xmlobj.NextKeyMarker
489
+ }
490
+ if (xmlobj.NextUploadIdMarker) {
491
+ result.nextUploadIdMarker = xmlobj.nextUploadIdMarker || ''
492
+ }
493
+
494
+ if (xmlobj.CommonPrefixes) {
495
+ toArray(xmlobj.CommonPrefixes).forEach((prefix) => {
496
+ // @ts-expect-error index check
497
+ result.prefixes.push({ prefix: sanitizeObjectKey(toArray<string>(prefix.Prefix)[0]) })
498
+ })
499
+ }
500
+
501
+ if (xmlobj.Upload) {
502
+ toArray(xmlobj.Upload).forEach((upload) => {
503
+ const uploadItem: ListMultipartResult['uploads'][number] = {
504
+ key: upload.Key,
505
+ uploadId: upload.UploadId,
506
+ storageClass: upload.StorageClass,
507
+ initiated: new Date(upload.Initiated),
508
+ }
509
+ if (upload.Initiator) {
510
+ uploadItem.initiator = { id: upload.Initiator.ID, displayName: upload.Initiator.DisplayName }
511
+ }
512
+ if (upload.Owner) {
513
+ uploadItem.owner = { id: upload.Owner.ID, displayName: upload.Owner.DisplayName }
514
+ }
515
+ result.uploads.push(uploadItem)
516
+ })
517
+ }
518
+ return result
519
+ }
520
+
521
+ export function parseObjectLockConfig(xml: string): ObjectLockInfo {
522
+ const xmlObj = parseXml(xml)
523
+ let lockConfigResult = {} as ObjectLockInfo
524
+ if (xmlObj.ObjectLockConfiguration) {
525
+ lockConfigResult = {
526
+ objectLockEnabled: xmlObj.ObjectLockConfiguration.ObjectLockEnabled,
527
+ } as ObjectLockInfo
528
+ let retentionResp
529
+ if (
530
+ xmlObj.ObjectLockConfiguration &&
531
+ xmlObj.ObjectLockConfiguration.Rule &&
532
+ xmlObj.ObjectLockConfiguration.Rule.DefaultRetention
533
+ ) {
534
+ retentionResp = xmlObj.ObjectLockConfiguration.Rule.DefaultRetention || {}
535
+ lockConfigResult.mode = retentionResp.Mode
536
+ }
537
+ if (retentionResp) {
538
+ const isUnitYears = retentionResp.Years
539
+ if (isUnitYears) {
540
+ lockConfigResult.validity = isUnitYears
541
+ lockConfigResult.unit = RETENTION_VALIDITY_UNITS.YEARS
542
+ } else {
543
+ lockConfigResult.validity = retentionResp.Days
544
+ lockConfigResult.unit = RETENTION_VALIDITY_UNITS.DAYS
545
+ }
546
+ }
547
+ }
548
+
549
+ return lockConfigResult
550
+ }
551
+
552
+ export function parseBucketVersioningConfig(xml: string) {
553
+ const xmlObj = parseXml(xml)
554
+ return xmlObj.VersioningConfiguration
555
+ }
556
+
557
+ // Used only in selectObjectContent API.
558
+ // extractHeaderType extracts the first half of the header message, the header type.
559
+ function extractHeaderType(stream: stream.Readable): string | undefined {
560
+ const headerNameLen = Buffer.from(stream.read(1)).readUInt8()
561
+ const headerNameWithSeparator = Buffer.from(stream.read(headerNameLen)).toString()
562
+ const splitBySeparator = (headerNameWithSeparator || '').split(':')
563
+ return splitBySeparator.length >= 1 ? splitBySeparator[1] : ''
564
+ }
565
+
566
+ function extractHeaderValue(stream: stream.Readable) {
567
+ const bodyLen = Buffer.from(stream.read(2)).readUInt16BE()
568
+ return Buffer.from(stream.read(bodyLen)).toString()
569
+ }
570
+
571
+ export function parseSelectObjectContentResponse(res: Buffer) {
572
+ const selectResults = new SelectResults({}) // will be returned
573
+
574
+ const responseStream = readableStream(res) // convert byte array to a readable responseStream
575
+ // @ts-ignore
576
+ while (responseStream._readableState.length) {
577
+ // Top level responseStream read tracker.
578
+ let msgCrcAccumulator // accumulate from start of the message till the message crc start.
579
+
580
+ const totalByteLengthBuffer = Buffer.from(responseStream.read(4))
581
+ msgCrcAccumulator = crc32(totalByteLengthBuffer)
582
+
583
+ const headerBytesBuffer = Buffer.from(responseStream.read(4))
584
+ msgCrcAccumulator = crc32(headerBytesBuffer, msgCrcAccumulator)
585
+
586
+ const calculatedPreludeCrc = msgCrcAccumulator.readInt32BE() // use it to check if any CRC mismatch in header itself.
587
+
588
+ const preludeCrcBuffer = Buffer.from(responseStream.read(4)) // read 4 bytes i.e 4+4 =8 + 4 = 12 ( prelude + prelude crc)
589
+ msgCrcAccumulator = crc32(preludeCrcBuffer, msgCrcAccumulator)
590
+
591
+ const totalMsgLength = totalByteLengthBuffer.readInt32BE()
592
+ const headerLength = headerBytesBuffer.readInt32BE()
593
+ const preludeCrcByteValue = preludeCrcBuffer.readInt32BE()
594
+
595
+ if (preludeCrcByteValue !== calculatedPreludeCrc) {
596
+ // Handle Header CRC mismatch Error
597
+ throw new Error(
598
+ `Header Checksum Mismatch, Prelude CRC of ${preludeCrcByteValue} does not equal expected CRC of ${calculatedPreludeCrc}`,
599
+ )
600
+ }
601
+
602
+ const headers: Record<string, unknown> = {}
603
+ if (headerLength > 0) {
604
+ const headerBytes = Buffer.from(responseStream.read(headerLength))
605
+ msgCrcAccumulator = crc32(headerBytes, msgCrcAccumulator)
606
+ const headerReaderStream = readableStream(headerBytes)
607
+ // @ts-ignore
608
+ while (headerReaderStream._readableState.length) {
609
+ const headerTypeName = extractHeaderType(headerReaderStream)
610
+ headerReaderStream.read(1) // just read and ignore it.
611
+ if (headerTypeName) {
612
+ headers[headerTypeName] = extractHeaderValue(headerReaderStream)
613
+ }
614
+ }
615
+ }
616
+
617
+ let payloadStream
618
+ const payLoadLength = totalMsgLength - headerLength - 16
619
+ if (payLoadLength > 0) {
620
+ const payLoadBuffer = Buffer.from(responseStream.read(payLoadLength))
621
+ msgCrcAccumulator = crc32(payLoadBuffer, msgCrcAccumulator)
622
+ // read the checksum early and detect any mismatch so we can avoid unnecessary further processing.
623
+ const messageCrcByteValue = Buffer.from(responseStream.read(4)).readInt32BE()
624
+ const calculatedCrc = msgCrcAccumulator.readInt32BE()
625
+ // Handle message CRC Error
626
+ if (messageCrcByteValue !== calculatedCrc) {
627
+ throw new Error(
628
+ `Message Checksum Mismatch, Message CRC of ${messageCrcByteValue} does not equal expected CRC of ${calculatedCrc}`,
629
+ )
630
+ }
631
+ payloadStream = readableStream(payLoadBuffer)
632
+ }
633
+ const messageType = headers['message-type']
634
+
635
+ switch (messageType) {
636
+ case 'error': {
637
+ const errorMessage = headers['error-code'] + ':"' + headers['error-message'] + '"'
638
+ throw new Error(errorMessage)
639
+ }
640
+ case 'event': {
641
+ const contentType = headers['content-type']
642
+ const eventType = headers['event-type']
643
+
644
+ switch (eventType) {
645
+ case 'End': {
646
+ selectResults.setResponse(res)
647
+ return selectResults
648
+ }
649
+
650
+ case 'Records': {
651
+ const readData = payloadStream?.read(payLoadLength)
652
+ selectResults.setRecords(readData)
653
+ break
654
+ }
655
+
656
+ case 'Progress':
657
+ {
658
+ switch (contentType) {
659
+ case 'text/xml': {
660
+ const progressData = payloadStream?.read(payLoadLength)
661
+ selectResults.setProgress(progressData.toString())
662
+ break
663
+ }
664
+ default: {
665
+ const errorMessage = `Unexpected content-type ${contentType} sent for event-type Progress`
666
+ throw new Error(errorMessage)
667
+ }
668
+ }
669
+ }
670
+ break
671
+ case 'Stats':
672
+ {
673
+ switch (contentType) {
674
+ case 'text/xml': {
675
+ const statsData = payloadStream?.read(payLoadLength)
676
+ selectResults.setStats(statsData.toString())
677
+ break
678
+ }
679
+ default: {
680
+ const errorMessage = `Unexpected content-type ${contentType} sent for event-type Stats`
681
+ throw new Error(errorMessage)
682
+ }
683
+ }
684
+ }
685
+ break
686
+ default: {
687
+ // Continuation message: Not sure if it is supported. did not find a reference or any message in response.
688
+ // It does not have a payload.
689
+ const warningMessage = `Un implemented event detected ${messageType}.`
690
+ // eslint-disable-next-line no-console
691
+ console.warn(warningMessage)
692
+ }
693
+ }
694
+ }
695
+ }
696
+ }
697
+ }
698
+
699
+ export function parseLifecycleConfig(xml: string) {
700
+ const xmlObj = parseXml(xml)
701
+ return xmlObj.LifecycleConfiguration
702
+ }
703
+
704
+ export function parseBucketEncryptionConfig(xml: string) {
705
+ return parseXml(xml)
706
+ }
707
+
708
+ export function parseObjectRetentionConfig(xml: string) {
709
+ const xmlObj = parseXml(xml)
710
+ const retentionConfig = xmlObj.Retention
711
+ return {
712
+ mode: retentionConfig.Mode,
713
+ retainUntilDate: retentionConfig.RetainUntilDate,
714
+ }
715
+ }
716
+
717
+ export function removeObjectsParser(xml: string) {
718
+ const xmlObj = parseXml(xml)
719
+ if (xmlObj.DeleteResult && xmlObj.DeleteResult.Error) {
720
+ // return errors as array always. as the response is object in case of single object passed in removeObjects
721
+ return toArray(xmlObj.DeleteResult.Error)
722
+ }
723
+ return []
724
+ }
725
+
726
+ // parse XML response for copy object
727
+ export function parseCopyObject(xml: string): CopyObjectResultV1 {
728
+ const result: CopyObjectResultV1 = {
729
+ etag: '',
730
+ lastModified: '',
731
+ }
732
+
733
+ let xmlobj = parseXml(xml)
734
+ if (!xmlobj.CopyObjectResult) {
735
+ throw new errors.InvalidXMLError('Missing tag: "CopyObjectResult"')
736
+ }
737
+ xmlobj = xmlobj.CopyObjectResult
738
+ if (xmlobj.ETag) {
739
+ result.etag = xmlobj.ETag.replace(/^"/g, '')
740
+ .replace(/"$/g, '')
741
+ .replace(/^&quot;/g, '')
742
+ .replace(/&quot;$/g, '')
743
+ .replace(/^&#34;/g, '')
744
+ .replace(/&#34;$/g, '')
745
+ }
746
+ if (xmlobj.LastModified) {
747
+ result.lastModified = new Date(xmlobj.LastModified)
748
+ }
749
+
750
+ return result
751
+ }
752
+
753
+ const formatObjInfo = (content: ObjectRowEntry, opts: { IsDeleteMarker?: boolean } = {}) => {
754
+ const { Key, LastModified, ETag, Size, VersionId, IsLatest } = content
755
+
756
+ if (!isObject(opts)) {
757
+ opts = {}
758
+ }
759
+
760
+ const name = sanitizeObjectKey(toArray(Key)[0] || '')
761
+ const lastModified = LastModified ? new Date(toArray(LastModified)[0] || '') : undefined
762
+ const etag = sanitizeETag(toArray(ETag)[0] || '')
763
+ const size = sanitizeSize(Size || '')
764
+
765
+ return {
766
+ name,
767
+ lastModified,
768
+ etag,
769
+ size,
770
+ versionId: VersionId,
771
+ isLatest: IsLatest,
772
+ isDeleteMarker: opts.IsDeleteMarker ? opts.IsDeleteMarker : false,
773
+ }
774
+ }
775
+
776
+ // parse XML response for list objects in a bucket
777
+ export function parseListObjects(xml: string) {
778
+ const result: {
779
+ objects: ObjectInfo[]
780
+ isTruncated?: boolean
781
+ nextMarker?: string
782
+ versionIdMarker?: string
783
+ keyMarker?: string
784
+ } = {
785
+ objects: [],
786
+ isTruncated: false,
787
+ nextMarker: undefined,
788
+ versionIdMarker: undefined,
789
+ keyMarker: undefined,
790
+ }
791
+ let isTruncated = false
792
+ let nextMarker
793
+ const xmlobj = fxpWithoutNumParser.parse(xml)
794
+
795
+ const parseCommonPrefixesEntity = (commonPrefixEntry: CommonPrefix[]) => {
796
+ if (commonPrefixEntry) {
797
+ toArray(commonPrefixEntry).forEach((commonPrefix) => {
798
+ result.objects.push({ prefix: sanitizeObjectKey(toArray(commonPrefix.Prefix)[0] || ''), size: 0 })
799
+ })
800
+ }
801
+ }
802
+
803
+ const listBucketResult: ListBucketResultV1 = xmlobj.ListBucketResult
804
+ const listVersionsResult: ListBucketResultV1 = xmlobj.ListVersionsResult
805
+
806
+ if (listBucketResult) {
807
+ if (listBucketResult.IsTruncated) {
808
+ isTruncated = listBucketResult.IsTruncated
809
+ }
810
+ if (listBucketResult.Contents) {
811
+ toArray(listBucketResult.Contents).forEach((content) => {
812
+ const name = sanitizeObjectKey(toArray(content.Key)[0] || '')
813
+ const lastModified = new Date(toArray(content.LastModified)[0] || '')
814
+ const etag = sanitizeETag(toArray(content.ETag)[0] || '')
815
+ const size = sanitizeSize(content.Size || '')
816
+ result.objects.push({ name, lastModified, etag, size })
817
+ })
818
+ }
819
+
820
+ if (listBucketResult.Marker) {
821
+ nextMarker = listBucketResult.Marker
822
+ }
823
+ if (listBucketResult.NextMarker) {
824
+ nextMarker = listBucketResult.NextMarker
825
+ } else if (isTruncated && result.objects.length > 0) {
826
+ nextMarker = result.objects[result.objects.length - 1]?.name
827
+ }
828
+ if (listBucketResult.CommonPrefixes) {
829
+ parseCommonPrefixesEntity(listBucketResult.CommonPrefixes)
830
+ }
831
+ }
832
+
833
+ if (listVersionsResult) {
834
+ if (listVersionsResult.IsTruncated) {
835
+ isTruncated = listVersionsResult.IsTruncated
836
+ }
837
+
838
+ if (listVersionsResult.Version) {
839
+ toArray(listVersionsResult.Version).forEach((content) => {
840
+ result.objects.push(formatObjInfo(content))
841
+ })
842
+ }
843
+ if (listVersionsResult.DeleteMarker) {
844
+ toArray(listVersionsResult.DeleteMarker).forEach((content) => {
845
+ result.objects.push(formatObjInfo(content, { IsDeleteMarker: true }))
846
+ })
847
+ }
848
+
849
+ if (listVersionsResult.NextKeyMarker) {
850
+ result.keyMarker = listVersionsResult.NextKeyMarker
851
+ }
852
+ if (listVersionsResult.NextVersionIdMarker) {
853
+ result.versionIdMarker = listVersionsResult.NextVersionIdMarker
854
+ }
855
+ if (listVersionsResult.CommonPrefixes) {
856
+ parseCommonPrefixesEntity(listVersionsResult.CommonPrefixes)
857
+ }
858
+ }
859
+
860
+ result.isTruncated = isTruncated
861
+ if (isTruncated) {
862
+ result.nextMarker = nextMarker
863
+ }
864
+ return result
865
+ }
866
+
867
+ export function uploadPartParser(xml: string) {
868
+ const xmlObj = parseXml(xml)
869
+ const respEl = xmlObj.CopyPartResult
870
+ return respEl
871
+ }