Add multi-printer support, change API
parent
90344c9be6
commit
8c8cea6eb6
37
README.md
37
README.md
|
@ -4,29 +4,36 @@ HTTP to IPP proxy, a.k.a. HSWAW Rube Goldberg printing microservice.
|
|||
|
||||
## Usage
|
||||
|
||||
TODO: This API is not stable, and is subject to change. Parameters will be added to specify printer kind, and some job attributes (like copy count, orientation, size...)
|
||||
|
||||
```
|
||||
POST /api/1/print - Print a document
|
||||
- body: PDF
|
||||
- response: 200 OK if sent successfully (does not check if print *actually* succeeded, just that adding it to the print queue was successful), 400 or 500 in case of errors
|
||||
|
||||
Debug routes:
|
||||
|
||||
GET /api/1/printer/attributes - Prints IPP attributes
|
||||
GET /api/1/printer/jobs - Lists IPP jobs
|
||||
GET /api/1/health - Just says 200 OK
|
||||
```
|
||||
`GET /` to see available API routes
|
||||
|
||||
## Quick start
|
||||
|
||||
Configuration available:
|
||||
|
||||
```
|
||||
PRINTSERVANT_PORT=3199 PRINTSERVANT_IPP_PRINTER_URL="ipp://printmaster.local:631/printers/DYMO_LabelWriter450" node index.js
|
||||
PRINTSERVANT_PORT - Port (default: 3199)
|
||||
PRINTSERVANT_CONFIG - JSON config, like so:
|
||||
{
|
||||
printers: [
|
||||
{
|
||||
name: 'DYMO_LabelWriter450',
|
||||
aliases: ['dymo', 'label'],
|
||||
ipp_url: 'ipp://$SECRET@printmaster.waw.hackerspace.pl/printers/DYMO_LabelWriter450',
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
PRINTSERVANT_SECRET - Optional, used to substitute $SECRET in printer configs' ipp_urls
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
PRINTSERVANT_CONFIG='{"printers":[{"name":"DYMO_LabelWriter450","aliases":["dymo"],"ipp_url":"ipp://printmaster.local:631/printers/DYMO_LabelWriter450"}]}' node index.js
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```
|
||||
docker build -t printservant .
|
||||
docker run --env PRINTSERVANT_IPP_PRINTER_URL="ipp://printmaster.local:631/printers/DYMO_LabelWriter450" -it -p 3199:3199 printservant
|
||||
docker run --env PRINTSERVANT_CONFIG='... put config here ...' -it -p 3199:3199 printservant
|
||||
```
|
||||
|
|
143
index.js
143
index.js
|
@ -1,35 +1,90 @@
|
|||
require('console-stamp')(console)
|
||||
|
||||
const ipp = require('ipp')
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
|
||||
// TODO:
|
||||
// - multiple printers
|
||||
// - take printer name, other params by query string
|
||||
const PORT = Number(process.env.PRINTSERVANT_PORT) || 3199
|
||||
const PRINTER_URL = process.env.PRINTSERVANT_IPP_PRINTER_URL
|
||||
// --- Config ---
|
||||
|
||||
if (!(PORT && PRINTER_URL)) {
|
||||
console.error("PRINTSERVANT_PORT and PRINTSERVANT_IPP_PRINTER_URL environment variables must be set")
|
||||
process.exit(1)
|
||||
const PORT = Number(process.env.PRINTSERVANT_PORT) || 3199
|
||||
const CONFIG = JSON.parse(process.env.PRINTSERVANT_CONFIG || 'null')
|
||||
const SECRET = process.env.PRINTSERVANT_SECRET || ''
|
||||
|
||||
if (!(PORT && CONFIG)) {
|
||||
throw new Error("PRINTSERVANT_PORT and PRINTSERVANT_CONFIG environment variables must be set")
|
||||
}
|
||||
|
||||
const printers = []
|
||||
|
||||
for (const printerConfig of CONFIG.printers) {
|
||||
const { name, aliases = [], ipp_url } = printerConfig
|
||||
|
||||
if (!(name && ipp_url)) {
|
||||
throw new Error("Printer config must have a name and url")
|
||||
}
|
||||
|
||||
if (ipp_url.includes('$SECRET') && !SECRET) {
|
||||
throw new Error(`Printer '${name}' config has a secret in the url but no PRINTSERVANT_CONFIG was provided`)
|
||||
}
|
||||
|
||||
const url = ipp_url.replace('$SECRET', SECRET)
|
||||
const ippPrinter = ipp.Printer(url)
|
||||
|
||||
printers.push({ name, aliases, ippPrinter })
|
||||
}
|
||||
|
||||
const printerMap = new Map()
|
||||
printers.forEach(printer => {
|
||||
printerMap.set(printer.name.toLowerCase(), printer)
|
||||
printer.aliases.forEach(alias => {
|
||||
printerMap.set(alias.toLowerCase(), printer)
|
||||
})
|
||||
})
|
||||
|
||||
function getPrinter(name) {
|
||||
return printerMap.get(name.toLowerCase())
|
||||
}
|
||||
|
||||
// --- Debug API ---
|
||||
|
||||
const app = express()
|
||||
const printer = ipp.Printer(PRINTER_URL)
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send('Hello World! I am printservant.')
|
||||
res.send(`<pre>Hello world! API routes available:
|
||||
- GET /
|
||||
- GET /health
|
||||
- GET /printers
|
||||
- Lists all printers (names and aliases)
|
||||
- GET /printers/:name/attributes
|
||||
- Prints IPP attributes
|
||||
- GET /printers/:name/jobs
|
||||
- Prints IPP jobs
|
||||
- POST /print?printer=:name (body: application/pdf)
|
||||
- Prints a PDF
|
||||
- :name - printer name or alias (case insensitive)
|
||||
- Response: 200 OK if sent to printer successfully (not necessarily printed), 4xx/5xx otherwise
|
||||
</pre>`)
|
||||
})
|
||||
|
||||
app.get('/api/1/health', (req, res) => {
|
||||
res.send("I'm cool, I think. I'm not sure. Thanks for asking though.")
|
||||
app.get('/health', (req, res) => {
|
||||
res.send(`Printservant is healthy (probably)`)
|
||||
})
|
||||
|
||||
function executeSimpleCommand(command, res) {
|
||||
printer.execute(command, null, function(error, result){
|
||||
if (error) {
|
||||
console.log(error);
|
||||
res.status(500)
|
||||
} else if (result.statusCode !== 'successful-ok') {
|
||||
app.get('/printers', (req, res) => {
|
||||
res.send(printers.map(({ name, aliases }) => ({ name, aliases })))
|
||||
})
|
||||
|
||||
function executeSimpleCommand(printerName, command, res) {
|
||||
const printer = getPrinter(printerName)
|
||||
if (!printer) {
|
||||
console.log(`Printer '${printerName}' not found`)
|
||||
res.status(404).send(`Printer '${printerName}' not found`)
|
||||
return
|
||||
}
|
||||
|
||||
printer.ippPrinter.execute(command, null, function (error, result) {
|
||||
if (error || result.statusCode !== 'successful-ok') {
|
||||
console.error(error || result);
|
||||
res.status(500)
|
||||
}
|
||||
|
||||
|
@ -37,26 +92,54 @@ function executeSimpleCommand(command, res) {
|
|||
});
|
||||
}
|
||||
|
||||
app.get('/api/1/printer/attributes', (req, res) => {
|
||||
console.log('Getting printer attributes')
|
||||
executeSimpleCommand("Get-Printer-Attributes", res)
|
||||
app.get('/printers/:name/attributes', (req, res) => {
|
||||
const { name } = req.params
|
||||
console.log(`Getting attributes for printer: '${name}'`)
|
||||
executeSimpleCommand(name, "Get-Printer-Attributes", res)
|
||||
})
|
||||
|
||||
app.get('/api/1/printer/jobs', (req, res) => {
|
||||
console.log('Getting printer jobs')
|
||||
executeSimpleCommand("Get-Jobs", res)
|
||||
app.get('/printers/:name/jobs', (req, res) => {
|
||||
const { name } = req.params
|
||||
console.log(`Getting jobs for printer: '${name}'`)
|
||||
executeSimpleCommand(name, "Get-Jobs", res)
|
||||
})
|
||||
|
||||
app.post('/api/1/print', bodyParser.raw({ type: '*/*' }), (req, res) => {
|
||||
// --- Print API ---
|
||||
|
||||
app.post('/print', bodyParser.raw({ type: '*/*' }), (req, res) => {
|
||||
const { body, query } = req
|
||||
|
||||
console.log("Received print job: ", { body, query })
|
||||
|
||||
if (!body || !body.length) {
|
||||
res.status(400).send("No data received, please send a PDF")
|
||||
// validate query params
|
||||
const { printer: printerName, ...otherParams } = query
|
||||
if (!printerName) {
|
||||
res.status(400).send("No printer specified, pass ?printer=:name in the query params")
|
||||
return
|
||||
} else if (Object.keys(otherParams).length) {
|
||||
res.status(400).send("Unknown query params, only ?printer=:name is supported")
|
||||
return
|
||||
}
|
||||
|
||||
const printer = getPrinter(printerName)
|
||||
if (!printer) {
|
||||
console.log(`Printer '${name}' not found`)
|
||||
res.status(404).send(`Printer '${name}' not found`)
|
||||
return
|
||||
}
|
||||
|
||||
// validate body
|
||||
if (!body || !body.length) {
|
||||
res.status(400).send("No data received, please send a PDF")
|
||||
return
|
||||
} else if (body.subarray(0, 4).toString() !== "%PDF") {
|
||||
res.status(415).send("Data does not look like a PDF, please send a PDF")
|
||||
return
|
||||
} else if (body.length > 10_000_000) {
|
||||
res.status(413).send("Data too large (limit: 10MB), please send a smaller PDF")
|
||||
return
|
||||
}
|
||||
|
||||
// send print job
|
||||
const msg = {
|
||||
"operation-attributes-tag": {
|
||||
"document-format": "application/pdf",
|
||||
|
@ -69,7 +152,7 @@ app.post('/api/1/print', bodyParser.raw({ type: '*/*' }), (req, res) => {
|
|||
};
|
||||
console.log("Sending print job: ", msg)
|
||||
|
||||
printer.execute("Print-Job", msg, function(error, result){
|
||||
printer.ippPrinter.execute("Print-Job", msg, function(error, result){
|
||||
console.log({ result, error });
|
||||
|
||||
if (error) {
|
||||
|
@ -82,6 +165,8 @@ app.post('/api/1/print', bodyParser.raw({ type: '*/*' }), (req, res) => {
|
|||
});
|
||||
})
|
||||
|
||||
// --- Let's goooooooo ---
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`printservant listening on port ${PORT}`)
|
||||
})
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
"": {
|
||||
"name": "printservant",
|
||||
"version": "0.0.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.2",
|
||||
"console-stamp": "^3.1.2",
|
||||
"express": "^4.18.2",
|
||||
"ipp": "^2.0.1",
|
||||
"patch-package": "^8.0.0"
|
||||
|
@ -178,6 +180,18 @@
|
|||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
},
|
||||
"node_modules/console-stamp": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/console-stamp/-/console-stamp-3.1.2.tgz",
|
||||
"integrity": "sha512-ab66x3NxOTxPuq71dI6gXEiw2X6ql4Le5gZz0bm7FW3FSCB00eztra/oQUuCoCGlsyKOxtULnHwphzMrRtzMBg==",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"dateformat": "^4.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
|
@ -223,6 +237,14 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/dateformat": {
|
||||
"version": "4.6.3",
|
||||
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
|
||||
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.2",
|
||||
"console-stamp": "^3.1.2",
|
||||
"express": "^4.18.2",
|
||||
"ipp": "^2.0.1",
|
||||
"patch-package": "^8.0.0"
|
||||
|
|
Loading…
Reference in New Issue