packages/@aws-cdk/toolkit-lib/lib/util/archive.ts (60 lines of code) (raw):

import { error } from 'console'; import { createWriteStream, promises as fs } from 'fs'; import * as path from 'path'; import * as glob from 'glob'; import { formatErrorMessage } from './format-error'; // eslint-disable-next-line @typescript-eslint/no-require-imports const archiver = require('archiver'); // Adapted from cdk-assets export async function zipDirectory(directory: string, outputFile: string): Promise<void> { // We write to a temporary file and rename at the last moment. This is so that if we are // interrupted during this process, we don't leave a half-finished file in the target location. const temporaryOutputFile = `${outputFile}.${randomString()}._tmp`; await writeZipFile(directory, temporaryOutputFile); await moveIntoPlace(temporaryOutputFile, outputFile); } function writeZipFile(directory: string, outputFile: string): Promise<void> { return new Promise(async (ok, fail) => { // The below options are needed to support following symlinks when building zip files: // - nodir: This will prevent symlinks themselves from being copied into the zip. // - follow: This will follow symlinks and copy the files within. const globOptions = { dot: true, nodir: true, follow: true, cwd: directory, }; const files = glob.sync('**', globOptions); // The output here is already sorted const output = createWriteStream(outputFile); const archive = archiver('zip'); archive.on('warning', fail); archive.on('error', fail); // archive has been finalized and the output file descriptor has closed, resolve promise // this has to be done before calling `finalize` since the events may fire immediately after. // see https://www.npmjs.com/package/archiver output.once('close', ok); archive.pipe(output); // Append files serially to ensure file order for (const file of files) { const fullPath = path.resolve(directory, file); // Exactly 2 promises // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism const [data, stat] = await Promise.all([fs.readFile(fullPath), fs.stat(fullPath)]); archive.append(data, { name: file, mode: stat.mode, }); } await archive.finalize(); }); } /** * Rename the file to the target location, taking into account: * * - That we may see EPERM on Windows while an Antivirus scanner still has the * file open, so retry a couple of times. * - This same function may be called in parallel and be interrupted at any point. */ async function moveIntoPlace(source: string, target: string) { let delay = 100; let attempts = 5; while (true) { try { // 'rename' is guaranteed to overwrite an existing target, as long as it is a file (not a directory) await fs.rename(source, target); return; } catch (e: any) { if (e.code !== 'EPERM' || attempts-- <= 0) { throw e; } error(formatErrorMessage(e)); await sleep(Math.floor(Math.random() * delay)); delay *= 2; } } } function sleep(ms: number) { return new Promise(ok => setTimeout(ok, ms)); } function randomString() { return Math.random().toString(36).replace(/[^a-z0-9]+/g, ''); }