73 lines
2.8 KiB
TypeScript
73 lines
2.8 KiB
TypeScript
|
|
import assert from "node:assert/strict";
|
||
|
|
import { createHash, randomBytes } from "node:crypto";
|
||
|
|
import { mkdtempSync } from "node:fs";
|
||
|
|
import { readFile, readdir, stat } from "node:fs/promises";
|
||
|
|
import { tmpdir } from "node:os";
|
||
|
|
import { join } from "node:path";
|
||
|
|
import { Readable } from "node:stream";
|
||
|
|
import { test } from "node:test";
|
||
|
|
|
||
|
|
import { UploadDir, hashOf, isValidObjectName } from "../src/upload.ts";
|
||
|
|
|
||
|
|
test("object name validation", () => {
|
||
|
|
const hash = "a".repeat(64);
|
||
|
|
assert.ok(isValidObjectName(hash));
|
||
|
|
assert.ok(isValidObjectName(`${hash}.mp4`));
|
||
|
|
assert.ok(isValidObjectName(`${hash}.webm`));
|
||
|
|
assert.ok(!isValidObjectName(`${hash}.`));
|
||
|
|
assert.ok(!isValidObjectName(`${hash}.MP4`), "uppercase extension");
|
||
|
|
assert.ok(!isValidObjectName(`${hash}.m p4`), "space in extension");
|
||
|
|
assert.ok(!isValidObjectName(`../../etc/passwd`), "traversal");
|
||
|
|
assert.ok(!isValidObjectName(`${"a".repeat(63)}.mp4`), "short hash");
|
||
|
|
assert.ok(!isValidObjectName(`${hash}.${"x".repeat(17)}`), "long extension");
|
||
|
|
assert.equal(hashOf(`${hash}.mp4`), hash);
|
||
|
|
});
|
||
|
|
|
||
|
|
function payload(bytes: Buffer): { name: string; body: Readable } {
|
||
|
|
const digest = createHash("sha256").update(bytes).digest("hex");
|
||
|
|
return { name: `${digest}.bin`, body: Readable.from([bytes]) };
|
||
|
|
}
|
||
|
|
|
||
|
|
test("receive stores a verified upload and is idempotent", async () => {
|
||
|
|
const dir = mkdtempSync(join(tmpdir(), "uploads-"));
|
||
|
|
const uploads = new UploadDir(dir, 1024 * 1024);
|
||
|
|
await uploads.init();
|
||
|
|
|
||
|
|
const bytes = randomBytes(4096);
|
||
|
|
const { name, body } = payload(bytes);
|
||
|
|
|
||
|
|
assert.deepEqual(await uploads.receive(name, body), { status: "created" });
|
||
|
|
assert.deepEqual(await readFile(join(dir, name)), bytes);
|
||
|
|
assert.ok(await uploads.exists(name));
|
||
|
|
|
||
|
|
const again = await uploads.receive(name, Readable.from([bytes]));
|
||
|
|
assert.deepEqual(again, { status: "exists" });
|
||
|
|
|
||
|
|
// The temp dir holds no leftovers either way.
|
||
|
|
assert.deepEqual(await readdir(join(dir, ".internaluploader-tmp")), []);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("receive rejects a body that does not match its name", async () => {
|
||
|
|
const dir = mkdtempSync(join(tmpdir(), "uploads-"));
|
||
|
|
const uploads = new UploadDir(dir, 1024 * 1024);
|
||
|
|
await uploads.init();
|
||
|
|
|
||
|
|
const name = `${"0".repeat(64)}.bin`;
|
||
|
|
const outcome = await uploads.receive(name, Readable.from([randomBytes(16)]));
|
||
|
|
assert.equal(outcome.status, "hash-mismatch");
|
||
|
|
await assert.rejects(stat(join(dir, name)));
|
||
|
|
assert.deepEqual(await readdir(join(dir, ".internaluploader-tmp")), []);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("receive enforces the size ceiling", async () => {
|
||
|
|
const dir = mkdtempSync(join(tmpdir(), "uploads-"));
|
||
|
|
const uploads = new UploadDir(dir, 1000);
|
||
|
|
await uploads.init();
|
||
|
|
|
||
|
|
const bytes = randomBytes(4096);
|
||
|
|
const { name, body } = payload(bytes);
|
||
|
|
const outcome = await uploads.receive(name, body);
|
||
|
|
assert.equal(outcome.status, "too-large");
|
||
|
|
assert.deepEqual(await readdir(join(dir, ".internaluploader-tmp")), []);
|
||
|
|
});
|