Book Pauk 5 лет назад
Родитель
Сommit
82f5ed4c44

+ 6 - 0
client/api/reader.js

@@ -27,6 +27,9 @@ class Reader {
                 response = await wsc.message(requestId);
                 response = await wsc.message(requestId);
                 callback(response);
                 callback(response);
 
 
+                if (!response.state)
+                    throw new Error('Неверный ответ api');
+
                 if (response.state == 'finish' || response.state == 'error') {
                 if (response.state == 'finish' || response.state == 'error') {
                     break;
                     break;
                 }
                 }
@@ -47,6 +50,9 @@ class Reader {
             response = response.data;
             response = response.data;
             callback(response);
             callback(response);
 
 
+            if (!response.state)
+                throw new Error('Неверный ответ api');
+
             if (response.state == 'finish' || response.state == 'error') {
             if (response.state == 'finish' || response.state == 'error') {
                 break;
                 break;
             }
             }

+ 40 - 10
client/components/Reader/HelpPage/DonateHelpPage/DonateHelpPage.vue

@@ -1,30 +1,54 @@
 <template>
 <template>
     <div class="page">
     <div class="page">
         <div class="box">
         <div class="box">
-            <p class="p">Проект существует исключительно на личном энтузиазме.</p>
-            <p class="p">Чтобы энтузиазма было побольше, вы можете пожертвовать на развитие проекта любую сумму:</p>
+            <p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
             <div class="address">
             <div class="address">
                 <img class="logo" src="./assets/yandex.png">
                 <img class="logo" src="./assets/yandex.png">
                 <el-button class="button" @click="donateYandexMoney">Пожертвовать</el-button><br>
                 <el-button class="button" @click="donateYandexMoney">Пожертвовать</el-button><br>
-                <div class="para">{{ yandexAddress }}</div>
+                <div class="para">{{ yandexAddress }}
+                    <el-tooltip :open-delay="500" effect="light">
+                        <template slot="content">
+                            Скопировать
+                        </template>
+                        <i class="el-icon-copy-document copy-icon" @click="copyAddress(yandexAddress, 'Яндекс кошелек')"></i>
+                    </el-tooltip>
+                </div>
             </div>
             </div>
 
 
             <div class="address">                
             <div class="address">                
                 <img class="logo" src="./assets/bitcoin.png">
                 <img class="logo" src="./assets/bitcoin.png">
-                <el-button class="button" @click="copyAddress(bitcoinAddress, 'Bitcoin')">Скопировать</el-button><br>
-                <div class="para">{{ bitcoinAddress }}</div>
+                <div class="para">{{ bitcoinAddress }}
+                    <el-tooltip :open-delay="500" effect="light">
+                        <template slot="content">
+                            Скопировать
+                        </template>
+                        <i class="el-icon-copy-document copy-icon" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')"></i>
+                    </el-tooltip>
+                </div>
             </div>
             </div>
 
 
             <div class="address">                
             <div class="address">                
                 <img class="logo" src="./assets/litecoin.png">
                 <img class="logo" src="./assets/litecoin.png">
-                <el-button class="button" @click="copyAddress(litecoinAddress, 'Litecoin')">Скопировать</el-button><br>
-                <div class="para">{{ litecoinAddress }}</div>
+                <div class="para">{{ litecoinAddress }}
+                    <el-tooltip :open-delay="500" effect="light">
+                        <template slot="content">
+                            Скопировать
+                        </template>
+                        <i class="el-icon-copy-document copy-icon" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')"></i>
+                    </el-tooltip>
+                </div>
             </div>
             </div>
 
 
             <div class="address">                
             <div class="address">                
                 <img class="logo" src="./assets/monero.png">
                 <img class="logo" src="./assets/monero.png">
-                <el-button class="button" @click="copyAddress(moneroAddress, 'Monero')">Скопировать</el-button><br>
-                <div class="para">{{ moneroAddress }}</div>
+                <div class="para">{{ moneroAddress }}
+                    <el-tooltip :open-delay="500" effect="light">
+                        <template slot="content">
+                            Скопировать
+                        </template>
+                        <i class="el-icon-copy-document copy-icon" @click="copyAddress(moneroAddress, 'Monero-адрес')"></i>
+                    </el-tooltip>
+                </div>
             </div>
             </div>
         </div>
         </div>
     </div>
     </div>
@@ -54,7 +78,7 @@ class DonateHelpPage extends Vue {
     async copyAddress(address, prefix) {
     async copyAddress(address, prefix) {
         const result = await copyTextToClipboard(address);
         const result = await copyTextToClipboard(address);
         if (result)
         if (result)
-            this.$notify.success({message: `${prefix}-адрес ${address} успешно скопирован в буфер обмена`});
+            this.$notify.success({message: `${prefix} ${address} успешно скопирован в буфер обмена`});
         else
         else
             this.$notify.error({message: 'Копирование не удалось'});
             this.$notify.error({message: 'Копирование не удалось'});
     }
     }
@@ -106,4 +130,10 @@ h5 {
     position: relative;
     position: relative;
     top: 10px;
     top: 10px;
 }
 }
+
+.copy-icon {
+    margin-left: 10px;
+    cursor: pointer;
+    font-size: 120%;
+}
 </style>
 </style>

+ 8 - 2
client/components/Reader/ProgressPage/ProgressPage.vue

@@ -16,6 +16,7 @@ const ruMessage = {
     'start': ' ',
     'start': ' ',
     'finish': ' ',
     'finish': ' ',
     'error': ' ',
     'error': ' ',
+    'queue': 'очередь',
     'download': 'скачивание',
     'download': 'скачивание',
     'decompress': 'распаковка',
     'decompress': 'распаковка',
     'convert': 'конвертирование',
     'convert': 'конвертирование',
@@ -49,8 +50,13 @@ class ProgressPage extends Vue {
     }
     }
 
 
     setState(state) {
     setState(state) {
-        if (state.state)
-            this.text = (ruMessage[state.state] ? ruMessage[state.state] : state.state);
+        if (state.state) {
+            if (state.state == 'queue') {
+                this.text = 'Номер в очереди: ' + (state.place ? state.place : '');
+            } else {
+                this.text = (ruMessage[state.state] ? ruMessage[state.state] : state.state);
+            }
+        }
         this.step = (state.step ? state.step : this.step);
         this.step = (state.step ? state.step : this.step);
         this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
         this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
         this.progress = state.progress || 0;
         this.progress = state.progress || 0;

+ 91 - 2
client/components/Reader/Reader.vue

@@ -90,6 +90,53 @@
                 </span>
                 </span>
             </el-dialog>
             </el-dialog>
 
 
+            <el-dialog
+                title="Здравствуйте, уважаемые читатели!"
+                :visible.sync="donationVisible"
+                width="90%">
+                <div style="word-break: normal">
+                    Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
+
+                    Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей.
+                    В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый
+                    сможет проголосовать рублем за то, чтобы читалка так и оставалась:
+
+                    <ul>
+                        <li>непрерывно улучшаемой</li>
+                        <li>без рекламы</li>
+                        <li>без регистрации</li>
+                        <li>Open Source</li>
+                    </ul>
+
+                    Автор также обращается с просьбой о помощи в распространении 
+                    <a href="https://omnireader.ru" target="_blank">ссылки</a>
+                    <el-tooltip :open-delay="500" effect="light">
+                        <template slot="content">
+                            Скопировать
+                        </template>
+                        <i class="el-icon-copy-document" style="cursor: pointer; font-size: 100%" @click="copyLink('https://omnireader.ru')"></i>
+                    </el-tooltip>
+                    на читалку через тематические форумы, соцсети, мессенджеры и пр.
+                    Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
+
+                    <br><br>
+                    Если соберется бóльшая сумма, то разработка децентрализованной библиотеки для свободного обмена книгами будет по возможности ускорена.
+                    <br><br>
+                    P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
+
+                    <br><br>
+                    <el-row type="flex" justify="center">
+                        <el-button type="success" round @click="openDonate">Помочь проекту</el-button>
+                    </el-row>
+                </div>
+
+                <span slot="footer" class="dialog-footer">
+                    <span class="clickable" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>                        
+                    <br><br>
+                    <el-button @click="donationDialogRemind">Напомнить позже</el-button>
+                </span>
+            </el-dialog>
+
         </el-main>
         </el-main>
     </el-container>
     </el-container>
 </template>
 </template>
@@ -200,6 +247,7 @@ class Reader extends Vue {
 
 
     whatsNewVisible = false;
     whatsNewVisible = false;
     whatsNewContent = '';
     whatsNewContent = '';
+    donationVisible = false;
 
 
     created() {
     created() {
         this.loading = true;
         this.loading = true;
@@ -258,9 +306,10 @@ class Reader extends Vue {
             this.checkActivateDonateHelpPage();
             this.checkActivateDonateHelpPage();
             this.loading = false;
             this.loading = false;
 
 
-            await this.showWhatsNew();
-
             this.updateRoute();
             this.updateRoute();
+
+            await this.showWhatsNew();
+            await this.showDonation();
         })();
         })();
     }
     }
 
 
@@ -272,6 +321,7 @@ class Reader extends Vue {
         this.clickControl = settings.clickControl;
         this.clickControl = settings.clickControl;
         this.blinkCachedLoad = settings.blinkCachedLoad;
         this.blinkCachedLoad = settings.blinkCachedLoad;
         this.showWhatsNewDialog = settings.showWhatsNewDialog;
         this.showWhatsNewDialog = settings.showWhatsNewDialog;
+        this.showDonationDialog2020 = settings.showDonationDialog2020;
         this.showToolButton = settings.showToolButton;
         this.showToolButton = settings.showToolButton;
         this.enableSitesFilter = settings.enableSitesFilter;
         this.enableSitesFilter = settings.enableSitesFilter;
 
 
@@ -337,6 +387,41 @@ class Reader extends Vue {
         }
         }
     }
     }
 
 
+    async showDonation() {
+        await utils.sleep(3000);
+        const today = utils.formatDate(new Date(), 'coDate');
+
+        if (this.mode == 'omnireader' && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
+            this.donationVisible = true;
+        }
+    }
+
+    donationDialogDisable() {
+        this.donationVisible = false;
+        if (this.showDonationDialog2020) {
+            const newSettings = Object.assign({}, this.settings, { showDonationDialog2020: false });
+            this.commit('reader/setSettings', newSettings);
+        }
+    }
+
+    donationDialogRemind() {
+        this.donationVisible = false;
+        this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate'));
+    }
+
+    openDonate() {
+        this.donationVisible = false;
+        this.donateToggle();
+    }
+
+    async copyLink(link) {
+        const result = await utils.copyTextToClipboard(link);
+        if (result)
+            this.$notify.success({message: `Ссылка ${link} успешно скопирована в буфер обмена`});
+        else
+            this.$notify.error({message: 'Копирование не удалось'});
+    }
+
     openVersionHistory() {
     openVersionHistory() {
         this.whatsNewVisible = false;
         this.whatsNewVisible = false;
         this.versionHistoryToggle();
         this.versionHistoryToggle();
@@ -455,6 +540,10 @@ class Reader extends Vue {
         return this.$store.state.reader.whatsNewContentHash;
         return this.$store.state.reader.whatsNewContentHash;
     }
     }
 
 
+    get donationRemindDate() {
+        return this.$store.state.reader.donationRemindDate;
+    }
+
     addAction(pos) {
     addAction(pos) {
         let a = this.actionList;
         let a = this.actionList;
         if (!a.length || a[a.length - 1] != pos) {
         if (!a.length || a[a.length - 1] != pos) {

+ 8 - 0
client/components/Reader/SettingsPage/SettingsPage.vue

@@ -471,6 +471,14 @@
                             <el-checkbox v-model="showWhatsNewDialog">Показывать уведомление "Что нового"</el-checkbox>
                             <el-checkbox v-model="showWhatsNewDialog">Показывать уведомление "Что нового"</el-checkbox>
                         </el-tooltip>
                         </el-tooltip>
                     </el-form-item>
                     </el-form-item>
+                    <el-form-item label="Уведомление">
+                        <el-tooltip :open-delay="500" effect="light">
+                            <template slot="content">
+                                Показывать уведомление "Оплатим хостинг вместе"
+                            </template>
+                            <el-checkbox v-model="showDonationDialog2020">Показывать "Оплатим хостинг вместе"</el-checkbox>
+                        </el-tooltip>
+                    </el-form-item>
                 </el-form>
                 </el-form>
 
 
                 <el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
                 <el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>

+ 12 - 0
client/components/Reader/versionHistory.js

@@ -1,4 +1,16 @@
 export const versionHistory = [
 export const versionHistory = [
+{
+    showUntil: '2020-01-27',
+    header: '0.8.3 (2020-01-28)',
+    content:
+`
+<ul>
+    <li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
+    <li>внутренние оптимизации</li>
+</ul>
+`
+},
+
 {
 {
     showUntil: '2020-01-19',
     showUntil: '2020-01-19',
     header: '0.8.2 (2020-01-20)',
     header: '0.8.2 (2020-01-20)',

+ 2 - 1
client/element.js

@@ -19,6 +19,7 @@ import ElCheckbox from 'element-ui/lib/checkbox';
 import ElTabs from 'element-ui/lib/tabs';
 import ElTabs from 'element-ui/lib/tabs';
 import ElTabPane from 'element-ui/lib/tab-pane';
 import ElTabPane from 'element-ui/lib/tab-pane';
 import ElTooltip from 'element-ui/lib/tooltip';
 import ElTooltip from 'element-ui/lib/tooltip';
+import ElRow from 'element-ui/lib/row';
 import ElCol from 'element-ui/lib/col';
 import ElCol from 'element-ui/lib/col';
 import ElContainer from 'element-ui/lib/container';
 import ElContainer from 'element-ui/lib/container';
 import ElAside from 'element-ui/lib/aside';
 import ElAside from 'element-ui/lib/aside';
@@ -43,7 +44,7 @@ import MessageBox from 'element-ui/lib/message-box';
 
 
 const components = {
 const components = {
     ElMenu, ElMenuItem, ElButton, ElButtonGroup, ElCheckbox, ElTabs, ElTabPane, ElTooltip,
     ElMenu, ElMenuItem, ElButton, ElButtonGroup, ElCheckbox, ElTabs, ElTabPane, ElTooltip,
-    ElCol, ElContainer, ElAside, ElMain, ElHeader,
+    ElRow, ElCol, ElContainer, ElAside, ElMain, ElHeader,
     ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn,
     ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn,
     ElProgress, ElSlider, ElForm, ElFormItem,
     ElProgress, ElSlider, ElForm, ElFormItem,
     ElColorPicker, ElDialog,
     ElColorPicker, ElDialog,

+ 5 - 0
client/store/modules/reader.js

@@ -182,6 +182,7 @@ const settingDefaults = {
     imageFitWidth: true,
     imageFitWidth: true,
     showServerStorageMessages: true,
     showServerStorageMessages: true,
     showWhatsNewDialog: true,
     showWhatsNewDialog: true,
+    showDonationDialog2020: true,
     enableSitesFilter: true,
     enableSitesFilter: true,
 
 
     fontShifts: {},
     fontShifts: {},
@@ -204,6 +205,7 @@ const state = {
     profilesRev: 0,
     profilesRev: 0,
     allowProfilesSave: false,//подстраховка для разработки
     allowProfilesSave: false,//подстраховка для разработки
     whatsNewContentHash: '',
     whatsNewContentHash: '',
+    donationRemindDate: '',
     currentProfile: '',
     currentProfile: '',
     settings: Object.assign({}, settingDefaults),
     settings: Object.assign({}, settingDefaults),
     settingsRev: {},
     settingsRev: {},
@@ -238,6 +240,9 @@ const mutations = {
     setWhatsNewContentHash(state, value) {
     setWhatsNewContentHash(state, value) {
         state.whatsNewContentHash = value;
         state.whatsNewContentHash = value;
     },
     },
+    setDonationRemindDate(state, value) {
+        state.donationRemindDate = value;
+    },
     setCurrentProfile(state, value) {
     setCurrentProfile(state, value) {
         state.currentProfile = value;
         state.currentProfile = value;
     },
     },

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "Liberama",
   "name": "Liberama",
-  "version": "0.8.2",
+  "version": "0.8.3",
   "author": "Book Pauk <bookpauk@gmail.com>",
   "author": "Book Pauk <bookpauk@gmail.com>",
   "license": "CC0-1.0",
   "license": "CC0-1.0",
   "repository": "bookpauk/liberama",
   "repository": "bookpauk/liberama",

+ 22 - 4
server/core/FileDecompressor.js

@@ -10,8 +10,9 @@ const utils = require('./utils');
 const FileDetector = require('./FileDetector');
 const FileDetector = require('./FileDetector');
 
 
 class FileDecompressor {
 class FileDecompressor {
-    constructor() {
+    constructor(limitFileSize = 0) {
         this.detector = new FileDetector();
         this.detector = new FileDetector();
+        this.limitFileSize = limitFileSize;
     }
     }
 
 
     async decompressNested(filename, outputDir) {
     async decompressNested(filename, outputDir) {
@@ -113,7 +114,7 @@ class FileDecompressor {
 
 
     async unZip(filename, outputDir) {
     async unZip(filename, outputDir) {
         const zip = new ZipStreamer();
         const zip = new ZipStreamer();
-        return await zip.unpack(filename, outputDir);
+        return await zip.unpack(filename, outputDir, null, this.limitFileSize);
     }
     }
 
 
     unBz2(filename, outputDir) {
     unBz2(filename, outputDir) {
@@ -125,9 +126,16 @@ class FileDecompressor {
     }
     }
 
 
     unTar(filename, outputDir) {
     unTar(filename, outputDir) {
-        return new Promise((resolve, reject) => {
+        return new Promise((resolve, reject) => { (async() => {
             const files = [];
             const files = [];
 
 
+            if (this.limitFileSize) {
+                if ((await fs.stat(filename)).size > this.limitFileSize) {
+                    reject('Файл слишком большой');
+                    return;
+                }
+            }
+
             const tarExtract = tar.extract(outputDir, {
             const tarExtract = tar.extract(outputDir, {
                 map: (header) => {
                 map: (header) => {
                     files.push({path: header.name, size: header.size});
                     files.push({path: header.name, size: header.size});
@@ -149,7 +157,7 @@ class FileDecompressor {
             });
             });
 
 
             inputStream.pipe(tarExtract);
             inputStream.pipe(tarExtract);
-        });
+        })().catch(reject); });
     }
     }
 
 
     decompressByStream(stream, filename, outputDir) {
     decompressByStream(stream, filename, outputDir) {
@@ -174,6 +182,16 @@ class FileDecompressor {
             });
             });
 
 
             stream.on('error', reject);
             stream.on('error', reject);
+
+            if (this.limitFileSize) {
+                let readSize = 0;
+                stream.on('data', (buffer) => {
+                    readSize += buffer.length;
+                    if (readSize > this.limitFileSize)
+                        stream.destroy(new Error('Файл слишком большой'));
+                });
+            }
+
             inputStream.on('error', reject);
             inputStream.on('error', reject);
             outputStream.on('error', reject);
             outputStream.on('error', reject);
         
         

+ 16 - 9
server/core/FileDownloader.js

@@ -1,12 +1,11 @@
 const got = require('got');
 const got = require('got');
 
 
-const maxDownloadSize = 50*1024*1024;
-
 class FileDownloader {
 class FileDownloader {
-    constructor() {
+    constructor(limitDownloadSize = 0) {
+        this.limitDownloadSize = limitDownloadSize;
     }
     }
 
 
-    async load(url, callback) {
+    async load(url, callback, abort) {
         let errMes = '';
         let errMes = '';
         const options = {
         const options = {
             encoding: null,
             encoding: null,
@@ -23,10 +22,14 @@ class FileDownloader {
         }
         }
 
 
         let prevProg = 0;
         let prevProg = 0;
-        const request = got(url, options).on('downloadProgress', progress => {
-            if (progress.transferred > maxDownloadSize) {
-                errMes = 'file too big';
-                request.cancel();
+        const request = got(url, options);
+
+        request.on('downloadProgress', progress => {
+            if (this.limitDownloadSize) {
+                if (progress.transferred > this.limitDownloadSize) {
+                    errMes = 'Файл слишком большой';
+                    request.cancel();
+                }
             }
             }
 
 
             let prog = 0;
             let prog = 0;
@@ -38,8 +41,12 @@ class FileDownloader {
             if (prog != prevProg && callback)
             if (prog != prevProg && callback)
                 callback(prog);
                 callback(prog);
             prevProg = prog;
             prevProg = prog;
-        });
 
 
+            if (abort && abort()) {
+                errMes = 'abort';
+                request.cancel();
+            }
+        });
 
 
         try {
         try {
             return (await request).body;
             return (await request).body;

+ 119 - 0
server/core/LimitedQueue.js

@@ -0,0 +1,119 @@
+class LimitedQueue {
+    constructor(enqueueAfter = 10, size = 100, timeout = 60*60*1000) {//timeout в ms
+        this.size = size;
+        this.timeout = timeout;
+
+        this.abortCount = 0;
+        this.enqueueAfter = enqueueAfter;
+        this.freed = enqueueAfter;
+        this.listeners = [];
+    }
+
+    _addListener(listener) {
+        this.listeners.push(listener);
+    }
+
+    //отсылаем сообщение первому ожидающему и удаляем его из списка
+    _emitFree() {
+        if (this.listeners.length > 0) {
+            let listener = this.listeners.shift();
+            listener.onFree();
+
+            for (let i = 0; i < this.listeners.length; i++) {
+                this.listeners[i].onPlaceChange(i + 1);
+            }
+        }
+    }
+
+    get(onPlaceChange) {
+        return new Promise((resolve, reject) => {
+            if (this.destroyed)
+                reject('destroyed');
+
+            const take = () => {
+                if (this.freed <= 0)
+                    throw new Error('Ошибка получения ресурсов в очереди ожидания');
+
+                this.freed--;
+                this.resetTimeout();
+
+                let aCount = this.abortCount;
+                return {
+                    ret: () => {
+                        if (aCount == this.abortCount) {
+                            this.freed++;
+                            this._emitFree();
+                            aCount = -1;
+                            this.resetTimeout();
+                        }
+                    },
+                    abort: () => {
+                        return (aCount != this.abortCount);
+                    },
+                    resetTimeout: this.resetTimeout.bind(this)
+                };
+            };
+
+            if (this.freed > 0) {
+                resolve(take());
+            } else {
+                if (this.listeners.length < this.size) {
+                    this._addListener({
+                        onFree: () => {
+                            resolve(take());
+                        },
+                        onError: (err) => {
+                            reject(err);
+                        },
+                        onPlaceChange: (i) => {
+                            if (onPlaceChange)
+                                onPlaceChange(i);
+                        }
+                    });
+                    if (onPlaceChange)
+                        onPlaceChange(this.listeners.length);
+                } else {
+                    reject('Превышен размер очереди ожидания');
+                }
+            }
+        });
+    }
+
+    resetTimeout() {
+        if (this.timer)
+            clearTimeout(this.timer);
+        this.timer = setTimeout(() => { this.clean(); }, this.timeout);
+    }
+
+    clean() {
+        this.timer = null;
+
+        if (this.freed < this.enqueueAfter) {
+            this.abortCount++;
+            //чистка listeners
+            for (const listener of this.listeners) {
+                listener.onError('Время ожидания в очереди истекло');
+            }
+            this.listeners = [];
+
+            this.freed = this.enqueueAfter;
+        }
+    }
+
+    destroy() {
+        if (this.timer) {
+            clearTimeout(this.timer);
+            this.timer = null;
+        }
+
+        for (const listener of this.listeners) {
+            listener.onError('destroy');
+        }
+        this.listeners = [];
+        this.abortCount++;
+
+        this.destroyed = true;
+    }
+}
+
+module.exports = LimitedQueue;

+ 13 - 7
server/core/Reader/BookConverter/ConvertBase.js

@@ -3,10 +3,11 @@ const iconv = require('iconv-lite');
 const chardet = require('chardet');
 const chardet = require('chardet');
 const he = require('he');
 const he = require('he');
 
 
+const LimitedQueue = require('../../LimitedQueue');
 const textUtils = require('./textUtils');
 const textUtils = require('./textUtils');
 const utils = require('../../utils');
 const utils = require('../../utils');
 
 
-let execConverterCounter = 0;
+const queue = new LimitedQueue(2, 20, 3*60*1000);//3 минуты ожидание подвижек
 
 
 class ConvertBase {
 class ConvertBase {
     constructor(config) {
     constructor(config) {
@@ -32,13 +33,16 @@ class ConvertBase {
             throw new Error('Внешний конвертер pdftohtml не найден');
             throw new Error('Внешний конвертер pdftohtml не найден');
     }
     }
 
 
-    async execConverter(path, args, onData) {
-        execConverterCounter++;
+    async execConverter(path, args, onData, abort) {
+        let q = null;
         try {
         try {
-            if (execConverterCounter > 10)
-                throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
+            q = await queue.get(() => {onData();});
+        } catch (e) {
+            throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
+        }
 
 
-            const result = await utils.spawnProcess(path, {args, onData});
+        try {
+            const result = await utils.spawnProcess(path, {args, onData, abort});
             if (result.code != 0) {
             if (result.code != 0) {
                 let error = result.code;
                 let error = result.code;
                 if (this.config.branch == 'development')
                 if (this.config.branch == 'development')
@@ -48,13 +52,15 @@ class ConvertBase {
         } catch(e) {
         } catch(e) {
             if (e.status == 'killed') {
             if (e.status == 'killed') {
                 throw new Error('Слишком долгое ожидание конвертера');
                 throw new Error('Слишком долгое ожидание конвертера');
+            } else if (e.status == 'abort') {
+                throw new Error('abort');
             } else if (e.status == 'error') {
             } else if (e.status == 'error') {
                 throw new Error(e.error);
                 throw new Error(e.error);
             } else {
             } else {
                 throw new Error(e);
                 throw new Error(e);
             }
             }
         } finally {
         } finally {
-            execConverterCounter--;
+            q.ret();
         }
         }
     }
     }
 
 

+ 3 - 3
server/core/Reader/BookConverter/ConvertDoc.js

@@ -16,7 +16,7 @@ class ConvertDoc extends ConvertDocX {
             return false;
             return false;
         await this.checkExternalConverterPresent();
         await this.checkExternalConverterPresent();
 
 
-        const {inputFiles, callback} = opts;
+        const {inputFiles, callback, abort} = opts;
 
 
         const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
         const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
         const docFile = `${outFile}.doc`;
         const docFile = `${outFile}.doc`;
@@ -24,9 +24,9 @@ class ConvertDoc extends ConvertDocX {
         const fb2File = `${outFile}.fb2`;
         const fb2File = `${outFile}.fb2`;
 
 
         await fs.copy(inputFiles.sourceFile, docFile);
         await fs.copy(inputFiles.sourceFile, docFile);
-        await this.execConverter(this.sofficePath, ['--headless', '--convert-to', 'docx', '--outdir', inputFiles.filesDir, docFile]);
+        await this.execConverter(this.sofficePath, ['--headless', '--convert-to', 'docx', '--outdir', inputFiles.filesDir, docFile], null, abort);
 
 
-        return await super.convert(docxFile, fb2File, callback);
+        return await super.convert(docxFile, fb2File, callback, abort);
     }
     }
 }
 }
 
 

+ 4 - 4
server/core/Reader/BookConverter/ConvertDocX.js

@@ -20,12 +20,12 @@ class ConvertDocX extends ConvertBase {
         return false;
         return false;
     }
     }
 
 
-    async convert(docxFile, fb2File, callback) {
+    async convert(docxFile, fb2File, callback, abort) {
         let perc = 0;
         let perc = 0;
         await this.execConverter(this.calibrePath, [docxFile, fb2File], () => {
         await this.execConverter(this.calibrePath, [docxFile, fb2File], () => {
             perc = (perc < 100 ? perc + 5 : 50);
             perc = (perc < 100 ? perc + 5 : 50);
             callback(perc);
             callback(perc);
-        });
+        }, abort);
 
 
         return await fs.readFile(fb2File);
         return await fs.readFile(fb2File);
     }
     }
@@ -35,7 +35,7 @@ class ConvertDocX extends ConvertBase {
             return false;
             return false;
         await this.checkExternalConverterPresent();
         await this.checkExternalConverterPresent();
 
 
-        const {inputFiles, callback} = opts;
+        const {inputFiles, callback, abort} = opts;
 
 
         const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
         const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
         const docxFile = `${outFile}.docx`;
         const docxFile = `${outFile}.docx`;
@@ -43,7 +43,7 @@ class ConvertDocX extends ConvertBase {
 
 
         await fs.copy(inputFiles.sourceFile, docxFile);
         await fs.copy(inputFiles.sourceFile, docxFile);
 
 
-        return await this.convert(docxFile, fb2File, callback);
+        return await this.convert(docxFile, fb2File, callback, abort);
     }
     }
 }
 }
 
 

+ 2 - 2
server/core/Reader/BookConverter/ConvertEpub.js

@@ -28,7 +28,7 @@ class ConvertEpub extends ConvertBase {
             return false;
             return false;
         await this.checkExternalConverterPresent();
         await this.checkExternalConverterPresent();
 
 
-        const {inputFiles, callback} = opts;
+        const {inputFiles, callback, abort} = opts;
 
 
         const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
         const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
         const epubFile = `${outFile}.epub`;
         const epubFile = `${outFile}.epub`;
@@ -40,7 +40,7 @@ class ConvertEpub extends ConvertBase {
         await this.execConverter(this.calibrePath, [epubFile, fb2File], () => {
         await this.execConverter(this.calibrePath, [epubFile, fb2File], () => {
             perc = (perc < 100 ? perc + 5 : 50);
             perc = (perc < 100 ? perc + 5 : 50);
             callback(perc);
             callback(perc);
-        });
+        }, abort);
 
 
         return await fs.readFile(fb2File);
         return await fs.readFile(fb2File);
     }
     }

+ 2 - 2
server/core/Reader/BookConverter/ConvertMobi.js

@@ -16,7 +16,7 @@ class ConvertMobi extends ConvertBase {
             return false;
             return false;
         await this.checkExternalConverterPresent();
         await this.checkExternalConverterPresent();
 
 
-        const {inputFiles, callback} = opts;
+        const {inputFiles, callback, abort} = opts;
 
 
         const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
         const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
         const mobiFile = `${outFile}.mobi`;
         const mobiFile = `${outFile}.mobi`;
@@ -28,7 +28,7 @@ class ConvertMobi extends ConvertBase {
         await this.execConverter(this.calibrePath, [mobiFile, fb2File], () => {
         await this.execConverter(this.calibrePath, [mobiFile, fb2File], () => {
             perc = (perc < 100 ? perc + 5 : 50);
             perc = (perc < 100 ? perc + 5 : 50);
             callback(perc);
             callback(perc);
-        });
+        }, abort);
 
 
         return await fs.readFile(fb2File);
         return await fs.readFile(fb2File);
     }
     }

+ 2 - 2
server/core/Reader/BookConverter/ConvertPdf.js

@@ -18,7 +18,7 @@ class ConvertPdf extends ConvertHtml {
             return false;
             return false;
         await this.checkExternalConverterPresent();
         await this.checkExternalConverterPresent();
 
 
-        const {inputFiles, callback} = opts;
+        const {inputFiles, callback, abort} = opts;
 
 
         const outFile = `${inputFiles.filesDir}/${utils.randomHexString(10)}.xml`;
         const outFile = `${inputFiles.filesDir}/${utils.randomHexString(10)}.xml`;
 
 
@@ -27,7 +27,7 @@ class ConvertPdf extends ConvertHtml {
         await this.execConverter(this.pdfToHtmlPath, ['-c', '-s', '-xml', inputFiles.sourceFile, outFile], () => {
         await this.execConverter(this.pdfToHtmlPath, ['-c', '-s', '-xml', inputFiles.sourceFile, outFile], () => {
             perc = (perc < 80 ? perc + 10 : 40);
             perc = (perc < 80 ? perc + 10 : 40);
             callback(perc);
             callback(perc);
-        });
+        }, abort);
         callback(80);
         callback(80);
 
 
         const data = await fs.readFile(outFile);
         const data = await fs.readFile(outFile);

+ 3 - 3
server/core/Reader/BookConverter/ConvertRtf.js

@@ -16,7 +16,7 @@ class ConvertRtf extends ConvertDocX {
             return false;
             return false;
         await this.checkExternalConverterPresent();
         await this.checkExternalConverterPresent();
 
 
-        const {inputFiles, callback} = opts;
+        const {inputFiles, callback, abort} = opts;
 
 
         const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
         const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
         const rtfFile = `${outFile}.rtf`;
         const rtfFile = `${outFile}.rtf`;
@@ -24,9 +24,9 @@ class ConvertRtf extends ConvertDocX {
         const fb2File = `${outFile}.fb2`;
         const fb2File = `${outFile}.fb2`;
 
 
         await fs.copy(inputFiles.sourceFile, rtfFile);
         await fs.copy(inputFiles.sourceFile, rtfFile);
-        await this.execConverter(this.sofficePath, ['--headless', '--convert-to', 'docx', '--outdir', inputFiles.filesDir, rtfFile]);
+        await this.execConverter(this.sofficePath, ['--headless', '--convert-to', 'docx', '--outdir', inputFiles.filesDir, rtfFile], null, abort);
 
 
-        return await super.convert(docxFile, fb2File, callback);
+        return await super.convert(docxFile, fb2File, callback, abort);
     }
     }
 }
 }
 
 

+ 6 - 3
server/core/Reader/BookConverter/index.js

@@ -26,11 +26,14 @@ class BookConverter {
         }
         }
     }
     }
 
 
-    async convertToFb2(inputFiles, outputFile, opts, callback) {
+    async convertToFb2(inputFiles, outputFile, opts, callback, abort) {
+        if (abort && abort())
+            throw new Error('abort');
+
         const selectedFileType = await this.detector.detectFile(inputFiles.selectedFile);
         const selectedFileType = await this.detector.detectFile(inputFiles.selectedFile);
         const data = await fs.readFile(inputFiles.selectedFile);
         const data = await fs.readFile(inputFiles.selectedFile);
 
 
-        const convertOpts = Object.assign({}, opts, {inputFiles, callback, dataType: selectedFileType});
+        const convertOpts = Object.assign({}, opts, {inputFiles, callback, abort, dataType: selectedFileType});
         let result = false;
         let result = false;
         for (const convert of this.convertFactory) {
         for (const convert of this.convertFactory) {
             result = await convert.run(data, convertOpts);
             result = await convert.run(data, convertOpts);
@@ -41,7 +44,7 @@ class BookConverter {
         }
         }
 
 
         if (!result && inputFiles.nesting) {
         if (!result && inputFiles.nesting) {
-            result = await this.convertToFb2(inputFiles.nesting, outputFile, opts, callback);
+            result = await this.convertToFb2(inputFiles.nesting, outputFile, opts, callback, abort);
         }
         }
 
 
         if (!result) {
         if (!result) {

+ 36 - 4
server/core/Reader/ReaderWorker.js

@@ -1,6 +1,7 @@
 const fs = require('fs-extra');
 const fs = require('fs-extra');
 const path = require('path');
 const path = require('path');
 
 
+const LimitedQueue = require('../LimitedQueue');
 const WorkerState = require('../WorkerState');//singleton
 const WorkerState = require('../WorkerState');//singleton
 const FileDownloader = require('../FileDownloader');
 const FileDownloader = require('../FileDownloader');
 const FileDecompressor = require('../FileDecompressor');
 const FileDecompressor = require('../FileDecompressor');
@@ -11,6 +12,7 @@ const utils = require('../utils');
 const log = new (require('../AppLogger'))().log;//singleton
 const log = new (require('../AppLogger'))().log;//singleton
 
 
 const cleanDirPeriod = 60*60*1000;//1 раз в час
 const cleanDirPeriod = 60*60*1000;//1 раз в час
+const queue = new LimitedQueue(5, 100, 5*60*1000);//5 минут ожидание подвижек
 
 
 let instance = null;
 let instance = null;
 
 
@@ -27,8 +29,8 @@ class ReaderWorker {
             fs.ensureDirSync(this.config.tempPublicDir);
             fs.ensureDirSync(this.config.tempPublicDir);
 
 
             this.workerState = new WorkerState();
             this.workerState = new WorkerState();
-            this.down = new FileDownloader();
-            this.decomp = new FileDecompressor();
+            this.down = new FileDownloader(config.maxUploadFileSize);
+            this.decomp = new FileDecompressor(2*config.maxUploadFileSize);
             this.bookConverter = new BookConverter(this.config);
             this.bookConverter = new BookConverter(this.config);
 
 
             this.remoteWebDavStorage = false;
             this.remoteWebDavStorage = false;
@@ -53,17 +55,35 @@ class ReaderWorker {
         let downloadedFilename = '';
         let downloadedFilename = '';
         let isUploaded = false;
         let isUploaded = false;
         let convertFilename = '';
         let convertFilename = '';
+
+        const overLoadMes = 'Слишком большая очередь загрузки. Пожалуйста, попробуйте позже.';
+        const overLoadErr = new Error(overLoadMes);
+
+        let q = null;
         try {
         try {
+            wState.set({state: 'queue', step: 1, totalSteps: 1});
+            try {
+                let qSize = 0;
+                q = await queue.get((place) => {
+                    wState.set({place, progress: (qSize ? Math.round((qSize - place)/qSize*100) : 0)});
+                    if (!qSize)
+                        qSize = place;
+                });
+            } catch (e) {
+                throw overLoadErr;
+            }
+
             wState.set({state: 'download', step: 1, totalSteps: 3, url});
             wState.set({state: 'download', step: 1, totalSteps: 3, url});
 
 
             const tempFilename = utils.randomHexString(30);
             const tempFilename = utils.randomHexString(30);
             const tempFilename2 = utils.randomHexString(30);
             const tempFilename2 = utils.randomHexString(30);
             const decompDirname = utils.randomHexString(30);
             const decompDirname = utils.randomHexString(30);
 
 
+            //download or use uploaded
             if (url.indexOf('file://') != 0) {//download
             if (url.indexOf('file://') != 0) {//download
                 const downdata = await this.down.load(url, (progress) => {
                 const downdata = await this.down.load(url, (progress) => {
                     wState.set({progress});
                     wState.set({progress});
-                });
+                }, q.abort);
 
 
                 downloadedFilename = `${this.config.tempDownloadDir}/${tempFilename}`;
                 downloadedFilename = `${this.config.tempDownloadDir}/${tempFilename}`;
                 await fs.writeFile(downloadedFilename, downdata);
                 await fs.writeFile(downloadedFilename, downdata);
@@ -76,6 +96,10 @@ class ReaderWorker {
             }
             }
             wState.set({progress: 100});
             wState.set({progress: 100});
 
 
+            if (q.abort())
+                throw overLoadErr;
+            q.resetTimeout();
+
             //decompress
             //decompress
             wState.set({state: 'decompress', step: 2, progress: 0});
             wState.set({state: 'decompress', step: 2, progress: 0});
             decompDir = `${this.config.tempDownloadDir}/${decompDirname}`;
             decompDir = `${this.config.tempDownloadDir}/${decompDirname}`;
@@ -88,12 +112,16 @@ class ReaderWorker {
             }
             }
             wState.set({progress: 100});
             wState.set({progress: 100});
             
             
+            if (q.abort())
+                throw overLoadErr;
+            q.resetTimeout();
+
             //конвертирование в fb2
             //конвертирование в fb2
             wState.set({state: 'convert', step: 3, progress: 0});
             wState.set({state: 'convert', step: 3, progress: 0});
             convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
             convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
             await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => {
             await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => {
                 wState.set({progress});
                 wState.set({progress});
-            });
+            }, q.abort);
 
 
             //сжимаем файл в tmp, если там уже нет с тем же именем-sha256
             //сжимаем файл в tmp, если там уже нет с тем же именем-sha256
             const compFilename = await this.decomp.gzipFileIfNotExists(convertFilename, this.config.tempPublicDir);
             const compFilename = await this.decomp.gzipFileIfNotExists(convertFilename, this.config.tempPublicDir);
@@ -120,9 +148,13 @@ class ReaderWorker {
 
 
         } catch (e) {
         } catch (e) {
             log(LM_ERR, e.stack);
             log(LM_ERR, e.stack);
+            if (e.message == 'abort')
+                e.message = overLoadMes;
             wState.set({state: 'error', error: e.message});
             wState.set({state: 'error', error: e.message});
         } finally {
         } finally {
             //clean
             //clean
+            if (q)
+                q.ret();
             if (decompDir)
             if (decompDir)
                 await fs.remove(decompDir);
                 await fs.remove(decompDir);
             if (downloadedFilename && !isUploaded)
             if (downloadedFilename && !isUploaded)

+ 10 - 1
server/core/ZipStreamer.js

@@ -52,7 +52,7 @@ class ZipStreamer {
         })().catch(reject); });
         })().catch(reject); });
     }
     }
 
 
-    unpack(zipFile, outputDir, entryCallback) {
+    unpack(zipFile, outputDir, entryCallback, limitFileSize = 0) {
         return new Promise((resolve, reject) => {
         return new Promise((resolve, reject) => {
             entryCallback = (entryCallback ? entryCallback : () => {});
             entryCallback = (entryCallback ? entryCallback : () => {});
             const unzip = new unzipStream({file: zipFile});
             const unzip = new unzipStream({file: zipFile});
@@ -67,6 +67,15 @@ class ZipStreamer {
             });
             });
 
 
             unzip.on('ready', () => {
             unzip.on('ready', () => {
+                if (limitFileSize) {
+                    for (const entry of Object.values(unzip.entries())) {
+                        if (!entry.isDirectory && entry.size > limitFileSize) {
+                            reject('Файл слишком большой');
+                            return;
+                        }
+                    }
+                }
+
                 unzip.extract(null, outputDir, (err) => {
                 unzip.extract(null, outputDir, (err) => {
                     if (err) reject(err);
                     if (err) reject(err);
                     unzip.close();
                     unzip.close();

+ 14 - 6
server/core/utils.js

@@ -37,8 +37,8 @@ async function touchFile(filename) {
 }
 }
 
 
 function spawnProcess(cmd, opts) {
 function spawnProcess(cmd, opts) {
-    let {args, killAfter, onData} = opts;
-    killAfter = (killAfter ? killAfter : 120*1000);
+    let {args, killAfter, onData, abort} = opts;
+    killAfter = (killAfter ? killAfter : 120);//seconds
     onData = (onData ? onData : () => {});
     onData = (onData ? onData : () => {});
     args = (args ? args : []);
     args = (args ? args : []);
 
 
@@ -67,10 +67,18 @@ function spawnProcess(cmd, opts) {
             reject({status: 'error', error, stdout, stderr});
             reject({status: 'error', error, stdout, stderr});
         });
         });
 
 
-        await sleep(killAfter);
-        if (!resolved) {
-            process.kill(proc.pid);
-            reject({status: 'killed', stdout, stderr});
+        while (!resolved) {
+            await sleep(1000);
+            killAfter -= 1;
+            if (killAfter <= 0 || (abort && abort())) {
+                process.kill(proc.pid);
+                if (killAfter <= 0) {
+                    reject({status: 'killed', stdout, stderr});
+                } else {
+                    reject({status: 'abort', stdout, stderr});
+                }
+                break;
+            }
         }
         }
     })().catch(reject); });
     })().catch(reject); });
 }
 }

+ 0 - 3
server/db/SqliteConnectionPool.js

@@ -14,7 +14,6 @@ class SqliteConnectionPool {
         if (!Number.isInteger(connCount) || connCount <= 0)
         if (!Number.isInteger(connCount) || connCount <= 0)
             return;
             return;
         this.connections = [];
         this.connections = [];
-        this.taken = new Set();
         this.freed = new Set();
         this.freed = new Set();
 
 
         for (let i = 0; i < connCount; i++) {
         for (let i = 0; i < connCount; i++) {
@@ -22,7 +21,6 @@ class SqliteConnectionPool {
             client.configure('busyTimeout', 10000); //ms
             client.configure('busyTimeout', 10000); //ms
 
 
             client.ret = () => {
             client.ret = () => {
-                this.taken.delete(i);
                 this.freed.add(i);
                 this.freed.add(i);
             };
             };
 
 
@@ -52,7 +50,6 @@ class SqliteConnectionPool {
         }
         }
 
 
         this.freed.delete(freeConnIndex);
         this.freed.delete(freeConnIndex);
-        this.taken.add(freeConnIndex);
 
 
         return this.connections[freeConnIndex];
         return this.connections[freeConnIndex];
     }
     }