import fs from "node:fs"; import path from "path"; import AdmZip from "adm-zip"; import { Ajv } from "ajv"; import addFormats from "ajv-formats"; import log from "electron-log/main"; import packageJson from "./../package.json"; const isDev = process.env.NODE_ENV === "development"; const root = path.join(__dirname, ".."); const sourcePublicPath = path.join(root, "public"); const runtimePublicPath = isDev ? sourcePublicPath : path.join(root, ".output/public"); /** * In charge to unzip data from showroom Bo, * validate data config and check version */ export default abstract class UnzipData { static zipFileToUse: string|null; private static resolvePublicPath(relativePath: string): string { const runtimePath = path.join(runtimePublicPath, relativePath) as fs.PathLike; if (fs.existsSync(runtimePath)) { return runtimePath as string; } return path.join(sourcePublicPath, relativePath); } /** * Init zipFileToUse if not already inited */ private static init() { if (!UnzipData.zipFileToUse) { // * Try to locate last zip file and load it. const zipFile = fs.readdirSync("static/data") .map((entryPath) => { const fullPath = path.join("static/data", entryPath); return { path: fullPath, stat: fs.lstatSync(fullPath) }; }) .filter((entry) => entry.stat.isFile() && entry.path.endsWith(".zip")) .sort((objA, objB) => objB.stat.mtime.getTime() - objA.stat.mtime.getTime()) .at(0) ?? null; if (zipFile) { UnzipData.zipFileToUse = zipFile.path; } } } /** * Decompress zip file content * And fetch data.json content */ public static loadZipFileContent(): void { UnzipData.init(); if (!UnzipData.zipFileToUse) { throw new Error("No json data to load"); } const zip = new AdmZip(fs.readFileSync(UnzipData.zipFileToUse)); UnzipData.removeOldExtractedFiles(); zip.extractAllTo("public/data"); } private static removeOldExtractedFiles() { // * Clear export dir fs.rmSync("public/data/pictures", { recursive: true, force: true }); fs.rmSync("public/data/data.json", { force: true }); } /** * Load json data file that was extracted */ public static loadJsonDataFile(): TypeApp | null { const jsonDataPath = UnzipData.resolvePublicPath("data/data.json"); const jsonSchemaPath = UnzipData.resolvePublicPath("schemas"); const jsonData = fs.readFileSync( jsonDataPath, { encoding: "utf8", flag: "r" } ); if (!UnzipData.validateAgainstJsonSchema(jsonSchemaPath, jsonData)) { log.warn(`Data for file ${UnzipData.zipFileToUse} is not valid JSON`); return null; } const appJsonConfig: TypeApp = JSON.parse(jsonData); const appVersion: string = packageJson.version; if (appJsonConfig.version !== appVersion) { throw new Error(`App version ${appVersion} is not compatible with config version ${appJsonConfig.version}`); } return appJsonConfig; } private static validateAgainstJsonSchema(jsonSchemaPath:string, jsonData: string): boolean { try { const schemaList = fs.readdirSync(jsonSchemaPath) .filter(schemaPath => schemaPath.endsWith(".schema.json")) .map(schemaPath => JSON.parse(fs.readFileSync(`${jsonSchemaPath}/${schemaPath}`, { encoding: "utf8", flag: "r" })) as Record); const appSchemaIndex = schemaList.findIndex((schema) => String(schema.$id).endsWith("app.schema.json")); if (appSchemaIndex === -1) { throw new Error("app.schema.json not found"); } const appSchema = schemaList.splice(appSchemaIndex, 1).at(0); if (!appSchema) { throw new Error("app.schema.json not found"); } const ajv = new Ajv(); addFormats(ajv); ajv.addMetaSchema(appSchema); ajv.addSchema(schemaList); return ajv.validate(appSchema, JSON.parse(jsonData)); } catch (err:unknown) { if (err instanceof Error) { log.error(err); return false; } throw new Error("validateAgainstJsonSchema unkown error"); } } }