UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

220 lines 7.96 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RemoteFolderArchive = void 0; const fs = require("fs"); const path = require("path"); const zlib = require("zlib"); const tar = require("tar-stream"); const tmp = require("tmp"); const stream_1 = require("stream"); const errors_1 = require("../errors"); // ANCHOR - RemoteFolderArchive /** * A folder packed as a gzipped tarball on disk, for seed uploads. Owned by * RemoteFolderSync: created for one upload and disposed in the same breath. The * tarball carries the manifest's files with their modes, the empty directories, * and the symlinks as real link entries. */ class RemoteFolderArchive { constructor(filePath, bytes) { this.bytes = bytes; this.filePath = filePath; } // Public Static Methods // /** * Packs a folder index into a gzipped tarball on disk. * * @param {FolderIndex} index - The scanned folder. * @param {ArchiveTmpFile} tmpFile - (Optional) Returns the path the archive is written to. * @returns {Promise<RemoteFolderArchive>} The packed archive handle. */ static async create(index, tmpFile = defaultTmpFile) { const tarPath = tmpFile(); RemoteFolderArchive.inFlight.add(tarPath); try { await packInto(tarPath, index); const archive = new RemoteFolderArchive(tarPath, fs.statSync(tarPath).size); RemoteFolderArchive.active.add(archive); return archive; } catch (e) { fs.rmSync(tarPath, { force: true }); const message = e instanceof Error ? e.message : String(e); throw new errors_1.RemoteBuildError('context', `could not package the folder (${message})`, 'Re-run the build.'); } finally { RemoteFolderArchive.inFlight.delete(tarPath); } } /** * Disposes every archive on disk and removes any tarball still being packed. * Called from the interrupt handler. * * @returns {void} */ static disposeAll() { for (const archive of [...RemoteFolderArchive.active]) { archive.dispose(); } for (const tarPath of [...RemoteFolderArchive.inFlight]) { RemoteFolderArchive.inFlight.delete(tarPath); removeQuietly(tarPath); } } // Public Methods // /** * Removes the tarball from disk. Safe to call more than once. * * @returns {void} */ dispose() { RemoteFolderArchive.active.delete(this); removeQuietly(this.filePath); } } exports.RemoteFolderArchive = RemoteFolderArchive; // Private Static Properties // RemoteFolderArchive.active = new Set(); // Archives on disk not yet disposed, so an interrupt handler can clean them up. RemoteFolderArchive.inFlight = new Set(); // Temp paths not yet owned by a finished archive, so an interrupt cleans partials too. // SECTION - Functions /** * Removes a temp archive, tolerating a file another handle still holds. Disposal runs * from the interrupt handler, where a throw would replace the exit with a crash. * * @param {string} filePath - The archive path to remove. * @returns {void} */ function removeQuietly(filePath) { try { fs.rmSync(filePath, { force: true }); } catch (_a) { // A locked tarball outlives the process; the OS reclaims the temp directory. } } /** * Packs the index into a gzipped tarball at tarPath. Any stream failure — a write * error, a gzip error, or a per-file size mismatch (a file changing while it is * read) — rejects the promise instead of surfacing as an unhandled 'error' event * that would crash the process. * * @param {string} tarPath - The tarball's destination path. * @param {FolderIndex} index - The scanned folder. * @returns {Promise<void>} */ function packInto(tarPath, index) { return new Promise((resolve, reject) => { const pack = tar.pack(); const state = { settled: false }; pack.on('error', (e) => failPack(state, pack, reject, e)); (0, stream_1.pipeline)(pack, zlib.createGzip(), fs.createWriteStream(tarPath), (err) => finishPack(state, pack, resolve, reject, err)); writeEntries(pack, index).catch((e) => failPack(state, pack, reject, e)); }); } /** * Writes every context entry into the pack and finalizes it. * * @param {tar.Pack} pack - The tar pack to write into. * @param {FolderIndex} index - The scanned folder. * @returns {Promise<void>} */ async function writeEntries(pack, index) { for (const dirRel of index.emptyDirs) { await addEntry(pack, { name: dirRel + '/', type: 'directory', mode: 0o755 }); } for (const [rel, meta] of Object.entries(index.manifest)) { await addFile(pack, path.join(index.root, ...rel.split('/')), rel, parseInt(meta.mode, 8)); } for (const link of index.symlinks) { await addEntry(pack, { name: link.path, type: 'symlink', linkname: link.target }); } pack.finalize(); } /** * Settles the pack promise as failed once, tearing down the pack. * * @param {PackState} state - The pack's settle guard. * @param {tar.Pack} pack - The tar pack to destroy. * @param {(reason: unknown) => void} reject - The promise's reject. * @param {unknown} error - The failure. * @returns {void} */ function failPack(state, pack, reject, error) { if (state.settled) { return; } state.settled = true; pack.destroy(); reject(error); } /** * Settles the pack promise when the output stream finishes, or fails it on error. * * @param {PackState} state - The pack's settle guard. * @param {tar.Pack} pack - The tar pack to destroy on error. * @param {() => void} resolve - The promise's resolve. * @param {(reason: unknown) => void} reject - The promise's reject. * @param {NodeJS.ErrnoException | null} err - The pipeline error, if any. * @returns {void} */ function finishPack(state, pack, resolve, reject, err) { if (err) { failPack(state, pack, reject, err); return; } if (!state.settled) { state.settled = true; resolve(); } } /** * Creates an empty temp file for the archive and returns its path. * * @returns {string} The temp file path. */ function defaultTmpFile() { // Without this, an exit that bypasses dispose leaves the packed source tree behind. tmp.setGracefulCleanup(); const tmpFile = tmp.fileSync({ postfix: '.tar.gz' }); fs.closeSync(tmpFile.fd); return tmpFile.name; } /** * Writes one bodyless entry (a directory or a symlink) into the tar pack. * * @param {tar.Pack} pack - The tar pack to write into. * @param {tar.Headers} headers - The entry headers. * @returns {Promise<void>} */ function addEntry(pack, headers) { return new Promise((resolve, reject) => { const entry = pack.entry(headers, (err) => (err ? reject(err) : resolve())); entry.on('error', reject); }); } /** * Streams one file into the tar pack under its POSIX-relative name. The entry sink * is error-listened: a size mismatch (the file changing while it is read) rejects * here instead of crashing the process. * * @param {tar.Pack} pack - The tar pack to write into. * @param {string} filePath - The absolute file path. * @param {string} name - The path relative to the folder root. * @param {number} mode - The file mode. * @returns {Promise<void>} */ function addFile(pack, filePath, name, mode) { const size = fs.statSync(filePath).size; return new Promise((resolve, reject) => { const entry = pack.entry({ name, size, mode }, (err) => (err ? reject(err) : resolve())); entry.on('error', reject); const source = fs.createReadStream(filePath); source.on('error', (e) => { entry.destroy(); reject(e); }); source.pipe(entry); }); } // !SECTION //# sourceMappingURL=archive.js.map