import { lstat, readdir, readFile, readlink } from "node:fs/promises"; import path from "node:path"; import { createGzip } from "node:zlib"; import { type Pack, pack } from "tar-stream"; const COMPUTE_MANIFEST_VERSION = "1" as const; /** * Create a tar.gz archive in memory from a build artifact directory. * * - Files are added under the `bundle/` prefix. * - Symlinks are preserved as symlink entries. * - A synthetic `compute.manifest.json` entry is injected at the tar root. * * Returns the gzipped archive as a Uint8Array. */ export async function createArchive( directory: string, entrypoint: string, signal?: AbortSignal, ): Promise { signal?.throwIfAborted(); const packer = pack(); const rootPath = path.resolve(directory); // Walk the directory and add all files await addDirectory(packer, rootPath, rootPath, "bundle", signal); // Inject the manifest as a synthetic entry const manifest = JSON.stringify( { manifestVersion: COMPUTE_MANIFEST_VERSION, entrypoint: `bundle/${entrypoint}`, }, null, 2, ); packer.entry({ name: "compute.manifest.json" }, manifest); // Signal end of archive packer.finalize(); // Pipe through gzip and collect into buffer return await collectGzipped(packer, signal); } async function addDirectory( packer: Pack, rootPath: string, fsPath: string, tarPrefix: string, signal?: AbortSignal, ): Promise { signal?.throwIfAborted(); const directoryPath = resolvePathWithinRoot(rootPath, fsPath); const entries = await readdir(directoryPath, { withFileTypes: true }); for (const entry of entries) { signal?.throwIfAborted(); const fullPath = path.join(directoryPath, entry.name); const tarPath = `${tarPrefix}/${entry.name}`; if (entry.isSymbolicLink()) { const linkStat = await lstat(fullPath); await addSymlink(packer, rootPath, fullPath, tarPath, linkStat, signal); } else if (entry.isDirectory()) { await addDirectory(packer, rootPath, fullPath, tarPath, signal); } else if (entry.isFile()) { const fileStat = await lstat(fullPath); await addFile(packer, rootPath, fullPath, tarPath, fileStat, signal); } } } async function addFile( packer: Pack, rootPath: string, fsPath: string, tarPath: string, fileStat: { size: number; mode: number; mtime: Date }, signal?: AbortSignal, ): Promise { signal?.throwIfAborted(); const filePath = resolvePathWithinRoot(rootPath, fsPath); const content = await readFile(filePath); packer.entry( { name: tarPath, size: content.length, mode: fileStat.mode, mtime: fileStat.mtime, }, content, ); } async function addSymlink( packer: Pack, rootPath: string, fsPath: string, tarPath: string, linkStat: { mode: number; mtime: Date }, signal?: AbortSignal, ): Promise { signal?.throwIfAborted(); const symlinkPath = resolvePathWithinRoot(rootPath, fsPath); const target = await readlink(symlinkPath); const linkname = await resolveArchiveSymlinkTarget( rootPath, symlinkPath, target, signal, ); packer.entry({ name: tarPath, type: "symlink", linkname, mode: linkStat.mode, mtime: linkStat.mtime, }); } async function resolveArchiveSymlinkTarget( rootPath: string, symlinkPath: string, target: string, signal?: AbortSignal, ): Promise { signal?.throwIfAborted(); const symlinkDir = path.dirname(symlinkPath); const resolvedTarget = resolvePathWithinRoot( rootPath, path.isAbsolute(target) ? target : path.resolve(symlinkDir, target), ); await lstat(resolvedTarget).catch((error) => { if (isENOENT(error)) { throw new Error( `Symlink target does not exist: ${symlinkPath} -> ${target} (resolved to ${resolvedTarget})`, ); } throw error; }); if (!path.isAbsolute(target)) { return target; } return path.relative(symlinkDir, resolvedTarget) || "."; } function isENOENT(error: unknown): boolean { return ( error instanceof Error && "code" in error && (error as { code?: string }).code === "ENOENT" ); } function resolvePathWithinRoot(rootPath: string, fsPath: string): string { const normalizedRootPath = path.resolve(rootPath); const resolvedPath = path.resolve(fsPath); const relativePath = path.relative(normalizedRootPath, resolvedPath); if ( relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath) ) { throw new Error(`Symlink target escapes archive root: ${resolvedPath}`); } return resolvedPath; } function collectGzipped( packer: Pack, signal?: AbortSignal, ): Promise { signal?.throwIfAborted(); const chunks: Buffer[] = []; const gzip = createGzip(); const onAbort = () => { // AbortSignal.abort() sets signal.reason; fallback keeps a stream error path if a non-standard signal omits it. const abortError = signal?.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError"); gzip.destroy(abortError); packer.destroy(abortError); }; signal?.addEventListener("abort", onAbort, { once: true }); const promise = new Promise((resolve, reject) => { packer.pipe(gzip); gzip.on("data", (chunk: Buffer) => { chunks.push(chunk); }); gzip.on("end", () => { resolve(Buffer.concat(chunks)); }); gzip.on("error", reject); packer.on("error", reject); }); return promise.finally(() => { signal?.removeEventListener("abort", onAbort); }); }