Compare commits

...

45 Commits

Author SHA1 Message Date
Book Pauk
0e5d1ed1c3 Merge branch 'release/0.8.3-3' 2020-02-06 20:27:20 +07:00
Book Pauk
91dc2f4f71 Поправки логирования 2020-02-06 20:25:49 +07:00
Book Pauk
950bab3023 Добавлено декодирование имен файлов при распаковке Zip-архива в случае,
если кодировка имени не дает создать файл на диске
2020-02-06 20:20:29 +07:00
Book Pauk
29082a10e6 Рефакторинг 2020-02-06 20:13:33 +07:00
Book Pauk
65c1227d88 Удален node-stream-zip, т.к. в него внесены ручные правки 2020-02-06 20:12:01 +07:00
Book Pauk
5d121a68cf Поправки скриптов деплоя и запуска, добавлен авторестарт при падении сервера 2020-02-06 16:40:13 +07:00
Book Pauk
d28a8db4ff Добавлен альтернативный метод вычисления ширины строки в пикселях 2020-01-31 16:59:34 +07:00
Book Pauk
ab9e7d10dd Добавлен отлов ошибок при инициализации, добавлена генерация ошибки measureText 2020-01-31 16:08:37 +07:00
Book Pauk
3ff72b26b9 0.8.3 2020-01-31 14:52:49 +07:00
Book Pauk
404b87d78d Небольшие поправки 2020-01-30 16:34:05 +07:00
Book Pauk
dcb8fbdbf4 Merge tag '0.8.3-2' into develop
0.8.3-2
2020-01-29 01:03:20 +07:00
Book Pauk
0fe513d7f5 Merge branch 'release/0.8.3-2' 2020-01-29 01:02:56 +07:00
Book Pauk
0be05325e4 Исправлен баг 2020-01-29 01:02:05 +07:00
Book Pauk
75b39308cd Merge tag '0.8.3-1' into develop
0.8.3-1
2020-01-28 21:32:25 +07:00
Book Pauk
35ded81713 Merge branch 'release/0.8.3-1' 2020-01-28 21:32:02 +07:00
Book Pauk
07c85280cd Исправлены таймауты для конвертера calibre, добавлен флаг запуска -vv, соответственно поправлено вычисление прогресса 2020-01-28 21:27:54 +07:00
Book Pauk
43f1d86be0 Merge tag '0.8.3' into develop
0.8.3
2020-01-28 20:21:40 +07:00
Book Pauk
82f5ed4c44 Merge branch 'release/0.8.3' 2020-01-28 20:21:31 +07:00
Book Pauk
0b53ad4b4d Версия 0.8.3 2020-01-28 20:20:10 +07:00
Book Pauk
56ad41d10c Поправки текста объявления 2020-01-28 20:18:02 +07:00
Book Pauk
249a4564e0 Добавлено уведомление "Оплатим хостинг вместе" 2020-01-28 19:46:34 +07:00
Book Pauk
efb2413720 Небольшое изменение содержимого страницы 2020-01-28 19:44:52 +07:00
Book Pauk
1226acefd6 Небольшие исправления, queue теперь в одном экземпляре на класс 2020-01-28 14:51:09 +07:00
Book Pauk
76f7d7bc90 Мелкая поправка 2020-01-27 19:52:56 +07:00
Book Pauk
a5cb2641fd Мелкая поправка 2020-01-27 19:42:30 +07:00
Book Pauk
57fc64af79 Добавлен abort конвертеров при истечении времени ожидания подвижек очереди 2020-01-27 19:34:10 +07:00
Book Pauk
f8b7b8b698 Исправления LimitedQueue, исправления багов, добавлена проверка флага abort 2020-01-27 18:57:42 +07:00
Book Pauk
3da6befe10 Добавлен класс LimitedQueue для организации очередей 2020-01-26 18:38:09 +07:00
Book Pauk
a50d61c3ce Добавлена очередь скачивания и конвертирования 2020-01-26 18:37:14 +07:00
Book Pauk
b7568975e7 Добавлена обработка state = 'queue' 2020-01-26 18:31:31 +07:00
Book Pauk
4b9475310f Убрал ненужный this.taken 2020-01-26 16:23:20 +07:00
Book Pauk
639f726c83 Добавлен лимит на размер файла при распаковке 2020-01-26 15:17:45 +07:00
Book Pauk
7997c486cf Мелкий рефакторинг 2020-01-26 15:07:14 +07:00
Book Pauk
2569d00bd0 Мелкие поправки 2020-01-26 13:47:25 +07:00
Book Pauk
2cd80d8fa1 Merge tag '0.8.2-5' into develop
0.8.2-5
2020-01-23 17:12:46 +07:00
Book Pauk
eedca4db9b Merge branch 'release/0.8.2-5' 2020-01-23 17:12:37 +07:00
Book Pauk
1d352a76ce Поправка опечаток 2020-01-23 17:00:17 +07:00
Book Pauk
17670aabf9 WebSocket: добавлен метод reader-storage, поправки багов 2020-01-23 16:59:08 +07:00
Book Pauk
3456b3d90e WebSocket: добавлен метод worker-get-state-finish, небольшой рефакторинг 2020-01-23 16:25:06 +07:00
Book Pauk
f3da5a9026 Поправил комментарий 2020-01-23 15:56:26 +07:00
Book Pauk
00cc63b7cd WebSocket: добавлен метод get-config 2020-01-23 15:54:46 +07:00
Book Pauk
8df80ce738 Мелкая поправка 2020-01-23 15:16:49 +07:00
Book Pauk
12e7a783b0 Небольшие изменения блокирования кнопок панели 2020-01-22 22:06:12 +07:00
Book Pauk
be86a15351 Добавил настройку proxy_read_timeout 2020-01-22 21:37:28 +07:00
Book Pauk
2c5022e7b4 Merge tag '0.8.2-4' into develop
0.8.2-4
2020-01-22 21:17:58 +07:00
40 changed files with 1791 additions and 227 deletions

View File

@@ -1,4 +1,5 @@
import axios from 'axios';
import wsc from './webSocketConnection';
const api = axios.create({
baseURL: '/api'
@@ -6,9 +7,20 @@ const api = axios.create({
class Misc {
async loadConfig() {
const response = await api.post('/config', {params: [
const query = {params: [
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
]});
]};
try {
await wsc.open();
return await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
} catch (e) {
console.error(e);
}
//если с WebSocket проблема, работаем по http
const response = await api.post('/config', query);
return response.data;
}
}

View File

@@ -1,6 +1,6 @@
import axios from 'axios';
import * as utils from '../share/utils';
import WebSocketConnection from './WebSocketConnection';
import wsc from './webSocketConnection';
const api = axios.create({
baseURL: '/api/reader'
@@ -12,16 +12,14 @@ const workerApi = axios.create({
class Reader {
constructor() {
this.wsc = new WebSocketConnection();
}
async getStateFinish(workerId, callback) {
async getWorkerStateFinish(workerId, callback) {
if (!callback) callback = () => {};
let response = {};
try {
const wsc = this.wsc;
await wsc.open();
const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
@@ -29,17 +27,19 @@ class Reader {
response = await wsc.message(requestId);
callback(response);
if (!response.state)
throw new Error('Неверный ответ api');
if (response.state == 'finish' || response.state == 'error') {
break;
}
}
return response;
} catch (e) {
//
console.error(e);
}
//с WebSocket проблема, проверяем по http
//если с WebSocket проблема, работаем по http
const refreshPause = 500;
let i = 0;
response = {};
@@ -50,6 +50,9 @@ class Reader {
response = response.data;
callback(response);
if (!response.state)
throw new Error('Неверный ответ api');
if (response.state == 'finish' || response.state == 'error') {
break;
}
@@ -80,12 +83,12 @@ class Reader {
callback({totalSteps: 4});
callback(response.data);
response = await this.getStateFinish(workerId, callback);
response = await this.getWorkerStateFinish(workerId, callback);
if (response) {
if (response.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
callback({step: 4});
const book = await this.loadCachedBook(response.path, callback, false, (response.size ? response.size : -1));
const book = await this.loadCachedBook(response.path, callback, response.size);
return Object.assign({}, response, {data: book.data});
}
@@ -103,75 +106,58 @@ class Reader {
}
}
async checkUrl(url) {
let fileExists = false;
async checkCachedBook(url) {
let estSize = -1;
try {
await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
fileExists = true;
const response = await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
if (response.headers['content-length']) {
estSize = response.headers['content-length'];
}
} catch (e) {
//
}
//восстановим при необходимости файл на сервере из удаленного облака
if (!fileExists) {
let response = await api.post('/restore-cached-file', {path: url});
const workerId = response.data.workerId;
if (!workerId)
throw new Error('Неверный ответ api');
response = await this.getStateFinish(workerId);
if (response.state == 'error') {
throw new Error(response.error);
}
}
return true;
}
async loadCachedBook(url, callback, restore = true, estSize = -1) {
if (!callback) callback = () => {};
let response = null;
callback({state: 'loading', progress: 0});
//получение размера файла
let fileExists = false;
if (estSize < 0) {
//восстановим при необходимости файл на сервере из удаленного облака
let response = null
try {
response = await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
if (response.headers['content-length']) {
estSize = response.headers['content-length'];
}
fileExists = true;
await wsc.open();
response = await wsc.message(wsc.send({action: 'reader-restore-cached-file', path: url}));
} catch (e) {
//
console.error(e);
//если с WebSocket проблема, работаем по http
response = await api.post('/restore-cached-file', {path: url});
response = response.data;
}
}
//восстановим при необходимости файл на сервере из удаленного облака
if (restore && !fileExists) {
response = await api.post('/restore-cached-file', {path: url});
const workerId = response.data.workerId;
const workerId = response.workerId;
if (!workerId)
throw new Error('Неверный ответ api');
response = await this.getStateFinish(workerId);
response = await this.getWorkerStateFinish(workerId);
if (response.state == 'error') {
throw new Error(response.error);
}
if (response.size && estSize < 0) {
estSize = response.size;
}
}
return estSize;
}
async loadCachedBook(url, callback, estSize = -1) {
if (!callback) callback = () => {};
callback({state: 'loading', progress: 0});
//получение размера файла
if (estSize && estSize < 0) {
estSize = await this.checkCachedBook(url);
}
//получение файла
estSize = (estSize > 0 ? estSize : 1000000);
const options = {
onDownloadProgress: progress => {
onDownloadProgress: (progress) => {
while (progress.loaded > estSize) estSize *= 1.5;
if (callback)
@@ -215,13 +201,22 @@ class Reader {
}
async storage(request) {
let response = await api.post('/storage', request);
let response = null;
try {
await wsc.open();
response = await wsc.message(wsc.send({action: 'reader-storage', body: request}));
} catch (e) {
console.error(e);
//если с WebSocket проблема, работаем по http
response = await api.post('/storage', request);
response = response.data;
}
const state = response.data.state;
const state = response.state;
if (!state)
throw new Error('Неверный ответ api');
return response.data;
return response;
}
}

View File

@@ -111,7 +111,11 @@ class WebSocketConnection {
requestId,
timeout,
onMessage: (mes) => {
resolve(mes);
if (mes.error) {
reject(mes.error);
} else {
resolve(mes);
}
},
onError: (e) => {
reject(e);
@@ -169,4 +173,4 @@ class WebSocketConnection {
}
}
export default WebSocketConnection;
export default new WebSocketConnection();

View File

@@ -1,30 +1,54 @@
<template>
<div class="page">
<div class="box">
<p class="p">Проект существует исключительно на личном энтузиазме.</p>
<p class="p">Чтобы энтузиазма было побольше, вы можете пожертвовать на развитие проекта любую сумму:</p>
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
<div class="address">
<img class="logo" src="./assets/yandex.png">
<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 class="address">
<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 class="address">
<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 class="address">
<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>
@@ -54,7 +78,7 @@ class DonateHelpPage extends Vue {
async copyAddress(address, prefix) {
const result = await copyTextToClipboard(address);
if (result)
this.$notify.success({message: `${prefix}-адрес ${address} успешно скопирован в буфер обмена`});
this.$notify.success({message: `${prefix} ${address} успешно скопирован в буфер обмена`});
else
this.$notify.error({message: 'Копирование не удалось'});
}
@@ -106,4 +130,10 @@ h5 {
position: relative;
top: 10px;
}
.copy-icon {
margin-left: 10px;
cursor: pointer;
font-size: 120%;
}
</style>

View File

@@ -16,6 +16,7 @@ const ruMessage = {
'start': ' ',
'finish': ' ',
'error': ' ',
'queue': 'очередь',
'download': 'скачивание',
'decompress': 'распаковка',
'convert': 'конвертирование',
@@ -46,11 +47,17 @@ class ProgressPage extends Vue {
hide() {
this.visible = false;
this.text = '';
}
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.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
this.progress = state.progress || 0;

View File

@@ -90,6 +90,53 @@
</span>
</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-container>
</template>
@@ -200,6 +247,7 @@ class Reader extends Vue {
whatsNewVisible = false;
whatsNewContent = '';
donationVisible = false;
created() {
this.loading = true;
@@ -258,9 +306,10 @@ class Reader extends Vue {
this.checkActivateDonateHelpPage();
this.loading = false;
await this.showWhatsNew();
this.updateRoute();
await this.showWhatsNew();
await this.showDonation();
})();
}
@@ -272,6 +321,7 @@ class Reader extends Vue {
this.clickControl = settings.clickControl;
this.blinkCachedLoad = settings.blinkCachedLoad;
this.showWhatsNewDialog = settings.showWhatsNewDialog;
this.showDonationDialog2020 = settings.showDonationDialog2020;
this.showToolButton = settings.showToolButton;
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() {
this.whatsNewVisible = false;
this.versionHistoryToggle();
@@ -455,6 +540,10 @@ class Reader extends Vue {
return this.$store.state.reader.whatsNewContentHash;
}
get donationRemindDate() {
return this.$store.state.reader.donationRemindDate;
}
addAction(pos) {
let a = this.actionList;
if (!a.length || a[a.length - 1] != pos) {
@@ -719,15 +808,16 @@ class Reader extends Vue {
case 'scrolling':
case 'search':
case 'copyText':
case 'recentBooks':
case 'refresh':
case 'offlineMode':
case 'recentBooks':
case 'settings':
if (this[`${button}Active`])
if (this.progressActive) {
classResult = classDisabled;
} else if (this[`${button}Active`]) {
classResult = classActive;
}
break;
}
switch (button) {
case 'undoAction':
if (this.actionCur <= 0)
classResult = classDisabled;

View File

@@ -272,7 +272,7 @@ class RecentBooksPage extends Vue {
async downloadBook(fb2path) {
try {
await readerApi.checkUrl(fb2path);
await readerApi.checkCachedBook(fb2path);
const d = this.$refs.download;
d.href = fb2path;

View File

@@ -471,6 +471,14 @@
<el-checkbox v-model="showWhatsNewDialog">Показывать уведомление "Что нового"</el-checkbox>
</el-tooltip>
</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 :model="form" size="mini" label-width="120px" @submit.native.prevent>

View File

@@ -27,7 +27,8 @@
<div v-show="!clickControl && showStatusBar" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"></div>
<!-- невидимым делать нельзя, вовремя не подгружаютя шрифты -->
<canvas ref="offscreenCanvas" class="layout" style="width: 0px; height: 0px"></canvas>
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
</div>
</template>
@@ -143,6 +144,8 @@ class TextPage extends Vue {
}
calcDrawProps() {
const wideLetter = 'Щ';
//preloaded fonts
this.fontList = [`12px ${this.fontName}`];
@@ -199,6 +202,22 @@ class TextPage extends Vue {
this.drawHelper.lineHeight = this.lineHeight;
this.drawHelper.context = this.context;
//альтернатива context.measureText
if (!this.context.measureText(wideLetter).width) {
const ctx = this.$refs.measureWidth;
this.drawHelper.measureText = function(text, style) {
ctx.innerText = text;
ctx.style.font = this.fontByStyle(style);
return ctx.clientWidth;
};
this.drawHelper.measureTextFont = function(text, font) {
ctx.innerText = text;
ctx.style.font = font;
return ctx.clientWidth;
}
}
//statusBar
this.statusBarClickable = this.drawHelper.statusBarClickable(this.statusBarTop, this.statusBarHeight);
@@ -211,8 +230,10 @@ class TextPage extends Vue {
this.parsed.wordWrap = this.wordWrap;
this.parsed.cutEmptyParagraphs = this.cutEmptyParagraphs;
this.parsed.addEmptyParagraphs = this.addEmptyParagraphs;
let t = '';
while (this.drawHelper.measureText(t, {}) < this.w) t += 'Щ';
let t = wideLetter;
if (!this.drawHelper.measureText(t, {}))
throw new Error('Ошибка measureText');
while (this.drawHelper.measureText(t, {}) < this.w) t += wideLetter;
this.parsed.maxWordLength = t.length - 1;
this.parsed.measureText = this.drawHelper.measureText.bind(this.drawHelper);
this.parsed.lineHeight = this.lineHeight;
@@ -368,47 +389,51 @@ class TextPage extends Vue {
if (this.lastBook) {
(async() => {
//подождем ленивый парсинг
this.stopLazyParse = true;
while (this.doingLazyParse) await sleep(10);
try {
//подождем ленивый парсинг
this.stopLazyParse = true;
while (this.doingLazyParse) await sleep(10);
const isParsed = await bookManager.hasBookParsed(this.lastBook);
if (!isParsed) {
return;
const isParsed = await bookManager.hasBookParsed(this.lastBook);
if (!isParsed) {
return;
}
this.book = await bookManager.getBook(this.lastBook);
this.meta = bookManager.metaOnly(this.book);
this.fb2 = this.meta.fb2;
let authorNames = [];
if (this.fb2.author) {
authorNames = this.fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
}
this.title = _.compact([
authorNames.join(', '),
this.fb2.bookTitle
]).join(' - ');
this.$root.$emit('set-app-title', this.title);
this.parsed = this.book.parsed;
this.page1 = null;
this.page2 = null;
this.statusBar = null;
await this.stopTextScrolling();
await this.calcPropsAndLoadFonts();
this.refreshTime();
if (this.lazyParseEnabled)
this.lazyParsePara();
} catch (e) {
this.$alert(e.message, 'Ошибка', {type: 'error'});
}
this.book = await bookManager.getBook(this.lastBook);
this.meta = bookManager.metaOnly(this.book);
this.fb2 = this.meta.fb2;
let authorNames = [];
if (this.fb2.author) {
authorNames = this.fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
}
this.title = _.compact([
authorNames.join(', '),
this.fb2.bookTitle
]).join(' - ');
this.$root.$emit('set-app-title', this.title);
this.parsed = this.book.parsed;
this.page1 = null;
this.page2 = null;
this.statusBar = null;
await this.stopTextScrolling();
this.calcPropsAndLoadFonts();
this.refreshTime();
if (this.lazyParseEnabled)
this.lazyParsePara();
})();
}
}

View File

@@ -1,4 +1,16 @@
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',
header: '0.8.2 (2020-01-20)',

View File

@@ -19,6 +19,7 @@ import ElCheckbox from 'element-ui/lib/checkbox';
import ElTabs from 'element-ui/lib/tabs';
import ElTabPane from 'element-ui/lib/tab-pane';
import ElTooltip from 'element-ui/lib/tooltip';
import ElRow from 'element-ui/lib/row';
import ElCol from 'element-ui/lib/col';
import ElContainer from 'element-ui/lib/container';
import ElAside from 'element-ui/lib/aside';
@@ -43,7 +44,7 @@ import MessageBox from 'element-ui/lib/message-box';
const components = {
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,
ElProgress, ElSlider, ElForm, ElFormItem,
ElColorPicker, ElDialog,

View File

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

View File

@@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/bash
npm run build:linux
sudo -u www-data cp -r ../../dist/linux/* /home/liberama

View File

@@ -8,6 +8,7 @@ server {
server_name omnireader.ru;
client_max_body_size 50m;
proxy_read_timeout 1h;
gzip on;
gzip_min_length 1024;

View File

@@ -3,6 +3,7 @@ server {
server_name omnireader.ru;
client_max_body_size 50m;
proxy_read_timeout 1h;
gzip on;
gzip_min_length 1024;

View File

@@ -1,3 +1,11 @@
#!/bin/sh
#!/bin/bash
sudo -H -u www-data sh -c "cd /var/www; /home/liberama/liberama"
sudo -H -u www-data bash -c "\
while true; do\
trap '' 2;\
cd /var/www;\
/home/liberama/liberama;\
trap 2;\
echo \"Restart after 5 sec. Press Ctrl+C to exit.\";\
sleep 5;\
done;"

7
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "Liberama",
"version": "0.8.2",
"version": "0.8.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -7423,11 +7423,6 @@
"semver": "^5.3.0"
}
},
"node-stream-zip": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.8.2.tgz",
"integrity": "sha512-zwP2F/R28Oqtl0gOLItk5QjJ6jEU8XO4kaUMgeqvCyXPgdCZlm8T/5qLMiNy+moJCBCiMQAaX7aVMRhT0t2vkQ=="
},
"nopt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "Liberama",
"version": "0.8.2",
"version": "0.8.3",
"author": "Book Pauk <bookpauk@gmail.com>",
"license": "CC0-1.0",
"repository": "bookpauk/liberama",
@@ -71,7 +71,6 @@
"lodash": "^4.17.15",
"minimist": "^1.2.0",
"multer": "^1.4.2",
"node-stream-zip": "^1.8.2",
"pako": "^1.0.10",
"path-browserify": "^1.0.0",
"safe-buffer": "^5.2.0",

View File

@@ -35,9 +35,9 @@ class ReaderController extends BaseController {
const request = req.body;
let error = '';
try {
if (!request.action)
if (!request.action)
throw new Error(`key 'action' is empty`);
if (!request.items || Array.isArray(request.data))
if (!request.items || Array.isArray(request.data))
throw new Error(`key 'items' is empty`);
return await this.readerStorage.doAction(request);

View File

@@ -1,5 +1,10 @@
const WebSocket = require ('ws');
const _ = require('lodash');
const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton
const ReaderStorage = require('../core/Reader/ReaderStorage');//singleton
const WorkerState = require('../core/WorkerState');//singleton
const log = new (require('../core/AppLogger'))().log;//singleton
const utils = require('../core/utils');
const cleanPeriod = 1*60*1000;//1 минута
@@ -8,6 +13,10 @@ const closeSocketOnIdle = 5*60*1000;//5 минут
class WebSocketController {
constructor(wss, config) {
this.config = config;
this.isDevelopment = (config.branch == 'development');
this.readerStorage = new ReaderStorage();
this.readerWorker = new ReaderWorker(config);
this.workerState = new WorkerState();
this.wss = wss;
@@ -37,15 +46,25 @@ class WebSocketController {
async onMessage(ws, message) {
let req = {};
try {
if (this.isDevelopment) {
log(`WebSocket-IN: ${message.substr(0, 4000)}`);
}
ws.lastActivity = Date.now();
req = JSON.parse(message);
switch (req.action) {
case 'test':
this.test(req, ws); break;
await this.test(req, ws); break;
case 'get-config':
await this.getConfig(req, ws); break;
case 'worker-get-state':
this.workerGetState(req, ws); break;
await this.workerGetState(req, ws); break;
case 'worker-get-state-finish':
this.workerGetStateFinish(req, ws); break;
await this.workerGetStateFinish(req, ws); break;
case 'reader-restore-cached-file':
await this.readerRestoreCachedFile(req, ws); break;
case 'reader-storage':
await this.readerStorageDo(req, ws); break;
default:
throw new Error(`Action not found: ${req.action}`);
@@ -58,10 +77,17 @@ class WebSocketController {
send(res, req, ws) {
if (ws.readyState == WebSocket.OPEN) {
ws.lastActivity = Date.now();
let r = Object.assign({}, res);
let r = res;
if (req.requestId)
r.requestId = req.requestId;
ws.send(JSON.stringify(r));
r = Object.assign({requestId: req.requestId}, r);
const message = JSON.stringify(r);
ws.send(message);
if (this.isDevelopment) {
log(`WebSocket-OUT: ${message.substr(0, 4000)}`);
}
}
}
@@ -70,6 +96,14 @@ class WebSocketController {
this.send({message: 'Liberama project is awesome'}, req, ws);
}
async getConfig(req, ws) {
if (Array.isArray(req.params)) {
this.send(_.pick(this.config, req.params), req, ws);
} else {
throw new Error('params is not an array');
}
}
async workerGetState(req, ws) {
if (!req.workerId)
throw new Error(`key 'workerId' is wrong`);
@@ -106,6 +140,25 @@ class WebSocketController {
}
}
async readerRestoreCachedFile(req, ws) {
if (!req.path)
throw new Error(`key 'path' is empty`);
const workerId = this.readerWorker.restoreCachedFile(req.path);
const state = this.workerState.getState(workerId);
this.send((state ? state : {}), req, ws);
}
async readerStorageDo(req, ws) {
if (!req.body)
throw new Error(`key 'body' is empty`);
if (!req.body.action)
throw new Error(`key 'action' is empty`);
if (!req.body.items || Array.isArray(req.body.data))
throw new Error(`key 'items' is empty`);
this.send(await this.readerStorage.doAction(req.body), req, ws);
}
}
module.exports = WebSocketController;

View File

@@ -25,7 +25,8 @@ class AppLogger {
loggerParams = [
{log: 'ConsoleLog'},
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.log`},
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.err.log`, exclude: [LM_OK, LM_INFO]},
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.err.log`, exclude: [LM_OK, LM_INFO, LM_TOTAL]},
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.fatal.log`, exclude: [LM_OK, LM_INFO, LM_WARN, LM_ERR, LM_TOTAL]},//LM_FATAL only
];
}

View File

@@ -3,15 +3,18 @@ const zlib = require('zlib');
const path = require('path');
const unbzip2Stream = require('unbzip2-stream');
const tar = require('tar-fs');
const ZipStreamer = require('./ZipStreamer');
const iconv = require('iconv-lite');
const ZipStreamer = require('./Zip/ZipStreamer');
const appLogger = new (require('./AppLogger'))();//singleton
const utils = require('./utils');
const FileDetector = require('./FileDetector');
const textUtils = require('./Reader/BookConverter/textUtils');
const utils = require('./utils');
class FileDecompressor {
constructor() {
constructor(limitFileSize = 0) {
this.detector = new FileDetector();
this.limitFileSize = limitFileSize;
}
async decompressNested(filename, outputDir) {
@@ -113,7 +116,25 @@ class FileDecompressor {
async unZip(filename, outputDir) {
const zip = new ZipStreamer();
return await zip.unpack(filename, outputDir);
try {
return await zip.unpack(filename, outputDir, {
limitFileSize: this.limitFileSize,
limitFileCount: 1000
});
} catch (e) {
fs.emptyDir(outputDir);
return await zip.unpack(filename, outputDir, {
limitFileSize: this.limitFileSize,
limitFileCount: 1000,
decodeEntryNameCallback: (nameRaw) => {
const enc = textUtils.getEncodingLite(nameRaw);
if (enc.indexOf('ISO-8859') < 0) {
return iconv.decode(nameRaw, enc);
}
return nameRaw;
}
});
}
}
unBz2(filename, outputDir) {
@@ -125,9 +146,16 @@ class FileDecompressor {
}
unTar(filename, outputDir) {
return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => { (async() => {
const files = [];
if (this.limitFileSize) {
if ((await fs.stat(filename)).size > this.limitFileSize) {
reject('Файл слишком большой');
return;
}
}
const tarExtract = tar.extract(outputDir, {
map: (header) => {
files.push({path: header.name, size: header.size});
@@ -149,7 +177,7 @@ class FileDecompressor {
});
inputStream.pipe(tarExtract);
});
})().catch(reject); });
}
decompressByStream(stream, filename, outputDir) {
@@ -174,6 +202,16 @@ class FileDecompressor {
});
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);
outputStream.on('error', reject);

View File

@@ -1,12 +1,11 @@
const got = require('got');
const maxDownloadSize = 50*1024*1024;
class FileDownloader {
constructor() {
constructor(limitDownloadSize = 0) {
this.limitDownloadSize = limitDownloadSize;
}
async load(url, callback) {
async load(url, callback, abort) {
let errMes = '';
const options = {
encoding: null,
@@ -23,10 +22,14 @@ class FileDownloader {
}
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;
@@ -38,8 +41,12 @@ class FileDownloader {
if (prog != prevProg && callback)
callback(prog);
prevProg = prog;
});
if (abort && abort()) {
errMes = 'abort';
request.cancel();
}
});
try {
return (await request).body;

View File

@@ -3,7 +3,7 @@ const fs = require('fs-extra');
const path = require('path');
const log = new (require('../AppLogger'))().log;//singleton
const ZipStreamer = require('../ZipStreamer');
const ZipStreamer = require('../Zip/ZipStreamer');
const utils = require('../utils');

119
server/core/LimitedQueue.js Normal file
View File

@@ -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;

View File

@@ -226,12 +226,12 @@ class Logger {
// catch ctrl+c event and exit normally
process.on('SIGINT', () => {
this.log(LM_WARN, 'Ctrl-C pressed, exiting...');
this.log(LM_FATAL, 'Ctrl-C pressed, exiting...');
process.exit(2);
});
process.on('SIGTERM', () => {
this.log(LM_WARN, 'Kill signal, exiting...');
this.log(LM_FATAL, 'Kill signal, exiting...');
process.exit(2);
});

View File

@@ -1,12 +1,12 @@
const fs = require('fs-extra');
const iconv = require('iconv-lite');
const chardet = require('chardet');
const he = require('he');
const LimitedQueue = require('../../LimitedQueue');
const textUtils = require('./textUtils');
const utils = require('../../utils');
let execConverterCounter = 0;
const queue = new LimitedQueue(2, 20, 3*60*1000);//3 минуты ожидание подвижек
class ConvertBase {
constructor(config) {
@@ -32,13 +32,26 @@ class ConvertBase {
throw new Error('Внешний конвертер pdftohtml не найден');
}
async execConverter(path, args, onData) {
execConverterCounter++;
async execConverter(path, args, onData, abort) {
onData = (onData ? onData : () => {});
let q = null;
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, {
killAfter: 600,
args,
onData: (data) => {
q.resetTimeout();
onData(data);
},
abort
});
if (result.code != 0) {
let error = result.code;
if (this.config.branch == 'development')
@@ -48,29 +61,21 @@ class ConvertBase {
} catch(e) {
if (e.status == 'killed') {
throw new Error('Слишком долгое ожидание конвертера');
} else if (e.status == 'abort') {
throw new Error('abort');
} else if (e.status == 'error') {
throw new Error(e.error);
} else {
throw new Error(e);
}
} finally {
execConverterCounter--;
q.ret();
}
}
decode(data) {
let selected = textUtils.getEncoding(data);
if (selected == 'ISO-8859-5') {
const charsetAll = chardet.detectAll(data.slice(0, 20000));
for (const charset of charsetAll) {
if (charset.name.indexOf('ISO-8859') < 0) {
selected = charset.name;
break;
}
}
}
if (selected.toLowerCase() != 'utf-8')
return iconv.decode(data, selected);
else

View File

@@ -16,7 +16,7 @@ class ConvertDoc extends ConvertDocX {
return false;
await this.checkExternalConverterPresent();
const {inputFiles, callback} = opts;
const {inputFiles, callback, abort} = opts;
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
const docFile = `${outFile}.doc`;
@@ -24,9 +24,9 @@ class ConvertDoc extends ConvertDocX {
const fb2File = `${outFile}.fb2`;
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);
}
}

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ class ConvertPdf extends ConvertHtml {
return false;
await this.checkExternalConverterPresent();
const {inputFiles, callback} = opts;
const {inputFiles, callback, abort} = opts;
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], () => {
perc = (perc < 80 ? perc + 10 : 40);
callback(perc);
});
}, abort);
callback(80);
const data = await fs.readFile(outFile);

View File

@@ -16,7 +16,7 @@ class ConvertRtf extends ConvertDocX {
return false;
await this.checkExternalConverterPresent();
const {inputFiles, callback} = opts;
const {inputFiles, callback, abort} = opts;
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
const rtfFile = `${outFile}.rtf`;
@@ -24,9 +24,9 @@ class ConvertRtf extends ConvertDocX {
const fb2File = `${outFile}.fb2`;
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);
}
}

View File

@@ -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 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;
for (const convert of this.convertFactory) {
result = await convert.run(data, convertOpts);
@@ -41,7 +44,7 @@ class BookConverter {
}
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) {

View File

@@ -1,4 +1,23 @@
function getEncoding(buf, returnAll) {
const chardet = require('chardet');
function getEncoding(buf) {
let selected = getEncodingLite(buf);
if (selected == 'ISO-8859-5') {
const charsetAll = chardet.detectAll(buf.slice(0, 20000));
for (const charset of charsetAll) {
if (charset.name.indexOf('ISO-8859') < 0) {
selected = charset.name;
break;
}
}
}
return selected;
}
function getEncodingLite(buf, returnAll) {
const lowerCase = 3;
const upperCase = 1;
@@ -106,5 +125,6 @@ function checkIfText(buf) {
module.exports = {
getEncoding,
getEncodingLite,
checkIfText,
}

View File

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

View File

@@ -2,7 +2,7 @@ const fs = require('fs-extra');
const path = require('path');
const zipStream = require('zip-stream');
const unzipStream = require('node-stream-zip');
const unzipStream = require('./node_stream_zip');
class ZipStreamer {
constructor() {
@@ -52,9 +52,15 @@ class ZipStreamer {
})().catch(reject); });
}
unpack(zipFile, outputDir, entryCallback) {
unpack(zipFile, outputDir, options, entryCallback) {
return new Promise((resolve, reject) => {
entryCallback = (entryCallback ? entryCallback : () => {});
const {
limitFileSize = 0,
limitFileCount = 0,
decodeEntryNameCallback = false,
} = options;
const unzip = new unzipStream({file: zipFile});
unzip.on('error', reject);
@@ -67,14 +73,41 @@ class ZipStreamer {
});
unzip.on('ready', () => {
if (limitFileCount || limitFileSize || decodeEntryNameCallback) {
const entries = Object.values(unzip.entries());
if (limitFileCount && entries.length > limitFileCount) {
reject('Слишком много файлов');
return;
}
for (const entry of entries) {
if (limitFileSize && !entry.isDirectory && entry.size > limitFileSize) {
reject('Файл слишком большой');
return;
}
if (decodeEntryNameCallback) {
entry.name = (decodeEntryNameCallback(entry.nameRaw)).toString();
}
}
}
unzip.extract(null, outputDir, (err) => {
if (err) reject(err);
unzip.close();
resolve(files);
if (err) {
reject(err);
return;
}
try {
unzip.close();
resolve(files);
} catch (e) {
reject(e);
}
});
});
});
}
}
module.exports = ZipStreamer;

File diff suppressed because it is too large Load Diff

View File

@@ -37,8 +37,8 @@ async function touchFile(filename) {
}
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 : () => {});
args = (args ? args : []);
@@ -67,10 +67,18 @@ function spawnProcess(cmd, opts) {
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); });
}

View File

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