Compare commits

...

146 Commits

Author SHA1 Message Date
Book Pauk
f7e1e09928 Merge branch 'release/0.11.8-7' 2022-07-19 00:52:36 +07:00
Book Pauk
f0832b07cb Исправление привнесенного бага 2022-07-19 00:50:44 +07:00
Book Pauk
7c253df291 Merge tag '0.11.8-6' into develop
0.11.8-6
2022-07-19 00:36:00 +07:00
Book Pauk
bb7cd9cbde Merge branch 'release/0.11.8-6' 2022-07-19 00:35:55 +07:00
Book Pauk
56c4182985 Небольшой тюнинг 2022-07-19 00:35:12 +07:00
Book Pauk
cb6c7536bf Небольшой тюнинг 2022-07-19 00:32:52 +07:00
Book Pauk
fbfe8cbda0 Решение проблемы невалидного tls-сертификата 2022-07-19 00:27:54 +07:00
Book Pauk
6129d2d7eb Небольшие поправки 2022-07-19 00:14:18 +07:00
Book Pauk
16b30c922a Улучшение работы с удаленным хранилищем 2022-07-18 23:54:25 +07:00
Book Pauk
c42ad66be6 Merge tag '0.11.8-5' into develop
0.11.8-5
2022-07-17 21:15:37 +07:00
Book Pauk
f36c13fea1 Merge branch 'release/0.11.8-5' 2022-07-17 21:15:31 +07:00
Book Pauk
4fd9d579e0 Небольшие доработки remoteSent, оптимизация отправки файлов 2022-07-17 21:10:52 +07:00
Book Pauk
e65a8a13ea Рефакторинг 2022-07-17 20:04:23 +07:00
Book Pauk
6ddb97d43e Тюнинг таймаутов 2022-07-17 17:11:34 +07:00
Book Pauk
89082603de Merge tag '0.11.8-4' into develop
0.11.8-4
2022-07-17 16:54:15 +07:00
Book Pauk
a9a3227433 Merge branch 'release/0.11.8-4' 2022-07-17 16:53:59 +07:00
Book Pauk
60cb3514b2 Тюнинг таймаутов 2022-07-17 16:53:12 +07:00
Book Pauk
4aeaa05f0b Merge tag '0.11.8-3' into develop
0.11.8-3
2022-07-17 15:58:34 +07:00
Book Pauk
9c06552278 Merge branch 'release/0.11.8-3' 2022-07-17 15:58:28 +07:00
Book Pauk
000f8dde82 Переход на RemoteStorage 2022-07-17 15:43:12 +07:00
Book Pauk
9ffc218002 Поправка 2022-07-16 21:36:50 +07:00
Book Pauk
68a188f099 Конфиг nginx 2022-07-16 21:10:33 +07:00
Book Pauk
8829bb3810 Конфиг nginx 2022-07-16 21:07:16 +07:00
Book Pauk
5164d2f536 Merge tag '0.11.8-2' into develop
0.11.8-2
2022-07-16 21:02:05 +07:00
Book Pauk
451538fcf7 Merge branch 'release/0.11.8-2' 2022-07-16 21:01:56 +07:00
Book Pauk
82a02ef339 Удаление более ненужной функциональности 2022-07-16 20:48:50 +07:00
Book Pauk
b834d4951f Обработка ошибок 2022-07-16 20:40:21 +07:00
Book Pauk
edc3b669be Добавлено восстановление файлов из webdav 2022-07-16 20:35:34 +07:00
Book Pauk
522826311d Переделка механизма чистки папок и отправки через RemoteWebDavStorage 2022-07-16 20:24:37 +07:00
Book Pauk
e69b9951d5 Отключил проверку валидности tls-сертификата 2022-07-16 18:43:09 +07:00
Book Pauk
c6300222ea Мелкий рефакторинг 2022-07-16 17:54:27 +07:00
Book Pauk
5aa6ee899c Изменение механизма работы с /tmp и /upload (начало) 2022-07-16 17:35:32 +07:00
Book Pauk
4b76f97d2b Поправки конфигов nginx 2022-07-16 15:45:52 +07:00
Book Pauk
5ccfe71c55 Начало работы над BookUpdateChecker 2022-07-16 13:16:57 +07:00
Book Pauk
97fc902cdb Поправлен баг 2022-07-15 23:53:54 +07:00
Book Pauk
7e935951d7 Поправка разметки 2022-07-15 23:17:30 +07:00
Book Pauk
810c6d68d2 Поправка разметки 2022-07-15 23:14:09 +07:00
Book Pauk
003dc70f4f Merge tag '0.11.8-1' into develop
0.11.8-1
2022-07-15 18:14:12 +07:00
Book Pauk
371ff64a95 Merge branch 'release/0.11.8-1' 2022-07-15 18:14:06 +07:00
Book Pauk
b0de5adbf3 Добавлена возможность скачивать обои 2022-07-15 18:11:24 +07:00
Book Pauk
d1d2b07c33 Поправки разметки 2022-07-15 17:42:19 +07:00
Book Pauk
d9b2444c1a Улучшен механизм загрузки обложек 2022-07-15 17:36:49 +07:00
Book Pauk
e7fae27031 Убрал отладку 2022-07-15 17:17:00 +07:00
Book Pauk
eb0c7b0a32 Отладка 2022-07-15 17:11:58 +07:00
Book Pauk
3d7ad0dd9a Небюольшие оптимизации загрузки обложек 2022-07-15 17:05:17 +07:00
Book Pauk
ae04feb311 Merge tag '0.11.8' into develop
0.11.8
2022-07-15 02:11:03 +07:00
Book Pauk
7b59f911ef Merge branch 'release/0.11.8' 2022-07-15 02:10:58 +07:00
Book Pauk
d3444da647 Поправки разметки 2022-07-15 01:58:42 +07:00
Book Pauk
66738d0c9c К предыдущему 2022-07-15 01:51:28 +07:00
Book Pauk
7e187acd68 Версия 0.11.8 2022-07-15 01:50:17 +07:00
Book Pauk
c751372a54 Добавлен resizeImage 2022-07-15 01:38:25 +07:00
Book Pauk
7fc98fc7da Добавление отображения обложки (coverpage) в окне загруженных файлов 2022-07-15 00:47:24 +07:00
Book Pauk
b56f45694e Добавлен coversStorage для хранения coverpage 2022-07-15 00:45:56 +07:00
Book Pauk
091ca521ef Новые upload-методы 2022-07-15 00:45:09 +07:00
Book Pauk
c7a17b0a76 Добавлена синхронизация файлов обоев 2022-07-14 20:14:40 +07:00
Book Pauk
26468b996a Мелкая поправка 2022-07-14 20:12:37 +07:00
Book Pauk
c4e240d87c Увеличил maxPayloadSize 2022-07-14 20:11:17 +07:00
Book Pauk
04713f47c8 Небольшие поправки 2022-07-14 16:14:25 +07:00
Book Pauk
37ab3493db Merge tag '0.11.7-6' into develop
0.11.7-6
2022-07-14 03:52:50 +07:00
Book Pauk
a4cb3c628e Merge branch 'release/0.11.7-6' 2022-07-14 03:52:44 +07:00
Book Pauk
8492da8a13 Небольшое улучшение 2022-07-14 03:51:59 +07:00
Book Pauk
98d7c64a56 Исправление багов 2022-07-14 03:34:55 +07:00
Book Pauk
25f121e5ed Merge tag '0.11.7-5' into develop
0.11.7-5
2022-07-14 01:57:36 +07:00
Book Pauk
4c8797c99c Merge branch 'release/0.11.7-5' 2022-07-14 01:57:30 +07:00
Book Pauk
1155aa285d Лишние пробелы 2022-07-14 01:57:03 +07:00
Book Pauk
239bbb8263 Добавлено восстановление из архива 2022-07-14 01:55:09 +07:00
Book Pauk
e6b9330108 Добавление работы с архивом 2022-07-14 01:17:09 +07:00
Book Pauk
935b767c2e Поправил поведение buttonActiveClass 2022-07-14 00:31:24 +07:00
Book Pauk
8acf3295b5 Поправил разметку 2022-07-14 00:31:09 +07:00
Book Pauk
48c3a12fa0 Улучшение парсинга плохих fb2 2022-07-14 00:30:27 +07:00
Book Pauk
a1dea514b7 Поправка разметки 2022-07-13 23:47:55 +07:00
Book Pauk
d4788439cb Merge tag '0.11.7-4' into develop
0.11.7-4
2022-07-13 16:38:10 +07:00
Book Pauk
0a60ad354c Merge branch 'release/0.11.7-4' 2022-07-13 16:38:04 +07:00
Book Pauk
c565a20344 Поправки разметки 2022-07-13 16:37:47 +07:00
Book Pauk
735ee88f0b Merge tag '0.11.7-3' into develop
0.11.7-3
2022-07-13 16:34:22 +07:00
Book Pauk
9405ce2cc0 Merge branch 'release/0.11.7-3' 2022-07-13 16:34:16 +07:00
Book Pauk
115277d88a Поправки разметки 2022-07-13 16:34:00 +07:00
Book Pauk
6925c11dbd Merge tag '0.11.7-2' into develop
0.11.7-2
2022-07-13 16:25:11 +07:00
Book Pauk
984d835892 Merge branch 'release/0.11.7-2' 2022-07-13 16:25:05 +07:00
Book Pauk
23353a4960 Улучшен парсинг fb2 2022-07-13 16:23:52 +07:00
Book Pauk
955bcda032 Поправки разметки 2022-07-13 15:01:35 +07:00
Book Pauk
81ad5d7a2c Поправки разметки 2022-07-13 14:47:24 +07:00
Book Pauk
dada7980ec Merge tag '0.11.7-1' into develop
0.11.7-1
2022-07-12 19:23:38 +07:00
Book Pauk
511a308646 Merge branch 'release/0.11.7-1' 2022-07-12 19:23:33 +07:00
Book Pauk
65c8f2cc81 Небольшие поправки на панели, изменена нумерация на обратную 2022-07-12 19:21:26 +07:00
Book Pauk
238c18bc48 Merge tag '0.11.7' into develop
0.11.7
2022-07-12 19:08:35 +07:00
Book Pauk
873a08fee1 Merge branch 'release/0.11.7' 2022-07-12 19:08:27 +07:00
Book Pauk
7e89228803 Версия 0.11.7 2022-07-12 19:07:39 +07:00
Book Pauk
fc630923a4 Настройка методов сортировки 2022-07-12 18:50:35 +07:00
Book Pauk
928f911d03 Добавлены подсказки к кнопкам 2022-07-12 17:53:14 +07:00
Book Pauk
7ffcd3fe1b Поправки поведения при скроллинге 2022-07-12 17:33:03 +07:00
Book Pauk
0efbaf643a Поправил сообщение об ошибке 2022-07-12 17:32:19 +07:00
Book Pauk
f1bf8e54ae Добавлен метод scrollToActiveBook 2022-07-12 17:10:50 +07:00
Book Pauk
b4aa6ab6c8 Поправки поиска 2022-07-12 16:58:34 +07:00
Book Pauk
72431f0202 Работа над группировкой 2022-07-12 16:51:32 +07:00
Book Pauk
04a326c0e4 Работа над группировкой 2022-07-12 15:51:43 +07:00
Book Pauk
931966f4f3 Поправки разметки 2022-07-12 15:05:17 +07:00
Book Pauk
8808cc4779 Работа над группировкой по файлам 2022-07-12 14:46:34 +07:00
Book Pauk
988c959eba Работа над группировкой файлов 2022-07-12 04:05:51 +07:00
Book Pauk
c0b658d9e6 К предыдущему 2022-07-12 01:41:18 +07:00
Book Pauk
3190246f34 Улучшена реакция на onResize 2022-07-12 01:35:19 +07:00
Book Pauk
d957b4a5f9 Добавлена возможность автосокрытия панели при прокрутке 2022-07-12 01:03:44 +07:00
Book Pauk
bef9e5705c Поправки текстовых строк 2022-07-11 23:53:54 +07:00
Book Pauk
eb2affa518 Приведение input к единому стилю 2022-07-11 23:50:51 +07:00
Book Pauk
07b9a3c033 Мелкие правки 2022-07-11 22:28:48 +07:00
Book Pauk
3ca14ae06a Работа над группировкой 2022-07-11 22:26:34 +07:00
Book Pauk
7caa0c2112 Начало добавления группировки в RecentBooksPage 2022-07-11 20:11:38 +07:00
Book Pauk
9c69f5bc01 Поправил размер иконки 2022-07-11 20:10:51 +07:00
Book Pauk
125a2e0f17 Исправление багов 2022-07-11 17:12:17 +07:00
Book Pauk
1b4360b897 Дополнение в convertRecent 2022-07-11 16:26:03 +07:00
Book Pauk
4775d6e47b Поправлен баг 2022-07-10 20:07:33 +07:00
Book Pauk
33fc553c55 Добавлен запрос на объединение позиций при
обнаружении похожего файла в загруженных
2022-07-10 19:54:00 +07:00
Book Pauk
25cad81c50 Улучшение отображения загруженных 2022-07-10 19:53:30 +07:00
Book Pauk
02a2099c1f Поправлен z-index 2022-07-10 19:52:58 +07:00
Book Pauk
1cda186b1a Добавлен диалог askYesNo 2022-07-10 19:52:29 +07:00
Book Pauk
f10291b6c6 Поправка названия действия 2022-07-10 19:51:31 +07:00
Book Pauk
26ab5d6765 Рефакторинг 2022-07-10 18:27:05 +07:00
Book Pauk
5edeed0747 Изменение механизма хранения книг 2022-07-10 17:31:21 +07:00
Book Pauk
c878ce432f Небольшое исправление опознававния кодировки 2022-07-10 17:20:47 +07:00
Book Pauk
81798897c8 Изменения в механизме хранения книг:
теперь ориентируемся на "ключ-filepath", а не "ключ-url"
2022-07-10 16:38:54 +07:00
Book Pauk
63840fadbc К предыдущему 2022-07-10 14:59:39 +07:00
Book Pauk
36aa057035 Поправка цвета 2022-07-09 21:00:09 +07:00
Book Pauk
30afd2421c Рефакторинг 2022-07-09 20:50:31 +07:00
Book Pauk
53a1d90bd8 Улучшение поведения при очереди загрузки книг 2022-07-09 02:01:14 +07:00
Book Pauk
2ecf6beef2 Небольшой багфикс 2022-07-09 01:56:42 +07:00
Book Pauk
85910a20e9 Улучшение ContentsPage 2022-07-08 20:50:55 +07:00
Book Pauk
66cf7790b3 Улучшения ContentsPage 2022-07-08 19:09:57 +07:00
Book Pauk
4a9eb7e4bb Удалил устаревшее 2022-07-08 14:30:44 +07:00
Book Pauk
07446696c1 Поправлен цвет заголовка 2022-07-08 13:52:45 +07:00
Book Pauk
a29f9d9a4b Унификация размеров окон 2022-07-08 13:43:59 +07:00
Book Pauk
d49c9baec3 Унификация интерфейса 2022-07-08 13:34:53 +07:00
Book Pauk
8c9d4a12ee Настройка цветов 2022-07-08 13:24:13 +07:00
Book Pauk
fce69e4657 Настройка цветов 2022-07-08 13:21:42 +07:00
Book Pauk
b387509f88 Добавил блокировку при загрузке книг, теперь загружаются последовательно 2022-07-08 12:26:47 +07:00
Book Pauk
8dc8bdc0d6 Merge tag '0.11.6-2' into develop
0.11.6-2
2022-07-07 19:43:47 +07:00
Book Pauk
00caae8363 Merge branch 'release/0.11.6-2' 2022-07-07 19:43:40 +07:00
Book Pauk
2ead8570a7 Небольшая поправка 2022-07-07 19:39:02 +07:00
Book Pauk
408315466b Частичный откат предыдущих изменений 2022-07-07 19:38:17 +07:00
Book Pauk
c651836554 Поправки скриптов запуска 2022-07-07 19:33:32 +07:00
Book Pauk
03a1e70fce Поправки, чтобы не падал в случае детача скрина 2022-07-07 19:05:54 +07:00
Book Pauk
ab5a11a24f Убрал сайт flibs.in из сетевых библиотек 2022-07-07 17:42:05 +07:00
Book Pauk
8cd6ed472c Изменил client_max_body_size 100m 2022-07-07 17:37:25 +07:00
Book Pauk
055181b744 Исправлен баг выпадающих списков в оглавлении 2022-07-07 17:34:03 +07:00
Book Pauk
e331a3920b Актуализация пакетов 2022-07-07 17:29:47 +07:00
Book Pauk
c62bccb470 Улучшил журналирование ошибок БД 2022-07-07 16:24:59 +07:00
Book Pauk
ea351ea293 Merge tag '0.11.6-1' into develop
0.11.6-1
2022-07-04 12:23:55 +07:00
61 changed files with 2360 additions and 758 deletions

View File

@@ -1,5 +1,6 @@
import axios from 'axios';
import * as utils from '../share/utils';
import * as cryptoUtils from '../share/cryptoUtils';
import wsc from './webSocketConnection';
const api = axios.create({
@@ -119,32 +120,7 @@ class Reader {
estSize = response.headers['content-length'];
}
} catch (e) {
//восстановим при необходимости файл на сервере из удаленного облака
let response = null
try {
response = await wsc.message(await wsc.send({action: 'reader-restore-cached-file', path: url}));
} catch (e) {
console.error(e);
//если с WebSocket проблема, работаем по http
response = await api.post('/restore-cached-file', {path: url});
response = response.data;
}
if (response.state == 'error') {
throw new Error(response.error);
}
const workerId = response.workerId;
if (!workerId)
throw new Error('Неверный ответ api');
response = await this.getWorkerStateFinish(workerId);
if (response.state == 'error') {
throw new Error(response.error);
}
if (response.size && estSize < 0) {
estSize = response.size;
}
//
}
return estSize;
@@ -174,11 +150,10 @@ class Reader {
return await axios.get(url, options);
}
async uploadFile(file, maxUploadFileSize, callback) {
if (!maxUploadFileSize)
maxUploadFileSize = 10*1024*1024;
async uploadFile(file, maxUploadFileSize = 10*1024*1024, callback) {
if (file.size > maxUploadFileSize)
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
let formData = new FormData();
formData.append('file', file, file.name);
@@ -225,6 +200,35 @@ class Reader {
return response;
}
makeUrlFromBuf(buf) {
const key = utils.toHex(cryptoUtils.sha256(buf));
return `disk://${key}`;
}
async uploadFileBuf(buf, url) {
if (!url)
url = this.makeUrlFromBuf(buf);
let response;
try {
await axios.head(url.replace('disk://', '/upload/'), {headers: {'Cache-Control': 'no-cache'}});
response = await wsc.message(await wsc.send({action: 'upload-file-touch', url}));
} catch (e) {
response = await wsc.message(await wsc.send({action: 'upload-file-buf', buf}));
}
if (response.error)
throw new Error(response.error);
return response;
}
async getUploadedFileBuf(url) {
url = url.replace('disk://', '/upload/');
return (await axios.get(url)).data;
}
}
export default new Reader();

View File

@@ -11,7 +11,7 @@
Открыть выбранную закладку
</q-tooltip>
</q-btn>
<q-input ref="search" v-model="search" class="col" rounded outlined dense bg-color="white" placeholder="Найти">
<q-input ref="search" v-model="search" class="col" outlined dense bg-color="white" placeholder="Найти">
<template #append>
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch" />
</template>

View File

@@ -5,19 +5,19 @@
</template>
<template #buttons>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
<span class="header-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
</span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
<span class="header-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
<q-icon name="la la-plus" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip>
</span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
<span class="header-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
<q-icon name="la la-minus" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip>
</span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="showHelp">
<span class="header-button row justify-center items-center" @mousedown.stop @click="showHelp">
<q-icon name="la la-question-circle" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
</span>
@@ -32,7 +32,7 @@
:options="rootLinkOptions"
style="width: 230px"
dropdown-icon="la la-angle-down la-sm"
rounded outlined dense emit-value map-options display-value-sanitize options-sanitize
outlined dense emit-value map-options display-value-sanitize options-sanitize
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
>
<template #prepend>
@@ -61,7 +61,7 @@
:options="selectedLinkOptions"
style="width: 50px"
dropdown-icon="la la-angle-down la-sm"
rounded outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
@@ -73,7 +73,7 @@
ref="input"
v-model="bookUrl"
class="col q-mr-sm"
rounded outlined dense
outlined dense
bg-color="white"
placeholder="Скопируйте сюда ссылку на книгу и нажмите 'Открыть'"
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
@@ -99,7 +99,7 @@
</template>
</q-input>
<q-btn :disabled="!bookUrl" rounded color="green-7" no-caps size="14px" @click="submitUrl">
<q-btn :disabled="!bookUrl" color="green-7" no-caps size="14px" @click="submitUrl">
Открыть
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Открыть в читалке
@@ -894,14 +894,15 @@ export default vueComponent(ExternalLibs);
background-color: #A0A0A0;
}
.full-screen-button {
.header-button {
width: 30px;
height: 30px;
cursor: pointer;
}
.full-screen-button:hover {
background-color: #69C05F;
.header-button:hover {
color: white;
background-color: #39902F;
}
.transparent-layout {

View File

@@ -23,15 +23,15 @@
<div class="q-mb-sm" />
<div v-show="selectedTab == 'contents'" class="tab-panel">
<div v-show="selectedTab == 'contents'" ref="tabPanelContents" class="tab-panel">
<div>
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
<div :ref="`mainitem${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="20px" />
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="24px" />
</div>
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="20px" />
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="24px" />
</div>
<div class="col row clickable" @click="setBookPos(item.offset)">
<div :style="item.indentStyle"></div>
@@ -42,8 +42,12 @@
</div>
</div>
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition">
<div v-for="subitem in item.list" :key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}">
<div v-if="item.expanded" :ref="`subdiv${item.key}`" class="subitems-transition">
<div
v-for="subitem in item.list"
:ref="`subitem${subitem.key}`"
:key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}"
>
<div class="col row clickable" @click="setBookPos(subitem.offset)">
<div class="no-expand-button"></div>
<div :style="subitem.indentStyle"></div>
@@ -61,10 +65,10 @@
</div>
</div>
<div v-show="selectedTab == 'images'" class="tab-panel">
<div v-show="selectedTab == 'images'" ref="tabPanelImages" class="tab-panel">
<div>
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
<div :ref="`image${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
<div class="col row clickable" @click="setBookPos(item.offset)">
<div class="image-thumb-box row justify-center items-center">
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center">
@@ -124,7 +128,10 @@ const componentOptions = {
watch: {
bookPos() {
this.updateBookPosSelection();
}
},
selectedTab() {
this.updateBookPosScrollTop();
},
},
};
class ContentsPage {
@@ -282,31 +289,30 @@ class ContentsPage {
if (!this.isVisible)
return;
await utils.sleep(50);
await this.$nextTick();
const bp = this.bookPos;
for (let i = 0; i < this.contents.length; i++) {
const item = this.contents[i];
const nextOffset = (i < this.contents.length - 1 ? this.contents[i + 1].offset : this.parsed.textLength);
if (bp >= item.offset && bp < nextOffset) {
item.isBookPos = true;
} else if (item.isBookPos) {
item.isBookPos = false;
}
for (let j = 0; j < item.list.length; j++) {
const subitem = item.list[j];
const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset);
if (bp >= subitem.offset && bp < nextSubOffset) {
subitem.isBookPos = true;
this.contents[i] = Object.assign(item, {list: item.list});
this.updateBookPosScrollTop('contents', item, subitem, j);
} else if (subitem.isBookPos) {
subitem.isBookPos = false;
this.contents[i] = Object.assign(item, {list: item.list});
}
}
if (bp >= item.offset && bp < nextOffset) {
this.contents[i] = Object.assign(item, {isBookPos: true});
} else if (item.isBookPos) {
this.contents[i] = Object.assign(item, {isBookPos: false});
}
}
for (let i = 0; i < this.images.length; i++) {
@@ -314,11 +320,96 @@ class ContentsPage {
const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
if (bp >= img.offset && bp < nextOffset) {
this.images[i] = Object.assign(img, {isBookPos: true});
this.images[i].isBookPos = true;
} else if (img.isBookPos) {
this.images[i] = Object.assign(img, {isBookPos: false});
this.images[i].isBookPos = false;
}
}
this.updateBookPosScrollTop();
}
/*getOffsetTop(key) {
let el = this.getFirstElem(this.$refs[`mainitem${key}`]);
return (el ? el.offsetTop : 0);
}*/
async updateBookPosScrollTop() {
try {
await this.$nextTick();
if (this.selectedTab == 'contents') {
let item;
let subitem;
let i;
//ищем выделенные item
for(const _item of this.contents) {
if (_item.isBookPos) {
item = _item;
for (let ii = 0; ii < item.list.length; ii++) {
const _subitem = item.list[ii];
if (_subitem.isBookPos) {
subitem = _subitem;
i = ii;
break;
}
}
break;
}
}
if (!item)
return;
//вычисляем и смещаем tabPanel.scrollTop
let el = this.getFirstElem(this.$refs[`mainitem${item.key}`]);
let elShift = 0;
if (subitem && item.expanded) {
const subEl = this.getFirstElem(this.$refs[`subitem${subitem.key}`]);
elShift = el.offsetHeight - subEl.offsetHeight*(i + 1);
} else {
elShift = el.offsetHeight;
}
const tabPanel = this.$refs.tabPanelContents;
const halfH = tabPanel.clientHeight/2;
const newScrollTop = el.offsetTop - halfH - elShift;
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
tabPanel.scrollTop = newScrollTop;
}
if (this.selectedTab == 'images') {
let item;
//ищем выделенные item
for(const _item of this.images) {
if (_item.isBookPos) {
item = _item;
break;
}
}
if (!item)
return;
//вычисляем и смещаем tabPanel.scrollTop
let el = this.getFirstElem(this.$refs[`image${item.key}`]);
const tabPanel = this.$refs.tabPanelImages;
const halfH = tabPanel.clientHeight/2;
const newScrollTop = el.offsetTop - halfH - el.offsetHeight/2;
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
tabPanel.scrollTop = newScrollTop;
}
} catch (e) {
console.error(e);
}
}
getFirstElem(items) {
return (Array.isArray(items) ? items[0] : items);
}
async expandClick(key) {
@@ -326,17 +417,17 @@ class ContentsPage {
const expanded = !item.expanded;
if (!expanded) {
const subitems = this.$refs[`subitem${key}`];
subitems.style.height = '0';
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
subdiv.style.height = '0';
await utils.sleep(200);
}
this.contents[key] = Object.assign({}, item, {expanded});
this.contents[key].expanded = expanded;
if (expanded) {
await this.$nextTick();
const subitems = this.$refs[`subitem${key}`];
subitems.style.height = subitems.scrollHeight + 'px';
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
subdiv.style.height = subdiv.scrollHeight + 'px';
}
}

View File

@@ -5,13 +5,20 @@
</template>
<div class="col column" style="min-width: 600px">
<q-btn-toggle
v-model="selectedTab"
toggle-color="primary"
no-caps unelevated
:options="buttons"
/>
<div class="separator"></div>
<div class="bg-grey-3 row">
<q-tabs
v-model="selectedTab"
active-color="black"
active-bg-color="white"
indicator-color="white"
dense
no-caps
inline-label
class="bg-grey-4 text-grey-7"
>
<q-tab v-for="btn in buttons" :key="btn.value" :name="btn.value" :label="btn.label" />
</q-tabs>
</div>
<keep-alive>
<component :is="activePage" ref="page" class="col"></component>
@@ -93,8 +100,4 @@ export default vueComponent(HelpPage);
</script>
<style scoped>
.separator {
height: 1px;
background-color: #E0E0E0;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div v-show="visible" class="column justify-center items-center z-max" style="background-color: rgba(0, 0, 0, 0.8)">
<div v-show="visible" class="column justify-center items-center" style="background-color: rgba(0, 0, 0, 0.8); z-index: 100;">
<div class="column justify-start items-center" style="height: 250px">
<q-circular-progress
show-value

View File

@@ -141,6 +141,7 @@
@load-file="loadFile"
@book-pos-changed="bookPosChanged"
@do-action="doAction"
@hide-tool-bar="hideToolBar"
></component>
</keep-alive>
@@ -193,6 +194,7 @@ import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
import bookManager from './share/bookManager';
import wallpaperStorage from './share/wallpaperStorage';
import coversStorage from './share/coversStorage';
import dynamicCss from '../../share/dynamicCss';
import rstore from '../../store/modules/reader';
@@ -201,6 +203,7 @@ import miscApi from '../../api/misc';
import {versionHistory} from './versionHistory';
import * as utils from '../../share/utils';
import LockQueue from '../../share/LockQueue';
const componentOptions = {
components: {
@@ -313,6 +316,8 @@ class Reader {
this.reader = this.$store.state.reader;
this.config = this.$store.state.config;
this.lock = new LockQueue(100);
this.$root.addEventHook('key', this.keyHook);
this.lastActivePage = false;
@@ -345,6 +350,13 @@ class Reader {
this.debouncedSetRecentBook(newValue);
}, 15000, {maxWait: 20000});
this.debouncedHideToolBar = _.debounce((event) => {
if (this.toolBarHideOnScroll && this.toolBarActive !== !!event.show) {
this.commit('reader/setToolBarActive', !!event.show);
this.$root.eventHook('resize');
}
}, 200);
document.addEventListener('fullscreenchange', () => {
this.fullScreenActive = (document.fullscreenElement !== null);
});
@@ -355,6 +367,8 @@ class Reader {
mounted() {
(async() => {
await wallpaperStorage.init();
await coversStorage.init();
await bookManager.init(this.settings);
bookManager.addEventListener(this.bookManagerEvent);
@@ -402,6 +416,7 @@ class Reader {
this.clickControlActive = this.clickControl;
this.blinkCachedLoad = settings.blinkCachedLoad;
this.showToolButton = settings.showToolButton;
this.toolBarHideOnScroll = settings.toolBarHideOnScroll;
this.enableSitesFilter = settings.enableSitesFilter;
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
this.splitToPara = settings.splitToPara;
@@ -438,22 +453,47 @@ class Reader {
//wallpaper css
async loadWallpapers() {
const wallpaperDataLength = await wallpaperStorage.getLength();
if (wallpaperDataLength !== this.wallpaperDataLength) {//оптимизация
this.wallpaperDataLength = wallpaperDataLength;
if (!_.isEqual(this.userWallpapers, this.prevUserWallpapers)) {//оптимизация
this.prevUserWallpapers = _.cloneDeep(this.userWallpapers);
let newCss = '';
let updated = false;
const wallpaperExists = new Set();
for (const wp of this.userWallpapers) {
const data = await wallpaperStorage.getData(wp.cssClass);
wallpaperExists.add(wp.cssClass);
let data = await wallpaperStorage.getData(wp.cssClass);
if (!data) {
//здесь будем восстанавливать данные с сервера
const url = `disk://${wp.cssClass.replace('user-paper', '')}`;
try {
data = await readerApi.getUploadedFileBuf(url);
await wallpaperStorage.setData(wp.cssClass, data);
updated = true;
} catch (e) {
console.error(e);
}
}
if (data) {
newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`;
}
}
//почистим wallpaperStorage
for (const key of await wallpaperStorage.getKeys()) {
if (!wallpaperExists.has(key)) {
await wallpaperStorage.removeData(key);
}
}
//обновим settings, если загружали обои из /upload/
if (updated) {
const newSettings = _.cloneDeep(this.settings);
newSettings.needUpdateSettingsView = (newSettings.needUpdateSettingsView < 10 ? newSettings.needUpdateSettingsView + 1 : 0);
this.commit('reader/setSettings', newSettings);
}
dynamicCss.replace('wallpapers', newCss);
}
}
@@ -662,6 +702,10 @@ class Reader {
this.$root.eventHook('resize');
}
hideToolBar(event) {
this.debouncedHideToolBar(event);
}
fullScreenToggle() {
this.fullScreenActive = !this.fullScreenActive;
if (this.fullScreenActive) {
@@ -897,7 +941,7 @@ class Reader {
refreshBook() {
const mrb = this.mostRecentBook();
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
this.loadBook(Object.assign({}, mrb, {force: true}));
}
undoAction() {
@@ -982,7 +1026,6 @@ class Reader {
classResult = classDisabled;
break;
case 'refresh':
case 'recentBooks':
if (!this.mostRecentBookReactive)
classResult = classDisabled;
break;
@@ -1051,7 +1094,7 @@ class Reader {
return result;
}
async loadBook(opts) {
async _loadBook(opts) {
if (!opts || !opts.url) {
this.mostRecentBook();
return;
@@ -1061,10 +1104,6 @@ class Reader {
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) &&
(url.indexOf('disk://') != 0))
url = 'http://' + url;
@@ -1091,33 +1130,37 @@ class Reader {
progress.show();
progress.setState({state: 'parse'});
// есть ли среди недавних
const key = bookManager.keyFromUrl(url);
let wasOpened = await bookManager.getRecentBook({key});
wasOpened = (wasOpened ? wasOpened : {});
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
const uploadFileName = (opts.uploadFileName ? opts.uploadFileName : '');
// есть ли среди загруженных
let wasOpened = bookManager.findRecentByUrlAndPath(url, opts.path);
wasOpened = (wasOpened ? _.cloneDeep(wasOpened) : {});
wasOpened = Object.assign(wasOpened, {
url: (opts.url !== undefined ? opts.url : wasOpened.url),
path: (opts.path !== undefined ? opts.path : wasOpened.path),
bookPos: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos),
bookPosSeen: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen),
uploadFileName: (opts.uploadFileName ? opts.uploadFileName : wasOpened.uploadFileName),
});
let book = null;
if (!opts.force) {
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша
const bookParsed = await bookManager.getBook({url, path: opts.path}, (prog) => {
const bookParsed = await bookManager.getBook(wasOpened, (prog) => {
progress.setState({progress: prog});
});
// если есть в локальном кэше
if (bookParsed) {
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, bookParsed));
await bookManager.setRecentBook(Object.assign(wasOpened, bookParsed));
this.mostRecentBook();
this.addAction(bookPos);
this.addAction(wasOpened.bookPos);
this.loaderActive = false;
progress.hide(); this.progressActive = false;
this.blinkCachedLoadMessage();
this.checkBookPosPercent();
await this.activateClickMapPage();
this.activateClickMapPage();//no await
return;
}
@@ -1131,7 +1174,7 @@ class Reader {
});
book = Object.assign({}, wasOpened, {data: resp.data});
} catch (e) {
//молчим
this.$root.notify.error('Конвертированный файл не найден на сервере.<br>Пробуем загрузить оригинал.', 'Ошибка загрузки');
}
}
}
@@ -1142,7 +1185,7 @@ class Reader {
if (!book) {
book = await readerApi.loadBook({
url,
uploadFileName,
uploadFileName: wasOpened.uploadFileName,
enableSitesFilter: this.enableSitesFilter,
skipHtmlCheck: (this.splitToPara ? true : false),
isText: (this.splitToPara ? true : false),
@@ -1159,14 +1202,44 @@ class Reader {
// добавляем в bookManager
progress.setState({state: 'parse', step: 5});
const addedBook = await bookManager.addBook(book, (prog) => {
progress.setState({progress: prog});
});
// sameBookKey
if (url.indexOf('disk://') == 0) {
//ищем такой файл в загруженных
let found = bookManager.findRecentBySameBookKey(wasOpened.uploadFileName);
found = (found ? _.cloneDeep(found) : found);
if (found) {
if (wasOpened.sameBookKey != found.sameBookKey) {
//спрашиваем, надо ли объединить файлы
const askResult = bookManager.keysEqual(found.path, addedBook.path) ||
await this.$root.stdDialog.askYesNo(`
Файл с именем "${wasOpened.uploadFileName}" уже есть в загруженных.
<br>Объединить позицию?`, 'Найдена похожая книга');
if (askResult) {
wasOpened.bookPos = found.bookPos;
wasOpened.bookPosSeen = found.bookPosSeen;
wasOpened.sameBookKey = found.sameBookKey;
}
}
} else {
wasOpened.sameBookKey = wasOpened.uploadFileName;
}
} else {
wasOpened.sameBookKey = addedBook.url;
}
if (!bookManager.keysEqual(wasOpened.path, addedBook.path))
delete wasOpened.loadTime;
// добавляем в историю
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, uploadFileName}, addedBook));
await bookManager.setRecentBook(Object.assign(wasOpened, addedBook));
this.mostRecentBook();
this.addAction(bookPos);
this.addAction(wasOpened.bookPos);
this.updateRoute(true);
this.loaderActive = false;
@@ -1177,11 +1250,11 @@ class Reader {
this.stopBlink = true;
this.checkBookPosPercent();
await this.activateClickMapPage();
this.activateClickMapPage();//no await
} catch (e) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
if (!this.showHelpOnErrorIfNeeded(e.message)) {
if (!this.showHelpOnErrorIfNeeded(url)) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
} finally {
@@ -1189,7 +1262,16 @@ class Reader {
}
}
async loadFile(opts) {
async loadBook(opts) {
await this.lock.get();
try {
await this._loadBook(opts);
} finally {
this.lock.ret();
}
}
async _loadFile(opts) {
this.progressActive = true;
await this.$nextTick();
@@ -1205,7 +1287,7 @@ class Reader {
progress.hide(); this.progressActive = false;
await this.loadBook({url, uploadFileName: opts.file.name, force: true});
await this._loadBook({url, uploadFileName: opts.file.name, force: true});
} catch (e) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
@@ -1213,6 +1295,15 @@ class Reader {
}
}
async loadFile(opts) {
await this.lock.get();
try {
await this._loadFile(opts);
} finally {
this.lock.ret();
}
}
blinkCachedLoadMessage() {
if (!this.blinkCachedLoad)
return;

View File

@@ -54,7 +54,7 @@
<br><br>
<div class="row justify-center">
<!--q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">
<!--q-btn class="q-px-sm" color="primary" dense no-caps @click="openDonate">
Помочь проекту
</q-btn-->
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -8,12 +8,10 @@
<span v-show="initStep">{{ initPercentage }}%</span>
<div v-show="!initStep" class="input">
<!--input ref="input"
placeholder="что ищем"
:value="needle" @input="needle = $event.target.value"/-->
<q-input ref="input" v-model="needle"
<q-input
ref="input" v-model="needle"
class="col" outlined dense
placeholder="что ищем"
placeholder="Найти"
@keydown="inputKeyDown"
/>
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">
@@ -108,7 +106,7 @@ class SearchPage {
this.parsed = parsed;
}
this.header = 'Найти';
this.header = 'Поиск в тексте';
await this.$nextTick();
this.$refs.input.focus();
this.$refs.input.select();

View File

@@ -1,19 +1,21 @@
<template>
<Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close">
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
<template #header>
Установить позицию
</template>
<div id="set-position-slider" class="slider q-px-md">
<q-slider
v-model="sliderValue"
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
:max="sliderMax"
label
:label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
color="primary"
/>
<div class="col column justify-center">
<div id="set-position-slider" class="slider q-px-md column justify-center">
<q-slider
v-model="sliderValue"
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
:max="sliderMax"
label
:label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
color="primary"
/>
</div>
</div>
</Window>
</template>
@@ -76,7 +78,8 @@ export default vueComponent(SetPositionPage);
<style scoped>
.slider {
margin: 20px;
margin: 0 20px 0 20px;
height: 35px;
background-color: #efefef;
border-radius: 15px;
}

View File

@@ -1,9 +0,0 @@
<div class="part-header">Показывать кнопки панели</div>
<div class="item row" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
<div class="label-3"></div>
<div class="col row">
<q-checkbox size="xs" v-model="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
/>
</div>
</div>

View File

@@ -1,10 +1,12 @@
<template>
<Window ref="window" height="95%" width="600px" @close="close">
<Window ref="window" width="600px" @close="close">
<template #header>
Настройки
</template>
<div class="col row">
<a ref="download" style="display: none;" target="_blank"></a>
<div class="full-height">
<q-tabs
ref="tabs"
@@ -24,7 +26,7 @@
<div v-show="tabsScrollable" class="q-pt-lg" />
<q-tab class="tab" name="profiles" icon="la la-users" label="Профили" />
<q-tab class="tab" name="view" icon="la la-eye" label="Вид" />
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" />
<q-tab class="tab" name="toolbar" icon="la la-grip-horizontal" label="Панель" />
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
@@ -82,8 +84,8 @@
</div>
</div>
<!-- Кнопки ---------------------------------------------------------------------->
<div v-if="selectedTab == 'buttons'" class="fit tab-panel">
@@include('./ButtonsTab.inc');
<div v-if="selectedTab == 'toolbar'" class="fit tab-panel">
@@include('./ToolBarTab.inc');
</div>
<!-- Управление ------------------------------------------------------------------>
<div v-if="selectedTab == 'keys'" class="fit column">
@@ -124,6 +126,7 @@ import NumInput from '../../share/NumInput.vue';
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
import wallpaperStorage from '../share/wallpaperStorage';
import readerApi from '../../../api/reader';
import rstore from '../../../store/modules/reader';
import defPalette from './defPalette';
@@ -636,8 +639,17 @@ class SettingsPage {
if (index < 0)
newUserWallpapers.push({label, cssClass});
if (!wallpaperStorage.keyExists(cssClass))
if (!wallpaperStorage.keyExists(cssClass)) {
await wallpaperStorage.setData(cssClass, data);
//отправим data на сервер в файл `/upload/${key}`
try {
//const res =
await readerApi.uploadFileBuf(data);
//console.log(res);
} catch (e) {
console.error(e);
}
}
this.userWallpapers = newUserWallpapers;
this.wallpaper = cssClass;
@@ -664,6 +676,27 @@ class SettingsPage {
}
}
async downloadWallpaper() {
if (this.wallpaper.indexOf('user-paper') != 0)
return;
try {
const d = this.$refs.download;
const dataUrl = await wallpaperStorage.getData(this.wallpaper);
if (!dataUrl)
throw new Error('Файл обоев не найден');
d.href = dataUrl;
d.download = `wallpaper-#${this.wallpaper.replace('user-paper', '').substring(0, 4)}`;
d.click();
} catch (e) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
}
keyHook(event) {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
this.close();
@@ -702,11 +735,11 @@ export default vueComponent(SettingsPage);
margin-bottom: 5px;
}
.label-1, .label-7 {
.label-1, .label-3, .label-7 {
width: 75px;
}
.label-2, .label-3, .label-4, .label-5 {
.label-2, .label-4, .label-5 {
width: 110px;
}

View File

@@ -0,0 +1,18 @@
<div class="part-header">Отображение</div>
<div class="item row no-wrap">
<div class="label-3"></div>
<q-checkbox size="xs" v-model="toolBarHideOnScroll" label="Скрывать/показывать панель при прокрутке" >
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Скрывать/показывть панель при прокрутке текста вперед/назад
</q-tooltip>
</q-checkbox>
</div>
<div class="part-header">Показывать кнопки</div>
<div class="item row no-wrap" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
<div class="label-3"></div>
<q-checkbox size="xs" v-model="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
/>
</div>

View File

@@ -13,7 +13,7 @@
ref="input"
v-model="search"
class="q-ml-sm col"
outlined dense rounded
outlined dense
bg-color="grey-4"
placeholder="Найти"
@click.stop

View File

@@ -102,6 +102,11 @@
Удалить выбранные обои
</q-tooltip>
</q-btn>
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-file-download" @click.stop="downloadWallpaper">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Скачать выбранные обои
</q-tooltip>
</q-btn>
</div>
</div>

View File

@@ -66,7 +66,14 @@ const componentOptions = {
watch: {
bookPos: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
this.draw();
if (this.userBookPosChange) {
this.$emit('hide-tool-bar', {show: (this.bookPos == 0 || this.bookPos < this.prevBookPos)});
this.prevBookPos = this.bookPos;
this.userBookPosChange = false;
}
},
bookPosSeen: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
@@ -99,6 +106,8 @@ class TextPage {
lastBook = null;
bookPos = 0;
bookPosSeen = null;
prevBookPos = 0;
userBookPosChange = false;
fontStyle = null;
fontSize = null;
@@ -155,7 +164,7 @@ class TextPage {
this.$root.addEventHook('resize', async() => {
this.$nextTick(this.onResize);
await utils.sleep(500);
await utils.sleep(200);
this.$nextTick(this.onResize);
});
}
@@ -499,12 +508,25 @@ class TextPage {
}
async onResize() {
if (this.resizing)
return;
this.resizing = true;
try {
const scrolled = this.doingScrolling;
if (scrolled)
await this.stopTextScrolling();
this.calcDrawProps();
this.setBackground();
this.draw();
if (scrolled)
this.startTextScrolling();
} catch (e) {
//
} finally {
this.resizing = false;
}
}
@@ -652,7 +674,7 @@ class TextPage {
}
if (this.book && this.bookPos > 0 && this.bookPos >= this.parsed.textLength) {
this.doEnd(true);
this.doEnd(true, false);
return;
}
@@ -675,7 +697,7 @@ class TextPage {
this.debouncedDrawPageDividerAndOrnament();
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
this.doEnd(true);
this.doEnd(true, false);
return;
}
}
@@ -911,12 +933,14 @@ class TextPage {
doDown() {
if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
this.userBookPosChange = true;
this.bookPos = this.linesDown[1].begin;
}
}
doUp() {
if (this.linesUp && this.linesUp.length > 1 && this.pageLineCount > 0) {
this.userBookPosChange = true;
this.bookPos = this.linesUp[1].begin;
}
}
@@ -929,6 +953,7 @@ class TextPage {
if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true;
this.userBookPosChange = true;
this.bookPos = this.linesDown[i].begin;
} else
this.doEnd();
@@ -944,6 +969,7 @@ class TextPage {
if (i >= 0 && this.linesUp.length > i) {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = false;
this.userBookPosChange = true;
this.bookPos = this.linesUp[i].begin;
}
}
@@ -952,10 +978,11 @@ class TextPage {
doHome() {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = false;
this.userBookPosChange = true;
this.bookPos = 0;
}
doEnd(noAni) {
doEnd(noAni, isUser = true) {
if (this.parsed.para.length && this.pageLineCount > 0) {
let i = this.parsed.para.length - 1;
let lastPos = this.parsed.para[i].offset + this.parsed.para[i].length - 1;
@@ -966,6 +993,7 @@ class TextPage {
if (!noAni)
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true;
this.userBookPosChange = isUser;
this.bookPos = lines[i].begin;
}
}

View File

@@ -3,6 +3,8 @@ import sax from '../../../../server/core/sax';
import * as utils from '../../../share/utils';
const maxImageLineCount = 100;
const maxParaLength = 10000;
const maxParaTextLength = 10000;
// defaults
const defaultSettings = {
@@ -83,6 +85,7 @@ export default class BookParser {
let binaryId = '';
let binaryType = '';
let dimPromises = [];
this.coverPageId = '';
//оглавление
this.contents = [];
@@ -226,13 +229,26 @@ export default class BookParser {
paraOffset += len;
};
const growParagraph = (text, len) => {
const growParagraph = (text, len, textRaw) => {
//начальный параграф
if (paraIndex < 0) {
newParagraph();
growParagraph(text, len);
return;
}
//ограничение на размер куска текста в параграфе
if (textRaw && textRaw.length > maxParaTextLength) {
while (textRaw.length > 0) {
const textPart = textRaw.substring(0, maxParaTextLength);
textRaw = textRaw.substring(maxParaTextLength);
newParagraph();
growParagraph(textPart, textPart.length);
}
return;
}
if (inSubtitle) {
curSubtitle.title += text;
} else if (inTitle) {
@@ -240,6 +256,14 @@ export default class BookParser {
}
const p = para[paraIndex];
//ограничение на размер параграфа
if (p.length > maxParaLength) {
newParagraph();
growParagraph(text, len);
return;
}
p.length += len;
p.text += text;
paraOffset += len;
@@ -266,7 +290,7 @@ export default class BookParser {
const href = attrs.href.value;
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
const {id, local} = this.imageHrefToId(href);
if (href[0] == '#') {//local
if (local) {//local
imageNum++;
if (inPara && !this.sets.showInlineImagesInCenter && !center)
@@ -278,6 +302,11 @@ export default class BookParser {
if (inPara && this.sets.showInlineImagesInCenter)
newParagraph();
//coverpage
if (path == '/fictionbook/description/title-info/coverpage/image') {
this.coverPageId = id;
}
} else {//external
imageNum++;
@@ -536,7 +565,7 @@ export default class BookParser {
tClose += (center ? '</center>' : '');
if (text != ' ')
growParagraph(`${tOpen}${text}${tClose}`, text.length);
growParagraph(`${tOpen}${text}${tClose}`, text.length, text);
else
growParagraph(' ', 1);
}

View File

@@ -1,10 +1,14 @@
import localForage from 'localforage';
import path from 'path-browserify';
import _ from 'lodash';
import * as utils from '../../../share/utils';
import BookParser from './BookParser';
import readerApi from '../../../api/reader';
import coversStorage from './coversStorage';
import * as utils from '../../../share/utils';
const maxDataSize = 500*1024*1024;//compressed bytes
const maxRecentLength = 5000;
//локальный кэш метаданных книг, ограничение maxDataSize
const bmMetaStore = localForage.createInstance({
@@ -17,9 +21,6 @@ const bmDataStore = localForage.createInstance({
});
//список недавно открытых книг
const bmRecentStoreOld = localForage.createInstance({
name: 'bmRecentStore'
});
const bmRecentStoreNew = localForage.createInstance({
name: 'bmRecentStoreNew'
});
@@ -39,7 +40,7 @@ class BookManager {
this.saveRecentItem = _.debounce(() => {
bmRecentStoreNew.setItem('recent-item', this.recentItem);
this.recentRev = (this.recentRev < 1000 ? this.recentRev + 1 : 1);
this.recentRev = (this.recentRev < maxRecentLength ? this.recentRev + 1 : 1);
bmRecentStoreNew.setItem('rev', this.recentRev);
}, 200, {maxWait: 300});
@@ -54,6 +55,9 @@ class BookManager {
if (this.recentItem)
this.recent[this.recentItem.key] = this.recentItem;
//конвертируем в новые ключи
await this.convertRecent();
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
if (this.recentLastKey) {
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
@@ -70,6 +74,40 @@ class BookManager {
this.loadStored();//no await
}
//TODO: убрать в 2025г
async convertRecent() {
const converted = await bmRecentStoreNew.getItem('recent-converted');
if (converted)
return;
const newRecent = {};
for (const book of Object.values(this.recent)) {
if (!book.path) {
continue;
}
const newKey = this.keyFromPath(book.path);
newRecent[newKey] = _.cloneDeep(book);
newRecent[newKey].key = newKey;
if (!newRecent[newKey].loadTime)
newRecent[newKey].loadTime = newRecent[newKey].addTime;
}
this.recent = newRecent;
//console.log(converted);
(async() => {
await utils.sleep(3000);
this.saveRecent();
this.emit('recent-changed');
this.emit('set-recent');
await bmRecentStoreNew.setItem('recent-converted', true);
})();
}
//Ленивая асинхронная загрузка bmMetaStore
async loadStored() {
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
@@ -196,8 +234,8 @@ class BookManager {
async addBook(newBook, callback) {
let meta = {url: newBook.url, path: newBook.path};
meta.key = this.keyFromUrl(meta.url);
meta.addTime = Date.now();
meta.key = this.keyFromPath(meta.path);
meta.addTime = Date.now();//время добавления в кеш
const cb = (perc) => {
const p = Math.round(30*perc/100);
@@ -232,10 +270,10 @@ class BookManager {
async hasBookParsed(meta) {
if (!this.books)
return false;
if (!meta.url)
if (!meta.path)
return false;
if (!meta.key)
meta.key = this.keyFromUrl(meta.url);
meta.key = this.keyFromPath(meta.path);
let book = this.books[meta.key];
@@ -250,8 +288,12 @@ class BookManager {
async getBook(meta, callback) {
let result = undefined;
if (!meta.path)
return;
if (!meta.key)
meta.key = this.keyFromUrl(meta.url);
meta.key = this.keyFromPath(meta.path);
result = this.books[meta.key];
@@ -261,11 +303,6 @@ class BookManager {
this.books[meta.key] = result;
}
//Если файл на сервере изменился, считаем, что в кеше его нету
if (meta.path && result && meta.path != result.path) {
return;
}
if (result && !result.parsed) {
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
callback(5);
@@ -310,9 +347,38 @@ class BookManager {
const parsed = new BookParser(this.settings);
const parsedMeta = await parsed.parse(data, callback);
//cover page
let coverPageUrl = '';
if (parsed.coverPageId && parsed.binary[parsed.coverPageId]) {
const bin = parsed.binary[parsed.coverPageId];
let dataUrl = `data:${bin.type};base64,${bin.data}`;
try {
dataUrl = await utils.resizeImage(dataUrl, 160, 160, 0.94);
} catch (e) {
console.error(e);
}
coverPageUrl = readerApi.makeUrlFromBuf(dataUrl);
//далее асинхронно
(async() => {
//отправим dataUrl на сервер в /upload
try {
await readerApi.uploadFileBuf(dataUrl, coverPageUrl);
} catch (e) {
console.error(e);
}
//сохраним в storage
await coversStorage.setData(coverPageUrl, dataUrl);
})();
}
const result = Object.assign({}, meta, parsedMeta, {
length: data.length,
textLength: parsed.textLength,
coverPageUrl,
parsed
});
@@ -325,10 +391,20 @@ class BookManager {
return result;
}
keyFromUrl(url) {
/*keyFromUrl(url) {
return utils.stringToHex(url);
}*/
keyFromPath(bookPath) {
return path.basename(bookPath);
}
keysEqual(bookPath1, bookPath2) {
if (bookPath1 === undefined || bookPath2 === undefined)
return false;
return (this.keyFromPath(bookPath1) === this.keyFromPath(bookPath2));
}
//-- recent --------------------------------------------------------------
async recentSetItem(item = null, skipCheck = false) {
const rev = await bmRecentStoreNew.getItem('rev');
@@ -369,7 +445,10 @@ class BookManager {
async setRecentBook(value) {
let result = this.metaOnly(value);
result.touchTime = Date.now();
result.touchTime = Date.now();//время последнего чтения
if (!result.loadTime)
result.loadTime = Date.now();//время загрузки файла
result.deleted = 0;
if (this.recent[result.key]) {
@@ -385,9 +464,9 @@ class BookManager {
return this.recent[value.key];
}
async delRecentBook(value) {
async delRecentBook(value, delFlag = 1) {
const item = this.recent[value.key];
item.deleted = 1;
item.deleted = delFlag;
if (this.recentLastKey == value.key) {
await this.recentSetLastKey(null);
@@ -397,11 +476,18 @@ class BookManager {
this.emit('recent-deleted', value.key);
}
async restoreRecentBook(value) {
const item = this.recent[value.key];
item.deleted = 0;
await this.recentSetItem(item);
}
async cleanRecentBooks() {
const sorted = this.getSortedRecent();
let isDel = false;
for (let i = 1000; i < sorted.length; i++) {
for (let i = maxRecentLength; i < sorted.length; i++) {
delete this.recent[sorted[i].key];
isDel = true;
}
@@ -421,7 +507,7 @@ class BookManager {
let max = 0;
let result = null;
for (let key in this.recent) {
for (const key in this.recent) {
const book = this.recent[key];
if (!book.deleted && book.touchTime > max) {
max = book.touchTime;
@@ -452,6 +538,43 @@ class BookManager {
return result;
}
findRecentByUrlAndPath(url, bookPath) {
if (bookPath) {
const key = this.keyFromPath(bookPath);
const book = this.recent[key];
if (book && !book.deleted)
return book;
}
let max = 0;
let result = null;
for (const key in this.recent) {
const book = this.recent[key];
if (!book.deleted && book.url == url && book.loadTime > max) {
max = book.loadTime;
result = book;
}
}
return result;
}
findRecentBySameBookKey(sameKey) {
let max = 0;
let result = null;
for (const key in this.recent) {
const book = this.recent[key];
if (!book.deleted && book.sameBookKey == sameKey && book.loadTime > max) {
max = book.loadTime;
result = book;
}
}
return result;
}
async setRecent(value) {
const mergedRecent = _.cloneDeep(this.recent);

View File

@@ -0,0 +1,61 @@
import localForage from 'localforage';
//import _ from 'lodash';
import * as utils from '../../../share/utils';
const maxDataSize = 100*1024*1024;
const coversStore = localForage.createInstance({
name: 'coversStorage'
});
class CoversStorage {
constructor() {
}
async init() {
this.cleanCovers(); //no await
}
async setData(key, data) {
await coversStore.setItem(key, {addTime: Date.now(), data});
}
async getData(key) {
const item = await coversStore.getItem(key);
return (item ? item.data : undefined);
}
async removeData(key) {
await coversStore.removeItem(key);
}
async cleanCovers() {
await utils.sleep(10000);
while (1) {// eslint-disable-line no-constant-condition
let size = 0;
let min = Date.now();
let toDel = null;
for (const key of (await coversStore.keys())) {
const item = await coversStore.getItem(key);
size += item.data.length;
if (item.addTime < min) {
toDel = key;
min = item.addTime;
}
}
if (size > maxDataSize && toDel) {
await this.removeData(toDel);
} else {
break;
}
}
}
}
export default new CoversStorage();

View File

@@ -32,6 +32,10 @@ class WallpaperStorage {
this.cachedKeys = await wpStore.keys();
}
async getKeys() {
return await wpStore.keys();
}
keyExists(key) {//не асинхронная
return this.cachedKeys.includes(key);
}

View File

@@ -1,4 +1,40 @@
export const versionHistory = [
{
version: '0.11.8',
releaseDate: '2022-07-14',
showUntil: '2022-07-13',
content:
`
<ul>
<li>добавлено отображение и синхронизация обложек в окне загруженных книг</li>
<li>добавлена синхронизация обоев</li>
</ul>
`
},
{
version: '0.11.7',
releaseDate: '2022-07-12',
showUntil: '2022-07-19',
content:
`
<ul>
<li>добавлено автосокрытие панели управления при листании, отключается в настройках</li>
<li>изменения в окне загруженных книг:</li>
<ul>
<li>добавлена группировка по версиям файла одной и той же книги</li>
<li>группировка происходит по имени загружаемого файла, либо по URL книги</li>
<li>добавлены различные методы сортировки списка загруженных книг</li>
<li>нумерация всегда осуществляется по времени загрузки</li>
</ul>
<li>незначительные общие изменения интерфейса, приведение к единому стилю</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.11.6',
releaseDate: '2022-07-02',

View File

@@ -55,6 +55,34 @@
</div>
</div>
<!--------------------------------------------------->
<div v-show="type == 'askYesNo'" class="bg-white no-wrap">
<div class="header row">
<div class="caption col row items-center q-ml-md">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div>
</div>
<div class="close-icon column justify-center items-center">
<q-btn v-close-popup flat round dense>
<q-icon name="la la-times" size="18px"></q-icon>
</q-btn>
</div>
</div>
<div class="q-mx-md">
<div v-html="message"></div>
</div>
<div class="buttons row justify-end q-pa-md">
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
Нет
</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
Да
</q-btn>
</div>
</div>
<!--------------------------------------------------->
<div v-show="type == 'prompt'" class="bg-white no-wrap">
<div class="header row">
@@ -262,6 +290,23 @@ class StdDialog {
});
}
askYesNo(message, caption, opts) {
return new Promise((resolve) => {
this.init(message, caption, opts);
this.hideTrigger = () => {
if (this.ok) {
resolve(true);
} else {
resolve(false);
}
};
this.type = 'askYesNo';
this.active = true;
});
}
prompt(message, caption, opts) {
return new Promise((resolve) => {
this.enableValidator = false;

View File

@@ -153,7 +153,7 @@ export default vueComponent(Window);
}
.header {
background: linear-gradient(to bottom right, green, #59B04F);
background: linear-gradient(to bottom right, #007000, #59B04F);
align-items: center;
height: 30px;
}
@@ -161,8 +161,8 @@ export default vueComponent(Window);
.header-text {
margin-left: 10px;
margin-right: 10px;
color: yellow;
text-shadow: 2px 1px 5px black, 2px 2px 5px black;
color: #FFFFA0;
text-shadow: 2px 2px 5px #005000, 2px 1px 5px #005000;
overflow: hidden;
white-space: nowrap;
}
@@ -174,7 +174,8 @@ export default vueComponent(Window);
}
.close-button:hover {
background-color: #69C05F;
color: white;
background-color: #FF3030;
}
</style>
</style>

View File

@@ -32,6 +32,8 @@ import {QPopupProxy} from 'quasar/src/components/popup-proxy';
import {QDialog} from 'quasar/src/components/dialog';
import {QChip} from 'quasar/src/components/chip';
import {QTree} from 'quasar/src/components/tree';
import {QVirtualScroll} from 'quasar/src/components/virtual-scroll';
//import {QExpansionItem} from 'quasar/src/components/expansion-item';
const components = {
@@ -62,6 +64,7 @@ const components = {
QChip,
QTree,
//QExpansionItem,
QVirtualScroll,
};
//directives

53
client/share/LockQueue.js Normal file
View File

@@ -0,0 +1,53 @@
class LockQueue {
constructor(queueSize) {
this.queueSize = queueSize;
this.freed = true;
this.waitingQueue = [];
}
//async
get(take = true) {
return new Promise((resolve, reject) => {
if (this.freed) {
if (take)
this.freed = false;
resolve();
return;
}
if (this.waitingQueue.length < this.queueSize) {
this.waitingQueue.push({resolve, reject});
} else {
reject(new Error('Lock queue is too long'));
}
});
}
ret() {
if (this.waitingQueue.length) {
this.waitingQueue.shift().resolve();
} else {
this.freed = true;
}
}
//async
wait() {
return this.get(false);
}
retAll() {
while (this.waitingQueue.length) {
this.waitingQueue.shift().resolve();
}
}
errAll(error = 'rejected') {
while (this.waitingQueue.length) {
this.waitingQueue.shift().reject(new Error(error));
}
}
}
export default LockQueue;

View File

@@ -363,4 +363,50 @@ export function getBookTitle(fb2) {
]).join(' - ');
return result;
}
export function resizeImage(dataUrl, toWidth, toHeight, quality = 0.9) {
return new Promise ((resolve, reject) => { (async() => {
const img = new Image();
let resolved = false;
img.onload = () => {
try {
let width = img.width;
let height = img.height;
if (width > height) {
if (width > toWidth) {
height = height * (toWidth / width);
width = toWidth;
}
} else {
if (height > toHeight) {
width = width * (toHeight / height);
height = toHeight;
}
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
const result = canvas.toDataURL('image/jpeg', quality);
resolved = true;
resolve(result);
} catch (e) {
reject(e);
return;
}
};
img.onerror = reject;
img.src = dataUrl;
await sleep(1000);
if (!resolved)
reject('Не удалось изменить размер');
})().catch(reject); });
}

View File

@@ -21,7 +21,7 @@ const readerActions = {
'offlineMode': 'Автономный режим (без интернета)',
'contents': 'Оглавление/закладки',
'libs': 'Сетевая библиотека',
'recentBooks': 'Открыть недавние',
'recentBooks': 'Показать загруженные',
'switchToolbar': 'Показать/скрыть панель управления',
'donate': '',
'bookBegin': 'В начало книги',
@@ -185,8 +185,14 @@ const settingDefaults = {
fontShifts: {},
showToolButton: {},
toolBarHideOnScroll: true,
userHotKeys: {},
userWallpapers: [],
recentShowSameBook: false,
recentSortMethod: '',
needUpdateSettingsView: 0,
};
for (const font of fonts)
@@ -222,9 +228,6 @@ const libsDefaults = {
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
]},
{r: 'https://flibs.in', s: 'https://flibs.in', list: [
{l: 'https://flibs.in', c: 'Flibs'},
]},
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
]},

View File

@@ -6,6 +6,7 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name beta.liberama.top;
set $liberama http://127.0.0.1:34082;
client_max_body_size 50m;
proxy_read_timeout 1h;
@@ -15,15 +16,20 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:34082;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:34082;
proxy_pass $liberama;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
}
location / {
@@ -32,6 +38,11 @@ server {
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
}
location ~* \.(?:manifest|appcache|html)$ {
@@ -50,6 +61,7 @@ server {
server {
listen 80;
server_name b.beta.liberama.top;
set $liberama http://127.0.0.1:34082;
client_max_body_size 50m;
proxy_read_timeout 1h;
@@ -59,15 +71,20 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:34082;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:34082;
proxy_pass $liberama;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
}
location / {
@@ -76,6 +93,11 @@ server {
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
}
location ~* \.(?:manifest|appcache|html)$ {

View File

@@ -6,6 +6,7 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name beta.omnireader.ru;
set $liberama http://127.0.0.1:34081;
client_max_body_size 50m;
proxy_read_timeout 1h;
@@ -15,15 +16,20 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:34081;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:34081;
proxy_pass $liberama;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
}
location / {
@@ -32,6 +38,11 @@ server {
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
}
location ~* \.(?:manifest|appcache|html)$ {

View File

@@ -1,6 +1,7 @@
server {
listen 80;
server_name beta.omnireader.ru;
set $liberama http://127.0.0.1:34081;
client_max_body_size 50m;
proxy_read_timeout 1h;
@@ -10,15 +11,20 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:34081;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:34081;
proxy_pass $liberama;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
}
location / {
@@ -27,6 +33,11 @@ server {
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
}
location ~* \.(?:manifest|appcache|html)$ {

View File

@@ -17,8 +17,9 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name liberama.top;
set $liberama http://127.0.0.1:55081;
client_max_body_size 50m;
client_max_body_size 100m;
proxy_read_timeout 1h;
gzip on;
@@ -26,12 +27,16 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:55081;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:55081;
proxy_pass $liberama;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
@@ -44,6 +49,11 @@ server {
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
}
location ~* \.(?:manifest|appcache|html)$ {
@@ -62,8 +72,9 @@ server {
server {
listen 80;
server_name b.liberama.top;
set $liberama http://127.0.0.1:55081;
client_max_body_size 50m;
client_max_body_size 100m;
proxy_read_timeout 1h;
gzip on;
@@ -71,15 +82,20 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:55081;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:55081;
proxy_pass $liberama;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
}
location / {
@@ -88,6 +104,11 @@ server {
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
}
location ~* \.(?:manifest|appcache|html)$ {

View File

@@ -1,7 +1,7 @@
#!/bin/bash
if ! pgrep -x "liberama" > /dev/null ; then
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama"
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama >/dev/null"
else
echo "Process 'liberama' already running"
fi

View File

@@ -6,8 +6,9 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name omnireader.ru;
set $liberama http://127.0.0.1:44081;
client_max_body_size 50m;
client_max_body_size 100m;
proxy_read_timeout 1h;
gzip on;
@@ -15,12 +16,16 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:44081;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:44081;
proxy_pass $liberama;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
@@ -33,6 +38,11 @@ server {
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
}
location ~* \.(?:manifest|appcache|html)$ {
@@ -52,7 +62,7 @@ server {
listen 80;
server_name old.omnireader.ru;
client_max_body_size 50m;
client_max_body_size 100m;
gzip on;
gzip_min_length 1024;

View File

@@ -1,6 +1,7 @@
server {
listen 80;
server_name omnireader.ru;
set $liberama http://127.0.0.1:44081;
client_max_body_size 50m;
proxy_read_timeout 1h;
@@ -10,12 +11,16 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:44081;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:44081;
proxy_pass $liberama;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
@@ -27,6 +32,11 @@ server {
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
}
location ~* \.(?:manifest|appcache|html)$ {

View File

@@ -1,4 +1,4 @@
#!/bin/bash
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama" & disown
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama >/dev/null & disown"
sudo service cron start

172
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "Liberama",
"version": "0.11.6",
"version": "0.11.8",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "Liberama",
"version": "0.11.6",
"version": "0.11.8",
"hasInstallScript": true,
"license": "CC0-1.0",
"dependencies": {
@@ -25,11 +25,11 @@
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"minimist": "^1.2.5",
"multer": "^1.4.3",
"multer": "^1.4.5-lts.1",
"pako": "^2.0.4",
"path-browserify": "^1.0.1",
"pidusage": "^3.0.0",
"quasar": "^2.3.2",
"quasar": "^2.7.5",
"safe-buffer": "^5.2.1",
"sanitize-html": "^2.5.3",
"sjcl": "^1.0.8",
@@ -38,8 +38,8 @@
"sqlite3": "^5.0.2",
"tar-fs": "^2.1.1",
"unbzip2-stream": "^1.4.3",
"vue": "^3.2.22",
"vue-router": "^4.0.12",
"vue": "^3.2.37",
"vue-router": "^4.1.1",
"vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0",
"webdav": "^4.7.0",
@@ -61,7 +61,7 @@
"css-loader": "^6.5.1",
"css-minimizer-webpack-plugin": "^4.0.0",
"eslint": "^8.19.0",
"eslint-plugin-vue": "^9.1.1",
"eslint-plugin-vue": "^9.2.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.4",
"pkg": "^5.5.1",
@@ -3239,15 +3239,14 @@
}
},
"node_modules/busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"dicer": "0.2.5",
"readable-stream": "1.1.x"
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=0.8.0"
"node": ">=10.16.0"
}
},
"node_modules/byte-length": {
@@ -4348,18 +4347,6 @@
"node": ">=0.10"
}
},
"node_modules/dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
"dependencies": {
"readable-stream": "1.1.x",
"streamsearch": "0.1.2"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -4785,9 +4772,9 @@
}
},
"node_modules/eslint-plugin-vue": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.1.1.tgz",
"integrity": "sha512-W9n5PB1X2jzC7CK6riG0oAcxjmKrjTF6+keL1rni8n57DZeilx/Fulz+IRJK3lYseLNAygN0I62L7DvioW40Tw==",
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.2.0.tgz",
"integrity": "sha512-W2hc+NUXoce8sZtWgZ45miQTy6jNyuSdub5aZ1IBune4JDeAyzucYX0TzkrQ1jMO52sNUDYlCIHDoaNePe0p5g==",
"dev": true,
"dependencies": {
"eslint-utils": "^3.0.0",
@@ -6429,11 +6416,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -7242,22 +7224,20 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/multer": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
"deprecated": "Multer 1.x is affected by CVE-2022-24434. This is fixed in v1.4.4-lts.1 which drops support for versions of Node.js before 6. Please upgrade to at least Node.js 6 and version 1.4.4-lts.1 of Multer. If you need support for older versions of Node.js, we are open to accepting patches that would fix the CVE on the main 1.x release line, whilst maintaining compatibility with Node.js 0.10.",
"version": "1.4.5-lts.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^0.2.11",
"busboy": "^1.0.0",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"object-assign": "^4.1.1",
"on-finished": "^2.3.0",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
},
"engines": {
"node": ">= 0.10.0"
"node": ">= 6.0.0"
}
},
"node_modules/multistream": {
@@ -8802,9 +8782,9 @@
}
},
"node_modules/quasar": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-2.7.4.tgz",
"integrity": "sha512-8OIa6azm7N6QUPjcZ5AhDCEBha5NnNqt+D1BMIteqaSqkVKFYBf+FMhUCC8R/Tc6Myz85vK7KGPn9tvaC6gXYQ==",
"version": "2.7.5",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-2.7.5.tgz",
"integrity": "sha512-DWI0S+bXASfMSPrB8c/LVsXpA4dF7cBUbaJlcrM+1ioTNBHtiudma2Nhk2SDd5bzk9AYVHh5A8JCZuKqQAXt7g==",
"engines": {
"node": ">= 10.18.1",
"npm": ">= 6.13.4",
@@ -8924,17 +8904,6 @@
"node": ">=0.10.0"
}
},
"node_modules/readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/rechoir": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz",
@@ -9735,18 +9704,13 @@
}
},
"node_modules/streamsearch": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
"integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=0.8.0"
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
},
"node_modules/string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@@ -10611,11 +10575,11 @@
}
},
"node_modules/vue-router": {
"version": "4.0.16",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.16.tgz",
"integrity": "sha512-JcO7cb8QJLBWE+DfxGUL3xUDOae/8nhM1KVdnudadTAORbuxIC/xAydC5Zr/VLHUDQi1ppuTF5/rjBGzgzrJNA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.1.tgz",
"integrity": "sha512-Wp1mEf2xCwT0ez7o9JvgpfBp9JGnVb+dPERzXDbugTatzJAJ60VWOhJKifQty85k+jOreoFHER4r5fu062PhPw==",
"dependencies": {
"@vue/devtools-api": "^6.0.0"
"@vue/devtools-api": "^6.1.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
@@ -13960,12 +13924,11 @@
"dev": true
},
"busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"requires": {
"dicer": "0.2.5",
"readable-stream": "1.1.x"
"streamsearch": "^1.1.0"
}
},
"byte-length": {
@@ -14792,15 +14755,6 @@
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true
},
"dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
"requires": {
"readable-stream": "1.1.x",
"streamsearch": "0.1.2"
}
},
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -15220,9 +15174,9 @@
}
},
"eslint-plugin-vue": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.1.1.tgz",
"integrity": "sha512-W9n5PB1X2jzC7CK6riG0oAcxjmKrjTF6+keL1rni8n57DZeilx/Fulz+IRJK3lYseLNAygN0I62L7DvioW40Tw==",
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.2.0.tgz",
"integrity": "sha512-W2hc+NUXoce8sZtWgZ45miQTy6jNyuSdub5aZ1IBune4JDeAyzucYX0TzkrQ1jMO52sNUDYlCIHDoaNePe0p5g==",
"dev": true,
"requires": {
"eslint-utils": "^3.0.0",
@@ -16340,11 +16294,6 @@
"call-bind": "^1.0.2"
}
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -16980,16 +16929,15 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"multer": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
"version": "1.4.5-lts.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
"requires": {
"append-field": "^1.0.0",
"busboy": "^0.2.11",
"busboy": "^1.0.0",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"object-assign": "^4.1.1",
"on-finished": "^2.3.0",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
}
@@ -18077,9 +18025,9 @@
}
},
"quasar": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-2.7.4.tgz",
"integrity": "sha512-8OIa6azm7N6QUPjcZ5AhDCEBha5NnNqt+D1BMIteqaSqkVKFYBf+FMhUCC8R/Tc6Myz85vK7KGPn9tvaC6gXYQ=="
"version": "2.7.5",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-2.7.5.tgz",
"integrity": "sha512-DWI0S+bXASfMSPrB8c/LVsXpA4dF7cBUbaJlcrM+1ioTNBHtiudma2Nhk2SDd5bzk9AYVHh5A8JCZuKqQAXt7g=="
},
"querystring": {
"version": "0.2.1",
@@ -18158,17 +18106,6 @@
}
}
},
"readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"rechoir": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz",
@@ -18773,14 +18710,9 @@
}
},
"streamsearch": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
"integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA=="
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="
},
"string-width": {
"version": "1.0.2",
@@ -19424,11 +19356,11 @@
}
},
"vue-router": {
"version": "4.0.16",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.16.tgz",
"integrity": "sha512-JcO7cb8QJLBWE+DfxGUL3xUDOae/8nhM1KVdnudadTAORbuxIC/xAydC5Zr/VLHUDQi1ppuTF5/rjBGzgzrJNA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.1.tgz",
"integrity": "sha512-Wp1mEf2xCwT0ez7o9JvgpfBp9JGnVb+dPERzXDbugTatzJAJ60VWOhJKifQty85k+jOreoFHER4r5fu062PhPw==",
"requires": {
"@vue/devtools-api": "^6.0.0"
"@vue/devtools-api": "^6.1.4"
}
},
"vue-style-loader": {

View File

@@ -1,6 +1,6 @@
{
"name": "Liberama",
"version": "0.11.6",
"version": "0.11.8",
"author": "Book Pauk <bookpauk@gmail.com>",
"license": "CC0-1.0",
"repository": "bookpauk/liberama",
@@ -32,7 +32,7 @@
"css-loader": "^6.5.1",
"css-minimizer-webpack-plugin": "^4.0.0",
"eslint": "^8.19.0",
"eslint-plugin-vue": "^9.1.1",
"eslint-plugin-vue": "^9.2.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.4",
"pkg": "^5.5.1",
@@ -63,11 +63,11 @@
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"minimist": "^1.2.5",
"multer": "^1.4.3",
"multer": "^1.4.5-lts.1",
"pako": "^2.0.4",
"path-browserify": "^1.0.1",
"pidusage": "^3.0.0",
"quasar": "^2.3.2",
"quasar": "^2.7.5",
"safe-buffer": "^5.2.1",
"sanitize-html": "^2.5.3",
"sjcl": "^1.0.8",
@@ -76,8 +76,8 @@
"sqlite3": "^5.0.2",
"tar-fs": "^2.1.1",
"unbzip2-stream": "^1.4.3",
"vue": "^3.2.22",
"vue-router": "^4.0.12",
"vue": "^3.2.37",
"vue-router": "^4.1.1",
"vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0",
"webdav": "^4.7.0",

View File

@@ -39,6 +39,11 @@ module.exports = {
],
jembaDb: [
{
dbName: 'app',
thread: true,
openAll: true,
},
{
dbName: 'reader-storage',
thread: true,
@@ -49,14 +54,14 @@ module.exports = {
servers: [
{
serverName: '1',
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top'
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top', 'book_update_checker'
ip: '0.0.0.0',
port: '33080',
},
],
remoteWebDavStorage: false,
/*
remoteWebDavStorage: false,
remoteWebDavStorage: {
url: '127.0.0.1:1900',
username: '',
@@ -64,5 +69,12 @@ module.exports = {
},
*/
remoteStorage: false,
/*
remoteStorage: {
url: 'https://127.0.0.1:11900',
accessToken: '',
},
*/
};

View File

@@ -0,0 +1,95 @@
const WebSocket = require ('ws');
//const _ = require('lodash');
const log = new (require('../core/AppLogger'))().log;//singleton
//const utils = require('../core/utils');
const cleanPeriod = 1*60*1000;//1 минута
const closeSocketOnIdle = 5*60*1000;//5 минут
class BookUpdateCheckerController {
constructor(wss, config) {
this.config = config;
this.isDevelopment = (config.branch == 'development');
//this.readerStorage = new JembaReaderStorage();
this.wss = wss;
wss.on('connection', (ws) => {
ws.on('message', (message) => {
this.onMessage(ws, message.toString());
});
ws.on('error', (err) => {
log(LM_ERR, err);
});
});
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
}
periodicClean() {
try {
const now = Date.now();
this.wss.clients.forEach((ws) => {
if (!ws.lastActivity || now - ws.lastActivity > closeSocketOnIdle - 50) {
ws.terminate();
}
});
} finally {
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
}
}
async onMessage(ws, message) {
let req = {};
try {
if (this.isDevelopment) {
log(`WebSocket-IN: ${message.substr(0, 4000)}`);
}
req = JSON.parse(message);
ws.lastActivity = Date.now();
//pong for WebSocketConnection
this.send({_rok: 1}, req, ws);
switch (req.action) {
case 'test':
await this.test(req, ws); break;
default:
throw new Error(`Action not found: ${req.action}`);
}
} catch (e) {
this.send({error: e.message}, req, ws);
}
}
send(res, req, ws) {
if (ws.readyState == WebSocket.OPEN) {
ws.lastActivity = Date.now();
let r = res;
if (req.requestId)
r = Object.assign({requestId: req.requestId}, r);
const message = JSON.stringify(r);
ws.send(message);
if (this.isDevelopment) {
log(`WebSocket-OUT: ${message.substr(0, 4000)}`);
}
}
}
//Actions ------------------------------------------------------------------
async test(req, ws) {
this.send({message: 'Liberama project is awesome'}, req, ws);
}
}
module.exports = BookUpdateCheckerController;

View File

@@ -68,24 +68,6 @@ class ReaderController extends BaseController {
res.status(400).send({error});
return false;
}
async restoreCachedFile(req, res) {
const request = req.body;
let error = '';
try {
if (!request.path)
throw new Error(`key 'path' is empty`);
const workerId = this.readerWorker.restoreCachedFile(request.path);
const state = this.workerState.getState(workerId);
return (state ? state : {});
} catch (e) {
error = e.message;
}
//bad request
res.status(400).send({error});
return false;
}
}
module.exports = ReaderController;

View File

@@ -25,6 +25,10 @@ class WebSocketController {
ws.on('message', (message) => {
this.onMessage(ws, message.toString());
});
ws.on('error', (err) => {
log(LM_ERR, err);
});
});
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
@@ -66,10 +70,12 @@ class WebSocketController {
await this.workerGetState(req, ws); break;
case 'worker-get-state-finish':
await this.workerGetStateFinish(req, ws); break;
case 'reader-restore-cached-file':
await this.readerRestoreCachedFile(req, ws); break;
case 'reader-storage':
await this.readerStorageDo(req, ws); break;
case 'upload-file-buf':
await this.uploadFileBuf(req, ws); break;
case 'upload-file-touch':
await this.uploadFileTouch(req, ws); break;
default:
throw new Error(`Action not found: ${req.action}`);
@@ -149,15 +155,6 @@ class WebSocketController {
}
}
async readerRestoreCachedFile(req, ws) {
if (!req.path)
throw new Error(`key 'path' is empty`);
const workerId = this.readerWorker.restoreCachedFile(req.path);
const state = this.workerState.getState(workerId);
this.send((state ? state : {}), req, ws);
}
async readerStorageDo(req, ws) {
if (!req.body)
throw new Error(`key 'body' is empty`);
@@ -168,6 +165,20 @@ class WebSocketController {
this.send(await this.readerStorage.doAction(req.body), req, ws);
}
async uploadFileBuf(req, ws) {
if (!req.buf)
throw new Error(`key 'buf' is empty`);
this.send({url: await this.readerWorker.saveFileBuf(req.buf)}, req, ws);
}
async uploadFileTouch(req, ws) {
if (!req.url)
throw new Error(`key 'url' is empty`);
this.send({url: await this.readerWorker.uploadFileTouch(req.url)}, req, ws);
}
}
module.exports = WebSocketController;

View File

@@ -3,4 +3,5 @@ module.exports = {
ReaderController: require('./ReaderController'),
WorkerController: require('./WorkerController'),
WebSocketController: require('./WebSocketController'),
BookUpdateCheckerController: require('./BookUpdateCheckerController'),
}

View File

@@ -0,0 +1,24 @@
let instance = null;
//singleton
class BUCServer {
constructor(config) {
if (!instance) {
this.config = Object.assign({}, config);
this.config.tempDownloadDir = `${config.tempDir}/download`;
fs.ensureDirSync(this.config.tempDownloadDir);
this.down = new FileDownloader(config.maxUploadFileSize);
instance = this;
}
return instance;
}
async main() {
}
}
module.exports = BUCServer;

View File

@@ -23,7 +23,7 @@ class FileDownloader {
estSize = res.headers['content-length'];
}
if (estSize > this.limitDownloadSize) {
if (this.limitDownloadSize && estSize > this.limitDownloadSize) {
throw new Error('Файл слишком большой');
}

View File

@@ -49,7 +49,7 @@ class BaseLog {
this.outputBuffer = [];
await this.flushImpl(this.data)
.catch(e => { console.log(e); ayncExit.exit(1); } );
.catch(e => { console.error(`Logger error: ${e}`); ayncExit.exit(1); } );
this.flushing = false;
}
@@ -218,6 +218,8 @@ class Logger {
} else {
console.log(mes);
}
return mes;
}
async close() {

View File

@@ -3,7 +3,7 @@ const chardet = require('chardet');
function getEncoding(buf) {
let selected = getEncodingLite(buf);
if (selected == 'ISO-8859-5') {
if (selected == 'ISO-8859-5' && buf.length > 10) {
const charsetAll = chardet.analyse(buf.slice(0, 20000));
for (const charset of charsetAll) {
if (charset.name.indexOf('ISO-8859') < 0) {

View File

@@ -2,6 +2,7 @@ const _ = require('lodash');
const utils = require('../utils');
const JembaConnManager = require('../../db/JembaConnManager');//singleton
const log = new (require('../AppLogger'))().log;//singleton
let instance = null;
@@ -20,25 +21,30 @@ class JembaReaderStorage {
}
async doAction(act) {
if (!_.isObject(act.items))
throw new Error('items is not an object');
try {
if (!_.isObject(act.items))
throw new Error('items is not an object');
let result = {};
switch (act.action) {
case 'check':
result = await this.checkItems(act.items);
break;
case 'get':
result = await this.getItems(act.items);
break;
case 'set':
result = await this.setItems(act.items, act.force);
break;
default:
throw new Error('Unknown action');
let result = {};
switch (act.action) {
case 'check':
result = await this.checkItems(act.items);
break;
case 'get':
result = await this.getItems(act.items);
break;
case 'set':
result = await this.setItems(act.items, act.force);
break;
default:
throw new Error('Unknown action');
}
return result;
} catch (e) {
log(LM_ERR, `JembaReaderStorage: ${e.message}`);
throw e;
}
return result;
}
async checkItems(items) {

View File

@@ -6,12 +6,16 @@ const WorkerState = require('../WorkerState');//singleton
const FileDownloader = require('../FileDownloader');
const FileDecompressor = require('../FileDecompressor');
const BookConverter = require('./BookConverter');
const RemoteWebDavStorage = require('../RemoteWebDavStorage');
const RemoteStorage = require('../RemoteStorage');
const JembaConnManager = require('../../db/JembaConnManager');//singleton
const ayncExit = new (require('../AsyncExit'))();
const utils = require('../utils');
const log = new (require('../AppLogger'))().log;//singleton
const cleanDirPeriod = 60*60*1000;//1 раз в час
const cleanDirPeriod = 60*60*1000;//каждый час
const remoteSendPeriod = 119*1000;//примерно раз 2 минуты
const queue = new LimitedQueue(5, 100, 2*60*1000 + 15000);//2 минуты ожидание подвижек
let instance = null;
@@ -33,15 +37,37 @@ class ReaderWorker {
this.decomp = new FileDecompressor(3*config.maxUploadFileSize);
this.bookConverter = new BookConverter(this.config);
this.remoteWebDavStorage = false;
if (config.remoteWebDavStorage) {
this.remoteWebDavStorage = new RemoteWebDavStorage(
Object.assign({maxContentLength: 3*config.maxUploadFileSize}, config.remoteWebDavStorage)
this.connManager = new JembaConnManager();
this.appDb = this.connManager.db['app'];
this.remoteStorage = false;
if (config.remoteStorage) {
this.remoteStorage = new RemoteStorage(
Object.assign({maxContentLength: 3*config.maxUploadFileSize}, config.remoteStorage)
);
}
this.periodicCleanDir(this.config.tempPublicDir, this.config.maxTempPublicDirSize, cleanDirPeriod);
this.periodicCleanDir(this.config.uploadDir, this.config.maxUploadPublicDirSize, cleanDirPeriod);
this.dirConfigArr = [
{
dir: this.config.tempPublicDir,
remoteDir: '/tmp',
maxSize: this.config.maxTempPublicDirSize,
moveToRemote: true,
},
{
dir: this.config.uploadDir,
remoteDir: '/upload',
maxSize: this.config.maxUploadPublicDirSize,
moveToRemote: true,
}
];
//преобразуем в объект для большего удобства
this.dirConfig = {};
for (const configRec of this.dirConfigArr)
this.dirConfig[configRec.remoteDir] = configRec;
this.remoteFilesToSend = [];
this.periodicCleanDir();//no await
instance = this;
}
@@ -54,7 +80,6 @@ class ReaderWorker {
let decompDir = '';
let downloadedFilename = '';
let isUploaded = false;
let isRestored = false;
let convertFilename = '';
const overLoadMes = 'Слишком большая очередь загрузки. Пожалуйста, попробуйте позже.';
@@ -94,8 +119,7 @@ class ReaderWorker {
if (!await fs.pathExists(downloadedFilename)) {
//если удалено из upload, попробуем восстановить из удаленного хранилища
try {
downloadedFilename = await this.restoreRemoteFile(fileHash);
isRestored = true;
await this.restoreRemoteFile(fileHash, '/upload');
} catch(e) {
throw new Error('Файл не найден на сервере (возможно был удален как устаревший). Пожалуйста, загрузите файл с диска на сервер заново.');
}
@@ -144,32 +168,12 @@ class ReaderWorker {
const finishFilename = path.basename(compFilename);
wState.finish({path: `/tmp/${finishFilename}`, size: stat.size});
//лениво сохраним compFilename в удаленном хранилище
if (this.remoteWebDavStorage) {
(async() => {
await utils.sleep(20*1000);
try {
//log(`remoteWebDavStorage.putFile ${path.basename(compFilename)}`);
await this.remoteWebDavStorage.putFile(compFilename);
} catch (e) {
log(LM_ERR, e.stack);
}
})();
}
//лениво сохраним downloadedFilename в tmp и в удаленном хранилище в случае isUploaded
if (this.remoteWebDavStorage && isUploaded && !isRestored) {
(async() => {
await utils.sleep(30*1000);
try {
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
const compDownloadedFilename = await this.decomp.gzipFileIfNotExists(downloadedFilename, this.config.tempPublicDir, true);
await this.remoteWebDavStorage.putFile(compDownloadedFilename);
} catch (e) {
log(LM_ERR, e.stack);
}
})();
}
//асинхронно через 30 сек добавим в очередь на отправку
//т.к. gzipFileIfNotExists может переупаковать файл
(async() => {
await utils.sleep(30*1000);
this.pushRemoteSend(compFilename, '/tmp');
})();
} catch (e) {
log(LM_ERR, e.stack);
@@ -211,6 +215,7 @@ class ReaderWorker {
if (!await fs.pathExists(outFilename)) {
await fs.move(file.path, outFilename);
this.pushRemoteSend(outFilename, '/upload');
} else {
await utils.touchFile(outFilename);
await fs.remove(file.path);
@@ -219,14 +224,42 @@ class ReaderWorker {
return `disk://${hash}`;
}
async restoreRemoteFile(filename) {
async saveFileBuf(buf) {
const hash = await utils.getBufHash(buf, 'sha256', 'hex');
const outFilename = `${this.config.uploadDir}/${hash}`;
if (!await fs.pathExists(outFilename)) {
await fs.writeFile(outFilename, buf);
this.pushRemoteSend(outFilename, '/upload');
} else {
await utils.touchFile(outFilename);
}
return `disk://${hash}`;
}
async uploadFileTouch(url) {
const outFilename = `${this.config.uploadDir}/${url.replace('disk://', '')}`;
await utils.touchFile(outFilename);
return url;
}
async restoreRemoteFile(filename, remoteDir) {
let targetDir = '';
if (this.dirConfig[remoteDir])
targetDir = this.dirConfig[remoteDir].dir;
else
throw new Error(`restoreRemoteFile: unknown remoteDir value (${remoteDir})`);
const basename = path.basename(filename);
const targetName = `${this.config.tempPublicDir}/${basename}`;
const targetName = `${targetDir}/${basename}`;
if (!await fs.pathExists(targetName)) {
let found = false;
if (this.remoteWebDavStorage) {
found = await this.remoteWebDavStorage.getFileSuccess(targetName);
if (this.remoteStorage) {
found = await this.remoteStorage.getFileSuccess(targetName, remoteDir);
}
if (!found) {
@@ -237,83 +270,170 @@ class ReaderWorker {
return targetName;
}
restoreCachedFile(filename) {
const workerId = this.workerState.generateWorkerId();
const wState = this.workerState.getControl(workerId);
wState.set({state: 'start'});
(async() => {
try {
wState.set({state: 'download', step: 1, totalSteps: 1, path: filename, progress: 0});
const targetName = await this.restoreRemoteFile(filename);
const stat = await fs.stat(targetName);
const basename = path.basename(filename);
wState.finish({path: `/tmp/${basename}`, size: stat.size, progress: 100});
} catch (e) {
if (e.message.indexOf('404') < 0)
log(LM_ERR, e.stack);
wState.set({state: 'error', error: e.message});
}
})();
return workerId;
}
async periodicCleanDir(dir, maxSize, timeout) {
try {
const list = await fs.readdir(dir);
let size = 0;
let files = [];
for (const name of list) {
const stat = await fs.stat(`${dir}/${name}`);
if (!stat.isDirectory()) {
size += stat.size;
files.push({name, stat});
}
}
log(`clean dir ${dir}, maxSize=${maxSize}, found ${files.length} files, total size=${size}`);
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
let i = 0;
let j = 0;
while (i < files.length && size > maxSize) {
const file = files[i];
const oldFile = `${dir}/${file.name}`;
let remoteSuccess = true;
//отправляем только this.config.tempPublicDir
if (this.remoteWebDavStorage && dir === this.config.tempPublicDir) {
remoteSuccess = false;
try {
//log(`remoteWebDavStorage.putFile ${path.basename(oldFile)}`);
await this.remoteWebDavStorage.putFile(oldFile);
remoteSuccess = true;
} catch (e) {
log(LM_ERR, e.stack);
}
}
//реально удаляем только если сохранили в хранилище
if (remoteSuccess || size > maxSize*1.2) {
await fs.remove(oldFile);
j++;
}
size -= file.stat.size;
i++;
}
log(`removed ${j} files`);
} catch(e) {
log(LM_ERR, e.stack);
} finally {
setTimeout(() => {
this.periodicCleanDir(dir, maxSize, timeout);
}, timeout);
pushRemoteSend(fileName, remoteDir) {
if (this.remoteStorage
&& this.dirConfig[remoteDir]
&& this.dirConfig[remoteDir].moveToRemote) {
this.remoteFilesToSend.push({fileName, remoteDir});
}
}
async remoteSendFile(sendFileRec) {
const {fileName, remoteDir} = sendFileRec;
const sent = this.remoteSent;
if (!fileName || sent[fileName])
return;
log(`remoteSendFile ${remoteDir}/${path.basename(fileName)}`);
//отправляем в remoteStorage
await this.remoteStorage.putFile(fileName, remoteDir);
sent[fileName] = true;
await this.appDb.insert({table: 'remote_sent', ignore: true, rows: [{id: fileName, remoteDir}]});
}
async remoteSendAll() {
if (!this.remoteStorage)
return;
const newSendQueue = [];
while (this.remoteFilesToSend.length) {
const sendFileRec = this.remoteFilesToSend.shift();
if (sendFileRec.remoteDir
&& this.dirConfig[sendFileRec.remoteDir]
&& this.dirConfig[sendFileRec.remoteDir].moveToRemote) {
try {
await this.remoteSendFile(sendFileRec);
} catch (e) {
newSendQueue.push(sendFileRec)
log(LM_ERR, e.stack);
}
}
}
this.remoteFilesToSend = newSendQueue;
}
async cleanDir(config) {
const {dir, remoteDir, maxSize, moveToRemote} = config;
const sent = this.remoteSent;
const list = await fs.readdir(dir);
let size = 0;
let files = [];
for (const filename of list) {
const filePath = `${dir}/${filename}`;
const stat = await fs.stat(filePath);
if (!stat.isDirectory()) {
size += stat.size;
files.push({name: filePath, stat});
}
}
log(LM_WARN, `clean dir ${dir}, maxSize=${maxSize}, found ${files.length} files, total size=${size}`);
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
//удаленное хранилище
if (moveToRemote && this.remoteStorage) {
const foundFiles = new Set();
for (const file of files) {
foundFiles.add(file.name);
//отсылаем на всякий случай перед удалением, если вдруг remoteSendAll не справился
try {
await this.remoteSendFile({fileName: file.name, remoteDir});
} catch (e) {
log(LM_ERR, e.stack);
}
}
//почистим remoteSent и БД
//несколько неоптимально, таскает все записи из таблицы
const rows = await this.appDb.select({table: 'remote_sent'});
for (const row of rows) {
if ((row.remoteDir === remoteDir && !foundFiles.has(row.id))
|| !this.dirConfig[row.remoteDir]) {
delete sent[row.id];
await this.appDb.delete({table: 'remote_sent', where: `@@id(${this.appDb.esc(row.id)})`});
}
}
}
let i = 0;
let j = 0;
while (i < files.length && size > maxSize) {
const file = files[i];
const oldFile = file.name;
//реально удаляем только если сохранили в хранилище или размер dir увеличен в 1.5 раза
if (!(moveToRemote && this.remoteStorage)
|| (moveToRemote && this.remoteStorage && sent[oldFile])
|| size > maxSize*1.5) {
await fs.remove(oldFile);
j++;
}
size -= file.stat.size;
i++;
}
log(LM_WARN, `removed ${j} files`);
}
async periodicCleanDir() {
try {
if (!this.remoteSent)
this.remoteSent = {};
//инициализация this.remoteSent
if (this.remoteStorage) {
const rows = await this.appDb.select({table: 'remote_sent'});
for (const row of rows) {
this.remoteSent[row.id] = true;
}
}
let lastCleanDirTime = 0;
let lastRemoteSendTime = 0;
while (1) {// eslint-disable-line no-constant-condition
//отсылка в удаленное хранилище
if (Date.now() - lastRemoteSendTime >= remoteSendPeriod) {
try {
await this.remoteSendAll();
} catch(e) {
log(LM_ERR, e.stack);
}
lastRemoteSendTime = Date.now();
}
//чистка папок
if (Date.now() - lastCleanDirTime >= cleanDirPeriod) {
for (const config of Object.values(this.dirConfig)) {
try {
await this.cleanDir(config);
} catch(e) {
log(LM_ERR, e.stack);
}
}
lastCleanDirTime = Date.now();
}
await utils.sleep(60*1000);//интервал проверки 1 минута
}
} catch (e) {
log(LM_FATAL, e.message);
ayncExit.exit(1);
}
}
}
module.exports = ReaderWorker;

View File

@@ -0,0 +1,98 @@
const fs = require('fs-extra');
const path = require('path');
const WebSocketConnection = require('./WebSocketConnection');
class RemoteStorage {
constructor(config) {
this.config = Object.assign({}, config);
this.config.maxContentLength = this.config.maxContentLength || 10*1024*1024;
this.accessToken = this.config.accessToken;
this.wsc = new WebSocketConnection(config.url, 10, 30, {rejectUnauthorized: false});
}
async wsRequest(query) {
const response = await this.wsc.message(
await this.wsc.send(Object.assign({accessToken: this.accessToken}, query), 600),
600
);
if (response.error)
throw new Error(response.error);
return response;
}
async wsStat(fileName) {
return await this.wsRequest({action: 'get-stat', fileName});
}
async wsGetFile(fileName) {
return this.wsRequest({action: 'get-file', fileName});
}
async wsPutFile(fileName, data) {//data base64 encoded string
return this.wsRequest({action: 'put-file', fileName, data});
}
async wsDelFile(fileName) {
return this.wsRequest({action: 'del-file', fileName});
}
makeRemoteFileName(fileName, dir = '') {
const base = path.basename(fileName);
if (base.length > 3) {
return `${dir}/${base.substr(0, 3)}/${base}`;
} else {
return `${dir}/${base}`;
}
}
async putFile(fileName, dir = '') {
if (!await fs.pathExists(fileName)) {
throw new Error(`File not found: ${fileName}`);
}
const remoteFilename = this.makeRemoteFileName(fileName, dir);
try {
const localStat = await fs.stat(fileName);
let remoteStat = await this.wsStat(remoteFilename);
remoteStat = remoteStat.stat;
if (remoteStat.isFile && localStat.size == remoteStat.size) {
return;
}
await this.wsDelFile(remoteFilename);
} catch (e) {
//
}
const data = await fs.readFile(fileName, 'base64');
await this.wsPutFile(remoteFilename, data);
}
async getFile(fileName, dir = '') {
if (await fs.pathExists(fileName)) {
return;
}
const remoteFilename = this.makeRemoteFileName(fileName, dir);
const response = await this.wsGetFile(remoteFilename);
await fs.writeFile(fileName, response.data, 'base64');
}
async getFileSuccess(filename, dir = '') {
try {
await this.getFile(filename, dir);
return true;
} catch (e) {
//
}
return false;
}
}
module.exports = RemoteStorage;

View File

@@ -46,16 +46,16 @@ class RemoteWebDavStorage {
return await this.wdc.createDirectory(dirname);
}
async putFile(filename) {
async putFile(filename, dir = '') {
if (!await fs.pathExists(filename)) {
throw new Error(`File not found: ${filename}`);
}
const base = path.basename(filename);
let remoteFilename = `/${base}`;
let remoteFilename = `${dir}/${base}`;
if (base.length > 3) {
const remoteDir = `/${base.substr(0, 3)}`;
const remoteDir = `${dir}/${base.substr(0, 3)}`;
try {
await this.mkdir(remoteDir);
} catch (e) {
@@ -79,24 +79,24 @@ class RemoteWebDavStorage {
await this.writeFile(remoteFilename, data);
}
async getFile(filename) {
async getFile(filename, dir = '') {
if (await fs.pathExists(filename)) {
return;
}
const base = path.basename(filename);
let remoteFilename = `/${base}`;
let remoteFilename = `${dir}/${base}`;
if (base.length > 3) {
remoteFilename = `/${base.substr(0, 3)}/${base}`;
remoteFilename = `${dir}/${base.substr(0, 3)}/${base}`;
}
const data = await this.readFile(remoteFilename);
await fs.writeFile(filename, data);
}
async getFileSuccess(filename) {
async getFileSuccess(filename, dir = '') {
try {
await this.getFile(filename);
await this.getFile(filename, dir);
return true;
} catch (e) {
//

View File

@@ -8,10 +8,13 @@ const cleanPeriod = 5*1000;//5 секунд
class WebSocketConnection {
//messageLifeTime в секундах (проверка каждый cleanPeriod интервал)
constructor(url, openTimeoutSecs = 10, messageLifeTimeSecs = 30) {
constructor(url, openTimeoutSecs = 10, messageLifeTimeSecs = 30, webSocketOptions = {}) {
this.WebSocket = (isBrowser ? WebSocket : require('ws'));
this.url = url;
this.webSocketOptions = webSocketOptions;
this.ws = null;
this.listeners = [];
this.messageQueue = [];
this.messageLifeTime = messageLifeTimeSecs*1000;
@@ -91,10 +94,10 @@ class WebSocketConnection {
const url = this.url || `${protocol}//${window.location.host}/ws`;
this.ws = new this.WebSocket(url);
} else {
this.ws = new this.WebSocket(this.url);
this.ws = new this.WebSocket(this.url, this.webSocketOptions);
}
const onopen = (e) => {
const onopen = () => {
this.connecting = false;
resolve(this.ws);
};

View File

@@ -34,6 +34,12 @@ function getFileHash(filename, hashName, enc) {
});
}
function getBufHash(buf, hashName, enc) {
const hash = crypto.createHash(hashName);
hash.update(buf);
return hash.digest(enc);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
@@ -129,6 +135,7 @@ module.exports = {
fromBase36,
bufferRemoveZeroes,
getFileHash,
getBufHash,
sleep,
toUnixTime,
randomHexString,

View File

@@ -0,0 +1,12 @@
module.exports = {
up: [
['create', {
table: 'remote_sent'
}],
],
down: [
['drop', {
table: 'remote_sent'
}],
]
};

View File

@@ -0,0 +1,6 @@
module.exports = {
table: 'migration1',
data: [
{id: 1, name: 'create', data: require('./001-create')}
]
}

View File

@@ -0,0 +1,16 @@
module.exports = {
up: [
['create', {
table: 'checked',
index: [
{field: 'queryTime', type: 'number'},
{field: 'checkTime', type: 'number'},
]
}],
],
down: [
['drop', {
table: 'checked'
}],
]
};

View File

@@ -0,0 +1,6 @@
module.exports = {
table: 'migration1',
data: [
{id: 1, name: 'create', data: require('./001-create')}
]
}

View File

@@ -1,4 +1,5 @@
module.exports = {
//'app': require('./jembaMigrations/app'),
'app': require('./app'),
'reader-storage': require('./reader-storage'),
'book-update-server': require('./book-update-server'),
};

View File

@@ -1,6 +1,6 @@
require('tls').DEFAULT_MIN_VERSION = 'TLSv1';
const fs = require('fs-extra');
const path = require('path');
const argv = require('minimist')(process.argv.slice(2));
const express = require('express');
const compression = require('compression');
@@ -11,6 +11,8 @@ const ayncExit = new (require('./core/AsyncExit'))();
let log = null;
const maxPayloadSize = 50;//in MB
async function init() {
//config
const configManager = new (require('./config'))();//singleton
@@ -63,7 +65,7 @@ async function main() {
if (serverCfg.mode !== 'none') {
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server, maxPayload: 10*1024*1024 });
const wss = new WebSocket.Server({ server, maxPayload: maxPayloadSize*1024*1024 });
const serverConfig = Object.assign({}, config, serverCfg);
@@ -75,20 +77,10 @@ async function main() {
}
app.use(compression({ level: 1 }));
app.use(express.json({limit: '10mb'}));
app.use(express.json({limit: `${maxPayloadSize}mb`}));
if (devModule)
devModule.logQueries(app);
app.use(express.static(serverConfig.publicDir, {
maxAge: '30d',
setHeaders: (res, filePath) => {
if (path.basename(path.dirname(filePath)) == 'tmp') {
res.set('Content-Type', 'application/xml');
res.set('Content-Encoding', 'gzip');
}
}
}));
require('./routes').initRoutes(app, wss, serverConfig);
if (devModule) {

View File

@@ -1,8 +1,24 @@
const c = require('./controllers');
const utils = require('./core/utils');
const fs = require('fs-extra');
const path = require('path');
const express = require('express');
const multer = require('multer');
const ReaderWorker = require('./core/Reader/ReaderWorker');//singleton
const log = new (require('./core/AppLogger'))().log;//singleton
const c = require('./controllers');
const utils = require('./core/utils');
function initRoutes(app, wss, config) {
//эксклюзив для update_checker
if (config.mode === 'book_update_checker') {
new c.BookUpdateCheckerController(wss, config);
return;
}
initStatic(app, config);
const misc = new c.MiscController(config);
const reader = new c.ReaderController(config);
const worker = new c.WorkerController(config);
@@ -29,7 +45,6 @@ function initRoutes(app, wss, config) {
['POST', '/api/reader/load-book', reader.loadBook.bind(reader), [aAll], {}],
['POST', '/api/reader/storage', reader.storage.bind(reader), [aAll], {}],
['POST', '/api/reader/upload-file', [upload.single('file'), reader.uploadFile.bind(reader)], [aAll], {}],
['POST', '/api/reader/restore-cached-file', reader.restoreCachedFile.bind(reader), [aAll], {}],
['POST', '/api/worker/get-state', worker.getState.bind(worker), [aAll], {}],
];
@@ -77,6 +92,48 @@ function initRoutes(app, wss, config) {
}
}
function initStatic(app, config) {
const readerWorker = new ReaderWorker(config);
//восстановление файлов в /tmp и /upload из webdav-storage, при необходимости
app.use(async(req, res, next) => {
if ((req.method !== 'GET' && req.method !== 'HEAD') ||
!(req.path.indexOf('/tmp/') === 0 || req.path.indexOf('/upload/') === 0)
) {
return next();
}
const filePath = `${config.publicDir}${req.path}`;
//восстановим
try {
if (!await fs.pathExists(filePath)) {
if (req.path.indexOf('/tmp/') === 0) {
await readerWorker.restoreRemoteFile(req.path, '/tmp');
} else if (req.path.indexOf('/upload/') === 0) {
await readerWorker.restoreRemoteFile(req.path, '/upload');
}
}
} catch(e) {
log(LM_ERR, `static::restoreRemoteFile ${req.path} > ${e.message}`);
}
return next();
});
const tmpDir = `${config.publicDir}/tmp`;
app.use(express.static(config.publicDir, {
maxAge: '30d',
setHeaders: (res, filePath) => {
if (path.dirname(filePath) == tmpDir) {
res.set('Content-Type', 'application/xml');
res.set('Content-Encoding', 'gzip');
}
},
}));
}
module.exports = {
initRoutes
}