Răsfoiți Sursa

Работа над загрузкой файла на сервер

Book Pauk 6 ani în urmă
părinte
comite
b33911b8ec

+ 31 - 0
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();

+ 9 - 2
client/components/Reader/LoaderPage/LoaderPage.vue

@@ -10,7 +10,8 @@
                 <el-button slot="append" icon="el-icon-check" @click="submitUrl"></el-button>
             </el-input>
             <div class="space"></div>
-            <el-button size="mini" @click="loadFle">
+            <input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/>
+            <el-button size="mini" @click="loadFileClick">
                 Загрузить файл с диска
             </el-button>
             <div class="space"></div>
@@ -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() {

+ 1 - 0
client/components/Reader/ProgressPage/ProgressPage.vue

@@ -21,6 +21,7 @@ const ruMessage = {
     'convert': 'конвертирование',
     'loading': 'загрузка',
     'parse': 'обработка',
+    'upload': 'отправка',
 };
 
 export default @Component({

+ 24 - 0
client/components/Reader/Reader.vue

@@ -50,6 +50,7 @@
             <keep-alive>
                 <component ref="page" :is="activePage"
                     @load-book="loadBook"
+                    @load-file="loadFile"
                     @book-pos-changed="bookPosChanged"
                     @tool-bar-toggle="toolBarToggle"
                     @full-screen-toogle="fullScreenToggle"
@@ -647,6 +648,29 @@ class Reader extends Vue {
         });
     }
 
+    loadFile(opts) {
+        this.progressActive = true;
+        this.$nextTick(async() => {
+            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) {

+ 91 - 5
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",

+ 1 - 0
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",

+ 1 - 0
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,
 

+ 1 - 0
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: [
             {

+ 18 - 12
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.type == 'file')
-                throw new Error(`file loading is not supported yet`);
+            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 == '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;
         }

+ 4 - 0
server/core/ReaderWorker.js

@@ -88,6 +88,10 @@ class ReaderWorker {
 
         return workerId;
     }
+
+    async saveFile(file) {
+        return `file://${file.filename}`;
+    }
 }
 
 module.exports = ReaderWorker;

+ 1 - 0
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);
 }

+ 25 - 3
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}`);