import puppeteer from 'puppeteer'; import type { Browser } from 'puppeteer'; import csvToJson from 'csvtojson'; import { createQuestLayout, createGangLayout, GangLayoutType } from '$lib/qrcode'; import type { QuestLayoutOptions } from '$lib/qrcode'; import { convertSvgsToPdf } from '$lib/util/svg-to-pdf'; import { promises as fs } from 'fs'; import Crypto from 'crypto'; import Path from 'path'; import * as time from './time'; import { dev } from '$app/environment'; import type { PdfFileResult } from './types'; import child from 'child_process'; const DELETE_TIMEOUT = dev ? time.ONE_MINUTE : time.ONE_HOUR; const tasks = new Map(); export function addTask(files: File[]): string { const task = new Task(files); tasks.set(task.id, task); return task.id; } export function getTask(taskId: string) { return tasks.get(taskId); } type Subtask = () => Promise; class Task { id: string; status: 'pending' | 'complete' | 'error'; files: File[]; results: PdfFileResult[] = []; subtasks: Subtask[] = []; start = 0; finish = 0; browser: Promise; taskDirectory: string; constructor(files: File[]) { this.start = Date.now(); this.id = Crypto.randomUUID(); this.taskDirectory = Path.join('static', 'pdfs', this.id); this.status = 'pending'; this.files = files; this.browser = getBrowser(); this.subtasks = this.files.map((file) => this.fileToSubtask(file)); this.run(); } fileToSubtask(file: File): Subtask { return async () => { const browser = await this.browser; return await getPdfForCsvFile(this.taskDirectory, file, browser); }; } async run() { console.log('Task started:', this.id); await fs.mkdir(this.taskDirectory, { recursive: true }); this.runTasks(); } async runTasks() { const nextTask = this.subtasks.shift(); if (nextTask) { await this.runTask(nextTask); } else { this.complete(); } } async runTask(subtask: Subtask) { try { const result = await subtask(); this.results.push(result); } catch (error) { console.error(error); } finally { this.runTasks(); } } get duration() { if (this.status === 'pending') { return Date.now() - this.start; } else { return this.finish - this.start; } } async complete() { this.finish = Date.now(); this.status = 'complete'; console.log('Task complete:', this.id); console.log(this.results.map((r) => r.filename).join('\n')); setTimeout(() => this.cleanup(), DELETE_TIMEOUT); const browser = await this.browser; await browser.close(); } async cleanup() { await fs.rm(this.taskDirectory, { recursive: true, force: true }); tasks.delete(this.id); console.log('Tasks in memory:', tasks.size); } } async function getPdfForCsvFile(taskDirectory: string, file: File, browser: Browser): Promise { console.time(`Generated PDF for ${file.name}...`); const parsedFile = Path.parse(file.name); const folderName = parsedFile.name; const outputPath = Path.join(taskDirectory, folderName); await fs.mkdir(outputPath, { recursive: true }); const csv = await file.text(); const json = await csvToJson().fromString(csv); const labels = json.map(toQuestLayoutOptions).map(createQuestLayout); const layouts = createGangLayout({ layout: GangLayoutType.AveryPresta61520, svgs: labels }); await convertSvgsToPdf({ svgs: layouts, browser, outputPath }); const filename = `${folderName}.zip`; const filepath = Path.join(taskDirectory, filename); const href = filepath.replace(/^static/, ''); await zipDirectory(taskDirectory, folderName, filename); console.timeEnd(`Generated PDF for ${file.name}...`); return { filename, filepath, href }; } async function zipDirectory(parent: string, directory: string, filename: string) { return new Promise((resolve, reject) => { const zip = child.spawn('zip', ['-r', filename, directory], { cwd: parent }); zip.on('close', (code) => { if (code === 0) { resolve(void 0); } else { reject(new Error(`zip process exited with code ${code}`)); } }); }); } // CSV data is expected to have these headers interface CsvRecord { Manufacturer: string; Model: string; 'Serial Number': string; 'Coast ID': string; 'Acquired Year': string; 'Asset Details URL': string; 'Service Request URL': string; } function toQuestLayoutOptions(item: CsvRecord): QuestLayoutOptions { return { width: 375, assetUrl: item['Asset Details URL'], serviceRequestUrl: item['Service Request URL'], manufacturer: item['Manufacturer'], model: item['Model'], serialNumber: item['Serial Number'], coastId: item['Coast ID'], yearAcquired: item['Acquired Year'], }; } async function getBrowser() { return await puppeteer.launch({ args: ['--no-sandbox'], headless: true, executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, }); }