import fs from "node:fs"; import fsp from "node:fs/promises"; import path from "node:path"; import archiver from "archiver"; import { createLogger } from "../logger/logger.controller.js"; import { emitBuildLog } from "./build.utils.js"; import { updateAppStats } from "./updateAppStats.utils.js"; const log = createLogger({ module: "utils", subModule: "zip" }); const exists = async (target: string) => { try { await fsp.access(target); return true; } catch { return false; } }; const getNextBuildNumber = async (buildNumberFile: string) => { if (!(await exists(buildNumberFile))) { await fsp.writeFile(buildNumberFile, "1", "utf8"); return 1; } const raw = await fsp.readFile(buildNumberFile, "utf8"); const current = Number.parseInt(raw.trim(), 10); if (Number.isNaN(current) || current < 1) { await fsp.writeFile(buildNumberFile, "1", "utf8"); return 1; } const next = current + 1; await fsp.writeFile(buildNumberFile, String(next), "utf8"); // update the server with the next build number await updateAppStats({ currentBuild: next, lastBuildAt: new Date(), building: true, }); return next; }; const cleanupOldBuilds = async (buildFolder: string, maxBuilds: number) => { const entries = await fsp.readdir(buildFolder, { withFileTypes: true }); const zipFiles: { fullPath: string; name: string; mtimeMs: number }[] = []; for (const entry of entries) { if (!entry.isFile()) continue; if (!/^LSTV3-\d+\.zip$/i.test(entry.name)) continue; const fullPath = path.join(buildFolder, entry.name); const stat = await fsp.stat(fullPath); zipFiles.push({ fullPath, name: entry.name, mtimeMs: stat.mtimeMs, }); } zipFiles.sort((a, b) => b.mtimeMs - a.mtimeMs); const toRemove = zipFiles.slice(maxBuilds); for (const file of toRemove) { await fsp.rm(file.fullPath, { force: true }); emitBuildLog(`Removed old build: ${file.name}`); } }; export const zipBuild = async () => { const appDir = process.env.DEV_DIR ?? ""; const maxBuilds = Number(process.env.MAX_BUILDS ?? 5); if (!appDir) { log.error({ notify: true }, "Forgot to add in the dev dir into the env"); return; } const includesFile = path.join(appDir, ".includes"); const buildNumberFile = path.join(appDir, ".buildNumber"); const buildFolder = path.join(appDir, "builds"); const tempFolder = path.join(appDir, "temp", "zip-temp"); if (!(await exists(includesFile))) { log.error({ notify: true }, "Missing .includes file common"); return; } await fsp.mkdir(buildFolder, { recursive: true }); const buildNumber = await getNextBuildNumber(buildNumberFile); const zipFileName = `LSTV3-${buildNumber}.zip`; const zipFile = path.join(buildFolder, zipFileName); // make the folders in case they are not created already emitBuildLog(`Using build number: ${buildNumber}`); if (await exists(tempFolder)) { await fsp.rm(tempFolder, { recursive: true, force: true }); } await fsp.mkdir(tempFolder, { recursive: true }); const includes = (await fsp.readFile(includesFile, "utf8")) .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); emitBuildLog(`Preparing zip from ${includes.length} include entries`); for (const relPath of includes) { const source = path.join(appDir, relPath); const dest = path.join(tempFolder, relPath); if (!(await exists(source))) { emitBuildLog(`Skipping missing path: ${relPath}`, "error"); continue; } const stat = await fsp.stat(source); await fsp.mkdir(path.dirname(dest), { recursive: true }); if (stat.isDirectory()) { emitBuildLog(`Copying folder: ${relPath}`); await fsp.cp(source, dest, { recursive: true }); } else { emitBuildLog(`Copying file: ${relPath}`); await fsp.copyFile(source, dest); } } // if something crazy happens and we get the same build lets just reuse it // if (await exists(zipFile)) { // await fsp.rm(zipFile, { force: true }); // } emitBuildLog(`Creating zip: ${zipFile}`); await new Promise((resolve, reject) => { const output = fs.createWriteStream(zipFile); const archive = archiver("zip", { zlib: { level: 9 } }); output.on("close", () => resolve()); output.on("error", reject); archive.on("error", reject); archive.pipe(output); // zip contents of temp folder, not temp folder itself archive.directory(tempFolder, false); archive.finalize(); }); await fsp.rm(tempFolder, { recursive: true, force: true }); emitBuildLog(`Zip completed successfully: ${zipFile}`); await cleanupOldBuilds(buildFolder, maxBuilds); await updateAppStats({ lastUpdated: new Date(), building: false, }); return { success: true, buildNumber, zipFile, zipFileName, }; };