diff --git a/lib/machine-loader.js b/lib/machine-loader.js index fd9375b1..d9113eb5 100644 --- a/lib/machine-loader.js +++ b/lib/machine-loader.js @@ -487,11 +487,18 @@ function updateDiagnostics (deviceId, images) { const directory = `${OPERATOR_DATA_DIR}/diagnostics/${deviceId}/` const { scan, front } = images - return updatePhotos(scan, front, directory) + return updatePhotos(directory, [['scan.jpg', scan], ['front.jpg', front]]) .then(() => db.none(sql, [deviceId, !!scan, !!front])) .catch(err => logger.error('while running machine diagnostics: ', err)) } +const updateFailedQRScans = (deviceId, frames) => { + const timestamp = (new Date()).toISOString() + const directory = `${OPERATOR_DATA_DIR}/failedQRScans/${deviceId}/` + const filenames = _.map(no => `${timestamp}-${no}.jpg`, _.range(0, _.size(frames))) + return updatePhotos(directory, _.zip(filenames, frames)) +} + function createPhoto (name, data, dir) { if (!data) { logger.error(`Diagnostics error: No data to save for ${name} photo`) @@ -503,16 +510,12 @@ function createPhoto (name, data, dir) { return fsPromises.writeFile(filename, decodedImageData) } -function updatePhotos (scan, front, dir) { +function updatePhotos (dir, photoPairs) { const dirname = path.join(dir) _.attempt(() => makeDir.sync(dirname)) - - const promises = [ - createPhoto('scan.jpg', scan, dirname), - createPhoto('front.jpg', front, dirname) - ] - - return Promise.all(promises) + return Promise.all(photoPairs.map( + ([filename, data]) => createPhoto(filename, data, dirname) + )) } module.exports = { @@ -530,5 +533,6 @@ module.exports = { getMachineIds, emptyMachineUnits, refillMachineUnits, - updateDiagnostics + updateDiagnostics, + updateFailedQRScans } diff --git a/lib/poller.js b/lib/poller.js index 3c33c93b..f3cfff5e 100644 --- a/lib/poller.js +++ b/lib/poller.js @@ -1,3 +1,5 @@ +const fs = require('fs/promises') +const path = require('path') const _ = require('lodash/fp') const Queue = require('queue-promise') const plugins = require('./plugins') @@ -32,6 +34,7 @@ const RADAR_UPDATE_INTERVAL = 5 * T.minutes const PRUNE_MACHINES_HEARTBEAT = 1 * T.day const TRANSACTION_BATCH_LIFECYCLE = 20 * T.minutes const TICKER_RATES_INTERVAL = 59 * T.seconds +const FAILED_SCANS_INTERVAL = 1 * T.day const EXTERNAL_COMPLIANCE_INTERVAL = 1 * T.minutes const CHECK_NOTIFICATION_INTERVAL = 20 * T.seconds @@ -41,6 +44,8 @@ const CACHE_ENTRY_TTL = 3600 // seconds const FAST_QUEUE_WAIT = 1 * T.seconds const SLOW_QUEUE_WAIT = 10 * T.seconds +const OPERATOR_DATA_DIR = process.env.OPERATOR_DATA_DIR + const FAST_QUEUE = new Queue({ concurrent: 600, interval: FAST_QUEUE_WAIT @@ -129,6 +134,77 @@ function updateCoinAtmRadar () { .then(rates => coinAtmRadar.update(rates, settings())) } +const readdir = dirpath => + fs.readdir(dirpath, { withFileTypes: true }) + .then(_.map(entry => _.set('path', path.join(dirpath, entry.name), entry))) + +const readdirRec = rootPath => + readdir(rootPath) + .then(entries => Promise.all( + entries.map(entry => entry.isDirectory() ? readdirRec(entry.path) : [entry]) + )) + .then(_.flatten) + +const stat = path => fs.stat(path).then(_.set('path', path)) +const pathComponents = p => path.normalize(p).split(path.sep) + +// @see lib/customers.js:updateIdCardData() +const cleanOldFailedPDF417Scans = () => { + const matcher = (c, pat) => typeof pat === 'function' ? pat(c) : c === pat + const PDF417ScanPathPattern = _.concat( + pathComponents(OPERATOR_DATA_DIR), + ["id-operator", s => /* customerid*/ true, "idcarddata", fname => path.extname(fname) === 'jpg'] + ) + const isPDF417Scan = entry => { + entry = pathComponents(entry.path) + return entry.length === PDF417ScanPathPattern.length + && _.isMatchWith(matcher, PDF417ScanPathPattern, pathComponents(entry.path)) + } + + let old = new Date() + old.setDate(old.getDate() - 2) // 2 days ago + old = old.getTime() + + /* NOTE: Small caveat to mtime: last time the file was written to. */ + const isOld = filestat => filestat.mtimeMs < old + + readdirRec(path.join(OPERATOR_DATA_DIR, 'id-operator')) + .then(entries => Promise.all( + entries + .filter(entry => entry.isFile() && isPDF417Scan(entry)) + .map(entry => stat(entry.path)) + )) + .then(filestats => Promise.all( + filestats + .filter(isOld) + .map(_.flow(_.get(['path']), fs.unlink)) + )) + .catch(err => { + console.log("Error cleaning up failed PDF417 scans:", err) + }) +} + +// @see lib/machine-loader.js:updateFailedQRScans() +const cleanOldFailedQRScans = () => { + const old = new Date() + old.setDate(old.getDate() - 2) // 2 days ago + + const isOld = filepath => { + const then = new Date(path.basename(filepath).replace(/-[0-9]+\.jpg$/, '')) + return then < old + } + + readdirRec(path.join(OPERATOR_DATA_DIR, 'failedQRScans')) + .then(entries => Promise.all( + entries + .filter(entry => entry.isFile() && isOld(entry.path)) + .map(entry => fs.unlink(entry.path)) + )) + .catch(err => { + console.log("Error cleaning up failed QR scans:", err) + }) +} + // function checkExternalCompliance (settings) { // return customers.checkExternalCompliance(settings) // } @@ -213,6 +289,8 @@ function doPolling (schema) { addToQueue(updateAndLoadSanctions, SANCTIONS_UPDATE_INTERVAL, schema, QUEUE.SLOW) addToQueue(updateCoinAtmRadar, RADAR_UPDATE_INTERVAL, schema, QUEUE.SLOW) addToQueue(pi().pruneMachinesHeartbeat, PRUNE_MACHINES_HEARTBEAT, schema, QUEUE.SLOW, settings) + addToQueue(cleanOldFailedQRScans, FAILED_SCANS_INTERVAL, schema, QUEUE.SLOW, settings) + addToQueue(cleanOldFailedPDF417Scans, FAILED_SCANS_INTERVAL, schema, QUEUE.SLOW, settings) // addToQueue(checkExternalCompliance, EXTERNAL_COMPLIANCE_INTERVAL, schema, QUEUE.SLOW, settings) } diff --git a/lib/routes.js b/lib/routes.js index 4165ea58..e5ac126b 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -32,6 +32,7 @@ const verifyUserRoutes = require('./routes/verifyUserRoutes') const verifyTxRoutes = require('./routes/verifyTxRoutes') const verifyPromoCodeRoutes = require('./routes/verifyPromoCodeRoutes') const probeRoutes = require('./routes/probeLnRoutes') +const failedQRScansRoutes = require('./routes/failedQRScans') const graphQLServer = require('./graphql/server') @@ -75,6 +76,7 @@ app.use('/cashbox', cashboxRoutes) app.use('/network', performanceRoutes) app.use('/diagnostics', diagnosticsRoutes) +app.use('/failedqrscans', failedQRScansRoutes) app.use('/verify_user', verifyUserRoutes) app.use('/verify_transaction', verifyTxRoutes) diff --git a/lib/routes/failedQRScans.js b/lib/routes/failedQRScans.js new file mode 100644 index 00000000..e484ef36 --- /dev/null +++ b/lib/routes/failedQRScans.js @@ -0,0 +1,14 @@ +const express = require('express') +const router = express.Router() + +const { updateFailedQRScans } = require('../machine-loader') + +function failedQRScans (req, res, next) { + return updateFailedQRScans(req.deviceId, req.body) + .then(() => res.status(200).send({ status: 'OK' })) + .catch(next) +} + +router.post('/', failedQRScans) + +module.exports = router