Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eedca4db9b | ||
|
|
1d352a76ce | ||
|
|
17670aabf9 | ||
|
|
3456b3d90e | ||
|
|
f3da5a9026 | ||
|
|
00cc63b7cd | ||
|
|
8df80ce738 | ||
|
|
12e7a783b0 | ||
|
|
be86a15351 | ||
|
|
2c5022e7b4 | ||
|
|
f4a996fcb9 | ||
|
|
fdbf508bbf | ||
|
|
500fafa5b2 | ||
|
|
bfa315c68b | ||
|
|
4972f085a3 | ||
|
|
9c13261929 | ||
|
|
e36dc4a913 | ||
|
|
4cccb56ee3 | ||
|
|
3199af570d | ||
|
|
7dad47b3c8 | ||
|
|
fbd50bad1d | ||
|
|
10469bae7b | ||
|
|
b6a000a001 | ||
|
|
59539e7e90 | ||
|
|
a2c41bc5ec | ||
|
|
c4a06858fb | ||
|
|
15b0f05a05 | ||
|
|
67feee9aa1 | ||
|
|
185fb57b8c | ||
|
|
e9039f8208 |
@@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import wsc from './webSocketConnection';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: '/api'
|
baseURL: '/api'
|
||||||
@@ -6,9 +7,20 @@ const api = axios.create({
|
|||||||
|
|
||||||
class Misc {
|
class Misc {
|
||||||
async loadConfig() {
|
async loadConfig() {
|
||||||
const response = await api.post('/config', {params: [
|
|
||||||
|
const query = {params: [
|
||||||
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
|
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
|
||||||
]});
|
]};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await wsc.open();
|
||||||
|
return await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
//если с WebSocket проблема, работаем по http
|
||||||
|
const response = await api.post('/config', query);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import * as utils from '../share/utils';
|
||||||
|
import wsc from './webSocketConnection';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: '/api/reader'
|
baseURL: '/api/reader'
|
||||||
@@ -9,44 +11,55 @@ const workerApi = axios.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
class Reader {
|
class Reader {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
async getStateFinish(workerId, callback) {
|
async getWorkerStateFinish(workerId, callback) {
|
||||||
if (!callback) callback = () => {};
|
if (!callback) callback = () => {};
|
||||||
|
|
||||||
//присылается текст, состоящий из json-объектов state каждые 300ms, с разделителем splitter между ними
|
let response = {};
|
||||||
const splitter = '-- aod2t5hDXU32bUFyqlFE next status --';
|
|
||||||
let lastIndex = 0;
|
|
||||||
let response = await workerApi.post('/get-state-finish', {workerId}, {
|
|
||||||
onDownloadProgress: progress => {
|
|
||||||
//небольая оптимизация, вместо простого responseText.split
|
|
||||||
const xhr = progress.target;
|
|
||||||
let currIndex = xhr.responseText.length;
|
|
||||||
if (lastIndex == currIndex)
|
|
||||||
return;
|
|
||||||
const last = xhr.responseText.substring(lastIndex, currIndex);
|
|
||||||
lastIndex = currIndex;
|
|
||||||
|
|
||||||
//быстрее будет last.split
|
try {
|
||||||
const res = last.split(splitter).pop();
|
await wsc.open();
|
||||||
if (res) {
|
const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
|
||||||
try {
|
|
||||||
callback(JSON.parse(res));
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
} catch (e) {
|
response = await wsc.message(requestId);
|
||||||
//
|
callback(response);
|
||||||
}
|
|
||||||
|
if (response.state == 'finish' || response.state == 'error') {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
//берем последний state
|
//если с WebSocket проблема, работаем по http
|
||||||
response = response.data.split(splitter).pop();
|
const refreshPause = 500;
|
||||||
|
let i = 0;
|
||||||
|
response = {};
|
||||||
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
|
const prevProgress = response.progress || 0;
|
||||||
|
const prevState = response.state || 0;
|
||||||
|
response = await workerApi.post('/get-state', {workerId});
|
||||||
|
response = response.data;
|
||||||
|
callback(response);
|
||||||
|
|
||||||
if (response) {
|
if (response.state == 'finish' || response.state == 'error') {
|
||||||
try {
|
break;
|
||||||
response = JSON.parse(response);
|
|
||||||
} catch (e) {
|
|
||||||
response = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (i > 0)
|
||||||
|
await utils.sleep(refreshPause);
|
||||||
|
|
||||||
|
i++;
|
||||||
|
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
||||||
|
throw new Error('Слишком долгое время ожидания');
|
||||||
|
}
|
||||||
|
//проверка воркера
|
||||||
|
i = (prevProgress != response.progress || prevState != response.state ? 1 : i);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -64,12 +77,12 @@ class Reader {
|
|||||||
callback({totalSteps: 4});
|
callback({totalSteps: 4});
|
||||||
callback(response.data);
|
callback(response.data);
|
||||||
|
|
||||||
response = await this.getStateFinish(workerId, callback);
|
response = await this.getWorkerStateFinish(workerId, callback);
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
if (response.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
|
if (response.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
|
||||||
callback({step: 4});
|
callback({step: 4});
|
||||||
const book = await this.loadCachedBook(response.path, callback, false, (response.size ? response.size : -1));
|
const book = await this.loadCachedBook(response.path, callback, response.size);
|
||||||
return Object.assign({}, response, {data: book.data});
|
return Object.assign({}, response, {data: book.data});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,75 +100,58 @@ class Reader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkUrl(url) {
|
async checkCachedBook(url) {
|
||||||
let fileExists = false;
|
let estSize = -1;
|
||||||
try {
|
try {
|
||||||
await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
|
const response = await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
|
||||||
fileExists = true;
|
|
||||||
|
if (response.headers['content-length']) {
|
||||||
|
estSize = response.headers['content-length'];
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
//восстановим при необходимости файл на сервере из удаленного облака
|
||||||
}
|
let response = null
|
||||||
|
|
||||||
//восстановим при необходимости файл на сервере из удаленного облака
|
|
||||||
if (!fileExists) {
|
|
||||||
let response = await api.post('/restore-cached-file', {path: url});
|
|
||||||
|
|
||||||
const workerId = response.data.workerId;
|
|
||||||
if (!workerId)
|
|
||||||
throw new Error('Неверный ответ api');
|
|
||||||
|
|
||||||
response = await this.getStateFinish(workerId);
|
|
||||||
if (response.state == 'error') {
|
|
||||||
throw new Error(response.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadCachedBook(url, callback, restore = true, estSize = -1) {
|
|
||||||
if (!callback) callback = () => {};
|
|
||||||
let response = null;
|
|
||||||
|
|
||||||
callback({state: 'loading', progress: 0});
|
|
||||||
|
|
||||||
//получение размера файла
|
|
||||||
let fileExists = false;
|
|
||||||
if (estSize < 0) {
|
|
||||||
try {
|
try {
|
||||||
response = await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
|
await wsc.open();
|
||||||
|
response = await wsc.message(wsc.send({action: 'reader-restore-cached-file', path: url}));
|
||||||
if (response.headers['content-length']) {
|
|
||||||
estSize = response.headers['content-length'];
|
|
||||||
}
|
|
||||||
fileExists = true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
console.error(e);
|
||||||
|
//если с WebSocket проблема, работаем по http
|
||||||
|
response = await api.post('/restore-cached-file', {path: url});
|
||||||
|
response = response.data;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
//восстановим при необходимости файл на сервере из удаленного облака
|
const workerId = response.workerId;
|
||||||
if (restore && !fileExists) {
|
|
||||||
response = await api.post('/restore-cached-file', {path: url});
|
|
||||||
|
|
||||||
const workerId = response.data.workerId;
|
|
||||||
if (!workerId)
|
if (!workerId)
|
||||||
throw new Error('Неверный ответ api');
|
throw new Error('Неверный ответ api');
|
||||||
|
|
||||||
response = await this.getStateFinish(workerId);
|
response = await this.getWorkerStateFinish(workerId);
|
||||||
if (response.state == 'error') {
|
if (response.state == 'error') {
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.size && estSize < 0) {
|
if (response.size && estSize < 0) {
|
||||||
estSize = response.size;
|
estSize = response.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return estSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCachedBook(url, callback, estSize = -1) {
|
||||||
|
if (!callback) callback = () => {};
|
||||||
|
|
||||||
|
callback({state: 'loading', progress: 0});
|
||||||
|
|
||||||
|
//получение размера файла
|
||||||
|
if (estSize && estSize < 0) {
|
||||||
|
estSize = await this.checkCachedBook(url);
|
||||||
|
}
|
||||||
|
|
||||||
//получение файла
|
//получение файла
|
||||||
estSize = (estSize > 0 ? estSize : 1000000);
|
estSize = (estSize > 0 ? estSize : 1000000);
|
||||||
const options = {
|
const options = {
|
||||||
onDownloadProgress: progress => {
|
onDownloadProgress: (progress) => {
|
||||||
while (progress.loaded > estSize) estSize *= 1.5;
|
while (progress.loaded > estSize) estSize *= 1.5;
|
||||||
|
|
||||||
if (callback)
|
if (callback)
|
||||||
@@ -199,13 +195,22 @@ class Reader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async storage(request) {
|
async storage(request) {
|
||||||
let response = await api.post('/storage', request);
|
let response = null;
|
||||||
|
try {
|
||||||
|
await wsc.open();
|
||||||
|
response = await wsc.message(wsc.send({action: 'reader-storage', body: request}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
//если с WebSocket проблема, работаем по http
|
||||||
|
response = await api.post('/storage', request);
|
||||||
|
response = response.data;
|
||||||
|
}
|
||||||
|
|
||||||
const state = response.data.state;
|
const state = response.state;
|
||||||
if (!state)
|
if (!state)
|
||||||
throw new Error('Неверный ответ api');
|
throw new Error('Неверный ответ api');
|
||||||
|
|
||||||
return response.data;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
176
client/api/webSocketConnection.js
Normal file
176
client/api/webSocketConnection.js
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
const cleanPeriod = 60*1000;//1 минута
|
||||||
|
|
||||||
|
class WebSocketConnection {
|
||||||
|
//messageLifeTime в минутах (cleanPeriod)
|
||||||
|
constructor(messageLifeTime = 5) {
|
||||||
|
this.ws = null;
|
||||||
|
this.timer = null;
|
||||||
|
this.listeners = [];
|
||||||
|
this.messageQueue = [];
|
||||||
|
this.messageLifeTime = messageLifeTime;
|
||||||
|
this.requestId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(listener) {
|
||||||
|
if (this.listeners.indexOf(listener) < 0)
|
||||||
|
this.listeners.push(Object.assign({regTime: Date.now()}, listener));
|
||||||
|
}
|
||||||
|
|
||||||
|
//рассылаем сообщение и удаляем те обработчики, которые его получили
|
||||||
|
emit(mes, isError) {
|
||||||
|
const len = this.listeners.length;
|
||||||
|
if (len > 0) {
|
||||||
|
let newListeners = [];
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
let emitted = false;
|
||||||
|
if (isError) {
|
||||||
|
if (listener.onError)
|
||||||
|
listener.onError(mes);
|
||||||
|
emitted = true;
|
||||||
|
} else {
|
||||||
|
if (listener.onMessage) {
|
||||||
|
if (listener.requestId) {
|
||||||
|
if (listener.requestId === mes.requestId) {
|
||||||
|
listener.onMessage(mes);
|
||||||
|
emitted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
listener.onMessage(mes);
|
||||||
|
emitted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emitted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emitted)
|
||||||
|
newListeners.push(listener);
|
||||||
|
}
|
||||||
|
this.listeners = newListeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.listeners.length != len;
|
||||||
|
}
|
||||||
|
|
||||||
|
open(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
||||||
|
resolve(this.ws);
|
||||||
|
} else {
|
||||||
|
let protocol = 'ws:';
|
||||||
|
if (window.location.protocol == 'https:') {
|
||||||
|
protocol = 'wss:'
|
||||||
|
}
|
||||||
|
|
||||||
|
url = url || `${protocol}//${window.location.host}/ws`;
|
||||||
|
|
||||||
|
this.ws = new WebSocket(url);
|
||||||
|
|
||||||
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
}
|
||||||
|
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||||
|
|
||||||
|
let resolved = false;
|
||||||
|
this.ws.onopen = (e) => {
|
||||||
|
resolved = true;
|
||||||
|
resolve(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onmessage = (e) => {
|
||||||
|
try {
|
||||||
|
const mes = JSON.parse(e.data);
|
||||||
|
this.messageQueue.push({regTime: Date.now(), mes});
|
||||||
|
|
||||||
|
let newMessageQueue = [];
|
||||||
|
for (const message of this.messageQueue) {
|
||||||
|
if (!this.emit(message.mes)) {
|
||||||
|
newMessageQueue.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messageQueue = newMessageQueue;
|
||||||
|
} catch (e) {
|
||||||
|
this.emit(e.message, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onerror = (e) => {
|
||||||
|
this.emit(e.message, true);
|
||||||
|
if (!resolved)
|
||||||
|
reject(e);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//timeout в минутах (cleanPeriod)
|
||||||
|
message(requestId, timeout = 2) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.addListener({
|
||||||
|
requestId,
|
||||||
|
timeout,
|
||||||
|
onMessage: (mes) => {
|
||||||
|
if (mes.error) {
|
||||||
|
reject(mes.error);
|
||||||
|
} else {
|
||||||
|
resolve(mes);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
send(req) {
|
||||||
|
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
||||||
|
const requestId = ++this.requestId;
|
||||||
|
this.ws.send(JSON.stringify(Object.assign({requestId}, req)));
|
||||||
|
return requestId;
|
||||||
|
} else {
|
||||||
|
throw new Error('WebSocket connection is not ready');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
||||||
|
this.ws.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
periodicClean() {
|
||||||
|
try {
|
||||||
|
this.timer = null;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
//чистка listeners
|
||||||
|
let newListeners = [];
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
if (now - listener.regTime < listener.timeout*cleanPeriod - 50) {
|
||||||
|
newListeners.push(listener);
|
||||||
|
} else {
|
||||||
|
if (listener.onError)
|
||||||
|
listener.onError('Время ожидания ответа истекло');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.listeners = newListeners;
|
||||||
|
|
||||||
|
//чистка messageQueue
|
||||||
|
let newMessageQueue = [];
|
||||||
|
for (const message of this.messageQueue) {
|
||||||
|
if (now - message.regTime < this.messageLifeTime*cleanPeriod - 50) {
|
||||||
|
newMessageQueue.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.messageQueue = newMessageQueue;
|
||||||
|
} finally {
|
||||||
|
if (this.ws.readyState == WebSocket.OPEN) {
|
||||||
|
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new WebSocketConnection();
|
||||||
@@ -719,15 +719,16 @@ class Reader extends Vue {
|
|||||||
case 'scrolling':
|
case 'scrolling':
|
||||||
case 'search':
|
case 'search':
|
||||||
case 'copyText':
|
case 'copyText':
|
||||||
case 'recentBooks':
|
case 'refresh':
|
||||||
case 'offlineMode':
|
case 'offlineMode':
|
||||||
|
case 'recentBooks':
|
||||||
case 'settings':
|
case 'settings':
|
||||||
if (this[`${button}Active`])
|
if (this.progressActive) {
|
||||||
|
classResult = classDisabled;
|
||||||
|
} else if (this[`${button}Active`]) {
|
||||||
classResult = classActive;
|
classResult = classActive;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
switch (button) {
|
|
||||||
case 'undoAction':
|
case 'undoAction':
|
||||||
if (this.actionCur <= 0)
|
if (this.actionCur <= 0)
|
||||||
classResult = classDisabled;
|
classResult = classDisabled;
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ class RecentBooksPage extends Vue {
|
|||||||
|
|
||||||
async downloadBook(fb2path) {
|
async downloadBook(fb2path) {
|
||||||
try {
|
try {
|
||||||
await readerApi.checkUrl(fb2path);
|
await readerApi.checkCachedBook(fb2path);
|
||||||
|
|
||||||
const d = this.$refs.download;
|
const d = this.$refs.download;
|
||||||
d.href = fb2path;
|
d.href = fb2path;
|
||||||
|
|||||||
@@ -464,7 +464,7 @@ class BookManager {
|
|||||||
|
|
||||||
addEventListener(listener) {
|
addEventListener(listener) {
|
||||||
if (this.eventListeners.indexOf(listener) < 0)
|
if (this.eventListeners.indexOf(listener) < 0)
|
||||||
this.eventListeners.push(listener);
|
this.eventListeners.push(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeEventListener(listener) {
|
removeEventListener(listener) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ server {
|
|||||||
server_name omnireader.ru;
|
server_name omnireader.ru;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
|
proxy_read_timeout 1h;
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
@@ -18,6 +19,13 @@ server {
|
|||||||
proxy_pass http://127.0.0.1:44081;
|
proxy_pass http://127.0.0.1:44081;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /ws {
|
||||||
|
proxy_pass http://127.0.0.1:44081;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
root /home/liberama/public;
|
root /home/liberama/public;
|
||||||
|
|
||||||
|
|||||||
59
docs/omnireader/omnireader_http
Normal file
59
docs/omnireader/omnireader_http
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name omnireader.ru;
|
||||||
|
|
||||||
|
client_max_body_size 50m;
|
||||||
|
proxy_read_timeout 1h;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_types *;
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://127.0.0.1:44081;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /ws {
|
||||||
|
proxy_pass http://127.0.0.1:44081;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /home/liberama/public;
|
||||||
|
|
||||||
|
location /tmp {
|
||||||
|
add_header Content-Type text/xml;
|
||||||
|
add_header Content-Encoding gzip;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
expires -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name old.omnireader.ru;
|
||||||
|
|
||||||
|
client_max_body_size 50m;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_types *;
|
||||||
|
|
||||||
|
root /home/oldreader;
|
||||||
|
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Обработка php файлов с помощью fpm
|
||||||
|
location ~ \.php$ {
|
||||||
|
try_files $uri =404;
|
||||||
|
include /etc/nginx/fastcgi.conf;
|
||||||
|
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
package-lock.json
generated
5
package-lock.json
generated
@@ -13075,6 +13075,11 @@
|
|||||||
"mkdirp": "^0.5.1"
|
"mkdirp": "^0.5.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ws": {
|
||||||
|
"version": "7.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz",
|
||||||
|
"integrity": "sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A=="
|
||||||
|
},
|
||||||
"xml2js": {
|
"xml2js": {
|
||||||
"version": "0.4.23",
|
"version": "0.4.23",
|
||||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
"vuex": "^3.1.1",
|
"vuex": "^3.1.1",
|
||||||
"vuex-persistedstate": "^2.5.4",
|
"vuex-persistedstate": "^2.5.4",
|
||||||
"webdav": "^2.10.1",
|
"webdav": "^2.10.1",
|
||||||
|
"ws": "^7.2.1",
|
||||||
"zip-stream": "^2.1.2"
|
"zip-stream": "^2.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ class ReaderController extends BaseController {
|
|||||||
const request = req.body;
|
const request = req.body;
|
||||||
let error = '';
|
let error = '';
|
||||||
try {
|
try {
|
||||||
if (!request.action)
|
if (!request.action)
|
||||||
throw new Error(`key 'action' is empty`);
|
throw new Error(`key 'action' is empty`);
|
||||||
if (!request.items || Array.isArray(request.data))
|
if (!request.items || Array.isArray(request.data))
|
||||||
throw new Error(`key 'items' is empty`);
|
throw new Error(`key 'items' is empty`);
|
||||||
|
|
||||||
return await this.readerStorage.doAction(request);
|
return await this.readerStorage.doAction(request);
|
||||||
|
|||||||
164
server/controllers/WebSocketController.js
Normal file
164
server/controllers/WebSocketController.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
const WebSocket = require ('ws');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton
|
||||||
|
const ReaderStorage = require('../core/Reader/ReaderStorage');//singleton
|
||||||
|
const WorkerState = require('../core/WorkerState');//singleton
|
||||||
|
const log = new (require('../core/AppLogger'))().log;//singleton
|
||||||
|
const utils = require('../core/utils');
|
||||||
|
|
||||||
|
const cleanPeriod = 1*60*1000;//1 минута
|
||||||
|
const closeSocketOnIdle = 5*60*1000;//5 минут
|
||||||
|
|
||||||
|
class WebSocketController {
|
||||||
|
constructor(wss, config) {
|
||||||
|
this.config = config;
|
||||||
|
this.isDevelopment = (config.branch == 'development');
|
||||||
|
|
||||||
|
this.readerStorage = new ReaderStorage();
|
||||||
|
this.readerWorker = new ReaderWorker(config);
|
||||||
|
this.workerState = new WorkerState();
|
||||||
|
|
||||||
|
this.wss = wss;
|
||||||
|
|
||||||
|
wss.on('connection', (ws) => {
|
||||||
|
ws.on('message', (message) => {
|
||||||
|
this.onMessage(ws, message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
periodicClean() {
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
this.wss.clients.forEach((ws) => {
|
||||||
|
if (!ws.lastActivity || now - ws.lastActivity > closeSocketOnIdle - 50) {
|
||||||
|
ws.terminate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onMessage(ws, message) {
|
||||||
|
let req = {};
|
||||||
|
try {
|
||||||
|
if (this.isDevelopment) {
|
||||||
|
log(`WebSocket-IN: ${message.substr(0, 4000)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.lastActivity = Date.now();
|
||||||
|
req = JSON.parse(message);
|
||||||
|
switch (req.action) {
|
||||||
|
case 'test':
|
||||||
|
await this.test(req, ws); break;
|
||||||
|
case 'get-config':
|
||||||
|
await this.getConfig(req, ws); break;
|
||||||
|
case 'worker-get-state':
|
||||||
|
await this.workerGetState(req, ws); break;
|
||||||
|
case 'worker-get-state-finish':
|
||||||
|
await this.workerGetStateFinish(req, ws); break;
|
||||||
|
case 'reader-restore-cached-file':
|
||||||
|
await this.readerRestoreCachedFile(req, ws); break;
|
||||||
|
case 'reader-storage':
|
||||||
|
await this.readerStorageDo(req, ws); break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Action not found: ${req.action}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.send({error: e.message}, req, ws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(res, req, ws) {
|
||||||
|
if (ws.readyState == WebSocket.OPEN) {
|
||||||
|
ws.lastActivity = Date.now();
|
||||||
|
let r = res;
|
||||||
|
if (req.requestId)
|
||||||
|
r = Object.assign({requestId: req.requestId}, r);
|
||||||
|
|
||||||
|
const message = JSON.stringify(r);
|
||||||
|
ws.send(message);
|
||||||
|
|
||||||
|
if (this.isDevelopment) {
|
||||||
|
log(`WebSocket-OUT: ${message.substr(0, 4000)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Actions ------------------------------------------------------------------
|
||||||
|
async test(req, ws) {
|
||||||
|
this.send({message: 'Liberama project is awesome'}, req, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfig(req, ws) {
|
||||||
|
if (Array.isArray(req.params)) {
|
||||||
|
this.send(_.pick(this.config, req.params), req, ws);
|
||||||
|
} else {
|
||||||
|
throw new Error('params is not an array');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async workerGetState(req, ws) {
|
||||||
|
if (!req.workerId)
|
||||||
|
throw new Error(`key 'workerId' is wrong`);
|
||||||
|
|
||||||
|
const state = this.workerState.getState(req.workerId);
|
||||||
|
this.send((state ? state : {}), req, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
async workerGetStateFinish(req, ws) {
|
||||||
|
if (!req.workerId)
|
||||||
|
throw new Error(`key 'workerId' is wrong`);
|
||||||
|
|
||||||
|
const refreshPause = 200;
|
||||||
|
let i = 0;
|
||||||
|
let state = {};
|
||||||
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
|
const prevProgress = state.progress || -1;
|
||||||
|
const prevState = state.state || '';
|
||||||
|
state = this.workerState.getState(req.workerId);
|
||||||
|
|
||||||
|
this.send((state ? state : {}), req, ws);
|
||||||
|
if (!state) break;
|
||||||
|
|
||||||
|
if (state.state != 'finish' && state.state != 'error')
|
||||||
|
await utils.sleep(refreshPause);
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
|
||||||
|
i++;
|
||||||
|
if (i > 2*60*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
||||||
|
this.send({state: 'error', error: 'Время ожидания процесса истекло'}, req, ws);
|
||||||
|
}
|
||||||
|
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async readerRestoreCachedFile(req, ws) {
|
||||||
|
if (!req.path)
|
||||||
|
throw new Error(`key 'path' is empty`);
|
||||||
|
|
||||||
|
const workerId = this.readerWorker.restoreCachedFile(req.path);
|
||||||
|
const state = this.workerState.getState(workerId);
|
||||||
|
this.send((state ? state : {}), req, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
async readerStorageDo(req, ws) {
|
||||||
|
if (!req.body)
|
||||||
|
throw new Error(`key 'body' is empty`);
|
||||||
|
if (!req.body.action)
|
||||||
|
throw new Error(`key 'action' is empty`);
|
||||||
|
if (!req.body.items || Array.isArray(req.body.data))
|
||||||
|
throw new Error(`key 'items' is empty`);
|
||||||
|
|
||||||
|
this.send(await this.readerStorage.doAction(req.body), req, ws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WebSocketController;
|
||||||
@@ -26,6 +26,7 @@ class WorkerController extends BaseController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: удалить бесполезную getStateFinish
|
||||||
async getStateFinish(req, res) {
|
async getStateFinish(req, res) {
|
||||||
const request = req.body;
|
const request = req.body;
|
||||||
let error = '';
|
let error = '';
|
||||||
@@ -47,10 +48,6 @@ class WorkerController extends BaseController {
|
|||||||
state = this.workerState.getState(request.workerId);
|
state = this.workerState.getState(request.workerId);
|
||||||
if (!state) break;
|
if (!state) break;
|
||||||
|
|
||||||
if (i == 0) {
|
|
||||||
state = Object.assign({dummy: '0'.repeat(1024)}, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.write(splitter + JSON.stringify(state));
|
res.write(splitter + JSON.stringify(state));
|
||||||
res.flush();
|
res.flush();
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ module.exports = {
|
|||||||
MiscController: require('./MiscController'),
|
MiscController: require('./MiscController'),
|
||||||
ReaderController: require('./ReaderController'),
|
ReaderController: require('./ReaderController'),
|
||||||
WorkerController: require('./WorkerController'),
|
WorkerController: require('./WorkerController'),
|
||||||
|
WebSocketController: require('./WebSocketController'),
|
||||||
}
|
}
|
||||||
@@ -212,7 +212,10 @@ class ReaderWorker {
|
|||||||
while (i < files.length && size > maxSize) {
|
while (i < files.length && size > maxSize) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
const oldFile = `${dir}/${file.name}`;
|
const oldFile = `${dir}/${file.name}`;
|
||||||
if (this.remoteWebDavStorage) {
|
|
||||||
|
//отправляем только this.config.tempPublicDir
|
||||||
|
//TODO: убрать в будущем, т.к. уже делается ленивое сохранение compFilename в удаленном хранилище
|
||||||
|
if (this.remoteWebDavStorage && dir === this.config.tempPublicDir) {
|
||||||
try {
|
try {
|
||||||
//log(`remoteWebDavStorage.putFile ${path.basename(oldFile)}`);
|
//log(`remoteWebDavStorage.putFile ${path.basename(oldFile)}`);
|
||||||
await this.remoteWebDavStorage.putFile(oldFile);
|
await this.remoteWebDavStorage.putFile(oldFile);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ const path = require('path');
|
|||||||
const argv = require('minimist')(process.argv.slice(2));
|
const argv = require('minimist')(process.argv.slice(2));
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const compression = require('compression');
|
const compression = require('compression');
|
||||||
|
const http = require('http');
|
||||||
|
const WebSocket = require ('ws');
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
//config
|
//config
|
||||||
@@ -46,10 +48,13 @@ async function main() {
|
|||||||
const config = new (require('./config'))().config;//singleton
|
const config = new (require('./config'))().config;//singleton
|
||||||
|
|
||||||
//servers
|
//servers
|
||||||
for (let server of config.servers) {
|
for (let serverCfg of config.servers) {
|
||||||
if (server.mode !== 'none') {
|
if (serverCfg.mode !== 'none') {
|
||||||
const app = express();
|
const app = express();
|
||||||
const serverConfig = Object.assign({}, config, server);
|
const server = http.createServer(app);
|
||||||
|
const wss = new WebSocket.Server({ server, maxPayload: 10*1024*1024 });
|
||||||
|
|
||||||
|
const serverConfig = Object.assign({}, config, serverCfg);
|
||||||
|
|
||||||
let devModule = undefined;
|
let devModule = undefined;
|
||||||
if (serverConfig.branch == 'development') {
|
if (serverConfig.branch == 'development') {
|
||||||
@@ -73,7 +78,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
require('./routes').initRoutes(app, serverConfig);
|
require('./routes').initRoutes(app, wss, serverConfig);
|
||||||
|
|
||||||
if (devModule) {
|
if (devModule) {
|
||||||
devModule.logErrors(app);
|
devModule.logErrors(app);
|
||||||
@@ -84,7 +89,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.listen(serverConfig.port, serverConfig.ip, function() {
|
server.listen(serverConfig.port, serverConfig.ip, function() {
|
||||||
log(`Server-${serverConfig.serverName} is ready on ${serverConfig.ip}:${serverConfig.port}, mode: ${serverConfig.mode}`);
|
log(`Server-${serverConfig.serverName} is ready on ${serverConfig.ip}:${serverConfig.port}, mode: ${serverConfig.mode}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ const c = require('./controllers');
|
|||||||
const utils = require('./core/utils');
|
const utils = require('./core/utils');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
|
|
||||||
function initRoutes(app, config) {
|
function initRoutes(app, wss, config) {
|
||||||
const misc = new c.MiscController(config);
|
const misc = new c.MiscController(config);
|
||||||
const reader = new c.ReaderController(config);
|
const reader = new c.ReaderController(config);
|
||||||
const worker = new c.WorkerController(config);
|
const worker = new c.WorkerController(config);
|
||||||
|
new c.WebSocketController(wss, config);
|
||||||
|
|
||||||
//access
|
//access
|
||||||
const [aAll, aNormal, aSite, aReader, aOmnireader] = // eslint-disable-line no-unused-vars
|
const [aAll, aNormal, aSite, aReader, aOmnireader] = // eslint-disable-line no-unused-vars
|
||||||
|
|||||||
Reference in New Issue
Block a user