import * as path from "node:path"; import * as Utils from "./utils"; import * as Messages from "./messages"; import { promises as fs } from "node:fs"; import type { Buffer } from "node:buffer"; /** * Reads the model folder contents * * @param model * @returns A promise of an object containing all * filePaths and the relative buffer */ export default async function getModelFolderContents( model: string, ): Promise<{ [filePath: string]: Buffer }> { try { const modelPath = `${model}${(!path.extname(model) && ".pass") || ""}`; const modelFilesList = await fs.readdir(modelPath); // No dot-starting files, manifest and signature and only files with an extension const modelSuitableRootPaths = Utils.removeHidden( modelFilesList, ).filter( (f) => !/(manifest|signature)/i.test(f) && /.+$/.test(path.parse(f).ext), ); const modelRecords = await Promise.all( modelSuitableRootPaths.map((fileOrDirectoryPath) => readFileOrDirectory( path.resolve(modelPath, fileOrDirectoryPath), ), ), ); return Object.fromEntries(modelRecords.flat(1)); } catch (err) { if (!isErrorErrNoException(err) || !isMissingFileError(err)) { throw err; } if (isFileReadingFailure(err)) { throw new Error( Messages.format( Messages.MODELS.FILE_NO_OPEN, JSON.stringify(err), ), ); } if (isDirectoryReadingFailure(err)) { throw new Error( Messages.format(Messages.MODELS.DIR_NOT_FOUND, err.path), ); } throw err; } } function isErrorErrNoException(err: unknown): err is NodeJS.ErrnoException { return Object.prototype.hasOwnProperty.call(err, "errno"); } function isMissingFileError( err: unknown, ): err is NodeJS.ErrnoException & { code: "ENOENT" } { return (err as NodeJS.ErrnoException).code === "ENOENT"; } function isDirectoryReadingFailure( err: NodeJS.ErrnoException, ): err is NodeJS.ErrnoException & { syscall: "scandir" } { return err.syscall === "scandir"; } function isFileReadingFailure( err: NodeJS.ErrnoException, ): err is NodeJS.ErrnoException & { syscall: "open" } { return err.syscall === "open"; } /** * Allows reading both a whole directory or a set of * file in the same flow * * @param filePath * @returns */ async function readFileOrDirectory( filePath: string, ): Promise<[key: string, content: Buffer][]> { const stats = await fs.lstat(filePath); if (stats.isDirectory()) { return readFilesInDirectory(filePath); } else { return getFileContents(filePath).then((result) => [result]); } } /** * Reads a directory and returns all * the files in it * * @param filePath * @returns */ async function readFilesInDirectory( filePath: string, ): Promise>[]> { const dirContent = await fs.readdir(filePath).then(Utils.removeHidden); return Promise.all( dirContent.map((fileName) => getFileContents(path.resolve(filePath, fileName), 2), ), ); } /** * @param filePath * @param pathSlicesDepthFromEnd used to preserve localization lproj content * @returns */ async function getFileContents( filePath: string, pathSlicesDepthFromEnd: number = 1, ): Promise<[key: string, content: Buffer]> { const fileComponents = filePath.split(path.sep); const fileName = fileComponents .slice(fileComponents.length - pathSlicesDepthFromEnd) .join("/"); const content = await fs.readFile(filePath); return [fileName, content]; }