@hanzo/s3 0.6.4 → 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,606 @@
1
+ /*
2
+ * Hanzo S3 Javascript Library for Amazon S3 Compatible Cloud Storage, (C) 2015 Hanzo AI, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import * as crypto from 'node:crypto'
18
+ import * as stream from 'node:stream'
19
+
20
+ import { XMLParser } from 'fast-xml-parser'
21
+ import ipaddr from 'ipaddr.js'
22
+ import _ from 'lodash'
23
+ import * as mime from 'mime-types'
24
+
25
+ import { fsp, fstat } from './async.ts'
26
+ import type { Binary, Encryption, ObjectMetaData, RequestHeaders, ResponseHeader } from './type.ts'
27
+ import { ENCRYPTION_TYPES } from './type.ts'
28
+
29
+ const MetaDataHeaderPrefix = 'x-amz-meta-'
30
+
31
+ export function hashBinary(buf: Buffer, enableSHA256: boolean) {
32
+ let sha256sum = ''
33
+ if (enableSHA256) {
34
+ sha256sum = crypto.createHash('sha256').update(buf).digest('hex')
35
+ }
36
+ const md5sum = crypto.createHash('md5').update(buf).digest('base64')
37
+
38
+ return { md5sum, sha256sum }
39
+ }
40
+
41
+ // S3 percent-encodes some extra non-standard characters in a URI . So comply with S3.
42
+ const encodeAsHex = (c: string) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
43
+ export function uriEscape(uriStr: string): string {
44
+ return encodeURIComponent(uriStr).replace(/[!'()*]/g, encodeAsHex)
45
+ }
46
+
47
+ export function uriResourceEscape(string: string) {
48
+ return uriEscape(string).replace(/%2F/g, '/')
49
+ }
50
+
51
+ export function getScope(region: string, date: Date, serviceName = 's3') {
52
+ return `${makeDateShort(date)}/${region}/${serviceName}/aws4_request`
53
+ }
54
+
55
+ /**
56
+ * isAmazonEndpoint - true if endpoint is 's3.amazonaws.com' or 's3.cn-north-1.amazonaws.com.cn'
57
+ */
58
+ export function isAmazonEndpoint(endpoint: string) {
59
+ return endpoint === 's3.amazonaws.com' || endpoint === 's3.cn-north-1.amazonaws.com.cn'
60
+ }
61
+
62
+ /**
63
+ * isVirtualHostStyle - verify if bucket name is support with virtual
64
+ * hosts. bucketNames with periods should be always treated as path
65
+ * style if the protocol is 'https:', this is due to SSL wildcard
66
+ * limitation. For all other buckets and Amazon S3 endpoint we will
67
+ * default to virtual host style.
68
+ */
69
+ export function isVirtualHostStyle(endpoint: string, protocol: string, bucket: string, pathStyle: boolean) {
70
+ if (protocol === 'https:' && bucket.includes('.')) {
71
+ return false
72
+ }
73
+ return isAmazonEndpoint(endpoint) || !pathStyle
74
+ }
75
+
76
+ export function isValidIP(ip: string) {
77
+ return ipaddr.isValid(ip)
78
+ }
79
+
80
+ /**
81
+ * @returns if endpoint is valid domain.
82
+ */
83
+ export function isValidEndpoint(endpoint: string) {
84
+ return isValidDomain(endpoint) || isValidIP(endpoint)
85
+ }
86
+
87
+ /**
88
+ * @returns if input host is a valid domain.
89
+ */
90
+ export function isValidDomain(host: string) {
91
+ if (!isString(host)) {
92
+ return false
93
+ }
94
+ // See RFC 1035, RFC 3696.
95
+ if (host.length === 0 || host.length > 255) {
96
+ return false
97
+ }
98
+ // Host cannot start or end with a '-'
99
+ if (host[0] === '-' || host.slice(-1) === '-') {
100
+ return false
101
+ }
102
+ // Host cannot start or end with a '_'
103
+ if (host[0] === '_' || host.slice(-1) === '_') {
104
+ return false
105
+ }
106
+ // Host cannot start with a '.'
107
+ if (host[0] === '.') {
108
+ return false
109
+ }
110
+
111
+ const nonAlphaNumerics = '`~!@#$%^&*()+={}[]|\\"\';:><?/'
112
+ // All non alphanumeric characters are invalid.
113
+ for (const char of nonAlphaNumerics) {
114
+ if (host.includes(char)) {
115
+ return false
116
+ }
117
+ }
118
+ // No need to regexp match, since the list is non-exhaustive.
119
+ // We let it be valid and fail later.
120
+ return true
121
+ }
122
+
123
+ /**
124
+ * Probes contentType using file extensions.
125
+ *
126
+ * @example
127
+ * ```
128
+ * // return 'image/png'
129
+ * probeContentType('file.png')
130
+ * ```
131
+ */
132
+ export function probeContentType(path: string) {
133
+ let contentType = mime.lookup(path)
134
+ if (!contentType) {
135
+ contentType = 'application/octet-stream'
136
+ }
137
+ return contentType
138
+ }
139
+
140
+ /**
141
+ * is input port valid.
142
+ */
143
+ export function isValidPort(port: unknown): port is number {
144
+ // Convert string port to number if needed
145
+ const portNum = typeof port === 'string' ? parseInt(port, 10) : port
146
+
147
+ // verify if port is a valid number
148
+ if (!isNumber(portNum) || isNaN(portNum)) {
149
+ return false
150
+ }
151
+
152
+ // port `0` is valid and special case
153
+ return 0 <= portNum && portNum <= 65535
154
+ }
155
+
156
+ export function isValidBucketName(bucket: unknown) {
157
+ if (!isString(bucket)) {
158
+ return false
159
+ }
160
+
161
+ // bucket length should be less than and no more than 63
162
+ // characters long.
163
+ if (bucket.length < 3 || bucket.length > 63) {
164
+ return false
165
+ }
166
+ // bucket with successive periods is invalid.
167
+ if (bucket.includes('..')) {
168
+ return false
169
+ }
170
+ // bucket cannot have ip address style.
171
+ if (/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/.test(bucket)) {
172
+ return false
173
+ }
174
+ // bucket should begin with alphabet/number and end with alphabet/number,
175
+ // with alphabet/number/.- in the middle.
176
+ if (/^[a-z0-9][a-z0-9.-]+[a-z0-9]$/.test(bucket)) {
177
+ return true
178
+ }
179
+ return false
180
+ }
181
+
182
+ /**
183
+ * check if objectName is a valid object name
184
+ */
185
+ export function isValidObjectName(objectName: unknown) {
186
+ if (!isValidPrefix(objectName)) {
187
+ return false
188
+ }
189
+
190
+ return objectName.length !== 0
191
+ }
192
+
193
+ /**
194
+ * check if prefix is valid
195
+ */
196
+ export function isValidPrefix(prefix: unknown): prefix is string {
197
+ if (!isString(prefix)) {
198
+ return false
199
+ }
200
+ if (prefix.length > 1024) {
201
+ return false
202
+ }
203
+ return true
204
+ }
205
+
206
+ /**
207
+ * check if typeof arg number
208
+ */
209
+ export function isNumber(arg: unknown): arg is number {
210
+ return typeof arg === 'number'
211
+ }
212
+
213
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
214
+ export type AnyFunction = (...args: any[]) => any
215
+
216
+ /**
217
+ * check if typeof arg function
218
+ */
219
+ export function isFunction(arg: unknown): arg is AnyFunction {
220
+ return typeof arg === 'function'
221
+ }
222
+
223
+ /**
224
+ * check if typeof arg string
225
+ */
226
+ export function isString(arg: unknown): arg is string {
227
+ return typeof arg === 'string'
228
+ }
229
+
230
+ /**
231
+ * check if typeof arg object
232
+ */
233
+ export function isObject(arg: unknown): arg is object {
234
+ return typeof arg === 'object' && arg !== null
235
+ }
236
+ /**
237
+ * check if typeof arg is plain object
238
+ */
239
+ export function isPlainObject(arg: unknown): arg is Record<string, unknown> {
240
+ return Object.prototype.toString.call(arg) === '[object Object]'
241
+ }
242
+ /**
243
+ * check if object is readable stream
244
+ */
245
+ export function isReadableStream(arg: unknown): arg is stream.Readable {
246
+ // eslint-disable-next-line @typescript-eslint/unbound-method
247
+ return isObject(arg) && isFunction((arg as stream.Readable)._read)
248
+ }
249
+
250
+ /**
251
+ * check if arg is boolean
252
+ */
253
+ export function isBoolean(arg: unknown): arg is boolean {
254
+ return typeof arg === 'boolean'
255
+ }
256
+
257
+ export function isEmpty(o: unknown): o is null | undefined {
258
+ return _.isEmpty(o)
259
+ }
260
+
261
+ export function isEmptyObject(o: Record<string, unknown>): boolean {
262
+ return Object.values(o).filter((x) => x !== undefined).length !== 0
263
+ }
264
+
265
+ export function isDefined<T>(o: T): o is Exclude<T, null | undefined> {
266
+ return o !== null && o !== undefined
267
+ }
268
+
269
+ /**
270
+ * check if arg is a valid date
271
+ */
272
+ export function isValidDate(arg: unknown): arg is Date {
273
+ // @ts-expect-error checknew Date(Math.NaN)
274
+ return arg instanceof Date && !isNaN(arg)
275
+ }
276
+
277
+ /**
278
+ * Create a Date string with format: 'YYYYMMDDTHHmmss' + Z
279
+ */
280
+ export function makeDateLong(date?: Date): string {
281
+ date = date || new Date()
282
+
283
+ // Gives format like: '2017-08-07T16:28:59.889Z'
284
+ const s = date.toISOString()
285
+
286
+ return s.slice(0, 4) + s.slice(5, 7) + s.slice(8, 13) + s.slice(14, 16) + s.slice(17, 19) + 'Z'
287
+ }
288
+
289
+ /**
290
+ * Create a Date string with format: 'YYYYMMDD'
291
+ */
292
+ export function makeDateShort(date?: Date) {
293
+ date = date || new Date()
294
+
295
+ // Gives format like: '2017-08-07T16:28:59.889Z'
296
+ const s = date.toISOString()
297
+
298
+ return s.slice(0, 4) + s.slice(5, 7) + s.slice(8, 10)
299
+ }
300
+
301
+ /**
302
+ * pipesetup sets up pipe() from left to right os streams array
303
+ * pipesetup will also make sure that error emitted at any of the upstream Stream
304
+ * will be emitted at the last stream. This makes error handling simple
305
+ */
306
+ export function pipesetup(...streams: [stream.Readable, ...stream.Duplex[], stream.Writable]) {
307
+ // @ts-expect-error ts can't narrow this
308
+ return streams.reduce((src: stream.Readable, dst: stream.Writable) => {
309
+ src.on('error', (err) => dst.emit('error', err))
310
+ return src.pipe(dst)
311
+ })
312
+ }
313
+
314
+ /**
315
+ * return a Readable stream that emits data
316
+ */
317
+ export function readableStream(data: unknown): stream.Readable {
318
+ const s = new stream.Readable()
319
+ s._read = () => {}
320
+ s.push(data)
321
+ s.push(null)
322
+ return s
323
+ }
324
+
325
+ /**
326
+ * Process metadata to insert appropriate value to `content-type` attribute
327
+ */
328
+ export function insertContentType(metaData: ObjectMetaData, filePath: string): ObjectMetaData {
329
+ // check if content-type attribute present in metaData
330
+ for (const key in metaData) {
331
+ if (key.toLowerCase() === 'content-type') {
332
+ return metaData
333
+ }
334
+ }
335
+
336
+ // if `content-type` attribute is not present in metadata, then infer it from the extension in filePath
337
+ return {
338
+ ...metaData,
339
+ 'content-type': probeContentType(filePath),
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Function prepends metadata with the appropriate prefix if it is not already on
345
+ */
346
+ export function prependXAMZMeta(metaData?: ObjectMetaData): RequestHeaders {
347
+ if (!metaData) {
348
+ return {}
349
+ }
350
+
351
+ return _.mapKeys(metaData, (value, key) => {
352
+ if (isAmzHeader(key) || isSupportedHeader(key) || isStorageClassHeader(key)) {
353
+ return key
354
+ }
355
+
356
+ return MetaDataHeaderPrefix + key
357
+ })
358
+ }
359
+
360
+ /**
361
+ * Checks if it is a valid header according to the AmazonS3 API
362
+ */
363
+ export function isAmzHeader(key: string) {
364
+ const temp = key.toLowerCase()
365
+ return (
366
+ temp.startsWith(MetaDataHeaderPrefix) ||
367
+ temp === 'x-amz-acl' ||
368
+ temp.startsWith('x-amz-server-side-encryption-') ||
369
+ temp === 'x-amz-server-side-encryption'
370
+ )
371
+ }
372
+
373
+ /**
374
+ * Checks if it is a supported Header
375
+ */
376
+ export function isSupportedHeader(key: string) {
377
+ const supported_headers = [
378
+ 'content-type',
379
+ 'cache-control',
380
+ 'content-encoding',
381
+ 'content-disposition',
382
+ 'content-language',
383
+ 'x-amz-website-redirect-location',
384
+ 'if-none-match',
385
+ 'if-match',
386
+ ]
387
+ return supported_headers.includes(key.toLowerCase())
388
+ }
389
+
390
+ /**
391
+ * Checks if it is a storage header
392
+ */
393
+ export function isStorageClassHeader(key: string) {
394
+ return key.toLowerCase() === 'x-amz-storage-class'
395
+ }
396
+
397
+ export function extractMetadata(headers: ResponseHeader) {
398
+ return _.mapKeys(
399
+ _.pickBy(headers, (value, key) => isSupportedHeader(key) || isStorageClassHeader(key) || isAmzHeader(key)),
400
+ (value, key) => {
401
+ const lower = key.toLowerCase()
402
+ if (lower.startsWith(MetaDataHeaderPrefix)) {
403
+ return lower.slice(MetaDataHeaderPrefix.length)
404
+ }
405
+
406
+ return key
407
+ },
408
+ )
409
+ }
410
+
411
+ export function getVersionId(headers: ResponseHeader = {}) {
412
+ return headers['x-amz-version-id'] || null
413
+ }
414
+
415
+ export function getSourceVersionId(headers: ResponseHeader = {}) {
416
+ return headers['x-amz-copy-source-version-id'] || null
417
+ }
418
+
419
+ export function sanitizeETag(etag = ''): string {
420
+ const replaceChars: Record<string, string> = {
421
+ '"': '',
422
+ '&quot;': '',
423
+ '&#34;': '',
424
+ '&QUOT;': '',
425
+ '&#x00022': '',
426
+ }
427
+ return etag.replace(/^("|&quot;|&#34;)|("|&quot;|&#34;)$/g, (m) => replaceChars[m] as string)
428
+ }
429
+
430
+ export function toMd5(payload: Binary): string {
431
+ // use string from browser and buffer from nodejs
432
+ // browser support is tested only against Hanzo S3 server
433
+ return crypto.createHash('md5').update(Buffer.from(payload)).digest().toString('base64')
434
+ }
435
+
436
+ export function toSha256(payload: Binary): string {
437
+ return crypto.createHash('sha256').update(payload).digest('hex')
438
+ }
439
+
440
+ /**
441
+ * toArray returns a single element array with param being the element,
442
+ * if param is just a string, and returns 'param' back if it is an array
443
+ * So, it makes sure param is always an array
444
+ */
445
+ export function toArray<T = unknown>(param: T | T[]): Array<T> {
446
+ if (!Array.isArray(param)) {
447
+ return [param] as T[]
448
+ }
449
+ return param
450
+ }
451
+
452
+ export function sanitizeObjectKey(objectName: string): string {
453
+ // + symbol characters are not decoded as spaces in JS. so replace them first and decode to get the correct result.
454
+ const asStrName = (objectName ? objectName.toString() : '').replace(/\+/g, ' ')
455
+ return decodeURIComponent(asStrName)
456
+ }
457
+
458
+ export function sanitizeSize(size?: string): number | undefined {
459
+ return size ? Number.parseInt(size) : undefined
460
+ }
461
+
462
+ export const PART_CONSTRAINTS = {
463
+ // absMinPartSize - absolute minimum part size (5 MiB)
464
+ ABS_MIN_PART_SIZE: 1024 * 1024 * 5,
465
+ // MIN_PART_SIZE - minimum part size 16MiB per object after which
466
+ MIN_PART_SIZE: 1024 * 1024 * 16,
467
+ // MAX_PARTS_COUNT - maximum number of parts for a single multipart session.
468
+ MAX_PARTS_COUNT: 10000,
469
+ // MAX_PART_SIZE - maximum part size 5GiB for a single multipart upload
470
+ // operation.
471
+ MAX_PART_SIZE: 1024 * 1024 * 1024 * 5,
472
+ // MAX_SINGLE_PUT_OBJECT_SIZE - maximum size 5GiB of object per PUT
473
+ // operation.
474
+ MAX_SINGLE_PUT_OBJECT_SIZE: 1024 * 1024 * 1024 * 5,
475
+ // MAX_MULTIPART_PUT_OBJECT_SIZE - maximum size 5TiB of object for
476
+ // Multipart operation.
477
+ MAX_MULTIPART_PUT_OBJECT_SIZE: 1024 * 1024 * 1024 * 1024 * 5,
478
+ }
479
+
480
+ const GENERIC_SSE_HEADER = 'X-Amz-Server-Side-Encryption'
481
+
482
+ const ENCRYPTION_HEADERS = {
483
+ // sseGenericHeader is the AWS SSE header used for SSE-S3 and SSE-KMS.
484
+ sseGenericHeader: GENERIC_SSE_HEADER,
485
+ // sseKmsKeyID is the AWS SSE-KMS key id.
486
+ sseKmsKeyID: GENERIC_SSE_HEADER + '-Aws-Kms-Key-Id',
487
+ } as const
488
+
489
+ /**
490
+ * Return Encryption headers
491
+ * @param encConfig
492
+ * @returns an object with key value pairs that can be used in headers.
493
+ */
494
+ export function getEncryptionHeaders(encConfig: Encryption): RequestHeaders {
495
+ const encType = encConfig.type
496
+
497
+ if (!isEmpty(encType)) {
498
+ if (encType === ENCRYPTION_TYPES.SSEC) {
499
+ return {
500
+ [ENCRYPTION_HEADERS.sseGenericHeader]: 'AES256',
501
+ }
502
+ } else if (encType === ENCRYPTION_TYPES.KMS) {
503
+ return {
504
+ [ENCRYPTION_HEADERS.sseGenericHeader]: encConfig.SSEAlgorithm,
505
+ [ENCRYPTION_HEADERS.sseKmsKeyID]: encConfig.KMSMasterKeyID,
506
+ }
507
+ }
508
+ }
509
+
510
+ return {}
511
+ }
512
+
513
+ export function partsRequired(size: number): number {
514
+ const maxPartSize = PART_CONSTRAINTS.MAX_MULTIPART_PUT_OBJECT_SIZE / (PART_CONSTRAINTS.MAX_PARTS_COUNT - 1)
515
+ let requiredPartSize = size / maxPartSize
516
+ if (size % maxPartSize > 0) {
517
+ requiredPartSize++
518
+ }
519
+ requiredPartSize = Math.trunc(requiredPartSize)
520
+ return requiredPartSize
521
+ }
522
+
523
+ /**
524
+ * calculateEvenSplits - computes splits for a source and returns
525
+ * start and end index slices. Splits happen evenly to be sure that no
526
+ * part is less than 5MiB, as that could fail the multipart request if
527
+ * it is not the last part.
528
+ */
529
+ export function calculateEvenSplits<T extends { Start?: number }>(
530
+ size: number,
531
+ objInfo: T,
532
+ ): {
533
+ startIndex: number[]
534
+ objInfo: T
535
+ endIndex: number[]
536
+ } | null {
537
+ if (size === 0) {
538
+ return null
539
+ }
540
+ const reqParts = partsRequired(size)
541
+ const startIndexParts: number[] = []
542
+ const endIndexParts: number[] = []
543
+
544
+ let start = objInfo.Start
545
+ if (isEmpty(start) || start === -1) {
546
+ start = 0
547
+ }
548
+ const divisorValue = Math.trunc(size / reqParts)
549
+
550
+ const reminderValue = size % reqParts
551
+
552
+ let nextStart = start
553
+
554
+ for (let i = 0; i < reqParts; i++) {
555
+ let curPartSize = divisorValue
556
+ if (i < reminderValue) {
557
+ curPartSize++
558
+ }
559
+
560
+ const currentStart = nextStart
561
+ const currentEnd = currentStart + curPartSize - 1
562
+ nextStart = currentEnd + 1
563
+
564
+ startIndexParts.push(currentStart)
565
+ endIndexParts.push(currentEnd)
566
+ }
567
+
568
+ return { startIndex: startIndexParts, endIndex: endIndexParts, objInfo: objInfo }
569
+ }
570
+ const fxp = new XMLParser({ numberParseOptions: { eNotation: false, hex: true, leadingZeros: true } })
571
+
572
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
573
+ export function parseXml(xml: string): any {
574
+ const result = fxp.parse(xml)
575
+ if (result.Error) {
576
+ throw result.Error
577
+ }
578
+
579
+ return result
580
+ }
581
+
582
+ /**
583
+ * get content size of object content to upload
584
+ */
585
+ export async function getContentLength(s: stream.Readable | Buffer | string): Promise<number | null> {
586
+ // use length property of string | Buffer
587
+ if (typeof s === 'string' || Buffer.isBuffer(s)) {
588
+ return s.length
589
+ }
590
+
591
+ // property of `fs.ReadStream`
592
+ const filePath = (s as unknown as Record<string, unknown>).path as string | undefined
593
+ if (filePath && typeof filePath === 'string') {
594
+ const stat = await fsp.lstat(filePath)
595
+ return stat.size
596
+ }
597
+
598
+ // property of `fs.ReadStream`
599
+ const fd = (s as unknown as Record<string, unknown>).fd as number | null | undefined
600
+ if (fd && typeof fd === 'number') {
601
+ const stat = await fstat(fd)
602
+ return stat.size
603
+ }
604
+
605
+ return null
606
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * joinHostPort combines host and port into a network address of the
3
+ * form "host:port". If host contains a colon, as found in literal
4
+ * IPv6 addresses, then JoinHostPort returns "[host]:port".
5
+ *
6
+ * @param host
7
+ * @param port
8
+ * @returns Cleaned up host
9
+ * @internal
10
+ */
11
+ export function joinHostPort(host: string, port?: number): string {
12
+ if (port === undefined) {
13
+ return host
14
+ }
15
+
16
+ // We assume that host is a literal IPv6 address if host has
17
+ // colons.
18
+ if (host.includes(':')) {
19
+ return `[${host}]:${port.toString()}`
20
+ }
21
+
22
+ return `${host}:${port.toString()}`
23
+ }