clone

push a directory of html from the terminal and get a four-character url back. a deno cli on cloudflare

feat/source-stable-push
nzip / worker / src / api / sites.ts
sites.ts 5.6K
1import { formatAddress } from "../../../shared/mod.ts";2import type { PatchSiteRequest, RevertRequest, SourceResponse } from "../../../shared/mod.ts";3import { purgeSiteCache } from "../cache.ts";4import {5 commitSite,6 getSiteByAddress,7 getVaultByName,8 listSites,9 siteHistory,10 type SiteRow,11 siteRowToDetail,12 siteRowToInfo,13} from "../db.ts";14import { type Env, json, siteUrl } from "../env.ts";15import {16 HEX64,17 passwordHashFor,18 resolveCommitTtl,19 resolvePathTarget,20 sourceManifest,21 ttlToExpiry,22} from "./common.ts";23import { ApiError, readJson } from "./errors.ts";2425export async function handleSiteList(url: URL, env: Env): Promise<Response> {26 const vaultName = url.searchParams.get("vault");27 let slot: number | undefined;28 if (vaultName) {29 const vault = await getVaultByName(env, vaultName);30 if (!vault) throw new ApiError(404, `unknown vault: ${vaultName}`);31 slot = vault.slot;32 }33 const rows = await listSites(env, slot);34 return json(rows.map((row) => siteRowToInfo(env, row, row.vault_name)));35}3637export async function handleSite(38 request: Request,39 env: Env,40 parts: string[],41 ctx: ExecutionContext,42): Promise<Response> {43 const site = await resolvePathTarget(env, parts[2]);44 if (!site) throw new ApiError(404, "site not found");4546 if (parts.length === 4 && parts[3] === "source" && request.method === "GET") {47 const manifest = await sourceManifest(env, site);48 return json<SourceResponse>({49 address: formatAddress(site.address),50 manifestHash: site.current_manifest,51 manifest,52 });53 }54 if (parts.length === 5 && parts[3] === "source" && request.method === "GET") {55 return await handleSourceBlob(env, site, parts[4]);56 }57 if (parts.length === 3 && request.method === "GET") {58 return json(await siteRowToDetail(env, site));59 }60 if (parts.length === 3 && request.method === "DELETE") {61 await env.DB.prepare("DELETE FROM sites WHERE address = ?").bind(62 site.address,63 ).run();64 const address = formatAddress(site.address);65 await purgeSiteCache(ctx, address);66 return json({ ok: true, address });67 }68 if (parts.length === 3 && request.method === "PATCH") {69 return await handlePatch(request, env, site, ctx);70 }71 if (72 parts.length === 4 && parts[3] === "revert" && request.method === "POST"73 ) {74 return await handleRevert(request, env, site, ctx);75 }76 throw new ApiError(404, "not found");77}7879async function handleSourceBlob(80 env: Env,81 site: SiteRow,82 hash: string,83): Promise<Response> {84 if (!HEX64.test(hash)) throw new ApiError(400, "invalid blob hash");85 const manifest = await sourceManifest(env, site);86 const entry = Object.values(manifest.files).find((file) => file.h === hash);87 if (!entry) throw new ApiError(404, "source blob not found");88 const blob = await env.CONTENT.get(`blob/${hash}`);89 if (!blob) throw new ApiError(404, "source blob not found");90 return new Response(blob.body, {91 headers: {92 "content-type": "application/octet-stream",93 "content-length": String(entry.s),94 "cache-control": "no-store",95 },96 });97}9899async function handlePatch(100 request: Request,101 env: Env,102 site: SiteRow,103 ctx: ExecutionContext,104): Promise<Response> {105 const body = await readJson<PatchSiteRequest>(request);106 const expiresAt = body.ttl === undefined ? undefined : ttlToExpiry(body.ttl);107 const passwordHash = await passwordHashFor(body.password);108 const updates: string[] = [];109 const values: (string | number | null)[] = [];110 if (expiresAt !== undefined) {111 updates.push("expires_at = ?");112 values.push(expiresAt);113 }114 if (passwordHash !== undefined) {115 updates.push("password_hash = ?", "auth_version = auth_version + 1");116 values.push(passwordHash);117 }118 if (updates.length > 0) {119 updates.push("updated_at = ?");120 values.push(Math.floor(Date.now() / 1000), site.address);121 await env.DB.prepare(122 `UPDATE sites SET ${updates.join(", ")} WHERE address = ?`,123 )124 .bind(...values).run();125 }126 const updated = await getSiteByAddress(env, site.address);127 if (!updated) throw new ApiError(404, "site not found");128 await purgeSiteCache(ctx, formatAddress(site.address));129 return json(await siteRowToDetail(env, updated));130}131132async function handleRevert(133 request: Request,134 env: Env,135 site: SiteRow,136 ctx: ExecutionContext,137): Promise<Response> {138 const body = await readJson<RevertRequest>(request).catch((error) => {139 if ((request.headers.get("content-length") ?? "0") === "0") {140 return {} as RevertRequest;141 }142 throw error;143 });144 const history = await siteHistory(env, site.address);145 const target = body.toSeq !== undefined146 ? history.find((entry) => entry.seq === body.toSeq)147 : history.find((entry) => entry.manifestHash !== site.current_manifest);148 if (!target) {149 if (body.toSeq !== undefined) {150 throw new ApiError(404, `no push #${body.toSeq} in history`);151 }152 throw new ApiError(409, "nothing to revert to");153 }154 const seq = await commitSite(env, {155 address: site.address,156 vaultSlot: site.vault_slot,157 alias: site.alias,158 manifestHash: target.manifestHash,159 expiresAt: site.expires_at,160 isNew: false,161 note: `revert to #${target.seq}`,162 });163 const address = formatAddress(site.address);164 await purgeSiteCache(ctx, address);165 const resolvedTtl = resolveCommitTtl(undefined, site.expires_at, null);166 return json({167 address,168 url: siteUrl(env, address),169 alias: site.alias,170 manifestHash: target.manifestHash,171 expiresAt: site.expires_at,172 ttl: resolvedTtl.ttl,173 ttlSource: resolvedTtl.ttlSource,174 protected: site.password_hash !== null,175 revertedTo: target.seq,176 seq,177 });178}