1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.Cache = void 0;
|
4 | const tslib_1 = require("tslib");
|
5 | const fslib_1 = require("@yarnpkg/fslib");
|
6 | const fslib_2 = require("@yarnpkg/fslib");
|
7 | const libzip_1 = require("@yarnpkg/libzip");
|
8 | const fs_1 = tslib_1.__importDefault(require("fs"));
|
9 | const MessageName_1 = require("./MessageName");
|
10 | const Report_1 = require("./Report");
|
11 | const hashUtils = tslib_1.__importStar(require("./hashUtils"));
|
12 | const miscUtils = tslib_1.__importStar(require("./miscUtils"));
|
13 | const structUtils = tslib_1.__importStar(require("./structUtils"));
|
14 | const CACHE_VERSION = 7;
|
15 | class Cache {
|
16 | constructor(cacheCwd, { configuration, immutable = configuration.get(`enableImmutableCache`), check = false }) {
|
17 |
|
18 |
|
19 |
|
20 | this.markedFiles = new Set();
|
21 | this.mutexes = new Map();
|
22 | this.configuration = configuration;
|
23 | this.cwd = cacheCwd;
|
24 | this.immutable = immutable;
|
25 | this.check = check;
|
26 | const cacheKeyOverride = configuration.get(`cacheKeyOverride`);
|
27 | if (cacheKeyOverride !== null) {
|
28 | this.cacheKey = `${cacheKeyOverride}`;
|
29 | }
|
30 | else {
|
31 | const compressionLevel = configuration.get(`compressionLevel`);
|
32 | const compressionKey = compressionLevel !== fslib_2.DEFAULT_COMPRESSION_LEVEL
|
33 | ? `c${compressionLevel}` : ``;
|
34 | this.cacheKey = [
|
35 | CACHE_VERSION,
|
36 | compressionKey,
|
37 | ].join(``);
|
38 | }
|
39 | }
|
40 | static async find(configuration, { immutable, check } = {}) {
|
41 | const cache = new Cache(configuration.get(`cacheFolder`), { configuration, immutable, check });
|
42 | await cache.setup();
|
43 | return cache;
|
44 | }
|
45 | get mirrorCwd() {
|
46 | if (!this.configuration.get(`enableMirror`))
|
47 | return null;
|
48 | const mirrorCwd = `${this.configuration.get(`globalFolder`)}/cache`;
|
49 | return mirrorCwd !== this.cwd ? mirrorCwd : null;
|
50 | }
|
51 | getVersionFilename(locator) {
|
52 | return `${structUtils.slugifyLocator(locator)}-${this.cacheKey}.zip`;
|
53 | }
|
54 | getChecksumFilename(locator, checksum) {
|
55 |
|
56 |
|
57 | const contentChecksum = getHashComponent(checksum);
|
58 |
|
59 |
|
60 |
|
61 | const significantChecksum = contentChecksum.slice(0, 10);
|
62 | return `${structUtils.slugifyLocator(locator)}-${significantChecksum}.zip`;
|
63 | }
|
64 | getLocatorPath(locator, expectedChecksum) {
|
65 |
|
66 |
|
67 | if (this.mirrorCwd === null)
|
68 | return fslib_2.ppath.resolve(this.cwd, this.getVersionFilename(locator));
|
69 |
|
70 |
|
71 | if (expectedChecksum === null)
|
72 | return null;
|
73 |
|
74 |
|
75 | const cacheKey = getCacheKeyComponent(expectedChecksum);
|
76 | if (cacheKey !== this.cacheKey)
|
77 | return null;
|
78 | return fslib_2.ppath.resolve(this.cwd, this.getChecksumFilename(locator, expectedChecksum));
|
79 | }
|
80 | getLocatorMirrorPath(locator) {
|
81 | const mirrorCwd = this.mirrorCwd;
|
82 | return mirrorCwd !== null ? fslib_2.ppath.resolve(mirrorCwd, this.getVersionFilename(locator)) : null;
|
83 | }
|
84 | async setup() {
|
85 | if (!this.configuration.get(`enableGlobalCache`)) {
|
86 | await fslib_2.xfs.mkdirPromise(this.cwd, { recursive: true });
|
87 | const gitignorePath = fslib_2.ppath.resolve(this.cwd, `.gitignore`);
|
88 | const gitignoreExists = await fslib_2.xfs.existsPromise(gitignorePath);
|
89 | if (!gitignoreExists) {
|
90 | await fslib_2.xfs.writeFilePromise(gitignorePath, `/.gitignore\n*.lock\n`);
|
91 | }
|
92 | }
|
93 | }
|
94 | async fetchPackageFromCache(locator, expectedChecksum, { onHit, onMiss, loader, skipIntegrityCheck }) {
|
95 | const mirrorPath = this.getLocatorMirrorPath(locator);
|
96 | const baseFs = new fslib_1.NodeFS();
|
97 | const validateFile = async (path, refetchPath = null) => {
|
98 | const actualChecksum = (!skipIntegrityCheck || !expectedChecksum) ? `${this.cacheKey}/${await hashUtils.checksumFile(path)}` : expectedChecksum;
|
99 | if (refetchPath !== null) {
|
100 | const previousChecksum = (!skipIntegrityCheck || !expectedChecksum) ? `${this.cacheKey}/${await hashUtils.checksumFile(refetchPath)}` : expectedChecksum;
|
101 | if (actualChecksum !== previousChecksum) {
|
102 | throw new Report_1.ReportError(MessageName_1.MessageName.CACHE_CHECKSUM_MISMATCH, `The remote archive doesn't match the local checksum - has the local cache been corrupted?`);
|
103 | }
|
104 | }
|
105 | if (expectedChecksum !== null && actualChecksum !== expectedChecksum) {
|
106 | let checksumBehavior;
|
107 |
|
108 | if (this.check)
|
109 | checksumBehavior = `throw`;
|
110 |
|
111 | else if (getCacheKeyComponent(expectedChecksum) !== getCacheKeyComponent(actualChecksum))
|
112 | checksumBehavior = `update`;
|
113 | else
|
114 | checksumBehavior = this.configuration.get(`checksumBehavior`);
|
115 | switch (checksumBehavior) {
|
116 | case `ignore`:
|
117 | return expectedChecksum;
|
118 | case `update`:
|
119 | return actualChecksum;
|
120 | default:
|
121 | case `throw`: {
|
122 | throw new Report_1.ReportError(MessageName_1.MessageName.CACHE_CHECKSUM_MISMATCH, `The remote archive doesn't match the expected checksum`);
|
123 | }
|
124 | }
|
125 | }
|
126 | return actualChecksum;
|
127 | };
|
128 | const validateFileAgainstRemote = async (cachePath) => {
|
129 | if (!loader)
|
130 | throw new Error(`Cache check required but no loader configured for ${structUtils.prettyLocator(this.configuration, locator)}`);
|
131 | const zipFs = await loader();
|
132 | const refetchPath = zipFs.getRealPath();
|
133 | zipFs.saveAndClose();
|
134 | await fslib_2.xfs.chmodPromise(refetchPath, 0o644);
|
135 | return await validateFile(cachePath, refetchPath);
|
136 | };
|
137 | const loadPackageThroughMirror = async () => {
|
138 | if (mirrorPath === null || !(await fslib_2.xfs.existsPromise(mirrorPath))) {
|
139 | const zipFs = await loader();
|
140 | const realPath = zipFs.getRealPath();
|
141 | zipFs.saveAndClose();
|
142 | return realPath;
|
143 | }
|
144 | const tempDir = await fslib_2.xfs.mktempPromise();
|
145 | const tempPath = fslib_2.ppath.join(tempDir, this.getVersionFilename(locator));
|
146 | await fslib_2.xfs.copyFilePromise(mirrorPath, tempPath, fs_1.default.constants.COPYFILE_FICLONE);
|
147 | return tempPath;
|
148 | };
|
149 | const loadPackage = async () => {
|
150 | if (!loader)
|
151 | throw new Error(`Cache entry required but missing for ${structUtils.prettyLocator(this.configuration, locator)}`);
|
152 | if (this.immutable)
|
153 | throw new Report_1.ReportError(MessageName_1.MessageName.IMMUTABLE_CACHE, `Cache entry required but missing for ${structUtils.prettyLocator(this.configuration, locator)}`);
|
154 | const originalPath = await loadPackageThroughMirror();
|
155 | await fslib_2.xfs.chmodPromise(originalPath, 0o644);
|
156 |
|
157 | const checksum = await validateFile(originalPath);
|
158 | const cachePath = this.getLocatorPath(locator, checksum);
|
159 | if (!cachePath)
|
160 | throw new Error(`Assertion failed: Expected the cache path to be available`);
|
161 | return await this.writeFileWithLock(cachePath, async () => {
|
162 | return await this.writeFileWithLock(mirrorPath, async () => {
|
163 |
|
164 | await fslib_2.xfs.movePromise(originalPath, cachePath);
|
165 | if (mirrorPath !== null)
|
166 | await fslib_2.xfs.copyFilePromise(cachePath, mirrorPath, fs_1.default.constants.COPYFILE_FICLONE);
|
167 | return [cachePath, checksum];
|
168 | });
|
169 | });
|
170 | };
|
171 | const loadPackageThroughMutex = async () => {
|
172 | const mutexedLoad = async () => {
|
173 |
|
174 |
|
175 |
|
176 | const tentativeCachePath = this.getLocatorPath(locator, expectedChecksum);
|
177 | const cacheExists = tentativeCachePath !== null
|
178 | ? await baseFs.existsPromise(tentativeCachePath)
|
179 | : false;
|
180 | const action = cacheExists
|
181 | ? onHit
|
182 | : onMiss;
|
183 | if (action)
|
184 | action();
|
185 | if (!cacheExists) {
|
186 | return loadPackage();
|
187 | }
|
188 | else {
|
189 | let checksum = null;
|
190 | const cachePath = tentativeCachePath;
|
191 | if (this.check)
|
192 | checksum = await validateFileAgainstRemote(cachePath);
|
193 | else
|
194 | checksum = await validateFile(cachePath);
|
195 | return [cachePath, checksum];
|
196 | }
|
197 | };
|
198 | const mutex = mutexedLoad();
|
199 | this.mutexes.set(locator.locatorHash, mutex);
|
200 | try {
|
201 | return await mutex;
|
202 | }
|
203 | finally {
|
204 | this.mutexes.delete(locator.locatorHash);
|
205 | }
|
206 | };
|
207 | for (let mutex; (mutex = this.mutexes.get(locator.locatorHash));)
|
208 | await mutex;
|
209 | const [cachePath, checksum] = await loadPackageThroughMutex();
|
210 | this.markedFiles.add(cachePath);
|
211 | let zipFs = null;
|
212 | const libzip = await libzip_1.getLibzipPromise();
|
213 | const lazyFs = new fslib_1.LazyFS(() => miscUtils.prettifySyncErrors(() => {
|
214 | return zipFs = new fslib_1.ZipFS(cachePath, { baseFs, libzip, readOnly: true });
|
215 | }, message => {
|
216 | return `Failed to open the cache entry for ${structUtils.prettyLocator(this.configuration, locator)}: ${message}`;
|
217 | }), fslib_2.ppath);
|
218 |
|
219 |
|
220 | const aliasFs = new fslib_1.AliasFS(cachePath, { baseFs: lazyFs, pathUtils: fslib_2.ppath });
|
221 | const releaseFs = () => {
|
222 | if (zipFs !== null) {
|
223 | zipFs.discardAndClose();
|
224 | }
|
225 | };
|
226 | return [aliasFs, releaseFs, checksum];
|
227 | }
|
228 | async writeFileWithLock(file, generator) {
|
229 | if (file === null)
|
230 | return await generator();
|
231 | await fslib_2.xfs.mkdirPromise(fslib_2.ppath.dirname(file), { recursive: true });
|
232 | return await fslib_2.xfs.lockPromise(file, async () => {
|
233 | return await generator();
|
234 | });
|
235 | }
|
236 | }
|
237 | exports.Cache = Cache;
|
238 | function getCacheKeyComponent(checksum) {
|
239 | const split = checksum.indexOf(`/`);
|
240 | return split !== -1 ? checksum.slice(0, split) : null;
|
241 | }
|
242 | function getHashComponent(checksum) {
|
243 | const split = checksum.indexOf(`/`);
|
244 | return split !== -1 ? checksum.slice(split + 1) : checksum;
|
245 | }
|