Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73c3beaff1 | ||
|
|
a6bdccd4ef | ||
|
|
8007991e7d | ||
|
|
0e5d1ed1c3 | ||
|
|
91dc2f4f71 | ||
|
|
950bab3023 | ||
|
|
29082a10e6 | ||
|
|
65c1227d88 | ||
|
|
5d121a68cf | ||
|
|
d28a8db4ff | ||
|
|
ab9e7d10dd | ||
|
|
3ff72b26b9 | ||
|
|
404b87d78d | ||
|
|
dcb8fbdbf4 | ||
|
|
0fe513d7f5 | ||
|
|
0be05325e4 | ||
|
|
75b39308cd | ||
|
|
35ded81713 | ||
|
|
07c85280cd | ||
|
|
43f1d86be0 | ||
|
|
82f5ed4c44 | ||
|
|
0b53ad4b4d | ||
|
|
56ad41d10c | ||
|
|
249a4564e0 | ||
|
|
efb2413720 | ||
|
|
1226acefd6 | ||
|
|
76f7d7bc90 | ||
|
|
a5cb2641fd | ||
|
|
57fc64af79 | ||
|
|
f8b7b8b698 | ||
|
|
3da6befe10 | ||
|
|
a50d61c3ce | ||
|
|
b7568975e7 | ||
|
|
4b9475310f | ||
|
|
639f726c83 | ||
|
|
7997c486cf | ||
|
|
2569d00bd0 | ||
|
|
2cd80d8fa1 |
@@ -27,6 +27,9 @@ class Reader {
|
||||
response = await wsc.message(requestId);
|
||||
callback(response);
|
||||
|
||||
if (!response.state)
|
||||
throw new Error('Неверный ответ api');
|
||||
|
||||
if (response.state == 'finish' || response.state == 'error') {
|
||||
break;
|
||||
}
|
||||
@@ -47,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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -70,7 +70,7 @@ class PasteTextPage extends Vue {
|
||||
}
|
||||
|
||||
loadBuffer() {
|
||||
this.$emit('load-buffer', {buffer: `<cut-title>${this.bookTitle}</cut-title>${this.$refs.textArea.value}`});
|
||||
this.$emit('load-buffer', {buffer: `<buffer><cut-title>${utils.escapeXml(this.bookTitle)}</cut-title>${this.$refs.textArea.value}</buffer>`});
|
||||
this.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -193,4 +193,13 @@ export function parseQuery(str) {
|
||||
query[first] = [query[first], second];
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
export function escapeXml(str) {
|
||||
return str.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
;
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
npm run build:linux
|
||||
sudo -u www-data cp -r ../../dist/linux/* /home/liberama
|
||||
|
||||
@@ -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
7
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
119
server/core/LimitedQueue.js
Normal 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;
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ class ConvertHtml extends ConvertBase {
|
||||
check(data, opts) {
|
||||
const {dataType} = opts;
|
||||
|
||||
//html?
|
||||
if (dataType && (dataType.ext == 'html' || dataType.ext == 'xml'))
|
||||
return {isText: false};
|
||||
|
||||
@@ -14,6 +15,11 @@ class ConvertHtml extends ConvertBase {
|
||||
return {isText: true};
|
||||
}
|
||||
|
||||
//из буфера обмена?
|
||||
if (data.toString().indexOf('<buffer>') == 0) {
|
||||
return {isText: false};
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
1055
server/core/Zip/node_stream_zip.js
Normal file
1055
server/core/Zip/node_stream_zip.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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); });
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user