web-print-service
Version:
Print a website service
300 lines (289 loc) • 10.1 kB
JavaScript
const express = require("express");
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json({limit: '5mb'}));
app.use(bodyParser.urlencoded({limit: '5mb',extended: true}));
const puppeteer = require('puppeteer');
const fs = require('fs');
//const {execSync} = require('child_process');
const os = require('os');
const ptp = os.platform()==="win32" ?require("pdf-to-printer"): require("unix-print");
const cron = require('node-cron');
const validURL = (url) => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
let browser;
const getBrowserInstance = async () => {
if (!browser || browser.isClosed()) {
browser = await puppeteer.launch({
ignoreHTTPSErrors: true,
args: ['--disable-gpu', '--single-process', '--no-zygote', '--no-sandbox', '--hide-scrollbars']
});
}
return browser;
};
const MAX_PUPPETEER_INSTANCES = 10; // Giới hạn phiên Puppeteer
let puppeteerInstances = 0;
const createPage = async () => {
if (puppeteerInstances >= MAX_PUPPETEER_INSTANCES) {
throw new Error('Too many Puppeteer instances');
}
puppeteerInstances++;
const browser = await getBrowserInstance();
const page = await browser.newPage();
page.on('close', () => puppeteerInstances--);
return page;
};
const homePage = (cb)=>{
ptp.getPrinters().then(ps=>{
let html =`<html>
<title>Print service</title>
<body>
<h1>Print service</h1>
<p>By truongpv (invncur@gmail.com)<p>
<h3>Printers</h3>
<ul>
${ps.map(p=>{
return `<li>${p.printer||p} - status: ${p.status}</li>`
})}
</ul>
<h3>API</h3>
<ul>
<li>
url: http://localhost:8081/html-print
</li>
<li>
method: POST
</li>
<li>
Body data (raw json): 1 JSON Object has property "html_content". Example: {
"html_context": "Hello Print service"
}
</li>
<li>
query:
<ul>
<li>
landscape: 1 or 0. default:0
</li>
<li>
page_size: (option) Specifies the paper size. A2, A3, A4, A5, A6, letter, legal, tabloid, statement, or a name selectable from your printer settings.
</li>
</ul>
</li>
<li>
Example:
<div style='background-color:#ddd;padding:5px'>curl --location 'http://localhost:8081/html-print' \
--header 'Content-Type: application/json' \
--data '{
"html_content":"<div>Test Print</div>"
}'</div>
</li>
</ul>
</body>
</html>`
cb(null,html);
}).catch(e=>{
cb("Can't get printers");
})
}
const printPage = async (req,res,error_required)=>{
let p = req.query.url;
let htmlContent = (req.body||{}).html_content;
if(!p && !htmlContent){
return res.status(400).send(error_required);
}
if(p && !htmlContent){
let buff = Buffer.from(p, 'base64');
p = buff.toString('utf8');
if (!validURL(p)) {
return res.status(400).send("Invalid URL");
}
}
let width = req.query.width;
//let height = req.query.height;
//find default printer
let printer = req.query.printer;
try{
if(!printer){
printer = await ptp.getDefaultPrinter();
if(!printer){
return res.status(400).send("Can't get default printer");
}
if(printer.status=="offline") return res.status(400).send(`Default printer (${printer.printer}) is offline`);
printer = printer.printer||printer;
console.log("use printer default",printer);
}else{
console.log("use printer",printer);
}
}catch(e){
console.error("Error find printer default",e);
return res.status(400).send("Can't get default printer");
}
if(!printer) return res.status(400).send("Not found printer");
let dir = __dirname + "/reports";
try{
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
console.log("Directory is created.");
}
}catch(e){
console.error("Can't create dir reports",e.message||e,dir);
return res.status(400).send(e.message);
}
let fileReport = `${dir}/rp-${new Date().getTime()}.pdf`;
//open page
let page;
try{
page = await createPage();
console.log("create page puppeteer",p)
if(htmlContent){
await page.setContent(htmlContent);
}else{
await page.goto(p, {waitUntil: 'networkidle0'});
}
}catch(e){
console.error("Error opening Puppeteer page", e.message);
return res.status(400).send(e.message);
}
await page.addStyleTag({
content: 'body { margin: 0;}',
})
let _height = await page.evaluate(() =>{
let printContent = document.getElementById('print-content');
if(!printContent) return 0;
return printContent.clientHeight;
});
if(_height) height = _height;
try{
await page.pdf({
path: fileReport,
printBackground: true,
width:(width?`${width}px`:undefined)
});
await page.close();
console.log("create pdf with width",width,fileReport);
}catch(e){
console.error("Error export to pdf",e.message);
await page.close();
return res.status(400).send(e.message);
}
if(os.platform()==="win32"){
const options = {
printer,
scale: "fit",
orientation: req.query.landscape?"90":undefined,
paperSize: req.query.page_size
};
ptp.print(fileReport,options)
.then(()=>{
res.send("Report was sent to print");
})
.catch((e)=>{
console.error(e.message)
res.status(400).send(e.message);
})
/*try{
execSync(`${__dirname}/dist/PDF.exe -print-settings "fit" -print-to "${printer}" ${fileReport}`);
res.send("Report was sent to print");
}catch(e){
console.error(e.message)
res.status(400).send(e.message);
}*/
}else{
const options = [];
if(req.query.landscape){
options.push(`-o landscape`);
}
options.push("-o fit-to-page");
if(req.query.page_size){
options.push(`-o media=${req.query.page_size}`);
}
ptp.print(fileReport,printer,options)
.then(()=>{
res.send("Report was sent to print");
})
.catch((e)=>{
console.error(e.message)
res.status(400).send(e.message);
})
}
}
const start =(port=8081)=>{
//dọn dẹp rác
cron.schedule('0 0 * * *', () => {
const dir = __dirname + '/reports';
fs.readdir(dir, (err, files) => {
if (err) return console.error(err);
files.forEach((file) => {
const filePath = `${dir}/${file}`;
const stats = fs.statSync(filePath);
const now = new Date().getTime();
const age = now - stats.mtime.getTime();
if (age > 7 * 24 * 60 * 60 * 1000) { // 7 ngày
fs.unlink(filePath, (err) => {
if (err) console.error(err);
});
}
});
});
});
//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();
});
app.get('/', (req, res) => {
console.log("query",req.query)
homePage((e,html)=>{
if(e) return res.status(500).send(e);
res.send(html);
})
});
app.post('/', (req, res) => {
console.log("body",req.body)
homePage((e,html)=>{
if(e) return res.status(500).send(e);
res.send(html);
})
});
app.get(`/web-print`, (req,res)=>{
printPage(req,res,"parameter url is required")
});
app.post(`/html-print` , (req,res)=>{
printPage(req,res,"Body with html_content is required")
});
//workers
const cluster = require('cluster');
const http = require('http');
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`);
});
} else {
console.log(`Worker ${process.pid} started`);
app.listen(port, () => console.log(`Print service is running at http://localhost:${port}`))
}
}
module.exports ={
start
}