Files
passkit-generator/src/getModelFolderContents.ts
2024-10-12 00:23:07 +02:00

145 lines
3.3 KiB
TypeScript

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<Awaited<ReturnType<typeof getFileContents>>[]> {
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];
}