Skip to content

Instantly share code, notes, and snippets.

@u1-liquid
Created October 6, 2025 08:03
Show Gist options
  • Select an option

  • Save u1-liquid/1b65ab1f9af527fc26b1a6576919fd5c to your computer and use it in GitHub Desktop.

Select an option

Save u1-liquid/1b65ab1f9af527fc26b1a6576919fd5c to your computer and use it in GitHub Desktop.
// Bindings required:
// - R2: R2 bucket
// - KV: KV namespace
// - MONITOR_URL: e.g. "https://example.com/test.json"
const MONITOR_URL = 'https://example.com/test.json'
export default {
async scheduled(event, env, ctx) {
ctx.waitUntil(run(env));
},
};
async function run(env) {
// Fetch the JSON without cache
const res = await fetch(MONITOR_URL, { cf: { cacheTtl: 0, cacheEverything: false } });
if (!res.ok) throw new Error(`monitor fetch failed: ${res.status}`);
const data = await res.json();
const nextUrl = data?.url;
if (!nextUrl || typeof nextUrl !== "string") {
throw new Error("url missing in JSON");
}
const prevUrl = await env.KV.get("url");
if (prevUrl && prevUrl === nextUrl) {
// No change detected
return;
}
// Download the artifact
const fileRes = await fetch(nextUrl, { cf: { cacheTtl: 0, cacheEverything: false } });
if (!fileRes.ok) throw new Error(`download failed: ${fileRes.status}`);
// Derive a key
const key = nextUrl.substring(nextUrl.lastIndexOf('/') + 1);
// Stream to R2
await env.R2.put(key, fileRes.body, {
httpMetadata: {
contentType: fileRes.headers.get("content-type") || undefined,
contentDisposition: fileRes.headers.get("content-disposition") || undefined,
},
customMetadata: {
source: nextUrl,
fetched_at: new Date().toISOString(),
},
});
// Persist state
await env.KV.put("url", nextUrl);
// refresh index.html (cheap list + single put)
await writeIndexHtml(env);
}
/* ---------------- index.html writer ---------------- */
async function writeIndexHtml(env) {
const objects = await listAll(env);
const html = renderIndexHtml({
title: "Files",
objects,
});
await env.R2.put('index.html', html, {
httpMetadata: { contentType: "text/html; charset=utf-8" },
});
}
async function listAll(env) {
const out = [];
let cursor;
do {
const page = await env.R2.list({ cursor, limit: 1000 });
out.push(...page.objects);
cursor = page.truncated ? page.cursor : undefined;
} while (cursor);
return out;
}
function renderIndexHtml({ title, objects }) {
const rows = objects
.map((o) => {
const name = o.key;
const href = encodePath(name); // relative link -> fetches same prefix
const size = formatBytes(o.size || 0);
const ts = o.uploaded ? isoLocal(o.uploaded) : "";
return `<tr>
<td><a href="${href}">${escapeHtml(name)}</a></td>
<td class="num">${size}</td>
<td class="mono">${ts}</td>
</tr>`;
})
.join("");
return `<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>${escapeHtml(title)}</title>
<style>
:root{--fg:#111;--muted:#666;--bg:#fff;--line:#e5e7eb;--card:#fafafa}
*{box-sizing:border-box}body{margin:0;font:14px/1.5 system-ui,Segoe UI,Roboto,Inter,Arial;color:var(--fg);background:var(--bg)}
header{padding:16px 20px;border-bottom:1px solid var(--line);background:var(--card)}
h1{margin:0;font-size:16px}
main{padding:16px 20px}
table{width:100%;border-collapse:collapse}
th,td{padding:10px;border-bottom:1px solid var(--line)}
th{text-align:left;color:var(--muted);font-weight:600}
td a{color:#0f62fe;text-decoration:none}td a:hover{text-decoration:underline}
.num{text-align:right}.mono{font-family:ui-monospace,SFMono-Regular,Consolas,Monaco,monospace;color:var(--muted)}
.empty{padding:24px;color:var(--muted)}
@media (max-width:640px){ .mono{display:none} th:nth-child(3), td:nth-child(3){display:none} }
</style>
<header><h1>${escapeHtml(title)}</h1></header>
<main>
${objects.length
? `<table><thead><tr><th>File</th><th class="num">Size</th><th class="mono">Uploaded</th></tr></thead><tbody>${rows}</tbody></table>`
: `<div class="empty">No files yet.</div>`}
</main>
</html>`;
}
/* ---------------- helpers ---------------- */
function encodePath(path) {
return path.split("/").map(encodeURIComponent).join("/");
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, (c) => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;" }[c]));
}
function formatBytes(n) {
const u = ["B","KB","MB","GB","TB"]; let i=0, v=n;
while (v >= 1024 && i < u.length-1) { v/=1024; i++; }
return `${v.toFixed(v<10&&i>0?1:0)} ${u[i]}`;
}
function isoLocal(d) {
const dt = new Date(d); const z = (x)=>String(x).padStart(2,"0");
return `${dt.getFullYear()}-${z(dt.getMonth()+1)}-${z(dt.getDate())} ${z(dt.getHours())}:${z(dt.getMinutes())}:${z(dt.getSeconds())}`;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment