@nxtedition/lib 19.0.34 → 19.0.37
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 +96 -161
package/package.json
CHANGED
package/s3.js
CHANGED
|
@@ -1,116 +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 { ETag, PartNumber: this.#number }
|
|
99
|
-
} finally {
|
|
100
|
-
await fs.promises.unlink(this.#writable.path)
|
|
101
|
-
this.#logger?.debug('deleted part')
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
9
|
export async function upload({
|
|
107
10
|
client: s3,
|
|
108
|
-
signal
|
|
11
|
+
signal,
|
|
109
12
|
logger,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
queueSize = 4,
|
|
113
|
-
leavePartsOnError = false,
|
|
13
|
+
partSize = 16e6,
|
|
14
|
+
queueSize = 2,
|
|
114
15
|
params,
|
|
115
16
|
}) {
|
|
116
17
|
if (s3 == null) {
|
|
@@ -139,77 +40,122 @@ export async function upload({
|
|
|
139
40
|
throw new Error(`Invalid ContentLength: ${ContentLength}`)
|
|
140
41
|
}
|
|
141
42
|
|
|
142
|
-
outerSignal?.throwIfAborted()
|
|
143
|
-
|
|
144
43
|
const queue = new PQueue({ concurrency: queueSize })
|
|
145
44
|
const promises = []
|
|
146
|
-
const ac = new AbortController()
|
|
147
|
-
const signal = ac.signal
|
|
148
|
-
|
|
149
|
-
const abort = () => ac.abort()
|
|
150
|
-
outerSignal?.addEventListener('abort', abort)
|
|
151
45
|
|
|
152
46
|
let uploadId
|
|
153
|
-
let uploadDir
|
|
154
47
|
try {
|
|
155
|
-
uploadDir = await fs.promises.mkdtemp(path.join(tmpdir, 's3-upload-'))
|
|
156
|
-
logger?.debug({ uploadDir }, 'created upload directory')
|
|
157
|
-
signal.throwIfAborted()
|
|
158
|
-
|
|
159
48
|
const multipartUploadOutput = await s3.send(
|
|
160
|
-
new AWS.CreateMultipartUploadCommand({
|
|
161
|
-
|
|
162
|
-
Key,
|
|
163
|
-
}),
|
|
49
|
+
new AWS.CreateMultipartUploadCommand({ Bucket, Key }),
|
|
50
|
+
{ abortSignal: signal },
|
|
164
51
|
)
|
|
165
52
|
uploadId = multipartUploadOutput.UploadId
|
|
166
53
|
logger = logger?.child({ uploadId })
|
|
167
54
|
logger?.debug('created multipart upload')
|
|
168
|
-
signal.throwIfAborted()
|
|
169
55
|
|
|
170
56
|
const uploader = {
|
|
171
57
|
size: 0,
|
|
172
58
|
hasher: crypto.createHash('md5'),
|
|
173
|
-
part:
|
|
174
|
-
|
|
59
|
+
part: {
|
|
60
|
+
/** @type {Array<Buffer>} **/ chunks: [],
|
|
61
|
+
hasher: crypto.createHash('md5'),
|
|
62
|
+
size: 0,
|
|
63
|
+
number: 1,
|
|
64
|
+
},
|
|
175
65
|
}
|
|
176
66
|
|
|
177
67
|
const maybeFlush = (minSize) => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const promise = queue.add(() => part.end(s3, { Bucket, Key, UploadId: uploadId }))
|
|
188
|
-
promise.catch((err) => {
|
|
189
|
-
ac.abort(err)
|
|
190
|
-
})
|
|
191
|
-
promises.push(promise)
|
|
68
|
+
const { part } = uploader
|
|
69
|
+
|
|
70
|
+
if (!part.size) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (minSize != null && part.size < minSize) {
|
|
75
|
+
return
|
|
192
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()
|
|
193
126
|
}
|
|
194
127
|
|
|
195
128
|
for await (const chunk of Body) {
|
|
196
|
-
signal
|
|
129
|
+
signal?.throwIfAborted()
|
|
197
130
|
|
|
198
131
|
uploader.hasher.update(chunk)
|
|
199
132
|
uploader.size += chunk.byteLength
|
|
200
133
|
|
|
201
|
-
|
|
134
|
+
uploader.part.hasher.update(chunk)
|
|
135
|
+
uploader.part.chunks.push(chunk)
|
|
136
|
+
uploader.part.size += chunk.byteLength
|
|
137
|
+
|
|
138
|
+
const thenable = maybeFlush(partSize)
|
|
202
139
|
if (thenable) {
|
|
203
140
|
await thenable
|
|
204
|
-
signal.throwIfAborted()
|
|
205
141
|
}
|
|
206
|
-
|
|
207
|
-
maybeFlush(partSize)
|
|
208
142
|
}
|
|
209
|
-
maybeFlush()
|
|
143
|
+
await maybeFlush()
|
|
144
|
+
|
|
145
|
+
const parts = []
|
|
146
|
+
const errors = []
|
|
147
|
+
for (const { part, error } of await Promise.all(promises)) {
|
|
148
|
+
if (error) {
|
|
149
|
+
errors.push(error)
|
|
150
|
+
} else if (part) {
|
|
151
|
+
parts.push(part)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
signal?.throwIfAborted()
|
|
210
155
|
|
|
211
|
-
|
|
212
|
-
|
|
156
|
+
if (errors.length > 0) {
|
|
157
|
+
throw new AggregateError(errors, 'upload failed')
|
|
158
|
+
}
|
|
213
159
|
|
|
214
160
|
if (parts.length === 0) {
|
|
215
161
|
throw new Error('upload empty')
|
|
@@ -222,7 +168,9 @@ export async function upload({
|
|
|
222
168
|
UploadId: uploadId,
|
|
223
169
|
MultipartUpload: { Parts: parts },
|
|
224
170
|
}),
|
|
171
|
+
{ abortSignal: signal },
|
|
225
172
|
)
|
|
173
|
+
signal?.throwIfAborted()
|
|
226
174
|
|
|
227
175
|
const result = {
|
|
228
176
|
size: uploader.size,
|
|
@@ -231,10 +179,6 @@ export async function upload({
|
|
|
231
179
|
parts,
|
|
232
180
|
}
|
|
233
181
|
|
|
234
|
-
logger?.debug(result, 'completed multipart upload')
|
|
235
|
-
|
|
236
|
-
signal.throwIfAborted()
|
|
237
|
-
|
|
238
182
|
const size = ContentLength != null ? Number(ContentLength) : null
|
|
239
183
|
const hash = ContentMD5
|
|
240
184
|
|
|
@@ -246,19 +190,15 @@ export async function upload({
|
|
|
246
190
|
throw new Error(`Expected hash ${hash} but got ${result.hash}`)
|
|
247
191
|
}
|
|
248
192
|
|
|
193
|
+
logger?.debug(result, 'completed multipart upload')
|
|
194
|
+
|
|
249
195
|
return result
|
|
250
196
|
} catch (err) {
|
|
251
|
-
|
|
197
|
+
logger?.error({ err }, 'failed multipart upload')
|
|
252
198
|
|
|
253
|
-
if (uploadId
|
|
199
|
+
if (uploadId) {
|
|
254
200
|
try {
|
|
255
|
-
await s3.send(
|
|
256
|
-
new AWS.AbortMultipartUploadCommand({
|
|
257
|
-
Bucket,
|
|
258
|
-
Key,
|
|
259
|
-
UploadId: uploadId,
|
|
260
|
-
}),
|
|
261
|
-
)
|
|
201
|
+
await s3.send(new AWS.AbortMultipartUploadCommand({ Bucket, Key, UploadId: uploadId }))
|
|
262
202
|
logger?.warn('aborted multipart upload')
|
|
263
203
|
} catch (er) {
|
|
264
204
|
throw new AggregateError([err, er])
|
|
@@ -266,10 +206,5 @@ export async function upload({
|
|
|
266
206
|
}
|
|
267
207
|
|
|
268
208
|
throw err
|
|
269
|
-
} finally {
|
|
270
|
-
outerSignal?.removeEventListener('abort', abort)
|
|
271
|
-
if (uploadDir) {
|
|
272
|
-
await fs.promises.rmdir(uploadDir, { recursive: true })
|
|
273
|
-
}
|
|
274
209
|
}
|
|
275
210
|
}
|