UNPKG

12.6 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.Cache = void 0;
4const tslib_1 = require("tslib");
5const fslib_1 = require("@yarnpkg/fslib");
6const fslib_2 = require("@yarnpkg/fslib");
7const libzip_1 = require("@yarnpkg/libzip");
8const fs_1 = tslib_1.__importDefault(require("fs"));
9const MessageName_1 = require("./MessageName");
10const Report_1 = require("./Report");
11const hashUtils = tslib_1.__importStar(require("./hashUtils"));
12const miscUtils = tslib_1.__importStar(require("./miscUtils"));
13const structUtils = tslib_1.__importStar(require("./structUtils"));
14const CACHE_VERSION = 7;
15class Cache {
16 constructor(cacheCwd, { configuration, immutable = configuration.get(`enableImmutableCache`), check = false }) {
17 // Contains the list of cache files that got accessed since the last time
18 // you cleared the variable. Useful to know which files aren't needed
19 // anymore when used in conjunction with fetchEverything.
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 // We only want the actual checksum (not the cache version, since the whole
56 // point is to avoid changing the filenames when the cache version changes)
57 const contentChecksum = getHashComponent(checksum);
58 // We only care about the first few characters. It doesn't matter if that
59 // makes the hash easier to collide with, because we check the file hashes
60 // during each install anyway.
61 const significantChecksum = contentChecksum.slice(0, 10);
62 return `${structUtils.slugifyLocator(locator)}-${significantChecksum}.zip`;
63 }
64 getLocatorPath(locator, expectedChecksum) {
65 // If there is no mirror, then the local cache *is* the mirror, in which
66 // case we use the versioned filename pattern.
67 if (this.mirrorCwd === null)
68 return fslib_2.ppath.resolve(this.cwd, this.getVersionFilename(locator));
69 // If we don't yet know the checksum, discard the path resolution for now
70 // until the checksum can be obtained from somewhere (mirror or network).
71 if (expectedChecksum === null)
72 return null;
73 // If the cache key changed then we assume that the content probably got
74 // altered as well and thus the existing path won't be good enough anymore.
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 // Using --check-cache overrides any preconfigured checksum behavior
108 if (this.check)
109 checksumBehavior = `throw`;
110 // If the lockfile references an old cache format, we tolerate different checksums
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 // Do this before moving the file so that we don't pollute the cache with corrupted archives
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 // Doing a move is important to ensure atomic writes (todo: cross-drive?)
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 // We don't yet know whether the cache path can be computed yet, since that
174 // depends on whether the cache is actually the mirror or not, and whether
175 // the checksum is known or not.
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 // We use an AliasFS to speed up getRealPath calls (e.g. VirtualFetcher.ensureVirtualLink)
219 // (there's no need to create the lazy baseFs instance to gather the already-known cachePath)
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}
237exports.Cache = Cache;
238function getCacheKeyComponent(checksum) {
239 const split = checksum.indexOf(`/`);
240 return split !== -1 ? checksum.slice(0, split) : null;
241}
242function getHashComponent(checksum) {
243 const split = checksum.indexOf(`/`);
244 return split !== -1 ? checksum.slice(split + 1) : checksum;
245}