printnatura/print-server.js

205 lines
7.0 KiB
JavaScript

const mysql = require('mysql2/promise');
const exec = require('child_process').exec;
const fs = require('fs-extra');
const path = require('path');
const colors = require('colors');
const axios = require('axios');
const yml = require('require-yml');
const selectQuery = fs.readFileSync(`sql/selectQueued.sql`).toString();
const jobDataQuery = fs.readFileSync(`sql/jobData.sql`).toString();
const jobArgsQuery = fs.readFileSync(`sql/jobArgs.sql`).toString();
const updateQuery = fs.readFileSync(`sql/updateState.sql`).toString();
const appDir = path.dirname(require.main.filename);
class PrintServer {
async start() {
let conf = yml(path.join(__dirname, 'config.yml'));
const localConfFile = path.join(__dirname, 'config.local.yml');
if (fs.existsSync(localConfFile))
conf = Object.assign({}, conf, yml(localConfFile));
this.conf = conf;
const decoration = '△▽'.repeat(10)
console.clear();
console.log(decoration, `${colors.bgBlack.white.bold(' Print')}${colors.bgBlack.green.bold('Natura ')}`, decoration, '\n')
await this.getToken();
await this.init();
}
async init() {
this.pool = await mysql.createPool(this.conf.db);
console.log('Connected to DB successfully.'.green);
await this.poll();
}
async stop() {
await this.end();
await axios.post(`${this.conf.salix.url}/api/Accounts/logout?access_token=${this.token}`);
}
async end() {
if (this.pollTimeout) {
clearTimeout(this.pollTimeout);
this.pollTimeout = null;
}
await this.pool.end();
}
async getToken() {
const salix = this.conf.salix;
let response = await axios.post(`${salix.url}/api/Accounts/login`, {
user: salix.user,
password: salix.password
});
this.token = response.data.token;
}
async poll() {
let delay = this.conf.refreshRate;
this.pollTimeout = null;
try {
const conn = await this.pool.getConnection();
try {
if (this.dbDisconnected) {
await conn.ping();
this.dbDisconnected = false;
console.log('DB connection recovered.'.green);
}
if (await this.printJob(conn))
delay = 0;
} finally {
await conn.release();
}
} catch (err) {
if (err.code === 'ETIMEDOUT') {
delay = this.conf.reconnectTimeout;
if (!this.dbDisconnected) {
this.dbDisconnected = true;
console.log(`DB connection lost: ${err.message}`.red);
}
} else
console.error(err);
}
this.pollTimeout = setTimeout(() => this.poll(), delay);
}
async printJob(conn) {
const conf = this.conf;
let jobId;
try {
let jobData;
const args = {};
try {
await conn.beginTransaction();
const [[printJob]] = await conn.query(selectQuery);
if (!printJob) {
await conn.rollback();
return;
}
jobId = printJob.id;
// Job data
const [[data]] = await conn.query(jobDataQuery, jobId);
jobData = data;
// Job arguments
const [res] = await conn.query(jobArgsQuery, jobId);
for (const row of res)
args[row.name] = row.value;
await conn.query(updateQuery, ['printing', null, jobId]);
await conn.commit();
} catch (err) {
await conn.rollback();
throw err;
}
// Path params
const usedParams = new Set();
const methodPath = jobData.method.replace(/{\w+}/g, function(match) {
const key = match.substr(1, match.length - 2);
const value = args[key];
usedParams.add(key);
return value !== undefined ? value : match;
});
let pdfData;
for (let attempts = 0; !pdfData && attempts < 2; attempts++) {
// URL params
const params = {userFk: jobData.userFk};
for (const key in args) {
if (!usedParams.has(key))
params[key] = args[key];
}
const urlParams = new URLSearchParams(params);
// Request
try {
const response = await axios({
method: 'get',
url: `${conf.salix.url}/api/${methodPath}?${urlParams.toString()}`,
responseType: 'arraybuffer',
headers: {
'Accept': 'application/pdf',
'Authorization': this.token
}
});
pdfData = response.data;
}
catch (err) {
if (err.response?.statusText === 'Unauthorized') {
await this.getToken();
} else
throw err;
}
}
// Save PDF to disk
const printer = jobData.printer;
const tmpPath = path.join(appDir, 'tmp')
if (!fs.existsSync(tmpPath))
fs.mkdirSync(tmpPath)
const tmpFilePath = path.join(tmpPath, `${Math.random().toString(36).substring(7)}.pdf`);
await fs.writeFile(tmpFilePath, pdfData, 'binary');
// Print PDF
try {
await pExec(`lp -d "${printer}" "${tmpFilePath}"`);
} catch(err) {
await fs.unlink(tmpFilePath);
throw new Error(`Print error: ${err.message}`);
}
await conn.query(updateQuery, ['printed', null, jobId]);
if (conf.debug)
console.debug(`(${colors.yellow(jobId)}) Document has been printed`, `[${args.collectionFk}, ${jobData.report}, ${printer}]`.green);
await fs.unlink(tmpFilePath);
} catch (err) {
let message = err.message;
if (err.name === 'AxiosError' && err.code === 'ERR_BAD_REQUEST') {
const resMessage = JSON.parse(err.response.data).error.message;
message = `${message}: ${resMessage}`;
}
await conn.query(updateQuery, ['error', message, jobId]);
throw new Error(`(${jobId}) ${message}`);
}
return jobId;
}
}
module.exports = PrintServer;
function pExec(command) {
return new Promise(function(resolve, reject) {
exec(command, function(err, stdout, stderr) {
if (err) return reject(err);
resolve({stdout, stderr})
});
});
}