commit c1028ae6b10e5ca092d6ebe93810abc42c3fb434 Author: DrMint <29893320+DrMint@users.noreply.github.com> Date: Sat Sep 2 21:37:45 2023 +0200 Initial commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..256d70c --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +PURGE_TOKEN=my-super-token +TARGET_HOST=http://localhost:8080 # Do not include the trailing / +OUTGOING_PORT=8081 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c41af49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# environment variables +.env +.env.production + +cache \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ff6057 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# middleware.accords-library.com + +This middleware handles caching of the website. + +## Install + +``` +bun install +bun run dev +``` diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..5ab7c12 Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..c22816d --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "scripts": { + "start": "bun run --watch src/index.ts" + }, + "dependencies": { + "hono": "^3.5.4" + }, + "devDependencies": { + "bun-types": "^0.6.2" + } +} diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..e859beb --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,66 @@ +import { BunFile } from "bun"; +import fs from "fs/promises"; + +type SerializableResponse = { + body: string; + headers: Record; + status: number; + statusText: string; +}; + +export const purgeCache = async (request: Request): Promise => { + const url = new URL(request.url); + if (url.pathname.endsWith("/*")) { + const pathnameWithoutWildcard = url.pathname.slice(0, -2); + await fs.rm(getFolderPath(pathnameWithoutWildcard), { + recursive: true, + force: true, + }); + } else { + await fs.rm(getFilePath(url.pathname), { force: true }); + } +}; + +export const getResponse = async (request: Request): Promise => { + const url = new URL(request.url); + const file = Bun.file(getFilePath(url.pathname)); + let serializableResponse; + if (fileExists(file)) { + serializableResponse = await file.json(); + console.log("🟢 Retrieved response from cache for", request.url); + } else { + serializableResponse = await createCache(request); + console.log("🟠 Generated response for", request.url); + } + const { body, headers, status, statusText } = serializableResponse; + return new Response(body, { headers, status, statusText }); +}; + +const createCache = async (request: Request): Promise => { + const url = new URL(request.url); + const response = await fetch( + `${Bun.env.TARGET_HOST}${url.pathname}${url.search}` + ); + const serializableReponse: SerializableResponse = { + body: await response.text(), + headers: Object.fromEntries(response.headers.entries()), + status: response.status, + statusText: response.statusText, + }; + // Only save cache is status is ok + if (response.ok) { + await fs.mkdir(getFolderPath(url.pathname), { recursive: true }); + await Bun.write( + getFilePath(url.pathname), + JSON.stringify(serializableReponse) + ); + } + return serializableReponse; +}; + +const fileExists = (file: BunFile): boolean => file.size > 0; + +const getFolderPath = (pathname: string): string => `./cache${pathname}`; + +const getFilePath = (pathname: string): string => + `${getFolderPath(pathname)}/index.res`; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..337437d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,22 @@ +import { Hono } from "hono"; +import { getResponse, purgeCache } from "./cache"; + +const app = new Hono(); + +app.on("PURGE", "*", async ({ req }) => { + if (req.headers.get("Authorization") !== `Bearer ${Bun.env.PURGE_TOKEN}`) { + console.log("🛑 Purge request rejected for", req.url); + return new Response(null, { status: 403 }); + } + console.log("🔥 Purge request accepted for", req.url); + await purgeCache(req.raw); + return new Response(null, { status: 200 }); +}); + +app.get(async ({ req }) => await getResponse(req.raw)); + +const server = Bun.serve({ + port: Bun.env.OUTGOING_PORT, + fetch: app.fetch, +}); +console.log(`👂 Listening on http://${server.hostname}:${server.port}`); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b6e358a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "strict": true, + "types": ["bun-types"] + } +}