// export-data.ts: add this to a val on Val Town as an HTTP file, and set an // environment variable EXPORT_TOKEN on that val (openssl rand -hex 32). // pull-data.ts on your VM then reads the val's SQLite and blobs through it, // and pull-env.ts the environment variables you name. It only reads: // SELECT and PRAGMA table_xinfo, blob list and get, named variables. // Delete the file and the variable once your data has moved. import { sqlite as scopedDb } from "https://esm.town/v/std/sqlite/main.ts"; import { sqlite as globalDb } from "https://esm.town/v/std/sqlite/global.ts"; import { blob as scopedBlobs } from "https://esm.town/v/std/blob/main.ts"; import { blob as globalBlobs } from "https://esm.town/v/std/blob"; export default async function (req: Request): Promise { const token = Deno.env.get("EXPORT_TOKEN"); if (!token || req.headers.get("Authorization") !== `Bearer ${token}`) { return new Response("Unauthorized", { status: 401, headers: { "Cache-Control": "no-store" } }); } const url = new URL(req.url); const global = url.searchParams.get("scope") === "global"; const db = global ? globalDb : scopedDb; const blobs = global ? globalBlobs : scopedBlobs; if (url.pathname === "/sql" && req.method === "POST") { const { sql, args } = await req.json(); if (!/^\s*(select|pragma table_xinfo)\b/i.test(sql)) { return new Response("read-only", { status: 400 }); } const r = await db.execute({ sql, args: args ?? [] }); const rows = r.rows.map((row) => r.columns.map((_, i) => row[i])); return Response.json({ columns: r.columns, rows }); } if (url.pathname === "/blobs") { return Response.json(await blobs.list(url.searchParams.get("prefix") ?? "")); } if (url.pathname === "/blob") { const res = await blobs.get(url.searchParams.get("key") ?? ""); return new Response(res.body); } if (url.pathname === "/env") { const names = (url.searchParams.get("names") ?? "").split(",").filter(Boolean); const blocked = (n: string) => n === "EXPORT_TOKEN" || /^val_?town/i.test(n); const missing = names.filter((n) => blocked(n) || Deno.env.get(n) === undefined); if (missing.length) return new Response(`not set: ${missing.join(",")}`, { status: 404 }); return Response.json(Object.fromEntries(names.map((n) => [n, Deno.env.get(n)])), { headers: { "Cache-Control": "no-store" }, }); } return new Response("export-data: /sql, /blobs, /blob?key=, /env?names=", { status: 404 }); }