@aws-sdk/lib-transfer-manager 0.0.0-stage → 0.1.0
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 +303 -2
- package/dist-cjs/index.js +2 -0
- package/dist-cjs/submodules/transfer-manager/index.js +3166 -0
- package/dist-cjs/submodules/worker/index.js +579 -0
- package/dist-es/index.js +1 -0
- package/dist-es/submodules/transfer-manager/S3TransferManager.js +2229 -0
- package/dist-es/submodules/transfer-manager/chunker.js +115 -0
- package/dist-es/submodules/transfer-manager/directory-transfer-utils.js +102 -0
- package/dist-es/submodules/transfer-manager/event-listener-types.js +1 -0
- package/dist-es/submodules/transfer-manager/file-manager.js +70 -0
- package/dist-es/submodules/transfer-manager/index.browser.js +11 -0
- package/dist-es/submodules/transfer-manager/index.js +11 -0
- package/dist-es/submodules/transfer-manager/join-streams.browser.js +101 -0
- package/dist-es/submodules/transfer-manager/join-streams.js +105 -0
- package/dist-es/submodules/transfer-manager/log-level.js +30 -0
- package/dist-es/submodules/transfer-manager/ordered-part-queue.js +57 -0
- package/dist-es/submodules/transfer-manager/types.js +1 -0
- package/dist-es/submodules/transfer-manager/worker-http-handler.browser.js +25 -0
- package/dist-es/submodules/transfer-manager/worker-http-handler.js +453 -0
- package/dist-es/submodules/worker/http-request-worker.js +578 -0
- package/dist-es/submodules/worker/index.js +1 -0
- package/dist-types/index.d.ts +2 -0
- package/dist-types/submodules/transfer-manager/S3TransferManager.d.ts +60 -0
- package/dist-types/submodules/transfer-manager/chunker.d.ts +7 -0
- package/dist-types/submodules/transfer-manager/directory-transfer-utils.d.ts +90 -0
- package/dist-types/submodules/transfer-manager/event-listener-types.d.ts +54 -0
- package/dist-types/submodules/transfer-manager/file-manager.d.ts +51 -0
- package/dist-types/submodules/transfer-manager/index.browser.d.ts +10 -0
- package/dist-types/submodules/transfer-manager/index.d.ts +10 -0
- package/dist-types/submodules/transfer-manager/join-streams.browser.d.ts +21 -0
- package/dist-types/submodules/transfer-manager/join-streams.d.ts +26 -0
- package/dist-types/submodules/transfer-manager/log-level.d.ts +13 -0
- package/dist-types/submodules/transfer-manager/ordered-part-queue.d.ts +73 -0
- package/dist-types/submodules/transfer-manager/types.d.ts +473 -0
- package/dist-types/submodules/transfer-manager/worker-http-handler.browser.d.ts +20 -0
- package/dist-types/submodules/transfer-manager/worker-http-handler.d.ts +410 -0
- package/dist-types/submodules/worker/http-request-worker.d.ts +1 -0
- package/dist-types/submodules/worker/index.d.ts +1 -0
- package/dist-types/ts3.4/index.d.ts +2 -0
- package/dist-types/ts3.4/submodules/transfer-manager/S3TransferManager.d.ts +38 -0
- package/dist-types/ts3.4/submodules/transfer-manager/chunker.d.ts +10 -0
- package/dist-types/ts3.4/submodules/transfer-manager/directory-transfer-utils.d.ts +23 -0
- package/dist-types/ts3.4/submodules/transfer-manager/event-listener-types.d.ts +12 -0
- package/dist-types/ts3.4/submodules/transfer-manager/file-manager.d.ts +9 -0
- package/dist-types/ts3.4/submodules/transfer-manager/index.browser.d.ts +3 -0
- package/dist-types/ts3.4/submodules/transfer-manager/index.d.ts +3 -0
- package/dist-types/ts3.4/submodules/transfer-manager/join-streams.browser.d.ts +13 -0
- package/dist-types/ts3.4/submodules/transfer-manager/join-streams.d.ts +13 -0
- package/dist-types/ts3.4/submodules/transfer-manager/log-level.d.ts +13 -0
- package/dist-types/ts3.4/submodules/transfer-manager/ordered-part-queue.d.ts +18 -0
- package/dist-types/ts3.4/submodules/transfer-manager/types.d.ts +184 -0
- package/dist-types/ts3.4/submodules/transfer-manager/worker-http-handler.browser.d.ts +17 -0
- package/dist-types/ts3.4/submodules/transfer-manager/worker-http-handler.d.ts +241 -0
- package/dist-types/ts3.4/submodules/worker/http-request-worker.d.ts +1 -0
- package/dist-types/ts3.4/submodules/worker/index.d.ts +1 -0
- package/package.json +108 -4
- package/transfer-manager.d.ts +5 -0
- package/transfer-manager.js +5 -0
- package/worker.d.ts +5 -0
- package/worker.js +5 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import { HttpRequest } from "@smithy/core/protocols";
|
|
2
|
+
import { concatBytes } from "@smithy/core/serde";
|
|
3
|
+
import { NodeHttpHandler } from "@smithy/node-http-handler";
|
|
4
|
+
import dns from "node:dns";
|
|
5
|
+
import { openSync, readSync, closeSync, writeSync, unlinkSync, constants as fsConstants } from "node:fs";
|
|
6
|
+
import { Crc32, Crc32c, Crc64Nvme } from "@aws-sdk/checksums/crc";
|
|
7
|
+
import { Sha1, Sha256 } from "@aws-sdk/checksums/sha";
|
|
8
|
+
import { Md5 } from "@aws-sdk/checksums/md5";
|
|
9
|
+
import { open } from "node:fs/promises";
|
|
10
|
+
import { dirname, join as pathJoin } from "node:path";
|
|
11
|
+
import { Agent as httpsAgent } from "node:https";
|
|
12
|
+
import { parentPort } from "node:worker_threads";
|
|
13
|
+
const DNS_TTL_MS = 1000;
|
|
14
|
+
const dnsCache = new Map();
|
|
15
|
+
const MAX_ERROR_BODY_BYTES = 16 * 1024;
|
|
16
|
+
const readErrorBodyBytes = async (body) => {
|
|
17
|
+
if (!body)
|
|
18
|
+
return undefined;
|
|
19
|
+
try {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
let total = 0;
|
|
22
|
+
for await (const chunk of body) {
|
|
23
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk);
|
|
24
|
+
chunks.push(buf);
|
|
25
|
+
total += buf.length;
|
|
26
|
+
if (total >= MAX_ERROR_BODY_BYTES)
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
return concatBytes(chunks);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const buildHttpStatusError = async (id, response) => {
|
|
36
|
+
return {
|
|
37
|
+
type: "httpDownloadHttpError",
|
|
38
|
+
id,
|
|
39
|
+
statusCode: response.statusCode,
|
|
40
|
+
headers: response.headers,
|
|
41
|
+
body: await readErrorBodyBytes(response.body),
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const O_DIRECT_ALIGN = 4096;
|
|
45
|
+
const O_DIRECT_FLAG = fsConstants.O_DIRECT ?? 0;
|
|
46
|
+
const O_DIRECT_SUPPORTED = process.platform === "linux" && O_DIRECT_FLAG > 0;
|
|
47
|
+
let oDirectEnabled = false;
|
|
48
|
+
let oDirectRequested = false;
|
|
49
|
+
let oDirectProbed = false;
|
|
50
|
+
let oDirectUsable = false;
|
|
51
|
+
function postLog(level, message) {
|
|
52
|
+
parentPort?.postMessage({ type: "workerLog", level, message });
|
|
53
|
+
}
|
|
54
|
+
function probeAlignedPad(dir, buf) {
|
|
55
|
+
const probePath = pathJoin(dir, `.tm-odirect-probe-${process.pid}-${Math.random().toString(36).slice(2)}`);
|
|
56
|
+
let found = -1;
|
|
57
|
+
for (let pad = 0; pad < O_DIRECT_ALIGN; pad += 8) {
|
|
58
|
+
let fd;
|
|
59
|
+
try {
|
|
60
|
+
fd = openSync(probePath, fsConstants.O_RDWR | fsConstants.O_CREAT | fsConstants.O_TRUNC | O_DIRECT_FLAG, 0o600);
|
|
61
|
+
const n = writeSync(fd, buf, pad, O_DIRECT_ALIGN, 0);
|
|
62
|
+
closeSync(fd);
|
|
63
|
+
fd = undefined;
|
|
64
|
+
if (n === O_DIRECT_ALIGN) {
|
|
65
|
+
found = pad;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
if (fd !== undefined) {
|
|
71
|
+
try {
|
|
72
|
+
closeSync(fd);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
unlinkSync(probePath);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
}
|
|
84
|
+
return found;
|
|
85
|
+
}
|
|
86
|
+
function ensureODirectProbed(dir) {
|
|
87
|
+
if (oDirectProbed) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
oDirectProbed = true;
|
|
91
|
+
const testBuf = Buffer.allocUnsafeSlow(O_DIRECT_ALIGN * 2);
|
|
92
|
+
oDirectUsable = probeAlignedPad(dir, testBuf) >= 0;
|
|
93
|
+
if (!oDirectUsable) {
|
|
94
|
+
postLog("warn", `useODirect was requested but O_DIRECT is not usable on the filesystem at "${dir}"; ` +
|
|
95
|
+
`falling back to buffered writes.`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
postLog("debug", `O_DIRECT probe succeeded for filesystem at "${dir}"; using O_DIRECT writes.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function spreadLookup(hostname, options, callback) {
|
|
102
|
+
if (options && options.family === 6) {
|
|
103
|
+
return dns.lookup(hostname, options, callback);
|
|
104
|
+
}
|
|
105
|
+
const deliver = (ips) => {
|
|
106
|
+
const ip = ips[Math.floor(Math.random() * ips.length)];
|
|
107
|
+
if (options && options.all) {
|
|
108
|
+
callback(null, [{ address: ip, family: 4 }]);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
callback(null, ip, 4);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const cached = dnsCache.get(hostname);
|
|
115
|
+
if (cached && cached.ips.length && Date.now() - cached.ts < DNS_TTL_MS) {
|
|
116
|
+
return deliver(cached.ips);
|
|
117
|
+
}
|
|
118
|
+
dns.resolve4(hostname, (err, ips) => {
|
|
119
|
+
if (err || !ips?.length) {
|
|
120
|
+
return dns.lookup(hostname, options, callback);
|
|
121
|
+
}
|
|
122
|
+
dnsCache.set(hostname, { ips, ts: Date.now() });
|
|
123
|
+
deliver(ips);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const UNVERIFIABLE_ALGORITHMS = ["SHA512", "XXHASH3", "XXHASH64", "XXHASH128"];
|
|
127
|
+
function createChecksum(algorithm) {
|
|
128
|
+
switch (algorithm) {
|
|
129
|
+
case "CRC64NVME":
|
|
130
|
+
return new Crc64Nvme();
|
|
131
|
+
case "CRC32C":
|
|
132
|
+
return new Crc32c();
|
|
133
|
+
case "CRC32":
|
|
134
|
+
return new Crc32();
|
|
135
|
+
case "SHA1":
|
|
136
|
+
return new Sha1();
|
|
137
|
+
case "SHA256":
|
|
138
|
+
return new Sha256();
|
|
139
|
+
case "MD5":
|
|
140
|
+
return new Md5();
|
|
141
|
+
default:
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function finalizeChecksumToBase64(checksum) {
|
|
146
|
+
const bytes = await checksum.digest();
|
|
147
|
+
return Buffer.from(bytes).toString("base64");
|
|
148
|
+
}
|
|
149
|
+
function parseContentRange(contentRange) {
|
|
150
|
+
if (!contentRange)
|
|
151
|
+
return undefined;
|
|
152
|
+
const match = contentRange.match(/^bytes\s+(\d+)-(\d+)\//);
|
|
153
|
+
if (!match)
|
|
154
|
+
return undefined;
|
|
155
|
+
const start = Number(match[1]);
|
|
156
|
+
const end = Number(match[2]);
|
|
157
|
+
if (end < start)
|
|
158
|
+
return undefined;
|
|
159
|
+
return { start, end };
|
|
160
|
+
}
|
|
161
|
+
function getChecksumHeaderValue(headers, algorithm) {
|
|
162
|
+
const headerName = `x-amz-checksum-${algorithm.toLowerCase()}`;
|
|
163
|
+
return headers[headerName];
|
|
164
|
+
}
|
|
165
|
+
function resolveDownloadChecksumAlgorithm(headers) {
|
|
166
|
+
const candidates = ["CRC64NVME", "CRC32C", "CRC32", "SHA256", "SHA1"];
|
|
167
|
+
for (const algorithm of candidates) {
|
|
168
|
+
if (getChecksumHeaderValue(headers, algorithm)) {
|
|
169
|
+
return algorithm;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const algorithm of UNVERIFIABLE_ALGORITHMS) {
|
|
173
|
+
if (getChecksumHeaderValue(headers, algorithm)) {
|
|
174
|
+
throw Object.assign(new Error(`S3 returned a ${algorithm} checksum, which worker threads cannot compute. ` +
|
|
175
|
+
`Set workerThreadCount: 1 to validate this object, or set ` +
|
|
176
|
+
`responseChecksumValidation: "WHEN_REQUIRED" to download it without validation.`), { name: "ChecksumValidationError", code: "CHECKSUM_ALGORITHM_UNSUPPORTED" });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
if (parentPort) {
|
|
182
|
+
let handler;
|
|
183
|
+
const port = parentPort;
|
|
184
|
+
const fdCache = new Map();
|
|
185
|
+
function getFd(filePath) {
|
|
186
|
+
let fd = fdCache.get(filePath);
|
|
187
|
+
if (fd === undefined) {
|
|
188
|
+
fd = openSync(filePath, "r");
|
|
189
|
+
fdCache.set(filePath, fd);
|
|
190
|
+
}
|
|
191
|
+
return fd;
|
|
192
|
+
}
|
|
193
|
+
const readFileSlice = (filePath, offset, length) => {
|
|
194
|
+
const fd = getFd(filePath);
|
|
195
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
196
|
+
let read = 0;
|
|
197
|
+
while (read < length) {
|
|
198
|
+
const n = readSync(fd, buffer, read, length - read, offset + read);
|
|
199
|
+
if (n === 0)
|
|
200
|
+
break;
|
|
201
|
+
read += n;
|
|
202
|
+
}
|
|
203
|
+
return buffer;
|
|
204
|
+
};
|
|
205
|
+
const computeDeclaredChecksum = async (data, algorithm) => {
|
|
206
|
+
const checksum = createChecksum(algorithm);
|
|
207
|
+
if (!checksum) {
|
|
208
|
+
throw new Error(`Unsupported checksum algorithm for threaded upload: ${algorithm}`);
|
|
209
|
+
}
|
|
210
|
+
checksum.update(data);
|
|
211
|
+
return finalizeChecksumToBase64(checksum);
|
|
212
|
+
};
|
|
213
|
+
const buildAwsChunkedBody = (data, checksumHeader, checksumValue) => {
|
|
214
|
+
const hexLen = data.byteLength.toString(16);
|
|
215
|
+
const parts = [Buffer.from(`${hexLen}\r\n`), data, Buffer.from("\r\n0\r\n")];
|
|
216
|
+
if (checksumHeader && checksumValue) {
|
|
217
|
+
parts.push(Buffer.from(`${checksumHeader}:${checksumValue}\r\n`));
|
|
218
|
+
}
|
|
219
|
+
parts.push(Buffer.from("\r\n"));
|
|
220
|
+
return Buffer.concat(parts);
|
|
221
|
+
};
|
|
222
|
+
const processRequest = async (msg) => {
|
|
223
|
+
const { id, request: serialized } = msg;
|
|
224
|
+
try {
|
|
225
|
+
let body;
|
|
226
|
+
if (msg.type === "httpRequestFromRAM") {
|
|
227
|
+
const fileData = Buffer.from(msg.sharedBuffer, msg.offset, msg.length);
|
|
228
|
+
if (msg.checksumAlgorithm && msg.checksumHeader) {
|
|
229
|
+
const checksumValue = await computeDeclaredChecksum(fileData, msg.checksumAlgorithm);
|
|
230
|
+
body = buildAwsChunkedBody(fileData, msg.checksumHeader, checksumValue);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
body = fileData;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else if (msg.type === "httpRequestFromFile") {
|
|
237
|
+
const fileData = readFileSlice(msg.filePath, msg.offset, msg.length);
|
|
238
|
+
if (msg.checksumAlgorithm && msg.checksumHeader) {
|
|
239
|
+
const checksumValue = await computeDeclaredChecksum(fileData, msg.checksumAlgorithm);
|
|
240
|
+
body = buildAwsChunkedBody(fileData, msg.checksumHeader, checksumValue);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
body = fileData;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const request = new HttpRequest({
|
|
247
|
+
method: serialized.method,
|
|
248
|
+
protocol: serialized.protocol,
|
|
249
|
+
hostname: serialized.hostname,
|
|
250
|
+
port: serialized.port,
|
|
251
|
+
path: serialized.path,
|
|
252
|
+
query: serialized.query,
|
|
253
|
+
headers: serialized.headers,
|
|
254
|
+
body,
|
|
255
|
+
});
|
|
256
|
+
const { response } = await handler.handle(request);
|
|
257
|
+
const chunks = [];
|
|
258
|
+
if (response.body) {
|
|
259
|
+
for await (const chunk of response.body) {
|
|
260
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
let responseBody;
|
|
264
|
+
const transferList = [];
|
|
265
|
+
if (chunks.length > 0) {
|
|
266
|
+
const concatenated = Buffer.concat(chunks);
|
|
267
|
+
const ab = new ArrayBuffer(concatenated.byteLength);
|
|
268
|
+
new Uint8Array(ab).set(concatenated);
|
|
269
|
+
responseBody = new Uint8Array(ab);
|
|
270
|
+
transferList.push(ab);
|
|
271
|
+
}
|
|
272
|
+
port.postMessage({
|
|
273
|
+
type: "httpResponse",
|
|
274
|
+
id,
|
|
275
|
+
response: {
|
|
276
|
+
statusCode: response.statusCode,
|
|
277
|
+
headers: response.headers,
|
|
278
|
+
body: responseBody,
|
|
279
|
+
},
|
|
280
|
+
}, transferList);
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
port.postMessage({
|
|
284
|
+
type: "httpError",
|
|
285
|
+
id,
|
|
286
|
+
error: err.message ?? String(err),
|
|
287
|
+
code: err.code,
|
|
288
|
+
name: err.name,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const processDownloadToFile = async (msg) => {
|
|
293
|
+
const { id, request: serialized, filePath, offset, expectedLength } = msg;
|
|
294
|
+
try {
|
|
295
|
+
const request = new HttpRequest({
|
|
296
|
+
method: serialized.method,
|
|
297
|
+
protocol: serialized.protocol,
|
|
298
|
+
hostname: serialized.hostname,
|
|
299
|
+
port: serialized.port,
|
|
300
|
+
path: serialized.path,
|
|
301
|
+
query: serialized.query,
|
|
302
|
+
headers: serialized.headers,
|
|
303
|
+
});
|
|
304
|
+
const { response } = await handler.handle(request);
|
|
305
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
306
|
+
port.postMessage(await buildHttpStatusError(id, response));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const contentRange = response.headers["content-range"];
|
|
310
|
+
const range = parseContentRange(contentRange);
|
|
311
|
+
const writeOffset = range ? range.start : offset;
|
|
312
|
+
const targetLength = range ? range.end - range.start + 1 : expectedLength;
|
|
313
|
+
const fh = await open(filePath, "r+");
|
|
314
|
+
try {
|
|
315
|
+
const verifiableDownloadChecksumAlgorithm = resolveDownloadChecksumAlgorithm(response.headers);
|
|
316
|
+
let checksum;
|
|
317
|
+
if (verifiableDownloadChecksumAlgorithm) {
|
|
318
|
+
checksum = createChecksum(verifiableDownloadChecksumAlgorithm);
|
|
319
|
+
}
|
|
320
|
+
let bytesWritten = 0;
|
|
321
|
+
let alignedBuf;
|
|
322
|
+
let pad = -1;
|
|
323
|
+
if (oDirectEnabled && writeOffset % O_DIRECT_ALIGN === 0 && targetLength >= O_DIRECT_ALIGN) {
|
|
324
|
+
ensureODirectProbed(dirname(filePath));
|
|
325
|
+
if (oDirectUsable) {
|
|
326
|
+
alignedBuf = Buffer.allocUnsafeSlow(targetLength + O_DIRECT_ALIGN);
|
|
327
|
+
pad = probeAlignedPad(dirname(filePath), alignedBuf);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else if (oDirectEnabled) {
|
|
331
|
+
postLog("trace", `O_DIRECT skipped: part not 4096-block-aligned (offset=${writeOffset}, length=${targetLength}, block=${O_DIRECT_ALIGN}), using buffered writes.`);
|
|
332
|
+
}
|
|
333
|
+
if (alignedBuf && pad >= 0) {
|
|
334
|
+
if (response.body) {
|
|
335
|
+
for await (const chunk of response.body) {
|
|
336
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
337
|
+
if (bytesWritten + buf.length > targetLength) {
|
|
338
|
+
await fh.close();
|
|
339
|
+
port.postMessage({
|
|
340
|
+
type: "httpDownloadError",
|
|
341
|
+
id,
|
|
342
|
+
error: `Bytes written (${bytesWritten + buf.length}) exceeds expected length (${targetLength})`,
|
|
343
|
+
code: "BYTES_EXCEEDED",
|
|
344
|
+
name: "DownloadValidationError",
|
|
345
|
+
});
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
buf.copy(alignedBuf, pad + bytesWritten, 0, buf.length);
|
|
349
|
+
if (checksum) {
|
|
350
|
+
checksum.update(buf);
|
|
351
|
+
}
|
|
352
|
+
bytesWritten += buf.length;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const alignedLength = Math.floor(bytesWritten / O_DIRECT_ALIGN) * O_DIRECT_ALIGN;
|
|
356
|
+
if (alignedLength > 0) {
|
|
357
|
+
const directFh = await open(filePath, fsConstants.O_RDWR | O_DIRECT_FLAG);
|
|
358
|
+
try {
|
|
359
|
+
await directFh.write(alignedBuf, pad, alignedLength, writeOffset);
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
await directFh.close().catch(() => { });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const tailLength = bytesWritten - alignedLength;
|
|
366
|
+
if (tailLength > 0) {
|
|
367
|
+
await fh.write(alignedBuf, pad + alignedLength, tailLength, writeOffset + alignedLength);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
if (response.body) {
|
|
372
|
+
for await (const chunk of response.body) {
|
|
373
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
374
|
+
if (bytesWritten + buf.length > targetLength) {
|
|
375
|
+
await fh.close();
|
|
376
|
+
port.postMessage({
|
|
377
|
+
type: "httpDownloadError",
|
|
378
|
+
id,
|
|
379
|
+
error: `Bytes written (${bytesWritten + buf.length}) exceeds expected length (${targetLength})`,
|
|
380
|
+
code: "BYTES_EXCEEDED",
|
|
381
|
+
name: "DownloadValidationError",
|
|
382
|
+
});
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
await fh.write(buf, 0, buf.length, writeOffset + bytesWritten);
|
|
386
|
+
if (checksum) {
|
|
387
|
+
checksum.update(buf);
|
|
388
|
+
}
|
|
389
|
+
bytesWritten += buf.length;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
await fh.close();
|
|
394
|
+
if (bytesWritten !== targetLength) {
|
|
395
|
+
port.postMessage({
|
|
396
|
+
type: "httpDownloadError",
|
|
397
|
+
id,
|
|
398
|
+
error: `Bytes written (${bytesWritten}) does not match expected length (${targetLength})`,
|
|
399
|
+
code: "LENGTH_MISMATCH",
|
|
400
|
+
name: "DownloadValidationError",
|
|
401
|
+
});
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
let checksumBase64;
|
|
405
|
+
if (checksum && verifiableDownloadChecksumAlgorithm) {
|
|
406
|
+
checksumBase64 = await finalizeChecksumToBase64(checksum);
|
|
407
|
+
const s3ChecksumValue = getChecksumHeaderValue(response.headers, verifiableDownloadChecksumAlgorithm);
|
|
408
|
+
if (s3ChecksumValue && s3ChecksumValue !== checksumBase64) {
|
|
409
|
+
port.postMessage({
|
|
410
|
+
type: "httpDownloadError",
|
|
411
|
+
id,
|
|
412
|
+
error: `Checksum mismatch: computed ${checksumBase64}, S3 returned ${s3ChecksumValue}`,
|
|
413
|
+
code: "CHECKSUM_MISMATCH",
|
|
414
|
+
name: "ChecksumValidationError",
|
|
415
|
+
});
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
port.postMessage({
|
|
420
|
+
type: "httpDownloadResult",
|
|
421
|
+
id,
|
|
422
|
+
statusCode: response.statusCode,
|
|
423
|
+
headers: response.headers,
|
|
424
|
+
bytesWritten,
|
|
425
|
+
checksum: checksumBase64,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
await fh.close().catch(() => { });
|
|
430
|
+
throw err;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
port.postMessage({
|
|
435
|
+
type: "httpDownloadError",
|
|
436
|
+
id,
|
|
437
|
+
error: err.message ?? String(err),
|
|
438
|
+
code: err.code,
|
|
439
|
+
name: err.name,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
const reusableBufferPool = [];
|
|
444
|
+
function acquireTransferBuffer(size) {
|
|
445
|
+
for (let i = 0; i < reusableBufferPool.length; i++) {
|
|
446
|
+
if (reusableBufferPool[i].byteLength >= size) {
|
|
447
|
+
return reusableBufferPool.splice(i, 1)[0];
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return Buffer.allocUnsafeSlow(size).buffer;
|
|
451
|
+
}
|
|
452
|
+
const processDownloadToTransfer = async (msg) => {
|
|
453
|
+
const { id, request: serialized, expectedSize, rangeIndex } = msg;
|
|
454
|
+
try {
|
|
455
|
+
const request = new HttpRequest({
|
|
456
|
+
method: serialized.method,
|
|
457
|
+
protocol: serialized.protocol,
|
|
458
|
+
hostname: serialized.hostname,
|
|
459
|
+
port: serialized.port,
|
|
460
|
+
path: serialized.path,
|
|
461
|
+
query: serialized.query,
|
|
462
|
+
headers: serialized.headers,
|
|
463
|
+
});
|
|
464
|
+
const { response } = await handler.handle(request);
|
|
465
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
466
|
+
port.postMessage(await buildHttpStatusError(id, response));
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const ab = acquireTransferBuffer(expectedSize);
|
|
470
|
+
const view = new Uint8Array(ab, 0, expectedSize);
|
|
471
|
+
const verifiableDownloadChecksumAlgorithm = resolveDownloadChecksumAlgorithm(response.headers);
|
|
472
|
+
let checksum;
|
|
473
|
+
if (verifiableDownloadChecksumAlgorithm) {
|
|
474
|
+
checksum = createChecksum(verifiableDownloadChecksumAlgorithm);
|
|
475
|
+
}
|
|
476
|
+
let bytesWritten = 0;
|
|
477
|
+
if (response.body) {
|
|
478
|
+
for await (const chunk of response.body) {
|
|
479
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
480
|
+
if (bytesWritten + buf.length > expectedSize) {
|
|
481
|
+
port.postMessage({
|
|
482
|
+
type: "httpDownloadError",
|
|
483
|
+
id,
|
|
484
|
+
error: `Bytes received (${bytesWritten + buf.length}) exceeds expected size (${expectedSize}) for range index ${rangeIndex}`,
|
|
485
|
+
code: "BYTES_EXCEEDED",
|
|
486
|
+
name: "DownloadValidationError",
|
|
487
|
+
});
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
view.set(buf, bytesWritten);
|
|
491
|
+
if (checksum) {
|
|
492
|
+
checksum.update(buf);
|
|
493
|
+
}
|
|
494
|
+
bytesWritten += buf.length;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
let checksumBase64;
|
|
498
|
+
if (checksum && verifiableDownloadChecksumAlgorithm) {
|
|
499
|
+
checksumBase64 = await finalizeChecksumToBase64(checksum);
|
|
500
|
+
const s3ChecksumValue = getChecksumHeaderValue(response.headers, verifiableDownloadChecksumAlgorithm);
|
|
501
|
+
if (s3ChecksumValue && s3ChecksumValue !== checksumBase64) {
|
|
502
|
+
port.postMessage({
|
|
503
|
+
type: "httpDownloadError",
|
|
504
|
+
id,
|
|
505
|
+
error: `Checksum mismatch: computed ${checksumBase64}, S3 returned ${s3ChecksumValue}`,
|
|
506
|
+
code: "CHECKSUM_MISMATCH",
|
|
507
|
+
name: "ChecksumValidationError",
|
|
508
|
+
});
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
port.postMessage({
|
|
513
|
+
type: "httpDownloadStreamResult",
|
|
514
|
+
id,
|
|
515
|
+
rangeIndex,
|
|
516
|
+
buffer: ab,
|
|
517
|
+
byteLength: bytesWritten,
|
|
518
|
+
statusCode: response.statusCode,
|
|
519
|
+
headers: response.headers,
|
|
520
|
+
checksum: checksumBase64,
|
|
521
|
+
}, [ab]);
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
port.postMessage({
|
|
525
|
+
type: "httpDownloadError",
|
|
526
|
+
id,
|
|
527
|
+
error: err.message ?? String(err),
|
|
528
|
+
code: err.code,
|
|
529
|
+
name: err.name,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
port.on("message", (msg) => {
|
|
534
|
+
if (msg.type === "done") {
|
|
535
|
+
for (const fd of fdCache.values()) {
|
|
536
|
+
try {
|
|
537
|
+
closeSync(fd);
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
handler?.destroy();
|
|
543
|
+
process.exit(0);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (msg.type === "config") {
|
|
547
|
+
const { maxSockets, useODirect } = msg;
|
|
548
|
+
oDirectRequested = useODirect === true;
|
|
549
|
+
oDirectEnabled = oDirectRequested && O_DIRECT_SUPPORTED;
|
|
550
|
+
if (oDirectRequested && !O_DIRECT_SUPPORTED) {
|
|
551
|
+
postLog("warn", `useODirect was requested but O_DIRECT is not available on this platform ` +
|
|
552
|
+
`(platform=${process.platform}, O_DIRECT flag ${O_DIRECT_FLAG > 0 ? "present" : "absent"}); ` +
|
|
553
|
+
`falling back to buffered writes.`);
|
|
554
|
+
}
|
|
555
|
+
handler = new NodeHttpHandler({
|
|
556
|
+
httpsAgent: new httpsAgent({
|
|
557
|
+
maxSockets,
|
|
558
|
+
keepAlive: true,
|
|
559
|
+
lookup: spreadLookup,
|
|
560
|
+
}),
|
|
561
|
+
});
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (msg.type === "httpRequestFromFile" || msg.type === "httpRequestFromRAM") {
|
|
565
|
+
processRequest(msg);
|
|
566
|
+
}
|
|
567
|
+
if (msg.type === "httpDownloadToFile") {
|
|
568
|
+
processDownloadToFile(msg);
|
|
569
|
+
}
|
|
570
|
+
if (msg.type === "httpDownloadStream") {
|
|
571
|
+
processDownloadToTransfer(msg);
|
|
572
|
+
}
|
|
573
|
+
if (msg.type === "returnBuffer") {
|
|
574
|
+
reusableBufferPool.push(msg.buffer);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
port.postMessage({ type: "ready" });
|
|
578
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./http-request-worker";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Logger } from "@smithy/types";
|
|
2
|
+
import type { IS3TransferManager, S3TransferManagerConfig } from "./types";
|
|
3
|
+
import type { DownloadStreamResult } from "./worker-http-handler";
|
|
4
|
+
import type { destroyStreams, joinStreams } from "./join-streams";
|
|
5
|
+
/**
|
|
6
|
+
* Structural contract for the worker HTTP handler. Both the Node.js
|
|
7
|
+
* `WorkerHttpHandler` and the browser stub satisfy this shape, keeping the
|
|
8
|
+
* shared implementation environment-agnostic.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export interface IWorkerHttpHandler {
|
|
13
|
+
getDownloadResult(token: string): {
|
|
14
|
+
bytesWritten: number;
|
|
15
|
+
checksum?: string;
|
|
16
|
+
} | undefined;
|
|
17
|
+
getStreamDownloadResult(token: string): DownloadStreamResult | undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
export interface WorkerHttpHandlerConstructor {
|
|
23
|
+
new (options?: {
|
|
24
|
+
workerThreadCount?: number;
|
|
25
|
+
maxConcurrentUploads?: number;
|
|
26
|
+
maxConcurrentDownloads?: number;
|
|
27
|
+
useODirect?: boolean;
|
|
28
|
+
logger?: Logger;
|
|
29
|
+
}): IWorkerHttpHandler;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export interface S3TransferManagerRuntimeDependencies {
|
|
35
|
+
joinStreams: typeof joinStreams;
|
|
36
|
+
destroyStreams: typeof destroyStreams;
|
|
37
|
+
WorkerHttpHandler: WorkerHttpHandlerConstructor;
|
|
38
|
+
createEmptyReadable: () => any;
|
|
39
|
+
defaultWorkerCount: () => number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
export interface S3TransferManagerConstructor {
|
|
45
|
+
new (config?: S3TransferManagerConfig): IS3TransferManager;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Binds environment runtime dependencies.
|
|
49
|
+
*
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
export declare const bindS3TransferManager: (deps: S3TransferManagerRuntimeDependencies) => S3TransferManagerConstructor;
|
|
53
|
+
/**
|
|
54
|
+
* Internal event handler for download lifecycle hooks.
|
|
55
|
+
*
|
|
56
|
+
* @internal
|
|
57
|
+
*/
|
|
58
|
+
export declare const internalEventHandler: {
|
|
59
|
+
afterInitialGetObject(): Promise<void>;
|
|
60
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface RawDataPart {
|
|
2
|
+
partNumber: number;
|
|
3
|
+
data: Uint8Array;
|
|
4
|
+
lastPart?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare const byteLength: (input: any) => number | undefined;
|
|
7
|
+
export declare const getChunk: (data: any, partSize: number) => AsyncGenerator<RawDataPart, void, undefined>;
|