106 lines
3.8 KiB
TypeScript
106 lines
3.8 KiB
TypeScript
import path from "path";
|
|
import { fileURLToPath, URL } from "url";
|
|
import fs from "fs/promises";
|
|
import axios, { AxiosError } from "axios";
|
|
import { createWriteStream } from "fs";
|
|
import ivm from "isolated-vm";
|
|
import { CommonImages, Config, HttpRespondError, PartsId2Index } from "./types.js";
|
|
|
|
export const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
export const fsExists = async (path: string) => (await fs.stat(path).catch((e) => false)) !== false;
|
|
|
|
export async function downloadToPath(url: string, path: string) {
|
|
const stream = createWriteStream(path);
|
|
const response = await axios.get(url, { responseType: "stream" });
|
|
response.data.pipe(stream);
|
|
await new Promise<void>(r => stream.once('finish', () => { stream.close(); r(); }));
|
|
}
|
|
|
|
/**
|
|
* Downloads the maker's HTML file and extracts the script from it,
|
|
* unless it is already cached.
|
|
*/
|
|
export async function fetchScript(makerId: string) {
|
|
const cacheDir = path.join(dirname, "..", "cache", "picrew", makerId);
|
|
const htmlPath = path.join(cacheDir, "index.html");
|
|
const scriptPath = path.join(cacheDir, "script.js");
|
|
|
|
await fs.mkdir(cacheDir, { recursive: true })
|
|
.catch((e) => { if (e?.code != "EEXIST") throw e; }); // Ignore "already exists" error
|
|
|
|
const htmlExists = await fsExists(htmlPath);
|
|
const scriptExists = await fsExists(scriptPath);
|
|
|
|
if (!htmlExists) {
|
|
try {
|
|
await downloadToPath(`https://picrew.me/en/image_maker/${encodeURIComponent(makerId)}`, htmlPath);
|
|
} catch(e) {
|
|
await fs.unlink(htmlPath).catch(() => {});
|
|
if (e instanceof AxiosError && e.status == 404) {
|
|
throw new HttpRespondError(`There is no Picrew maker with ID ${makerId}.`, 400);
|
|
}
|
|
else throw e;
|
|
}
|
|
}
|
|
|
|
if (!scriptExists) {
|
|
const text = await fs.readFile(htmlPath, { encoding: "utf8" });
|
|
|
|
const START_MATCHER = "<script>window.__NUXT__=",
|
|
END_MATCHER = "));</script>";
|
|
|
|
const startIndex = text.indexOf(START_MATCHER),
|
|
endIndex = text.indexOf(END_MATCHER);
|
|
|
|
const script = text.substring(startIndex, endIndex + END_MATCHER.length)
|
|
.replace(/^\<script\>/, "")
|
|
.replace(/\<\/script\>$/, "")
|
|
.replace("window.__NUXT__=", "");
|
|
|
|
await fs.writeFile(scriptPath, script);
|
|
}
|
|
}
|
|
|
|
export async function extractDataFromScript(makerId: string) {
|
|
const code = await fs.readFile(path.join(dirname, "..", "cache", "picrew", makerId, "script.js"), { encoding: "utf8" });
|
|
const isolate = new ivm.Isolate();
|
|
const context = await isolate.createContext();
|
|
const script = await isolate.compileScript(code);
|
|
const res = await script.run(context, { copy: true });
|
|
return {
|
|
config: res.state.config as Config,
|
|
commonImages: res.state.commonImages as CommonImages,
|
|
cdnRoot: res.state.picrewData.cdnRoot,
|
|
partsId2Index: res.state.partsId2Index as PartsId2Index,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Download an asset if it's not cached already, then
|
|
* return its file path.
|
|
*/
|
|
export async function getAsset(url: string) {
|
|
// https://cdn.picrew.me/app/image_maker/{makerId}/{pId}/{random_string}.png
|
|
const [ makerId, cId, filename ] = new URL(url).pathname.split("/").slice(-3);
|
|
|
|
//console.log(`Starting download: ${makerId}/${cId}/${filename}`);
|
|
|
|
const dir = path.join(dirname, "..", "cache", "assets", makerId, cId);
|
|
const filePath = path.join(dir, filename);
|
|
|
|
await fs.mkdir(dir, { recursive: true })
|
|
.catch((e) => { if (e?.code != "EEXIST") throw e; }); // Ignore "already exists" error
|
|
|
|
if (!await fsExists(filePath)) {
|
|
await downloadToPath(url, filePath);
|
|
}
|
|
|
|
//console.log(`Finished download: ${makerId}/${cId}/${filename}`);
|
|
return filePath;
|
|
}
|
|
|
|
export async function isValidSha256(hash: string) {
|
|
return /^[0-9A-f]{64}$/.test(hash);
|
|
}
|