Skip to content

Instantly share code, notes, and snippets.

@peterc
Last active May 30, 2026 19:07
Show Gist options
  • Select an option

  • Save peterc/e971b5de717654373a88db93c240109f to your computer and use it in GitHub Desktop.

Select an option

Save peterc/e971b5de717654373a88db93c240109f to your computer and use it in GitHub Desktop.
A temporary file dropping/sharing service
// drop
//
// A dependency-free Bun web app for short-lived file sharing. It serves a
// drag-and-drop page, accepts SVG, PNG, PDF, GIF, WEBP, and JPEG uploads after
// detecting the file type from contents, stores each upload in /tmp/images under
// a random UUID filename, and deletes stored files after 5 minutes. The storage
// directory is capped at 1 GB by deleting the oldest files first, and is kept at
// 0700 so only the app's Unix user can inspect stored files directly.
//
// Run:
// bun run drop.ts
//
// Options:
// --port PORT Listening port. Defaults to 8000.
// --host HOST Listening host. Defaults to Bun's default host.
// --public-url URL Public base URL used in upload responses, for example
// https://drop.example.com. Defaults to deriving the base
// URL from the incoming request's scheme and Host header.
// --help, -h Print usage and exit.
//
// Example:
// bun run drop.ts -- --port 9000 --host 0.0.0.0 --public-url https://drop.example.com
//
// Assumes an upstream proxy always sets X-Forwarded-For; rate limiting trusts
// its leftmost value as the client IP. Direct (un-proxied) exposure would let
// clients spoof XFF to bypass the limit.
import { chmod, mkdir, readdir, stat, unlink } from "node:fs/promises";
import { existsSync } from "node:fs";
import { extname, join, resolve, sep } from "node:path";
const DIR = "/tmp/images";
const TTL_MS = 5 * 60 * 1000;
const DEFAULT_PORT = 8000;
const DEFAULT_HOST = undefined;
const DEFAULT_PUBLIC_URL = undefined;
const MAX_BYTES = 8 * 1024 * 1024;
const MAX_DIR_BYTES = 1024 * 1024 * 1024;
const MAGIC_BYTES = 4096;
function usage(message?: string): never {
if (message) console.error(message);
console.error("usage: bun run drop.ts [--port PORT] [--host HOST] [--public-url URL]");
process.exit(message ? 1 : 0);
}
function parsePublicUrl(value: string): string {
let url: URL;
try { url = new URL(value); }
catch { usage("--public-url must be an absolute http(s) URL"); }
if (url.protocol !== "http:" && url.protocol !== "https:") {
usage("--public-url must use http or https");
}
url.pathname = url.pathname.replace(/\/+$/, "");
url.search = "";
url.hash = "";
return url.toString().replace(/\/$/, "");
}
function parseArgs(args: string[]): { port: number; host?: string; publicUrl?: string } {
let port = DEFAULT_PORT;
let host = DEFAULT_HOST;
let publicUrl = DEFAULT_PUBLIC_URL;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "--help" || arg === "-h") usage();
if (arg === "--port") {
const value = args[++i];
if (!value) usage("--port requires a value");
port = Number(value);
if (!Number.isInteger(port) || port < 1 || port > 65535) usage("--port must be an integer from 1 to 65535");
continue;
}
if (arg === "--host") {
host = args[++i];
if (!host) usage("--host requires a value");
continue;
}
if (arg === "--public-url") {
const value = args[++i];
if (!value) usage("--public-url requires a value");
publicUrl = parsePublicUrl(value);
continue;
}
usage(`unknown argument: ${arg}`);
}
return { port, host, publicUrl };
}
const { port: PORT, host: HOST, publicUrl: PUBLIC_URL } = parseArgs(Bun.argv.slice(2));
if (!existsSync(DIR)) await mkdir(DIR, { recursive: true, mode: 0o700 });
await chmod(DIR, 0o700);
function scheduleUnlink(path: string, atMs: number) {
const delay = Math.max(0, atMs - Date.now());
setTimeout(() => { unlink(path).catch(() => {}); }, delay);
}
async function storedFiles() {
const files: { path: string; size: number; mtimeMs: number }[] = [];
for (const name of await readdir(DIR).catch(() => [])) {
const path = join(DIR, name);
const s = await stat(path).catch(() => null);
if (s?.isFile()) files.push({ path, size: s.size, mtimeMs: s.mtimeMs });
}
return files;
}
async function makeRoomFor(bytes: number) {
let files = await storedFiles();
let total = files.reduce((sum, file) => sum + file.size, 0);
if (total + bytes <= MAX_DIR_BYTES) return;
files = files.sort((a, b) => a.mtimeMs - b.mtimeMs);
for (const file of files) {
await unlink(file.path).catch(() => {});
total -= file.size;
if (total + bytes <= MAX_DIR_BYTES) return;
}
}
await makeRoomFor(0);
for (const name of await readdir(DIR).catch(() => [])) {
const full = join(DIR, name);
const s = await stat(full).catch(() => null);
if (s?.isFile()) scheduleUnlink(full, s.mtimeMs + TTL_MS);
}
const html = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>drop</title>
<style>
html,body{margin:0;height:100%}
body{display:flex;align-items:center;justify-content:center;font-family:system-ui,sans-serif;background:#111;color:#bbb;transition:background .1s}
body.drag{background:#1a2a3a;color:#fff}
.hint{font-size:1.2rem;opacity:.7;pointer-events:none}
#log{position:fixed;bottom:12px;left:12px;right:12px;font-family:ui-monospace,monospace;font-size:.85rem;color:#7af;word-break:break-all}
</style>
</head>
<body>
<div class="hint">drop SVG, PNG, PDF, GIF, WEBP, or JPEG anywhere</div>
<div id="log"></div>
<script>
const log = document.getElementById('log');
let depth = 0;
addEventListener('dragenter', e => { e.preventDefault(); if (++depth) document.body.classList.add('drag'); });
addEventListener('dragover', e => e.preventDefault());
addEventListener('dragleave', e => { if (--depth <= 0) { depth = 0; document.body.classList.remove('drag'); } });
addEventListener('drop', async e => {
e.preventDefault();
depth = 0;
document.body.classList.remove('drag');
for (const f of e.dataTransfer.files) {
if (f.size > 8 * 1024 * 1024) { log.textContent = 'too big (' + (f.size/1048576).toFixed(1) + ' MB, max 8 MB): ' + f.name; continue; }
const fd = new FormData();
fd.append('image', f);
log.textContent = 'uploading ' + f.name + '…';
const res = await fetch('/upload', { method: 'POST', body: fd });
if (!res.ok) { log.textContent = 'upload failed: ' + res.status + ' ' + await res.text(); continue; }
const url = await res.text();
try { await navigator.clipboard.writeText(url); log.textContent = 'copied: ' + url; }
catch { log.textContent = url + ' (clipboard blocked — copy manually)'; }
}
});
</script>
</body>
</html>`;
const RATE_WINDOW_MS = 60 * 1000;
const RATE_MAX = 5;
const hits = new Map<string, number[]>();
function clientIP(req: Request, server: { requestIP(r: Request): { address: string } | null }): string {
const xff = req.headers.get("x-forwarded-for");
if (xff) return xff.split(",")[0].trim();
return server.requestIP(req)?.address ?? "unknown";
}
function checkRate(ip: string): number {
const now = Date.now();
const arr = (hits.get(ip) ?? []).filter(t => now - t < RATE_WINDOW_MS);
if (arr.length >= RATE_MAX) {
hits.set(ip, arr);
return Math.ceil((RATE_WINDOW_MS - (now - arr[0])) / 1000);
}
arr.push(now);
hits.set(ip, arr);
return 0;
}
setInterval(() => {
const now = Date.now();
for (const [ip, arr] of hits) {
const kept = arr.filter(t => now - t < RATE_WINDOW_MS);
if (kept.length === 0) hits.delete(ip);
else if (kept.length !== arr.length) hits.set(ip, kept);
}
}, RATE_WINDOW_MS).unref?.();
type FileType = { ext: string; contentType: string };
const FILE_TYPES = {
gif: { ext: ".gif", contentType: "image/gif" },
jpg: { ext: ".jpg", contentType: "image/jpeg" },
pdf: { ext: ".pdf", contentType: "application/pdf" },
png: { ext: ".png", contentType: "image/png" },
svg: { ext: ".svg", contentType: "image/svg+xml; charset=utf-8" },
webp: { ext: ".webp", contentType: "image/webp" },
} as const satisfies Record<string, FileType>;
function ascii(bytes: Uint8Array, start: number, text: string): boolean {
if (bytes.length < start + text.length) return false;
for (let i = 0; i < text.length; i++) {
if (bytes[start + i] !== text.charCodeAt(i)) return false;
}
return true;
}
function looksLikeSvg(bytes: Uint8Array): boolean {
let text = new TextDecoder("utf-8").decode(bytes).replace(/^\ufeff/, "").trimStart().toLowerCase();
if (text.startsWith("<?xml")) {
const end = text.indexOf("?>");
if (end === -1) return false;
text = text.slice(end + 2).trimStart();
}
while (text.startsWith("<!--")) {
const end = text.indexOf("-->");
if (end === -1) return false;
text = text.slice(end + 3).trimStart();
}
if (text.startsWith("<!doctype")) {
if (!/^<!doctype\s+svg(?:\s|>)/.test(text)) return false;
const end = text.indexOf(">");
if (end === -1) return false;
text = text.slice(end + 1).trimStart();
}
return text.startsWith("<svg");
}
async function detectFileType(file: File): Promise<FileType | null> {
const bytes = new Uint8Array(await file.slice(0, MAGIC_BYTES).arrayBuffer());
if (
bytes.length >= 8 &&
bytes[0] === 0x89 &&
bytes[1] === 0x50 &&
bytes[2] === 0x4e &&
bytes[3] === 0x47 &&
bytes[4] === 0x0d &&
bytes[5] === 0x0a &&
bytes[6] === 0x1a &&
bytes[7] === 0x0a
) return FILE_TYPES.png;
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
return FILE_TYPES.jpg;
}
if (ascii(bytes, 0, "GIF87a") || ascii(bytes, 0, "GIF89a")) return FILE_TYPES.gif;
if (ascii(bytes, 0, "%PDF-")) return FILE_TYPES.pdf;
if (ascii(bytes, 0, "RIFF") && ascii(bytes, 8, "WEBP")) return FILE_TYPES.webp;
if (looksLikeSvg(bytes)) return FILE_TYPES.svg;
return null;
}
function contentTypeForName(name: string): string {
switch (extname(name).toLowerCase()) {
case ".gif": return FILE_TYPES.gif.contentType;
case ".jpg":
case ".jpeg": return FILE_TYPES.jpg.contentType;
case ".pdf": return FILE_TYPES.pdf.contentType;
case ".png": return FILE_TYPES.png.contentType;
case ".svg": return FILE_TYPES.svg.contentType;
case ".webp": return FILE_TYPES.webp.contentType;
default: return "application/octet-stream";
}
}
const FILE_HEADERS = {
"x-content-type-options": "nosniff",
"cache-control": "no-store",
"content-security-policy": "default-src 'none'; sandbox",
};
Bun.serve({
port: PORT,
hostname: HOST,
idleTimeout: 60,
maxRequestBodySize: MAX_BYTES + 64 * 1024,
async fetch(req, server) {
const url = new URL(req.url);
if (req.method === "GET" && url.pathname === "/") {
return new Response(html, {
headers: {
"content-type": "text/html; charset=utf-8",
"cache-control": "no-store",
},
});
}
if (req.method === "POST" && url.pathname === "/upload") {
const ip = clientIP(req, server);
const retry = checkRate(ip);
if (retry > 0) {
return new Response(`rate limit: try again in ${retry}s`, {
status: 429,
headers: { "retry-after": String(retry) },
});
}
let form;
try { form = await req.formData(); }
catch { return new Response("too large or malformed", { status: 413 }); }
const file = form.get("image");
if (!(file instanceof File)) return new Response("no file", { status: 400 });
if (file.size > MAX_BYTES) return new Response("too large", { status: 413 });
const type = await detectFileType(file);
if (!type) return new Response("unsupported file type", { status: 415 });
await makeRoomFor(file.size);
const name = crypto.randomUUID() + type.ext;
const path = join(DIR, name);
await Bun.write(path, file);
await makeRoomFor(0);
scheduleUnlink(path, Date.now() + TTL_MS);
const publicUrl = `${PUBLIC_URL ?? `${url.protocol}//${url.host}`}/i/${name}`;
return new Response(publicUrl, { headers: { "content-type": "text/plain" } });
}
if (req.method === "GET" && url.pathname.startsWith("/i/")) {
let name: string;
try { name = decodeURIComponent(url.pathname.slice(3)); }
catch { return new Response("bad name", { status: 400 }); }
if (!name || name.includes("/") || name.includes("\\") || name.includes("\0") || name === "." || name === "..") {
return new Response("bad name", { status: 400 });
}
const full = resolve(DIR, name);
if (full !== join(DIR, name) || !full.startsWith(DIR + sep)) {
return new Response("bad path", { status: 400 });
}
const f = Bun.file(full);
if (!(await f.exists())) return new Response("not found", { status: 404 });
return new Response(f, { headers: { ...FILE_HEADERS, "content-type": contentTypeForName(name) } });
}
return new Response("not found", { status: 404 });
},
});
console.log(`drop server on http://${HOST ?? "localhost"}:${PORT}${DIR} (TTL ${TTL_MS / 1000}s)`);
console.log(`public URL: ${PUBLIC_URL ?? "derived from request"}`);
@peterc

peterc commented May 30, 2026

Copy link
Copy Markdown
Author

This is handy for running on exe.dev VMs (or locally and exposed via Tailscale/similar) for quick image sharing with remote agents.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment