@controlplane/cli
Version:
Control Plane Corporation CLI
220 lines • 7.96 kB
JavaScript
;
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