Compare commits

...

129 Commits

Author SHA1 Message Date
Book Pauk
0fe513d7f5 Merge branch 'release/0.8.3-2' 2020-01-29 01:02:56 +07:00
Book Pauk
0be05325e4 Исправлен баг 2020-01-29 01:02:05 +07:00
Book Pauk
75b39308cd Merge tag '0.8.3-1' into develop
0.8.3-1
2020-01-28 21:32:25 +07:00
Book Pauk
35ded81713 Merge branch 'release/0.8.3-1' 2020-01-28 21:32:02 +07:00
Book Pauk
07c85280cd Исправлены таймауты для конвертера calibre, добавлен флаг запуска -vv, соответственно поправлено вычисление прогресса 2020-01-28 21:27:54 +07:00
Book Pauk
43f1d86be0 Merge tag '0.8.3' into develop
0.8.3
2020-01-28 20:21:40 +07:00
Book Pauk
82f5ed4c44 Merge branch 'release/0.8.3' 2020-01-28 20:21:31 +07:00
Book Pauk
0b53ad4b4d Версия 0.8.3 2020-01-28 20:20:10 +07:00
Book Pauk
56ad41d10c Поправки текста объявления 2020-01-28 20:18:02 +07:00
Book Pauk
249a4564e0 Добавлено уведомление "Оплатим хостинг вместе" 2020-01-28 19:46:34 +07:00
Book Pauk
efb2413720 Небольшое изменение содержимого страницы 2020-01-28 19:44:52 +07:00
Book Pauk
1226acefd6 Небольшие исправления, queue теперь в одном экземпляре на класс 2020-01-28 14:51:09 +07:00
Book Pauk
76f7d7bc90 Мелкая поправка 2020-01-27 19:52:56 +07:00
Book Pauk
a5cb2641fd Мелкая поправка 2020-01-27 19:42:30 +07:00
Book Pauk
57fc64af79 Добавлен abort конвертеров при истечении времени ожидания подвижек очереди 2020-01-27 19:34:10 +07:00
Book Pauk
f8b7b8b698 Исправления LimitedQueue, исправления багов, добавлена проверка флага abort 2020-01-27 18:57:42 +07:00
Book Pauk
3da6befe10 Добавлен класс LimitedQueue для организации очередей 2020-01-26 18:38:09 +07:00
Book Pauk
a50d61c3ce Добавлена очередь скачивания и конвертирования 2020-01-26 18:37:14 +07:00
Book Pauk
b7568975e7 Добавлена обработка state = 'queue' 2020-01-26 18:31:31 +07:00
Book Pauk
4b9475310f Убрал ненужный this.taken 2020-01-26 16:23:20 +07:00
Book Pauk
639f726c83 Добавлен лимит на размер файла при распаковке 2020-01-26 15:17:45 +07:00
Book Pauk
7997c486cf Мелкий рефакторинг 2020-01-26 15:07:14 +07:00
Book Pauk
2569d00bd0 Мелкие поправки 2020-01-26 13:47:25 +07:00
Book Pauk
2cd80d8fa1 Merge tag '0.8.2-5' into develop
0.8.2-5
2020-01-23 17:12:46 +07:00
Book Pauk
eedca4db9b Merge branch 'release/0.8.2-5' 2020-01-23 17:12:37 +07:00
Book Pauk
1d352a76ce Поправка опечаток 2020-01-23 17:00:17 +07:00
Book Pauk
17670aabf9 WebSocket: добавлен метод reader-storage, поправки багов 2020-01-23 16:59:08 +07:00
Book Pauk
3456b3d90e WebSocket: добавлен метод worker-get-state-finish, небольшой рефакторинг 2020-01-23 16:25:06 +07:00
Book Pauk
f3da5a9026 Поправил комментарий 2020-01-23 15:56:26 +07:00
Book Pauk
00cc63b7cd WebSocket: добавлен метод get-config 2020-01-23 15:54:46 +07:00
Book Pauk
8df80ce738 Мелкая поправка 2020-01-23 15:16:49 +07:00
Book Pauk
12e7a783b0 Небольшие изменения блокирования кнопок панели 2020-01-22 22:06:12 +07:00
Book Pauk
be86a15351 Добавил настройку proxy_read_timeout 2020-01-22 21:37:28 +07:00
Book Pauk
2c5022e7b4 Merge tag '0.8.2-4' into develop
0.8.2-4
2020-01-22 21:17:58 +07:00
Book Pauk
f4a996fcb9 Merge branch 'release/0.8.2-4' 2020-01-22 21:17:52 +07:00
Book Pauk
fdbf508bbf Используем протокол WSS при необходимости 2020-01-22 21:17:10 +07:00
Book Pauk
500fafa5b2 Merge tag '0.8.2-3' into develop
0.8.2-3
2020-01-22 21:05:36 +07:00
Book Pauk
bfa315c68b Merge branch 'release/0.8.2-3' 2020-01-22 21:05:27 +07:00
Book Pauk
4972f085a3 Мелкая поправка 2020-01-22 20:59:52 +07:00
Book Pauk
9c13261929 Добавлена настройка для вебсокетов, добавлен конфиг nginx omnireader_http 2020-01-22 20:58:57 +07:00
Book Pauk
e36dc4a913 Небольшие поправки 2020-01-22 20:28:46 +07:00
Book Pauk
4cccb56ee3 Поправил комментарий 2020-01-22 20:15:33 +07:00
Book Pauk
3199af570d Добавлен WebSocketServer и контроллер для него 2020-01-22 20:06:51 +07:00
Book Pauk
7dad47b3c8 Добавлено использование WebSocketConnection 2020-01-22 20:02:42 +07:00
Book Pauk
fbd50bad1d Исправления багов 2020-01-22 20:02:05 +07:00
Book Pauk
10469bae7b Мелкая поправка 2020-01-22 20:01:21 +07:00
Book Pauk
b6a000a001 Добавлен пакет ws 2020-01-22 20:00:52 +07:00
Book Pauk
59539e7e90 Добавлен класс WebSocketConnection 2020-01-22 19:32:11 +07:00
Book Pauk
a2c41bc5ec Merge tag '0.8.2-2' into develop
0.8.2-2
2020-01-21 16:56:20 +07:00
Book Pauk
c4a06858fb Merge branch 'release/0.8.2-2' 2020-01-21 16:56:12 +07:00
Book Pauk
15b0f05a05 Добавил комментарий 2020-01-21 16:55:41 +07:00
Book Pauk
67feee9aa1 Поправлен баг 2020-01-21 16:53:34 +07:00
Book Pauk
185fb57b8c Удален нерабочий код 2020-01-21 16:25:30 +07:00
Book Pauk
e9039f8208 Merge tag '0.8.2-1' into develop
0.8.2-1
2020-01-21 16:14:21 +07:00
Book Pauk
440d1b3ba0 Merge branch 'release/0.8.2-1' 2020-01-21 16:14:15 +07:00
Book Pauk
9c7a6c64b0 Небольшие поправки 2020-01-21 16:13:38 +07:00
Book Pauk
7cc63fe849 Добавлена автоматическая отправка загруженной книги удаленное хранилище 2020-01-21 15:53:23 +07:00
Book Pauk
5647e8219d Мелкий рефакторинг 2020-01-21 14:58:42 +07:00
Book Pauk
81629fab7a Замена webdav-fs на webdav 2020-01-21 13:54:21 +07:00
Book Pauk
992d2033f3 Merge tag '0.8.2' into develop
0.8.2
2020-01-20 21:49:08 +07:00
Book Pauk
d52d4a1278 Merge branch 'release/0.8.2' 2020-01-20 21:49:00 +07:00
Book Pauk
57a44c5952 Версия 0.8.2 2020-01-20 21:48:31 +07:00
Book Pauk
a04161ac7c Добавил принудительную загрузку книги в обход кэша, если указан URL 2020-01-20 21:44:09 +07:00
Book Pauk
47e46f13c3 Добавлен работа с RemoteWebDavStorage, в т.ч. через api 2020-01-20 21:39:55 +07:00
Book Pauk
5535bd91c8 В конфиг добавлена опция remoteWebDavStorage 2020-01-20 21:37:31 +07:00
Book Pauk
8747a00de6 Поправлен баг 2020-01-20 21:36:44 +07:00
Book Pauk
c926b86926 Добавлен пакет webdav-fs 2020-01-20 21:22:27 +07:00
Book Pauk
010ac9aa7c Доработка api, восстановление кэшированного файла из хранилища 2020-01-20 21:21:13 +07:00
Book Pauk
4ab0c337f1 Рефакторинг 2020-01-15 16:20:46 +07:00
Book Pauk
f814c42fdd Поправлен баг в getStateFinish 2020-01-15 16:06:28 +07:00
Book Pauk
02aee3e625 Добавлена переупаковка файла книги по максимуму через 5 сек после загрузки и конвертирования 2020-01-15 15:49:45 +07:00
Book Pauk
52a32cfdd1 Добавлена обработка ошибок JSON.parse 2020-01-12 20:06:50 +07:00
Book Pauk
6faa7b2efe Уменьшение запросов get-state к api, добавлен метод get-state-finish 2020-01-12 18:51:12 +07:00
Book Pauk
f8481413c9 Мелкий рефакторинг 2020-01-12 17:03:34 +07:00
Book Pauk
0951d01383 Merge tag '0.8.1-1' into develop
0.8.1-1
2020-01-10 21:47:58 +07:00
Book Pauk
da34472a6f Merge branch 'release/0.8.1-1' 2020-01-10 21:47:50 +07:00
Book Pauk
e89b6e3ea0 Добавлен компонент GithubCorner на LoaderPage 2020-01-09 20:15:32 +07:00
Book Pauk
977bab4745 0.8.1 2020-01-09 20:14:49 +07:00
Book Pauk
26c73109fe Небольшая поправка 2020-01-08 13:46:24 +07:00
Book Pauk
65f911ad51 Поправил CRLF => LF 2020-01-07 23:12:06 +07:00
Book Pauk
f8ed5ebd6a Merge tag '0.8.1' into develop
0.8.1
2020-01-07 22:57:38 +07:00
Book Pauk
e4cb61bebe Merge branch 'release/0.8.1' 2020-01-07 22:57:30 +07:00
Book Pauk
7d5310af42 Версия 0.8.1 2020-01-07 22:57:06 +07:00
Book Pauk
f68c610c0d Добавлена поддержка формата FB3 "для галочки" 2020-01-07 22:52:28 +07:00
Book Pauk
ccfb6a6d73 Исправление бага "Request path contains unescaped characters" 2020-01-07 20:31:52 +07:00
Book Pauk
da55996e22 Удалил неиспользуемые пакеты 2020-01-05 15:28:35 +07:00
Book Pauk
ecd8400a34 Удалил неиспользуемые пакеты 2020-01-05 15:15:55 +07:00
Book Pauk
03914883bc Поправил readme 2020-01-05 13:37:09 +07:00
Book Pauk
9981e1f3bd Мелкие поправки 2020-01-03 17:06:03 +07:00
Book Pauk
4d1df66025 Небольшой рефакторинг 2020-01-03 16:58:37 +07:00
Book Pauk
a0f64e188b Поправил readme 2020-01-03 15:59:00 +07:00
Book Pauk
08407a1094 Merge tag '0.8.0-1' into develop
0.8.0-1
2020-01-02 23:45:55 +07:00
Book Pauk
445ea3bb2e Merge branch 'release/0.8.0-1' 2020-01-02 23:45:46 +07:00
Book Pauk
0e0aab98b1 Поправил историю версий 2020-01-02 23:45:16 +07:00
Book Pauk
721d5eb0c1 Поправил readme 2020-01-02 23:45:03 +07:00
Book Pauk
6d99dbc3a7 Мелкая поправка 2020-01-02 22:49:31 +07:00
Book Pauk
2be31f649b Merge tag '0.8.0' into develop
0.8.0
2020-01-02 22:26:15 +07:00
Book Pauk
828ac27c03 Merge branch 'release/0.8.0' 2020-01-02 22:26:06 +07:00
Book Pauk
b3d614002f Поправил readme 2020-01-02 22:25:41 +07:00
Book Pauk
2b2000ca10 Поправил readme 2020-01-02 22:12:43 +07:00
Book Pauk
8d7428d099 Избавление от предупреждений npm 2020-01-02 20:03:20 +07:00
Book Pauk
57f8322f31 Поправил описание 2020-01-02 19:47:07 +07:00
Book Pauk
bee7bc4294 0.8.0 2020-01-02 19:45:13 +07:00
Book Pauk
28702065bc Добавил repository 2020-01-02 19:43:49 +07:00
Book Pauk
c248057081 Добавлен файл лицензии 2020-01-02 19:33:15 +07:00
Book Pauk
6186f5e138 Версия 0.8.0 2020-01-02 19:32:56 +07:00
Book Pauk
2201d8176d Удалена yandex-метрика 2020-01-01 14:55:29 +07:00
Book Pauk
2ba6819876 Окончательный переход на https 2020-01-01 14:48:32 +07:00
Book Pauk
a393b2a370 Окончательный переход на https 2020-01-01 14:34:54 +07:00
Book Pauk
59fe713df2 Немного улучшил загрузку веб-шрифтов 2019-12-25 01:29:32 +07:00
Book Pauk
4b8efaca9a Merge tag '0.7.9-3' into develop
0.7.9-3
2019-12-24 23:47:32 +07:00
Book Pauk
a26100a8d0 Merge branch 'release/0.7.9-3' 2019-12-24 23:47:21 +07:00
Book Pauk
8c52f4718c Добавил require('tls').DEFAULT_MIN_VERSION = 'TLSv1' 2019-12-24 23:45:04 +07:00
Book Pauk
85b5c3c4ec Merge tag '0.7.9-2' into develop
0.7.9-2
2019-12-23 22:25:47 +07:00
Book Pauk
4fd559e4c7 Merge branch 'release/0.7.9-2' 2019-12-23 22:25:40 +07:00
Book Pauk
a337d0ddc7 Попытка обновить pkg, версия node 10.4.1 глючит с setTimeout 2019-12-23 22:24:36 +07:00
Book Pauk
9e4cb7071e Merge tag '0.7.9-1' into develop
0.7.9-1
2019-12-23 21:02:11 +07:00
Book Pauk
c3f1707343 Merge branch 'release/0.7.9-1' 2019-12-23 21:02:02 +07:00
Book Pauk
1ed058a553 Мелкие поправки 2019-12-23 20:57:44 +07:00
Book Pauk
0500a8178d Merge tag '0.7.9' into develop
0.7.9
2019-11-27 18:16:52 +07:00
Book Pauk
7d0059f573 Merge branch 'release/0.7.9' 2019-11-27 18:16:43 +07:00
Book Pauk
4e3b882362 Версия 0.7.9 2019-11-27 18:15:28 +07:00
Book Pauk
13cf47873e Добавлен неубираемый баннер о переходе на httpS 2019-11-27 18:11:34 +07:00
Book Pauk
7ee23ec38f Удален устаревший код 2019-11-27 17:33:30 +07:00
Book Pauk
eebf17c42c Добавлена проверка наличия файла на сервере перед скачиванием fb2 2019-11-27 17:12:07 +07:00
Book Pauk
f84536788b Merge tag '0.7.8b' into develop
0.7.8b
2019-11-25 16:00:10 +07:00
Book Pauk
4bbfdc2cb2 Merge branch 'release/0.7.8b' 2019-11-25 16:00:00 +07:00
Book Pauk
211fec35e3 Исправлен баг 2019-11-25 15:56:29 +07:00
Book Pauk
b8214a46ae Merge tag '0.7.8' into develop
0.7.8
2019-11-25 15:39:35 +07:00
58 changed files with 2451 additions and 670 deletions

106
LICENSE.md Normal file
View File

@@ -0,0 +1,106 @@
# CC0 1.0 Universal
## Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator and
subsequent owner(s) (each and all, an “owner”) of an original work of
authorship and/or a database (each, a “Work”).
Certain owners wish to permanently relinquish those rights to a Work for the
purpose of contributing to a commons of creative, cultural and scientific works
(“Commons”) that the public can reliably and without fear of later claims of
infringement build upon, modify, incorporate in other works, reuse and
redistribute as freely as possible in any form whatsoever and for any purposes,
including without limitation commercial purposes. These owners may contribute
to the Commons to promote the ideal of a free culture and the further
production of creative, cultural and scientific works, or to gain reputation or
greater distribution for their Work in part through the use and efforts of
others.
For these and/or other purposes and motivations, and without any expectation of
additional consideration or compensation, the person associating CC0 with a
Work (the “Affirmer”), to the extent that he or she is an owner of Copyright
and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and
publicly distribute the Work under its terms, with knowledge of his or her
Copyright and Related Rights in the Work and the meaning and intended legal
effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights (“Copyright and
Related Rights”). Copyright and Related Rights include, but are not limited
to, the following:
1. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
2. moral rights retained by the original author(s) and/or performer(s);
3. publicity and privacy rights pertaining to a persons image or likeness
depicted in a Work;
4. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(i), below;
5. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
6. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation thereof,
including any amended or successor version of such directive); and
7. other similar, equivalent or corresponding rights throughout the world
based on applicable law or treaty, and any national implementations
thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention of,
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
unconditionally waives, abandons, and surrenders all of Affirmers Copyright
and Related Rights and associated claims and causes of action, whether now
known or unknown (including existing as well as future claims and causes of
action), in the Work (i) in all territories worldwide, (ii) for the maximum
duration provided by applicable law or treaty (including future time
extensions), (iii) in any current or future medium and for any number of
copies, and (iv) for any purpose whatsoever, including without limitation
commercial, advertising or promotional purposes (the “Waiver”). Affirmer makes
the Waiver for the benefit of each member of the public at large and to the
detriment of Affirmers heirs and successors, fully intending that such Waiver
shall not be subject to revocation, rescission, cancellation, termination, or
any other legal or equitable action to disrupt the quiet enjoyment of the Work
by the public as contemplated by Affirmers express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason be
judged legally invalid or ineffective under applicable law, then the Waiver
shall be preserved to the maximum extent permitted taking into account
Affirmers express Statement of Purpose. In addition, to the extent the Waiver
is so judged Affirmer hereby grants to each affected person a royalty-free, non
transferable, non sublicensable, non exclusive, irrevocable and unconditional
license to exercise Affirmers Copyright and Related Rights in the Work (i) in
all territories worldwide, (ii) for the maximum duration provided by applicable
law or treaty (including future time extensions), (iii) in any current or
future medium and for any number of copies, and (iv) for any purpose
whatsoever, including without limitation commercial, advertising or promotional
purposes (the “License”). The License shall be deemed effective as of the date
CC0 was applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder of the
License, and in such case Affirmer hereby affirms that he or she will not (i)
exercise any of his or her remaining Copyright and Related Rights in the Work
or (ii) assert any associated claims and causes of action with respect to the
Work, in either case contrary to Affirmers express Statement of Purpose.
4. Limitations and Disclaimers.
1. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
2. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied, statutory
or otherwise, including without limitation warranties of title,
merchantability, fitness for a particular purpose, non infringement, or
the absence of latent or other defects, accuracy, or the present or
absence of errors, whether or not discoverable, all to the greatest
extent permissible under applicable law.
3. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any persons Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the Work.
4. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
For more information, please see
http://creativecommons.org/publicdomain/zero/1.0/.

View File

@@ -1,3 +1,43 @@
# Liberama # Liberama
Свободный обмен книгами в формате fb2 Браузерная онлайн-читалка книг и децентрализованная библиотека.
Читалка ![](https://omnireader.ru/favicon.ico)[OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS:
![](docs/assets/face.jpg)
![](docs/assets/reader.jpg)
## VPS
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader](docs/omnireader/README.md)
## Сборка проекта
Необходима версия node.js не ниже 10.
```
$ git clone https://github.com/bookpauk/liberama
$ cd liberama
$ npm i
```
### Windows
```
$ npm run build:win
```
### Linux
```
$ npm run build:linux
```
Результат сборки будет доступен в каталоге `dist/linux|win` в виде исполнимого (standalone) файла
### Разработка
```
$ npm run dev
```
## Помочь проекту
* bitcoin: 3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85
* litecoin: MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ
* monero: 8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz

View File

@@ -24,8 +24,8 @@ async function main() {
await fs.ensureDir(tempDownloadDir); await fs.ensureDir(tempDownloadDir);
//sqlite3 //sqlite3
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.0.4/node-v64-linux-x64.tar.gz'; const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.1.1/node-v72-linux-x64.tar.gz';
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v64-linux-x64/node_sqlite3.node`; const sqliteDecompressedFilename = `${tempDownloadDir}/node-v72-linux-x64/node_sqlite3.node`;
if (!await fs.pathExists(sqliteDecompressedFilename)) { if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку // Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку

View File

@@ -24,8 +24,8 @@ async function main() {
await fs.ensureDir(tempDownloadDir); await fs.ensureDir(tempDownloadDir);
//sqlite3 //sqlite3
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.0.4/node-v64-win32-x64.tar.gz'; const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.1.1/node-v72-win32-x64.tar.gz';
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v64-win32-x64/node_sqlite3.node`; const sqliteDecompressedFilename = `${tempDownloadDir}/node-v72-win32-x64/node_sqlite3.node`;
if (!await fs.pathExists(sqliteDecompressedFilename)) { if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку // Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку

View File

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

View File

@@ -1,6 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import * as utils from '../share/utils'; import * as utils from '../share/utils';
import wsc from './webSocketConnection';
const api = axios.create({ const api = axios.create({
baseURL: '/api/reader' baseURL: '/api/reader'
@@ -11,8 +11,67 @@ const workerApi = axios.create({
}); });
class Reader { class Reader {
constructor() {
}
async getWorkerStateFinish(workerId, callback) {
if (!callback) callback = () => {};
let response = {};
try {
await wsc.open();
const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
while (1) {// eslint-disable-line no-constant-condition
response = await wsc.message(requestId);
callback(response);
if (!response.state)
throw new Error('Неверный ответ api');
if (response.state == 'finish' || response.state == 'error') {
break;
}
}
return response;
} catch (e) {
console.error(e);
}
//если с WebSocket проблема, работаем по http
const refreshPause = 500;
let i = 0;
response = {};
while (1) {// eslint-disable-line no-constant-condition
const prevProgress = response.progress || 0;
const prevState = response.state || 0;
response = await workerApi.post('/get-state', {workerId});
response = response.data;
callback(response);
if (!response.state)
throw new Error('Неверный ответ api');
if (response.state == 'finish' || response.state == 'error') {
break;
}
if (i > 0)
await utils.sleep(refreshPause);
i++;
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
throw new Error('Слишком долгое время ожидания');
}
//проверка воркера
i = (prevProgress != response.progress || prevState != response.state ? 1 : i);
}
return response;
}
async loadBook(opts, callback) { async loadBook(opts, callback) {
const refreshPause = 300;
if (!callback) callback = () => {}; if (!callback) callback = () => {};
let response = await api.post('/load-book', opts); let response = await api.post('/load-book', opts);
@@ -22,58 +81,90 @@ class Reader {
throw new Error('Неверный ответ api'); throw new Error('Неверный ответ api');
callback({totalSteps: 4}); callback({totalSteps: 4});
let i = 0;
while (1) {// eslint-disable-line no-constant-condition
callback(response.data); callback(response.data);
if (response.data.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл response = await this.getWorkerStateFinish(workerId, callback);
if (response) {
if (response.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
callback({step: 4}); callback({step: 4});
const book = await this.loadCachedBook(response.data.path, callback); const book = await this.loadCachedBook(response.path, callback, response.size);
return Object.assign({}, response.data, {data: book.data}); return Object.assign({}, response, {data: book.data});
} }
if (response.data.state == 'error') {
let errMes = response.data.error; if (response.state == 'error') {
let errMes = response.error;
if (errMes.indexOf('getaddrinfo') >= 0 || if (errMes.indexOf('getaddrinfo') >= 0 ||
errMes.indexOf('ECONNRESET') >= 0 || errMes.indexOf('ECONNRESET') >= 0 ||
errMes.indexOf('EINVAL') >= 0 || errMes.indexOf('EINVAL') >= 0 ||
errMes.indexOf('404') >= 0) errMes.indexOf('404') >= 0)
errMes = `Ресурс не найден по адресу: ${response.data.url}`; errMes = `Ресурс не найден по адресу: ${response.url}`;
throw new Error(errMes); throw new Error(errMes);
} }
if (i > 0) } else {
await utils.sleep(refreshPause); throw new Error('Пустой ответ сервера');
i++;
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
throw new Error('Слишком долгое время ожидания');
}
//проверка воркера
const prevProgress = response.data.progress;
const prevState = response.data.state;
response = await workerApi.post('/get-state', {workerId});
i = (prevProgress != response.data.progress || prevState != response.data.state ? 1 : i);
} }
} }
async loadCachedBook(url, callback){ async checkCachedBook(url) {
const response = await axios.head(url); let estSize = -1;
try {
const response = await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
let estSize = 1000000;
if (response.headers['content-length']) { if (response.headers['content-length']) {
estSize = response.headers['content-length']; estSize = response.headers['content-length'];
} }
} catch (e) {
//восстановим при необходимости файл на сервере из удаленного облака
let response = null
try {
await wsc.open();
response = await wsc.message(wsc.send({action: 'reader-restore-cached-file', path: url}));
} catch (e) {
console.error(e);
//если с WebSocket проблема, работаем по http
response = await api.post('/restore-cached-file', {path: url});
response = response.data;
}
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;
}
async loadCachedBook(url, callback, estSize = -1) {
if (!callback) callback = () => {};
callback({state: 'loading', progress: 0}); callback({state: 'loading', progress: 0});
//получение размера файла
if (estSize && estSize < 0) {
estSize = await this.checkCachedBook(url);
}
//получение файла
estSize = (estSize > 0 ? estSize : 1000000);
const options = { const options = {
onDownloadProgress: progress => { onDownloadProgress: (progress) => {
while (progress.loaded > estSize) estSize *= 1.5; while (progress.loaded > estSize) estSize *= 1.5;
if (callback) if (callback)
callback({progress: Math.round((progress.loaded*100)/estSize)}); callback({progress: Math.round((progress.loaded*100)/estSize)});
} }
} }
//загрузка
return await axios.get(url, options); return await axios.get(url, options);
} }
@@ -110,13 +201,22 @@ class Reader {
} }
async storage(request) { async storage(request) {
let response = await api.post('/storage', request); let response = null;
try {
await wsc.open();
response = await wsc.message(wsc.send({action: 'reader-storage', body: request}));
} catch (e) {
console.error(e);
//если с WebSocket проблема, работаем по http
response = await api.post('/storage', request);
response = response.data;
}
const state = response.data.state; const state = response.state;
if (!state) if (!state)
throw new Error('Неверный ответ api'); throw new Error('Неверный ответ api');
return response.data; return response;
} }
} }

View File

@@ -0,0 +1,176 @@
const cleanPeriod = 60*1000;//1 минута
class WebSocketConnection {
//messageLifeTime в минутах (cleanPeriod)
constructor(messageLifeTime = 5) {
this.ws = null;
this.timer = null;
this.listeners = [];
this.messageQueue = [];
this.messageLifeTime = messageLifeTime;
this.requestId = 0;
}
addListener(listener) {
if (this.listeners.indexOf(listener) < 0)
this.listeners.push(Object.assign({regTime: Date.now()}, listener));
}
//рассылаем сообщение и удаляем те обработчики, которые его получили
emit(mes, isError) {
const len = this.listeners.length;
if (len > 0) {
let newListeners = [];
for (const listener of this.listeners) {
let emitted = false;
if (isError) {
if (listener.onError)
listener.onError(mes);
emitted = true;
} else {
if (listener.onMessage) {
if (listener.requestId) {
if (listener.requestId === mes.requestId) {
listener.onMessage(mes);
emitted = true;
}
} else {
listener.onMessage(mes);
emitted = true;
}
} else {
emitted = true;
}
}
if (!emitted)
newListeners.push(listener);
}
this.listeners = newListeners;
}
return this.listeners.length != len;
}
open(url) {
return new Promise((resolve, reject) => {
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
resolve(this.ws);
} else {
let protocol = 'ws:';
if (window.location.protocol == 'https:') {
protocol = 'wss:'
}
url = url || `${protocol}//${window.location.host}/ws`;
this.ws = new WebSocket(url);
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
let resolved = false;
this.ws.onopen = (e) => {
resolved = true;
resolve(e);
};
this.ws.onmessage = (e) => {
try {
const mes = JSON.parse(e.data);
this.messageQueue.push({regTime: Date.now(), mes});
let newMessageQueue = [];
for (const message of this.messageQueue) {
if (!this.emit(message.mes)) {
newMessageQueue.push(message);
}
}
this.messageQueue = newMessageQueue;
} catch (e) {
this.emit(e.message, true);
}
};
this.ws.onerror = (e) => {
this.emit(e.message, true);
if (!resolved)
reject(e);
};
}
});
}
//timeout в минутах (cleanPeriod)
message(requestId, timeout = 2) {
return new Promise((resolve, reject) => {
this.addListener({
requestId,
timeout,
onMessage: (mes) => {
if (mes.error) {
reject(mes.error);
} else {
resolve(mes);
}
},
onError: (e) => {
reject(e);
}
});
});
}
send(req) {
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
const requestId = ++this.requestId;
this.ws.send(JSON.stringify(Object.assign({requestId}, req)));
return requestId;
} else {
throw new Error('WebSocket connection is not ready');
}
}
close() {
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
this.ws.close();
}
}
periodicClean() {
try {
this.timer = null;
const now = Date.now();
//чистка listeners
let newListeners = [];
for (const listener of this.listeners) {
if (now - listener.regTime < listener.timeout*cleanPeriod - 50) {
newListeners.push(listener);
} else {
if (listener.onError)
listener.onError('Время ожидания ответа истекло');
}
}
this.listeners = newListeners;
//чистка messageQueue
let newMessageQueue = [];
for (const message of this.messageQueue) {
if (now - message.regTime < this.messageLifeTime*cleanPeriod - 50) {
newMessageQueue.push(message);
}
}
this.messageQueue = newMessageQueue;
} finally {
if (this.ws.readyState == WebSocket.OPEN) {
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
}
}
}
}
export default new WebSocketConnection();

View File

@@ -215,22 +215,6 @@ class App extends Vue {
window.history.replaceState({}, '', '/'); window.history.replaceState({}, '', '/');
this.$router.replace({ path: '/reader', query: q }); this.$router.replace({ path: '/reader', query: q });
} }
//yandex-метрика для omnireader
if (this.config.branch == 'production' && this.mode == 'omnireader' && !this.yaMetricsDone) {
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");// eslint-disable-line no-unexpected-multiline
ym(52347334, "init", {// eslint-disable-line no-undef
id:52347334,
clickmap:true,
trackLinks:true,
accurateTrackBounce:true
});
this.yaMetricsDone = true;
}
} }
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------

View File

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

View File

@@ -0,0 +1,128 @@
<template>
<div id="vue-github-corner">
<a :href="url" id="github-corner" target="_blank" aria-label="View source on Github" >
<svg id="github-corner-svg"
aria-hidden="true"
viewBox="0 0 250 250"
:width="size" :height="size"
:style="svgStyle" >
<path :d="svgPath1" @mouseenter="flipColor" @mouseleave="flipColor"></path>
<path :d="svgPath2" :style="gitStyle" class="octo-arm"></path>
<path :d="svgPath3" :style="gitStyle" class="octo-body"></path>
</svg>
</a>
</div>
</template>
<script>
export default {
name: 'GithubCorner',
props: {
url: {
type: String,
default: '/'
},
size: {
type: Number,
default: 80
},
colorScheme: {
type: String,
default: 'auto'
},
cornerColor: {
type: String,
default: '#625D5D'
},
gitColor: {
type: String,
default: 'PeachPuff'
},
leftCorner: {
type: Boolean,
default: false
},
flipOnHover: {
type: Boolean,
default: false
}
},
data () {
return {
svgStyle: {
fill: this.cornerColor,
right: (this.leftCorner ? 'auto' : '0'),
left: (this.leftCorner ? '0' : 'auto'),
transform: (this.leftCorner ? 'scale(-1, 1)' : 'none')
},
gitStyle: {
fill: this.gitColor
},
flipped: false,
svgPath1: 'M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z',
svgPath2: 'M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2',
svgPath3: 'M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z'
}
},
methods: {
flipColor: function() {
if (this.flipOnHover) {
let holdSvgFill = this.svgStyle.fill
this.svgStyle.fill = this.gitStyle.fill
this.gitStyle.fill = holdSvgFill
}
}
},
beforeMount: function() {
if (this.colorScheme != 'auto') {
let sch = this.colorScheme
this.gitStyle.fill = '#fff'
if (sch.toLowerCase() == 'black') {
this.svgStyle.fill = '#151513'
}
if (sch.toLowerCase() == 'green') {
this.svgStyle.fill = '#64CEAA'
}
if (sch.toLowerCase() == 'red') {
this.svgStyle.fill = '#FD6C6C'
}
if (sch.toLowerCase() == 'blue') {
this.svgStyle.fill = '#70B7FD'
}
if (sch.toLowerCase() == 'white') {
this.svgStyle.fill = '#fff'
this.gitStyle.fill = '#151513'
}
}
}
}
</script>
<style>
#github-corner .octo-arm {
transform-origin: 130px 106px
}
#github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
@keyframes octocat-wave {
0% { transform: rotate(0deg); }
20% { transform: rotate(-25deg); }
40% { transform: rotate(10deg); }
60% { transform: rotate(-25deg); }
80% { transform: rotate(10deg); }
100% { transform: rotate(0deg); }
}
#github-corner-svg {
color: #fff;
position: absolute;
top: 0;
border: 0;
}
#github-corner-svg, #github-corner-svg .octo-arm, #github-corner-svg .octo-body {
transition: fill 1s ease;
}
</style>

View File

@@ -1,5 +1,6 @@
<template> <template>
<div ref="main" class="main"> <div ref="main" class="main">
<GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F"></GithubCorner>
<div class="part top"> <div class="part top">
<span class="greeting bold-font">{{ title }}</span> <span class="greeting bold-font">{{ title }}</span>
<div class="space"></div> <div class="space"></div>
@@ -54,11 +55,14 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import Vue from 'vue';
import Component from 'vue-class-component'; import Component from 'vue-class-component';
import GithubCorner from './GithubCorner/GithubCorner.vue';
import PasteTextPage from './PasteTextPage/PasteTextPage.vue'; import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
import {versionHistory} from '../versionHistory'; import {versionHistory} from '../versionHistory';
export default @Component({ export default @Component({
components: { components: {
GithubCorner,
PasteTextPage, PasteTextPage,
}, },
}) })
@@ -108,7 +112,7 @@ class LoaderPage extends Vue {
submitUrl() { submitUrl() {
if (this.bookUrl) { if (this.bookUrl) {
this.$emit('load-book', {url: this.bookUrl}); this.$emit('load-book', {url: this.bookUrl, force: true});
this.bookUrl = ''; this.bookUrl = '';
} }
} }

View File

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

View File

@@ -91,88 +91,53 @@
</el-dialog> </el-dialog>
<el-dialog <el-dialog
title="Внимание!" title="Здравствуйте, уважаемые читатели!"
:visible.sync="migrationVisible1" :visible.sync="donationVisible"
width="90%"> width="90%">
<div> <div style="word-break: normal">
Появилась httpS-версия сайта по адресу <a href="https://omnireader.ru" target="_blank">https://omnireader.ru</a><br> Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
Работа по httpS-протоколу, помимо безопасности соединения, позволяет воспользоваться всеми возможностями
современных браузеров, а именно, применительно к нашему ресурсу: Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей.
В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый
сможет проголосовать рублем за то, чтобы читалка так и оставалась:
<ul> <ul>
<li>возможность автономной работы с читалкой (без доступа к интернету), кеширование сайта через appcache</li> <li>непрерывно улучшаемой</li>
<li>безопасная передача на сервер данных о настройках и читаемых книгах при включенной синхронизации; все данные шифруются на стороне <li>без рекламы</li>
браузера ключом доступа и никто (в т.ч. администратор) не имеет возможности их прочитать <li>без регистрации</li>
<li>использование встроенных в JS функций шифрования и других</li> <li>Open Source</li>
</ul> </ul>
Для того, чтобы перейти на новую версию с сохранением настроек и читаемых книг необходимо синхронизировать обе читалки: Автор также обращается с просьбой о помощи в распространении
<ul> <a href="https://omnireader.ru" target="_blank">ссылки</a>
<li>зайти в "Настройки"->"Профили" и поставить галочку "Включить синхронизацию с сервером"</li> <el-tooltip :open-delay="500" effect="light">
<li>там же добавить профиль устройства с любым именем для синхронизации настроек<br> <template slot="content">
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i> Скопировать
после этого все данные будут автоматически сохранены на сервер </template>
</span> <i class="el-icon-copy-document" style="cursor: pointer; font-size: 100%" @click="copyLink('https://omnireader.ru')"></i>
</li> </el-tooltip>
<li>далее нажать на кнопку "Показать ключ доступа" и кликнуть по ссылке "Ссылка для ввода ключа"<br> на читалку через тематические форумы, соцсети, мессенджеры и пр.
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i> Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
произойдет переход на https-версию читалки и откроется окно для ввода ключа
</span><br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
подтвердив ввод ключа нажатием "OK", включив синхронизацию с сервером и выбрав профиль устройства, вы восстановите все ваши настройки в новой версии
</span>
</li>
</ul>
<br><br>
Если соберется бóльшая сумма, то разработка децентрализованной библиотеки для свободного обмена книгами будет по возможности ускорена.
<br><br>
P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
Старая http-версия сайта будет доступна до конца 2019 года.<br> <br><br>
Приносим извинения за доставленные неудобства. <el-row type="flex" justify="center">
<el-button type="success" round @click="openDonate">Помочь проекту</el-button>
</el-row>
</div> </div>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="migrationDialogDisable">Больше не показывать</el-button> <span class="clickable" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
<el-button @click="migrationDialogRemind">Напомнить позже</el-button> <br><br>
</span> <el-button @click="donationDialogRemind">Напомнить позже</el-button>
</el-dialog>
<el-dialog
title="Внимание!"
:visible.sync="migrationVisible2"
width="90%">
<div>
Информация для пользователей старой версии читалки по адресу <a href="http://omnireader.ru" target="_blank">http://omnireader.ru</a><br>
Для того, чтобы перейти на новую httpS-версию с сохранением настроек и читаемых книг необходимо синхронизировать обе читалки:
<ul>
<li>перейти на старую версию ресурса <a href="http://omnireader.ru" target="_blank">http://omnireader.ru</a></li>
<li>зайти в "Настройки"->"Профили" и поставить галочку "Включить синхронизацию с сервером"</li>
<li>там же добавить профиль устройства с любым именем для синхронизации настроек<br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
после этого все данные будут автоматически сохранены на сервер
</span>
</li>
<li>далее нажать на кнопку "Показать ключ доступа" и кликнуть по ссылке "Ссылка для ввода ключа"<br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
произойдет переход на https-версию читалки и откроется окно для ввода ключа
</span><br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
подтвердив ввод ключа нажатием "OK", включив синхронизацию с сервером и выбрав профиль устройства, вы восстановите все ваши настройки в новой версии
</span>
</li>
</ul>
Старая http-версия сайта будет доступна до конца 2019 года.<br>
Приносим извинения за доставленные неудобства.
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="migrationDialogDisable">Больше не показывать</el-button>
<el-button @click="migrationDialogRemind">Напомнить позже</el-button>
</span> </span>
</el-dialog> </el-dialog>
</el-main> </el-main>
</el-container> </el-container>
</template> </template>
@@ -282,8 +247,7 @@ class Reader extends Vue {
whatsNewVisible = false; whatsNewVisible = false;
whatsNewContent = ''; whatsNewContent = '';
migrationVisible1 = false; donationVisible = false;
migrationVisible2 = false;
created() { created() {
this.loading = true; this.loading = true;
@@ -320,15 +284,6 @@ class Reader extends Vue {
}); });
this.loadSettings(); this.loadSettings();
//TODO: убрать в будущем
if (this.showToolButton['history']) {
const newShowToolButton = Object.assign({}, this.showToolButton);
newShowToolButton['recentBooks'] = true;
delete newShowToolButton['history'];
const newSettings = Object.assign({}, this.settings, { showToolButton: newShowToolButton });
this.commit('reader/setSettings', newSettings);
}
} }
mounted() { mounted() {
@@ -351,10 +306,10 @@ class Reader extends Vue {
this.checkActivateDonateHelpPage(); this.checkActivateDonateHelpPage();
this.loading = false; this.loading = false;
await this.showWhatsNew();
await this.showMigration();
this.updateRoute(); this.updateRoute();
await this.showWhatsNew();
await this.showDonation();
})(); })();
} }
@@ -366,7 +321,7 @@ class Reader extends Vue {
this.clickControl = settings.clickControl; this.clickControl = settings.clickControl;
this.blinkCachedLoad = settings.blinkCachedLoad; this.blinkCachedLoad = settings.blinkCachedLoad;
this.showWhatsNewDialog = settings.showWhatsNewDialog; this.showWhatsNewDialog = settings.showWhatsNewDialog;
this.showMigrationDialog = settings.showMigrationDialog; this.showDonationDialog2020 = settings.showDonationDialog2020;
this.showToolButton = settings.showToolButton; this.showToolButton = settings.showToolButton;
this.enableSitesFilter = settings.enableSitesFilter; this.enableSitesFilter = settings.enableSitesFilter;
@@ -432,31 +387,39 @@ class Reader extends Vue {
} }
} }
async showMigration() { async showDonation() {
await utils.sleep(3000); await utils.sleep(3000);
if (!this.settingsActive && const today = utils.formatDate(new Date(), 'coDate');
this.mode == 'omnireader' && this.showMigrationDialog && this.migrationRemindDate != utils.formatDate(new Date(), 'coDate')) {
if (window.location.protocol == 'http:') { if (this.mode == 'omnireader' && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
this.migrationVisible1 = true; this.donationVisible = true;
} else if (window.location.protocol == 'https:') {
this.migrationVisible2 = true;
}
} }
} }
migrationDialogDisable() { donationDialogDisable() {
this.migrationVisible1 = false; this.donationVisible = false;
this.migrationVisible2 = false; if (this.showDonationDialog2020) {
if (this.showMigrationDialog) { const newSettings = Object.assign({}, this.settings, { showDonationDialog2020: false });
const newSettings = Object.assign({}, this.settings, { showMigrationDialog: false });
this.commit('reader/setSettings', newSettings); this.commit('reader/setSettings', newSettings);
} }
} }
migrationDialogRemind() { donationDialogRemind() {
this.migrationVisible1 = false; this.donationVisible = false;
this.migrationVisible2 = false; this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate'));
this.commit('reader/setMigrationRemindDate', utils.formatDate(new Date(), 'coDate')); }
openDonate() {
this.donationVisible = false;
this.donateToggle();
}
async copyLink(link) {
const result = await utils.copyTextToClipboard(link);
if (result)
this.$notify.success({message: `Ссылка ${link} успешно скопирована в буфер обмена`});
else
this.$notify.error({message: 'Копирование не удалось'});
} }
openVersionHistory() { openVersionHistory() {
@@ -577,8 +540,8 @@ class Reader extends Vue {
return this.$store.state.reader.whatsNewContentHash; return this.$store.state.reader.whatsNewContentHash;
} }
get migrationRemindDate() { get donationRemindDate() {
return this.$store.state.reader.migrationRemindDate; return this.$store.state.reader.donationRemindDate;
} }
addAction(pos) { addAction(pos) {
@@ -845,15 +808,16 @@ class Reader extends Vue {
case 'scrolling': case 'scrolling':
case 'search': case 'search':
case 'copyText': case 'copyText':
case 'recentBooks': case 'refresh':
case 'offlineMode': case 'offlineMode':
case 'recentBooks':
case 'settings': case 'settings':
if (this[`${button}Active`]) if (this.progressActive) {
classResult = classDisabled;
} else if (this[`${button}Active`]) {
classResult = classActive; classResult = classActive;
break;
} }
break;
switch (button) {
case 'undoAction': case 'undoAction':
if (this.actionCur <= 0) if (this.actionCur <= 0)
classResult = classDisabled; classResult = classDisabled;
@@ -950,7 +914,8 @@ class Reader extends Vue {
return; return;
} }
let url = opts.url; let url = encodeURI(decodeURI(opts.url));
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) && if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
(url.indexOf('file://') != 0)) (url.indexOf('file://') != 0))
url = 'http://' + url; url = 'http://' + url;

View File

@@ -5,6 +5,7 @@
<span v-show="loading"><i class="el-icon-loading" style="font-size: 25px"></i> <span style="position: relative; top: -4px">Список загружается</span></span> <span v-show="loading"><i class="el-icon-loading" style="font-size: 25px"></i> <span style="position: relative; top: -4px">Список загружается</span></span>
</template> </template>
<a ref="download" style='display: none;'></a>
<el-table <el-table
:data="tableData" :data="tableData"
style="width: 570px" style="width: 570px"
@@ -72,7 +73,7 @@
> >
<template slot-scope="scope"> <template slot-scope="scope">
<a v-show="isUrl(scope.row.url)" :href="scope.row.url" target="_blank">Оригинал</a><br> <a v-show="isUrl(scope.row.url)" :href="scope.row.url" target="_blank">Оригинал</a><br>
<a :href="scope.row.path" :download="getFileNameFromPath(scope.row.path)">Скачать FB2</a> <a :href="scope.row.path" @click.prevent="downloadBook(scope.row.path)">Скачать FB2</a>
</template> </template>
</el-table-column> </el-table-column>
@@ -104,6 +105,7 @@ import _ from 'lodash';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import bookManager from '../share/bookManager'; import bookManager from '../share/bookManager';
import readerApi from '../../../api/reader';
export default @Component({ export default @Component({
components: { components: {
@@ -209,7 +211,7 @@ class RecentBooksPage extends Vue {
a.middleName a.middleName
]).join(' ')); ]).join(' '));
author = authorNames.join(', '); author = authorNames.join(', ');
} else { } else {//TODO: убрать в будущем
author = _.compact([ author = _.compact([
fb2.lastName, fb2.lastName,
fb2.firstName, fb2.firstName,
@@ -268,8 +270,20 @@ class RecentBooksPage extends Vue {
return result; return result;
} }
getFileNameFromPath(fb2Path) { async downloadBook(fb2path) {
return path.basename(fb2Path).substr(0, 10) + '.fb2'; try {
await readerApi.checkCachedBook(fb2path);
const d = this.$refs.download;
d.href = fb2path;
d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
d.click();
} catch (e) {
let errMes = e.message;
if (errMes.indexOf('404') >= 0)
errMes = 'Файл не найден на сервере (возможно был удален как устаревший)';
this.$alert(errMes, 'Ошибка', {type: 'error'});
}
} }
openOriginal(url) { openOriginal(url) {

View File

@@ -474,15 +474,15 @@
<el-form-item label="Уведомление"> <el-form-item label="Уведомление">
<el-tooltip :open-delay="500" effect="light"> <el-tooltip :open-delay="500" effect="light">
<template slot="content"> <template slot="content">
Показывать диалог о переходе на httpS-версию Показывать уведомление "Оплатим хостинг вместе"
</template> </template>
<el-checkbox v-model="showMigrationDialog">Уведомлять о переходе на httpS-версию</el-checkbox> <el-checkbox v-model="showDonationDialog2020">Показывать "Оплатим хостинг вместе"</el-checkbox>
</el-tooltip> </el-tooltip>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-form :model="form" size="mini" label-width="120px" @submit.native.prevent> <el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
<div class="partHeader">Прочее</div> <div class="partHeader">Другое</div>
<el-form-item label="Парам. в URL"> <el-form-item label="Парам. в URL">
<el-tooltip :open-delay="500" effect="light"> <el-tooltip :open-delay="500" effect="light">

View File

@@ -225,30 +225,23 @@ class TextPage extends Vue {
//scrolling page //scrolling page
const pageSpace = this.scrollHeight - this.pageLineCount*this.lineHeight; const pageSpace = this.scrollHeight - this.pageLineCount*this.lineHeight;
let y = pageSpace/2; let top = pageSpace/2;
if (this.showStatusBar) if (this.showStatusBar)
y += this.statusBarHeight*(this.statusBarTop ? 1 : 0); top += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
let page1 = this.$refs.scrollBox1; let page1 = this.$refs.scrollBox1.style;
let page2 = this.$refs.scrollBox2; let page2 = this.$refs.scrollBox2.style;
page1.style.perspective = '3072px'; page1.perspective = page2.perspective = '3072px';
page2.style.perspective = '3072px';
page1.style.width = this.w + this.indentLR + 'px'; page1.width = page2.width = this.w + this.indentLR + 'px';
page2.style.width = this.w + this.indentLR + 'px'; page1.height = page2.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
page1.style.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px'; page1.top = page2.top = top + 'px';
page2.style.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px'; page1.left = page2.left = this.indentLR + 'px';
page1.style.top = y + 'px';
page2.style.top = y + 'px';
page1.style.left = this.indentLR + 'px';
page2.style.left = this.indentLR + 'px';
page1 = this.$refs.scrollingPage1; page1 = this.$refs.scrollingPage1.style;
page2 = this.$refs.scrollingPage2; page2 = this.$refs.scrollingPage2.style;
page1.style.width = this.w + this.indentLR + 'px'; page1.width = page2.width = this.w + this.indentLR + 'px';
page2.style.width = this.w + this.indentLR + 'px'; page1.height = page2.height = this.scrollHeight + this.lineHeight + 'px';
page1.style.height = this.scrollHeight + this.lineHeight + 'px';
page2.style.height = this.scrollHeight + this.lineHeight + 'px';
} }
async checkLoadedFonts() { async checkLoadedFonts() {
@@ -334,15 +327,17 @@ class TextPage extends Vue {
this.draw(); this.draw();
// шрифты хрен знает когда подгружаются в div, поэтому // ширина шрифта некоторое время выдается неверно, поэтому
if (!omitLoadFonts) {
const parsed = this.parsed; const parsed = this.parsed;
await sleep(5000); await sleep(100);
if (this.parsed === parsed) { if (this.parsed === parsed) {
parsed.force = true; parsed.force = true;
this.draw(); this.draw();
parsed.force = false; parsed.force = false;
} }
} }
}
loadSettings() { loadSettings() {
(async() => { (async() => {

View File

@@ -32,9 +32,6 @@ export default class BookParser {
//defaults //defaults
let fb2 = { let fb2 = {
firstName: '',
middleName: '',
lastName: '',
bookTitle: '', bookTitle: '',
}; };

View File

@@ -319,7 +319,6 @@ class BookManager {
metaOnly(book) { metaOnly(book) {
let result = Object.assign({}, book); let result = Object.assign({}, book);
delete result.data;//можно будет убрать эту строку со временем
delete result.parsed; delete result.parsed;
return result; return result;
} }

View File

@@ -1,4 +1,63 @@
export const versionHistory = [ export const versionHistory = [
{
showUntil: '2020-01-27',
header: '0.8.3 (2020-01-28)',
content:
`
<ul>
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
<li>внутренние оптимизации</li>
</ul>
`
},
{
showUntil: '2020-01-19',
header: '0.8.2 (2020-01-20)',
content:
`
<ul>
<li>внутренние оптимизации</li>
</ul>
`
},
{
showUntil: '2020-01-06',
header: '0.8.1 (2020-01-07)',
content:
`
<ul>
<li>добавлена частичная поддержка формата FB3</li>
<li>исправлен баг "Request path contains unescaped characters"</li>
</ul>
`
},
{
showUntil: '2020-01-05',
header: '0.8.0 (2020-01-02)',
content:
`
<ul>
<li>окончательный переход на https</li>
<li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li>
</ul>
`
},
{
showUntil: '2019-11-26',
header: '0.7.9 (2019-11-27)',
content:
`
<ul>
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
<li>исправления багов</li>
</ul>
`
},
{ {
showUntil: '2019-11-24', showUntil: '2019-11-24',
header: '0.7.8 (2019-11-25)', header: '0.7.8 (2019-11-25)',
@@ -6,6 +65,7 @@ export const versionHistory = [
` `
<ul> <ul>
<li>улучшение html-фильтров для сайтов</li> <li>улучшение html-фильтров для сайтов</li>
<li>исправления багов</li>
</ul> </ul>
` `
}, },

View File

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

View File

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

BIN
docs/assets/face.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
docs/assets/reader.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

84
docs/omnireader/README.md Normal file
View File

@@ -0,0 +1,84 @@
## Разворачивание сервера OmniReader в Ubuntu:
### git, clone
```
sudo apt install ssh git
git clone https://github.com/bookpauk/liberama
```
### node.js
```
sudo apt install -y curl
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt install -y nodejs
```
### install packages
```
cd liberama
npm i
```
### create public dir
```
sudo mkdir /home/liberama
sudo chown www-data.www-data /home/liberama
```
### external converter `calibre`, download from https://download.calibre-ebook.com/
```
wget "https://download.calibre-ebook.com/3.39.1/calibre-3.39.1-x86_64.txz"
sudo -u www-data mkdir -p /home/liberama/data/calibre
sudo -u www-data tar xvf calibre-3.39.1-x86_64.txz -C /home/liberama/data/calibre
```
### external converters
```
sudo apt install libreoffice
sudo apt install poppler-utils
```
### nginx, server config
Для своего домена необходимо будет подправить docs/omnireader/omnireader.
Можно также настроить сервер для HTTP, без SSL.
```
sudo apt install nginx
sudo cp docs/omnireader/omnireader /etc/nginx/sites-available/omnireader
sudo ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader
sudo rm /etc/nginx/sites-enabled/default
sudo service nginx reload
sudo chown -R www-data.www-data /var/www
```
### certbot
Следовать инструкции установки certbot https://certbot.eff.org/lets-encrypt/ubuntubionic-nginx
### old.omnireader
```
sudo apt install php7.2 php7.2-curl php7.2-mbstring php7.2-fpm
sudo service php7.2-fpm restart
sudo mkdir /home/oldreader
sudo chown www-data.www-data /home/oldreader
sudo -u www-data cp -r docs/omnireader/old/* /home/oldreader
```
## Деплой и запуск
```
cd docs/omnireader
./deploy.sh
./run_server.sh
```
После первого запуска будет создан конфигурационный файл `/home/liberama/data/config.json`.
Необходимо переключить приложение в режим `omnireader`, отредактировав опцию `servers`:
```
"servers": [
{
"serverName": "1",
"mode": "omnireader",
"ip": "0.0.0.0",
"port": "44081"
}
]
```
и перезапустить `run_server.sh`

View File

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

View File

@@ -8,6 +8,7 @@ server {
server_name omnireader.ru; server_name omnireader.ru;
client_max_body_size 50m; client_max_body_size 50m;
proxy_read_timeout 1h;
gzip on; gzip on;
gzip_min_length 1024; gzip_min_length 1024;
@@ -18,6 +19,13 @@ server {
proxy_pass http://127.0.0.1:44081; proxy_pass http://127.0.0.1:44081;
} }
location /ws {
proxy_pass http://127.0.0.1:44081;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / { location / {
root /home/liberama/public; root /home/liberama/public;
@@ -36,26 +44,7 @@ server {
listen 80; listen 80;
server_name omnireader.ru; server_name omnireader.ru;
client_max_body_size 50m; return 301 https://$host$request_uri;
gzip on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location /api {
proxy_pass http://127.0.0.1:44081;
}
location /tmp {
root /home/liberama/public;
add_header Content-Type text/xml;
add_header Content-Encoding gzip;
}
location / {
root /home/liberama/public;
}
} }
server { server {

View File

@@ -0,0 +1,59 @@
server {
listen 80;
server_name omnireader.ru;
client_max_body_size 50m;
proxy_read_timeout 1h;
gzip on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location /api {
proxy_pass http://127.0.0.1:44081;
}
location /ws {
proxy_pass http://127.0.0.1:44081;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / {
root /home/liberama/public;
location /tmp {
add_header Content-Type text/xml;
add_header Content-Encoding gzip;
}
location ~* \.(?:manifest|appcache|html)$ {
expires -1;
}
}
}
server {
listen 80;
server_name old.omnireader.ru;
client_max_body_size 50m;
gzip on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
root /home/oldreader;
index index.html;
# Обработка php файлов с помощью fpm
location ~ \.php$ {
try_files $uri =404;
include /etc/nginx/fastcgi.conf;
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}
}

View File

@@ -1,39 +0,0 @@
sudo bash
mkdir /home/liberama
chown www-data.www-data /home/liberama
### oldreader
# ubuntu 18
apt install php7.2 php7.2-curl php7.2-mbstring php7.2-fpm
service php7.2-fpm restart
mkdir /home/oldreader
chown www-data /home/oldreader
chgrp www-data /home/oldreader
sudo -u www-data cp -r ./old/* /home/oldreader
###
### external converter
# calibre releases https://download.calibre-ebook.com/
# download, unpack to data/calibre
# 3.39.1
wget "https://download.calibre-ebook.com/3.39.1/calibre-3.39.1-x86_64.txz"
sudo -u www-data mkdir -p /home/liberama/data/calibre
sudo -u www-data tar xvf calibre-3.39.1-x86_64.txz -C /home/liberama/data/calibre
apt install libreoffice
apt install poppler-utils
###
apt install nginx
cp omnireader /etc/nginx/sites-available/omnireader
ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader
rm /etc/nginx/sites-enabled/default
service nginx reload
chown -R www-data.www-data /var/www
exit

View File

@@ -1 +1,3 @@
#!/bin/sh
sudo -H -u www-data sh -c "cd /var/www; /home/liberama/liberama" sudo -H -u www-data sh -c "cd /var/www; /home/liberama/liberama"

View File

@@ -1,12 +0,0 @@
# Разворачивание среды:
# GIT REPO
sudo apt install ssh git
git clone
#nodejs
sudo apt install -y curl
curl -sL https://deb.nodesource.com/setup_10.x | sudo bash -
sudo apt install -y nodejs
npm i

936
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,9 @@
{ {
"name": "Liberama", "name": "Liberama",
"version": "0.7.8", "version": "0.8.3",
"author": "Book Pauk <bookpauk@gmail.com>",
"license": "CC0-1.0",
"repository": "bookpauk/liberama",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },
@@ -27,21 +30,18 @@
"babel-plugin-transform-decorators-legacy": "^1.3.5", "babel-plugin-transform-decorators-legacy": "^1.3.5",
"babel-preset-env": "^1.3.2", "babel-preset-env": "^1.3.2",
"clean-webpack-plugin": "^1.0.1", "clean-webpack-plugin": "^1.0.1",
"copy-webpack-plugin": "^4.6.0", "copy-webpack-plugin": "^5.1.1",
"css-loader": "^1.0.0", "css-loader": "^1.0.0",
"disable-output-webpack-plugin": "^1.0.1",
"element-theme-chalk": "^2.12.0", "element-theme-chalk": "^2.12.0",
"eslint": "^5.16.0", "eslint": "^5.16.0",
"eslint-plugin-html": "^5.0.5", "eslint-plugin-html": "^5.0.5",
"eslint-plugin-node": "^8.0.0", "eslint-plugin-node": "^8.0.0",
"eslint-plugin-vue": "^5.2.3", "eslint-plugin-vue": "^5.2.3",
"event-hooks-webpack-plugin": "^2.1.4",
"file-loader": "^3.0.1", "file-loader": "^3.0.1",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.5.0", "mini-css-extract-plugin": "^0.5.0",
"null-loader": "^0.1.1",
"optimize-css-assets-webpack-plugin": "^5.0.3", "optimize-css-assets-webpack-plugin": "^5.0.3",
"pkg": "4.3.7", "pkg": "^4.4.2",
"terser-webpack-plugin": "^1.4.1", "terser-webpack-plugin": "^1.4.1",
"url-loader": "^1.1.2", "url-loader": "^1.1.2",
"vue-class-component": "^6.3.2", "vue-class-component": "^6.3.2",
@@ -77,13 +77,15 @@
"safe-buffer": "^5.2.0", "safe-buffer": "^5.2.0",
"sjcl": "^1.0.8", "sjcl": "^1.0.8",
"sql-template-strings": "^2.2.2", "sql-template-strings": "^2.2.2",
"sqlite": "3.0.0", "sqlite": "^3.0.3",
"tar-fs": "^2.0.0", "tar-fs": "^2.0.0",
"unbzip2-stream": "^1.3.3", "unbzip2-stream": "^1.3.3",
"vue": "github:paulkamer/vue#fix_palemoon_clickhandlers_dist", "vue": "github:paulkamer/vue#fix_palemoon_clickhandlers_dist",
"vue-router": "^3.1.3", "vue-router": "^3.1.3",
"vuex": "^3.1.1", "vuex": "^3.1.1",
"vuex-persistedstate": "^2.5.4", "vuex-persistedstate": "^2.5.4",
"webdav": "^2.10.1",
"ws": "^7.2.1",
"zip-stream": "^2.1.2" "zip-stream": "^2.1.2"
} }
} }

View File

@@ -45,5 +45,14 @@ module.exports = {
}, },
], ],
remoteWebDavStorage: false,
/*
remoteWebDavStorage: {
url: '127.0.0.1:1900',
username: '',
password: '',
},
*/
}; };

View File

@@ -10,6 +10,7 @@ const propsToSave = [
'useExternalBookConverter', 'useExternalBookConverter',
'servers', 'servers',
'remoteWebDavStorage',
]; ];
let instance = null; let instance = null;
@@ -41,9 +42,9 @@ class ConfigManager {
process.env.NODE_ENV = this.branch; process.env.NODE_ENV = this.branch;
this.branchConfigFile = __dirname + `/${this.branch}.js`; this.branchConfigFile = __dirname + `/${this.branch}.js`;
await fs.access(this.branchConfigFile);
this._config = require(this.branchConfigFile); this._config = require(this.branchConfigFile);
await fs.ensureDir(this._config.dataDir);
this._userConfigFile = `${this._config.dataDir}/config.json`; this._userConfigFile = `${this._config.dataDir}/config.json`;
this.inited = true; this.inited = true;
@@ -83,6 +84,7 @@ class ConfigManager {
async save() { async save() {
if (!this.inited) if (!this.inited)
throw new Error('not inited'); throw new Error('not inited');
const dataToSave = _.pick(this._config, propsToSave); const dataToSave = _.pick(this._config, propsToSave);
await fs.writeFile(this.userConfigFile, JSON.stringify(dataToSave, null, 4)); await fs.writeFile(this.userConfigFile, JSON.stringify(dataToSave, null, 4));
} }

View File

@@ -62,6 +62,24 @@ class ReaderController extends BaseController {
res.status(400).send({error}); res.status(400).send({error});
return false; 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; module.exports = ReaderController;

View File

@@ -0,0 +1,164 @@
const WebSocket = require ('ws');
const _ = require('lodash');
const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton
const ReaderStorage = require('../core/Reader/ReaderStorage');//singleton
const WorkerState = require('../core/WorkerState');//singleton
const log = new (require('../core/AppLogger'))().log;//singleton
const utils = require('../core/utils');
const cleanPeriod = 1*60*1000;//1 минута
const closeSocketOnIdle = 5*60*1000;//5 минут
class WebSocketController {
constructor(wss, config) {
this.config = config;
this.isDevelopment = (config.branch == 'development');
this.readerStorage = new ReaderStorage();
this.readerWorker = new ReaderWorker(config);
this.workerState = new WorkerState();
this.wss = wss;
wss.on('connection', (ws) => {
ws.on('message', (message) => {
this.onMessage(ws, message);
});
});
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)}`);
}
ws.lastActivity = Date.now();
req = JSON.parse(message);
switch (req.action) {
case 'test':
await this.test(req, ws); break;
case 'get-config':
await this.getConfig(req, ws); break;
case 'worker-get-state':
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;
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);
}
async getConfig(req, ws) {
if (Array.isArray(req.params)) {
this.send(_.pick(this.config, req.params), req, ws);
} else {
throw new Error('params is not an array');
}
}
async workerGetState(req, ws) {
if (!req.workerId)
throw new Error(`key 'workerId' is wrong`);
const state = this.workerState.getState(req.workerId);
this.send((state ? state : {}), req, ws);
}
async workerGetStateFinish(req, ws) {
if (!req.workerId)
throw new Error(`key 'workerId' is wrong`);
const refreshPause = 200;
let i = 0;
let state = {};
while (1) {// eslint-disable-line no-constant-condition
const prevProgress = state.progress || -1;
const prevState = state.state || '';
state = this.workerState.getState(req.workerId);
this.send((state ? state : {}), req, ws);
if (!state) break;
if (state.state != 'finish' && state.state != 'error')
await utils.sleep(refreshPause);
else
break;
i++;
if (i > 2*60*1000/refreshPause) {//2 мин ждем телодвижений воркера
this.send({state: 'error', error: 'Время ожидания процесса истекло'}, req, ws);
}
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
}
}
async readerRestoreCachedFile(req, ws) {
if (!req.path)
throw new Error(`key 'path' is empty`);
const workerId = this.readerWorker.restoreCachedFile(req.path);
const state = this.workerState.getState(workerId);
this.send((state ? state : {}), req, ws);
}
async readerStorageDo(req, ws) {
if (!req.body)
throw new Error(`key 'body' is empty`);
if (!req.body.action)
throw new Error(`key 'action' is empty`);
if (!req.body.items || Array.isArray(req.body.data))
throw new Error(`key 'items' is empty`);
this.send(await this.readerStorage.doAction(req.body), req, ws);
}
}
module.exports = WebSocketController;

View File

@@ -1,5 +1,6 @@
const BaseController = require('./BaseController'); const BaseController = require('./BaseController');
const WorkerState = require('../core/WorkerState');//singleton const WorkerState = require('../core/WorkerState');//singleton
const utils = require('../core/utils');
class WorkerController extends BaseController { class WorkerController extends BaseController {
constructor(config) { constructor(config) {
@@ -15,6 +16,7 @@ class WorkerController extends BaseController {
throw new Error(`key 'workerId' is wrong`); throw new Error(`key 'workerId' is wrong`);
const state = this.workerState.getState(request.workerId); const state = this.workerState.getState(request.workerId);
return (state ? state : {}); return (state ? state : {});
} catch (e) { } catch (e) {
error = e.message; error = e.message;
@@ -23,6 +25,60 @@ class WorkerController extends BaseController {
res.status(400).send({error}); res.status(400).send({error});
return false; return false;
} }
//TODO: удалить бесполезную getStateFinish
async getStateFinish(req, res) {
const request = req.body;
let error = '';
try {
if (!request.workerId)
throw new Error(`key 'workerId' is wrong`);
res.writeHead(200, {
'Content-Type': 'text/json; charset=utf-8',
});
const splitter = '-- aod2t5hDXU32bUFyqlFE next status --';
const refreshPause = 200;
let i = 0;
let prevProgress = -1;
let prevState = '';
let state;
while (1) {// eslint-disable-line no-constant-condition
state = this.workerState.getState(request.workerId);
if (!state) break;
res.write(splitter + JSON.stringify(state));
res.flush();
if (state.state != 'finish' && state.state != 'error')
await utils.sleep(refreshPause);
else
break;
i++;
if (i > 2*60*1000/refreshPause) {//2 мин ждем телодвижений воркера
res.write(splitter + JSON.stringify({state: 'error', error: 'Слишком долгое время ожидания'}));
break;
}
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
prevProgress = state.progress;
prevState = state.state;
}
if (!state) {
res.write(splitter + JSON.stringify({}));
}
res.end();
return false;
} catch (e) {
error = e.message;
}
//bad request
res.status(400).send({error});
return false;
}
} }
module.exports = WorkerController; module.exports = WorkerController;

View File

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

View File

@@ -5,12 +5,14 @@ const unbzip2Stream = require('unbzip2-stream');
const tar = require('tar-fs'); const tar = require('tar-fs');
const ZipStreamer = require('./ZipStreamer'); const ZipStreamer = require('./ZipStreamer');
const appLogger = new (require('./AppLogger'))();//singleton
const utils = require('./utils'); const utils = require('./utils');
const FileDetector = require('./FileDetector'); const FileDetector = require('./FileDetector');
class FileDecompressor { class FileDecompressor {
constructor() { constructor(limitFileSize = 0) {
this.detector = new FileDetector(); this.detector = new FileDetector();
this.limitFileSize = limitFileSize;
} }
async decompressNested(filename, outputDir) { async decompressNested(filename, outputDir) {
@@ -112,7 +114,7 @@ class FileDecompressor {
async unZip(filename, outputDir) { async unZip(filename, outputDir) {
const zip = new ZipStreamer(); const zip = new ZipStreamer();
return await zip.unpack(filename, outputDir); return await zip.unpack(filename, outputDir, null, this.limitFileSize);
} }
unBz2(filename, outputDir) { unBz2(filename, outputDir) {
@@ -124,9 +126,16 @@ class FileDecompressor {
} }
unTar(filename, outputDir) { unTar(filename, outputDir) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => { (async() => {
const files = []; const files = [];
if (this.limitFileSize) {
if ((await fs.stat(filename)).size > this.limitFileSize) {
reject('Файл слишком большой');
return;
}
}
const tarExtract = tar.extract(outputDir, { const tarExtract = tar.extract(outputDir, {
map: (header) => { map: (header) => {
files.push({path: header.name, size: header.size}); files.push({path: header.name, size: header.size});
@@ -148,7 +157,7 @@ class FileDecompressor {
}); });
inputStream.pipe(tarExtract); inputStream.pipe(tarExtract);
}); })().catch(reject); });
} }
decompressByStream(stream, filename, outputDir) { decompressByStream(stream, filename, outputDir) {
@@ -173,6 +182,16 @@ class FileDecompressor {
}); });
stream.on('error', reject); stream.on('error', reject);
if (this.limitFileSize) {
let readSize = 0;
stream.on('data', (buffer) => {
readSize += buffer.length;
if (readSize > this.limitFileSize)
stream.destroy(new Error('Файл слишком большой'));
});
}
inputStream.on('error', reject); inputStream.on('error', reject);
outputStream.on('error', reject); outputStream.on('error', reject);
@@ -189,9 +208,9 @@ class FileDecompressor {
}); });
} }
async gzipFile(inputFile, outputFile) { async gzipFile(inputFile, outputFile, level = 1) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const gzip = zlib.createGzip({level: 1}); const gzip = zlib.createGzip({level});
const input = fs.createReadStream(inputFile); const input = fs.createReadStream(inputFile);
const output = fs.createWriteStream(outputFile); const output = fs.createWriteStream(outputFile);
@@ -208,7 +227,21 @@ class FileDecompressor {
const outFilename = `${outDir}/${hash}`; const outFilename = `${outDir}/${hash}`;
if (!await fs.pathExists(outFilename)) { if (!await fs.pathExists(outFilename)) {
await this.gzipFile(filename, outFilename); await this.gzipFile(filename, outFilename, 1);
// переупакуем через некоторое время на максималках
const filenameCopy = `${filename}.copy`;
await fs.copy(filename, filenameCopy);
(async() => {
await utils.sleep(5000);
const filenameGZ = `${filename}.gz`;
await this.gzipFile(filenameCopy, filenameGZ, 9);
await fs.move(filenameGZ, outFilename, {overwrite: true});
await fs.remove(filenameCopy);
})().catch((e) => { if (appLogger.inited) appLogger.log(LM_ERR, `FileDecompressor.gzipFileIfNotExists: ${e.message}`) });
} else { } else {
await utils.touchFile(outFilename); await utils.touchFile(outFilename);
} }

View File

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

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

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

View File

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

View File

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

View File

@@ -4,14 +4,15 @@ const path = require('path');
const ConvertBase = require('./ConvertBase'); const ConvertBase = require('./ConvertBase');
class ConvertDocX extends ConvertBase { class ConvertDocX extends ConvertBase {
check(data, opts) { async check(data, opts) {
const {inputFiles} = opts; const {inputFiles} = opts;
if (this.config.useExternalBookConverter && if (this.config.useExternalBookConverter &&
inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'zip') { inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'zip') {
//ищем файл '[Content_Types].xml' //ищем файл '[Content_Types].xml'
for (const file of inputFiles.files) { for (const file of inputFiles.files) {
if (file.path == '[Content_Types].xml') { if (file.path == '[Content_Types].xml') {
return true; const contentTypes = await fs.readFile(`${inputFiles.filesDir}/${file.path}`, 'utf8');
return contentTypes.indexOf('/word/document.xml') >= 0;
} }
} }
} }
@@ -19,22 +20,22 @@ class ConvertDocX extends ConvertBase {
return false; return false;
} }
async convert(docxFile, fb2File, callback) { async convert(docxFile, fb2File, callback, abort) {
let perc = 0; let perc = 0;
await this.execConverter(this.calibrePath, [docxFile, fb2File], () => { await this.execConverter(this.calibrePath, [docxFile, fb2File, '-vv'], () => {
perc = (perc < 100 ? perc + 5 : 50); perc = (perc < 100 ? perc + 1 : 50);
callback(perc); callback(perc);
}); }, abort);
return await fs.readFile(fb2File); return await fs.readFile(fb2File);
} }
async run(data, opts) { async run(data, opts) {
if (!this.check(data, opts)) if (!(await this.check(data, opts)))
return false; return false;
await this.checkExternalConverterPresent(); await this.checkExternalConverterPresent();
const {inputFiles, callback} = opts; const {inputFiles, callback, abort} = opts;
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`; const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
const docxFile = `${outFile}.docx`; const docxFile = `${outFile}.docx`;
@@ -42,7 +43,7 @@ class ConvertDocX extends ConvertBase {
await fs.copy(inputFiles.sourceFile, docxFile); await fs.copy(inputFiles.sourceFile, docxFile);
return await this.convert(docxFile, fb2File, callback); return await this.convert(docxFile, fb2File, callback, abort);
} }
} }

View File

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

View File

@@ -23,7 +23,7 @@ class ConvertFb2 extends ConvertBase {
const right = data.indexOf('?>', left); const right = data.indexOf('?>', left);
if (right >= 0) { if (right >= 0) {
const head = data.slice(left, right + 2).toString(); const head = data.slice(left, right + 2).toString();
const m = head.match(/encoding="(.*)"/); const m = head.match(/encoding="(.*?)"/);
if (m) { if (m) {
let encoding = m[1].toLowerCase(); let encoding = m[1].toLowerCase();
if (encoding != 'utf-8') { if (encoding != 'utf-8') {

View File

@@ -0,0 +1,51 @@
const fs = require('fs-extra');
const ConvertHtml = require('./ConvertHtml');
class ConvertDocX extends ConvertHtml {
async check(data, opts) {
const {inputFiles} = opts;
if (this.config.useExternalBookConverter &&
inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'zip') {
//ищем файл '[Content_Types].xml'
for (const file of inputFiles.files) {
if (file.path == '[Content_Types].xml') {
const contentTypes = await fs.readFile(`${inputFiles.filesDir}/${file.path}`, 'utf8');
return contentTypes.indexOf('/fb3/body.xml') >= 0;
}
}
}
return false;
}
getTitle(text) {
let title = '';
const m = text.match(/<title>([\s\S]*?)<\/title>/);
if (m)
title = m[1];
return title.trim();
}
async run(data, opts) {
if (!(await this.check(data, opts)))
return false;
const {inputFiles} = opts;
let text = await fs.readFile(`${inputFiles.filesDir}/fb3/body.xml`, 'utf8');
const title = this.getTitle(text)
.replace(/<\/?p>/g, '')
;
text = `<title>${title}</title>` + text
.replace(/<title>/g, '<br><b>')
.replace(/<\/title>/g, '</b><br>')
.replace(/<subtitle>/g, '<br><br><subtitle>')
;
return await super.run(Buffer.from(text), {skipCheck: true, cutTitle: true});
}
}
module.exports = ConvertDocX;

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ const convertClassFactory = [
require('./ConvertPdf'), require('./ConvertPdf'),
require('./ConvertRtf'), require('./ConvertRtf'),
require('./ConvertDocX'), require('./ConvertDocX'),
require('./ConvertFb3'),
require('./ConvertDoc'), require('./ConvertDoc'),
require('./ConvertMobi'), require('./ConvertMobi'),
require('./ConvertFb2'), require('./ConvertFb2'),
@@ -25,11 +26,14 @@ class BookConverter {
} }
} }
async convertToFb2(inputFiles, outputFile, opts, callback) { async convertToFb2(inputFiles, outputFile, opts, callback, abort) {
if (abort && abort())
throw new Error('abort');
const selectedFileType = await this.detector.detectFile(inputFiles.selectedFile); const selectedFileType = await this.detector.detectFile(inputFiles.selectedFile);
const data = await fs.readFile(inputFiles.selectedFile); const data = await fs.readFile(inputFiles.selectedFile);
const convertOpts = Object.assign({}, opts, {inputFiles, callback, dataType: selectedFileType}); const convertOpts = Object.assign({}, opts, {inputFiles, callback, abort, dataType: selectedFileType});
let result = false; let result = false;
for (const convert of this.convertFactory) { for (const convert of this.convertFactory) {
result = await convert.run(data, convertOpts); result = await convert.run(data, convertOpts);
@@ -40,7 +44,7 @@ class BookConverter {
} }
if (!result && inputFiles.nesting) { if (!result && inputFiles.nesting) {
result = await this.convertToFb2(inputFiles.nesting, outputFile, opts, callback); result = await this.convertToFb2(inputFiles.nesting, outputFile, opts, callback, abort);
} }
if (!result) { if (!result) {

View File

@@ -1,14 +1,19 @@
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path'); const path = require('path');
const LimitedQueue = require('../LimitedQueue');
const WorkerState = require('../WorkerState');//singleton const WorkerState = require('../WorkerState');//singleton
const FileDownloader = require('../FileDownloader'); const FileDownloader = require('../FileDownloader');
const FileDecompressor = require('../FileDecompressor'); const FileDecompressor = require('../FileDecompressor');
const BookConverter = require('./BookConverter'); const BookConverter = require('./BookConverter');
const RemoteWebDavStorage = require('../RemoteWebDavStorage');
const utils = require('../utils'); const utils = require('../utils');
const log = new (require('../AppLogger'))().log;//singleton const log = new (require('../AppLogger'))().log;//singleton
const cleanDirPeriod = 60*60*1000;//1 раз в час
const queue = new LimitedQueue(5, 100, 5*60*1000);//5 минут ожидание подвижек
let instance = null; let instance = null;
//singleton //singleton
@@ -24,12 +29,19 @@ class ReaderWorker {
fs.ensureDirSync(this.config.tempPublicDir); fs.ensureDirSync(this.config.tempPublicDir);
this.workerState = new WorkerState(); this.workerState = new WorkerState();
this.down = new FileDownloader(); this.down = new FileDownloader(config.maxUploadFileSize);
this.decomp = new FileDecompressor(); this.decomp = new FileDecompressor(2*config.maxUploadFileSize);
this.bookConverter = new BookConverter(this.config); this.bookConverter = new BookConverter(this.config);
this.periodicCleanDir(this.config.tempPublicDir, this.config.maxTempPublicDirSize, 60*60*1000);//1 раз в час this.remoteWebDavStorage = false;
this.periodicCleanDir(this.config.uploadDir, this.config.maxUploadPublicDirSize, 60*60*1000);//1 раз в час if (config.remoteWebDavStorage) {
this.remoteWebDavStorage = new RemoteWebDavStorage(
Object.assign({maxContentLength: config.maxUploadFileSize}, config.remoteWebDavStorage)
);
}
this.periodicCleanDir(this.config.tempPublicDir, this.config.maxTempPublicDirSize, cleanDirPeriod);
this.periodicCleanDir(this.config.uploadDir, this.config.maxUploadPublicDirSize, cleanDirPeriod);
instance = this; instance = this;
} }
@@ -39,22 +51,39 @@ class ReaderWorker {
async loadBook(opts, wState) { async loadBook(opts, wState) {
const url = opts.url; const url = opts.url;
let errMes = '';
let decompDir = ''; let decompDir = '';
let downloadedFilename = ''; let downloadedFilename = '';
let isUploaded = false; let isUploaded = false;
let convertFilename = ''; let convertFilename = '';
const overLoadMes = 'Слишком большая очередь загрузки. Пожалуйста, попробуйте позже.';
const overLoadErr = new Error(overLoadMes);
let q = null;
try { try {
wState.set({state: 'queue', step: 1, totalSteps: 1});
try {
let qSize = 0;
q = await queue.get((place) => {
wState.set({place, progress: (qSize ? Math.round((qSize - place)/qSize*100) : 0)});
if (!qSize)
qSize = place;
});
} catch (e) {
throw overLoadErr;
}
wState.set({state: 'download', step: 1, totalSteps: 3, url}); wState.set({state: 'download', step: 1, totalSteps: 3, url});
const tempFilename = utils.randomHexString(30); const tempFilename = utils.randomHexString(30);
const tempFilename2 = utils.randomHexString(30); const tempFilename2 = utils.randomHexString(30);
const decompDirname = utils.randomHexString(30); const decompDirname = utils.randomHexString(30);
//download or use uploaded
if (url.indexOf('file://') != 0) {//download if (url.indexOf('file://') != 0) {//download
const downdata = await this.down.load(url, (progress) => { const downdata = await this.down.load(url, (progress) => {
wState.set({progress}); wState.set({progress});
}); }, q.abort);
downloadedFilename = `${this.config.tempDownloadDir}/${tempFilename}`; downloadedFilename = `${this.config.tempDownloadDir}/${tempFilename}`;
await fs.writeFile(downloadedFilename, downdata); await fs.writeFile(downloadedFilename, downdata);
@@ -67,6 +96,10 @@ class ReaderWorker {
} }
wState.set({progress: 100}); wState.set({progress: 100});
if (q.abort())
throw overLoadErr;
q.resetTimeout();
//decompress //decompress
wState.set({state: 'decompress', step: 2, progress: 0}); wState.set({state: 'decompress', step: 2, progress: 0});
decompDir = `${this.config.tempDownloadDir}/${decompDirname}`; decompDir = `${this.config.tempDownloadDir}/${decompDirname}`;
@@ -79,27 +112,49 @@ class ReaderWorker {
} }
wState.set({progress: 100}); wState.set({progress: 100});
if (q.abort())
throw overLoadErr;
q.resetTimeout();
//конвертирование в fb2 //конвертирование в fb2
wState.set({state: 'convert', step: 3, progress: 0}); wState.set({state: 'convert', step: 3, progress: 0});
convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`; convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => { await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => {
wState.set({progress}); wState.set({progress});
}); }, q.abort);
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256 //сжимаем файл в tmp, если там уже нет с тем же именем-sha256
const compFilename = await this.decomp.gzipFileIfNotExists(convertFilename, `${this.config.tempPublicDir}`); const compFilename = await this.decomp.gzipFileIfNotExists(convertFilename, this.config.tempPublicDir);
const stat = await fs.stat(compFilename);
wState.set({progress: 100}); wState.set({progress: 100});
//finish //finish
const finishFilename = path.basename(compFilename); const finishFilename = path.basename(compFilename);
wState.finish({path: `/tmp/${finishFilename}`}); 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);
}
})();
}
} catch (e) { } catch (e) {
log(LM_ERR, e.stack); log(LM_ERR, e.stack);
wState.set({state: 'error', error: (errMes ? errMes : e.message)}); if (e.message == 'abort')
e.message = overLoadMes;
wState.set({state: 'error', error: e.message});
} finally { } finally {
//clean //clean
if (q)
q.ret();
if (decompDir) if (decompDir)
await fs.remove(decompDir); await fs.remove(decompDir);
if (downloadedFilename && !isUploaded) if (downloadedFilename && !isUploaded)
@@ -133,6 +188,41 @@ class ReaderWorker {
return `file://${hash}`; return `file://${hash}`;
} }
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 basename = path.basename(filename);
const targetName = `${this.config.tempPublicDir}/${basename}`;
if (!await fs.pathExists(targetName)) {
let found = false;
if (this.remoteWebDavStorage) {
found = await this.remoteWebDavStorage.getFileSuccess(targetName);
}
if (!found) {
throw new Error('404 Файл не найден');
}
}
const stat = await fs.stat(targetName);
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) { async periodicCleanDir(dir, maxSize, timeout) {
try { try {
const list = await fs.readdir(dir); const list = await fs.readdir(dir);
@@ -153,7 +243,19 @@ class ReaderWorker {
let i = 0; let i = 0;
while (i < files.length && size > maxSize) { while (i < files.length && size > maxSize) {
const file = files[i]; const file = files[i];
await fs.remove(`${dir}/${file.name}`); const oldFile = `${dir}/${file.name}`;
//отправляем только this.config.tempPublicDir
//TODO: убрать в будущем, т.к. уже делается ленивое сохранение compFilename в удаленном хранилище
if (this.remoteWebDavStorage && dir === this.config.tempPublicDir) {
try {
//log(`remoteWebDavStorage.putFile ${path.basename(oldFile)}`);
await this.remoteWebDavStorage.putFile(oldFile);
} catch (e) {
log(LM_ERR, e.stack);
}
}
await fs.remove(oldFile);
size -= file.stat.size; size -= file.stat.size;
i++; i++;
} }

View File

@@ -0,0 +1,107 @@
const fs = require('fs-extra');
const path = require('path');
const { createClient } = require('webdav');
class RemoteWebDavStorage {
constructor(config) {
this.config = Object.assign({}, config);
this.config.maxContentLength = this.config.maxContentLength || 10*1024*1024;
this.wdc = createClient(config.url, this.config);
}
_convertStat(data) {
return {
isDirectory: function() {
return data.type === "directory";
},
isFile: function() {
return data.type === "file";
},
mtime: (new Date(data.lastmod)).getTime(),
name: data.basename,
size: data.size || 0
};
}
async stat(filename) {
const stat = await this.wdc.stat(filename);
return this._convertStat(stat);
}
async writeFile(filename, data) {
return await this.wdc.putFileContents(filename, data, { maxContentLength: this.config.maxContentLength })
}
async unlink(filename) {
return await this.wdc.deleteFile(filename);
}
async readFile(filename) {
return await this.wdc.getFileContents(filename, { maxContentLength: this.config.maxContentLength })
}
async mkdir(dirname) {
return await this.wdc.createDirectory(dirname);
}
async putFile(filename) {
if (!await fs.pathExists(filename)) {
throw new Error(`File not found: ${filename}`);
}
const base = path.basename(filename);
let remoteFilename = `/${base}`;
if (base.length > 3) {
const remoteDir = `/${base.substr(0, 3)}`;
try {
await this.mkdir(remoteDir);
} catch (e) {
//
}
remoteFilename = `${remoteDir}/${base}`;
}
try {
const localStat = await fs.stat(filename);
const remoteStat = await this.stat(remoteFilename);
if (remoteStat.isFile && localStat.size == remoteStat.size) {
return;
}
await this.unlink(remoteFilename);
} catch (e) {
//
}
const data = await fs.readFile(filename);
await this.writeFile(remoteFilename, data);
}
async getFile(filename) {
if (await fs.pathExists(filename)) {
return;
}
const base = path.basename(filename);
let remoteFilename = `/${base}`;
if (base.length > 3) {
remoteFilename = `/${base.substr(0, 3)}/${base}`;
}
const data = await this.readFile(remoteFilename);
await fs.writeFile(filename, data);
}
async getFileSuccess(filename) {
try {
await this.getFile(filename);
return true;
} catch (e) {
//
}
return false;
}
}
module.exports = RemoteWebDavStorage;

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
require('tls').DEFAULT_MIN_VERSION = 'TLSv1';
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path'); const path = require('path');
const argv = require('minimist')(process.argv.slice(2)); const argv = require('minimist')(process.argv.slice(2));
const express = require('express'); const express = require('express');
const compression = require('compression'); const compression = require('compression');
const http = require('http');
const WebSocket = require ('ws');
async function init() { async function init() {
//config //config
@@ -18,7 +21,7 @@ async function init() {
const log = appLogger.log; const log = appLogger.log;
//dirs //dirs
log(`${config.name} v${config.version}`); log(`${config.name} v${config.version}, Node.js ${process.version}`);
log('Initializing'); log('Initializing');
await fs.ensureDir(config.dataDir); await fs.ensureDir(config.dataDir);
@@ -45,10 +48,13 @@ async function main() {
const config = new (require('./config'))().config;//singleton const config = new (require('./config'))().config;//singleton
//servers //servers
for (let server of config.servers) { for (let serverCfg of config.servers) {
if (server.mode !== 'none') { if (serverCfg.mode !== 'none') {
const app = express(); const app = express();
const serverConfig = Object.assign({}, config, server); const server = http.createServer(app);
const wss = new WebSocket.Server({ server, maxPayload: 10*1024*1024 });
const serverConfig = Object.assign({}, config, serverCfg);
let devModule = undefined; let devModule = undefined;
if (serverConfig.branch == 'development') { if (serverConfig.branch == 'development') {
@@ -72,7 +78,7 @@ async function main() {
} }
})); }));
require('./routes').initRoutes(app, serverConfig); require('./routes').initRoutes(app, wss, serverConfig);
if (devModule) { if (devModule) {
devModule.logErrors(app); devModule.logErrors(app);
@@ -83,7 +89,7 @@ async function main() {
}); });
} }
app.listen(serverConfig.port, serverConfig.ip, function() { server.listen(serverConfig.port, serverConfig.ip, function() {
log(`Server-${serverConfig.serverName} is ready on ${serverConfig.ip}:${serverConfig.port}, mode: ${serverConfig.mode}`); log(`Server-${serverConfig.serverName} is ready on ${serverConfig.ip}:${serverConfig.port}, mode: ${serverConfig.mode}`);
}); });
} }

View File

@@ -2,10 +2,11 @@ const c = require('./controllers');
const utils = require('./core/utils'); const utils = require('./core/utils');
const multer = require('multer'); const multer = require('multer');
function initRoutes(app, config) { function initRoutes(app, wss, config) {
const misc = new c.MiscController(config); const misc = new c.MiscController(config);
const reader = new c.ReaderController(config); const reader = new c.ReaderController(config);
const worker = new c.WorkerController(config); const worker = new c.WorkerController(config);
new c.WebSocketController(wss, config);
//access //access
const [aAll, aNormal, aSite, aReader, aOmnireader] = // eslint-disable-line no-unused-vars const [aAll, aNormal, aSite, aReader, aOmnireader] = // eslint-disable-line no-unused-vars
@@ -28,7 +29,9 @@ function initRoutes(app, config) {
['POST', '/api/reader/load-book', reader.loadBook.bind(reader), [aAll], {}], ['POST', '/api/reader/load-book', reader.loadBook.bind(reader), [aAll], {}],
['POST', '/api/reader/storage', reader.storage.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/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], {}], ['POST', '/api/worker/get-state', worker.getState.bind(worker), [aAll], {}],
['POST', '/api/worker/get-state-finish', worker.getStateFinish.bind(worker), [aAll], {}],
]; ];
//to app //to app