Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7e1e09928 | ||
|
|
f0832b07cb | ||
|
|
7c253df291 | ||
|
|
bb7cd9cbde | ||
|
|
56c4182985 | ||
|
|
cb6c7536bf | ||
|
|
fbfe8cbda0 | ||
|
|
6129d2d7eb | ||
|
|
16b30c922a | ||
|
|
c42ad66be6 | ||
|
|
f36c13fea1 | ||
|
|
4fd9d579e0 | ||
|
|
e65a8a13ea | ||
|
|
6ddb97d43e | ||
|
|
89082603de | ||
|
|
a9a3227433 | ||
|
|
60cb3514b2 | ||
|
|
4aeaa05f0b | ||
|
|
9c06552278 | ||
|
|
000f8dde82 | ||
|
|
9ffc218002 | ||
|
|
68a188f099 | ||
|
|
8829bb3810 | ||
|
|
5164d2f536 | ||
|
|
451538fcf7 | ||
|
|
82a02ef339 | ||
|
|
b834d4951f | ||
|
|
edc3b669be | ||
|
|
522826311d | ||
|
|
e69b9951d5 | ||
|
|
c6300222ea | ||
|
|
5aa6ee899c | ||
|
|
4b76f97d2b | ||
|
|
5ccfe71c55 | ||
|
|
97fc902cdb | ||
|
|
7e935951d7 | ||
|
|
810c6d68d2 | ||
|
|
003dc70f4f | ||
|
|
371ff64a95 | ||
|
|
b0de5adbf3 | ||
|
|
d1d2b07c33 | ||
|
|
d9b2444c1a | ||
|
|
e7fae27031 | ||
|
|
eb0c7b0a32 | ||
|
|
3d7ad0dd9a | ||
|
|
ae04feb311 | ||
|
|
7b59f911ef | ||
|
|
d3444da647 | ||
|
|
66738d0c9c | ||
|
|
7e187acd68 | ||
|
|
c751372a54 | ||
|
|
7fc98fc7da | ||
|
|
b56f45694e | ||
|
|
091ca521ef | ||
|
|
c7a17b0a76 | ||
|
|
26468b996a | ||
|
|
c4e240d87c | ||
|
|
04713f47c8 | ||
|
|
37ab3493db | ||
|
|
a4cb3c628e | ||
|
|
8492da8a13 | ||
|
|
98d7c64a56 | ||
|
|
25f121e5ed | ||
|
|
4c8797c99c | ||
|
|
1155aa285d | ||
|
|
239bbb8263 | ||
|
|
e6b9330108 | ||
|
|
935b767c2e | ||
|
|
8acf3295b5 | ||
|
|
48c3a12fa0 | ||
|
|
a1dea514b7 | ||
|
|
d4788439cb | ||
|
|
0a60ad354c | ||
|
|
c565a20344 | ||
|
|
735ee88f0b | ||
|
|
9405ce2cc0 | ||
|
|
115277d88a | ||
|
|
6925c11dbd | ||
|
|
984d835892 | ||
|
|
23353a4960 | ||
|
|
955bcda032 | ||
|
|
81ad5d7a2c | ||
|
|
dada7980ec | ||
|
|
511a308646 | ||
|
|
65c8f2cc81 | ||
|
|
238c18bc48 |
@@ -1,5 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as utils from '../share/utils';
|
import * as utils from '../share/utils';
|
||||||
|
import * as cryptoUtils from '../share/cryptoUtils';
|
||||||
import wsc from './webSocketConnection';
|
import wsc from './webSocketConnection';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
@@ -119,32 +120,7 @@ class Reader {
|
|||||||
estSize = response.headers['content-length'];
|
estSize = response.headers['content-length'];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//восстановим при необходимости файл на сервере из удаленного облака
|
//
|
||||||
let response = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
response = await wsc.message(await wsc.send({action: 'reader-restore-cached-file', path: url}));
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
//если с WebSocket проблема, работаем по http
|
|
||||||
response = await api.post('/restore-cached-file', {path: url});
|
|
||||||
response = response.data;
|
|
||||||
}
|
|
||||||
if (response.state == 'error') {
|
|
||||||
throw new Error(response.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const workerId = response.workerId;
|
|
||||||
if (!workerId)
|
|
||||||
throw new Error('Неверный ответ api');
|
|
||||||
|
|
||||||
response = await this.getWorkerStateFinish(workerId);
|
|
||||||
if (response.state == 'error') {
|
|
||||||
throw new Error(response.error);
|
|
||||||
}
|
|
||||||
if (response.size && estSize < 0) {
|
|
||||||
estSize = response.size;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return estSize;
|
return estSize;
|
||||||
@@ -174,11 +150,10 @@ class Reader {
|
|||||||
return await axios.get(url, options);
|
return await axios.get(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile(file, maxUploadFileSize, callback) {
|
async uploadFile(file, maxUploadFileSize = 10*1024*1024, callback) {
|
||||||
if (!maxUploadFileSize)
|
|
||||||
maxUploadFileSize = 10*1024*1024;
|
|
||||||
if (file.size > maxUploadFileSize)
|
if (file.size > maxUploadFileSize)
|
||||||
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
||||||
|
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
formData.append('file', file, file.name);
|
formData.append('file', file, file.name);
|
||||||
|
|
||||||
@@ -225,6 +200,35 @@ class Reader {
|
|||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
makeUrlFromBuf(buf) {
|
||||||
|
const key = utils.toHex(cryptoUtils.sha256(buf));
|
||||||
|
return `disk://${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFileBuf(buf, url) {
|
||||||
|
if (!url)
|
||||||
|
url = this.makeUrlFromBuf(buf);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
await axios.head(url.replace('disk://', '/upload/'), {headers: {'Cache-Control': 'no-cache'}});
|
||||||
|
response = await wsc.message(await wsc.send({action: 'upload-file-touch', url}));
|
||||||
|
} catch (e) {
|
||||||
|
response = await wsc.message(await wsc.send({action: 'upload-file-buf', buf}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.error)
|
||||||
|
throw new Error(response.error);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUploadedFileBuf(url) {
|
||||||
|
url = url.replace('disk://', '/upload/');
|
||||||
|
return (await axios.get(url)).data;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Reader();
|
export default new Reader();
|
||||||
@@ -194,6 +194,7 @@ import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
|
|||||||
|
|
||||||
import bookManager from './share/bookManager';
|
import bookManager from './share/bookManager';
|
||||||
import wallpaperStorage from './share/wallpaperStorage';
|
import wallpaperStorage from './share/wallpaperStorage';
|
||||||
|
import coversStorage from './share/coversStorage';
|
||||||
import dynamicCss from '../../share/dynamicCss';
|
import dynamicCss from '../../share/dynamicCss';
|
||||||
|
|
||||||
import rstore from '../../store/modules/reader';
|
import rstore from '../../store/modules/reader';
|
||||||
@@ -366,6 +367,8 @@ class Reader {
|
|||||||
mounted() {
|
mounted() {
|
||||||
(async() => {
|
(async() => {
|
||||||
await wallpaperStorage.init();
|
await wallpaperStorage.init();
|
||||||
|
await coversStorage.init();
|
||||||
|
|
||||||
await bookManager.init(this.settings);
|
await bookManager.init(this.settings);
|
||||||
bookManager.addEventListener(this.bookManagerEvent);
|
bookManager.addEventListener(this.bookManagerEvent);
|
||||||
|
|
||||||
@@ -450,22 +453,47 @@ class Reader {
|
|||||||
|
|
||||||
//wallpaper css
|
//wallpaper css
|
||||||
async loadWallpapers() {
|
async loadWallpapers() {
|
||||||
const wallpaperDataLength = await wallpaperStorage.getLength();
|
if (!_.isEqual(this.userWallpapers, this.prevUserWallpapers)) {//оптимизация
|
||||||
if (wallpaperDataLength !== this.wallpaperDataLength) {//оптимизация
|
this.prevUserWallpapers = _.cloneDeep(this.userWallpapers);
|
||||||
this.wallpaperDataLength = wallpaperDataLength;
|
|
||||||
|
|
||||||
let newCss = '';
|
let newCss = '';
|
||||||
|
let updated = false;
|
||||||
|
const wallpaperExists = new Set();
|
||||||
for (const wp of this.userWallpapers) {
|
for (const wp of this.userWallpapers) {
|
||||||
const data = await wallpaperStorage.getData(wp.cssClass);
|
wallpaperExists.add(wp.cssClass);
|
||||||
|
|
||||||
|
let data = await wallpaperStorage.getData(wp.cssClass);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
//здесь будем восстанавливать данные с сервера
|
//здесь будем восстанавливать данные с сервера
|
||||||
|
const url = `disk://${wp.cssClass.replace('user-paper', '')}`;
|
||||||
|
try {
|
||||||
|
data = await readerApi.getUploadedFileBuf(url);
|
||||||
|
await wallpaperStorage.setData(wp.cssClass, data);
|
||||||
|
updated = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`;
|
newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//почистим wallpaperStorage
|
||||||
|
for (const key of await wallpaperStorage.getKeys()) {
|
||||||
|
if (!wallpaperExists.has(key)) {
|
||||||
|
await wallpaperStorage.removeData(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//обновим settings, если загружали обои из /upload/
|
||||||
|
if (updated) {
|
||||||
|
const newSettings = _.cloneDeep(this.settings);
|
||||||
|
newSettings.needUpdateSettingsView = (newSettings.needUpdateSettingsView < 10 ? newSettings.needUpdateSettingsView + 1 : 0);
|
||||||
|
this.commit('reader/setSettings', newSettings);
|
||||||
|
}
|
||||||
|
|
||||||
dynamicCss.replace('wallpapers', newCss);
|
dynamicCss.replace('wallpapers', newCss);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -998,7 +1026,6 @@ class Reader {
|
|||||||
classResult = classDisabled;
|
classResult = classDisabled;
|
||||||
break;
|
break;
|
||||||
case 'refresh':
|
case 'refresh':
|
||||||
case 'recentBooks':
|
|
||||||
if (!this.mostRecentBookReactive)
|
if (!this.mostRecentBookReactive)
|
||||||
classResult = classDisabled;
|
classResult = classDisabled;
|
||||||
break;
|
break;
|
||||||
@@ -1108,6 +1135,7 @@ class Reader {
|
|||||||
wasOpened = (wasOpened ? _.cloneDeep(wasOpened) : {});
|
wasOpened = (wasOpened ? _.cloneDeep(wasOpened) : {});
|
||||||
|
|
||||||
wasOpened = Object.assign(wasOpened, {
|
wasOpened = Object.assign(wasOpened, {
|
||||||
|
url: (opts.url !== undefined ? opts.url : wasOpened.url),
|
||||||
path: (opts.path !== undefined ? opts.path : wasOpened.path),
|
path: (opts.path !== undefined ? opts.path : wasOpened.path),
|
||||||
bookPos: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos),
|
bookPos: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos),
|
||||||
bookPosSeen: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen),
|
bookPosSeen: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen),
|
||||||
|
|||||||
@@ -7,6 +7,20 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #buttons>
|
||||||
|
<div
|
||||||
|
class="row justify-center items-center"
|
||||||
|
:class="{'header-button': !archive, 'header-button-pressed': archive}"
|
||||||
|
@mousedown.stop @click="archiveToggle"
|
||||||
|
>
|
||||||
|
<q-icon class="q-mr-xs" name="la la-archive" size="20px" />
|
||||||
|
<span style="font-size: 90%">Архив</span>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
{{ (archive ? 'Скрыть архивные' : 'Показать архивные') }}
|
||||||
|
</q-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<a ref="download" style="display: none;" target="_blank"></a>
|
<a ref="download" style="display: none;" target="_blank"></a>
|
||||||
|
|
||||||
<div id="vs-container" ref="vsContainer" class="recent-books-scroll col">
|
<div id="vs-container" ref="vsContainer" class="recent-books-scroll col">
|
||||||
@@ -39,10 +53,25 @@
|
|||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
ref="input"
|
||||||
|
v-model="search"
|
||||||
|
class="q-ml-sm q-mt-xs"
|
||||||
|
outlined dense
|
||||||
|
style="width: 185px"
|
||||||
|
bg-color="white"
|
||||||
|
placeholder="Найти"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
<q-select
|
<q-select
|
||||||
ref="sortMethod"
|
ref="sortMethod"
|
||||||
v-model="sortMethod"
|
v-model="sortMethod"
|
||||||
class="q-ml-md q-mt-xs"
|
class="q-ml-sm q-mt-xs"
|
||||||
:options="sortMethodOptions"
|
:options="sortMethodOptions"
|
||||||
style="width: 180px"
|
style="width: 180px"
|
||||||
bg-color="white"
|
bg-color="white"
|
||||||
@@ -60,21 +89,6 @@
|
|||||||
<div style="height: 28px; padding-top: 2px; overflow: hidden" v-html="scope.opt.label" />
|
<div style="height: 28px; padding-top: 2px; overflow: hidden" v-html="scope.opt.label" />
|
||||||
</template>
|
</template>
|
||||||
</q-select>
|
</q-select>
|
||||||
|
|
||||||
<q-input
|
|
||||||
ref="input"
|
|
||||||
v-model="search"
|
|
||||||
class="q-ml-sm q-mt-xs"
|
|
||||||
outlined dense
|
|
||||||
style="width: 180px"
|
|
||||||
bg-color="white"
|
|
||||||
placeholder="Найти"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-virtual-scroll
|
<q-virtual-scroll
|
||||||
@@ -86,13 +100,14 @@
|
|||||||
@virtual-scroll="onScroll"
|
@virtual-scroll="onScroll"
|
||||||
>
|
>
|
||||||
<div class="table-row row" :class="{even: index % 2 > 0, 'active-book': item.active, 'active-parent-book': item.activeParent}">
|
<div class="table-row row" :class="{even: index % 2 > 0, 'active-book': item.active, 'active-parent-book': item.activeParent}">
|
||||||
<div v-show="item.inGroup" class="row-part column justify-center items-center" style="width: 40px; border-right: 1px solid #cccccc">
|
<div v-show="item.inGroup" class="row-part column justify-center items-center" style="width: 40px">
|
||||||
<q-icon name="la la-code-branch" size="24px" style="color: green" />
|
<q-icon name="la la-code-branch" size="24px" style="color: green" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row-part column justify-center items-stretch" style="width: 80px">
|
<div class="row-part column justify-center items-stretch" style="width: 80px">
|
||||||
<div class="col row justify-center items-center clickable" @click="loadBook(item)">
|
<div class="col row justify-center items-center clickable" style="padding: 0 2px 0 2px" @click="loadBook(item)">
|
||||||
<q-icon name="la la-book" size="40px" style="color: #dddddd" />
|
<div v-show="isLoadedCover(item.coverPageUrl)" style="height: 80px" v-html="getCoverHtml(item.coverPageUrl)" />
|
||||||
|
<q-icon v-show="!isLoadedCover(item.coverPageUrl)" name="la la-book" size="40px" style="color: #dddddd" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="!showSameBook && item.group && item.group.length > 0" class="row justify-center" style="font-size: 70%">
|
<div v-show="!showSameBook && item.group && item.group.length > 0" class="row justify-center" style="font-size: 70%">
|
||||||
@@ -100,58 +115,86 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row-part column items-stretch clickable break-word" :style="{ 'width': (350 - 40*(+item.inGroup)) + 'px' }" style="font-size: 75%" @click="loadBook(item)">
|
<div class="row-part column items-stretch clickable break-word" @click="loadBook(item)">
|
||||||
<div class="row" style="font-size: 80%">
|
<div
|
||||||
<div class="row justify-center row-info-top" style="width: 30px">
|
class="col" style="border: 1px solid #cccccc; border-bottom: 0; padding: 4px; line-height: 140%;"
|
||||||
{{ item.num }}
|
:style="{ 'width': (380 - 40*(+item.inGroup)) + 'px' }"
|
||||||
</div>
|
>
|
||||||
<div class="row justify-center row-info-top" style="width: 130px">
|
<div class="text-green-10" style="font-size: 80%">
|
||||||
Читался: {{ item.touchTime }}
|
|
||||||
</div>
|
|
||||||
<div class="row justify-center row-info-top" style="width: 138px">
|
|
||||||
Загружен: {{ item.loadTime }}
|
|
||||||
</div>
|
|
||||||
<div class="row justify-center row-info-top" style="width: 1px">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col q-mt-xs" :style="{ 'width': (340 - 40*(+item.inGroup)) + 'px' }">
|
|
||||||
<div class="text-green-10" style="font-size: 105%">
|
|
||||||
{{ item.desc.author }}
|
{{ item.desc.author }}
|
||||||
</div>
|
</div>
|
||||||
<div>{{ item.desc.title }}</div>
|
<div style="font-size: 75%">
|
||||||
<!--div>{{ item.path }}</div-->
|
{{ item.desc.title }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row q-mt-xs" style="font-size: 80%">
|
<div class="row" style="font-size: 10px">
|
||||||
<div class="row justify-center row-info-bottom" style="width: 60px">
|
<div class="row justify-center items-center row-info-top" style="width: 60px">
|
||||||
{{ item.desc.textLen }}
|
{{ item.desc.textLen }}
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-center row-info-bottom" style="width: 60px">
|
|
||||||
|
<div class="row items-center row-info-top" :style="`width: ${(260 - 40*(+item.inGroup))}px; padding: 1px`">
|
||||||
|
<div class="read-bar" :style="`width: ${100*item.readPart}%`"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-center items-center row-info-top" style="width: 59px">
|
||||||
{{ item.desc.perc }}
|
{{ item.desc.perc }}
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-center row-info-bottom" style="width: 1px">
|
<div class="row-info-top" style="width: 1px">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="read-bar" :style="`width: ${(340 - 40*(+item.inGroup))*item.readPart}px`"></div>
|
<div class="row" style="font-size: 10px" :style="{ 'width': (380 - 40*(+item.inGroup)) + 'px' }">
|
||||||
</div>
|
<div class="row justify-center items-center row-info-bottom" style="width: 30px">
|
||||||
|
{{ item.num }}
|
||||||
<div class="row-part column justify-center" style="width: 80px; font-size: 75%">
|
</div>
|
||||||
<div>
|
<div class="col row">
|
||||||
<a v-show="isUrl(item.url)" :href="item.url" target="_blank">Оригинал</a><br><br>
|
<div class="row justify-center items-center row-info-bottom time-info" style="width: 50%">
|
||||||
<a :href="item.path" @click.prevent="downloadBook(item.path, item.fullTitle)">Скачать FB2</a>
|
Загружен: {{ item.loadTime }}
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center items-center row-info-bottom time-info" style="width: 50%">
|
||||||
|
Читался: {{ item.touchTime }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row-info-bottom" style="width: 1px">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row-part column justify-center">
|
<div
|
||||||
<q-btn
|
class="row-part column"
|
||||||
dense
|
style="width: 90px;"
|
||||||
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
>
|
||||||
|
<div
|
||||||
|
class="col column justify-center"
|
||||||
|
style="font-size: 75%; padding-left: 6px; border: 1px solid #cccccc; border-left: 0;"
|
||||||
|
>
|
||||||
|
<div :style="`margin-top: ${(archive ? 20 : 0)}px`">
|
||||||
|
<a v-show="isUrl(item.url)" :href="item.url" target="_blank">Оригинал</a><br><br>
|
||||||
|
<a :href="item.path" @click.prevent="downloadBook(item.path, item.fullTitle)">Скачать FB2</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="del-button self-end row justify-center items-center clickable"
|
||||||
@click="handleDel(item.key)"
|
@click="handleDel(item.key)"
|
||||||
>
|
>
|
||||||
<q-icon class="la la-times" size="14px" />
|
<q-icon class="la la-times" size="12px" />
|
||||||
</q-btn>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
{{ (archive ? 'Удалить окончательно' : 'Перенести в архив') }}
|
||||||
|
</q-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-show="archive"
|
||||||
|
class="restore-button self-start row justify-center items-center clickable"
|
||||||
|
@click="handleRestore(item.key)"
|
||||||
|
>
|
||||||
|
<q-icon class="la la-arrow-left" size="14px" />
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
Восстановить из архива
|
||||||
|
</q-tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-virtual-scroll>
|
</q-virtual-scroll>
|
||||||
@@ -171,6 +214,7 @@ import LockQueue from '../../../share/LockQueue';
|
|||||||
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';
|
import readerApi from '../../../api/reader';
|
||||||
|
import coversStorage from '../share/coversStorage';
|
||||||
|
|
||||||
const componentOptions = {
|
const componentOptions = {
|
||||||
components: {
|
components: {
|
||||||
@@ -196,6 +240,10 @@ class RecentBooksPage {
|
|||||||
tableData = [];
|
tableData = [];
|
||||||
sortMethod = '';
|
sortMethod = '';
|
||||||
showSameBook = false;
|
showSameBook = false;
|
||||||
|
archive = false;
|
||||||
|
|
||||||
|
covers = {};
|
||||||
|
coversLoadFunc = {};
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
@@ -221,6 +269,7 @@ class RecentBooksPage {
|
|||||||
this.showBar();
|
this.showBar();
|
||||||
await this.updateTableData();
|
await this.updateTableData();
|
||||||
await this.scrollToActiveBook();
|
await this.scrollToActiveBook();
|
||||||
|
//await this.scrollRefresh();
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +296,7 @@ class RecentBooksPage {
|
|||||||
|
|
||||||
//подготовка полей
|
//подготовка полей
|
||||||
for (const book of sorted) {
|
for (const book of sorted) {
|
||||||
if (book.deleted)
|
if ((!this.archive && book.deleted) || (this.archive && book.deleted != 1))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
let d = new Date();
|
let d = new Date();
|
||||||
@@ -274,6 +323,11 @@ class RecentBooksPage {
|
|||||||
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : (book.uploadFileName ? book.uploadFileName : book.url)));
|
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : (book.uploadFileName ? book.uploadFileName : book.url)));
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
|
key: book.key,
|
||||||
|
url: book.url,
|
||||||
|
path: book.path,
|
||||||
|
deleted: book.deleted,
|
||||||
|
|
||||||
touchTime,
|
touchTime,
|
||||||
loadTime,
|
loadTime,
|
||||||
desc: {
|
desc: {
|
||||||
@@ -283,14 +337,12 @@ class RecentBooksPage {
|
|||||||
textLen,
|
textLen,
|
||||||
},
|
},
|
||||||
readPart,
|
readPart,
|
||||||
url: book.url,
|
|
||||||
path: book.path,
|
|
||||||
fullTitle: bt.fullTitle,
|
fullTitle: bt.fullTitle,
|
||||||
key: book.key,
|
|
||||||
sameBookKey: book.sameBookKey,
|
sameBookKey: book.sameBookKey,
|
||||||
active: (activeBook.key == book.key),
|
active: (activeBook.key == book.key),
|
||||||
activeParent: false,
|
activeParent: false,
|
||||||
inGroup: false,
|
inGroup: false,
|
||||||
|
coverPageUrl: book.coverPageUrl,
|
||||||
|
|
||||||
//для сортировки
|
//для сортировки
|
||||||
loadTimeRaw,
|
loadTimeRaw,
|
||||||
@@ -299,12 +351,11 @@ class RecentBooksPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//нумерация
|
//нумерация
|
||||||
let num = 0;
|
|
||||||
|
|
||||||
result.sort((a, b) => b.loadTimeRaw - a.loadTimeRaw);
|
result.sort((a, b) => b.loadTimeRaw - a.loadTimeRaw);
|
||||||
for (const book of result) {
|
let num = 0;
|
||||||
|
for (let i = result.length - 1; i >= 0; i--) {
|
||||||
num++;
|
num++;
|
||||||
book.num = num;
|
result[i].num = num;
|
||||||
}
|
}
|
||||||
|
|
||||||
//фильтрация
|
//фильтрация
|
||||||
@@ -404,7 +455,8 @@ class RecentBooksPage {
|
|||||||
wordEnding(num, type = 0) {
|
wordEnding(num, type = 0) {
|
||||||
const endings = [
|
const endings = [
|
||||||
['ов', '', 'а', 'а', 'а', 'ов', 'ов', 'ов', 'ов', 'ов'],
|
['ов', '', 'а', 'а', 'а', 'ов', 'ов', 'ов', 'ов', 'ов'],
|
||||||
['й', 'я', 'и', 'и', 'и', 'й', 'й', 'й', 'й', 'й']
|
['й', 'я', 'и', 'и', 'и', 'й', 'й', 'й', 'й', 'й'],
|
||||||
|
['о', '', 'о', 'о', 'о', 'о', 'о', 'о', 'о', 'о']
|
||||||
];
|
];
|
||||||
const deci = num % 100;
|
const deci = num % 100;
|
||||||
if (deci > 10 && deci < 20) {
|
if (deci > 10 && deci < 20) {
|
||||||
@@ -416,7 +468,7 @@ class RecentBooksPage {
|
|||||||
|
|
||||||
get header() {
|
get header() {
|
||||||
const len = (this.tableData ? this.tableData.length : 0);
|
const len = (this.tableData ? this.tableData.length : 0);
|
||||||
return `${(this.search ? 'Найдено' : 'Всего')} ${len} файл${this.wordEnding(len)}`;
|
return `${(this.search ? `Найден${this.wordEnding(len, 2)}` : 'Всего')} ${len} файл${this.wordEnding(len)}${this.archive ? ' в архиве' : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadBook(fb2path, fullTitle) {
|
async downloadBook(fb2path, fullTitle) {
|
||||||
@@ -442,15 +494,30 @@ class RecentBooksPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleDel(key) {
|
async handleDel(key) {
|
||||||
await bookManager.delRecentBook({key});
|
if (!this.archive) {
|
||||||
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
|
await bookManager.delRecentBook({key});
|
||||||
|
this.$root.notify.info('Перенесено в архив');
|
||||||
if (!bookManager.mostRecentBook())
|
} else {
|
||||||
this.close();
|
if (await this.$root.stdDialog.confirm('Подтвердите удаление из архива:', ' ')) {
|
||||||
|
await bookManager.delRecentBook({key}, 2);
|
||||||
|
this.$root.notify.info('Удалено безвозвратно');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadBook(row) {
|
async handleRestore(key) {
|
||||||
this.$emit('load-book', {url: row.url, path: row.path});
|
await bookManager.restoreRecentBook({key});
|
||||||
|
this.$root.notify.info('Восстановлено из архива');
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadBook(item) {
|
||||||
|
//чтобы не обновлять лишний раз updateTableData
|
||||||
|
this.inited = false;
|
||||||
|
|
||||||
|
if (item.deleted)
|
||||||
|
await this.handleRestore(item.key);
|
||||||
|
|
||||||
|
this.$emit('load-book', {url: item.url, path: item.path});
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,6 +574,8 @@ class RecentBooksPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async scrollToActiveBook() {
|
async scrollToActiveBook() {
|
||||||
|
await this.$nextTick();
|
||||||
|
|
||||||
this.lockScroll = true;
|
this.lockScroll = true;
|
||||||
try {
|
try {
|
||||||
let activeIndex = -1;
|
let activeIndex = -1;
|
||||||
@@ -552,6 +621,16 @@ class RecentBooksPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async scrollRefresh() {
|
||||||
|
this.lockScroll = true;
|
||||||
|
await utils.sleep(100);
|
||||||
|
try {
|
||||||
|
this.$refs.virtualScroll.refresh();
|
||||||
|
} finally {
|
||||||
|
await utils.sleep(100);
|
||||||
|
this.lockScroll = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get sortMethodOptions() {
|
get sortMethodOptions() {
|
||||||
return [
|
return [
|
||||||
@@ -566,6 +645,11 @@ class RecentBooksPage {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
archiveToggle() {
|
||||||
|
this.archive = !this.archive;
|
||||||
|
this.updateTableData();
|
||||||
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.$emit('recent-books-close');
|
this.$emit('recent-books-close');
|
||||||
}
|
}
|
||||||
@@ -576,6 +660,59 @@ class RecentBooksPage {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
makeCoverHtml(data) {
|
||||||
|
return `<img src="${data}" style="height: 100%; width: 100%; object-fit: contain" />`;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadedCover(coverPageUrl) {
|
||||||
|
if (!coverPageUrl)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
let loadedCover = this.covers[coverPageUrl];
|
||||||
|
|
||||||
|
if (loadedCover == 'error')
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!loadedCover) {
|
||||||
|
(async() => {
|
||||||
|
if (this.coversLoadFunc[coverPageUrl])
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.coversLoadFunc[coverPageUrl] = (async() => {
|
||||||
|
//сначала заглянем в storage
|
||||||
|
let data = await coversStorage.getData(coverPageUrl);
|
||||||
|
if (data) {
|
||||||
|
this.covers[coverPageUrl] = this.makeCoverHtml(data);
|
||||||
|
} else {//иначе идем на сервер
|
||||||
|
try {
|
||||||
|
data = await readerApi.getUploadedFileBuf(coverPageUrl);
|
||||||
|
await coversStorage.setData(coverPageUrl, data);
|
||||||
|
this.covers[coverPageUrl] = this.makeCoverHtml(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.covers[coverPageUrl] = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.coversLoadFunc[coverPageUrl]();
|
||||||
|
} finally {
|
||||||
|
this.coversLoadFunc[coverPageUrl] = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (loadedCover != undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCoverHtml(coverPageUrl) {
|
||||||
|
if (coverPageUrl && this.covers[coverPageUrl])
|
||||||
|
return this.covers[coverPageUrl];
|
||||||
|
else
|
||||||
|
return '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default vueComponent(RecentBooksPage);
|
export default vueComponent(RecentBooksPage);
|
||||||
@@ -600,11 +737,10 @@ export default vueComponent(RecentBooksPage);
|
|||||||
|
|
||||||
.table-row {
|
.table-row {
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
border-bottom: 1px solid #cccccc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-part {
|
.row-part {
|
||||||
padding: 4px 4px 4px 4px;
|
padding: 4px 0px 4px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clickable {
|
.clickable {
|
||||||
@@ -612,18 +748,11 @@ export default vueComponent(RecentBooksPage);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.break-word {
|
.break-word {
|
||||||
line-height: 180%;
|
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.read-bar {
|
|
||||||
height: 3px;
|
|
||||||
background-color: #aaaaaa;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.even {
|
.even {
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
}
|
}
|
||||||
@@ -644,18 +773,6 @@ export default vueComponent(RecentBooksPage);
|
|||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-info-top {
|
|
||||||
line-height: 110%;
|
|
||||||
border-left: 1px solid #cccccc;
|
|
||||||
border-bottom: 1px solid #cccccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-info-bottom {
|
|
||||||
line-height: 110%;
|
|
||||||
border: 1px solid #cccccc;
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-button {
|
.tool-button {
|
||||||
min-width: 30px;
|
min-width: 30px;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
@@ -664,4 +781,78 @@ export default vueComponent(RecentBooksPage);
|
|||||||
margin: 10px 6px 0px 3px;
|
margin: 10px 6px 0px 3px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row-info-bottom {
|
||||||
|
line-height: 110%;
|
||||||
|
border-left: 1px solid #cccccc;
|
||||||
|
border-bottom: 1px solid #cccccc;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-info-top {
|
||||||
|
line-height: 110%;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
border-right: 0;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-info, .row-info-top {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-bar {
|
||||||
|
height: 6px;
|
||||||
|
background-color: #b8b8b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-button {
|
||||||
|
width: 25px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
border-left: 1px solid #cccccc;
|
||||||
|
border-bottom: 1px solid #cccccc;
|
||||||
|
border-radius: 0 0 0 10px;
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-button:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: #FF3030;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restore-button {
|
||||||
|
width: 25px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
border-right: 1px solid #cccccc;
|
||||||
|
border-bottom: 1px solid #cccccc;
|
||||||
|
border-radius: 0 0 10px 0;
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restore-button:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: #00bb00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button, .header-button-pressed {
|
||||||
|
width: 80px;
|
||||||
|
height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #555555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: #39902F;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button-pressed {
|
||||||
|
color: black;
|
||||||
|
background-color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button-pressed:hover {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close">
|
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
|
||||||
<template #header>
|
<template #header>
|
||||||
Установить позицию
|
Установить позицию
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div id="set-position-slider" class="slider q-px-md">
|
<div class="col column justify-center">
|
||||||
<q-slider
|
<div id="set-position-slider" class="slider q-px-md column justify-center">
|
||||||
v-model="sliderValue"
|
<q-slider
|
||||||
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
|
v-model="sliderValue"
|
||||||
|
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
|
||||||
:max="sliderMax"
|
|
||||||
label
|
:max="sliderMax"
|
||||||
:label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
|
label
|
||||||
color="primary"
|
:label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
|
||||||
/>
|
color="primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
</template>
|
</template>
|
||||||
@@ -76,7 +78,8 @@ export default vueComponent(SetPositionPage);
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.slider {
|
.slider {
|
||||||
margin: 20px;
|
margin: 0 20px 0 20px;
|
||||||
|
height: 35px;
|
||||||
background-color: #efefef;
|
background-color: #efefef;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="col row">
|
<div class="col row">
|
||||||
|
<a ref="download" style="display: none;" target="_blank"></a>
|
||||||
|
|
||||||
<div class="full-height">
|
<div class="full-height">
|
||||||
<q-tabs
|
<q-tabs
|
||||||
ref="tabs"
|
ref="tabs"
|
||||||
@@ -124,6 +126,7 @@ import NumInput from '../../share/NumInput.vue';
|
|||||||
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
|
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
|
||||||
import wallpaperStorage from '../share/wallpaperStorage';
|
import wallpaperStorage from '../share/wallpaperStorage';
|
||||||
|
|
||||||
|
import readerApi from '../../../api/reader';
|
||||||
import rstore from '../../../store/modules/reader';
|
import rstore from '../../../store/modules/reader';
|
||||||
import defPalette from './defPalette';
|
import defPalette from './defPalette';
|
||||||
|
|
||||||
@@ -636,8 +639,17 @@ class SettingsPage {
|
|||||||
|
|
||||||
if (index < 0)
|
if (index < 0)
|
||||||
newUserWallpapers.push({label, cssClass});
|
newUserWallpapers.push({label, cssClass});
|
||||||
if (!wallpaperStorage.keyExists(cssClass))
|
if (!wallpaperStorage.keyExists(cssClass)) {
|
||||||
await wallpaperStorage.setData(cssClass, data);
|
await wallpaperStorage.setData(cssClass, data);
|
||||||
|
//отправим data на сервер в файл `/upload/${key}`
|
||||||
|
try {
|
||||||
|
//const res =
|
||||||
|
await readerApi.uploadFileBuf(data);
|
||||||
|
//console.log(res);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.userWallpapers = newUserWallpapers;
|
this.userWallpapers = newUserWallpapers;
|
||||||
this.wallpaper = cssClass;
|
this.wallpaper = cssClass;
|
||||||
@@ -664,6 +676,27 @@ class SettingsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadWallpaper() {
|
||||||
|
if (this.wallpaper.indexOf('user-paper') != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const d = this.$refs.download;
|
||||||
|
|
||||||
|
const dataUrl = await wallpaperStorage.getData(this.wallpaper);
|
||||||
|
|
||||||
|
if (!dataUrl)
|
||||||
|
throw new Error('Файл обоев не найден');
|
||||||
|
|
||||||
|
d.href = dataUrl;
|
||||||
|
d.download = `wallpaper-#${this.wallpaper.replace('user-paper', '').substring(0, 4)}`;
|
||||||
|
|
||||||
|
d.click();
|
||||||
|
} catch (e) {
|
||||||
|
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
||||||
this.close();
|
this.close();
|
||||||
|
|||||||
@@ -102,6 +102,11 @@
|
|||||||
Удалить выбранные обои
|
Удалить выбранные обои
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-file-download" @click.stop="downloadWallpaper">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
Скачать выбранные обои
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import sax from '../../../../server/core/sax';
|
|||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
const maxImageLineCount = 100;
|
const maxImageLineCount = 100;
|
||||||
|
const maxParaLength = 10000;
|
||||||
|
const maxParaTextLength = 10000;
|
||||||
|
|
||||||
// defaults
|
// defaults
|
||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
@@ -83,6 +85,7 @@ export default class BookParser {
|
|||||||
let binaryId = '';
|
let binaryId = '';
|
||||||
let binaryType = '';
|
let binaryType = '';
|
||||||
let dimPromises = [];
|
let dimPromises = [];
|
||||||
|
this.coverPageId = '';
|
||||||
|
|
||||||
//оглавление
|
//оглавление
|
||||||
this.contents = [];
|
this.contents = [];
|
||||||
@@ -226,13 +229,26 @@ export default class BookParser {
|
|||||||
paraOffset += len;
|
paraOffset += len;
|
||||||
};
|
};
|
||||||
|
|
||||||
const growParagraph = (text, len) => {
|
const growParagraph = (text, len, textRaw) => {
|
||||||
|
//начальный параграф
|
||||||
if (paraIndex < 0) {
|
if (paraIndex < 0) {
|
||||||
newParagraph();
|
newParagraph();
|
||||||
growParagraph(text, len);
|
growParagraph(text, len);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//ограничение на размер куска текста в параграфе
|
||||||
|
if (textRaw && textRaw.length > maxParaTextLength) {
|
||||||
|
while (textRaw.length > 0) {
|
||||||
|
const textPart = textRaw.substring(0, maxParaTextLength);
|
||||||
|
textRaw = textRaw.substring(maxParaTextLength);
|
||||||
|
|
||||||
|
newParagraph();
|
||||||
|
growParagraph(textPart, textPart.length);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (inSubtitle) {
|
if (inSubtitle) {
|
||||||
curSubtitle.title += text;
|
curSubtitle.title += text;
|
||||||
} else if (inTitle) {
|
} else if (inTitle) {
|
||||||
@@ -240,6 +256,14 @@ export default class BookParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const p = para[paraIndex];
|
const p = para[paraIndex];
|
||||||
|
|
||||||
|
//ограничение на размер параграфа
|
||||||
|
if (p.length > maxParaLength) {
|
||||||
|
newParagraph();
|
||||||
|
growParagraph(text, len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
p.length += len;
|
p.length += len;
|
||||||
p.text += text;
|
p.text += text;
|
||||||
paraOffset += len;
|
paraOffset += len;
|
||||||
@@ -266,7 +290,7 @@ export default class BookParser {
|
|||||||
const href = attrs.href.value;
|
const href = attrs.href.value;
|
||||||
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
|
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
|
||||||
const {id, local} = this.imageHrefToId(href);
|
const {id, local} = this.imageHrefToId(href);
|
||||||
if (href[0] == '#') {//local
|
if (local) {//local
|
||||||
imageNum++;
|
imageNum++;
|
||||||
|
|
||||||
if (inPara && !this.sets.showInlineImagesInCenter && !center)
|
if (inPara && !this.sets.showInlineImagesInCenter && !center)
|
||||||
@@ -278,6 +302,11 @@ export default class BookParser {
|
|||||||
|
|
||||||
if (inPara && this.sets.showInlineImagesInCenter)
|
if (inPara && this.sets.showInlineImagesInCenter)
|
||||||
newParagraph();
|
newParagraph();
|
||||||
|
|
||||||
|
//coverpage
|
||||||
|
if (path == '/fictionbook/description/title-info/coverpage/image') {
|
||||||
|
this.coverPageId = id;
|
||||||
|
}
|
||||||
} else {//external
|
} else {//external
|
||||||
imageNum++;
|
imageNum++;
|
||||||
|
|
||||||
@@ -536,7 +565,7 @@ export default class BookParser {
|
|||||||
tClose += (center ? '</center>' : '');
|
tClose += (center ? '</center>' : '');
|
||||||
|
|
||||||
if (text != ' ')
|
if (text != ' ')
|
||||||
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
growParagraph(`${tOpen}${text}${tClose}`, text.length, text);
|
||||||
else
|
else
|
||||||
growParagraph(' ', 1);
|
growParagraph(' ', 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import localForage from 'localforage';
|
|||||||
import path from 'path-browserify';
|
import path from 'path-browserify';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import * as utils from '../../../share/utils';
|
|
||||||
import BookParser from './BookParser';
|
import BookParser from './BookParser';
|
||||||
|
import readerApi from '../../../api/reader';
|
||||||
|
import coversStorage from './coversStorage';
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
const maxDataSize = 500*1024*1024;//compressed bytes
|
const maxDataSize = 500*1024*1024;//compressed bytes
|
||||||
const maxRecentLength = 5000;
|
const maxRecentLength = 5000;
|
||||||
@@ -345,9 +347,38 @@ class BookManager {
|
|||||||
const parsed = new BookParser(this.settings);
|
const parsed = new BookParser(this.settings);
|
||||||
|
|
||||||
const parsedMeta = await parsed.parse(data, callback);
|
const parsedMeta = await parsed.parse(data, callback);
|
||||||
|
|
||||||
|
//cover page
|
||||||
|
let coverPageUrl = '';
|
||||||
|
if (parsed.coverPageId && parsed.binary[parsed.coverPageId]) {
|
||||||
|
const bin = parsed.binary[parsed.coverPageId];
|
||||||
|
let dataUrl = `data:${bin.type};base64,${bin.data}`;
|
||||||
|
try {
|
||||||
|
dataUrl = await utils.resizeImage(dataUrl, 160, 160, 0.94);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
coverPageUrl = readerApi.makeUrlFromBuf(dataUrl);
|
||||||
|
|
||||||
|
//далее асинхронно
|
||||||
|
(async() => {
|
||||||
|
//отправим dataUrl на сервер в /upload
|
||||||
|
try {
|
||||||
|
await readerApi.uploadFileBuf(dataUrl, coverPageUrl);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
//сохраним в storage
|
||||||
|
await coversStorage.setData(coverPageUrl, dataUrl);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
const result = Object.assign({}, meta, parsedMeta, {
|
const result = Object.assign({}, meta, parsedMeta, {
|
||||||
length: data.length,
|
length: data.length,
|
||||||
textLength: parsed.textLength,
|
textLength: parsed.textLength,
|
||||||
|
coverPageUrl,
|
||||||
parsed
|
parsed
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -433,9 +464,9 @@ class BookManager {
|
|||||||
return this.recent[value.key];
|
return this.recent[value.key];
|
||||||
}
|
}
|
||||||
|
|
||||||
async delRecentBook(value) {
|
async delRecentBook(value, delFlag = 1) {
|
||||||
const item = this.recent[value.key];
|
const item = this.recent[value.key];
|
||||||
item.deleted = 1;
|
item.deleted = delFlag;
|
||||||
|
|
||||||
if (this.recentLastKey == value.key) {
|
if (this.recentLastKey == value.key) {
|
||||||
await this.recentSetLastKey(null);
|
await this.recentSetLastKey(null);
|
||||||
@@ -445,6 +476,13 @@ class BookManager {
|
|||||||
this.emit('recent-deleted', value.key);
|
this.emit('recent-deleted', value.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async restoreRecentBook(value) {
|
||||||
|
const item = this.recent[value.key];
|
||||||
|
item.deleted = 0;
|
||||||
|
|
||||||
|
await this.recentSetItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
async cleanRecentBooks() {
|
async cleanRecentBooks() {
|
||||||
const sorted = this.getSortedRecent();
|
const sorted = this.getSortedRecent();
|
||||||
|
|
||||||
|
|||||||
61
client/components/Reader/share/coversStorage.js
Normal file
61
client/components/Reader/share/coversStorage.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import localForage from 'localforage';
|
||||||
|
//import _ from 'lodash';
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
|
const maxDataSize = 100*1024*1024;
|
||||||
|
|
||||||
|
const coversStore = localForage.createInstance({
|
||||||
|
name: 'coversStorage'
|
||||||
|
});
|
||||||
|
|
||||||
|
class CoversStorage {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
this.cleanCovers(); //no await
|
||||||
|
}
|
||||||
|
|
||||||
|
async setData(key, data) {
|
||||||
|
await coversStore.setItem(key, {addTime: Date.now(), data});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getData(key) {
|
||||||
|
const item = await coversStore.getItem(key);
|
||||||
|
return (item ? item.data : undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeData(key) {
|
||||||
|
await coversStore.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanCovers() {
|
||||||
|
await utils.sleep(10000);
|
||||||
|
|
||||||
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
|
let size = 0;
|
||||||
|
let min = Date.now();
|
||||||
|
let toDel = null;
|
||||||
|
for (const key of (await coversStore.keys())) {
|
||||||
|
const item = await coversStore.getItem(key);
|
||||||
|
|
||||||
|
size += item.data.length;
|
||||||
|
|
||||||
|
if (item.addTime < min) {
|
||||||
|
toDel = key;
|
||||||
|
min = item.addTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (size > maxDataSize && toDel) {
|
||||||
|
await this.removeData(toDel);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new CoversStorage();
|
||||||
@@ -32,6 +32,10 @@ class WallpaperStorage {
|
|||||||
this.cachedKeys = await wpStore.keys();
|
this.cachedKeys = await wpStore.keys();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getKeys() {
|
||||||
|
return await wpStore.keys();
|
||||||
|
}
|
||||||
|
|
||||||
keyExists(key) {//не асинхронная
|
keyExists(key) {//не асинхронная
|
||||||
return this.cachedKeys.includes(key);
|
return this.cachedKeys.includes(key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,18 @@
|
|||||||
export const versionHistory = [
|
export const versionHistory = [
|
||||||
|
{
|
||||||
|
version: '0.11.8',
|
||||||
|
releaseDate: '2022-07-14',
|
||||||
|
showUntil: '2022-07-13',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлено отображение и синхронизация обложек в окне загруженных книг</li>
|
||||||
|
<li>добавлена синхронизация обоев</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
version: '0.11.7',
|
version: '0.11.7',
|
||||||
releaseDate: '2022-07-12',
|
releaseDate: '2022-07-12',
|
||||||
|
|||||||
@@ -363,4 +363,50 @@ export function getBookTitle(fb2) {
|
|||||||
]).join(' - ');
|
]).join(' - ');
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resizeImage(dataUrl, toWidth, toHeight, quality = 0.9) {
|
||||||
|
return new Promise ((resolve, reject) => { (async() => {
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
let resolved = false;
|
||||||
|
img.onload = () => {
|
||||||
|
try {
|
||||||
|
let width = img.width;
|
||||||
|
let height = img.height;
|
||||||
|
|
||||||
|
if (width > height) {
|
||||||
|
if (width > toWidth) {
|
||||||
|
height = height * (toWidth / width);
|
||||||
|
width = toWidth;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (height > toHeight) {
|
||||||
|
width = width * (toHeight / height);
|
||||||
|
height = toHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.drawImage(img, 0, 0, width, height);
|
||||||
|
const result = canvas.toDataURL('image/jpeg', quality);
|
||||||
|
resolved = true;
|
||||||
|
resolve(result);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = reject;
|
||||||
|
|
||||||
|
img.src = dataUrl;
|
||||||
|
|
||||||
|
await sleep(1000);
|
||||||
|
if (!resolved)
|
||||||
|
reject('Не удалось изменить размер');
|
||||||
|
})().catch(reject); });
|
||||||
}
|
}
|
||||||
@@ -191,6 +191,8 @@ const settingDefaults = {
|
|||||||
|
|
||||||
recentShowSameBook: false,
|
recentShowSameBook: false,
|
||||||
recentSortMethod: '',
|
recentSortMethod: '',
|
||||||
|
|
||||||
|
needUpdateSettingsView: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const font of fonts)
|
for (const font of fonts)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ server {
|
|||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
|
||||||
server_name beta.liberama.top;
|
server_name beta.liberama.top;
|
||||||
|
set $liberama http://127.0.0.1:34082;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
@@ -15,15 +16,20 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:34082;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:34082;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@@ -32,6 +38,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
@@ -50,6 +61,7 @@ server {
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name b.beta.liberama.top;
|
server_name b.beta.liberama.top;
|
||||||
|
set $liberama http://127.0.0.1:34082;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
@@ -59,15 +71,20 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:34082;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:34082;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@@ -76,6 +93,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ server {
|
|||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
|
||||||
server_name beta.omnireader.ru;
|
server_name beta.omnireader.ru;
|
||||||
|
set $liberama http://127.0.0.1:34081;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
@@ -15,15 +16,20 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:34081;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:34081;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@@ -32,6 +38,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name beta.omnireader.ru;
|
server_name beta.omnireader.ru;
|
||||||
|
set $liberama http://127.0.0.1:34081;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
@@ -10,15 +11,20 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:34081;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:34081;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@@ -27,6 +33,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ server {
|
|||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
|
||||||
server_name liberama.top;
|
server_name liberama.top;
|
||||||
|
set $liberama http://127.0.0.1:55081;
|
||||||
|
|
||||||
client_max_body_size 100m;
|
client_max_body_size 100m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
@@ -26,12 +27,16 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:55081;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:55081;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
@@ -44,6 +49,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
@@ -62,6 +72,7 @@ server {
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name b.liberama.top;
|
server_name b.liberama.top;
|
||||||
|
set $liberama http://127.0.0.1:55081;
|
||||||
|
|
||||||
client_max_body_size 100m;
|
client_max_body_size 100m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
@@ -71,15 +82,20 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:55081;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:55081;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@@ -88,6 +104,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ server {
|
|||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
|
||||||
server_name omnireader.ru;
|
server_name omnireader.ru;
|
||||||
|
set $liberama http://127.0.0.1:44081;
|
||||||
|
|
||||||
client_max_body_size 100m;
|
client_max_body_size 100m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
@@ -15,12 +16,16 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:44081;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:44081;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
@@ -33,6 +38,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name omnireader.ru;
|
server_name omnireader.ru;
|
||||||
|
set $liberama http://127.0.0.1:44081;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
@@ -10,12 +11,16 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:44081;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:44081;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
@@ -27,6 +32,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "Liberama",
|
"name": "Liberama",
|
||||||
"version": "0.11.7",
|
"version": "0.11.8",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "Liberama",
|
"name": "Liberama",
|
||||||
"version": "0.11.7",
|
"version": "0.11.8",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "CC0-1.0",
|
"license": "CC0-1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Liberama",
|
"name": "Liberama",
|
||||||
"version": "0.11.7",
|
"version": "0.11.8",
|
||||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||||
"license": "CC0-1.0",
|
"license": "CC0-1.0",
|
||||||
"repository": "bookpauk/liberama",
|
"repository": "bookpauk/liberama",
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
jembaDb: [
|
jembaDb: [
|
||||||
|
{
|
||||||
|
dbName: 'app',
|
||||||
|
thread: true,
|
||||||
|
openAll: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
dbName: 'reader-storage',
|
dbName: 'reader-storage',
|
||||||
thread: true,
|
thread: true,
|
||||||
@@ -49,14 +54,14 @@ module.exports = {
|
|||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
serverName: '1',
|
serverName: '1',
|
||||||
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top'
|
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top', 'book_update_checker'
|
||||||
ip: '0.0.0.0',
|
ip: '0.0.0.0',
|
||||||
port: '33080',
|
port: '33080',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
remoteWebDavStorage: false,
|
|
||||||
/*
|
/*
|
||||||
|
remoteWebDavStorage: false,
|
||||||
remoteWebDavStorage: {
|
remoteWebDavStorage: {
|
||||||
url: '127.0.0.1:1900',
|
url: '127.0.0.1:1900',
|
||||||
username: '',
|
username: '',
|
||||||
@@ -64,5 +69,12 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
remoteStorage: false,
|
||||||
|
/*
|
||||||
|
remoteStorage: {
|
||||||
|
url: 'https://127.0.0.1:11900',
|
||||||
|
accessToken: '',
|
||||||
|
},
|
||||||
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
95
server/controllers/BookUpdateCheckerController.js
Normal file
95
server/controllers/BookUpdateCheckerController.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
const WebSocket = require ('ws');
|
||||||
|
//const _ = require('lodash');
|
||||||
|
|
||||||
|
const log = new (require('../core/AppLogger'))().log;//singleton
|
||||||
|
//const utils = require('../core/utils');
|
||||||
|
|
||||||
|
const cleanPeriod = 1*60*1000;//1 минута
|
||||||
|
const closeSocketOnIdle = 5*60*1000;//5 минут
|
||||||
|
|
||||||
|
class BookUpdateCheckerController {
|
||||||
|
constructor(wss, config) {
|
||||||
|
this.config = config;
|
||||||
|
this.isDevelopment = (config.branch == 'development');
|
||||||
|
|
||||||
|
//this.readerStorage = new JembaReaderStorage();
|
||||||
|
|
||||||
|
this.wss = wss;
|
||||||
|
|
||||||
|
wss.on('connection', (ws) => {
|
||||||
|
ws.on('message', (message) => {
|
||||||
|
this.onMessage(ws, message.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', (err) => {
|
||||||
|
log(LM_ERR, err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
periodicClean() {
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
this.wss.clients.forEach((ws) => {
|
||||||
|
if (!ws.lastActivity || now - ws.lastActivity > closeSocketOnIdle - 50) {
|
||||||
|
ws.terminate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onMessage(ws, message) {
|
||||||
|
let req = {};
|
||||||
|
try {
|
||||||
|
if (this.isDevelopment) {
|
||||||
|
log(`WebSocket-IN: ${message.substr(0, 4000)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
req = JSON.parse(message);
|
||||||
|
|
||||||
|
ws.lastActivity = Date.now();
|
||||||
|
|
||||||
|
//pong for WebSocketConnection
|
||||||
|
this.send({_rok: 1}, req, ws);
|
||||||
|
|
||||||
|
switch (req.action) {
|
||||||
|
case 'test':
|
||||||
|
await this.test(req, ws); break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Action not found: ${req.action}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.send({error: e.message}, req, ws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(res, req, ws) {
|
||||||
|
if (ws.readyState == WebSocket.OPEN) {
|
||||||
|
ws.lastActivity = Date.now();
|
||||||
|
let r = res;
|
||||||
|
if (req.requestId)
|
||||||
|
r = Object.assign({requestId: req.requestId}, r);
|
||||||
|
|
||||||
|
const message = JSON.stringify(r);
|
||||||
|
ws.send(message);
|
||||||
|
|
||||||
|
if (this.isDevelopment) {
|
||||||
|
log(`WebSocket-OUT: ${message.substr(0, 4000)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Actions ------------------------------------------------------------------
|
||||||
|
async test(req, ws) {
|
||||||
|
this.send({message: 'Liberama project is awesome'}, req, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BookUpdateCheckerController;
|
||||||
@@ -68,24 +68,6 @@ 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;
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ class WebSocketController {
|
|||||||
ws.on('message', (message) => {
|
ws.on('message', (message) => {
|
||||||
this.onMessage(ws, message.toString());
|
this.onMessage(ws, message.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ws.on('error', (err) => {
|
||||||
|
log(LM_ERR, err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||||
@@ -66,10 +70,12 @@ class WebSocketController {
|
|||||||
await this.workerGetState(req, ws); break;
|
await this.workerGetState(req, ws); break;
|
||||||
case 'worker-get-state-finish':
|
case 'worker-get-state-finish':
|
||||||
await this.workerGetStateFinish(req, ws); break;
|
await this.workerGetStateFinish(req, ws); break;
|
||||||
case 'reader-restore-cached-file':
|
|
||||||
await this.readerRestoreCachedFile(req, ws); break;
|
|
||||||
case 'reader-storage':
|
case 'reader-storage':
|
||||||
await this.readerStorageDo(req, ws); break;
|
await this.readerStorageDo(req, ws); break;
|
||||||
|
case 'upload-file-buf':
|
||||||
|
await this.uploadFileBuf(req, ws); break;
|
||||||
|
case 'upload-file-touch':
|
||||||
|
await this.uploadFileTouch(req, ws); break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Action not found: ${req.action}`);
|
throw new Error(`Action not found: ${req.action}`);
|
||||||
@@ -149,15 +155,6 @@ class WebSocketController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readerRestoreCachedFile(req, ws) {
|
|
||||||
if (!req.path)
|
|
||||||
throw new Error(`key 'path' is empty`);
|
|
||||||
|
|
||||||
const workerId = this.readerWorker.restoreCachedFile(req.path);
|
|
||||||
const state = this.workerState.getState(workerId);
|
|
||||||
this.send((state ? state : {}), req, ws);
|
|
||||||
}
|
|
||||||
|
|
||||||
async readerStorageDo(req, ws) {
|
async readerStorageDo(req, ws) {
|
||||||
if (!req.body)
|
if (!req.body)
|
||||||
throw new Error(`key 'body' is empty`);
|
throw new Error(`key 'body' is empty`);
|
||||||
@@ -168,6 +165,20 @@ class WebSocketController {
|
|||||||
|
|
||||||
this.send(await this.readerStorage.doAction(req.body), req, ws);
|
this.send(await this.readerStorage.doAction(req.body), req, ws);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async uploadFileBuf(req, ws) {
|
||||||
|
if (!req.buf)
|
||||||
|
throw new Error(`key 'buf' is empty`);
|
||||||
|
|
||||||
|
this.send({url: await this.readerWorker.saveFileBuf(req.buf)}, req, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFileTouch(req, ws) {
|
||||||
|
if (!req.url)
|
||||||
|
throw new Error(`key 'url' is empty`);
|
||||||
|
|
||||||
|
this.send({url: await this.readerWorker.uploadFileTouch(req.url)}, req, ws);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = WebSocketController;
|
module.exports = WebSocketController;
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ module.exports = {
|
|||||||
ReaderController: require('./ReaderController'),
|
ReaderController: require('./ReaderController'),
|
||||||
WorkerController: require('./WorkerController'),
|
WorkerController: require('./WorkerController'),
|
||||||
WebSocketController: require('./WebSocketController'),
|
WebSocketController: require('./WebSocketController'),
|
||||||
|
BookUpdateCheckerController: require('./BookUpdateCheckerController'),
|
||||||
}
|
}
|
||||||
0
server/core/BookUpdateChecker/BUCClient.js
Normal file
0
server/core/BookUpdateChecker/BUCClient.js
Normal file
24
server/core/BookUpdateChecker/BUCServer.js
Normal file
24
server/core/BookUpdateChecker/BUCServer.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
let instance = null;
|
||||||
|
|
||||||
|
//singleton
|
||||||
|
class BUCServer {
|
||||||
|
constructor(config) {
|
||||||
|
if (!instance) {
|
||||||
|
this.config = Object.assign({}, config);
|
||||||
|
|
||||||
|
this.config.tempDownloadDir = `${config.tempDir}/download`;
|
||||||
|
fs.ensureDirSync(this.config.tempDownloadDir);
|
||||||
|
|
||||||
|
this.down = new FileDownloader(config.maxUploadFileSize);
|
||||||
|
|
||||||
|
instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async main() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BUCServer;
|
||||||
@@ -23,7 +23,7 @@ class FileDownloader {
|
|||||||
estSize = res.headers['content-length'];
|
estSize = res.headers['content-length'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (estSize > this.limitDownloadSize) {
|
if (this.limitDownloadSize && estSize > this.limitDownloadSize) {
|
||||||
throw new Error('Файл слишком большой');
|
throw new Error('Файл слишком большой');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ 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 RemoteStorage = require('../RemoteStorage');
|
||||||
|
const JembaConnManager = require('../../db/JembaConnManager');//singleton
|
||||||
|
const ayncExit = new (require('../AsyncExit'))();
|
||||||
|
|
||||||
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 cleanDirPeriod = 60*60*1000;//каждый час
|
||||||
|
const remoteSendPeriod = 119*1000;//примерно раз 2 минуты
|
||||||
|
|
||||||
const queue = new LimitedQueue(5, 100, 2*60*1000 + 15000);//2 минуты ожидание подвижек
|
const queue = new LimitedQueue(5, 100, 2*60*1000 + 15000);//2 минуты ожидание подвижек
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
@@ -33,15 +37,37 @@ class ReaderWorker {
|
|||||||
this.decomp = new FileDecompressor(3*config.maxUploadFileSize);
|
this.decomp = new FileDecompressor(3*config.maxUploadFileSize);
|
||||||
this.bookConverter = new BookConverter(this.config);
|
this.bookConverter = new BookConverter(this.config);
|
||||||
|
|
||||||
this.remoteWebDavStorage = false;
|
this.connManager = new JembaConnManager();
|
||||||
if (config.remoteWebDavStorage) {
|
this.appDb = this.connManager.db['app'];
|
||||||
this.remoteWebDavStorage = new RemoteWebDavStorage(
|
|
||||||
Object.assign({maxContentLength: 3*config.maxUploadFileSize}, config.remoteWebDavStorage)
|
this.remoteStorage = false;
|
||||||
|
if (config.remoteStorage) {
|
||||||
|
this.remoteStorage = new RemoteStorage(
|
||||||
|
Object.assign({maxContentLength: 3*config.maxUploadFileSize}, config.remoteStorage)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.periodicCleanDir(this.config.tempPublicDir, this.config.maxTempPublicDirSize, cleanDirPeriod);
|
this.dirConfigArr = [
|
||||||
this.periodicCleanDir(this.config.uploadDir, this.config.maxUploadPublicDirSize, cleanDirPeriod);
|
{
|
||||||
|
dir: this.config.tempPublicDir,
|
||||||
|
remoteDir: '/tmp',
|
||||||
|
maxSize: this.config.maxTempPublicDirSize,
|
||||||
|
moveToRemote: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dir: this.config.uploadDir,
|
||||||
|
remoteDir: '/upload',
|
||||||
|
maxSize: this.config.maxUploadPublicDirSize,
|
||||||
|
moveToRemote: true,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
//преобразуем в объект для большего удобства
|
||||||
|
this.dirConfig = {};
|
||||||
|
for (const configRec of this.dirConfigArr)
|
||||||
|
this.dirConfig[configRec.remoteDir] = configRec;
|
||||||
|
|
||||||
|
this.remoteFilesToSend = [];
|
||||||
|
this.periodicCleanDir();//no await
|
||||||
|
|
||||||
instance = this;
|
instance = this;
|
||||||
}
|
}
|
||||||
@@ -54,7 +80,6 @@ class ReaderWorker {
|
|||||||
let decompDir = '';
|
let decompDir = '';
|
||||||
let downloadedFilename = '';
|
let downloadedFilename = '';
|
||||||
let isUploaded = false;
|
let isUploaded = false;
|
||||||
let isRestored = false;
|
|
||||||
let convertFilename = '';
|
let convertFilename = '';
|
||||||
|
|
||||||
const overLoadMes = 'Слишком большая очередь загрузки. Пожалуйста, попробуйте позже.';
|
const overLoadMes = 'Слишком большая очередь загрузки. Пожалуйста, попробуйте позже.';
|
||||||
@@ -94,8 +119,7 @@ class ReaderWorker {
|
|||||||
if (!await fs.pathExists(downloadedFilename)) {
|
if (!await fs.pathExists(downloadedFilename)) {
|
||||||
//если удалено из upload, попробуем восстановить из удаленного хранилища
|
//если удалено из upload, попробуем восстановить из удаленного хранилища
|
||||||
try {
|
try {
|
||||||
downloadedFilename = await this.restoreRemoteFile(fileHash);
|
await this.restoreRemoteFile(fileHash, '/upload');
|
||||||
isRestored = true;
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
throw new Error('Файл не найден на сервере (возможно был удален как устаревший). Пожалуйста, загрузите файл с диска на сервер заново.');
|
throw new Error('Файл не найден на сервере (возможно был удален как устаревший). Пожалуйста, загрузите файл с диска на сервер заново.');
|
||||||
}
|
}
|
||||||
@@ -144,32 +168,12 @@ class ReaderWorker {
|
|||||||
const finishFilename = path.basename(compFilename);
|
const finishFilename = path.basename(compFilename);
|
||||||
wState.finish({path: `/tmp/${finishFilename}`, size: stat.size});
|
wState.finish({path: `/tmp/${finishFilename}`, size: stat.size});
|
||||||
|
|
||||||
//лениво сохраним compFilename в удаленном хранилище
|
//асинхронно через 30 сек добавим в очередь на отправку
|
||||||
if (this.remoteWebDavStorage) {
|
//т.к. gzipFileIfNotExists может переупаковать файл
|
||||||
(async() => {
|
(async() => {
|
||||||
await utils.sleep(20*1000);
|
await utils.sleep(30*1000);
|
||||||
try {
|
this.pushRemoteSend(compFilename, '/tmp');
|
||||||
//log(`remoteWebDavStorage.putFile ${path.basename(compFilename)}`);
|
})();
|
||||||
await this.remoteWebDavStorage.putFile(compFilename);
|
|
||||||
} catch (e) {
|
|
||||||
log(LM_ERR, e.stack);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
//лениво сохраним downloadedFilename в tmp и в удаленном хранилище в случае isUploaded
|
|
||||||
if (this.remoteWebDavStorage && isUploaded && !isRestored) {
|
|
||||||
(async() => {
|
|
||||||
await utils.sleep(30*1000);
|
|
||||||
try {
|
|
||||||
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
|
|
||||||
const compDownloadedFilename = await this.decomp.gzipFileIfNotExists(downloadedFilename, this.config.tempPublicDir, true);
|
|
||||||
await this.remoteWebDavStorage.putFile(compDownloadedFilename);
|
|
||||||
} catch (e) {
|
|
||||||
log(LM_ERR, e.stack);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(LM_ERR, e.stack);
|
log(LM_ERR, e.stack);
|
||||||
@@ -211,6 +215,7 @@ class ReaderWorker {
|
|||||||
|
|
||||||
if (!await fs.pathExists(outFilename)) {
|
if (!await fs.pathExists(outFilename)) {
|
||||||
await fs.move(file.path, outFilename);
|
await fs.move(file.path, outFilename);
|
||||||
|
this.pushRemoteSend(outFilename, '/upload');
|
||||||
} else {
|
} else {
|
||||||
await utils.touchFile(outFilename);
|
await utils.touchFile(outFilename);
|
||||||
await fs.remove(file.path);
|
await fs.remove(file.path);
|
||||||
@@ -219,14 +224,42 @@ class ReaderWorker {
|
|||||||
return `disk://${hash}`;
|
return `disk://${hash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async restoreRemoteFile(filename) {
|
async saveFileBuf(buf) {
|
||||||
|
const hash = await utils.getBufHash(buf, 'sha256', 'hex');
|
||||||
|
const outFilename = `${this.config.uploadDir}/${hash}`;
|
||||||
|
|
||||||
|
if (!await fs.pathExists(outFilename)) {
|
||||||
|
await fs.writeFile(outFilename, buf);
|
||||||
|
this.pushRemoteSend(outFilename, '/upload');
|
||||||
|
} else {
|
||||||
|
await utils.touchFile(outFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `disk://${hash}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFileTouch(url) {
|
||||||
|
const outFilename = `${this.config.uploadDir}/${url.replace('disk://', '')}`;
|
||||||
|
|
||||||
|
await utils.touchFile(outFilename);
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreRemoteFile(filename, remoteDir) {
|
||||||
|
let targetDir = '';
|
||||||
|
if (this.dirConfig[remoteDir])
|
||||||
|
targetDir = this.dirConfig[remoteDir].dir;
|
||||||
|
else
|
||||||
|
throw new Error(`restoreRemoteFile: unknown remoteDir value (${remoteDir})`);
|
||||||
|
|
||||||
const basename = path.basename(filename);
|
const basename = path.basename(filename);
|
||||||
const targetName = `${this.config.tempPublicDir}/${basename}`;
|
const targetName = `${targetDir}/${basename}`;
|
||||||
|
|
||||||
if (!await fs.pathExists(targetName)) {
|
if (!await fs.pathExists(targetName)) {
|
||||||
let found = false;
|
let found = false;
|
||||||
if (this.remoteWebDavStorage) {
|
if (this.remoteStorage) {
|
||||||
found = await this.remoteWebDavStorage.getFileSuccess(targetName);
|
found = await this.remoteStorage.getFileSuccess(targetName, remoteDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
@@ -237,83 +270,170 @@ class ReaderWorker {
|
|||||||
return targetName;
|
return targetName;
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreCachedFile(filename) {
|
pushRemoteSend(fileName, remoteDir) {
|
||||||
const workerId = this.workerState.generateWorkerId();
|
if (this.remoteStorage
|
||||||
const wState = this.workerState.getControl(workerId);
|
&& this.dirConfig[remoteDir]
|
||||||
wState.set({state: 'start'});
|
&& this.dirConfig[remoteDir].moveToRemote) {
|
||||||
|
this.remoteFilesToSend.push({fileName, remoteDir});
|
||||||
(async() => {
|
|
||||||
try {
|
|
||||||
wState.set({state: 'download', step: 1, totalSteps: 1, path: filename, progress: 0});
|
|
||||||
|
|
||||||
const targetName = await this.restoreRemoteFile(filename);
|
|
||||||
const stat = await fs.stat(targetName);
|
|
||||||
|
|
||||||
const basename = path.basename(filename);
|
|
||||||
wState.finish({path: `/tmp/${basename}`, size: stat.size, progress: 100});
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message.indexOf('404') < 0)
|
|
||||||
log(LM_ERR, e.stack);
|
|
||||||
wState.set({state: 'error', error: e.message});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return workerId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async periodicCleanDir(dir, maxSize, timeout) {
|
|
||||||
try {
|
|
||||||
const list = await fs.readdir(dir);
|
|
||||||
|
|
||||||
let size = 0;
|
|
||||||
let files = [];
|
|
||||||
for (const name of list) {
|
|
||||||
const stat = await fs.stat(`${dir}/${name}`);
|
|
||||||
if (!stat.isDirectory()) {
|
|
||||||
size += stat.size;
|
|
||||||
files.push({name, stat});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log(`clean dir ${dir}, maxSize=${maxSize}, found ${files.length} files, total size=${size}`);
|
|
||||||
|
|
||||||
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
let j = 0;
|
|
||||||
while (i < files.length && size > maxSize) {
|
|
||||||
const file = files[i];
|
|
||||||
const oldFile = `${dir}/${file.name}`;
|
|
||||||
|
|
||||||
let remoteSuccess = true;
|
|
||||||
//отправляем только this.config.tempPublicDir
|
|
||||||
if (this.remoteWebDavStorage && dir === this.config.tempPublicDir) {
|
|
||||||
remoteSuccess = false;
|
|
||||||
try {
|
|
||||||
//log(`remoteWebDavStorage.putFile ${path.basename(oldFile)}`);
|
|
||||||
await this.remoteWebDavStorage.putFile(oldFile);
|
|
||||||
remoteSuccess = true;
|
|
||||||
} catch (e) {
|
|
||||||
log(LM_ERR, e.stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//реально удаляем только если сохранили в хранилище
|
|
||||||
if (remoteSuccess || size > maxSize*1.2) {
|
|
||||||
await fs.remove(oldFile);
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
|
|
||||||
size -= file.stat.size;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
log(`removed ${j} files`);
|
|
||||||
} catch(e) {
|
|
||||||
log(LM_ERR, e.stack);
|
|
||||||
} finally {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.periodicCleanDir(dir, maxSize, timeout);
|
|
||||||
}, timeout);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async remoteSendFile(sendFileRec) {
|
||||||
|
const {fileName, remoteDir} = sendFileRec;
|
||||||
|
const sent = this.remoteSent;
|
||||||
|
|
||||||
|
if (!fileName || sent[fileName])
|
||||||
|
return;
|
||||||
|
|
||||||
|
log(`remoteSendFile ${remoteDir}/${path.basename(fileName)}`);
|
||||||
|
|
||||||
|
//отправляем в remoteStorage
|
||||||
|
await this.remoteStorage.putFile(fileName, remoteDir);
|
||||||
|
|
||||||
|
sent[fileName] = true;
|
||||||
|
await this.appDb.insert({table: 'remote_sent', ignore: true, rows: [{id: fileName, remoteDir}]});
|
||||||
|
}
|
||||||
|
|
||||||
|
async remoteSendAll() {
|
||||||
|
if (!this.remoteStorage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const newSendQueue = [];
|
||||||
|
while (this.remoteFilesToSend.length) {
|
||||||
|
const sendFileRec = this.remoteFilesToSend.shift();
|
||||||
|
|
||||||
|
if (sendFileRec.remoteDir
|
||||||
|
&& this.dirConfig[sendFileRec.remoteDir]
|
||||||
|
&& this.dirConfig[sendFileRec.remoteDir].moveToRemote) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.remoteSendFile(sendFileRec);
|
||||||
|
} catch (e) {
|
||||||
|
newSendQueue.push(sendFileRec)
|
||||||
|
log(LM_ERR, e.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.remoteFilesToSend = newSendQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanDir(config) {
|
||||||
|
const {dir, remoteDir, maxSize, moveToRemote} = config;
|
||||||
|
const sent = this.remoteSent;
|
||||||
|
|
||||||
|
const list = await fs.readdir(dir);
|
||||||
|
|
||||||
|
let size = 0;
|
||||||
|
let files = [];
|
||||||
|
for (const filename of list) {
|
||||||
|
const filePath = `${dir}/${filename}`;
|
||||||
|
const stat = await fs.stat(filePath);
|
||||||
|
if (!stat.isDirectory()) {
|
||||||
|
size += stat.size;
|
||||||
|
files.push({name: filePath, stat});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(LM_WARN, `clean dir ${dir}, maxSize=${maxSize}, found ${files.length} files, total size=${size}`);
|
||||||
|
|
||||||
|
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
|
||||||
|
|
||||||
|
//удаленное хранилище
|
||||||
|
if (moveToRemote && this.remoteStorage) {
|
||||||
|
const foundFiles = new Set();
|
||||||
|
for (const file of files) {
|
||||||
|
foundFiles.add(file.name);
|
||||||
|
|
||||||
|
//отсылаем на всякий случай перед удалением, если вдруг remoteSendAll не справился
|
||||||
|
try {
|
||||||
|
await this.remoteSendFile({fileName: file.name, remoteDir});
|
||||||
|
} catch (e) {
|
||||||
|
log(LM_ERR, e.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//почистим remoteSent и БД
|
||||||
|
//несколько неоптимально, таскает все записи из таблицы
|
||||||
|
const rows = await this.appDb.select({table: 'remote_sent'});
|
||||||
|
for (const row of rows) {
|
||||||
|
if ((row.remoteDir === remoteDir && !foundFiles.has(row.id))
|
||||||
|
|| !this.dirConfig[row.remoteDir]) {
|
||||||
|
delete sent[row.id];
|
||||||
|
await this.appDb.delete({table: 'remote_sent', where: `@@id(${this.appDb.esc(row.id)})`});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
let j = 0;
|
||||||
|
while (i < files.length && size > maxSize) {
|
||||||
|
const file = files[i];
|
||||||
|
const oldFile = file.name;
|
||||||
|
|
||||||
|
//реально удаляем только если сохранили в хранилище или размер dir увеличен в 1.5 раза
|
||||||
|
if (!(moveToRemote && this.remoteStorage)
|
||||||
|
|| (moveToRemote && this.remoteStorage && sent[oldFile])
|
||||||
|
|| size > maxSize*1.5) {
|
||||||
|
await fs.remove(oldFile);
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
|
||||||
|
size -= file.stat.size;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(LM_WARN, `removed ${j} files`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async periodicCleanDir() {
|
||||||
|
try {
|
||||||
|
if (!this.remoteSent)
|
||||||
|
this.remoteSent = {};
|
||||||
|
|
||||||
|
//инициализация this.remoteSent
|
||||||
|
if (this.remoteStorage) {
|
||||||
|
const rows = await this.appDb.select({table: 'remote_sent'});
|
||||||
|
for (const row of rows) {
|
||||||
|
this.remoteSent[row.id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastCleanDirTime = 0;
|
||||||
|
let lastRemoteSendTime = 0;
|
||||||
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
|
//отсылка в удаленное хранилище
|
||||||
|
if (Date.now() - lastRemoteSendTime >= remoteSendPeriod) {
|
||||||
|
try {
|
||||||
|
await this.remoteSendAll();
|
||||||
|
} catch(e) {
|
||||||
|
log(LM_ERR, e.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRemoteSendTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
//чистка папок
|
||||||
|
if (Date.now() - lastCleanDirTime >= cleanDirPeriod) {
|
||||||
|
for (const config of Object.values(this.dirConfig)) {
|
||||||
|
try {
|
||||||
|
await this.cleanDir(config);
|
||||||
|
} catch(e) {
|
||||||
|
log(LM_ERR, e.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCleanDirTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.sleep(60*1000);//интервал проверки 1 минута
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(LM_FATAL, e.message);
|
||||||
|
ayncExit.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ReaderWorker;
|
module.exports = ReaderWorker;
|
||||||
98
server/core/RemoteStorage.js
Normal file
98
server/core/RemoteStorage.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const WebSocketConnection = require('./WebSocketConnection');
|
||||||
|
|
||||||
|
class RemoteStorage {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = Object.assign({}, config);
|
||||||
|
this.config.maxContentLength = this.config.maxContentLength || 10*1024*1024;
|
||||||
|
|
||||||
|
this.accessToken = this.config.accessToken;
|
||||||
|
|
||||||
|
this.wsc = new WebSocketConnection(config.url, 10, 30, {rejectUnauthorized: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
async wsRequest(query) {
|
||||||
|
const response = await this.wsc.message(
|
||||||
|
await this.wsc.send(Object.assign({accessToken: this.accessToken}, query), 600),
|
||||||
|
600
|
||||||
|
);
|
||||||
|
if (response.error)
|
||||||
|
throw new Error(response.error);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async wsStat(fileName) {
|
||||||
|
return await this.wsRequest({action: 'get-stat', fileName});
|
||||||
|
}
|
||||||
|
|
||||||
|
async wsGetFile(fileName) {
|
||||||
|
return this.wsRequest({action: 'get-file', fileName});
|
||||||
|
}
|
||||||
|
|
||||||
|
async wsPutFile(fileName, data) {//data base64 encoded string
|
||||||
|
return this.wsRequest({action: 'put-file', fileName, data});
|
||||||
|
}
|
||||||
|
|
||||||
|
async wsDelFile(fileName) {
|
||||||
|
return this.wsRequest({action: 'del-file', fileName});
|
||||||
|
}
|
||||||
|
|
||||||
|
makeRemoteFileName(fileName, dir = '') {
|
||||||
|
const base = path.basename(fileName);
|
||||||
|
if (base.length > 3) {
|
||||||
|
return `${dir}/${base.substr(0, 3)}/${base}`;
|
||||||
|
} else {
|
||||||
|
return `${dir}/${base}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async putFile(fileName, dir = '') {
|
||||||
|
if (!await fs.pathExists(fileName)) {
|
||||||
|
throw new Error(`File not found: ${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteFilename = this.makeRemoteFileName(fileName, dir);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const localStat = await fs.stat(fileName);
|
||||||
|
let remoteStat = await this.wsStat(remoteFilename);
|
||||||
|
remoteStat = remoteStat.stat;
|
||||||
|
|
||||||
|
if (remoteStat.isFile && localStat.size == remoteStat.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.wsDelFile(remoteFilename);
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fs.readFile(fileName, 'base64');
|
||||||
|
await this.wsPutFile(remoteFilename, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFile(fileName, dir = '') {
|
||||||
|
if (await fs.pathExists(fileName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteFilename = this.makeRemoteFileName(fileName, dir);
|
||||||
|
|
||||||
|
const response = await this.wsGetFile(remoteFilename);
|
||||||
|
await fs.writeFile(fileName, response.data, 'base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFileSuccess(filename, dir = '') {
|
||||||
|
try {
|
||||||
|
await this.getFile(filename, dir);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RemoteStorage;
|
||||||
@@ -46,16 +46,16 @@ class RemoteWebDavStorage {
|
|||||||
return await this.wdc.createDirectory(dirname);
|
return await this.wdc.createDirectory(dirname);
|
||||||
}
|
}
|
||||||
|
|
||||||
async putFile(filename) {
|
async putFile(filename, dir = '') {
|
||||||
if (!await fs.pathExists(filename)) {
|
if (!await fs.pathExists(filename)) {
|
||||||
throw new Error(`File not found: ${filename}`);
|
throw new Error(`File not found: ${filename}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const base = path.basename(filename);
|
const base = path.basename(filename);
|
||||||
let remoteFilename = `/${base}`;
|
let remoteFilename = `${dir}/${base}`;
|
||||||
|
|
||||||
if (base.length > 3) {
|
if (base.length > 3) {
|
||||||
const remoteDir = `/${base.substr(0, 3)}`;
|
const remoteDir = `${dir}/${base.substr(0, 3)}`;
|
||||||
try {
|
try {
|
||||||
await this.mkdir(remoteDir);
|
await this.mkdir(remoteDir);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -79,24 +79,24 @@ class RemoteWebDavStorage {
|
|||||||
await this.writeFile(remoteFilename, data);
|
await this.writeFile(remoteFilename, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFile(filename) {
|
async getFile(filename, dir = '') {
|
||||||
if (await fs.pathExists(filename)) {
|
if (await fs.pathExists(filename)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const base = path.basename(filename);
|
const base = path.basename(filename);
|
||||||
let remoteFilename = `/${base}`;
|
let remoteFilename = `${dir}/${base}`;
|
||||||
if (base.length > 3) {
|
if (base.length > 3) {
|
||||||
remoteFilename = `/${base.substr(0, 3)}/${base}`;
|
remoteFilename = `${dir}/${base.substr(0, 3)}/${base}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await this.readFile(remoteFilename);
|
const data = await this.readFile(remoteFilename);
|
||||||
await fs.writeFile(filename, data);
|
await fs.writeFile(filename, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFileSuccess(filename) {
|
async getFileSuccess(filename, dir = '') {
|
||||||
try {
|
try {
|
||||||
await this.getFile(filename);
|
await this.getFile(filename, dir);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ const cleanPeriod = 5*1000;//5 секунд
|
|||||||
|
|
||||||
class WebSocketConnection {
|
class WebSocketConnection {
|
||||||
//messageLifeTime в секундах (проверка каждый cleanPeriod интервал)
|
//messageLifeTime в секундах (проверка каждый cleanPeriod интервал)
|
||||||
constructor(url, openTimeoutSecs = 10, messageLifeTimeSecs = 30) {
|
constructor(url, openTimeoutSecs = 10, messageLifeTimeSecs = 30, webSocketOptions = {}) {
|
||||||
this.WebSocket = (isBrowser ? WebSocket : require('ws'));
|
this.WebSocket = (isBrowser ? WebSocket : require('ws'));
|
||||||
this.url = url;
|
this.url = url;
|
||||||
|
this.webSocketOptions = webSocketOptions;
|
||||||
|
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
|
|
||||||
this.listeners = [];
|
this.listeners = [];
|
||||||
this.messageQueue = [];
|
this.messageQueue = [];
|
||||||
this.messageLifeTime = messageLifeTimeSecs*1000;
|
this.messageLifeTime = messageLifeTimeSecs*1000;
|
||||||
@@ -91,10 +94,10 @@ class WebSocketConnection {
|
|||||||
const url = this.url || `${protocol}//${window.location.host}/ws`;
|
const url = this.url || `${protocol}//${window.location.host}/ws`;
|
||||||
this.ws = new this.WebSocket(url);
|
this.ws = new this.WebSocket(url);
|
||||||
} else {
|
} else {
|
||||||
this.ws = new this.WebSocket(this.url);
|
this.ws = new this.WebSocket(this.url, this.webSocketOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onopen = (e) => {
|
const onopen = () => {
|
||||||
this.connecting = false;
|
this.connecting = false;
|
||||||
resolve(this.ws);
|
resolve(this.ws);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ function getFileHash(filename, hashName, enc) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBufHash(buf, hashName, enc) {
|
||||||
|
const hash = crypto.createHash(hashName);
|
||||||
|
hash.update(buf);
|
||||||
|
return hash.digest(enc);
|
||||||
|
}
|
||||||
|
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
@@ -129,6 +135,7 @@ module.exports = {
|
|||||||
fromBase36,
|
fromBase36,
|
||||||
bufferRemoveZeroes,
|
bufferRemoveZeroes,
|
||||||
getFileHash,
|
getFileHash,
|
||||||
|
getBufHash,
|
||||||
sleep,
|
sleep,
|
||||||
toUnixTime,
|
toUnixTime,
|
||||||
randomHexString,
|
randomHexString,
|
||||||
|
|||||||
12
server/db/jembaMigrations/app/001-create.js
Normal file
12
server/db/jembaMigrations/app/001-create.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
up: [
|
||||||
|
['create', {
|
||||||
|
table: 'remote_sent'
|
||||||
|
}],
|
||||||
|
],
|
||||||
|
down: [
|
||||||
|
['drop', {
|
||||||
|
table: 'remote_sent'
|
||||||
|
}],
|
||||||
|
]
|
||||||
|
};
|
||||||
6
server/db/jembaMigrations/app/index.js
Normal file
6
server/db/jembaMigrations/app/index.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
table: 'migration1',
|
||||||
|
data: [
|
||||||
|
{id: 1, name: 'create', data: require('./001-create')}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
server/db/jembaMigrations/book-update-server/001-create.js
Normal file
16
server/db/jembaMigrations/book-update-server/001-create.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
up: [
|
||||||
|
['create', {
|
||||||
|
table: 'checked',
|
||||||
|
index: [
|
||||||
|
{field: 'queryTime', type: 'number'},
|
||||||
|
{field: 'checkTime', type: 'number'},
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
],
|
||||||
|
down: [
|
||||||
|
['drop', {
|
||||||
|
table: 'checked'
|
||||||
|
}],
|
||||||
|
]
|
||||||
|
};
|
||||||
6
server/db/jembaMigrations/book-update-server/index.js
Normal file
6
server/db/jembaMigrations/book-update-server/index.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
table: 'migration1',
|
||||||
|
data: [
|
||||||
|
{id: 1, name: 'create', data: require('./001-create')}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
//'app': require('./jembaMigrations/app'),
|
'app': require('./app'),
|
||||||
'reader-storage': require('./reader-storage'),
|
'reader-storage': require('./reader-storage'),
|
||||||
|
'book-update-server': require('./book-update-server'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
require('tls').DEFAULT_MIN_VERSION = 'TLSv1';
|
require('tls').DEFAULT_MIN_VERSION = 'TLSv1';
|
||||||
|
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
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');
|
||||||
@@ -11,6 +11,8 @@ const ayncExit = new (require('./core/AsyncExit'))();
|
|||||||
|
|
||||||
let log = null;
|
let log = null;
|
||||||
|
|
||||||
|
const maxPayloadSize = 50;//in MB
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
//config
|
//config
|
||||||
const configManager = new (require('./config'))();//singleton
|
const configManager = new (require('./config'))();//singleton
|
||||||
@@ -63,7 +65,7 @@ async function main() {
|
|||||||
if (serverCfg.mode !== 'none') {
|
if (serverCfg.mode !== 'none') {
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
const wss = new WebSocket.Server({ server, maxPayload: 10*1024*1024 });
|
const wss = new WebSocket.Server({ server, maxPayload: maxPayloadSize*1024*1024 });
|
||||||
|
|
||||||
const serverConfig = Object.assign({}, config, serverCfg);
|
const serverConfig = Object.assign({}, config, serverCfg);
|
||||||
|
|
||||||
@@ -75,20 +77,10 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.use(compression({ level: 1 }));
|
app.use(compression({ level: 1 }));
|
||||||
app.use(express.json({limit: '10mb'}));
|
app.use(express.json({limit: `${maxPayloadSize}mb`}));
|
||||||
if (devModule)
|
if (devModule)
|
||||||
devModule.logQueries(app);
|
devModule.logQueries(app);
|
||||||
|
|
||||||
app.use(express.static(serverConfig.publicDir, {
|
|
||||||
maxAge: '30d',
|
|
||||||
setHeaders: (res, filePath) => {
|
|
||||||
if (path.basename(path.dirname(filePath)) == 'tmp') {
|
|
||||||
res.set('Content-Type', 'application/xml');
|
|
||||||
res.set('Content-Encoding', 'gzip');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
require('./routes').initRoutes(app, wss, serverConfig);
|
require('./routes').initRoutes(app, wss, serverConfig);
|
||||||
|
|
||||||
if (devModule) {
|
if (devModule) {
|
||||||
|
|||||||
@@ -1,8 +1,24 @@
|
|||||||
const c = require('./controllers');
|
const fs = require('fs-extra');
|
||||||
const utils = require('./core/utils');
|
const path = require('path');
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
|
|
||||||
|
const ReaderWorker = require('./core/Reader/ReaderWorker');//singleton
|
||||||
|
const log = new (require('./core/AppLogger'))().log;//singleton
|
||||||
|
|
||||||
|
const c = require('./controllers');
|
||||||
|
const utils = require('./core/utils');
|
||||||
|
|
||||||
function initRoutes(app, wss, config) {
|
function initRoutes(app, wss, config) {
|
||||||
|
//эксклюзив для update_checker
|
||||||
|
if (config.mode === 'book_update_checker') {
|
||||||
|
new c.BookUpdateCheckerController(wss, config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initStatic(app, config);
|
||||||
|
|
||||||
const misc = new c.MiscController(config);
|
const 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);
|
||||||
@@ -29,7 +45,6 @@ function initRoutes(app, wss, 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], {}],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -77,6 +92,48 @@ function initRoutes(app, wss, config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initStatic(app, config) {
|
||||||
|
const readerWorker = new ReaderWorker(config);
|
||||||
|
|
||||||
|
//восстановление файлов в /tmp и /upload из webdav-storage, при необходимости
|
||||||
|
app.use(async(req, res, next) => {
|
||||||
|
if ((req.method !== 'GET' && req.method !== 'HEAD') ||
|
||||||
|
!(req.path.indexOf('/tmp/') === 0 || req.path.indexOf('/upload/') === 0)
|
||||||
|
) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = `${config.publicDir}${req.path}`;
|
||||||
|
|
||||||
|
//восстановим
|
||||||
|
try {
|
||||||
|
if (!await fs.pathExists(filePath)) {
|
||||||
|
if (req.path.indexOf('/tmp/') === 0) {
|
||||||
|
await readerWorker.restoreRemoteFile(req.path, '/tmp');
|
||||||
|
} else if (req.path.indexOf('/upload/') === 0) {
|
||||||
|
await readerWorker.restoreRemoteFile(req.path, '/upload');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
log(LM_ERR, `static::restoreRemoteFile ${req.path} > ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
const tmpDir = `${config.publicDir}/tmp`;
|
||||||
|
app.use(express.static(config.publicDir, {
|
||||||
|
maxAge: '30d',
|
||||||
|
|
||||||
|
setHeaders: (res, filePath) => {
|
||||||
|
if (path.dirname(filePath) == tmpDir) {
|
||||||
|
res.set('Content-Type', 'application/xml');
|
||||||
|
res.set('Content-Encoding', 'gzip');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initRoutes
|
initRoutes
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user