import { createHash, randomUUID } from "node:crypto"; import { createWriteStream } from "node:fs"; import { mkdir, rename, stat, unlink } from "node:fs/promises"; import { join } from "node:path"; import type { Readable } from "node:stream"; /** * `<64 hex sha256>` plus an optional short, lowercase, alphanumeric extension. * Everything else is rejected before any filesystem call — the name is the only * client-controlled thing that ever reaches a path. */ const NAME_PATTERN = /^[0-9a-f]{64}(\.[a-z0-9]{1,16})?$/; export function isValidObjectName(name: string): boolean { return NAME_PATTERN.test(name); } export function hashOf(name: string): string { return name.slice(0, 64); } export type UploadOutcome = | { status: "created" } | { status: "exists" } | { status: "hash-mismatch"; actual: string } | { status: "too-large" }; export class UploadDir { private readonly downloadsDir: string; private readonly maxBytes: number; private readonly tmpDir: string; // No parameter properties: `node --test` runs this file under strip-only // type stripping, which rejects them. constructor(downloadsDir: string, maxBytes: number) { this.downloadsDir = downloadsDir; this.maxBytes = maxBytes; // Same filesystem as the destination, or the final rename stops being // atomic and a half-written file could be seen with its final name. this.tmpDir = join(downloadsDir, ".internaluploader-tmp"); } async init(): Promise { await mkdir(this.tmpDir, { recursive: true }); } async exists(name: string): Promise { try { await stat(join(this.downloadsDir, name)); return true; } catch { return false; } } /** * Stream a request body to disk, verify its sha256 matches the declared * name, and only then rename it into the downloads directory. Idempotent: * a name that already exists is reported without touching anything. */ async receive(name: string, body: Readable): Promise { if (await this.exists(name)) { return { status: "exists" }; } const tmpPath = join(this.tmpDir, randomUUID()); const hash = createHash("sha256"); let received = 0; try { await new Promise((resolve, reject) => { const out = createWriteStream(tmpPath, { flags: "wx", mode: 0o644 }); body.on("data", (chunk: Buffer) => { received += chunk.length; if (received > this.maxBytes) { body.destroy(); out.destroy(); reject(new TooLargeError()); return; } hash.update(chunk); }); body.on("error", reject); out.on("error", reject); out.on("finish", resolve); body.pipe(out); }); const actual = hash.digest("hex"); if (actual !== hashOf(name)) { await unlink(tmpPath); return { status: "hash-mismatch", actual }; } // Lost race with a concurrent identical upload: same content, so the // file already on disk is exactly what this request carried. if (await this.exists(name)) { await unlink(tmpPath); return { status: "exists" }; } await rename(tmpPath, join(this.downloadsDir, name)); return { status: "created" }; } catch (error) { await unlink(tmpPath).catch(() => {}); if (error instanceof TooLargeError) { return { status: "too-large" }; } throw error; } } } class TooLargeError extends Error { constructor() { super("upload exceeds the configured maximum"); } }