From e60f6ec8461154dd58af42aad72ecefc1c2af436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Preu=C3=9F?= Date: Sun, 21 Jan 2024 16:07:29 +0100 Subject: [PATCH] Add README.md Add windows support --- README.md | 58 ++++++++ index.js | 24 ++-- print-service.js | 338 ----------------------------------------------- print.js | 23 ++++ 4 files changed, 95 insertions(+), 348 deletions(-) create mode 100644 README.md delete mode 100644 print-service.js create mode 100644 print.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..653ec71 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Print Utils + +## Description + +This is a [Sendcloud](https://www.sendcloud.com) compatible Print Client which runs on Windows, Linux and Mac. Its goal +is providing a simple and easy to use interface to print documents from any application to a printer connected to the +computer. + +## Installation + +To be done. + +## Usage + +### List Printers + +#### Request: + +```bash +curl --location 'http://127.0.0.1:1903/printers' +``` + +#### Response: + +> The `default` property is currently always `false` because the default printer is not yet supported. + +```json +[ + { + "default": false, + "format": "PDF", + "id": "PM-241-BT (Network)", + "name": "PM-241-BT (Network)" + } +] +``` + +### Print a PDF + +#### Request: + +```bash +curl --location 'http://127.0.0.1:1903/printers/PM-241-BT%20(Network)/print' \ +--form 'file=@"label.pdf"' \ +--form 'copies="1"' +``` + +#### Response: + +> On windows the response does not return any real job id. Instead, it returns a 0. + +```json +{ + "jobs": [ + "1" + ] +} +``` \ No newline at end of file diff --git a/index.js b/index.js index 05e07f2..40fcfd9 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -import {getPrinters, print} from "unix-print"; +import { getPrinters, print } from './print.js' import express from 'express' import cors from 'cors' @@ -8,11 +8,11 @@ import multer from 'multer' const app = express() const port = 1903 -const upload = multer({dest: 'uploads/'}) +const upload = multer({ dest: 'uploads/' }) app.use(cors()) -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({extended: true})); +app.use(bodyParser.json()) +app.use(bodyParser.urlencoded({ extended: true })) app.get('/', (req, res) => { res.send('Hello World!') @@ -24,21 +24,21 @@ app.get('/printers', async (req, res) => { return { default: false, format: 'PDF', - id: printer.printer, - name: printer.printer, + id: printer.printer || printer.deviceId, + name: printer.printer || printer.name, } })) }) app.post('/printers/:printer/print', upload.single('file'), async (req, res) => { - const {printer} = req.params + const { printer } = req.params // query: user-id: string, docType: label // form data: file: binary, copies: string, shipment-ids: string - const fileToPrint = req.file.path; - const printJob = await print(fileToPrint, printer); - const jobId = getRequestId(printJob); + const fileToPrint = req.file.path + const printJob = await print(fileToPrint, printer) + const jobId = getRequestId(printJob) return res.json({ jobs: [jobId], @@ -51,6 +51,10 @@ app.listen(port, () => { }) function getRequestId(printJob) { + if (printJob === undefined) { + return '0' + } + const requestId = printJob.stdout.split(' ')[3] return requestId.split('-')[1] } \ No newline at end of file diff --git a/print-service.js b/print-service.js deleted file mode 100644 index fc06c6a..0000000 --- a/print-service.js +++ /dev/null @@ -1,338 +0,0 @@ -import globalAxiosClient, { printAxiosClient } from '@/utils/axios' -import store from '@/store/store.js' - -import Segment from '@/app/users/tracking/segment.js' -import i18n from '@/i18n/i18n' -import serializeUrlParams from '@/utils/serialize-url-params.js' -import ClientHelperService from '@/services/client-helper.service.js' -import ModalService from '@/services/modal.service.js' -import { getAbsoluteBackendURL } from '@/utils/backend' -import * as DownloadsApi from '../api/downloads.api.js' - -import ToastService from '@/services/toast.service' - -export default { - printers: undefined, - - /** - * Opens a new window to download the document, or the current window URL - * is changed to display the PDF - * - * @param {string} url URL where the document can be downloaded - * @param {Object} params An `Object` of key/value pairs to be turned into a query string - * @param {string} [fileName] File name to save as - * @return {Promise} - */ - _downloadDocument(url, params = {}, fileName) { - // The URL passed is a relative one, with the frontend running on - // app.sendcloud.com and the document generating endpoints over at - // eu-central-1-0.app.sendcloud.com, we need to make this URL absolute. - const absoluteUrl = getAbsoluteBackendURL(url) - const queryString = serializeUrlParams({ ...params, download: true }) - return DownloadsApi.push(new URL(`${absoluteUrl}?${queryString}`), fileName) - }, - - /** - * @param {string} printerId - * @param {string} doc File contents - * @param {string} docType Document type - * @param {{ numberOfCopies?: number - * , shipmentIds?: Array - * }} [options={}] - * @returns {Promise} Rejected promise if the browser is unsupported, otherwise a resolved - * Promise with the result from the endpoint - */ - _printDocument(printerId, doc, docType, options = {}) { - const { numberOfCopies = 1, shipmentIds = [] } = options - if (!ClientHelperService.isBrowserSupported()) { - // eslint-disable-next-line prefer-promise-reject-errors - return Promise.reject({ - printer: printerId, - data: doc, - reason: 'unsupported_browser', - }) - } - - const API_URL = store.getters.definitions.desktop_client.api_url - const user = store.getters.user - - const payload = new FormData() - const blob = new Blob([doc], { type: 'application/pdf' }) - payload.append('file', blob) - payload.append('copies', numberOfCopies.toString()) - payload.append('shipment-ids', shipmentIds.join(',')) - - const config = { - params: { 'user-id': user.id, 'docType': docType }, - } - - return printAxiosClient.post(`${API_URL}/printers/${encodeURIComponent(printerId)}/print`, payload, config) - }, - - /** - * Download customs documents to the user's device, either in a new window or the current window. - * - * @param {Array} shipmentIds List of shipment IDs - * @param {Object} printFormat `Object` containing paper size of the document - * @returns {Promise} - */ - downloadCustomsDocuments(shipmentIds, printFormat) { - const params = { - ids: shipmentIds.join(','), - start_from: printFormat.startAt, - paper_size: printFormat.size || 'A4', - type: 'customs', - } - - return this._downloadDocument('/xhr/parcel/label', params, 'customs') - }, - - /** - * Download labels to the user's device, either in a new window or the current window, - * using the user's selected print options. - * - * @param {Array} shipmentIds List of shipment blob UUIDs - * @param {Object} printFormat `Object` containing paper size and where on the page to start printing labels - */ - downloadLabels(shipmentIds, printFormat) { - if (store.getters.user.is_account_on_hold) { - return this.showAccountOnHoldModal() - } - - const params = { - ids: shipmentIds.join(','), - start_from: printFormat.startAt, - paper_size: printFormat.size, - } - - this._downloadDocument('/xhr/parcel/label', params, 'labels') - }, - - /** - * Download packing slips to the user's device, either in a new window or the current window. - * - * @param {Array} shipmentBlobUuids List of shipment blob UUIDs - * @param {Object} _printFormat (Currently unused) Object containing paper size and document position - * @param {number} senderAddressId - */ - downloadPackingSlips(shipmentBlobUuids, _printFormat, senderAddressId) { - this._downloadDocument( - '/orders/packgo/slips/packing-slip', - { - blobs: shipmentBlobUuids.join(','), - sender_address: senderAddressId, - }, - 'packing_slip', - ) - }, - - /** - * Download picking lists to the user's device, either in a new window or the current window. - * - * @param {Array} shipmentBlobUuids List of shipment blob UUIDs - * @param {Object} _printFormat (Currently unused) Object containing paper size and document position - */ - downloadPickingLists(shipmentBlobUuids, _printFormat) { - this._downloadDocument( - '/orders/packgo/slips/picking-list', - { - blobs: shipmentBlobUuids.join(','), - }, - 'picking_list', - ) - }, - - /** - * Check if a printer exists in the service's stored list of printers. - * - * @param {string} printerId - * @returns {boolean} `true` if a printer with a matching `id` is found, otherwise `false`. - */ - exists(printerId) { - if (!Array.isArray(this.printers) || !printerId) { - return false - } - - const foundPrinter = this.printers.find(printer => printer.id === printerId) - return foundPrinter !== undefined && foundPrinter !== null - }, - - /** - * Fetch printers and store them in the service for future reference. - * - * @returns {Promise} An empty `Array` if the browser is not supported, an `Array` - * of printer `Object`s installed on the user's device, or `undefined` in case of failure. - */ - async findAll() { - if (!ClientHelperService.isBrowserSupported()) { - this.printers = [] - return Promise.resolve([]) - } - - try { - const API_URL = store.getters.definitions.desktop_client.api_url - const user = store.getters.user - - const { data: printers } = await printAxiosClient.get(`${API_URL}/printers`, { - params: { 'user-id': user.id }, - }) - this.printers = printers - return Promise.resolve(printers) - } catch (error) { - this.printers = undefined - return Promise.resolve(undefined) - } - }, - - /** - * List printers that are stored in the service. - * - * @returns {Promise} An empty `Array` if the browser is not supported, an `Array` - * of printer `Object`s installed on the user's device, or `undefined` in case of failure. Will be `undefined` - * if the `findAll()` method has not previously been called. - */ - async getAll() { - // TODO: Remove this condition after the full FE/BE split - if (!this.printers) { - await this.findAll() - } - return this.printers - }, - - /** - * Retrieve the default printer from the provided `printers` argument. - * - * @param {Array|undefined} printers A list of printers to inspect - * @returns {Object|undefined} The user's default printer `Object`, if available - otherwise `undefined` - */ - getDefault(printers) { - if (!Array.isArray(printers)) { - return undefined - } - return printers.find(printer => printer.default === true) - }, - - /** - * Print customs documents with the user's selected printer and print options. - * - * @param {Array} shipmentIds List of shipment IDs - * @param {string} printerId - * @param {Object} printFormat `Object` containing paper size definition and where - * @param {number} numberOfCopies - */ - async printCustomsDocuments(shipmentIds, printerId, printFormat, numberOfCopies = 1) { - const params = { - ids: shipmentIds.join(','), - start_from: printFormat.start_from || 0, - paper_size: printFormat.size || 'A4', - download: false, - type: 'customs', - } - - const { data } = await globalAxiosClient.get('/xhr/parcel/label', { responseType: 'arraybuffer', params }) - await this._printDocument(printerId, data, 'customs', { - numberOfCopies, - shipmentIds, - }) - }, - - /** - * Print labels with the user's selected printer and print options - * - * @param {Array} shipmentIds List of shipment IDs - * @param {string} printerId - * @param {Object} printFormat `Object` containing paper size definition and where - * on the page to start printing labels - */ - printLabels(shipmentIds, printerId, printFormat) { - if (store.getters.user.is_account_on_hold) { - return this.showAccountOnHoldModal() - } - - const params = { - ids: shipmentIds.join(','), - start_from: printFormat.startAt || 0, - paper_size: printFormat.size || 'A6', - download: false, - } - - return globalAxiosClient - .get('/xhr/parcel/label', { responseType: 'arraybuffer', params }) - .then((response) => { - return this._printDocument(printerId, response.data, 'label', { shipmentIds }) - }) - .catch((error) => { - if (error.errors && error.errors[0].detail) { - for (const err of error.errors) { - ToastService.error(err.detail) - } - } - }) - }, - - /** - * Print packing slips with the user's selected printer - * - * @param {Array} shipmentBlobUuids List of shipment blob UUIDs - * @param {string} printerId - * @param {Object} _printFormat (Currently unused) `Object` containing paper size definition and where - * on the page to start printing documents - * @param {number} senderAddressId - */ - async printPackingSlips(shipmentBlobUuids, printerId, _printFormat, senderAddressId) { - const url = `/orders/packgo/slips/packing-slip?sender_address=${senderAddressId}&blobs=${shipmentBlobUuids.join( - ',', - )}` - - const { data } = await globalAxiosClient.get(url, { responseType: 'arraybuffer' }) - return this._printDocument(printerId, data, 'packing-slip', { shipmentIds: shipmentBlobUuids }) - }, - - /** - * Print picking lists with the user's selected printer - * - * @param {Array} shipmentBlobUuids List of shipment blob UUIDs - * @param {string} printerId - * @param {Object} _printFormat (Currently unused) `Object` containing paper size definition and where - * on the page to start printing documents - */ - async printPickingLists(shipmentBlobUuids, printerId, _printFormat) { - const url = `/orders/packgo/slips/picking-list?blobs=${shipmentBlobUuids.join(',')}` - - try { - const { data } = await globalAxiosClient.get(url, { responseType: 'arraybuffer' }) - const result = await this._printDocument(printerId, data, 'picking-list', { shipmentIds: shipmentBlobUuids }) - return Promise.resolve(result) - } catch (error) { - // Reject with the URL (the caller may use it as a fallback, retry, etc): - return Promise.reject(url) - } - }, - - /** - * Displays a confirmation modal explaining the user that printing labels is unavailable due to the - * account being on hold. - * - * @returns {Promise} - */ - async showAccountOnHoldModal() { - Segment.track('Print Labels on Hold modal') - - return new Promise((_resolve, reject) => { - ModalService.build('AccountOnHoldModal', { - id: 'account-on-hold', - confirm: () => { - reject() - }, - title: `${i18n.t("Sorry! You can't print any labels right now.")} 😕`, - bodyMessage: `

${i18n.t( - "We've placed your account on hold in order to review it for security reasons. This means you can't create or print any labels at this time.", - )}

-

${i18n.t("Don't worry—these reviews are usually finished within a few hours on normal business days.")}

-

${i18n.t( - 'If you have any concerns in the meantime, feel free to reach out to our customer support team.', - )}

`, - }) - }) - }, -} diff --git a/print.js b/print.js new file mode 100644 index 0000000..852cfb3 --- /dev/null +++ b/print.js @@ -0,0 +1,23 @@ +import { platform } from 'os'; +import * as windowsMethods from 'pdf-to-printer'; +import * as linuxMethods from 'unix-print'; + +let methods; + +if (platform() === 'win32') { + methods = windowsMethods.default; +} else { + methods = linuxMethods.default; +} + +export const print = async (file, printer) => { + if (platform() === 'win32') { + return await methods.print(file, {printer}); + } + + return await methods.print(file, printer); +} + +export const getPrinters = async () => { + return await methods.getPrinters(); +} \ No newline at end of file