@nxtedition/lib 19.0.33 → 19.0.35
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/package.json +1 -1
- package/s3.js +84 -163
package/package.json
CHANGED
package/s3.js
CHANGED
|
@@ -1,118 +1,17 @@
|
|
|
1
1
|
import crypto from 'node:crypto'
|
|
2
2
|
import stream from 'node:stream'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import os from 'node:os'
|
|
5
|
-
import fs from 'node:fs'
|
|
6
|
-
import assert from 'node:assert'
|
|
7
3
|
import AWS from '@aws-sdk/client-s3'
|
|
8
4
|
import PQueue from 'p-queue'
|
|
9
5
|
|
|
10
6
|
const CONTENT_MD5_EXPR = /^[A-F0-9]{32}$/i
|
|
11
7
|
const CONTENT_LENGTH_EXPR = /^\d+$/i
|
|
12
8
|
|
|
13
|
-
const noop = (arg0) => {}
|
|
14
|
-
|
|
15
|
-
class PartUploader {
|
|
16
|
-
#number
|
|
17
|
-
#path
|
|
18
|
-
#size
|
|
19
|
-
#writable
|
|
20
|
-
#callback
|
|
21
|
-
#hasher
|
|
22
|
-
#signal
|
|
23
|
-
#logger
|
|
24
|
-
|
|
25
|
-
constructor({ dir, number, signal, logger }) {
|
|
26
|
-
this.#writable = null
|
|
27
|
-
this.#callback = noop
|
|
28
|
-
this.#hasher = crypto.createHash('md5')
|
|
29
|
-
this.#size = 0
|
|
30
|
-
this.#signal = signal
|
|
31
|
-
this.#number = number
|
|
32
|
-
this.#path = path.join(dir, `${this.#number}.part`)
|
|
33
|
-
this.#logger = logger?.child({ part: this.#number, path: this.#path })
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
get size() {
|
|
37
|
-
return this.#size
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async write(chunk) {
|
|
41
|
-
if (!this.#writable) {
|
|
42
|
-
this.#writable = fs
|
|
43
|
-
.createWriteStream(this.#path, { signal: this.#signal })
|
|
44
|
-
.on('drain', () => {
|
|
45
|
-
this.#callback(null)
|
|
46
|
-
this.#callback = noop
|
|
47
|
-
})
|
|
48
|
-
.on('error', (err) => {
|
|
49
|
-
this.#callback(err)
|
|
50
|
-
this.callback = noop
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
this.#logger?.debug('created part')
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (this.#writable.errored) {
|
|
57
|
-
throw this.#writable.errored
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
this.#size += chunk.byteLength
|
|
61
|
-
this.#hasher.update(chunk)
|
|
62
|
-
|
|
63
|
-
if (!this.#writable.write(chunk)) {
|
|
64
|
-
await new Promise((resolve, reject) => {
|
|
65
|
-
this.#callback = (err) => (err ? reject(err) : resolve(null))
|
|
66
|
-
})
|
|
67
|
-
this.#signal.throwIfAborted()
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async end(s3, params) {
|
|
72
|
-
try {
|
|
73
|
-
if (!this.#writable) {
|
|
74
|
-
throw new Error('No data to send')
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (this.#writable.errored) {
|
|
78
|
-
throw this.#writable.errored
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
this.#writable.end()
|
|
82
|
-
await stream.promises.finished(this.#writable)
|
|
83
|
-
|
|
84
|
-
assert(this.#writable.bytesWritten === this.#size, 'Expected size to match bytesWritten')
|
|
85
|
-
|
|
86
|
-
this.#logger?.debug('uploading part')
|
|
87
|
-
const { ETag } = await s3.send(
|
|
88
|
-
new AWS.UploadPartCommand({
|
|
89
|
-
...params,
|
|
90
|
-
ContentMD5: this.#hasher.digest('base64'),
|
|
91
|
-
ContentLength: this.#size,
|
|
92
|
-
PartNumber: this.#number,
|
|
93
|
-
Body: fs.createReadStream(this.#path, { signal: this.#signal }),
|
|
94
|
-
}),
|
|
95
|
-
)
|
|
96
|
-
this.#logger?.debug({ etag: ETag }, 'uploaded part')
|
|
97
|
-
|
|
98
|
-
return { part: { ETag, PartNumber: this.#number } }
|
|
99
|
-
} catch (err) {
|
|
100
|
-
return { error: err }
|
|
101
|
-
} finally {
|
|
102
|
-
await fs.promises.unlink(this.#writable.path)
|
|
103
|
-
this.#logger?.debug('deleted part')
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
9
|
export async function upload({
|
|
109
10
|
client: s3,
|
|
110
|
-
signal
|
|
11
|
+
signal,
|
|
111
12
|
logger,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
queueSize = 4,
|
|
115
|
-
leavePartsOnError = false,
|
|
13
|
+
partSize = 16e6,
|
|
14
|
+
queueSize = 2,
|
|
116
15
|
params,
|
|
117
16
|
}) {
|
|
118
17
|
if (s3 == null) {
|
|
@@ -141,85 +40,118 @@ export async function upload({
|
|
|
141
40
|
throw new Error(`Invalid ContentLength: ${ContentLength}`)
|
|
142
41
|
}
|
|
143
42
|
|
|
144
|
-
outerSignal?.throwIfAborted()
|
|
145
|
-
|
|
146
43
|
const queue = new PQueue({ concurrency: queueSize })
|
|
147
44
|
const promises = []
|
|
148
|
-
const ac = new AbortController()
|
|
149
|
-
const signal = ac.signal
|
|
150
|
-
|
|
151
|
-
const abort = () => ac.abort()
|
|
152
|
-
outerSignal?.addEventListener('abort', abort)
|
|
153
45
|
|
|
154
46
|
let uploadId
|
|
155
|
-
let uploadDir
|
|
156
47
|
try {
|
|
157
|
-
uploadDir = await fs.promises.mkdtemp(path.join(tmpdir, 's3-upload-'))
|
|
158
|
-
logger?.debug({ uploadDir }, 'created upload directory')
|
|
159
|
-
signal.throwIfAborted()
|
|
160
|
-
|
|
161
48
|
const multipartUploadOutput = await s3.send(
|
|
162
|
-
new AWS.CreateMultipartUploadCommand({
|
|
163
|
-
|
|
164
|
-
Key,
|
|
165
|
-
}),
|
|
49
|
+
new AWS.CreateMultipartUploadCommand({ Bucket, Key }),
|
|
50
|
+
{ abortSignal: signal },
|
|
166
51
|
)
|
|
167
52
|
uploadId = multipartUploadOutput.UploadId
|
|
168
53
|
logger = logger?.child({ uploadId })
|
|
169
54
|
logger?.debug('created multipart upload')
|
|
170
|
-
signal.throwIfAborted()
|
|
171
55
|
|
|
172
56
|
const uploader = {
|
|
173
57
|
size: 0,
|
|
174
58
|
hasher: crypto.createHash('md5'),
|
|
175
|
-
part:
|
|
176
|
-
|
|
59
|
+
part: {
|
|
60
|
+
/** @type {Array<Buffer>} **/ chunks: [],
|
|
61
|
+
hasher: crypto.createHash('md5'),
|
|
62
|
+
size: 0,
|
|
63
|
+
number: 1,
|
|
64
|
+
},
|
|
177
65
|
}
|
|
178
66
|
|
|
179
67
|
const maybeFlush = (minSize) => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
number: ++uploader.number,
|
|
185
|
-
logger,
|
|
186
|
-
signal,
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
const promise = queue.add(() => part.end(s3, { Bucket, Key, UploadId: uploadId }))
|
|
190
|
-
promise.catch((err) => {
|
|
191
|
-
ac.abort(err)
|
|
192
|
-
})
|
|
193
|
-
promises.push(promise)
|
|
68
|
+
const { part } = uploader
|
|
69
|
+
|
|
70
|
+
if (!part.size) {
|
|
71
|
+
return
|
|
194
72
|
}
|
|
73
|
+
|
|
74
|
+
if (minSize != null && part.size < minSize) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const chunks = part.chunks
|
|
79
|
+
const number = part.number
|
|
80
|
+
const size = part.size
|
|
81
|
+
const hasher = part.hasher
|
|
82
|
+
|
|
83
|
+
part.chunks = []
|
|
84
|
+
part.number += 1
|
|
85
|
+
part.size = 0
|
|
86
|
+
part.hasher = crypto.createHash('md5')
|
|
87
|
+
|
|
88
|
+
promises.push(
|
|
89
|
+
queue
|
|
90
|
+
.add(
|
|
91
|
+
async ({ signal }) => {
|
|
92
|
+
logger?.debug({ number, size }, 'part upload started')
|
|
93
|
+
try {
|
|
94
|
+
const { ETag } = await s3.send(
|
|
95
|
+
new AWS.UploadPartCommand({
|
|
96
|
+
Bucket,
|
|
97
|
+
Key,
|
|
98
|
+
UploadId: uploadId,
|
|
99
|
+
ContentMD5: hasher.digest('base64'),
|
|
100
|
+
ContentLength: size,
|
|
101
|
+
PartNumber: number,
|
|
102
|
+
Body: new stream.Readable({
|
|
103
|
+
read() {
|
|
104
|
+
for (const chunk of chunks.splice(0)) {
|
|
105
|
+
this.push(chunk)
|
|
106
|
+
}
|
|
107
|
+
this.push(null)
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
}),
|
|
111
|
+
{ abortSignal: signal },
|
|
112
|
+
)
|
|
113
|
+
logger?.debug({ number, size, etag: ETag }, 'part upload completed')
|
|
114
|
+
return { part: { ETag, PartNumber: number } }
|
|
115
|
+
} catch (err) {
|
|
116
|
+
logger?.debug({ err }, 'part upload failed')
|
|
117
|
+
return { error: err }
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{ signal },
|
|
121
|
+
)
|
|
122
|
+
.catch((err) => ({ error: err })),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return queue.onEmpty()
|
|
195
126
|
}
|
|
196
127
|
|
|
197
128
|
for await (const chunk of Body) {
|
|
198
|
-
signal
|
|
129
|
+
signal?.throwIfAborted()
|
|
199
130
|
|
|
200
131
|
uploader.hasher.update(chunk)
|
|
201
132
|
uploader.size += chunk.byteLength
|
|
202
133
|
|
|
203
|
-
|
|
134
|
+
uploader.part.hasher.update(chunk)
|
|
135
|
+
uploader.part.chunks.push(chunk)
|
|
136
|
+
uploader.part.size += chunk.byteLength
|
|
137
|
+
|
|
138
|
+
const thenable = maybeFlush(partSize)
|
|
204
139
|
if (thenable) {
|
|
205
140
|
await thenable
|
|
206
|
-
signal.throwIfAborted()
|
|
207
141
|
}
|
|
208
|
-
|
|
209
|
-
maybeFlush(partSize)
|
|
210
142
|
}
|
|
211
|
-
maybeFlush()
|
|
143
|
+
await maybeFlush()
|
|
212
144
|
|
|
213
145
|
const parts = []
|
|
214
146
|
const errors = []
|
|
215
147
|
for (const { part, error } of await Promise.all(promises)) {
|
|
216
148
|
if (error) {
|
|
217
149
|
errors.push(error)
|
|
218
|
-
} else {
|
|
150
|
+
} else if (part) {
|
|
219
151
|
parts.push(part)
|
|
220
152
|
}
|
|
221
153
|
}
|
|
222
|
-
signal
|
|
154
|
+
signal?.throwIfAborted()
|
|
223
155
|
|
|
224
156
|
if (errors.length > 0) {
|
|
225
157
|
throw new AggregateError(errors, 'upload failed')
|
|
@@ -236,7 +168,9 @@ export async function upload({
|
|
|
236
168
|
UploadId: uploadId,
|
|
237
169
|
MultipartUpload: { Parts: parts },
|
|
238
170
|
}),
|
|
171
|
+
{ abortSignal: signal },
|
|
239
172
|
)
|
|
173
|
+
signal?.throwIfAborted()
|
|
240
174
|
|
|
241
175
|
const result = {
|
|
242
176
|
size: uploader.size,
|
|
@@ -245,10 +179,6 @@ export async function upload({
|
|
|
245
179
|
parts,
|
|
246
180
|
}
|
|
247
181
|
|
|
248
|
-
logger?.debug(result, 'completed multipart upload')
|
|
249
|
-
|
|
250
|
-
signal.throwIfAborted()
|
|
251
|
-
|
|
252
182
|
const size = ContentLength != null ? Number(ContentLength) : null
|
|
253
183
|
const hash = ContentMD5
|
|
254
184
|
|
|
@@ -260,19 +190,15 @@ export async function upload({
|
|
|
260
190
|
throw new Error(`Expected hash ${hash} but got ${result.hash}`)
|
|
261
191
|
}
|
|
262
192
|
|
|
193
|
+
logger?.debug(result, 'completed multipart upload')
|
|
194
|
+
|
|
263
195
|
return result
|
|
264
196
|
} catch (err) {
|
|
265
|
-
|
|
197
|
+
logger?.error({ err }, 'failed multipart upload')
|
|
266
198
|
|
|
267
|
-
if (uploadId
|
|
199
|
+
if (uploadId) {
|
|
268
200
|
try {
|
|
269
|
-
await s3.send(
|
|
270
|
-
new AWS.AbortMultipartUploadCommand({
|
|
271
|
-
Bucket,
|
|
272
|
-
Key,
|
|
273
|
-
UploadId: uploadId,
|
|
274
|
-
}),
|
|
275
|
-
)
|
|
201
|
+
await s3.send(new AWS.AbortMultipartUploadCommand({ Bucket, Key, UploadId: uploadId }))
|
|
276
202
|
logger?.warn('aborted multipart upload')
|
|
277
203
|
} catch (er) {
|
|
278
204
|
throw new AggregateError([err, er])
|
|
@@ -280,10 +206,5 @@ export async function upload({
|
|
|
280
206
|
}
|
|
281
207
|
|
|
282
208
|
throw err
|
|
283
|
-
} finally {
|
|
284
|
-
outerSignal?.removeEventListener('abort', abort)
|
|
285
|
-
if (uploadDir) {
|
|
286
|
-
await fs.promises.rmdir(uploadDir, { recursive: true })
|
|
287
|
-
}
|
|
288
209
|
}
|
|
289
210
|
}
|