diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 54fd1d5b..2e10f188 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -1,5 +1,5 @@ -const log = require('../core/getLogger').getLog(); const BaseController = require('./BaseController'); +const log = require('../core/getLogger').getLog(); const _ = require('lodash'); class MiscController extends BaseController { diff --git a/server/controllers/ReaderController.js b/server/controllers/ReaderController.js new file mode 100644 index 00000000..fecc9cc1 --- /dev/null +++ b/server/controllers/ReaderController.js @@ -0,0 +1,39 @@ +const BaseController = require('./BaseController'); +const ReaderWorker = require('../core/ReaderWorker'); +const workerState = require('../core/workerState'); +//const log = require('../core/getLogger').getLog(); +//const _ = require('lodash'); + +class ReaderController extends BaseController { + constructor(connPool, config) { + super(connPool, config); + this.readerWorker = new ReaderWorker(config); + } + + async loadBook(req, res) { + const request = req.body; + let error = ''; + try { + if (!request.type || !(request.type == 'url' || request.type == 'file')) + throw new Error(`key 'type' is wrong`); + + if (request.type == 'file') + throw new Error(`file loading is not supported yet`); + + if (request.type == 'url') { + if (!request.url) + throw new Error(`key 'url' is empty`); + const workerId = this.readerWorker.loadBookUrl(request.url); + const state = workerState.getState(workerId); + return (state ? state : {}); + } + } catch (e) { + error = e.message; + } + //bad request + res.status(400).send({error}); + return false; + } +} + +module.exports = ReaderController; diff --git a/server/controllers/WorkerController.js b/server/controllers/WorkerController.js new file mode 100644 index 00000000..3437077f --- /dev/null +++ b/server/controllers/WorkerController.js @@ -0,0 +1,23 @@ +const BaseController = require('./BaseController'); +const workerState = require('../core/workerState'); + +class WorkerController extends BaseController { + async getState(req, res) { + const request = req.body; + let error = ''; + try { + if (!request.workerId) + throw new Error(`key 'workerId' is wrong`); + + const state = workerState.getState(request.workerId); + return (state ? state : {}); + } catch (e) { + error = e.message; + } + //bad request + res.status(400).send({error}); + return false; + } +} + +module.exports = WorkerController; diff --git a/server/controllers/index.js b/server/controllers/index.js index 9e6aace2..fbbad7b7 100644 --- a/server/controllers/index.js +++ b/server/controllers/index.js @@ -1,3 +1,5 @@ module.exports = { MiscController: require('./MiscController'), + ReaderController: require('./ReaderController'), + WorkerController: require('./WorkerController'), } \ No newline at end of file diff --git a/server/core/ReaderWorker.js b/server/core/ReaderWorker.js new file mode 100644 index 00000000..f47280dd --- /dev/null +++ b/server/core/ReaderWorker.js @@ -0,0 +1,46 @@ +const workerState = require('./workerState'); +const utils = require('./utils'); + +const fs = require('fs-extra'); +const util = require('util'); +const stream = require('stream'); +const pipeline = util.promisify(stream.pipeline); +const download = require('download'); + +class ReaderWorker { + constructor(config) { + this.config = config; + this.tempDownloadDir = `${config.tempDir}/download`; + fs.ensureDirSync(this.tempDownloadDir); + } + + async loadBook(wState, url) { + try { + wState.set({state: 'download', step: 1, totalSteps: 3, url}); + + const tempFilename = utils.randomHexString(30); + const d = download(url); + d.on('downloadProgress', progress => { + wState.set({progress: Math.round(progress.percent*100)}); + }) + + await pipeline(d, fs.createWriteStream(`${this.tempDownloadDir}/${tempFilename}`)); + + wState.finish({step: 3, file: tempFilename}); + } catch (e) { + wState.set({state: 'error', error: e.message}); + } + } + + loadBookUrl(url) { + const workerId = workerState.generateWorkerId(); + const wState = workerState.getControl(workerId); + wState.set({state: 'start'}); + + this.loadBook(wState, url); + + return workerId; + } +} + +module.exports = ReaderWorker; \ No newline at end of file diff --git a/server/core/utils.js b/server/core/utils.js index 6f3233f6..2daf6118 100644 --- a/server/core/utils.js +++ b/server/core/utils.js @@ -1,7 +1,14 @@ +const crypto = require('crypto'); + function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } +function randomHexString(len) { + return crypto.randomBytes(len).toString('hex') +} + module.exports = { - sleep + sleep, + randomHexString }; \ No newline at end of file diff --git a/server/core/workerState.js b/server/core/workerState.js new file mode 100644 index 00000000..b9526d27 --- /dev/null +++ b/server/core/workerState.js @@ -0,0 +1,56 @@ +const utils = require('./utils'); + +const cleanInterval = 3600; //sec +const cleanAfterLastModified = cleanInterval - 60; //sec + +class WorkerState { + constructor() { + this.states = {}; + setTimeout(this.cleanStates.bind(this), cleanInterval*1000); + } + + generateWorkerId() { + return utils.randomHexString(20); + } + + getControl(workerId) { + return { + set: state => this.setState(workerId, state), + finish: state => this.finishState(workerId, state), + get: workerId => this.getState(workerId), + }; + } + + setState(workerId, state) { + this.states[workerId] = Object.assign({}, this.states[workerId], state, { + workerId, + lastModified: Date.now() + }); + } + + finishState(workerId, state) { + this.states[workerId] = Object.assign({}, this.states[workerId], state, { + workerId, + state: 'finish', + lastModified: Date.now() + }); + } + + getState(workerId) { + return this.states[workerId]; + } + + cleanStates() { + const now = Date.now(); + for (let workerID in this.states) { + if ((now - this.states[workerID].lastModified) >= cleanAfterLastModified*1000) { + delete this.states[workerID]; + } + } + setTimeout(this.cleanStates.bind(this), cleanInterval*1000); + } +} + +const workerState = new WorkerState(); + +module.exports = workerState; \ No newline at end of file diff --git a/server/routes.js b/server/routes.js index 6897586b..7bb412d5 100644 --- a/server/routes.js +++ b/server/routes.js @@ -2,14 +2,18 @@ const c = require('./controllers'); function initRoutes(app, connPool, config) { const misc = new c.MiscController(connPool, config); + const reader = new c.ReaderController(connPool, config); + const worker = new c.WorkerController(connPool, config); //access - const [all, normal, site, reader, omnireader] = // eslint-disable-line no-unused-vars + const [aAll, aNormal, aSite, aReader, aOmnireader] = // eslint-disable-line no-unused-vars [config.mode, 'normal', 'site', 'reader', 'omnireader']; //routes const routes = [ - ['POST', '/api/config', misc.getConfig.bind(misc), [all], {}], + ['POST', '/api/config', misc.getConfig.bind(misc), [aAll], {}], + ['POST', '/api/reader/load-book', reader.loadBook.bind(reader), [aAll], {}], + ['POST', '/api/worker/get-state', worker.getState.bind(worker), [aAll], {}], ]; //to app