From d121d60b76994bc65ae92e7b1c77863d72c0493e Mon Sep 17 00:00:00 2001 From: Gabriel Simmer Date: Wed, 7 Aug 2024 11:10:04 +0100 Subject: [PATCH] Port to itty-router --- worker/package-lock.json | 9 ++ worker/package.json | 9 +- worker/src/index.js | 287 ++++++++++++++++++++------------------- 3 files changed, 165 insertions(+), 140 deletions(-) diff --git a/worker/package-lock.json b/worker/package-lock.json index 6050554..5d04ea4 100644 --- a/worker/package-lock.json +++ b/worker/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "worker", "version": "0.0.0", + "dependencies": { + "itty-router": "^5.0.17" + }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.4.5", "vitest": "1.5.0", @@ -1574,6 +1577,12 @@ "dev": true, "license": "ISC" }, + "node_modules/itty-router": { + "version": "5.0.17", + "resolved": "https://registry.npmjs.org/itty-router/-/itty-router-5.0.17.tgz", + "integrity": "sha512-ZHnPI0OOyTTLuNp2FdciejYaK4Wl3ZV3O0yEm8njOGggh/k/ek3BL7X2I5YsCOfc5vLhIJgj3Z4pUtLs6k9Ucg==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz", diff --git a/worker/package.json b/worker/package.json index 5834975..fbfa904 100644 --- a/worker/package.json +++ b/worker/package.json @@ -10,7 +10,10 @@ }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.4.5", - "wrangler": "^3.60.3", - "vitest": "1.5.0" + "vitest": "1.5.0", + "wrangler": "^3.60.3" + }, + "dependencies": { + "itty-router": "^5.0.17" } -} \ No newline at end of file +} diff --git a/worker/src/index.js b/worker/src/index.js index b0a74e5..d18fa7d 100644 --- a/worker/src/index.js +++ b/worker/src/index.js @@ -1,140 +1,153 @@ import index from "./index.html"; +import { IttyRouter, json, error, withParams } from 'itty-router' + +const router = IttyRouter(); +const cache = caches.default; + +async function withCache(request, env) { + const url = new URL(request.url); + const { pathname } = url; + const cacheUrl = new URL(request.url); + + // Construct the cache key from the cache URL + const cacheKey = new Request(cacheUrl.toString(), request); + let response = await cache.match(cacheKey); + + if (response) { + return response; + } + + request.cacheKey = cacheKey; +} + +async function snapshot(request, env, ctx, date = "LATEST") { + const kv = env.KV_STORE; + const bucket = env.R2_BUCKET; + + const { value, metadata } = await kv.getWithMetadata(date); + if (value == null) { + return {error: `${date} key not found`}; + } + const object = await bucket.get(`${value}.png`); + if (object === null) { + return {error: `${date} object not found`}; + } + + if (metadata != null) { + date = metadata.date; + } + + return { + hash: value, + file: `https://daily-servo-r2.gmem.ca/${value}.png`, + date: date, + } +} + +async function renderIndex(request, env, ctx) { + let l = await snapshot(request, env, ctx); + const response = new HTMLRewriter() + .on("#date", { + element(element) { + element.setInnerContent(l.date); + }, + }) + .on("#img", { + element(element) { + element.setAttribute("src", l.file); + }, + }) + .on("#img-link", { + element(element) { + element.setAttribute("href", l.file); + }, + }) + .transform( + new Response(index, { headers: { "Content-Type": "text/html", + "Cache-Control": "s-maxage=3600" } }), + ); + ctx.waitUntil(cache.put(request.cacheKey, response.clone())); + return response +} + +async function latest(request, env) { + let l = await snapshot(); + const response = new Response(JSON.stringify(l), { + headers: { "Content-Type": "application/json", + "Cache-Control": "s-maxage=3600"} + }); + ctx.waitUntil(cache.put(request.cacheKey, response.clone())); + return response; +} + +async function snapshotList(request, env, ctx) { + const kv = env.KV_STORE; + + let list = await kv.list(); + let filtered = await Promise.all( + list.keys.filter((value) => value.name != "LATEST") + .map(async (value) => { const v = await kv.get(value.name); return { date: value.name, hash: v } } )); + const response = new Response(JSON.stringify({ + latest: await kv.get("LATEST"), + list: filtered, + }), { + headers: { "Content-Type": "application/json", + "Cache-Control": "s-maxage=3600"} + }); + ctx.waitUntil(cache.put(request.cacheKey, response.clone())); + return response; +} + +async function withAuth(request, env, ctx) { + if (request.headers.get("Authorization") != env.API_TOKEN) { + return new Response("Unauthorized", { status: 403 }); + } +} + +async function newSnapshot(request, env, ctx) { + const body = await request.formData(); + const { + date, + hash, + file + } = Object.fromEntries(body) + + // Don't bother uploading to R2 if the hashes match. + let latest = kv.get("LATEST"); + if (latest != hash) { + await bucket.put(`${hash}.png`, file); + } + + await kv.put("LATEST", hash, { + metadata: { date: date } + }); + // Keep for 1 year. + await kv.put(`${date}`, hash, { expirationTtl: 31_536_000 }); + + return new Response("Uploaded", { status: 201}); +} + +async function specificSnapshot(request, env, ctx) { + const { pathname } = new URL(request.url); + const response = await snapshot(request, env, ctx, pathname.replace("/", "")) + if (response.error != undefined) { + return new Response(JSON.stringify(response), { status: 404, headers: {"Content-Type": "application/json"} }); + } + return response +} + +router + .get("/", withCache, renderIndex) + .get("/latest", withCache, latest) + .get("/latest.json", withCache, snapshot) + .get("/list.json", withCache, snapshotList) + .post("/new", withAuth, newSnapshot) + .get("*", withCache, specificSnapshot) export default { - async fetch(request, env, ctx) { - const url = new URL(request.url); - const { pathname } = url; - - const cacheUrl = new URL(request.url); - - // Construct the cache key from the cache URL - const cacheKey = new Request(cacheUrl.toString(), request); - const cache = caches.default; - let response = await cache.match(cacheKey); - if (response) { - return response; - } - - const kv = env.KV_STORE; - const bucket = env.R2_BUCKET; - - const snapshot = async(date = "LATEST") => { - const { value, metadata } = await kv.getWithMetadata(date); - if (value == null) { - return {error: `${date} key not found`}; - } - const object = await bucket.get(`${value}.png`); - if (object === null) { - return {error: `${date} object not found`}; - } - - if (metadata != null) { - date = metadata.date; - } - - return { - hash: value, - file: `https://daily-servo-r2.gmem.ca/${value}.png`, - date: date, - } - } - - switch (pathname) { - case "/": { - let l = await snapshot(); - const response = new HTMLRewriter() - .on("#date", { - element(element) { - element.setInnerContent(l.date); - }, - }) - .on("#img", { - element(element) { - element.setAttribute("src", l.file); - }, - }) - .on("#img-link", { - element(element) { - element.setAttribute("href", l.file); - }, - }) - .transform( - new Response(index, { headers: { "Content-Type": "text/html", - "Cache-Control": "s-maxage=3600" } }), - ); - ctx.waitUntil(cache.put(cacheKey, response.clone())); - return response - } - case "/latest": { - let l = await latest(); - if (l.error != undefined) { - return new Response(JSON.stringify(l), { - status: 500, - headers: { "Content-Type": "application/json" } - }); - } - return Response.redirect(`${l.file}`, 302); - } - case "/latest.json": { - let l = await snapshot(); - const response = new Response(JSON.stringify(l), { - headers: { "Content-Type": "application/json", - "Cache-Control": "s-maxage=3600"} - }); - ctx.waitUntil(cache.put(cacheKey, response.clone())); - return response; - } - case "/list.json": { - let list = await kv.list(); - let filtered = await Promise.all( - list.keys.filter((value) => value.name != "LATEST") - .map(async (value) => { const v = await kv.get(value.name); return { date: value.name, hash: v } } )); - const response = new Response(JSON.stringify({ - latest: await kv.get("LATEST"), - list: filtered, - }), { - headers: { "Content-Type": "application/json", - "Cache-Control": "s-maxage=3600"} - }); - ctx.waitUntil(cache.put(cacheKey, response.clone())); - return response; - } - case "/new": { - if (request.method == "POST" && request.headers.get("Authorization") == env.API_TOKEN) { - const body = await request.formData(); - const { - date, - hash, - file - } = Object.fromEntries(body) - - // Don't bother uploading to R2 if the hashes match. - let latest = kv.get("LATEST"); - if (latest != hash) { - await bucket.put(`${hash}.png`, file); - } - - await kv.put("LATEST", hash, { - metadata: { date: date } - }); - // Keep for 1 year. - await kv.put(`${date}`, hash, { expirationTtl: 31_536_000 }); - - return new Response("Uploaded", { status: 201 }); - } else { - return new Response("Unauthorized", { status: 403 }); - } - } - default: - let snap = await snapshot(pathname.replace("/", "")); - if (snap.error != undefined) { - return new Response("Not found", { status: 404 }); - } - const response = new Response(JSON.stringify(snap), { - headers: { "Content-Type": "application/json", "Cache-Control": "s-maxage=3600" } - }); - ctx.waitUntil(cache.put(cacheKey, response.clone())); - return response; - } - }, -}; + fetch: (request, ...args) => + router + .fetch(request, ...args) + .then(json) + .catch(error) +}