Initial Commit
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { Unpacker } from "@suaveplan/packer";
|
||||
import {
|
||||
SERVER_KEYS_CODES,
|
||||
ServerKeysError,
|
||||
} from "../../error-codes/error-codes.js";
|
||||
import { checkSignal, loadFile } from "../keys/keys-io.js";
|
||||
import type { FileCommandOptions, FilesLogger } from "./files.types.js";
|
||||
import { deserializePacket, hexToBytes, readManifest } from "./packet-io.js";
|
||||
|
||||
const SILENT: () => void = () => {};
|
||||
const SILENT_LOGGER: FilesLogger = { info: SILENT, error: SILENT };
|
||||
|
||||
function pickLogger(options: FileCommandOptions | undefined): FilesLogger {
|
||||
return options?.logger ?? SILENT_LOGGER;
|
||||
}
|
||||
|
||||
/** Result envelope for {@link serveFileCommand}. */
|
||||
export interface ServeFileResult {
|
||||
readonly outputPath: string;
|
||||
readonly bytesWritten: number;
|
||||
readonly packetsRead: number;
|
||||
readonly fileId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstruct the original file from the prepared packet directory.
|
||||
* Verifies each packet's signature + checksum against the manifest's
|
||||
* `signerPublicKey` before writing to disk. Output path defaults to
|
||||
* `<packetDir>/<manifest.originalFilename>`.
|
||||
*/
|
||||
export async function serveFileCommand(
|
||||
packetDir: string,
|
||||
options: FileCommandOptions = {}
|
||||
): Promise<ServeFileResult> {
|
||||
if (!packetDir) {
|
||||
throw new ServerKeysError(
|
||||
SERVER_KEYS_CODES.INVALID_INPUT,
|
||||
"packetDir is required"
|
||||
);
|
||||
}
|
||||
checkSignal(options.signal, "serve-file");
|
||||
const manifest = await readManifest(packetDir);
|
||||
const verifyKey = hexToBytes(manifest.signerPublicKey);
|
||||
const reconstructed = new Uint8Array(manifest.originalSize);
|
||||
let writeOffset = 0;
|
||||
let packetsRead = 0;
|
||||
const sorted = [...manifest.packets].sort((a, b) => a.index - b.index);
|
||||
if (sorted.length !== manifest.totalPackets) {
|
||||
throw new ServerKeysError(
|
||||
SERVER_KEYS_CODES.PACKET_INTEGRITY_FAILED,
|
||||
`Manifest declares ${manifest.totalPackets} packets but only ${sorted.length} entries present`
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const record = sorted[i];
|
||||
/* istanbul ignore start -- defensive: `sorted` is derived from manifest.packets via Array.sort, which preserves length and never introduces sparse holes. */
|
||||
if (!record) continue;
|
||||
/* istanbul ignore stop */
|
||||
if (record.index !== i) {
|
||||
throw new ServerKeysError(
|
||||
SERVER_KEYS_CODES.PACKET_INTEGRITY_FAILED,
|
||||
`Packet sequence broken at index ${i}: manifest entry has index=${record.index}`
|
||||
);
|
||||
}
|
||||
checkSignal(options.signal, "serve-file");
|
||||
const buf = await loadFile(path.join(packetDir, record.filename));
|
||||
const { packed } = deserializePacket(buf);
|
||||
const unpacker = new Unpacker();
|
||||
let result: Awaited<ReturnType<typeof unpacker.unpack>>;
|
||||
try {
|
||||
result = await unpacker.unpack(packed, {
|
||||
verifySignature: true,
|
||||
verifyKey,
|
||||
verifyChecksum: true,
|
||||
});
|
||||
} catch (err) {
|
||||
let extras: {
|
||||
cause?: Error;
|
||||
context: { index: number };
|
||||
} = { context: { index: record.index } };
|
||||
/* istanbul ignore start -- defensive: Unpacker.unpack rejects with Error subclasses on every documented failure mode, so the else branch is unreachable. */
|
||||
if (err instanceof Error) {
|
||||
extras = { cause: err, context: { index: record.index } };
|
||||
}
|
||||
/* istanbul ignore stop */
|
||||
throw new ServerKeysError(
|
||||
SERVER_KEYS_CODES.PACKET_INTEGRITY_FAILED,
|
||||
`Packet ${record.index} (${record.filename}) failed verification`,
|
||||
extras
|
||||
);
|
||||
}
|
||||
if (result.data.byteLength !== record.originalSize) {
|
||||
throw new ServerKeysError(
|
||||
SERVER_KEYS_CODES.PACKET_INTEGRITY_FAILED,
|
||||
`Packet ${record.index} size mismatch: expected ${record.originalSize} got ${result.data.byteLength}`
|
||||
);
|
||||
}
|
||||
reconstructed.set(result.data, writeOffset);
|
||||
writeOffset += result.data.byteLength;
|
||||
packetsRead++;
|
||||
}
|
||||
if (writeOffset !== manifest.originalSize) {
|
||||
throw new ServerKeysError(
|
||||
SERVER_KEYS_CODES.PACKET_INTEGRITY_FAILED,
|
||||
`Reassembled size ${writeOffset} does not match manifest.originalSize ${manifest.originalSize}`
|
||||
);
|
||||
}
|
||||
const outputPath =
|
||||
options.output ?? path.join(packetDir, manifest.originalFilename);
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await fs.writeFile(outputPath, reconstructed);
|
||||
pickLogger(options).info(
|
||||
`serve-file: fileId=${manifest.fileId} packets=${packetsRead} bytes=${writeOffset} out=${outputPath}`
|
||||
);
|
||||
return {
|
||||
outputPath,
|
||||
bytesWritten: writeOffset,
|
||||
packetsRead,
|
||||
fileId: manifest.fileId,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user