(cwd: string, cacheFile: string)
| 272 | } |
| 273 | |
| 274 | async function restoreCacheFromFile (cwd: string, cacheFile: string) { |
| 275 | if (!existsSync(cacheFile)) { |
| 276 | return false |
| 277 | } |
| 278 | |
| 279 | const resolvedCwd = resolve(cwd) + '/' |
| 280 | const files = parseTar(await readFile(cacheFile)) |
| 281 | for (const file of files) { |
| 282 | let fd: FileHandle | undefined = undefined |
| 283 | try { |
| 284 | const filePath = resolve(cwd, file.name) |
| 285 | |
| 286 | // Prevent path traversal attacks |
| 287 | if (!filePath.startsWith(resolvedCwd)) { |
| 288 | consola.warn(`Skipping unsafe cache path: ${file.name}`) |
| 289 | continue |
| 290 | } |
| 291 | |
| 292 | await mkdir(dirname(filePath), { recursive: true }) |
| 293 | |
| 294 | // Stat before open('w') since it truncates the file |
| 295 | const existingStats = await stat(filePath).catch(() => null) |
| 296 | const cachedSize = file.data?.byteLength ?? 0 |
| 297 | if (existingStats?.isFile() && existingStats.size === cachedSize) { |
| 298 | const lastModified = Number.parseInt(file.attrs?.mtime?.toString().padEnd(13, '0') || '0') |
| 299 | if (existingStats.mtime.getTime() >= lastModified) { |
| 300 | consola.debug(`Skipping \`${file.name}\` (up to date or newer than cache)`) |
| 301 | continue |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | fd = await open(filePath, 'w') |
| 306 | await fd.writeFile(file.data!) |
| 307 | } catch (err) { |
| 308 | console.error(err) |
| 309 | } finally { |
| 310 | await fd?.close() |
| 311 | } |
| 312 | } |
| 313 | return true |
| 314 | } |
| 315 | |
| 316 | async function writeCache (cwd: string, sources: string | string[], cacheFile: string) { |
| 317 | const fileEntries = await readFilesRecursive(sources, { |
no test coverage detected
searching dependent graphs…