diff --git a/api/main_endpoints/models/User.js b/api/main_endpoints/models/User.js index 35a3450be..96bc45da3 100644 --- a/api/main_endpoints/models/User.js +++ b/api/main_endpoints/models/User.js @@ -70,6 +70,10 @@ const UserSchema = new Schema( type: Number, default: 0 }, + escrowPagesPrinted: { + type: Number, + default: 0 + }, apiKey: { type: String, default: '' diff --git a/api/main_endpoints/routes/Printer.js b/api/main_endpoints/routes/Printer.js index a0621b7e8..909aa8a3d 100644 --- a/api/main_endpoints/routes/Printer.js +++ b/api/main_endpoints/routes/Printer.js @@ -17,10 +17,12 @@ const { UNAUTHORIZED, NOT_FOUND, SERVER_ERROR, + BAD_REQUEST } = require('../../util/constants').STATUS_CODES; const { PRINTING = {} } = require('../../config/config.json'); +const User = require('../models/User.js'); // see https://github.com/SCE-Development/Quasar/tree/dev/docker-compose.dev.yml#L11 let PRINTER_URL = process.env.PRINTER_URL @@ -89,6 +91,7 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { return res.status(OK).send({ printId: null }); } + const user = await User.findById(decodedToken._id); const dir = path.join(__dirname, 'printing'); const { totalChunks, chunkIdx } = req.body; @@ -97,7 +100,7 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { return res.sendStatus(OK); } - const { copies, sides, id } = req.body; + const { copies, sides, id, totalPages } = req.body; const chunks = await fs.promises.readdir(dir); const assembledPdfFromChunks = path.join(dir, id + '.pdf'); @@ -116,12 +119,17 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { } } - const stream = await fs.createReadStream(assembledPdfFromChunks); + const stream = await fs.promises.readFile(assembledPdfFromChunks); const data = new FormData(); data.append('file', stream, {filename: id, type: 'application/pdf'}); data.append('copies', copies); data.append('sides', sides); + if (Number(totalPages) > 30 - user.pagesPrinted - user.escrowPagesPrinted) { + await cleanUpChunks(dir, id); + return res.sendStatus(BAD_REQUEST); + } + try { // full pdf can be sent to quasar no problem const printRes = await axios.post(PRINTER_URL + '/print', data, { @@ -137,6 +145,9 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { await cleanUpChunks(dir, id); res.status(OK).send(printId); + + user.escrowPagesPrinted += Number(totalPages); + await user.save(); } catch (err) { logger.error('/sendPrintRequest had an error: ', err); @@ -145,4 +156,50 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { } }); +router.get('/status', async (req, res) => { + if (!checkIfTokenSent(req)) { + logger.warn('/status was requested without a token'); + return res.sendStatus(UNAUTHORIZED); + } + + const decodedToken = await decodeToken(req); + if (!decodedToken || Object.keys(decodedToken) === 0) { + logger.warn('/status was requested with an invalid token'); + return res.sendStatus(UNAUTHORIZED); + } + if (!PRINTING.ENABLED) { + logger.warn('Printing is disabled, returning 200 and completed status to mock the printing server'); + return res.status(OK).send({ status: 'completed' }); + } + + try { + const url = new URL('/status/', PRINTER_URL); + url.searchParams.append('id', req.query.id); + const response = await fetch(url, { + method: 'GET', + }); + + const user = await User.findById(decodedToken._id); + + // { status: string } + const json = await response.json(); + const pages = Math.abs(Number(req.query.pages)); + + if (json.status === 'completed') { + user.pagesPrinted += pages; + user.escrowPagesPrinted -= pages; + await user.save(); + } + + if (json.status === 'failed') { + user.escrowPagesPrinted -= pages; + await user.save(); + } + + res.status(OK).send(json); + } catch (err) { + res.sendStatus(SERVER_ERROR); + } +}); + module.exports = router; diff --git a/api/main_endpoints/routes/User.js b/api/main_endpoints/routes/User.js index a9c0d37b5..4f0e72704 100644 --- a/api/main_endpoints/routes/User.js +++ b/api/main_endpoints/routes/User.js @@ -112,6 +112,7 @@ router.post('/search', function(req, res) { lastLogin: result.lastLogin, membershipValidUntil: result.membershipValidUntil, pagesPrinted: result.pagesPrinted, + escrowPagesPrinted: result.escrowPagesPrinted, doorCode: result.doorCode, _id: result._id }; @@ -312,7 +313,7 @@ router.post('/getPagesPrintedCount', (req, res) => { .status(NOT_FOUND) .send({ message: `${req.body.email} not found.` }); } - return res.status(OK).json(result.pagesPrinted); + return res.status(OK).json(result.pagesPrinted + result.escrowPagesPrinted); }); }); diff --git a/src/APIFunctions/2DPrinting.js b/src/APIFunctions/2DPrinting.js index 9c1ea06a0..51ad4b8b3 100644 --- a/src/APIFunctions/2DPrinting.js +++ b/src/APIFunctions/2DPrinting.js @@ -60,6 +60,24 @@ export function parseRange(pages, maxPages) { return result; } +export async function getPrintStatus(printId, totalPages, token) { + const url = new URL('/api/Printer/status', BASE_API_URL); + url.searchParams.append('id', printId); + url.searchParams.append('pages', totalPages); + + const response = await fetch(url, { + headers: { + 'Authorization': `Bearer ${token}` + }, + method: 'GET', + }); + + const json = await response.json(); + const status = json.status; + + return status; +} + /** * Print the page * @param {Object} data - PDF File and its configurations @@ -80,6 +98,7 @@ export async function printPage(data, token) { const pdf = data.get('file'); const sides = data.get('sides'); const copies = data.get('copies'); + const totalPages = data.get('totalPages'); const id = crypto.randomUUID(); const CHUNK_SIZE = 1024 * 1024 * 0.5; // 0.5 MB ------- SENT DATA **CANNOT** EXCEED 1 MB const totalChunks = Math.ceil(pdf.size / CHUNK_SIZE); @@ -98,6 +117,7 @@ export async function printPage(data, token) { chunkData.append('id', id); chunkData.append('sides', sides); chunkData.append('copies', copies); + chunkData.append('totalPages', totalPages); } try { diff --git a/src/APIFunctions/User.js b/src/APIFunctions/User.js index cb072a556..bcb9ebbd1 100644 --- a/src/APIFunctions/User.js +++ b/src/APIFunctions/User.js @@ -96,6 +96,7 @@ export async function editUser(userToEdit, token) { discordDiscrim, discordID, pagesPrinted, + escrowPagesPrinted, accessLevel, lastLogin, emailVerified, @@ -122,6 +123,7 @@ export async function editUser(userToEdit, token) { discordDiscrim, discordID, pagesPrinted, + escrowPagesPrinted, accessLevel, lastLogin, emailVerified, diff --git a/src/Components/Printing/JobStatus.js b/src/Components/Printing/JobStatus.js new file mode 100644 index 000000000..55f01a581 --- /dev/null +++ b/src/Components/Printing/JobStatus.js @@ -0,0 +1,14 @@ +import React from 'react'; + +export default function JobStatus(props) { + return ( +
+
+ + + +

{props.fileName} ({props.id}): {props.status}

+
+
+ ); +} diff --git a/src/Pages/2DPrinting/2DPrinting.js b/src/Pages/2DPrinting/2DPrinting.js index ff23d3cd0..505a03eb4 100644 --- a/src/Pages/2DPrinting/2DPrinting.js +++ b/src/Pages/2DPrinting/2DPrinting.js @@ -4,8 +4,8 @@ import { parseRange, printPage, getPagesPrinted, + getPrintStatus, } from '../../APIFunctions/2DPrinting'; -import { editUser } from '../../APIFunctions/User'; import { PDFDocument } from 'pdf-lib'; import { healthCheck } from '../../APIFunctions/2DPrinting'; @@ -13,6 +13,7 @@ import ConfirmationModal from '../../Components/DecisionModal/ConfirmationModal.js'; import { useSCE } from '../../Components/context/SceContext.js'; +import JobStatus from '../../Components/Printing/JobStatus.js'; export default function Printing() { const { user, setUser } = useSCE(); @@ -33,6 +34,7 @@ export default function Printing() { const [printerHealthy, setPrinterHealthy] = useState(false); const [loading, setLoading] = useState(true); const [PdfFile, setPdfFile] = useState(null); + const [printJobs, setPrintJobs] = useState({}); async function checkPrinterHealth() { setLoading(true); @@ -52,7 +54,67 @@ export default function Printing() { } } + async function tryRemoveJob(status, id) { + const completedOrFailed = ['completed', 'failed'].includes(status); + if (!completedOrFailed) return; + + setTimeout(() => { + setPrintJobs((prev) => { + const newPrintJobs = {...prev}; + + if (!(id in newPrintJobs)) { + return prev; + } + + delete newPrintJobs[id]; + window.localStorage.setItem('printJobs', JSON.stringify(newPrintJobs)); + return {...newPrintJobs}; + }); + }, 5000); + } + useEffect(() => { + if (printJobs === null || Object.keys(printJobs).length === 0) return; + const ids = Object.keys(printJobs); + + const interval = setInterval(async () => { + if (ids.length === 0) { + clearInterval(interval); + return; + } + + ids.map(async (id) => { + const completedOrFailed = ['completed', 'failed'].includes(printJobs[id].status); + if (completedOrFailed) return; + + const status = await getPrintStatus(id, printJobs[id].pages, user.token); + const newPrintJobs = {...printJobs}; + newPrintJobs[id].status = status; + setPrintJobs(newPrintJobs); + window.localStorage.setItem('printJobs', JSON.stringify(newPrintJobs)); + + tryRemoveJob(status, id); + }); + }, 1000); + + return () => clearInterval(interval); + }, [printJobs]); + + useEffect(() => { + if (!!window.localStorage) { + const jobsFromLocal = JSON.parse(window.localStorage.getItem('printJobs')); + if (!!jobsFromLocal) { + setPrintJobs(() => { + const ids = Object.keys(jobsFromLocal); + ids.map(async (id) => { + tryRemoveJob(jobsFromLocal[id].status, id); + }); + + return jobsFromLocal; + }); + } + } + checkPrinterHealth(); getNumberOfPagesPrintedSoFar(); }, []); @@ -200,20 +262,26 @@ export default function Printing() { data.append('file', PdfFile); data.append('sides', sides); data.append('copies', copies); - let status = await printPage(data, user.token); + data.append('totalPages', pagesToBeUsedInPrintRequest); + const printReq = await printPage(data, user.token); - if (!status.error) { - editUser( - { ...user, pagesPrinted: pagesPrinted + pagesToBeUsedInPrintRequest }, - user.token, - ); - setPrintStatus('Printing succeeded!'); - setPrintStatusColor('success'); - } else { + try { + const printId = printReq?.responseData['print_id']; + const newPrintJobs = {...printJobs, + [printId]: { + status: 'created', + fileName: PdfFile.name, + pages: pagesToBeUsedInPrintRequest + } + }; + setPrintJobs(newPrintJobs); + window.localStorage.setItem('printJobs', JSON.stringify(newPrintJobs)); + getNumberOfPagesPrintedSoFar(); + } catch (err) { setPrintStatus('Printing failed. Please try again or reach out to SCE Dev team if the issue persists.'); setPrintStatusColor('error'); } - getNumberOfPagesPrintedSoFar(); + setTimeout(() => { setPrintStatus(null); }, 5000); @@ -417,6 +485,14 @@ export default function Printing() { return (
+
+ { + Object.keys(printJobs).map(id => ( + + )) + } +
+ ); } -