/** * thread-keeper * @module utils.SuccessLog * @author The Harvard Library Innovation Lab * @license MIT */ import fs from "fs"; import readline from "node:readline"; import crypto from "crypto"; import { DATA_PATH } from "../const.js"; /** * Utility class for handling success logs. Keeps trace of the hashes of the PDFs that were generated. */ export class SuccessLog { /** * Complete path to `success-log.json`. * @type {string} */ static filepath = `${DATA_PATH}success-log.tsv`; /** * Hashmap of all the sha512 hashes present in the current log file. * Used for fast lookups. * * @type {object.} */ #hashes = {}; /** * On init: * - Create log file if it doesn't exist * - Load hashes from file into `this.#hashes`. */ constructor() { const filepath = SuccessLog.filepath; // Create file if it does not exist if (!fs.existsSync(filepath)) { this.reset(); } // Load hashes from existing file into hashmap (asynchronous) const readLogs = readline.createInterface({ input: fs.createReadStream(filepath), crlfDelay: Infinity }); readLogs.on("line", (line) => { // Skip lines that are not log lines if (line[0] === "d" || line[0] === "\n") { return; } // Grab last 95 chars of line, check it's a sha512 hash, add to #hashes. const lineLength = line.length; const hash = line.substring(lineLength - 95); if (hash.length === 95 && hash.startsWith("sha512-")) { this.#hashes[hash] = true; } }); } /** * Calculates hash of a PDF an: * - Creates a success log entry * - Updates `this.#hashes` (so it doesn't need to reload from file) * * @param {string} identifier - Can be an IP or access key * @param {string} why - Reason for creating this archive * @param {Buffer} pdfBytes - Used to store a SHA512 hash of the PDF that was delivered */ add(identifier, why, pdfBytes) { // Calculate SHA512 hash of the PDF const hash = crypto.createHash('sha512').update(pdfBytes).digest('base64'); // "why" field sanitization why = why .replaceAll("\t", " ") .replaceAll("\n", " ") .replaceAll("<", "") .replaceAll(">", "") .replaceAll("{", "") .replaceAll("}", ""); // Save entry const entry = `${new Date().toISOString()}\t${identifier}\t${why}\tsha512-${hash}\n`; fs.appendFileSync(SuccessLog.filepath, entry); this.#hashes[`sha512-${hash}`] = true; } /** * Checks whether or not a given hash is present in the logs. * @param {string} hash * @returns {boolean} */ findHashInLogs(hash) { hash = String(hash); // Compensate for the absence of "sha512-" if (hash.length === 88) { hash = `sha512-${hash}`; } if (hash.length < 95) { return false; } return hash in this.#hashes && this.#hashes[hash] === true; } /** * Resets `success-log.json`. * Also clears `this.#hashes`. * @returns {void} */ reset() { fs.writeFileSync(SuccessLog.filepath, "date-time\tidentifier\twhy\thash\n"); this.#hashes = {}; } }