Concept project · runs in your browserTypeScript · React.js · Next.js · Web Crypto API
File Upload and Image Processing Service in Node.js Demo
Drop a real image or PDF (it never leaves your browser) and watch it go through an upload pipeline: type sniffed from magic bytes, SHA-256 computed, presigned URLs issued per 512 KB part, parts sent on a throttled and optionally flaky network, then verified and turned into WebP variants. Pause, resume, abort, or try the samples that get refused.
Interactive demo
## How it works
Before anything is sent, the browser reads the first 512 bytes and identifies the file from its magic bytes (89 50 4E 47 is PNG, 4D 5A is a Windows executable), then hashes the whole file with crypto.subtle.digest("SHA-256").
POST /uploads checks size, type and extension and answers with an upload id. Each 512 KB part gets its own short-lived presigned URL and goes straight to the bucket, so the API never carries file bytes. Pausing stops after the current part; resuming asks the bucket which parts it already has.
On complete, a worker step reassembles the object, sniffs it again and compares its SHA-256 with the one the client declared. Anything that does not match is deleted and marked rejected. Images are then resized to fixed widths and encoded as WebP, which also drops EXIF data.
## View source
The code running in the window above, exactly as deployed.
app/lab/file-upload-service/upload.ts226 lines
/**
* Upload service core: policy checks, magic-byte sniffing, checksums, presigned part URLs and an
* in-memory "bucket" that stores multipart uploads. No React here.
*
* Production shape (Express.js + S3-compatible storage):
* POST /uploads -> validate metadata, CreateMultipartUpload, presign part URLs
* PUT <presigned part url> -> browser sends each chunk straight to the bucket
* GET /uploads/:id/parts -> ListParts, so a client can resume
* POST /uploads/:id/complete -> CompleteMultipartUpload, then re-check type + checksum in a worker
* DELETE /uploads/:id -> AbortMultipartUpload
*
* Files never leave the browser in this demo; the bucket is a Map in memory.
*/
export const MAX_BYTES = 10 * 1024 * 1024;
export const PART_SIZE = 512 * 1024;
export const URL_TTL_S = 900;
export const ALLOWED: Record<string, string[]> = {
"image/png": ["png"],
"image/jpeg": ["jpg", "jpeg"],
"image/gif": ["gif"],
"image/webp": ["webp"],
"application/pdf": ["pdf"],
};
/* ------------------------------------------------------------------ */
/* Magic bytes */
/* ------------------------------------------------------------------ */
type Sig = { mime: string; label: string; test: (b: Uint8Array) => boolean };
const at = (b: Uint8Array, off: number, bytes: number[]) => bytes.every((x, i) => b[off + i] === x);
const ascii = (b: Uint8Array, off: number, s: string) => at(b, off, [...s].map((c) => c.charCodeAt(0)));
const SIGNATURES: Sig[] = [
{ mime: "image/png", label: "PNG", test: (b) => at(b, 0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) },
{ mime: "image/jpeg", label: "JPEG", test: (b) => at(b, 0, [0xff, 0xd8, 0xff]) },
{ mime: "image/gif", label: "GIF", test: (b) => ascii(b, 0, "GIF87a") || ascii(b, 0, "GIF89a") },
{ mime: "image/webp", label: "WebP", test: (b) => ascii(b, 0, "RIFF") && ascii(b, 8, "WEBP") },
{ mime: "application/pdf", label: "PDF", test: (b) => ascii(b, 0, "%PDF-") },
{ mime: "application/zip", label: "ZIP (also docx/xlsx)", test: (b) => at(b, 0, [0x50, 0x4b, 0x03, 0x04]) },
{ mime: "application/x-msdownload", label: "Windows executable (MZ)", test: (b) => ascii(b, 0, "MZ") },
{ mime: "application/x-elf", label: "Linux executable (ELF)", test: (b) => at(b, 0, [0x7f, 0x45, 0x4c, 0x46]) },
{ mime: "video/mp4", label: "MP4", test: (b) => ascii(b, 4, "ftyp") },
{ mime: "image/svg+xml", label: "SVG (can carry scripts)", test: (b) => /^\s*(<\?xml[^>]*>\s*)?<svg[\s>]/i.test(new TextDecoder().decode(b.slice(0, 256))) },
];
/** Identify a file from its first bytes, ignoring its name and the browser's guess. */
export function sniff(head: Uint8Array): { mime: string; label: string } {
for (const s of SIGNATURES) if (s.test(head)) return { mime: s.mime, label: s.label };
const sample = head.slice(0, 512);
const printable = sample.every((x) => x === 9 || x === 10 || x === 13 || (x >= 32 && x < 127) || x >= 128);
return printable && sample.length ? { mime: "text/plain", label: "plain text" } : { mime: "application/octet-stream", label: "unknown binary" };
}
export const hexHead = (b: Uint8Array, n = 12) => Array.from(b.slice(0, n), (x) => x.toString(16).padStart(2, "0")).join(" ");
/* ------------------------------------------------------------------ */
/* Policy */
/* ------------------------------------------------------------------ */
export type InitRequest = { filename: string; size: number; contentType: string; sha256: string };
export type ApiError = { status: 400 | 403 | 404 | 409 | 410 | 413 | 415 | 422; error: string };
/** What POST /uploads checks before handing out any URL. The client can lie here; complete() re-checks. */
export function validateInit(req: InitRequest): ApiError | null {
if (!req.filename || req.filename.length > 200 || /[\\/]/.test(req.filename)) return { status: 400, error: "filename must be 1–200 chars with no path separators" };
if (!Number.isInteger(req.size) || req.size <= 0) return { status: 400, error: "size must be a positive integer" };
if (req.size > MAX_BYTES) return { status: 413, error: `file is ${(req.size / 1048576).toFixed(1)} MB; the limit is ${MAX_BYTES / 1048576} MB` };
if (!ALLOWED[req.contentType]) return { status: 415, error: `${req.contentType || "unknown type"} is not accepted (images and PDF only)` };
const ext = req.filename.split(".").pop()?.toLowerCase() ?? "";
if (!ALLOWED[req.contentType]!.includes(ext)) return { status: 422, error: `extension .${ext} does not match ${req.contentType}` };
if (!/^[a-f0-9]{64}$/.test(req.sha256)) return { status: 400, error: "sha256 must be 64 hex characters" };
return null;
}
/* ------------------------------------------------------------------ */
/* Crypto */
/* ------------------------------------------------------------------ */
const toHex = (buf: ArrayBuffer) => Array.from(new Uint8Array(buf), (x) => x.toString(16).padStart(2, "0")).join("");
export async function sha256Hex(data: ArrayBuffer | Uint8Array): Promise<string> {
const view = data instanceof Uint8Array ? data : new Uint8Array(data);
return toHex(await crypto.subtle.digest("SHA-256", view as Uint8Array<ArrayBuffer>));
}
const SIGNING_KEY = "demo-bucket-signing-key"; // production: the storage provider's secret, never in the browser
/** A presigned PUT URL for one part: an HMAC over method, path, part, upload id and expiry. */
export async function presignPart(uploadId: string, key: string, partNumber: number, nowS: number) {
const expires = nowS + URL_TTL_S;
const canonical = ["PUT", `/media/${key}`, `partNumber=${partNumber}`, `uploadId=${uploadId}`, `expires=${expires}`].join("\n");
const k = await crypto.subtle.importKey("raw", new TextEncoder().encode(SIGNING_KEY), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const sig = toHex(await crypto.subtle.sign("HMAC", k, new TextEncoder().encode(canonical)));
return {
url: `https://media.acme-storage.test/media/${key}?partNumber=${partNumber}&uploadId=${uploadId}&X-Amz-Expires=${URL_TTL_S}&X-Amz-Signature=${sig}`,
expires,
};
}
/* ------------------------------------------------------------------ */
/* Records (persisted) and the bucket (memory only) */
/* ------------------------------------------------------------------ */
export type Status = "uploading" | "paused" | "interrupted" | "verifying" | "processing" | "ready" | "rejected" | "aborted";
export type Variant = { width: number; height: number; bytes: number; type: string };
export type UploadRecord = {
id: string;
key: string;
filename: string;
size: number;
declaredType: string;
sniffed: string;
sniffedLabel: string;
head: string;
sha256: string;
partCount: number;
partsDone: number[]; // part numbers the bucket confirmed (ETag received)
etags: Record<number, string>;
status: Status;
error: string | null;
createdAt: number;
dims: { w: number; h: number } | null;
variants: Variant[];
thumb: string | null; // small data URL kept for the gallery
};
export const partCount = (size: number) => Math.max(1, Math.ceil(size / PART_SIZE));
export const partRange = (n: number, size: number): [number, number] => [(n - 1) * PART_SIZE, Math.min(size, n * PART_SIZE)];
/** The bucket: uploadId -> part number -> bytes. Lost on reload, which is what makes "resume" interesting. */
export class Bucket {
private parts = new Map<string, Map<number, Uint8Array>>();
create(uploadId: string) {
this.parts.set(uploadId, new Map());
}
/** PUT part: stores bytes and returns the ETag (here: first 16 hex of the part's SHA-256). */
async putPart(uploadId: string, n: number, bytes: Uint8Array): Promise<string> {
if (!this.parts.has(uploadId)) this.create(uploadId);
this.parts.get(uploadId)!.set(n, bytes);
return `"${(await sha256Hex(bytes)).slice(0, 16)}"`;
}
listParts(uploadId: string): number[] {
return [...(this.parts.get(uploadId)?.keys() ?? [])].sort((a, b) => a - b);
}
abort(uploadId: string) {
this.parts.delete(uploadId);
}
/** CompleteMultipartUpload: concatenate parts in order. Fails if any part is missing. */
complete(uploadId: string, count: number): Uint8Array | null {
const m = this.parts.get(uploadId);
if (!m) return null;
let total = 0;
for (let i = 1; i <= count; i++) {
const p = m.get(i);
if (!p) return null;
total += p.length;
}
const out = new Uint8Array(total);
let off = 0;
for (let i = 1; i <= count; i++) {
const p = m.get(i)!;
out.set(p, off);
off += p.length;
}
return out;
}
}
/**
* The worker step after complete: re-sniff the stored object (the client may have lied in POST /uploads)
* and compare its SHA-256 with the one the client declared.
*/
export async function verifyObject(rec: UploadRecord, object: Uint8Array): Promise<{ ok: true } | { ok: false; error: string }> {
if (object.length !== rec.size) return { ok: false, error: `size mismatch: expected ${rec.size}, stored ${object.length}` };
const actual = sniff(object.slice(0, 512));
if (actual.mime !== rec.declaredType) return { ok: false, error: `content is ${actual.label}, declared ${rec.declaredType}` };
const sum = await sha256Hex(object);
if (sum !== rec.sha256) return { ok: false, error: "checksum mismatch: object differs from what the client hashed" };
return { ok: true };
}
export const newId = (n: number) => `up_${(n * 2654435761 >>> 0).toString(36)}`;
export function isRecordList(v: unknown) {
return Array.isArray(v) && v.every((r) => r && typeof (r as UploadRecord).id === "string" && Array.isArray((r as UploadRecord).partsDone));
}
/* ------------------------------------------------------------------ */
/* Sample files (generated in the browser, nothing downloaded) */
/* ------------------------------------------------------------------ */
export function samplePdf(): File {
const text = `%PDF-1.4
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] >> endobj
trailer << /Root 1 0 R >>
%%EOF
% Invoice INV-2207 for Acme Dental, Order #1042
`;
return new File([text.repeat(40)], "invoice-INV-2207.pdf", { type: "application/pdf" });
}
/** An "image" that is really a Windows executable header: the classic renamed-file attack. */
export function sampleFakeJpg(): File {
const b = new Uint8Array(48 * 1024);
b.set([0x4d, 0x5a, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00]);
for (let i = 64; i < b.length; i++) b[i] = (i * 31) % 251;
return new File([b], "holiday-photo.jpg", { type: "image/jpeg" });
}
export function sampleTooLarge(): File {
const b = new Uint8Array(12 * 1024 * 1024);
b.set([0x50, 0x4b, 0x03, 0x04]);
return new File([b], "site-backup.zip", { type: "application/zip" });
}app/lab/file-upload-service/images.ts71 lines
/**
* Image step of the pipeline, done with <canvas> in the browser. In production this runs in a worker
* with sharp (libvips); the rules are the same: auto-orient, strip metadata, resize to fixed widths,
* encode as WebP, keep a tiny preview.
*/
import type { Variant } from "./upload";
export const WIDTHS = [320, 768, 1600];
function canvasBlob(c: HTMLCanvasElement, type: string, quality: number): Promise<Blob> {
return new Promise((resolve, reject) => c.toBlob((b) => (b ? resolve(b) : reject(new Error("encode failed"))), type, quality));
}
/**
* Re-encoding through a canvas drops EXIF (including GPS) because only pixels are copied.
* createImageBitmap applies the EXIF orientation first, so the output is upright.
*/
export async function makeVariants(blob: Blob): Promise<{ dims: { w: number; h: number }; variants: Variant[]; thumb: string }> {
const bmp = await createImageBitmap(blob);
const dims = { w: bmp.width, h: bmp.height };
const widths = WIDTHS.filter((w) => w < dims.w);
if (!widths.length) widths.push(dims.w); // never upscale
const variants: Variant[] = [];
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("canvas 2d context unavailable");
for (const w of widths) {
const h = Math.round((dims.h / dims.w) * w);
canvas.width = w;
canvas.height = h;
ctx.drawImage(bmp, 0, 0, w, h);
const out = await canvasBlob(canvas, "image/webp", 0.8);
variants.push({ width: w, height: h, bytes: out.size, type: out.type });
}
// Tiny preview kept with the record (and in localStorage).
const tw = Math.min(160, dims.w);
canvas.width = tw;
canvas.height = Math.round((dims.h / dims.w) * tw);
ctx.drawImage(bmp, 0, 0, canvas.width, canvas.height);
const thumb = canvas.toDataURL("image/webp", 0.6);
bmp.close();
return { dims, variants, thumb };
}
/** A sample photo drawn on a canvas, so visitors without an image handy can try the pipeline. */
export async function samplePng(): Promise<File> {
const c = document.createElement("canvas");
c.width = 1800;
c.height = 1100;
const ctx = c.getContext("2d")!;
const g = ctx.createLinearGradient(0, 0, c.width, c.height);
g.addColorStop(0, "#0f766e");
g.addColorStop(1, "#1e3a8a");
ctx.fillStyle = g;
ctx.fillRect(0, 0, c.width, c.height);
for (let i = 0; i < 60; i++) {
ctx.fillStyle = `hsla(${(i * 37) % 360} 80% 70% / 0.25)`;
ctx.beginPath();
ctx.arc((i * 211) % c.width, (i * 137) % c.height, 30 + ((i * 53) % 140), 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = "#ffffff";
ctx.font = "bold 120px system-ui, sans-serif";
ctx.fillText("Acme Dental", 120, 560);
ctx.font = "56px system-ui, sans-serif";
ctx.fillText("Spring open day · sample image", 124, 660);
const blob = await canvasBlob(c, "image/png", 1);
return new File([blob], "acme-open-day.png", { type: "image/png" });
}app/lab/file-upload-service/Demo.tsx566 lines
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { PanelTitle, Pill, TerminalLog, btn, btnDanger, btnPrimary, formatBytes, useLog, useStoredState } from "@/components/lab/systems/ui";
import { makeVariants, samplePng } from "./images";
import {
Bucket,
MAX_BYTES,
PART_SIZE,
hexHead,
isRecordList,
newId,
partCount,
partRange,
presignPart,
sampleFakeJpg,
samplePdf,
sampleTooLarge,
sha256Hex,
sniff,
validateInit,
verifyObject,
type UploadRecord,
} from "./upload";
const KEY = "lab:file-upload-service:v1";
const SPEEDS = { "slow 3G": 400 * 1024, "4G": 2.5 * 1024 * 1024, "Wi-Fi": 8 * 1024 * 1024 } as const;
type Speed = keyof typeof SPEEDS;
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export default function Demo() {
const [records, setRecords, resetRecords] = useStoredState<UploadRecord[]>(KEY, () => [], isRecordList);
const recRef = useRef<UploadRecord[]>([]);
useEffect(() => {
if (records) recRef.current = records;
}, [records]);
const bucket = useRef(new Bucket());
const files = useRef(new Map<string, File>());
const paused = useRef(new Set<string>());
const running = useRef(new Set<string>());
const [inflight, setInflight] = useState<Record<string, number>>({});
const [speed, setSpeed] = useState<Speed>("4G");
const [flaky, setFlaky] = useState(false);
const [selected, setSelected] = useState<string | null>(null);
const [dragging, setDragging] = useState(false);
const [announce, setAnnounce] = useState("");
const [urlPreview, setUrlPreview] = useState<string | null>(null);
const { lines, push, clear } = useLog(120);
const inputRef = useRef<HTMLInputElement>(null);
const settings = useRef({ speed, flaky });
useEffect(() => {
settings.current = { speed, flaky };
}, [speed, flaky]);
const patch = useCallback(
(id: string, p: Partial<UploadRecord>) => {
setRecords((rs) => {
const next = (rs ?? []).map((r) => (r.id === id ? { ...r, ...p } : r));
recRef.current = next;
return next;
});
},
[setRecords],
);
const get = (id: string) => recRef.current.find((r) => r.id === id);
// After a reload the bucket (memory) is empty and File objects are gone: mark live uploads as interrupted.
const normalised = useRef(false);
useEffect(() => {
if (!records || normalised.current) return;
normalised.current = true;
if (records.some((r) => r.status === "uploading" || r.status === "verifying" || r.status === "processing")) {
setRecords(records.map((r) => (["uploading", "verifying", "processing"].includes(r.status) ? { ...r, status: "interrupted" } : r)));
}
}, [records, setRecords]);
/* ---------------- the upload pipeline ---------------- */
const uploadParts = useCallback(
async (id: string) => {
if (running.current.has(id)) return;
running.current.add(id);
try {
const file = files.current.get(id);
let rec = get(id);
if (!file || !rec) return;
patch(id, { status: "uploading", error: null });
// Resume: ask the bucket which parts it has (GET /uploads/:id/parts).
const stored = new Set(bucket.current.listParts(id));
push(`GET /uploads/${id}/parts → 200 [${[...stored].join(",") || "none"}]`, "dim");
for (const n of rec.partsDone) {
if (stored.has(n)) continue;
// The record says done but the in-memory bucket was lost on reload: re-check the local bytes
// against the saved ETag and restore them without "sending" again.
const [a, b] = partRange(n, file.size);
const bytes = new Uint8Array(await file.slice(a, b).arrayBuffer());
const etag = await bucket.current.putPart(id, n, bytes);
if (etag !== rec.etags[n]) {
push(`part ${n}: ETag differs from the stored one; will re-upload`, "warn");
rec = { ...rec, partsDone: rec.partsDone.filter((x) => x !== n) };
patch(id, { partsDone: rec.partsDone });
} else push(`part ${n}: matches stored ETag ${etag}, skipped`, "dim");
}
for (let n = 1; n <= rec.partCount; n++) {
if (get(id)?.partsDone.includes(n)) continue;
if (get(id)?.status === "aborted") return;
if (paused.current.has(id)) {
patch(id, { status: "paused" });
push(`upload ${id} paused before part ${n}/${rec.partCount}`, "warn");
setAnnounce(`${rec.filename} paused`);
return;
}
const { url } = await presignPart(id, rec.key, n, Math.floor(Date.now() / 1000));
if (n === 1 || n === rec.partCount) setUrlPreview(url);
const [a, b] = partRange(n, file.size);
const bytes = new Uint8Array(await file.slice(a, b).arrayBuffer());
let attempt = 0;
for (;;) {
// simulate transfer time at the chosen network speed, in 100 ms steps
const total = (bytes.length / SPEEDS[settings.current.speed]) * 1000;
const failAt = settings.current.flaky && Math.random() < 0.2 ? Math.random() * 0.8 : null;
let t = 0;
let failed = false;
while (t < total) {
await sleep(Math.min(100, total - t));
t += 100;
const frac = Math.min(1, t / total);
setInflight((m) => ({ ...m, [id]: frac }));
if (failAt !== null && frac >= failAt) {
failed = true;
break;
}
}
if (!failed) break;
attempt += 1;
if (attempt > 3) {
patch(id, { status: "paused", error: `part ${n} failed 4 times; press resume to try again` });
push(`PUT part ${n} → network error (4th try), upload paused`, "err");
setInflight((m) => ({ ...m, [id]: 0 }));
return;
}
const wait = 300 * 2 ** (attempt - 1);
push(`PUT part ${n} → ECONNRESET, retry ${attempt}/3 in ${wait} ms`, "warn");
setInflight((m) => ({ ...m, [id]: 0 }));
await sleep(wait);
}
if (get(id)?.status === "aborted") return;
const etag = await bucket.current.putPart(id, n, bytes);
const cur = get(id)!;
patch(id, { partsDone: [...cur.partsDone, n], etags: { ...cur.etags, [n]: etag } });
setInflight((m) => ({ ...m, [id]: 0 }));
push(`PUT part ${n}/${rec.partCount} (${formatBytes(bytes.length)}) → 200 ETag ${etag}`, "info");
}
// POST /uploads/:id/complete
patch(id, { status: "verifying" });
push(`POST /uploads/${id}/complete {parts: ${rec.partCount}}`, "info");
const object = bucket.current.complete(id, rec.partCount);
const fresh = get(id)!;
if (!object) {
patch(id, { status: "rejected", error: "missing parts at complete" });
push(`400 missing parts`, "err");
return;
}
const check = await verifyObject(fresh, object);
if (!check.ok) {
patch(id, { status: "rejected", error: check.error });
bucket.current.abort(id);
push(`worker: ✗ ${check.error} → object deleted, status rejected`, "err");
setAnnounce(`${fresh.filename} rejected: ${check.error}`);
return;
}
push(`worker: ✓ magic bytes ${fresh.sniffedLabel}, sha256 matches`, "ok");
if (fresh.declaredType.startsWith("image/")) {
patch(id, { status: "processing" });
try {
const out = await makeVariants(new Blob([object as Uint8Array<ArrayBuffer>], { type: fresh.declaredType }));
patch(id, { ...out, status: "ready" });
push(`worker: ${out.variants.map((v) => `${v.width}w ${formatBytes(v.bytes)}`).join(", ")} (EXIF stripped)`, "ok");
} catch {
patch(id, { status: "rejected", error: "image could not be decoded" });
push(`worker: ✗ image decode failed → rejected`, "err");
return;
}
} else {
patch(id, { status: "ready" });
}
push(`200 ${id} ready`, "ok");
setAnnounce(`${fresh.filename} uploaded and ready`);
} finally {
running.current.delete(id);
}
},
[patch, push],
);
const accept = useCallback(
async (file: File) => {
const head = new Uint8Array(await file.slice(0, 512).arrayBuffer());
const s = sniff(head);
push(`client: ${file.name} ${formatBytes(file.size)} · browser says ${file.type || "?"} · magic bytes say ${s.label}`, "dim");
if (s.mime !== file.type) push(`client: warning, ${file.name} does not look like ${file.type || "its extension"} (the server re-checks)`, "warn");
const tooBig = file.size > MAX_BYTES;
const sha = tooBig ? "0".repeat(64) : await sha256Hex(await file.arrayBuffer());
if (!tooBig) push(`client: sha256 ${sha.slice(0, 16)}…`, "dim");
// Resume an interrupted or paused upload of the same bytes.
const existing = recRef.current.find((r) => r.sha256 === sha && r.size === file.size && ["interrupted", "paused"].includes(r.status));
if (existing) {
files.current.set(existing.id, file);
paused.current.delete(existing.id);
push(`client: same checksum as ${existing.id}, resuming (${existing.partsDone.length}/${existing.partCount} parts done)`, "accent");
setSelected(existing.id);
void uploadParts(existing.id);
return;
}
const body = { filename: file.name, size: file.size, contentType: file.type, sha256: sha };
push(`POST /uploads ${JSON.stringify({ ...body, sha256: sha.slice(0, 8) + "…" })}`, "info");
const err = validateInit(body);
const n = (Date.now() % 1_000_000_000) + file.size;
const id = newId(n);
const rec: UploadRecord = {
id,
key: `u/42/${id}/${file.name.replace(/[^\w.-]/g, "_")}`,
filename: file.name,
size: file.size,
declaredType: file.type,
sniffed: s.mime,
sniffedLabel: s.label,
head: hexHead(head),
sha256: sha,
partCount: partCount(file.size),
partsDone: [],
etags: {},
status: err ? "rejected" : "uploading",
error: err ? `${err.status}: ${err.error}` : null,
createdAt: Date.now(),
dims: null,
variants: [],
thumb: null,
};
setRecords((rs) => {
const next = [rec, ...(rs ?? [])].slice(0, 12);
recRef.current = next;
return next;
});
setSelected(id);
if (err) {
push(`${err.status} ${err.error}`, "err");
setAnnounce(`${file.name} refused: ${err.error}`);
return;
}
push(`201 {uploadId: "${id}", partSize: ${PART_SIZE}, parts: ${rec.partCount}, urlsExpireIn: 900}`, "ok");
files.current.set(id, file);
bucket.current.create(id);
void uploadParts(id);
},
[push, setRecords, uploadParts],
);
function onFiles(list: FileList | null) {
if (!list) return;
for (const f of Array.from(list).slice(0, 5)) void accept(f);
}
function pause(id: string) {
paused.current.add(id);
push(`client: pause requested for ${id} (finishes the current part)`, "dim");
}
function resume(id: string) {
paused.current.delete(id);
if (!files.current.has(id)) {
setSelected(id);
setAnnounce("Select the same file again to resume this upload");
inputRef.current?.click();
return;
}
void uploadParts(id);
}
function abort(id: string) {
paused.current.add(id);
bucket.current.abort(id);
files.current.delete(id);
patch(id, { status: "aborted" });
push(`DELETE /uploads/${id} → 204 (AbortMultipartUpload, parts discarded)`, "warn");
}
function remove(id: string) {
abort(id);
setRecords((rs) => {
const next = (rs ?? []).filter((r) => r.id !== id);
recRef.current = next;
return next;
});
if (selected === id) setSelected(null);
}
function reset() {
for (const id of files.current.keys()) paused.current.add(id);
files.current.clear();
bucket.current = new Bucket();
resetRecords();
clear();
setSelected(null);
setUrlPreview(null);
setInflight({});
}
async function sample(kind: "png" | "pdf" | "fake" | "big") {
const f = kind === "png" ? await samplePng() : kind === "pdf" ? samplePdf() : kind === "fake" ? sampleFakeJpg() : sampleTooLarge();
await accept(f);
}
if (!records) return <div className="p-6 font-mono text-sm text-fg-muted">loading uploads…</div>;
const sel = records.find((r) => r.id === selected) ?? null;
return (
<div className="space-y-5 p-4 sm:p-6">
<p className="sr-only" aria-live="polite">
{announce}
</p>
<div className="grid gap-5 lg:grid-cols-[1fr_22rem]">
<div className="min-w-0 space-y-5">
<div
onDragOver={(e) => {
e.preventDefault();
setDragging(true);
}}
onDragLeave={() => setDragging(false)}
onDrop={(e) => {
e.preventDefault();
setDragging(false);
onFiles(e.dataTransfer.files);
}}
className={`rounded-lg border-2 border-dashed p-6 text-center transition-colors ${dragging ? "border-accent bg-accent-soft/50" : "border-border"}`}
>
<p className="text-sm">Drop images or PDFs here, or</p>
<button type="button" className={`${btnPrimary} mt-2`} onClick={() => inputRef.current?.click()}>
Choose files
</button>
<input ref={inputRef} type="file" multiple className="sr-only" aria-label="Choose files to upload" onChange={(e) => {
onFiles(e.target.files);
e.target.value = "";
}} />
<p className="mt-2 text-xs text-fg-muted">
PNG, JPEG, GIF, WebP or PDF up to {MAX_BYTES / 1048576} MB. Files stay in your browser; nothing is sent anywhere.
</p>
<div className="mt-3 flex flex-wrap justify-center gap-2">
<span className="self-center text-xs text-fg-muted">or try a sample:</span>
<button type="button" className={btn} onClick={() => sample("png")}>
photo.png
</button>
<button type="button" className={btn} onClick={() => sample("pdf")}>
invoice.pdf
</button>
<button type="button" className={btn} onClick={() => sample("fake")}>
renamed .exe as .jpg
</button>
<button type="button" className={btn} onClick={() => sample("big")}>
12 MB zip
</button>
</div>
</div>
<div className="flex flex-wrap items-center gap-3 text-xs">
<label className="flex items-center gap-2 font-mono">
network
<select value={speed} onChange={(e) => setSpeed(e.target.value as Speed)} className="rounded border border-border bg-bg-elev px-1 py-0.5">
{(Object.keys(SPEEDS) as Speed[]).map((s) => (
<option key={s}>{s}</option>
))}
</select>
</label>
<label className="flex items-center gap-2">
<input type="checkbox" checked={flaky} onChange={(e) => setFlaky(e.target.checked)} />
flaky connection (20% of parts drop)
</label>
<button type="button" className={`${btn} ml-auto`} onClick={reset}>
reset demo
</button>
</div>
<ul className="space-y-2" aria-label="Uploads">
{records.map((r) => {
const done = r.partsDone.length + (inflight[r.id] ?? 0);
const pct = Math.round((done / r.partCount) * 100);
const active = r.status === "uploading";
return (
<li key={r.id} className={`rounded-md border p-3 ${selected === r.id ? "border-accent" : "border-border"}`}>
<div className="flex items-start gap-3">
<div className="grid size-12 shrink-0 place-items-center overflow-hidden rounded bg-bg-muted font-mono text-[10px] text-fg-muted">
{r.thumb ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={r.thumb} alt="" className="size-full object-cover" />
) : (
(r.filename.split(".").pop() ?? "").toUpperCase()
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<button type="button" onClick={() => setSelected(r.id)} className="truncate text-left text-sm font-medium hover:text-accent">
{r.filename}
</button>
<StatusPill status={r.status} />
</div>
<p className="font-mono text-[11px] text-fg-muted">
{formatBytes(r.size)} · {r.partsDone.length}/{r.partCount} parts · {r.id}
</p>
{r.status !== "rejected" && r.status !== "ready" && r.status !== "aborted" && (
<div className="mt-1.5 h-1.5 overflow-hidden rounded bg-bg-muted" role="progressbar" aria-label={`${r.filename} upload progress`} aria-valuenow={pct} aria-valuemin={0} aria-valuemax={100}>
<div className="h-full bg-accent transition-[width]" style={{ width: `${pct}%` }} />
</div>
)}
{r.error && <p className="mt-1 text-xs text-danger">{r.error}</p>}
{r.status === "interrupted" && <p className="mt-1 text-xs text-warn-fg">Interrupted by a reload. Select the same file again to resume from part {r.partsDone.length + 1}.</p>}
</div>
<div className="flex shrink-0 flex-col gap-1">
{active && (
<button type="button" className={btn} onClick={() => pause(r.id)}>
pause
</button>
)}
{(r.status === "paused" || r.status === "interrupted") && (
<button type="button" className={btn} onClick={() => resume(r.id)}>
resume
</button>
)}
{(active || r.status === "paused" || r.status === "interrupted") && (
<button type="button" className={btnDanger} onClick={() => abort(r.id)}>
abort
</button>
)}
{!active && r.status !== "paused" && r.status !== "interrupted" && (
<button type="button" className={btnDanger} onClick={() => remove(r.id)} aria-label={`Remove ${r.filename}`}>
×
</button>
)}
</div>
</div>
</li>
);
})}
{!records.length && <li className="rounded-md border border-border p-4 text-center text-sm text-fg-muted">No uploads yet. Drop a file or try a sample.</li>}
</ul>
</div>
<div className="min-w-0 space-y-5">
<FlowDiagram status={sel?.status ?? null} />
<section aria-label="Upload details" className="rounded-md border border-border p-3 text-xs">
<PanelTitle>details</PanelTitle>
{sel ? (
<dl className="grid grid-cols-[6.5rem_1fr] gap-x-2 gap-y-1 font-mono text-[11px]">
<dt className="text-fg-muted">declared</dt>
<dd>{sel.declaredType || "—"}</dd>
<dt className="text-fg-muted">magic bytes</dt>
<dd className={sel.sniffed === sel.declaredType ? "text-accent" : "text-danger"}>{sel.sniffedLabel}</dd>
<dt className="text-fg-muted">first bytes</dt>
<dd className="break-all">{sel.head}</dd>
<dt className="text-fg-muted">sha256</dt>
<dd className="break-all">{sel.sha256}</dd>
<dt className="text-fg-muted">object key</dt>
<dd className="break-all">{sel.key}</dd>
{sel.dims && (
<>
<dt className="text-fg-muted">original</dt>
<dd>
{sel.dims.w}×{sel.dims.h}
</dd>
</>
)}
{sel.variants.map((v) => (
<div key={v.width} className="contents">
<dt className="text-fg-muted">{v.width}w</dt>
<dd>
{v.width}×{v.height} {v.type.replace("image/", "")} · {formatBytes(v.bytes)}
</dd>
</div>
))}
</dl>
) : (
<p className="text-fg-muted">Select an upload to see its checks and variants.</p>
)}
{sel?.thumb && (
// eslint-disable-next-line @next/next/no-img-element
<img src={sel.thumb} alt={`Preview of ${sel.filename}`} className="mt-3 w-full rounded border border-border" />
)}
</section>
{urlPreview && (
<section aria-label="Last presigned URL" className="rounded-md border border-border p-3">
<PanelTitle>last presigned PUT URL</PanelTitle>
<p className="font-mono text-[10px] break-all text-fg-muted">{urlPreview}</p>
</section>
)}
</div>
</div>
<section aria-label="Request log">
<PanelTitle>request log</PanelTitle>
<TerminalLog lines={lines} label="Upload API and worker log" className="h-56" empty="$ waiting for a file…" />
</section>
</div>
);
}
function StatusPill({ status }: { status: UploadRecord["status"] }) {
const tone = ({ ready: "ok", rejected: "err", aborted: "muted", paused: "warn", interrupted: "warn", uploading: "info", verifying: "info", processing: "info" } as const)[status];
return <Pill tone={tone}>{status}</Pill>;
}
/** Sequence diagram of the presigned flow, highlighting where the selected upload is. */
function FlowDiagram({ status }: { status: UploadRecord["status"] | null }) {
const actors = ["Browser", "API", "Bucket", "Worker"];
const steps: { from: number; to: number; text: string; on: UploadRecord["status"][] }[] = [
{ from: 0, to: 1, text: "POST /uploads (size, type, sha256)", on: [] },
{ from: 1, to: 0, text: "201 uploadId + presigned URLs", on: [] },
{ from: 0, to: 2, text: "PUT part n (direct)", on: ["uploading", "paused", "interrupted"] },
{ from: 0, to: 1, text: "POST /uploads/:id/complete", on: ["verifying"] },
{ from: 1, to: 3, text: "job: sniff + checksum", on: ["verifying", "rejected"] },
{ from: 3, to: 2, text: "write WebP variants", on: ["processing", "ready"] },
];
const W = 340;
const col = (i: number) => 30 + i * ((W - 60) / 3);
const rowH = 30;
const H = 34 + steps.length * rowH + 8;
return (
<section aria-label="Presigned upload flow" className="rounded-md border border-border p-3">
<PanelTitle>presigned upload flow</PanelTitle>
<svg viewBox={`0 0 ${W} ${H}`} className="w-full" role="img" aria-label={`Sequence: ${steps.map((s, i) => `${i + 1}. ${actors[s.from]} to ${actors[s.to]}: ${s.text}`).join("; ")}`}>
<defs>
<marker id="fu-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="currentColor" />
</marker>
</defs>
{actors.map((a, i) => (
<g key={a}>
<text x={col(i)} y={14} textAnchor="middle" fontSize={11} fontFamily="monospace" fill="var(--fg)">
{a}
</text>
<line x1={col(i)} x2={col(i)} y1={22} y2={H - 4} stroke="var(--border)" strokeDasharray="3 3" />
</g>
))}
{steps.map((s, i) => {
const y = 40 + i * rowH;
const hot = status !== null && s.on.includes(status);
const colour = hot ? (status === "rejected" ? "var(--danger)" : "var(--accent)") : "var(--fg-muted)";
const x1 = col(s.from);
const x2 = col(s.to);
return (
<g key={i} style={{ color: colour }}>
<text x={(x1 + x2) / 2} y={y - 5} textAnchor="middle" fontSize={8.5} fontFamily="monospace" fill={colour}>
{i + 1}. {s.text}
</text>
<line x1={x1} x2={x2 + (x2 > x1 ? -3 : 3)} y1={y} y2={y} stroke={colour} strokeWidth={hot ? 2 : 1} markerEnd="url(#fu-arrow)" />
</g>
);
})}
</svg>
</section>
);
}components/lab/systems/ui.tsx175 lines
"use client";
/**
* Small shared pieces for the systems/backend lab demos (job queue, webhook relay, uploads, chat).
* Theme-aware controls use tokens; terminal panels are always dark and use fixed colours.
*/
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
export const btn =
"inline-flex items-center justify-center gap-1.5 rounded-md border border-border px-2.5 py-1 font-mono text-xs hover:border-accent hover:text-accent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-border disabled:hover:text-inherit";
export const btnPrimary =
"inline-flex items-center justify-center gap-1.5 rounded-md bg-accent px-3 py-1.5 text-sm font-medium text-accent-fg hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50";
export const btnDanger =
"inline-flex items-center justify-center gap-1.5 rounded-md border border-border px-2.5 py-1 font-mono text-xs hover:border-danger hover:text-danger disabled:opacity-50";
export const input =
"w-full rounded-md border border-border bg-bg-elev px-2.5 py-1.5 text-sm outline-none focus:border-accent";
export const label = "block text-xs font-medium text-fg-muted";
export type Tone = "ok" | "warn" | "err" | "info" | "dim" | "accent";
export type LogLine = { id: number; text: string; tone: Tone };
export const TONE: Record<Tone, string> = {
ok: "text-[#4ade80]",
warn: "text-[#fbbf24]",
err: "text-[#f87171]",
info: "text-[#a1a1aa]",
dim: "text-[#71717a]",
accent: "text-[#60a5fa]",
};
/** Always-dark request/event log. Newest line first. */
export function TerminalLog({
lines,
label: name,
empty = "$ waiting for events…",
className = "h-56",
}: {
lines: LogLine[];
label: string;
empty?: string;
className?: string;
}) {
return (
<ol
role="log"
aria-live="polite"
aria-label={name}
className={`overflow-y-auto rounded-md border border-border bg-[#0b0b0d] p-3 font-mono text-[12px] leading-relaxed ${className}`}
>
{lines.length === 0 && <li className="text-[#71717a]">{empty}</li>}
{lines.map((l) => (
<li key={l.id} className={`break-words ${TONE[l.tone]}`}>
{l.text}
</li>
))}
</ol>
);
}
/** Log buffer with stable ids (no Date.now in render). */
export function useLog(max = 80) {
const [lines, setLines] = useState<LogLine[]>([]);
const seq = useRef(0);
const push = useCallback(
(text: string, tone: Tone = "info") => {
seq.current += 1;
const id = seq.current;
setLines((l) => [{ id, text, tone }, ...l].slice(0, max));
},
[max],
);
const clear = useCallback(() => setLines([]), []);
return { lines, push, clear };
}
/**
* State persisted to localStorage under `key`. The initial render uses `null` (server and first client
* render match); the stored value or the seed is loaded in an effect, so it is hydration-safe.
*/
export function useStoredState<T>(key: string, seed: () => T, isValid: (v: unknown) => boolean = () => true) {
const [state, setState] = useState<T | null>(null);
const seedRef = useRef(seed);
const validRef = useRef(isValid);
useEffect(() => {
let loaded: T | null = null;
try {
const raw = localStorage.getItem(key);
if (raw) {
const parsed: unknown = JSON.parse(raw);
if (validRef.current(parsed)) loaded = parsed as T;
}
} catch {
/* storage blocked or corrupted: fall back to the seed */
}
setState(loaded ?? seedRef.current());
}, [key]);
// Throttled save (at most every 750 ms) so a fast simulation does not write on every tick,
// but a simulation that changes state constantly still gets saved.
const latest = useRef<T | null>(null);
const timer = useRef<number | null>(null);
useEffect(() => {
latest.current = state;
if (state === null || timer.current !== null) return;
timer.current = window.setTimeout(() => {
timer.current = null;
try {
localStorage.setItem(key, JSON.stringify(latest.current));
} catch {
/* quota exceeded or storage blocked: the demo still works in memory */
}
}, 750);
}, [key, state]);
useEffect(
() => () => {
if (timer.current !== null) window.clearTimeout(timer.current);
},
[],
);
const reset = useCallback(() => {
try {
localStorage.removeItem(key);
} catch {
/* ignore */
}
setState(seedRef.current());
}, [key]);
return [state, setState, reset] as const;
}
/** setInterval that always calls the latest callback. `ms = null` stops it. */
export function useInterval(cb: () => void, ms: number | null) {
const ref = useRef(cb);
useEffect(() => {
ref.current = cb;
}, [cb]);
useEffect(() => {
if (ms === null) return;
const id = window.setInterval(() => ref.current(), ms);
return () => window.clearInterval(id);
}, [ms]);
}
/** Section heading used inside demos. */
export function PanelTitle({ children, right }: { children: ReactNode; right?: ReactNode }) {
return (
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
<h3 className="font-mono text-sm font-semibold">{children}</h3>
{right}
</div>
);
}
/** Small status pill with token colours. */
export function Pill({ tone, children }: { tone: "ok" | "warn" | "err" | "muted" | "info"; children: ReactNode }) {
const cls = {
ok: "border-accent/40 bg-accent-soft text-accent",
warn: "border-warn-fg/40 bg-warn-bg text-warn-fg",
err: "border-danger/40 text-danger",
muted: "border-border text-fg-muted",
info: "border-accent-2/40 text-accent-2",
}[tone];
return <span className={`inline-flex items-center rounded-full border px-2 py-0.5 font-mono text-[11px] ${cls}`}>{children}</span>;
}
export function formatBytes(n: number) {
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
return `${(n / 1024 / 1024).toFixed(2)} MB`;
}