web-print-service
Version:
Print a website service
268 lines (243 loc) • 8.71 kB
JavaScript
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 };