netlify/functions/link.ts (69 lines of code) (raw):
import { gzipSync } from "node:zlib";
import type { Config, Context } from "@netlify/functions";
import { getStore } from "@netlify/blobs";
import type { LatestSavedSettings } from "app/settings.ts";
const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const LINK_CHARS = BASE62;
const MAX_SETTINGS_SIZE = 50_000;
function newId(count: number): string {
let s: string = "";
for (let i = 0; i < count; i++) {
s += LINK_CHARS[Math.floor(Math.random() * LINK_CHARS.length)];
}
return s;
}
export default async (req: Request, context: Context): Promise<Response> => {
const store = getStore("links");
if (req.method === "POST") {
let id: string | undefined = context.params["id"];
if (!id) {
do {
id = newId(6);
} while (await store.getMetadata(id));
} else {
const chars = new Set(LINK_CHARS);
if (!Iterator.from(id).every(c => c === "-" || chars.has(c))) {
return new Response(`Invalid id: '${id}'.`, { status: 400 });
}
const origId = id;
while (await store.getMetadata(id)) {
id = origId + "-" + newId(2);
}
}
// There's a small race here (TOCTOU) with checking id uniqueness, but
// this is a low traffic endpoint and mistakenly overwriting an id has
// low impact.
// Limit the maximum size to avoid abuse.
const requestText = await req.text();
if (requestText.length > MAX_SETTINGS_SIZE) {
return new Response("Settings too large", { status: 413 });
}
const settings = JSON.parse(requestText) as LatestSavedSettings;
const metadata = {
created: (new Date()).toISOString(),
// We don't actually expire anything yet, but if we need to in the
// future we can.
expires: settings.expires,
};
const gzipped = gzipSync(JSON.stringify(settings))
await store.set(id, new Blob([gzipped]), { metadata });
return Response.json({ id });
}
else if (req.method === "GET") {
if (!("id" in context.params)) {
return new Response("Missing id.", { status: 400 });
}
const { id } = context.params;
const result = await store.get(id, { type: "stream" });
if (!result) {
// FIXME: this should be a 404, however there seems to be an issue
// with netlify-cli dev (at least, not sure about production) which
// causes 404 to be retried as different paths. See
// https://github.com/netlify/cli/issues/1442.
return new Response("Link does not exist.", { status: 400 });
}
return new Response(result, {
headers: {
"Content-Encoding": "gzip",
"Content-Type": "application/json"
}
});
}
else {
return new Response(null, { status: 405 });
}
};
export const config: Config = {
path: ["/link/:id?"],
method: ["POST", "GET"]
};