feat(admin): moved server build/update to full app
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m27s
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m27s
This commit is contained in:
177
backend/utils/zipper.utils.ts
Normal file
177
backend/utils/zipper.utils.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
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<void>((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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user