import { SockpuppetPlus } from "@cgg/sockpuppet"; import { serveDir, serveFile } from "@std/http/file-server"; import { BearMetalStore } from "@bearmetal/store"; import { ensureDir, ensureFile, exists } from "@std/fs"; import { Router } from "./router.ts"; import { getPackVersion } from "./util/packVersion.ts"; import { createTagRoutes } from "./tags/routes.ts"; import { createResourcesRoutes } from "./resources/routes.ts"; import { readDirDirs } from "./util/readDir.ts"; import { unzipResources } from "./resources/unzip.ts"; const installPath = Deno.env.get("BMP_INSTALL_DIR") || "./"; const sockpuppet = new SockpuppetPlus(); sockpuppet.addHandler((req: Request) => { const url = new URL(req.url); if (!url.pathname.startsWith("/images")) return; return serveFile(req, url.searchParams.get("location") as string); }); // sockpuppet.addHandler((req: Request) => { // const method = req.method; // if (method === "OPTIONS") { // return new Response(null, { // status: 200, // headers: { // "Access-Control-Allow-Origin": "*", // "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", // }, // }); // } // }); const router = new Router(); router.route("/api/dir") .get(async () => { using store = new BearMetalStore(); const mcPath = store.get("mcPath") as string; if (mcPath) { const worlds: { world: string; icon: string; path: string; }[] = []; try { for await (const file of Deno.readDir(mcPath)) { if (file.isDirectory && file.name.startsWith("saves")) { for await ( const world of Deno.readDir(mcPath + "/" + file.name) ) { if (world.isDirectory) { for await ( const f of Deno.readDir( mcPath + "/" + file.name + "/" + world.name, ) ) { if (f.name.endsWith(".dat")) { worlds.push({ world: world.name, icon: Deno.realPathSync( mcPath + "/" + file.name + "/" + world.name + "/icon.png", ), path: Deno.realPathSync( mcPath + "/" + file.name + "/" + world.name, ), }); } } } } } } } catch (e: any) { store.set("mcPath", ""); return new Response(e, { status: 500 }); } return new Response(JSON.stringify({ worlds, mcPath }), { status: 200, }); } return new Response(JSON.stringify({ mcPath }), { status: mcPath ? 200 : 500, }); }) .post(async (req) => { using store = new BearMetalStore(); const formData = await req.formData(); const dir = formData.get("mcPath") as string; if (!dir) return new Response(null, { status: 400 }); store.set("mcPath", dir); if (!store.get("mcPath")) return new Response(null, { status: 500 }); return new Response(null, { status: 200 }); }); router.route("/api/world") .post(async (req) => { using store = new BearMetalStore(); const worldPath = await req.text(); if (!worldPath) return new Response(null, { status: 400 }); const mcPath = store.get("mcPath") as string; if (!mcPath) { return new Response("Tried to set world, but MC path is not set.", { status: 500, }); } const realWorldPath = Deno.realPathSync(worldPath); store.set("world", realWorldPath); return new Response(null, { status: 200 }); }) .get((req) => { using store = new BearMetalStore(); const worldPath = store.get("world") as string; if (!worldPath) return new Response(null, { status: 400 }); return new Response(worldPath.split("/").pop() as string, { status: 200 }); }); router.route("/api/pack") .get(() => { using store = new BearMetalStore(); return new Response( JSON.stringify({ packName: store.get("packname") as string }), { status: 200 }, ); }) .post(async (req) => { using store = new BearMetalStore(); const formData = await req.formData(); const packName = formData.get("packName") as string; if (!packName) return new Response(null, { status: 400 }); createPack(store, packName); store.set("packname", packName); return new Response(JSON.stringify({ packName }), { status: 200 }); }); router.route("/api/pack/namespaces") .get(async () => { using store = new BearMetalStore(); const namespaces = Array.from(Deno.readDirSync(store.get("packlocation"))) .filter((dir) => dir.isDirectory).map((dir) => dir.name); return new Response(JSON.stringify(namespaces), { status: 200 }); }) .post(async (req) => { using store = new BearMetalStore(); const namespace = await req.text(); if (!namespace) { return new Response("Namespace is required", { status: 400 }); } const namespaceRx = /^[a-zA-Z0-9_\-]+$/; if (!namespaceRx.test(namespace)) { return new Response( "Namespace must only contain letters, numbers, underscores, and dashes", { status: 400 }, ); } await ensureDir(store.get("packlocation") + "/" + namespace); return new Response(null, { status: 200 }); }); router.route("/api/packs") .get(async () => { using store = new BearMetalStore(); const packs: { name: string; path: string }[] = []; const world = store.get("world") as string; for await (const pack of Deno.readDir(world + "/datapacks")) { if ( pack.isDirectory && await exists(world + "/datapacks/" + pack.name + "/bmp_dev") ) { packs.push({ name: pack.name, path: Deno.realPathSync(world + "/datapacks/" + pack.name), }); } } return new Response("No BMP packs found", { status: 400 }); }); router.route("/api/pack/version") .get(() => { using store = new BearMetalStore(); const version = getPackVersion(store); return new Response(version.toString(), { status: 200, }); }) .post(async (req) => { using store = new BearMetalStore(); const version = await req.text(); if (!version) return new Response(null, { status: 400 }); try { store.set("version", version); const packMeta = Deno.readTextFileSync( store.get("packlocation") + "/pack.mcmeta", ); const packMetaJson = JSON.parse(packMeta); packMetaJson.pack.pack_format = parseInt(version); await Deno.writeTextFile( store.get("packlocation") + "/pack.mcmeta", JSON.stringify(packMetaJson), ); const packVersionSchema = Deno.readTextFileSync( installPath + "pack_versions/" + version + ".json", ); const packVersionSchemaJson = JSON.parse(packVersionSchema); packVersionSchemaJson.mcVersion = store.get("mcVersion"); const versionResourceDir = await readDirDirs(installPath + "resources/"); if (!versionResourceDir.includes(version)) { unzipResources(); } } catch (e: any) { return new Response(e, { status: 500 }); } return new Response(null, { status: 200 }); }); router.route("/api/versions") .get(() => { const versions = Array.from(Deno.readDirSync(installPath + "pack_versions")) .filter((v) => v.isFile).map((version) => version.name.replace(".json", "") ); return new Response(JSON.stringify(versions), { status: 200 }); }); createTagRoutes(router); createResourcesRoutes(router); router.route("/api/stream/test") .get(() => { const stream = new ReadableStream({ async start(controller) { const enc = new TextEncoder(); controller.enqueue(enc.encode("Hello")); controller.enqueue(enc.encode(", ")); controller.enqueue(enc.encode("World!")); controller.close(); }, }); return new Response(stream, { headers: { "content-type": "text/plain", "x-content-type-options": "nosniff", }, }); }) sockpuppet.addHandler((req: Request) => { if (new URL(req.url).pathname.startsWith("/api")) return; return serveDir(req, { fsRoot: installPath + "dist", }); }); sockpuppet.addHandler(router.handle); async function createPack(store: BearMetalStore, packName: string) { const realWorldPath = store.get("world"); store.set("packlocation", realWorldPath + "/datapacks/" + packName); await ensureDir(store.get("packlocation")); await ensureFile(store.get("packlocation") + "/bmp_dev"); await ensureFile(store.get("packlocation") + "/pack.mcmeta"); if (!Deno.readTextFileSync(store.get("packlocation") + "/pack.mcmeta")) { await Deno.writeTextFile( store.get("packlocation") + "/pack.mcmeta", `{"pack":{"pack_format":48,"description":"${packName}"}}`, ); } }