Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03ce50153e | ||
|
|
15d01ad7fc | ||
|
|
e2b29e2c2f | ||
|
|
ce7ae84e0f | ||
|
|
01eb545f15 | ||
|
|
706738c7f1 | ||
|
|
6afa78cde9 | ||
|
|
71f5710bba | ||
|
|
0d87043f91 | ||
|
|
e25375fb7a | ||
|
|
41822999c8 | ||
|
|
07444bc7c2 | ||
|
|
ec48e5b0b7 | ||
|
|
e8e2e9297f | ||
|
|
4f871dd5ca | ||
|
|
f5f07a591a | ||
|
|
4c11e6918f | ||
|
|
403b9c0508 | ||
|
|
ee8ba75371 | ||
|
|
a2773fb180 | ||
|
|
ca36d588fc | ||
|
|
1e65707b7f | ||
|
|
eddf34ce55 | ||
|
|
0fb43aa33c | ||
|
|
b273b02da4 | ||
|
|
0b997f9673 | ||
|
|
bdb2ae57a8 | ||
|
|
b5e563679a | ||
|
|
992c104262 | ||
|
|
555154031e | ||
|
|
acb083e429 | ||
|
|
4a527d192d | ||
|
|
39c3bf17dd | ||
|
|
afc8c84f41 | ||
|
|
a085e04c4d | ||
|
|
2f82b0db34 | ||
|
|
0124c2b17d | ||
|
|
d2cfbbc9f3 | ||
|
|
c59f48822c | ||
|
|
b2d6584c4a | ||
|
|
8f7cafb240 | ||
|
|
08fd0f15ff | ||
|
|
dbb1bfe587 | ||
|
|
fe4b7a5a85 | ||
|
|
d8df5d76e5 | ||
|
|
b65dcc5ade | ||
|
|
a5c387a19e | ||
|
|
07c38d9a9f | ||
|
|
20ac8a444b | ||
|
|
7b601c9c7f | ||
|
|
8d2f74daa4 | ||
|
|
01e82dca5f |
@@ -66,7 +66,7 @@ class Reader {
|
|||||||
await utils.sleep(refreshPause);
|
await utils.sleep(refreshPause);
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
if (i > 180*1000/refreshPause) {//3 мин ждем телодвижений воркера
|
||||||
throw new Error('Слишком долгое время ожидания');
|
throw new Error('Слишком долгое время ожидания');
|
||||||
}
|
}
|
||||||
//проверка воркера
|
//проверка воркера
|
||||||
@@ -181,9 +181,8 @@ class Reader {
|
|||||||
maxUploadFileSize = 10*1024*1024;
|
maxUploadFileSize = 10*1024*1024;
|
||||||
if (file.size > maxUploadFileSize)
|
if (file.size > maxUploadFileSize)
|
||||||
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
||||||
|
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file, file.name);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -128,6 +128,10 @@ class App extends Vue {
|
|||||||
|
|
||||||
this.setAppTitle();
|
this.setAppTitle();
|
||||||
(async() => {
|
(async() => {
|
||||||
|
//запросим persistent storage
|
||||||
|
if (navigator.storage && navigator.storage.persist) {
|
||||||
|
navigator.storage.persist();
|
||||||
|
}
|
||||||
await this.routerReady();
|
await this.routerReady();
|
||||||
this.redirectIfNeeded();
|
this.redirectIfNeeded();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
<span class="greeting"><b>{{ title }}</b></span>
|
<span class="greeting"><b>{{ title }}</b></span>
|
||||||
<div class="q-my-sm"></div>
|
<div class="q-my-sm"></div>
|
||||||
<span class="greeting">Добро пожаловать!</span>
|
<span class="greeting">Добро пожаловать!</span>
|
||||||
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</b></span>
|
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz<span v-if="isExternalConverter">, rar</span></b></span>
|
||||||
<span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span>
|
<span v-if="isExternalConverter" class="greeting">...а также частично форматы: <b>epub, mobi, rtf, doc, docx, pdf, djvu</b></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
||||||
|
|||||||
@@ -593,12 +593,6 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshBookSplitToPara() {
|
|
||||||
if (this.mostRecentBook()) {
|
|
||||||
this.loadBook({url: this.mostRecentBook().url, skipCheck: true, isText: true, force: true});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recentBooksClose() {
|
recentBooksClose() {
|
||||||
this.recentBooksActive = false;
|
this.recentBooksActive = false;
|
||||||
}
|
}
|
||||||
@@ -688,9 +682,14 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshBook() {
|
refreshBook(mode) {
|
||||||
if (this.mostRecentBook()) {
|
const mrb = this.mostRecentBook();
|
||||||
this.loadBook({url: this.mostRecentBook().url, force: true});
|
if (mrb) {
|
||||||
|
if (mode && mode == 'split') {
|
||||||
|
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, skipCheck: true, isText: true, force: true});
|
||||||
|
} else {
|
||||||
|
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -846,8 +845,12 @@ class Reader extends Vue {
|
|||||||
|
|
||||||
let url = encodeURI(decodeURI(opts.url));
|
let url = encodeURI(decodeURI(opts.url));
|
||||||
|
|
||||||
|
//TODO: убрать конвертирование 'file://' после 06.2021
|
||||||
|
if (url.length == 71 && url.indexOf('file://') == 0)
|
||||||
|
url = url.replace(/^file/, 'disk');
|
||||||
|
|
||||||
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
||||||
(url.indexOf('file://') != 0))
|
(url.indexOf('disk://') != 0))
|
||||||
url = 'http://' + url;
|
url = 'http://' + url;
|
||||||
|
|
||||||
// уже просматривается сейчас
|
// уже просматривается сейчас
|
||||||
@@ -878,6 +881,7 @@ class Reader extends Vue {
|
|||||||
wasOpened = (wasOpened ? wasOpened : {});
|
wasOpened = (wasOpened ? wasOpened : {});
|
||||||
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
|
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
|
||||||
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
|
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
|
||||||
|
const uploadFileName = (opts.uploadFileName ? opts.uploadFileName : '');
|
||||||
|
|
||||||
let book = null;
|
let book = null;
|
||||||
|
|
||||||
@@ -924,7 +928,8 @@ class Reader extends Vue {
|
|||||||
url,
|
url,
|
||||||
skipCheck: (opts.skipCheck ? true : false),
|
skipCheck: (opts.skipCheck ? true : false),
|
||||||
isText: (opts.isText ? true : false),
|
isText: (opts.isText ? true : false),
|
||||||
enableSitesFilter: this.enableSitesFilter
|
enableSitesFilter: this.enableSitesFilter,
|
||||||
|
uploadFileName
|
||||||
},
|
},
|
||||||
(state) => {
|
(state) => {
|
||||||
progress.setState(state);
|
progress.setState(state);
|
||||||
@@ -940,7 +945,7 @@ class Reader extends Vue {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// добавляем в историю
|
// добавляем в историю
|
||||||
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, addedBook));
|
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, uploadFileName}, addedBook));
|
||||||
this.mostRecentBook();
|
this.mostRecentBook();
|
||||||
this.addAction(bookPos);
|
this.addAction(bookPos);
|
||||||
this.updateRoute(true);
|
this.updateRoute(true);
|
||||||
@@ -977,7 +982,7 @@ class Reader extends Vue {
|
|||||||
|
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
|
|
||||||
await this.loadBook({url});
|
await this.loadBook({url, uploadFileName: opts.file.name, force: true});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
this.loaderActive = true;
|
this.loaderActive = true;
|
||||||
@@ -1049,7 +1054,7 @@ class Reader extends Vue {
|
|||||||
this.copyTextToggle();
|
this.copyTextToggle();
|
||||||
break;
|
break;
|
||||||
case 'splitToPara':
|
case 'splitToPara':
|
||||||
this.refreshBookSplitToPara();
|
this.refreshBook('split');
|
||||||
break;
|
break;
|
||||||
case 'refresh':
|
case 'refresh':
|
||||||
this.refreshBook();
|
this.refreshBook();
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ class RecentBooksPage extends Vue {
|
|||||||
pagination = {};
|
pagination = {};
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
|
this.firstInit = true;
|
||||||
this.pagination = {rowsPerPage: 0};
|
this.pagination = {rowsPerPage: 0};
|
||||||
|
|
||||||
this.columns = [
|
this.columns = [
|
||||||
@@ -167,26 +168,11 @@ class RecentBooksPage extends Vue {
|
|||||||
this.initing = true;
|
this.initing = true;
|
||||||
|
|
||||||
|
|
||||||
if (!bookManager.loaded) {
|
if (this.firstInit) {//для отзывчивости
|
||||||
await this.updateTableData(10);
|
await this.updateTableData(20);
|
||||||
//для отзывчивости
|
this.firstInit = false;
|
||||||
await utils.sleep(100);
|
|
||||||
let i = 0;
|
|
||||||
let j = 5;
|
|
||||||
while (i < 500 && !bookManager.loaded) {
|
|
||||||
if (i % j == 0) {
|
|
||||||
bookManager.sortedRecentCached = null;
|
|
||||||
await this.updateTableData(20);
|
|
||||||
j *= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
await utils.sleep(100);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//для отзывчивости
|
|
||||||
await utils.sleep(100);
|
|
||||||
}
|
}
|
||||||
|
await utils.sleep(50);
|
||||||
await this.updateTableData();
|
await this.updateTableData();
|
||||||
this.initing = false;
|
this.initing = false;
|
||||||
})();
|
})();
|
||||||
@@ -310,7 +296,7 @@ class RecentBooksPage extends Vue {
|
|||||||
|
|
||||||
isUrl(url) {
|
isUrl(url) {
|
||||||
if (url)
|
if (url)
|
||||||
return (url.indexOf('file://') != 0);
|
return (url.indexOf('disk://') != 0);
|
||||||
else
|
else
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,8 +216,15 @@ class ServerStorage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
error(message) {
|
error(message) {
|
||||||
if (this.showServerStorageMessages && !this.offlineModeActive)
|
if (this.showServerStorageMessages && !this.offlineModeActive) {
|
||||||
this.$root.notify.error(message);
|
this.errorMessageCounter = (this.errorMessageCounter ? this.errorMessageCounter + 1 : 1);
|
||||||
|
const hint = (this.errorMessageCounter < 2 ? '' :
|
||||||
|
'<div><br>Надоело это сообщение? Добавьте в настройках кнопку "Автономный режим" ' +
|
||||||
|
'<i class="la la-unlink" style="font-size: 20px; color: white"></i> на панель инструментов и активируйте ее.</div>'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.$root.notify.error(message + hint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings(force = false, doNotifySuccess = true) {
|
async loadSettings(force = false, doNotifySuccess = true) {
|
||||||
@@ -580,8 +587,8 @@ class ServerStorage extends Vue {
|
|||||||
let newRecent = {};
|
let newRecent = {};
|
||||||
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
||||||
//ждем весь bm.recent
|
//ждем весь bm.recent
|
||||||
while (!bookManager.loaded)
|
/*while (!bookManager.loaded)
|
||||||
await utils.sleep(100);
|
await utils.sleep(100);*/
|
||||||
|
|
||||||
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
|
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
|
||||||
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export default class DrawHelper {
|
|||||||
const fh = h - 2*pad;
|
const fh = h - 2*pad;
|
||||||
const fh2 = fh/2;
|
const fh2 = fh/2;
|
||||||
|
|
||||||
const t1 = `${Math.floor((bookPos + 1)/1000)}k/${Math.floor(textLength/1000)}k`;
|
const t1 = `${Math.floor((bookPos + 1)/1000)}/${Math.floor(textLength/1000)}`;
|
||||||
const w1 = this.measureTextFont(t1, font) + fh2;
|
const w1 = this.measureTextFont(t1, font) + fh2;
|
||||||
const read = (bookPos + 1)/textLength;
|
const read = (bookPos + 1)/textLength;
|
||||||
const t2 = `${(read*100).toFixed(2)}%`;
|
const t2 = `${(read*100).toFixed(2)}%`;
|
||||||
|
|||||||
@@ -1053,7 +1053,7 @@ class TextPage extends Vue {
|
|||||||
|
|
||||||
onStatusBarClick() {
|
onStatusBarClick() {
|
||||||
const url = this.meta.url;
|
const url = this.meta.url;
|
||||||
if (url && url.indexOf('file://') != 0) {
|
if (url && url.indexOf('disk://') != 0) {
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
} else {
|
} else {
|
||||||
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
|
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import _ from 'lodash';
|
|||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
import BookParser from './BookParser';
|
import BookParser from './BookParser';
|
||||||
|
|
||||||
const maxDataSize = 300*1024*1024;//compressed bytes
|
const maxDataSize = 500*1024*1024;//compressed bytes
|
||||||
|
|
||||||
//локальный кэш метаданных книг, ограничение maxDataSize
|
//локальный кэш метаданных книг, ограничение maxDataSize
|
||||||
const bmMetaStore = localForage.createInstance({
|
const bmMetaStore = localForage.createInstance({
|
||||||
@@ -39,7 +39,7 @@ class BookManager {
|
|||||||
|
|
||||||
this.saveRecentItem = _.debounce(() => {
|
this.saveRecentItem = _.debounce(() => {
|
||||||
bmRecentStoreNew.setItem('recent-item', this.recentItem);
|
bmRecentStoreNew.setItem('recent-item', this.recentItem);
|
||||||
this.recentRev++;
|
this.recentRev = (this.recentRev < 1000 ? this.recentRev + 1 : 1);
|
||||||
bmRecentStoreNew.setItem('rev', this.recentRev);
|
bmRecentStoreNew.setItem('rev', this.recentRev);
|
||||||
}, 200, {maxWait: 300});
|
}, 200, {maxWait: 300});
|
||||||
|
|
||||||
@@ -64,6 +64,12 @@ class BookManager {
|
|||||||
|
|
||||||
await this.cleanRecentBooks();
|
await this.cleanRecentBooks();
|
||||||
|
|
||||||
|
//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
|
||||||
|
{
|
||||||
|
await this.convertFileToDiskPrefix();
|
||||||
|
if (this.recentRev > 10)
|
||||||
|
await bmRecentStoreOld.clear();
|
||||||
|
}
|
||||||
} else {//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
|
} else {//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
|
||||||
this.recentLast = await bmRecentStoreOld.getItem('recent-last');
|
this.recentLast = await bmRecentStoreOld.getItem('recent-last');
|
||||||
if (this.recentLast) {
|
if (this.recentLast) {
|
||||||
@@ -453,6 +459,33 @@ class BookManager {
|
|||||||
return isDel;
|
return isDel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async convertFileToDiskPrefix() {
|
||||||
|
let isConverted = false;
|
||||||
|
|
||||||
|
const newRecent = {};
|
||||||
|
for (let key of Object.keys(this.recent)) {
|
||||||
|
let newKey = key;
|
||||||
|
let newUrl = this.recent[key].url;
|
||||||
|
|
||||||
|
if (newKey.indexOf('66696c65') == 0) {
|
||||||
|
newKey = newKey.replace(/^66696c65/, '6469736b');
|
||||||
|
if (newUrl)
|
||||||
|
newUrl = newUrl.replace(/^file/, 'disk');
|
||||||
|
isConverted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
newRecent[newKey] = this.recent[key];
|
||||||
|
newRecent[newKey].key = newKey;
|
||||||
|
if (newUrl)
|
||||||
|
newRecent[newKey].url = newUrl;
|
||||||
|
}
|
||||||
|
if (isConverted) {
|
||||||
|
this.recent = newRecent;
|
||||||
|
await this.recentSetItem(null, true);
|
||||||
|
}
|
||||||
|
return isConverted;
|
||||||
|
}
|
||||||
|
|
||||||
mostRecentBook() {
|
mostRecentBook() {
|
||||||
if (this.recentLastKey) {
|
if (this.recentLastKey) {
|
||||||
return this.recent[this.recentLastKey];
|
return this.recent[this.recentLastKey];
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
export const versionHistory = [
|
export const versionHistory = [
|
||||||
|
{
|
||||||
|
showUntil: '2020-12-10',
|
||||||
|
header: '0.9.10 (2020-12-03)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлена частичная поддержка формата Djvu</li>
|
||||||
|
<li>добавлена поддержка Rar-архивов</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
showUntil: '2020-11-20',
|
showUntil: '2020-11-20',
|
||||||
header: '0.9.9 (2020-11-21)',
|
header: '0.9.9 (2020-11-21)',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog v-model="active">
|
<q-dialog v-model="active" no-route-dismiss>
|
||||||
<div class="column bg-white no-wrap">
|
<div class="column bg-white no-wrap">
|
||||||
<div class="header row">
|
<div class="header row">
|
||||||
<div class="caption col row items-center q-ml-md">
|
<div class="caption col row items-center q-ml-md">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide">
|
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide" no-route-dismiss>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|
||||||
<!--------------------------------------------------->
|
<!--------------------------------------------------->
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
sudo -H -u www-data /home/beta.liberama/liberama
|
sudo -H -u www-data bash -c "cd /var/www; /home/beta.liberama/liberama"
|
||||||
|
|||||||
@@ -27,15 +27,19 @@ sudo chown www-data.www-data /home/liberama
|
|||||||
|
|
||||||
### external converter `calibre`, download from https://download.calibre-ebook.com/
|
### external converter `calibre`, download from https://download.calibre-ebook.com/
|
||||||
```
|
```
|
||||||
wget "https://download.calibre-ebook.com/3.39.1/calibre-3.39.1-x86_64.txz"
|
wget "https://download.calibre-ebook.com/5.5.0/calibre-5.5.0-x86_64.txz"
|
||||||
sudo -u www-data mkdir -p /home/liberama/data/calibre
|
sudo -u www-data mkdir -p /home/liberama/data/calibre
|
||||||
sudo -u www-data tar xvf calibre-3.39.1-x86_64.txz -C /home/liberama/data/calibre
|
sudo -u www-data tar xvf calibre-5.5.0-x86_64.txz -C /home/liberama/data/calibre
|
||||||
```
|
```
|
||||||
|
|
||||||
### external converters
|
### external converters
|
||||||
```
|
```
|
||||||
|
sudo apt install rar
|
||||||
sudo apt install libreoffice
|
sudo apt install libreoffice
|
||||||
sudo apt install poppler-utils
|
sudo apt install poppler-utils
|
||||||
|
sudo apt install djvulibre-bin
|
||||||
|
sudo apt install libtiff-tools
|
||||||
|
sudo apt install graphicsmagick-imagemagick-compat
|
||||||
```
|
```
|
||||||
|
|
||||||
### nginx, server config
|
### nginx, server config
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
if ! pgrep -x "liberama" > /dev/null ; then
|
if ! pgrep -x "liberama" > /dev/null ; then
|
||||||
sudo -H -u www-data /home/liberama/liberama
|
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama"
|
||||||
else
|
else
|
||||||
echo "Process 'liberama' already running"
|
echo "Process 'liberama' already running"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
sudo -H -u www-data /home/liberama/liberama &
|
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama" & disown
|
||||||
sudo service cron start
|
sudo service cron start
|
||||||
|
|||||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Liberama",
|
"name": "Liberama",
|
||||||
"version": "0.9.9",
|
"version": "0.9.10",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -10124,6 +10124,21 @@
|
|||||||
"integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
|
"integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"pidusage": {
|
||||||
|
"version": "2.0.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.21.tgz",
|
||||||
|
"integrity": "sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA==",
|
||||||
|
"requires": {
|
||||||
|
"safe-buffer": "^5.2.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"pify": {
|
"pify": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Liberama",
|
"name": "Liberama",
|
||||||
"version": "0.9.9",
|
"version": "0.9.10",
|
||||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||||
"license": "CC0-1.0",
|
"license": "CC0-1.0",
|
||||||
"repository": "bookpauk/liberama",
|
"repository": "bookpauk/liberama",
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon --inspect --exec 'node server'",
|
"dev": "nodemon --inspect --ignore server/public --ignore server/data --exec 'node server'",
|
||||||
"build:client": "webpack --config build/webpack.prod.config.js",
|
"build:client": "webpack --config build/webpack.prod.config.js",
|
||||||
"build:linux": "npm run build:client && node build/linux && pkg -t latest-linux-x64 -o dist/linux/liberama .",
|
"build:linux": "npm run build:client && node build/linux && pkg -t latest-linux-x64 -o dist/linux/liberama .",
|
||||||
"build:win": "npm run build:client && node build/win && pkg -t latest-win-x64 -o dist/win/liberama .",
|
"build:win": "npm run build:client && node build/win && pkg -t latest-win-x64 -o dist/win/liberama .",
|
||||||
@@ -72,6 +72,7 @@
|
|||||||
"multer": "^1.4.2",
|
"multer": "^1.4.2",
|
||||||
"pako": "^1.0.11",
|
"pako": "^1.0.11",
|
||||||
"path-browserify": "^1.0.0",
|
"path-browserify": "^1.0.0",
|
||||||
|
"pidusage": "^2.0.21",
|
||||||
"quasar": "^1.14.3",
|
"quasar": "^1.14.3",
|
||||||
"safe-buffer": "^5.2.0",
|
"safe-buffer": "^5.2.0",
|
||||||
"sjcl": "^1.0.8",
|
"sjcl": "^1.0.8",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class ReaderController extends BaseController {
|
|||||||
enableSitesFilter: (request.hasOwnProperty('enableSitesFilter') ? request.enableSitesFilter : true),
|
enableSitesFilter: (request.hasOwnProperty('enableSitesFilter') ? request.enableSitesFilter : true),
|
||||||
skipCheck: (request.hasOwnProperty('skipCheck') ? request.skipCheck : false),
|
skipCheck: (request.hasOwnProperty('skipCheck') ? request.skipCheck : false),
|
||||||
isText: (request.hasOwnProperty('isText') ? request.isText : false),
|
isText: (request.hasOwnProperty('isText') ? request.isText : false),
|
||||||
|
uploadFileName: (request.hasOwnProperty('uploadFileName') ? request.uploadFileName : false),
|
||||||
});
|
});
|
||||||
const state = this.workerState.getState(workerId);
|
const state = this.workerState.getState(workerId);
|
||||||
return (state ? state : {});
|
return (state ? state : {});
|
||||||
|
|||||||
@@ -136,8 +136,9 @@ class WebSocketController {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
if (i > 2*60*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
if (i > 3*60*1000/refreshPause) {//3 мин ждем телодвижений воркера
|
||||||
this.send({state: 'error', error: 'Время ожидания процесса истекло'}, req, ws);
|
this.send({state: 'error', error: 'Время ожидания процесса истекло'}, req, ws);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
|
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ class FileDecompressor {
|
|||||||
constructor(limitFileSize = 0) {
|
constructor(limitFileSize = 0) {
|
||||||
this.detector = new FileDetector();
|
this.detector = new FileDetector();
|
||||||
this.limitFileSize = limitFileSize;
|
this.limitFileSize = limitFileSize;
|
||||||
|
|
||||||
|
this.rarPath = '/usr/bin/rar';
|
||||||
|
this.rarExists = false;
|
||||||
|
(async() => {
|
||||||
|
if (await fs.pathExists(this.rarPath))
|
||||||
|
this.rarExists = true;
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
async decompressNested(filename, outputDir) {
|
async decompressNested(filename, outputDir) {
|
||||||
@@ -30,7 +37,11 @@ class FileDecompressor {
|
|||||||
files: []
|
files: []
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!fileType || !(fileType.ext == 'zip' || fileType.ext == 'bz2' || fileType.ext == 'gz' || fileType.ext == 'tar')) {
|
if (!fileType || !(
|
||||||
|
fileType.ext == 'zip' || fileType.ext == 'bz2' || fileType.ext == 'gz'
|
||||||
|
|| fileType.ext == 'tar' || (this.rarExists && fileType.ext == 'rar')
|
||||||
|
)
|
||||||
|
) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +105,11 @@ class FileDecompressor {
|
|||||||
async decompress(fileExt, filename, outputDir) {
|
async decompress(fileExt, filename, outputDir) {
|
||||||
let files = [];
|
let files = [];
|
||||||
|
|
||||||
|
if (fileExt == 'rar' && this.rarExists) {
|
||||||
|
files = await this.unRar(filename, outputDir);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
switch (fileExt) {
|
switch (fileExt) {
|
||||||
case 'zip':
|
case 'zip':
|
||||||
files = await this.unZip(filename, outputDir);
|
files = await this.unZip(filename, outputDir);
|
||||||
@@ -119,17 +135,16 @@ class FileDecompressor {
|
|||||||
try {
|
try {
|
||||||
return await zip.unpack(filename, outputDir, {
|
return await zip.unpack(filename, outputDir, {
|
||||||
limitFileSize: this.limitFileSize,
|
limitFileSize: this.limitFileSize,
|
||||||
limitFileCount: 1000,
|
limitFileCount: 10000,
|
||||||
decodeEntryNameCallback: (nameRaw) => {
|
decodeEntryNameCallback: (nameRaw) => {
|
||||||
return utils.bufferRemoveZeroes(nameRaw);
|
return utils.bufferRemoveZeroes(nameRaw);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
fs.emptyDir(outputDir);
|
fs.emptyDir(outputDir);
|
||||||
return await zip.unpack(filename, outputDir, {
|
return await zip.unpack(filename, outputDir, {
|
||||||
limitFileSize: this.limitFileSize,
|
limitFileSize: this.limitFileSize,
|
||||||
limitFileCount: 1000,
|
limitFileCount: 10000,
|
||||||
decodeEntryNameCallback: (nameRaw) => {
|
decodeEntryNameCallback: (nameRaw) => {
|
||||||
nameRaw = utils.bufferRemoveZeroes(nameRaw);
|
nameRaw = utils.bufferRemoveZeroes(nameRaw);
|
||||||
const enc = textUtils.getEncodingLite(nameRaw);
|
const enc = textUtils.getEncodingLite(nameRaw);
|
||||||
@@ -156,7 +171,7 @@ class FileDecompressor {
|
|||||||
|
|
||||||
if (this.limitFileSize) {
|
if (this.limitFileSize) {
|
||||||
if ((await fs.stat(filename)).size > this.limitFileSize) {
|
if ((await fs.stat(filename)).size > this.limitFileSize) {
|
||||||
reject('Файл слишком большой');
|
reject(new Error('Файл слишком большой'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,7 +237,39 @@ class FileDecompressor {
|
|||||||
|
|
||||||
inputStream.pipe(stream).pipe(outputStream);
|
inputStream.pipe(stream).pipe(outputStream);
|
||||||
})().catch(reject); });
|
})().catch(reject); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async unRar(filename, outputDir) {
|
||||||
|
try {
|
||||||
|
const args = ['x', '-p-', '-y', filename, `${outputDir}`];
|
||||||
|
const result = await utils.spawnProcess(this.rarPath, {
|
||||||
|
killAfter: 60,
|
||||||
|
args
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.code == 0) {
|
||||||
|
const files = [];
|
||||||
|
await utils.findFiles(async(file) => {
|
||||||
|
const stat = await fs.stat(file);
|
||||||
|
files.push({path: path.relative(outputDir, file), size: stat.size});
|
||||||
|
}, outputDir);
|
||||||
|
|
||||||
|
return files;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const error = `${result.code}|FORLOG|, exec: ${this.rarPath}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
|
||||||
|
throw new Error(`Архиватор Rar завершился с ошибкой: ${error}`);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
if (e.status == 'killed') {
|
||||||
|
throw new Error('Слишком долгое ожидание архиватора Rar');
|
||||||
|
} else if (e.status == 'error') {
|
||||||
|
throw new Error(e.error);
|
||||||
|
} else {
|
||||||
|
throw new Error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async gzipBuffer(buf) {
|
async gzipBuffer(buf) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class FileDetector {
|
|||||||
|
|
||||||
fromBuffer(buffer, callback) {
|
fromBuffer(buffer, callback) {
|
||||||
let result = null;
|
let result = null;
|
||||||
|
//console.log(buffer);
|
||||||
const invalidSignaturesList = this.validateSigantures();
|
const invalidSignaturesList = this.validateSigantures();
|
||||||
if (invalidSignaturesList.length) {
|
if (invalidSignaturesList.length) {
|
||||||
return callback(invalidSignaturesList);
|
return callback(invalidSignaturesList);
|
||||||
|
|||||||
@@ -676,7 +676,9 @@
|
|||||||
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" },
|
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" },
|
||||||
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" },
|
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" },
|
||||||
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d27312e3027" },
|
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d27312e3027" },
|
||||||
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d27312e3027" }
|
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d27312e3027" },
|
||||||
|
{ "type": "equal", "end": 40, "bytes": "fffe3c003f0078006d006c002000760065007200730069006f006e003d00220031002e0030002200" },
|
||||||
|
{ "type": "equal", "end": 40, "bytes": "fffe3c003f0078006d006c002000760065007200730069006f006e003d00270031002e0030002700" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class LimitedQueue {
|
|||||||
get(onPlaceChange) {
|
get(onPlaceChange) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (this.destroyed)
|
if (this.destroyed)
|
||||||
reject('destroyed');
|
reject(new Error('destroyed'));
|
||||||
|
|
||||||
const take = () => {
|
const take = () => {
|
||||||
if (this.freed <= 0)
|
if (this.freed <= 0)
|
||||||
@@ -73,7 +73,7 @@ class LimitedQueue {
|
|||||||
if (onPlaceChange)
|
if (onPlaceChange)
|
||||||
onPlaceChange(this.listeners.length);
|
onPlaceChange(this.listeners.length);
|
||||||
} else {
|
} else {
|
||||||
reject('Превышен размер очереди ожидания');
|
reject(new Error('Превышен размер очереди ожидания'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
1
server/core/Reader/BookConverter/.gitignore
vendored
1
server/core/Reader/BookConverter/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
test
|
|
||||||
@@ -6,7 +6,7 @@ const LimitedQueue = require('../../LimitedQueue');
|
|||||||
const textUtils = require('./textUtils');
|
const textUtils = require('./textUtils');
|
||||||
const utils = require('../../utils');
|
const utils = require('../../utils');
|
||||||
|
|
||||||
const queue = new LimitedQueue(2, 20, 3*60*1000);//3 минуты ожидание подвижек
|
const queue = new LimitedQueue(3, 20, 2*60*1000);//2 минуты ожидание подвижек
|
||||||
|
|
||||||
class ConvertBase {
|
class ConvertBase {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
@@ -42,20 +42,35 @@ class ConvertBase {
|
|||||||
throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
|
throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abort = (abort ? abort : () => false);
|
||||||
|
const myAbort = () => {
|
||||||
|
return q.abort() || abort();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (myAbort())
|
||||||
|
throw new Error('abort');
|
||||||
|
|
||||||
const result = await utils.spawnProcess(path, {
|
const result = await utils.spawnProcess(path, {
|
||||||
killAfter: 600,
|
killAfter: 3600,//1 час
|
||||||
args,
|
args,
|
||||||
onData: (data) => {
|
onData: (data) => {
|
||||||
q.resetTimeout();
|
if (queue.freed > 0)
|
||||||
|
q.resetTimeout();
|
||||||
onData(data);
|
onData(data);
|
||||||
},
|
},
|
||||||
abort
|
//будем периодически проверять работу конвертера и если очереди нет, то разрешаем работу пинком onData
|
||||||
|
onUsage: (stats) => {
|
||||||
|
if (queue.freed > 0 && stats.cpu >= 10) {
|
||||||
|
q.resetTimeout();
|
||||||
|
onData('.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUsageInterval: 10,
|
||||||
|
abort: myAbort
|
||||||
});
|
});
|
||||||
if (result.code != 0) {
|
if (result.code != 0) {
|
||||||
let error = result.code;
|
const error = `${result.code}|FORLOG|, exec: ${path}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
|
||||||
if (this.config.branch == 'development')
|
|
||||||
error = `exec: ${path}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
|
|
||||||
throw new Error(`Внешний конвертер завершился с ошибкой: ${error}`);
|
throw new Error(`Внешний конвертер завершился с ошибкой: ${error}`);
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
|||||||
102
server/core/Reader/BookConverter/ConvertDjvu.js
Normal file
102
server/core/Reader/BookConverter/ConvertDjvu.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const utils = require('../../utils');
|
||||||
|
|
||||||
|
const ConvertHtml = require('./ConvertHtml');
|
||||||
|
|
||||||
|
class ConvertDjvu extends ConvertHtml {
|
||||||
|
check(data, opts) {
|
||||||
|
const {inputFiles} = opts;
|
||||||
|
|
||||||
|
return this.config.useExternalBookConverter &&
|
||||||
|
inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'djvu';
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(data, opts) {
|
||||||
|
if (!this.check(data, opts))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const {inputFiles, callback, abort, uploadFileName} = opts;
|
||||||
|
|
||||||
|
const ddjvuPath = '/usr/bin/ddjvu';
|
||||||
|
if (!await fs.pathExists(ddjvuPath))
|
||||||
|
throw new Error('Внешний конвертер ddjvu не найден');
|
||||||
|
|
||||||
|
const tiffsplitPath = '/usr/bin/tiffsplit';
|
||||||
|
if (!await fs.pathExists(tiffsplitPath))
|
||||||
|
throw new Error('Внешний конвертер tiffsplitPath не найден');
|
||||||
|
|
||||||
|
const mogrifyPath = '/usr/bin/mogrify';
|
||||||
|
if (!await fs.pathExists(mogrifyPath))
|
||||||
|
throw new Error('Внешний конвертер mogrifyPath не найден');
|
||||||
|
|
||||||
|
const dir = `${inputFiles.filesDir}/`;
|
||||||
|
const inpFile = `${dir}${path.basename(inputFiles.sourceFile)}`;
|
||||||
|
const tifFile = `${inpFile}.tif`;
|
||||||
|
|
||||||
|
//конвертируем в tiff
|
||||||
|
let perc = 0;
|
||||||
|
await this.execConverter(ddjvuPath, ['-format=tiff', '-quality=50', '-verbose', inputFiles.sourceFile, tifFile], () => {
|
||||||
|
perc = (perc < 100 ? perc + 1 : 40);
|
||||||
|
callback(perc);
|
||||||
|
}, abort);
|
||||||
|
|
||||||
|
const tifFileSize = (await fs.stat(tifFile)).size;
|
||||||
|
let limitSize = 3*this.config.maxUploadFileSize;
|
||||||
|
if (tifFileSize > limitSize) {
|
||||||
|
throw new Error(`Файл для конвертирования слишком большой|FORLOG| ${tifFileSize} > ${limitSize}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
//разбиваем на файлы
|
||||||
|
await this.execConverter(tiffsplitPath, [tifFile, dir], null, abort);
|
||||||
|
|
||||||
|
await fs.remove(tifFile);
|
||||||
|
|
||||||
|
//конвертируем в jpg
|
||||||
|
await this.execConverter(mogrifyPath, ['-quality', '20', '-scale', '2048', '-verbose', '-format', 'jpg', `${dir}*.tif`], () => {
|
||||||
|
perc = (perc < 100 ? perc + 1 : 40);
|
||||||
|
callback(perc);
|
||||||
|
}, abort);
|
||||||
|
|
||||||
|
//читаем изображения
|
||||||
|
const loadImage = async(image) => {
|
||||||
|
image.data = (await fs.readFile(image.file)).toString('base64');
|
||||||
|
image.name = path.basename(image.file);
|
||||||
|
}
|
||||||
|
|
||||||
|
let files = [];
|
||||||
|
await utils.findFiles(async(file) => {
|
||||||
|
if (path.extname(file) == '.jpg')
|
||||||
|
files.push({name: file, base: path.basename(file)});
|
||||||
|
}, dir);
|
||||||
|
|
||||||
|
files.sort((a, b) => a.base.localeCompare(b.base));
|
||||||
|
|
||||||
|
let images = [];
|
||||||
|
let loading = [];
|
||||||
|
files.forEach(f => {
|
||||||
|
const image = {file: f.name};
|
||||||
|
images.push(image);
|
||||||
|
loading.push(loadImage(image));
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(loading);
|
||||||
|
|
||||||
|
//формируем текст
|
||||||
|
limitSize = 2*this.config.maxUploadFileSize;
|
||||||
|
let title = '';
|
||||||
|
if (uploadFileName)
|
||||||
|
title = uploadFileName;
|
||||||
|
let text = `<title>${title}</title>`;
|
||||||
|
for (const image of images) {
|
||||||
|
text += `<fb2-image type="image/jpeg" name="${image.name}">${image.data}</fb2-image>`;
|
||||||
|
|
||||||
|
if (text.length > limitSize) {
|
||||||
|
throw new Error(`Файл для конвертирования слишком большой|FORLOG| text.length: ${text.length} > ${limitSize}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await super.run(Buffer.from(text), {skipCheck: true, isText: true, cutTitle: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ConvertDjvu;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
const ConvertBase = require('./ConvertBase');
|
const ConvertBase = require('./ConvertBase');
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require('iconv-lite');
|
||||||
|
const textUtils = require('./textUtils');
|
||||||
|
|
||||||
class ConvertFb2 extends ConvertBase {
|
class ConvertFb2 extends ConvertBase {
|
||||||
check(data, opts) {
|
check(data, opts) {
|
||||||
@@ -9,26 +10,46 @@ class ConvertFb2 extends ConvertBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async run(data, opts) {
|
async run(data, opts) {
|
||||||
if (!this.check(data, opts))
|
let newData = data;
|
||||||
|
|
||||||
|
//Корректируем кодировку, 16-битные кодировки должны стать utf-8
|
||||||
|
const encoding = textUtils.getEncoding(newData);
|
||||||
|
if (encoding.indexOf('UTF-16') == 0) {
|
||||||
|
newData = Buffer.from(iconv.decode(newData, encoding));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.check(newData, opts))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return this.checkEncoding(data);
|
return this.checkEncoding(newData);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkEncoding(data) {
|
checkEncoding(data) {
|
||||||
let result = data;
|
let result = data;
|
||||||
|
|
||||||
const left = data.indexOf('<?xml version="1.0"');
|
let q = '"';
|
||||||
|
let left = data.indexOf('<?xml version="1.0"');
|
||||||
|
if (left < 0) {
|
||||||
|
left = data.indexOf('<?xml version=\'1.0\'');
|
||||||
|
q = '\'';
|
||||||
|
}
|
||||||
|
|
||||||
if (left >= 0) {
|
if (left >= 0) {
|
||||||
const right = data.indexOf('?>', left);
|
const right = data.indexOf('?>', left);
|
||||||
if (right >= 0) {
|
if (right >= 0) {
|
||||||
const head = data.slice(left, right + 2).toString();
|
const head = data.slice(left, right + 2).toString();
|
||||||
const m = head.match(/encoding="(.*?)"/);
|
const m = head.match(/encoding=['"](.*?)['"]/);
|
||||||
if (m) {
|
if (m) {
|
||||||
let encoding = m[1].toLowerCase();
|
let encoding = m[1].toLowerCase();
|
||||||
if (encoding != 'utf-8') {
|
if (encoding != 'utf-8') {
|
||||||
result = iconv.decode(data, encoding);
|
//encoding может не соответсвовать реальной кодировке файла, поэтому:
|
||||||
result = Buffer.from(result.toString().replace(m[0], 'encoding="utf-8"'));
|
let calcEncoding = textUtils.getEncoding(data);
|
||||||
|
if (calcEncoding.indexOf('ISO-8859') >= 0) {
|
||||||
|
calcEncoding = encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = iconv.decode(data, calcEncoding);
|
||||||
|
result = Buffer.from(result.toString().replace(m[0], `encoding=${q}utf-8${q}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ class ConvertHtml extends ConvertBase {
|
|||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
//если разброс не слишком большой, выделяем параграфы
|
//если разброс не слишком большой, выделяем параграфы
|
||||||
if (d < 10 && spaceCounter.length) {
|
if (d < 20 && spaceCounter.length) {
|
||||||
total /= 20;
|
total /= 20;
|
||||||
i = spaceCounter.length - 1;
|
i = spaceCounter.length - 1;
|
||||||
while (i > 0 && (!spaceCounter[i] || spaceCounter[i] < total)) i--;
|
while (i > 0 && (!spaceCounter[i] || spaceCounter[i] < total)) i--;
|
||||||
|
|||||||
@@ -16,15 +16,17 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
async run(notUsed, opts) {
|
async run(notUsed, opts) {
|
||||||
if (!this.check(notUsed, opts))
|
if (!this.check(notUsed, opts))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
await this.checkExternalConverterPresent();
|
await this.checkExternalConverterPresent();
|
||||||
|
|
||||||
const {inputFiles, callback, abort} = opts;
|
const {inputFiles, callback, abort, uploadFileName} = opts;
|
||||||
|
|
||||||
|
const inpFile = inputFiles.sourceFile;
|
||||||
const outFile = `${inputFiles.filesDir}/${utils.randomHexString(10)}.xml`;
|
const outFile = `${inputFiles.filesDir}/${utils.randomHexString(10)}.xml`;
|
||||||
|
|
||||||
//конвертируем в xml
|
//конвертируем в xml
|
||||||
let perc = 0;
|
let perc = 0;
|
||||||
await this.execConverter(this.pdfToHtmlPath, ['-c', '-s', '-xml', inputFiles.sourceFile, outFile], () => {
|
await this.execConverter(this.pdfToHtmlPath, ['-nodrm', '-c', '-s', '-xml', inpFile, outFile], () => {
|
||||||
perc = (perc < 80 ? perc + 10 : 40);
|
perc = (perc < 80 ? perc + 10 : 40);
|
||||||
callback(perc);
|
callback(perc);
|
||||||
}, abort);
|
}, abort);
|
||||||
@@ -186,10 +188,17 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
indents[0] = 0;
|
indents[0] = 0;
|
||||||
|
|
||||||
//формируем текст
|
//формируем текст
|
||||||
|
const limitSize = 2*this.config.maxUploadFileSize;
|
||||||
|
if (!title && uploadFileName)
|
||||||
|
title = uploadFileName;
|
||||||
let text = `<title>${title}</title>`;
|
let text = `<title>${title}</title>`;
|
||||||
let concat = '';
|
let concat = '';
|
||||||
let sp = '';
|
let sp = '';
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
|
if (text.length > limitSize) {
|
||||||
|
throw new Error(`Файл для конвертирования слишком большой|FORLOG| text.length: ${text.length} > ${limitSize}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (line.isImage) {
|
if (line.isImage) {
|
||||||
text += `<fb2-image type="${line.type}" name="${line.name}">${line.data}</fb2-image>`;
|
text += `<fb2-image type="${line.type}" name="${line.name}">${line.data}</fb2-image>`;
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const FileDetector = require('../../FileDetector');
|
|||||||
//порядок важен
|
//порядок важен
|
||||||
const convertClassFactory = [
|
const convertClassFactory = [
|
||||||
require('./ConvertEpub'),
|
require('./ConvertEpub'),
|
||||||
|
require('./ConvertDjvu'),
|
||||||
require('./ConvertPdf'),
|
require('./ConvertPdf'),
|
||||||
require('./ConvertRtf'),
|
require('./ConvertRtf'),
|
||||||
require('./ConvertDocX'),
|
require('./ConvertDocX'),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const utils = require('../utils');
|
|||||||
const log = new (require('../AppLogger'))().log;//singleton
|
const log = new (require('../AppLogger'))().log;//singleton
|
||||||
|
|
||||||
const cleanDirPeriod = 60*60*1000;//1 раз в час
|
const cleanDirPeriod = 60*60*1000;//1 раз в час
|
||||||
const queue = new LimitedQueue(5, 100, 5*60*1000);//5 минут ожидание подвижек
|
const queue = new LimitedQueue(5, 100, 2*60*1000 + 15000);//2 минуты ожидание подвижек
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ class ReaderWorker {
|
|||||||
|
|
||||||
this.workerState = new WorkerState();
|
this.workerState = new WorkerState();
|
||||||
this.down = new FileDownloader(config.maxUploadFileSize);
|
this.down = new FileDownloader(config.maxUploadFileSize);
|
||||||
this.decomp = new FileDecompressor(2*config.maxUploadFileSize);
|
this.decomp = new FileDecompressor(3*config.maxUploadFileSize);
|
||||||
this.bookConverter = new BookConverter(this.config);
|
this.bookConverter = new BookConverter(this.config);
|
||||||
|
|
||||||
this.remoteWebDavStorage = false;
|
this.remoteWebDavStorage = false;
|
||||||
@@ -81,7 +81,7 @@ class ReaderWorker {
|
|||||||
const decompDirname = utils.randomHexString(30);
|
const decompDirname = utils.randomHexString(30);
|
||||||
|
|
||||||
//download or use uploaded
|
//download or use uploaded
|
||||||
if (url.indexOf('file://') != 0) {//download
|
if (url.indexOf('disk://') != 0) {//download
|
||||||
const downdata = await this.down.load(url, (progress) => {
|
const downdata = await this.down.load(url, (progress) => {
|
||||||
wState.set({progress});
|
wState.set({progress});
|
||||||
}, q.abort);
|
}, q.abort);
|
||||||
@@ -130,6 +130,8 @@ class ReaderWorker {
|
|||||||
convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
|
convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
|
||||||
await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => {
|
await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => {
|
||||||
wState.set({progress});
|
wState.set({progress});
|
||||||
|
if (queue.freed > 0)
|
||||||
|
q.resetTimeout();
|
||||||
}, q.abort);
|
}, q.abort);
|
||||||
|
|
||||||
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
|
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
|
||||||
@@ -171,10 +173,15 @@ class ReaderWorker {
|
|||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(LM_ERR, e.stack);
|
log(LM_ERR, e.stack);
|
||||||
|
let mes = e.message.split('|FORLOG|');
|
||||||
|
if (mes[1])
|
||||||
|
log(LM_ERR, mes[0] + mes[1]);
|
||||||
log(LM_ERR, `downloadedFilename: ${downloadedFilename}`);
|
log(LM_ERR, `downloadedFilename: ${downloadedFilename}`);
|
||||||
if (e.message == 'abort')
|
|
||||||
e.message = overLoadMes;
|
mes = mes[0];
|
||||||
wState.set({state: 'error', error: e.message});
|
if (mes == 'abort')
|
||||||
|
mes = overLoadMes;
|
||||||
|
wState.set({state: 'error', error: mes});
|
||||||
} finally {
|
} finally {
|
||||||
//clean
|
//clean
|
||||||
if (q)
|
if (q)
|
||||||
@@ -209,7 +216,7 @@ class ReaderWorker {
|
|||||||
await fs.remove(file.path);
|
await fs.remove(file.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `file://${hash}`;
|
return `disk://${hash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async restoreRemoteFile(filename) {
|
async restoreRemoteFile(filename) {
|
||||||
|
|||||||
@@ -76,13 +76,13 @@ class ZipStreamer {
|
|||||||
if (limitFileCount || limitFileSize || decodeEntryNameCallback) {
|
if (limitFileCount || limitFileSize || decodeEntryNameCallback) {
|
||||||
const entries = Object.values(unzip.entries());
|
const entries = Object.values(unzip.entries());
|
||||||
if (limitFileCount && entries.length > limitFileCount) {
|
if (limitFileCount && entries.length > limitFileCount) {
|
||||||
reject('Слишком много файлов');
|
reject(new Error('Слишком много файлов'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (limitFileSize && !entry.isDirectory && entry.size > limitFileSize) {
|
if (limitFileSize && !entry.isDirectory && entry.size > limitFileSize) {
|
||||||
reject('Файл слишком большой');
|
reject(new Error('Файл слишком большой'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const baseX = require('base-x');
|
const baseX = require('base-x');
|
||||||
|
const pidusage = require('pidusage');
|
||||||
|
|
||||||
const BASE36 = '0123456789abcdefghijklmnopqrstuvwxyz';
|
const BASE36 = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||||
const bs36 = baseX(BASE36);
|
const bs36 = baseX(BASE36);
|
||||||
@@ -45,10 +47,11 @@ async function touchFile(filename) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function spawnProcess(cmd, opts) {
|
function spawnProcess(cmd, opts) {
|
||||||
let {args, killAfter, onData, abort} = opts;
|
let {args, killAfter, onData, onUsage, onUsageInterval, abort} = opts;
|
||||||
killAfter = (killAfter ? killAfter : 120);//seconds
|
killAfter = (killAfter ? killAfter : 120);//seconds
|
||||||
onData = (onData ? onData : () => {});
|
onData = (onData ? onData : () => {});
|
||||||
args = (args ? args : []);
|
args = (args ? args : []);
|
||||||
|
onUsageInterval = (onUsageInterval ? onUsageInterval : 30);//seconds
|
||||||
|
|
||||||
return new Promise((resolve, reject) => { (async() => {
|
return new Promise((resolve, reject) => { (async() => {
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
@@ -75,9 +78,19 @@ function spawnProcess(cmd, opts) {
|
|||||||
reject({status: 'error', error, stdout, stderr});
|
reject({status: 'error', error, stdout, stderr});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//ждем процесс, контролируем его работу раз в секунду
|
||||||
|
let onUsageCounter = onUsageInterval;
|
||||||
while (!resolved) {
|
while (!resolved) {
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
killAfter -= 1;
|
|
||||||
|
onUsageCounter--;
|
||||||
|
if (onUsage && onUsageCounter <= 0) {
|
||||||
|
const stats = await pidusage(proc.pid);
|
||||||
|
onUsage(stats);
|
||||||
|
onUsageCounter = onUsageInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
killAfter--;
|
||||||
if (killAfter <= 0 || (abort && abort())) {
|
if (killAfter <= 0 || (abort && abort())) {
|
||||||
process.kill(proc.pid);
|
process.kill(proc.pid);
|
||||||
if (killAfter <= 0) {
|
if (killAfter <= 0) {
|
||||||
@@ -91,6 +104,22 @@ function spawnProcess(cmd, opts) {
|
|||||||
})().catch(reject); });
|
})().catch(reject); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findFiles(callback, dir) {
|
||||||
|
if (!(callback && dir))
|
||||||
|
return;
|
||||||
|
let result = true;
|
||||||
|
const files = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const found = path.resolve(dir, file.name);
|
||||||
|
if (file.isDirectory())
|
||||||
|
result = await findFiles(callback, found);
|
||||||
|
else
|
||||||
|
await callback(found);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
toBase36,
|
toBase36,
|
||||||
fromBase36,
|
fromBase36,
|
||||||
@@ -99,5 +128,6 @@ module.exports = {
|
|||||||
sleep,
|
sleep,
|
||||||
randomHexString,
|
randomHexString,
|
||||||
touchFile,
|
touchFile,
|
||||||
spawnProcess
|
spawnProcess,
|
||||||
|
findFiles
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user