@pnpm/fetching.tarball-fetcher 1102.1.6 → 1102.1.8
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/CHANGELOG.md +24 -0
- package/lib/gitHostedTarballFetcher.js +72 -37
- package/lib/localTarballFetcher.js +5 -2
- package/lib/remoteTarballFetcher.d.ts +1 -0
- package/lib/remoteTarballFetcher.js +245 -154
- package/package.json +11 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @pnpm/tarball-fetcher
|
|
2
2
|
|
|
3
|
+
## 1102.1.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Large package downloads and large files inside gzip and bzip2 package archives now use bounded memory during installation. Package manifests and archive metadata larger than 64 MiB are rejected.
|
|
8
|
+
|
|
9
|
+
- Updated dependencies:
|
|
10
|
+
- @pnpm/error@1100.2.2
|
|
11
|
+
- @pnpm/exec.prepare-package@1100.0.42
|
|
12
|
+
- @pnpm/fs.graceful-fs@1100.2.5
|
|
13
|
+
- @pnpm/fs.packlist@1100.0.8
|
|
14
|
+
- @pnpm/network.fetch@1100.1.22
|
|
15
|
+
- @pnpm/store.index@1101.0.1
|
|
16
|
+
|
|
17
|
+
## 1102.1.7
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies:
|
|
22
|
+
- @pnpm/core-loggers@1101.0.2
|
|
23
|
+
- @pnpm/exec.prepare-package@1100.0.41
|
|
24
|
+
- @pnpm/fs.packlist@1100.0.7
|
|
25
|
+
- @pnpm/network.fetch@1100.1.21
|
|
26
|
+
|
|
3
27
|
## 1102.1.6
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
|
@@ -45,6 +45,58 @@ export function createGitHostedTarballFetcher(fetchRemoteTarball, fetcherOpts) {
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
async function prepareGitHostedPkg(filesMap, cafs, rawFilesIndexFile, filesIndexFile, opts, fetcherOpts, resolution) {
|
|
48
|
+
const { shouldBeBuilt, pkgDir, ignoredBuild } = await unpackAndPreparePackage(cafs, filesMap, opts, fetcherOpts, resolution);
|
|
49
|
+
filesIndexFile = resolveGitHostedIndexFile({
|
|
50
|
+
filesIndexFile,
|
|
51
|
+
ignoredBuild,
|
|
52
|
+
ignoreScripts: opts.ignoreScripts,
|
|
53
|
+
pkgResolutionId: fetcherOpts.pkgResolutionId ?? createGitHostedTarballPkgResolutionId(resolution),
|
|
54
|
+
shouldBeBuilt,
|
|
55
|
+
});
|
|
56
|
+
const files = await packlist(pkgDir);
|
|
57
|
+
const isUnchanged = !resolution.path && files.length === filesMap.size && (!shouldBeBuilt || ignoredBuild);
|
|
58
|
+
if (isUnchanged) {
|
|
59
|
+
return handleUnchangedGitPkg({
|
|
60
|
+
filesIndexFile,
|
|
61
|
+
filesMap,
|
|
62
|
+
ignoredBuild,
|
|
63
|
+
ignoreScripts: opts.ignoreScripts,
|
|
64
|
+
rawFilesIndexFile,
|
|
65
|
+
shouldBeBuilt,
|
|
66
|
+
storeIndex: opts.storeIndex,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return reindexAndAddGitPkgFiles({
|
|
70
|
+
cafs,
|
|
71
|
+
fetcherOpts,
|
|
72
|
+
files,
|
|
73
|
+
filesIndexFile,
|
|
74
|
+
ignoredBuild,
|
|
75
|
+
opts,
|
|
76
|
+
pkgDir,
|
|
77
|
+
rawFilesIndexFile,
|
|
78
|
+
shouldBeBuilt,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function reindexAndAddGitPkgFiles(opts) {
|
|
82
|
+
opts.opts.storeIndex.delete(opts.rawFilesIndexFile);
|
|
83
|
+
return {
|
|
84
|
+
filesIndexFile: opts.filesIndexFile,
|
|
85
|
+
...await addFilesFromDir({
|
|
86
|
+
storeDir: opts.cafs.storeDir,
|
|
87
|
+
storeIndex: opts.opts.storeIndex,
|
|
88
|
+
dir: opts.pkgDir,
|
|
89
|
+
files: opts.files,
|
|
90
|
+
filesIndexFile: opts.filesIndexFile,
|
|
91
|
+
pkg: opts.fetcherOpts.pkg,
|
|
92
|
+
readManifest: opts.fetcherOpts.readManifest,
|
|
93
|
+
requiresPrepare: opts.shouldBeBuilt,
|
|
94
|
+
}),
|
|
95
|
+
ignoredBuild: opts.ignoredBuild,
|
|
96
|
+
requiresPrepare: opts.shouldBeBuilt,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function unpackAndPreparePackage(cafs, filesMap, opts, fetcherOpts, resolution) {
|
|
48
100
|
const tempLocation = await cafs.tempDir();
|
|
49
101
|
cafs.importPackage(tempLocation, {
|
|
50
102
|
filesResponse: {
|
|
@@ -59,47 +111,30 @@ async function prepareGitHostedPkg(filesMap, cafs, rawFilesIndexFile, filesIndex
|
|
|
59
111
|
allowBuild: fetcherOpts.allowBuild,
|
|
60
112
|
pkgResolutionId: fetcherOpts.pkgResolutionId ?? createGitHostedTarballPkgResolutionId(resolution),
|
|
61
113
|
}, tempLocation, resolution.path ?? '');
|
|
62
|
-
|
|
63
|
-
|
|
114
|
+
return { shouldBeBuilt, pkgDir, ignoredBuild };
|
|
115
|
+
}
|
|
116
|
+
function resolveGitHostedIndexFile(opts) {
|
|
117
|
+
const shouldReindex = opts.shouldBeBuilt &&
|
|
118
|
+
((opts.ignoredBuild && !opts.ignoreScripts) || (!opts.ignoredBuild && opts.filesIndexFile.endsWith('\tnot-built')));
|
|
119
|
+
if (shouldReindex) {
|
|
120
|
+
return gitHostedStoreIndexKey(opts.pkgResolutionId, { built: !opts.ignoredBuild });
|
|
64
121
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
storeIndex.set(filesIndexFile, data);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
storeIndex.delete(rawFilesIndexFile);
|
|
77
|
-
return {
|
|
78
|
-
filesIndexFile,
|
|
79
|
-
filesMap,
|
|
80
|
-
ignoredBuild,
|
|
81
|
-
requiresPrepare: shouldBeBuilt,
|
|
82
|
-
};
|
|
122
|
+
return opts.filesIndexFile;
|
|
123
|
+
}
|
|
124
|
+
function handleUnchangedGitPkg(opts) {
|
|
125
|
+
if (!opts.ignoredBuild || !opts.ignoreScripts) {
|
|
126
|
+
const data = opts.storeIndex.get(opts.rawFilesIndexFile);
|
|
127
|
+
if (data) {
|
|
128
|
+
data.requiresPrepare = opts.shouldBeBuilt;
|
|
129
|
+
opts.storeIndex.set(opts.filesIndexFile, data);
|
|
83
130
|
}
|
|
84
131
|
}
|
|
85
|
-
storeIndex.delete(rawFilesIndexFile);
|
|
86
|
-
// Important! We cannot remove the temp location at this stage.
|
|
87
|
-
// Even though we have the index of the package,
|
|
88
|
-
// the linking of files to the store is in progress.
|
|
132
|
+
opts.storeIndex.delete(opts.rawFilesIndexFile);
|
|
89
133
|
return {
|
|
90
|
-
filesIndexFile,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
dir: pkgDir,
|
|
95
|
-
files,
|
|
96
|
-
filesIndexFile,
|
|
97
|
-
pkg: fetcherOpts.pkg,
|
|
98
|
-
readManifest: fetcherOpts.readManifest,
|
|
99
|
-
requiresPrepare: shouldBeBuilt,
|
|
100
|
-
}),
|
|
101
|
-
ignoredBuild,
|
|
102
|
-
requiresPrepare: shouldBeBuilt,
|
|
134
|
+
filesIndexFile: opts.filesIndexFile,
|
|
135
|
+
filesMap: opts.filesMap,
|
|
136
|
+
ignoredBuild: opts.ignoredBuild,
|
|
137
|
+
requiresPrepare: opts.shouldBeBuilt,
|
|
103
138
|
};
|
|
104
139
|
}
|
|
105
140
|
function createGitHostedTarballPkgResolutionId(resolution) {
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import gfs from '@pnpm/fs.graceful-fs';
|
|
3
3
|
import { addFilesFromTarball } from '@pnpm/worker';
|
|
4
|
+
import { MAX_BUFFERED_DOWNLOAD_SIZE } from './remoteTarballFetcher.js';
|
|
4
5
|
const isAbsolutePath = /^\/|^[A-Z]:/i;
|
|
5
6
|
export function createLocalTarballFetcher(storeIndex) {
|
|
6
7
|
const fetch = async (cafs, resolution, opts) => {
|
|
7
8
|
const tarball = resolvePath(opts.lockfileDir, resolution.tarball.slice(5));
|
|
8
|
-
const
|
|
9
|
+
const source = gfs.statSync(tarball).size > MAX_BUFFERED_DOWNLOAD_SIZE
|
|
10
|
+
? { tarballFile: tarball }
|
|
11
|
+
: { buffer: gfs.readFileSync(tarball) };
|
|
9
12
|
const result = await addFilesFromTarball({
|
|
10
13
|
storeDir: cafs.storeDir,
|
|
11
14
|
storeIndex,
|
|
12
|
-
|
|
15
|
+
...source,
|
|
13
16
|
filesIndexFile: opts.filesIndexFile,
|
|
14
17
|
integrity: resolution.integrity,
|
|
15
18
|
readManifest: opts.readManifest,
|
|
@@ -3,6 +3,7 @@ import type { FetchOptions, FetchResult } from '@pnpm/fetching.fetcher-base';
|
|
|
3
3
|
import type { FetchFromRegistry, GetAuthHeader, RetryTimeoutOptions } from '@pnpm/fetching.types';
|
|
4
4
|
import type { Cafs } from '@pnpm/store.cafs-types';
|
|
5
5
|
import type { StoreIndex } from '@pnpm/store.index';
|
|
6
|
+
export declare const MAX_BUFFERED_DOWNLOAD_SIZE: number;
|
|
6
7
|
export interface HttpResponse {
|
|
7
8
|
body: string;
|
|
8
9
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
1
2
|
import { isIP } from 'node:net';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import { requestRetryLogger } from '@pnpm/core-loggers';
|
|
3
5
|
import { FetchError, FetchTimeoutError, isError, isFetchTimeoutError, redactUrlForDisplay } from '@pnpm/error';
|
|
4
6
|
import { globalWarn } from '@pnpm/logger';
|
|
@@ -7,9 +9,10 @@ import { addFilesFromTarball } from '@pnpm/worker';
|
|
|
7
9
|
import * as retry from '@zkochan/retry';
|
|
8
10
|
import throttle from 'lodash.throttle';
|
|
9
11
|
import { BadTarballError } from './errorTypes/index.js';
|
|
12
|
+
export const MAX_BUFFERED_DOWNLOAD_SIZE = 64 * 1024 * 1024;
|
|
10
13
|
const BIG_TARBALL_SIZE = 1024 * 1024 * 5; // 5 MB
|
|
11
14
|
export function createDownloader(fetchFromRegistry, gotOpts) {
|
|
12
|
-
const
|
|
15
|
+
const defaultRetryOpts = {
|
|
13
16
|
factor: 10,
|
|
14
17
|
maxTimeout: 6e4, // 1 minute
|
|
15
18
|
minTimeout: 1e4, // 10 seconds
|
|
@@ -19,166 +22,254 @@ export function createDownloader(fetchFromRegistry, gotOpts) {
|
|
|
19
22
|
const fetchMinSpeedKiBps = gotOpts.fetchMinSpeedKiBps ?? 50; // 50 KiB/s
|
|
20
23
|
return async function download(url, opts) {
|
|
21
24
|
const authHeaderValue = getSecureNodeMirrorAuthHeader(opts.getAuthHeaderByURI(url, { pkgName: opts.pkg?.name }), url, opts.appendManifest?.name);
|
|
22
|
-
const downloadRetryOpts = { ...
|
|
25
|
+
const downloadRetryOpts = { ...defaultRetryOpts, ...opts.retry };
|
|
23
26
|
const op = retry.operation(downloadRetryOpts);
|
|
24
27
|
return new Promise((resolve, reject) => {
|
|
25
|
-
op.attempt(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
catch (error) { // eslint-disable-line
|
|
30
|
-
const status = error.response?.status;
|
|
31
|
-
const isManualRedirect = opts.redirect === 'manual' && status >= 300 && status < 400;
|
|
32
|
-
if (isManualRedirect ||
|
|
33
|
-
status === 401 ||
|
|
34
|
-
status === 403 ||
|
|
35
|
-
status === 404 ||
|
|
36
|
-
error.code === 'ERR_PNPM_PREPARE_PKG_FAILURE' ||
|
|
37
|
-
isNonRetryableError(error)) {
|
|
38
|
-
reject(error);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
const timeout = op.retry(error);
|
|
42
|
-
if (timeout === false) {
|
|
43
|
-
reject(op.mainError());
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
// Extract error properties into a plain object because Error properties
|
|
47
|
-
// are non-enumerable and don't serialize well through the logging system
|
|
48
|
-
const errorInfo = {
|
|
49
|
-
name: error.name,
|
|
50
|
-
message: error.message,
|
|
51
|
-
code: error.code,
|
|
52
|
-
errno: error.errno,
|
|
53
|
-
// For HTTP errors from our ResponseError class
|
|
54
|
-
status: error.status,
|
|
55
|
-
statusCode: error.statusCode,
|
|
56
|
-
// undici wraps the actual network error in a cause property
|
|
57
|
-
cause: error.cause ? {
|
|
58
|
-
code: error.cause.code,
|
|
59
|
-
errno: error.cause.errno,
|
|
60
|
-
} : undefined,
|
|
61
|
-
};
|
|
62
|
-
requestRetryLogger.debug({
|
|
63
|
-
attempt,
|
|
64
|
-
error: errorInfo,
|
|
65
|
-
maxRetries: downloadRetryOpts.retries,
|
|
66
|
-
method: 'GET',
|
|
67
|
-
timeout,
|
|
68
|
-
url: redactUrlForDisplay(url),
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
async function fetch(currentAttempt) {
|
|
74
|
-
let data;
|
|
75
|
-
try {
|
|
76
|
-
const res = await fetchFromRegistry(url, {
|
|
28
|
+
op.attempt((attempt) => {
|
|
29
|
+
void executeDownloadAttempt({
|
|
30
|
+
attempt,
|
|
77
31
|
authHeaderValue,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
retry: { retries: 0 },
|
|
88
|
-
redirect: opts.redirect,
|
|
89
|
-
timeout: gotOpts.timeout,
|
|
32
|
+
downloadRetryOpts,
|
|
33
|
+
fetchFromRegistry,
|
|
34
|
+
fetchMinSpeedKiBps,
|
|
35
|
+
gotOpts,
|
|
36
|
+
op,
|
|
37
|
+
opts,
|
|
38
|
+
reject,
|
|
39
|
+
resolve,
|
|
40
|
+
url,
|
|
90
41
|
});
|
|
91
|
-
if (res.status !== 200) {
|
|
92
|
-
throw new FetchError({ url, authHeaderValue }, res);
|
|
93
|
-
}
|
|
94
|
-
// When Content-Encoding is present, Content-Length refers to the encoded form
|
|
95
|
-
// of the data, not the decoded bytes that the fetch implementation yields.
|
|
96
|
-
// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding
|
|
97
|
-
const isEncoded = isContentEncoded(res.headers.get('content-encoding'));
|
|
98
|
-
const contentLength = !isEncoded && res.headers.has('content-length') && res.headers.get('content-length');
|
|
99
|
-
const parsedLength = typeof contentLength === 'string' ? parseInt(contentLength, 10) : NaN;
|
|
100
|
-
const size = Number.isFinite(parsedLength) && parsedLength >= 0 ? parsedLength : null;
|
|
101
|
-
if (opts.onStart != null) {
|
|
102
|
-
opts.onStart(size, currentAttempt);
|
|
103
|
-
}
|
|
104
|
-
// In order to reduce the amount of logs, we only report the download progress of big tarballs
|
|
105
|
-
const onProgress = (size != null && size >= BIG_TARBALL_SIZE && opts.onProgress)
|
|
106
|
-
? throttle(opts.onProgress, 500)
|
|
107
|
-
: undefined;
|
|
108
|
-
const startTime = Date.now();
|
|
109
|
-
let downloaded = 0;
|
|
110
|
-
if (size !== null) {
|
|
111
|
-
// Known size: pre-allocate and copy directly (avoids intermediate array + second copy pass)
|
|
112
|
-
data = Buffer.from(new SharedArrayBuffer(size));
|
|
113
|
-
for await (const chunk of res.body) {
|
|
114
|
-
const bytes = chunk;
|
|
115
|
-
const nextDownloaded = downloaded + bytes.byteLength;
|
|
116
|
-
if (nextDownloaded > size) {
|
|
117
|
-
throw new BadTarballError({
|
|
118
|
-
expectedSize: size,
|
|
119
|
-
receivedSize: nextDownloaded,
|
|
120
|
-
tarballUrl: url,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
data.set(bytes, downloaded);
|
|
124
|
-
downloaded = nextDownloaded;
|
|
125
|
-
onProgress?.(downloaded);
|
|
126
|
-
}
|
|
127
|
-
if (size !== downloaded) {
|
|
128
|
-
throw new BadTarballError({
|
|
129
|
-
expectedSize: size,
|
|
130
|
-
receivedSize: downloaded,
|
|
131
|
-
tarballUrl: url,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
const chunks = [];
|
|
137
|
-
for await (const chunk of res.body) {
|
|
138
|
-
const bytes = chunk;
|
|
139
|
-
chunks.push(bytes);
|
|
140
|
-
downloaded += bytes.byteLength;
|
|
141
|
-
onProgress?.(downloaded);
|
|
142
|
-
}
|
|
143
|
-
data = Buffer.from(new SharedArrayBuffer(downloaded));
|
|
144
|
-
let offset = 0;
|
|
145
|
-
for (const chunk of chunks) {
|
|
146
|
-
data.set(chunk, offset);
|
|
147
|
-
offset += chunk.byteLength;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
const elapsedSec = (Date.now() - startTime) / 1000;
|
|
151
|
-
const avgKiBps = Math.floor((downloaded / elapsedSec) / 1024);
|
|
152
|
-
if (downloaded > 0 && elapsedSec > 1 && avgKiBps < fetchMinSpeedKiBps) {
|
|
153
|
-
const sizeKb = Math.floor(downloaded / 1024);
|
|
154
|
-
globalWarn(`Tarball download average speed ${avgKiBps} KiB/s (size ${sizeKb} KiB) is below ${fetchMinSpeedKiBps} KiB/s: ${redactUrlForDisplay(url)} (GET)`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
catch (err) {
|
|
158
|
-
const error = isFetchTimeoutError(err)
|
|
159
|
-
? new FetchTimeoutError('FETCH_TIMEOUT', url, gotOpts.timeout, { cause: err })
|
|
160
|
-
: isError(err) ? err : new Error(String(err), { cause: err });
|
|
161
|
-
Object.assign(error, {
|
|
162
|
-
attempts: currentAttempt,
|
|
163
|
-
resource: url,
|
|
164
|
-
});
|
|
165
|
-
throw error;
|
|
166
|
-
}
|
|
167
|
-
return addFilesFromTarball({
|
|
168
|
-
buffer: data,
|
|
169
|
-
storeDir: opts.cafs.storeDir,
|
|
170
|
-
storeIndex: opts.storeIndex,
|
|
171
|
-
readManifest: opts.readManifest,
|
|
172
|
-
integrity: opts.integrity,
|
|
173
|
-
filesIndexFile: opts.filesIndexFile,
|
|
174
|
-
pkgId: opts.pkgId,
|
|
175
|
-
url,
|
|
176
|
-
pkg: opts.pkg,
|
|
177
|
-
appendManifest: opts.appendManifest,
|
|
178
|
-
ignoreFilePattern: opts.ignoreFilePattern,
|
|
179
42
|
});
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function executeDownloadAttempt(ctx) {
|
|
47
|
+
try {
|
|
48
|
+
const result = await fetchTarball({
|
|
49
|
+
attempt: ctx.attempt,
|
|
50
|
+
authHeaderValue: ctx.authHeaderValue,
|
|
51
|
+
fetchFromRegistry: ctx.fetchFromRegistry,
|
|
52
|
+
fetchMinSpeedKiBps: ctx.fetchMinSpeedKiBps,
|
|
53
|
+
gotOpts: ctx.gotOpts,
|
|
54
|
+
opts: ctx.opts,
|
|
55
|
+
url: ctx.url,
|
|
56
|
+
});
|
|
57
|
+
ctx.resolve(result);
|
|
58
|
+
}
|
|
59
|
+
catch (error) { // eslint-disable-line
|
|
60
|
+
handleDownloadRetry({
|
|
61
|
+
attempt: ctx.attempt,
|
|
62
|
+
downloadRetryOpts: ctx.downloadRetryOpts,
|
|
63
|
+
error,
|
|
64
|
+
op: ctx.op,
|
|
65
|
+
opts: ctx.opts,
|
|
66
|
+
reject: ctx.reject,
|
|
67
|
+
url: ctx.url,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function fetchTarball(ctx) {
|
|
72
|
+
let data;
|
|
73
|
+
try {
|
|
74
|
+
const res = await ctx.fetchFromRegistry(ctx.url, {
|
|
75
|
+
authHeaderValue: ctx.authHeaderValue,
|
|
76
|
+
headers: { 'accept-encoding': 'identity' },
|
|
77
|
+
retry: { retries: 0 },
|
|
78
|
+
redirect: ctx.opts.redirect,
|
|
79
|
+
timeout: ctx.gotOpts.timeout,
|
|
80
|
+
});
|
|
81
|
+
if (res.status !== 200) {
|
|
82
|
+
throw new FetchError({ url: ctx.url, authHeaderValue: ctx.authHeaderValue }, res);
|
|
180
83
|
}
|
|
84
|
+
const size = parseTarballContentLength(res);
|
|
85
|
+
ctx.opts.onStart?.(size, ctx.attempt);
|
|
86
|
+
const onProgress = (size != null && size >= BIG_TARBALL_SIZE && ctx.opts.onProgress)
|
|
87
|
+
? throttle(ctx.opts.onProgress, 500)
|
|
88
|
+
: undefined;
|
|
89
|
+
data = await readResponseBody({
|
|
90
|
+
body: res.body,
|
|
91
|
+
fetchMinSpeedKiBps: ctx.fetchMinSpeedKiBps,
|
|
92
|
+
onProgress,
|
|
93
|
+
size,
|
|
94
|
+
storeDir: ctx.opts.cafs.storeDir,
|
|
95
|
+
url: ctx.url,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
throw wrapFetchError(err, ctx.url, ctx.attempt, ctx.gotOpts.timeout);
|
|
100
|
+
}
|
|
101
|
+
return importDownloadedTarball(ctx, data);
|
|
102
|
+
}
|
|
103
|
+
async function importDownloadedTarball(ctx, data) {
|
|
104
|
+
try {
|
|
105
|
+
return await addFilesFromTarball({
|
|
106
|
+
buffer: data.buffer,
|
|
107
|
+
tarballFile: data.tarballFile,
|
|
108
|
+
storeDir: ctx.opts.cafs.storeDir,
|
|
109
|
+
storeIndex: ctx.opts.storeIndex,
|
|
110
|
+
readManifest: ctx.opts.readManifest,
|
|
111
|
+
integrity: ctx.opts.integrity,
|
|
112
|
+
filesIndexFile: ctx.opts.filesIndexFile,
|
|
113
|
+
pkgId: ctx.opts.pkgId,
|
|
114
|
+
url: ctx.url,
|
|
115
|
+
pkg: ctx.opts.pkg,
|
|
116
|
+
appendManifest: ctx.opts.appendManifest,
|
|
117
|
+
ignoreFilePattern: ctx.opts.ignoreFilePattern,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await data.cleanup?.();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function parseTarballContentLength(res) {
|
|
125
|
+
const isEncoded = isContentEncoded(res.headers.get('content-encoding'));
|
|
126
|
+
const contentLength = !isEncoded && res.headers.has('content-length') && res.headers.get('content-length');
|
|
127
|
+
const parsedLength = typeof contentLength === 'string' ? parseInt(contentLength, 10) : NaN;
|
|
128
|
+
return Number.isFinite(parsedLength) && parsedLength >= 0 ? parsedLength : null;
|
|
129
|
+
}
|
|
130
|
+
async function readResponseBody(opts) {
|
|
131
|
+
const startTime = Date.now();
|
|
132
|
+
if (opts.size != null && opts.size <= MAX_BUFFERED_DOWNLOAD_SIZE) {
|
|
133
|
+
const buffer = await readKnownSizeBody(opts);
|
|
134
|
+
checkDownloadSpeed(opts.size, startTime, opts.fetchMinSpeedKiBps, opts.url);
|
|
135
|
+
return { buffer };
|
|
136
|
+
}
|
|
137
|
+
const state = { chunks: [], downloaded: 0 };
|
|
138
|
+
try {
|
|
139
|
+
for await (const chunk of opts.body) {
|
|
140
|
+
const bytes = chunk;
|
|
141
|
+
state.downloaded += bytes.byteLength;
|
|
142
|
+
checkDownloadedSize(opts, state.downloaded, false);
|
|
143
|
+
await bufferDownloadChunk(state, bytes, opts.storeDir);
|
|
144
|
+
opts.onProgress?.(state.downloaded);
|
|
145
|
+
}
|
|
146
|
+
checkDownloadedSize(opts, state.downloaded, true);
|
|
147
|
+
checkDownloadSpeed(state.downloaded, startTime, opts.fetchMinSpeedKiBps, opts.url);
|
|
148
|
+
await state.file?.close();
|
|
149
|
+
return finishDownloadBuffer(state);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
await state.file?.close().catch(() => { });
|
|
153
|
+
if (state.directory)
|
|
154
|
+
await fs.rm(state.directory, { recursive: true, force: true });
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function readKnownSizeBody(opts) {
|
|
159
|
+
const buffer = Buffer.from(new SharedArrayBuffer(opts.size));
|
|
160
|
+
let downloaded = 0;
|
|
161
|
+
for await (const chunk of opts.body) {
|
|
162
|
+
const bytes = chunk;
|
|
163
|
+
checkDownloadedSize(opts, downloaded + bytes.byteLength, false);
|
|
164
|
+
buffer.set(bytes, downloaded);
|
|
165
|
+
downloaded += bytes.byteLength;
|
|
166
|
+
opts.onProgress?.(downloaded);
|
|
167
|
+
}
|
|
168
|
+
checkDownloadedSize(opts, downloaded, true);
|
|
169
|
+
return buffer;
|
|
170
|
+
}
|
|
171
|
+
function checkDownloadedSize(opts, downloaded, finished) {
|
|
172
|
+
if (opts.size == null)
|
|
173
|
+
return;
|
|
174
|
+
if (downloaded > opts.size || (finished && downloaded !== opts.size)) {
|
|
175
|
+
throw new BadTarballError({ expectedSize: opts.size, receivedSize: downloaded, tarballUrl: opts.url });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function bufferDownloadChunk(state, bytes, storeDir) {
|
|
179
|
+
if (!state.file && state.downloaded > MAX_BUFFERED_DOWNLOAD_SIZE) {
|
|
180
|
+
await fs.mkdir(storeDir, { recursive: true });
|
|
181
|
+
state.directory = await fs.mkdtemp(path.join(storeDir, 'download-'));
|
|
182
|
+
state.file = await fs.open(path.join(state.directory, 'archive'), 'wx', 0o600);
|
|
183
|
+
for (const buffered of state.chunks) {
|
|
184
|
+
// eslint-disable-next-line no-await-in-loop -- writes preserve tarball byte order
|
|
185
|
+
await state.file.writeFile(buffered);
|
|
186
|
+
}
|
|
187
|
+
state.chunks.length = 0;
|
|
188
|
+
}
|
|
189
|
+
if (state.file) {
|
|
190
|
+
await state.file.writeFile(bytes);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
state.chunks.push(bytes);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function finishDownloadBuffer(state) {
|
|
197
|
+
if (state.directory) {
|
|
198
|
+
const tempDirectory = state.directory;
|
|
199
|
+
return {
|
|
200
|
+
tarballFile: path.join(tempDirectory, 'archive'),
|
|
201
|
+
cleanup: async () => fs.rm(tempDirectory, { recursive: true, force: true }),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const buffer = Buffer.from(new SharedArrayBuffer(state.downloaded));
|
|
205
|
+
let offset = 0;
|
|
206
|
+
for (const chunk of state.chunks) {
|
|
207
|
+
buffer.set(chunk, offset);
|
|
208
|
+
offset += chunk.byteLength;
|
|
209
|
+
}
|
|
210
|
+
return { buffer };
|
|
211
|
+
}
|
|
212
|
+
function checkDownloadSpeed(downloaded, startTime, fetchMinSpeedKiBps, url) {
|
|
213
|
+
const elapsedSec = (Date.now() - startTime) / 1000;
|
|
214
|
+
const avgKiBps = Math.floor((downloaded / elapsedSec) / 1024);
|
|
215
|
+
if (downloaded > 0 && elapsedSec > 1 && avgKiBps < fetchMinSpeedKiBps) {
|
|
216
|
+
const sizeKb = Math.floor(downloaded / 1024);
|
|
217
|
+
globalWarn(`Tarball download average speed ${avgKiBps} KiB/s (size ${sizeKb} KiB) is below ${fetchMinSpeedKiBps} KiB/s: ${redactUrlForDisplay(url)} (GET)`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function wrapFetchError(err, url, attempt, timeout) {
|
|
221
|
+
const error = isFetchTimeoutError(err)
|
|
222
|
+
? new FetchTimeoutError('FETCH_TIMEOUT', url, timeout, { cause: err })
|
|
223
|
+
: isError(err) ? err : new Error(String(err), { cause: err });
|
|
224
|
+
Object.assign(error, {
|
|
225
|
+
attempts: attempt,
|
|
226
|
+
resource: url,
|
|
227
|
+
});
|
|
228
|
+
return error;
|
|
229
|
+
}
|
|
230
|
+
function handleDownloadRetry(opts) {
|
|
231
|
+
const status = opts.error.response?.status;
|
|
232
|
+
const isManualRedirect = opts.opts.redirect === 'manual' && status >= 300 && status < 400;
|
|
233
|
+
const isFatal = isManualRedirect ||
|
|
234
|
+
status === 401 ||
|
|
235
|
+
status === 403 ||
|
|
236
|
+
status === 404 ||
|
|
237
|
+
opts.error.code === 'ERR_PNPM_PREPARE_PKG_FAILURE' ||
|
|
238
|
+
opts.error.code === 'ERR_PNPM_TARBALL_ENTRY_TOO_LARGE' ||
|
|
239
|
+
isNonRetryableError(opts.error);
|
|
240
|
+
if (isFatal) {
|
|
241
|
+
opts.reject(opts.error);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const timeout = opts.op.retry(opts.error);
|
|
245
|
+
if (timeout === false) {
|
|
246
|
+
opts.reject(opts.op.mainError());
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
logRetryAttempt(opts.attempt, opts.error, opts.downloadRetryOpts.retries ?? 0, timeout, opts.url);
|
|
250
|
+
}
|
|
251
|
+
function logRetryAttempt(attempt, error, // eslint-disable-line
|
|
252
|
+
maxRetries, timeout, url) {
|
|
253
|
+
const errorInfo = {
|
|
254
|
+
name: error.name,
|
|
255
|
+
message: error.message,
|
|
256
|
+
code: error.code,
|
|
257
|
+
errno: error.errno,
|
|
258
|
+
status: error.status,
|
|
259
|
+
statusCode: error.statusCode,
|
|
260
|
+
cause: error.cause ? {
|
|
261
|
+
code: error.cause.code,
|
|
262
|
+
errno: error.cause.errno,
|
|
263
|
+
} : undefined,
|
|
181
264
|
};
|
|
265
|
+
requestRetryLogger.debug({
|
|
266
|
+
attempt,
|
|
267
|
+
error: errorInfo,
|
|
268
|
+
maxRetries,
|
|
269
|
+
method: 'GET',
|
|
270
|
+
timeout,
|
|
271
|
+
url: redactUrlForDisplay(url),
|
|
272
|
+
});
|
|
182
273
|
}
|
|
183
274
|
function getSecureNodeMirrorAuthHeader(authHeaderValue, url, appendedPackageName) {
|
|
184
275
|
if (authHeaderValue == null || appendedPackageName !== 'node')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/fetching.tarball-fetcher",
|
|
3
|
-
"version": "1102.1.
|
|
3
|
+
"version": "1102.1.8",
|
|
4
4
|
"description": "Fetcher for packages hosted as tarballs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -29,15 +29,15 @@
|
|
|
29
29
|
"!*.map"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@pnpm/core-loggers": "1101.0.
|
|
33
|
-
"@pnpm/error": "1100.2.
|
|
34
|
-
"@pnpm/exec.prepare-package": "1100.0.
|
|
32
|
+
"@pnpm/core-loggers": "1101.0.2",
|
|
33
|
+
"@pnpm/error": "1100.2.2",
|
|
34
|
+
"@pnpm/exec.prepare-package": "1100.0.42",
|
|
35
35
|
"@pnpm/fetching.fetcher-base": "1100.2.12",
|
|
36
36
|
"@pnpm/fetching.types": "1100.0.3",
|
|
37
|
-
"@pnpm/fs.graceful-fs": "1100.2.
|
|
38
|
-
"@pnpm/fs.packlist": "1100.0.
|
|
39
|
-
"@pnpm/network.fetch": "1100.1.
|
|
40
|
-
"@pnpm/store.index": "1101.0.
|
|
37
|
+
"@pnpm/fs.graceful-fs": "1100.2.5",
|
|
38
|
+
"@pnpm/fs.packlist": "1100.0.8",
|
|
39
|
+
"@pnpm/network.fetch": "1100.1.22",
|
|
40
|
+
"@pnpm/store.index": "1101.0.1",
|
|
41
41
|
"@pnpm/types": "1102.1.1",
|
|
42
42
|
"@zkochan/retry": "^0.2.0",
|
|
43
43
|
"lodash.throttle": "4.1.1",
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@pnpm/logger": "^1100.0.0",
|
|
48
|
-
"@pnpm/worker": "^1100.4.
|
|
48
|
+
"@pnpm/worker": "^1100.4.8"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@jest/globals": "30.4.1",
|
|
52
|
-
"@pnpm/fetching.tarball-fetcher": "1102.1.
|
|
52
|
+
"@pnpm/fetching.tarball-fetcher": "1102.1.8",
|
|
53
53
|
"@pnpm/logger": "1100.0.0",
|
|
54
54
|
"@pnpm/store.cafs-types": "1100.1.1",
|
|
55
|
-
"@pnpm/store.create-cafs-store": "1100.0.
|
|
55
|
+
"@pnpm/store.create-cafs-store": "1100.0.34",
|
|
56
56
|
"@pnpm/test-fixtures": "1100.0.1",
|
|
57
57
|
"@pnpm/text.ordinal-comparator": "1100.0.0",
|
|
58
58
|
"@pnpm/types": "1102.1.1",
|