UNPKG

web-print-service

Version:

Print a website service

268 lines (243 loc) 8.71 kB
const express = require("express"); const bodyParser = require("body-parser"); const puppeteer = require("puppeteer"); const fs = require("fs"); const os = require("os"); const ptp = os.platform() === "win32" ? require("pdf-to-printer") : require("unix-print"); const cron = require("node-cron"); const validator = require("validator"); // Puppeteer configuration const MAX_PUPPETEER_INSTANCES = 5; let browser; const pageQueue = []; // Hàng đợi xử lý yêu cầu const taskQueue = []; let isProcessing = false; // Worker handler function const processTaskQueue = async () => { if (isProcessing || taskQueue.length === 0) return; isProcessing = true; const task = taskQueue.shift(); try { await task(); } catch (error) { console.error("Task processing error:", error.message); } finally { isProcessing = false; processTaskQueue(); // Tiếp tục xử lý hàng đợi } }; // Thêm yêu cầu vào hàng đợi const enqueueTask = (task, timeout = 30000) => { return new Promise((resolve, reject) => { const timeoutHandler = setTimeout(() => { reject(new Error("Task timed out")); }, timeout); taskQueue.push(async () => { clearTimeout(timeoutHandler); // Hủy timeout khi task bắt đầu try { await task(); resolve(); } catch (error) { reject(error); } finally { processTaskQueue(); // Tiếp tục xử lý task tiếp theo } }); processTaskQueue(); }); }; //Check url const validURL = (url) => { return validator.isURL(url) }; // Initialize Puppeteer const getBrowserInstance = async () => { if (!browser) { console.log("launch new browser..."); browser = await puppeteer.launch({ ignoreHTTPSErrors: true, args: ['--disable-gpu', '--no-sandbox', '--hide-scrollbars'], }); } return browser; }; // Get or create a Puppeteer page const getPage = async () => { if (pageQueue.length > 0) { return pageQueue.shift(); // Reuse existing page } const browser = await getBrowserInstance(); console.log("launch new page..."); const page = await browser.newPage(); return page; }; // Return page to queue const releasePage = (page) => { if (pageQueue.length < MAX_PUPPETEER_INSTANCES) { pageQueue.push(page); } else { page.close(); // Close page if max instances exceeded } }; // Get default printer or specified printer const getPrinter = async (printerName) => { if (printerName) { return printerName; } const printer = await ptp.getDefaultPrinter(); /*if (!printer || printer.status === "offline") { throw new Error("Default printer is offline or unavailable"); }*/ if (!printer) { throw new Error("Default printer is unavailable"); } return printer.printer || printer.name || printer; }; // Save HTML content or URL as PDF const savePDF = async (page, options) => { const { htmlContent, url, path, width } = options; if (htmlContent) { await page.setContent(htmlContent); } else if (url) { if(!validURL(url)){ throw new Error("Invalid URL"); } await page.goto(url, { waitUntil: "networkidle0" }); } else { throw new Error("No HTML content or URL provided"); } await page.pdf({ path, printBackground: true, width: width ? `${width}px` : undefined, }); }; // Print PDF const printPDF = async (filePath, printer, options) => { const printOptions = os.platform() === "win32" ? { printer, scale: "fit", orientation: options.landscape ? "90" : undefined, paperSize: options.page_size, } : ["-o fit-to-page", ...(options.page_size ? [`-o media=${options.page_size}`] : [])]; if (os.platform() === "win32") { await ptp.print(filePath, printOptions); } else { await ptp.print(filePath, printer, printOptions); } }; // Handle printing request const handlePrintRequest = async (req, res) => { try { await enqueueTask(async () => { const {html_content: htmlContent } = req.body || {}; const url = req.query.url; const printerName = req.query.printer; const width = req.query.width; const options = { url, htmlContent, width }; if (!htmlContent && !url) { return res.status(400).send("HTML content or URL is required"); } try { const printer = await getPrinter(printerName); const page = await getPage(); const reportDir = `${__dirname}/reports`; if (!fs.existsSync(reportDir)) { fs.mkdirSync(reportDir); } const filePath = `${reportDir}/rp-${Date.now()}.pdf`; await savePDF(page, { ...options, path: filePath }); releasePage(page); // Return page to pool await printPDF(filePath, printer, req.query); res.send("Printed successfully"); } catch (error) { console.error("Print error:", error.message); res.status(500).send(error.message); } }, 60000); // 60 giây timeout } catch (error) { if (error.message === "Task timed out") { res.status(408).send("Request Timeout: Task took too long to process"); } else { res.status(500).send(error.message); } } }; // Schedule cleanup for old files cron.schedule("0 0 * * *", () => { const dir = `${__dirname}/reports`; fs.readdir(dir, (err, files) => { if (err) return console.error("Cleanup error:", err); files.forEach((file) => { const filePath = `${dir}/${file}`; const stats = fs.statSync(filePath); const age = Date.now() - stats.mtimeMs; if (age > 7 * 24 * 60 * 60 * 1000) { fs.unlink(filePath, (err) => { if (err) console.error("File delete error:", err); }); } }); }); }); // API Endpoints const app = express(); app.use(bodyParser.json({ limit: "5mb" })); app.use(bodyParser.urlencoded({ limit: "5mb", extended: true })); //allow cross domain app.options('/*', function(req, res) { res.header('Access-Control-Allow-Origin', req.headers.origin || '*'); res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,HEAD,DELETE,OPTIONS'); res.header('Access-Control-Allow-Headers', 'content-Type,x-requested-with,X-Access-Token,Authorization,Content-Encoding,Accept-Encoding'); res.sendStatus(200); }); app.use(function(req, res, next) { res.header('Access-Control-Allow-Origin', req.headers.origin || '*'); res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,HEAD,DELETE,OPTIONS'); res.header('Access-Control-Allow-Headers', 'content-Type,x-requested-with,X-Access-Token,Authorization,Content-Encoding,Accept-Encoding'); next(); }); let packageInfo = JSON.parse(fs.readFileSync(__dirname + "/package.json",'utf8')); app.get("/", (req, res) => res.send(`Print Service ${packageInfo.version} is running`)); app.get("/printers", (req,res)=>{ ptp.getPrinters().then(ps=>{ let html =`<html> <title>Print service</title> <body> <h3>Printers</h3> <ul> ${ps.map(p=>{ return `<li>${p.printer|| p.name||JSON.stringify(p)} - status: ${p.status}</li>` })} </ul> </body> </html>` res.send(html); }).catch(e=>{ res.status(500).send("Can't get printers"); }) }); app.post("/html-print", handlePrintRequest); app.get("/web-print", handlePrintRequest); // Start server const start =(port=8081)=>{ const cluster = require('cluster'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); // Fork workers. for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log(`worker ${worker.process.pid} died. Restarting....`); }); } else { console.log(`Worker ${process.pid} started`); app.listen(port, () => console.log(`Print service is running at http://localhost:${port}`)) } } module.exports = { start };