diff --git a/deno.json b/deno.json index d7a5bcd..f88f14b 100644 --- a/deno.json +++ b/deno.json @@ -12,7 +12,7 @@ }, "imports": { "@babel/plugin-transform-react-jsx-development": "npm:@babel/plugin-transform-react-jsx-development@^7.25.7", - "@bearmetal/store": "jsr:@bearmetal/store@^0.0.4", + "@bearmetal/store": "jsr:@bearmetal/store@^0.0.5", "@cgg/sockpuppet": "../sockpuppet.ts/server/mod.ts", "@cgg/sockpuppet/client": "../sockpuppet.ts/client/mod.ts", "@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.0", diff --git a/deno.lock b/deno.lock index 460a2cb..dc5d8a0 100644 --- a/deno.lock +++ b/deno.lock @@ -1,7 +1,7 @@ { "version": "4", "specifiers": { - "jsr:@bearmetal/store@^0.0.4": "0.0.4", + "jsr:@bearmetal/store@^0.0.5": "0.0.5", "jsr:@std/cli@^1.0.6": "1.0.6", "jsr:@std/encoding@^1.0.5": "1.0.5", "jsr:@std/fmt@^1.0.2": "1.0.2", @@ -28,8 +28,8 @@ "npm:vite@^5.4.8": "5.4.9" }, "jsr": { - "@bearmetal/store@0.0.4": { - "integrity": "f5859476184d6f7b3957d18c7c82a37b6b89bb75e18db3186fde94ccb4253dab", + "@bearmetal/store@0.0.5": { + "integrity": "d17da24c91bcc05707deb8a55017ebdf5d8eebd2f6293dcb2bbfac57e4e3b395", "dependencies": [ "jsr:@std/fs@^1.0.4" ] @@ -1391,7 +1391,7 @@ }, "workspace": { "dependencies": [ - "jsr:@bearmetal/store@^0.0.4", + "jsr:@bearmetal/store@^0.0.5", "jsr:@std/fs@^1.0.4", "jsr:@std/http@^1.0.8", "npm:@babel/plugin-transform-react-jsx-development@^7.25.7", diff --git a/server/main.ts b/server/main.ts index 5985de8..14f93b0 100644 --- a/server/main.ts +++ b/server/main.ts @@ -2,18 +2,107 @@ import { SockpuppetPlus } from "@cgg/sockpuppet"; import { serveDir, serveFile } from "@std/http/file-server"; import { BearMetalStore } from "@bearmetal/store"; import { ensureDir } from "@std/fs"; +import { Router } from "./router.ts"; const installPath = Deno.env.get("BMP_INSTALL_DIR") || "./"; const sockpuppet = new SockpuppetPlus(); -sockpuppet.addHandler(async (req: Request) => { - console.log(req.url); +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); }); +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); + store.set("packlocation", realWorldPath + "/datapacks/bmp_dev"); + await ensureDir(store.get("packlocation") as string); + 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 serveFile(req, worldPath); + }); + sockpuppet.addHandler((req: Request) => { if (new URL(req.url).pathname.startsWith("/api")) return; @@ -22,114 +111,4 @@ sockpuppet.addHandler((req: Request) => { }); }); -sockpuppet.addHandler(async (req: Request) => { - if (!new URL(req.url).pathname.startsWith("/api")) return; - const store = new BearMetalStore(); - - const API_DIR_ROUTE = new URLPattern({ - pathname: "/api/dir", - }); - - const match = API_DIR_ROUTE.exec(req.url); - if (!match) return; - - switch (req.method) { - case "GET": { - 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, - }); - } - case "POST": { - 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 }); - } - default: - return new Response(null, { status: 405 }); - } -}); - -sockpuppet.addHandler(async (req: Request) => { - if (!new URL(req.url).pathname.startsWith("/api")) return; - const store = new BearMetalStore(); - - const API_WORLD_ROUTE = new URLPattern({ - pathname: "/api/world", - }); - - const match = API_WORLD_ROUTE.exec(req.url); - if (!match) return; - - switch (req.method) { - case "GET": { - return new Response(store.get("world").split("/").pop() || "", { - status: 200, - }); - } - case "POST": { - 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); - store.set("packlocation", realWorldPath + "/datapacks/bmp_dev"); - await ensureDir(store.get("packlocation") as string); - return new Response(null, { status: 200 }); - } - } -}); +sockpuppet.addHandler(router.handle); diff --git a/server/router.ts b/server/router.ts new file mode 100644 index 0000000..06f2962 --- /dev/null +++ b/server/router.ts @@ -0,0 +1,68 @@ +export class Router { + private routes: Record = {}; + + public route(route: string) { + const methods: Record = { + get: () => undefined, + post: () => undefined, + put: () => undefined, + delete: () => undefined, + }; + this.routes[route] = this.routes[route] || []; + this.routes[route].push((r, c) => { + switch (r.method) { + case "GET": + return methods.get?.(r, c); + case "POST": + return methods.post?.(r, c); + case "PUT": + return methods.put?.(r, c); + case "DELETE": + return methods.delete?.(r, c); + default: + return undefined; + } + }); + return { + get(handler: Handler) { + methods.get = handler; + return this; + }, + post(handler: Handler) { + methods.post = handler; + return this; + }, + put(handler: Handler) { + methods.put = handler; + return this; + }, + delete(handler: Handler) { + methods.delete = handler; + return this; + }, + }; + } + + public handle = async (req: Request): Promise => { + const url = new URL(req.url); + const route = url.pathname; + if (route in this.routes) { + let res; + for (const handler of this.routes[route]) { + res = await handler(req, { + url, + state: {}, + }); + if (res) { + return res; + } + } + } + return new Response("Not found", { status: 404 }); + }; +} + +export type Handler = ( + req: Request, + ctx: Context, +) => Promise | Response | undefined; diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..c8eb33d --- /dev/null +++ b/types.ts @@ -0,0 +1,6 @@ +declare global { + interface Context { + url: URL; + state: Record; + } +}