diff --git a/client/api/reader.js b/client/api/reader.js index 91cb1e98..1088cf26 100644 --- a/client/api/reader.js +++ b/client/api/reader.js @@ -1,6 +1,7 @@ import axios from 'axios'; import {sleep} from '../share/utils'; +const maxFileUploadSize = 10*1024*1024; const api = axios.create({ baseURL: '/api/reader' }); @@ -65,6 +66,36 @@ class Reader { //загрузка return await axios.get(url, options); } + + async uploadFile(file, callback) { + if (file.size > maxFileUploadSize) + throw new Error(`Размер файла превышает ${maxFileUploadSize} байт`); + + let formData = new FormData(); + formData.append('file', file); + + const options = { + headers: { + 'Content-Type': 'multipart/form-data' + }, + onUploadProgress: progress => { + const total = (progress.total ? progress.total : progress.loaded + 200000); + if (callback) + callback({state: 'upload', progress: Math.round((progress.loaded*100)/total)}); + } + + }; + + let response = await api.post('/upload-file', formData, options); + if (response.data.state == 'error') + throw new Error(response.data.error); + + const url = response.data.url; + if (!url) + throw new Error('Неверный ответ api'); + + return url; + } } export default new Reader(); \ No newline at end of file diff --git a/client/components/Reader/LoaderPage/LoaderPage.vue b/client/components/Reader/LoaderPage/LoaderPage.vue index 75e7ddb9..923bf1a7 100644 --- a/client/components/Reader/LoaderPage/LoaderPage.vue +++ b/client/components/Reader/LoaderPage/LoaderPage.vue @@ -10,7 +10,8 @@
- + + Загрузить файл с диска
@@ -66,7 +67,13 @@ class LoaderPage extends Vue { } } - loadFle() { + loadFileClick() { + this.$refs.file.click(); + } + + loadFile() { + const file = this.$refs.file.files[0]; + this.$emit('load-file', {file}); } openHelp() { diff --git a/client/components/Reader/ProgressPage/ProgressPage.vue b/client/components/Reader/ProgressPage/ProgressPage.vue index cae5a345..7d1e9cb1 100644 --- a/client/components/Reader/ProgressPage/ProgressPage.vue +++ b/client/components/Reader/ProgressPage/ProgressPage.vue @@ -21,6 +21,7 @@ const ruMessage = { 'convert': 'конвертирование', 'loading': 'загрузка', 'parse': 'обработка', + 'upload': 'отправка', }; export default @Component({ diff --git a/client/components/Reader/Reader.vue b/client/components/Reader/Reader.vue index a45ad777..585e027e 100644 --- a/client/components/Reader/Reader.vue +++ b/client/components/Reader/Reader.vue @@ -50,6 +50,7 @@ { + const progress = this.$refs.page; + try { + progress.show(); + progress.setState({state: 'upload'}); + + const url = await readerApi.uploadFile(opts.file, (state) => { + progress.setState(state); + }); + + this.loadBook(url); + + progress.hide(); this.progressActive = false; + } catch (e) { + progress.hide(); this.progressActive = false; + this.loaderActive = true; + this.$alert(e.message, 'Ошибка', {type: 'error'}); + } + }); + } + blinkCachedLoadMessage() { this.blinkCount = 30; if (!this.inBlink) { diff --git a/package-lock.json b/package-lock.json index 8098ff27..698fbd36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -619,6 +619,11 @@ "normalize-path": "^2.1.1" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -1840,8 +1845,7 @@ "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "buffer-xor": { "version": "1.0.3", @@ -1855,6 +1859,38 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", @@ -2379,7 +2415,6 @@ "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, "requires": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -3251,6 +3286,38 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -6521,6 +6588,21 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "multer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.1.tgz", + "integrity": "sha512-zzOLNRxzszwd+61JFuAo0fxdQfvku12aNJgnla0AQ+hHxFmfc/B7jBVuPr5Rmvu46Jze/iJrFpSOsD7afO8SDw==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, "multistream": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/multistream/-/multistream-2.1.1.tgz", @@ -10405,6 +10487,11 @@ "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", "dev": true }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -11036,8 +11123,7 @@ "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "uglify-js": { "version": "3.4.9", diff --git a/package.json b/package.json index 81a98c46..379f402d 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "iconv-lite": "^0.4.24", "localforage": "^1.7.3", "lodash": "^4.17.11", + "multer": "^1.4.1", "path-browserify": "^1.0.0", "sql-template-strings": "^2.2.2", "sqlite": "^3.0.0", diff --git a/server/config/base.js b/server/config/base.js index d27eb37f..0670e60b 100644 --- a/server/config/base.js +++ b/server/config/base.js @@ -13,6 +13,7 @@ module.exports = { tempDir: `${dataDir}/tmp`, logDir: `${dataDir}/log`, publicDir: `${execDir}/public`, + uploadDir: `${execDir}/public/upload`, dbFileName: 'db.sqlite', loggingEnabled: true, diff --git a/server/config/production.js b/server/config/production.js index f9067cf4..98c40e72 100644 --- a/server/config/production.js +++ b/server/config/production.js @@ -10,6 +10,7 @@ module.exports = Object.assign({}, base, { tempDir: `${dataDir}/tmp`, logDir: `${dataDir}/log`, publicDir: `${execDir}/public`, + uploadDir: `${execDir}/public/upload`, servers: [ { diff --git a/server/controllers/ReaderController.js b/server/controllers/ReaderController.js index fecc9cc1..ae92f3a2 100644 --- a/server/controllers/ReaderController.js +++ b/server/controllers/ReaderController.js @@ -14,19 +14,25 @@ class ReaderController extends BaseController { 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.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; + } - 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 : {}); - } + async uploadFile(req, res) { + const file = req.file; + let error = ''; + try { + const url = await this.readerWorker.saveFile(file); + return ({url}); } catch (e) { error = e.message; } diff --git a/server/core/ReaderWorker.js b/server/core/ReaderWorker.js index ffccbe43..b90c64ed 100644 --- a/server/core/ReaderWorker.js +++ b/server/core/ReaderWorker.js @@ -88,6 +88,10 @@ class ReaderWorker { return workerId; } + + async saveFile(file) { + return `file://${file.filename}`; + } } module.exports = ReaderWorker; \ No newline at end of file diff --git a/server/index.js b/server/index.js index 4e655661..ff7157a2 100644 --- a/server/index.js +++ b/server/index.js @@ -13,6 +13,7 @@ const SqliteConnectionPool = require('./core/SqliteConnectionPool'); async function init() { await fs.ensureDir(config.dataDir); + await fs.ensureDir(config.uploadDir); await fs.ensureDir(config.tempDir); await fs.emptyDir(config.tempDir); } diff --git a/server/routes.js b/server/routes.js index 7bb412d5..1bee3b3a 100644 --- a/server/routes.js +++ b/server/routes.js @@ -1,4 +1,6 @@ const c = require('./controllers'); +const utils = require('./core/utils'); +const multer = require('multer'); function initRoutes(app, connPool, config) { const misc = new c.MiscController(connPool, config); @@ -9,16 +11,35 @@ function initRoutes(app, connPool, config) { const [aAll, aNormal, aSite, aReader, aOmnireader] = // eslint-disable-line no-unused-vars [config.mode, 'normal', 'site', 'reader', 'omnireader']; + //multer + const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, config.uploadDir); + }, + filename: (req, file, cb) => { + cb(null, utils.randomHexString(30)); + } + }); + const upload = multer({ storage, limits: {fileSize: 10*1024*1024} }); + //routes const routes = [ ['POST', '/api/config', misc.getConfig.bind(misc), [aAll], {}], ['POST', '/api/reader/load-book', reader.loadBook.bind(reader), [aAll], {}], + ['POST', '/api/reader/upload-file', [upload.single('file'), reader.uploadFile.bind(reader)], [aAll], {}], ['POST', '/api/worker/get-state', worker.getState.bind(worker), [aAll], {}], ]; //to app for (let route of routes) { - let [httpMethod, path, controller, access, options] = route; + let callbacks = []; + let [httpMethod, path, controllers, access, options] = route; + let controller = controllers; + if (Array.isArray(controllers)) { + controller = controllers[controllers.length - 1]; + callbacks = controllers.slice(0, -1); + } + access = new Set(access); let callback = () => {}; @@ -38,13 +59,14 @@ function initRoutes(app, connPool, config) { res.status(403); }; } + callbacks.push(callback); switch (httpMethod) { case 'GET' : - app.get(path, callback); + app.get(path, ...callbacks); break; case 'POST': - app.post(path, callback); + app.post(path, ...callbacks); break; default: throw new Error(`initRoutes error: unknown httpMethod: ${httpMethod}`);