Compare commits

...

21 Commits

Author SHA1 Message Date
Book Pauk
440d1b3ba0 Merge branch 'release/0.8.2-1' 2020-01-21 16:14:15 +07:00
Book Pauk
9c7a6c64b0 Небольшие поправки 2020-01-21 16:13:38 +07:00
Book Pauk
7cc63fe849 Добавлена автоматическая отправка загруженной книги удаленное хранилище 2020-01-21 15:53:23 +07:00
Book Pauk
5647e8219d Мелкий рефакторинг 2020-01-21 14:58:42 +07:00
Book Pauk
81629fab7a Замена webdav-fs на webdav 2020-01-21 13:54:21 +07:00
Book Pauk
992d2033f3 Merge tag '0.8.2' into develop
0.8.2
2020-01-20 21:49:08 +07:00
Book Pauk
d52d4a1278 Merge branch 'release/0.8.2' 2020-01-20 21:49:00 +07:00
Book Pauk
57a44c5952 Версия 0.8.2 2020-01-20 21:48:31 +07:00
Book Pauk
a04161ac7c Добавил принудительную загрузку книги в обход кэша, если указан URL 2020-01-20 21:44:09 +07:00
Book Pauk
47e46f13c3 Добавлен работа с RemoteWebDavStorage, в т.ч. через api 2020-01-20 21:39:55 +07:00
Book Pauk
5535bd91c8 В конфиг добавлена опция remoteWebDavStorage 2020-01-20 21:37:31 +07:00
Book Pauk
8747a00de6 Поправлен баг 2020-01-20 21:36:44 +07:00
Book Pauk
c926b86926 Добавлен пакет webdav-fs 2020-01-20 21:22:27 +07:00
Book Pauk
010ac9aa7c Доработка api, восстановление кэшированного файла из хранилища 2020-01-20 21:21:13 +07:00
Book Pauk
4ab0c337f1 Рефакторинг 2020-01-15 16:20:46 +07:00
Book Pauk
f814c42fdd Поправлен баг в getStateFinish 2020-01-15 16:06:28 +07:00
Book Pauk
02aee3e625 Добавлена переупаковка файла книги по максимуму через 5 сек после загрузки и конвертирования 2020-01-15 15:49:45 +07:00
Book Pauk
52a32cfdd1 Добавлена обработка ошибок JSON.parse 2020-01-12 20:06:50 +07:00
Book Pauk
6faa7b2efe Уменьшение запросов get-state к api, добавлен метод get-state-finish 2020-01-12 18:51:12 +07:00
Book Pauk
f8481413c9 Мелкий рефакторинг 2020-01-12 17:03:34 +07:00
Book Pauk
0951d01383 Merge tag '0.8.1-1' into develop
0.8.1-1
2020-01-10 21:47:58 +07:00
13 changed files with 507 additions and 48 deletions

View File

@@ -1,7 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import * as utils from '../share/utils';
const api = axios.create({ const api = axios.create({
baseURL: '/api/reader' baseURL: '/api/reader'
}); });
@@ -11,8 +9,50 @@ const workerApi = axios.create({
}); });
class Reader { class Reader {
async getStateFinish(workerId, callback) {
if (!callback) callback = () => {};
//присылается текст, состоящий из json-объектов state каждые 300ms, с разделителем splitter между ними
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
const res = last.split(splitter).pop();
if (res) {
try {
callback(JSON.parse(res));
} catch (e) {
//
}
}
}
});
//берем последний state
response = response.data.split(splitter).pop();
if (response) {
try {
response = JSON.parse(response);
} catch (e) {
response = false;
}
}
return response;
}
async loadBook(opts, callback) { async loadBook(opts, callback) {
const refreshPause = 300;
if (!callback) callback = () => {}; if (!callback) callback = () => {};
let response = await api.post('/load-book', opts); let response = await api.post('/load-book', opts);
@@ -22,53 +62,98 @@ class Reader {
throw new Error('Неверный ответ api'); throw new Error('Неверный ответ api');
callback({totalSteps: 4}); callback({totalSteps: 4});
callback(response.data);
let i = 0; response = await this.getStateFinish(workerId, callback);
while (1) {// eslint-disable-line no-constant-condition
callback(response.data);
if (response.data.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл if (response) {
if (response.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
callback({step: 4}); callback({step: 4});
const book = await this.loadCachedBook(response.data.path, callback); const book = await this.loadCachedBook(response.path, callback, false, (response.size ? response.size : -1));
return Object.assign({}, response.data, {data: book.data}); return Object.assign({}, response, {data: book.data});
} }
if (response.data.state == 'error') {
let errMes = response.data.error; if (response.state == 'error') {
let errMes = response.error;
if (errMes.indexOf('getaddrinfo') >= 0 || if (errMes.indexOf('getaddrinfo') >= 0 ||
errMes.indexOf('ECONNRESET') >= 0 || errMes.indexOf('ECONNRESET') >= 0 ||
errMes.indexOf('EINVAL') >= 0 || errMes.indexOf('EINVAL') >= 0 ||
errMes.indexOf('404') >= 0) errMes.indexOf('404') >= 0)
errMes = `Ресурс не найден по адресу: ${response.data.url}`; errMes = `Ресурс не найден по адресу: ${response.url}`;
throw new Error(errMes); throw new Error(errMes);
} }
if (i > 0) } else {
await utils.sleep(refreshPause); throw new Error('Пустой ответ сервера');
i++;
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
throw new Error('Слишком долгое время ожидания');
}
//проверка воркера
const prevProgress = response.data.progress;
const prevState = response.data.state;
response = await workerApi.post('/get-state', {workerId});
i = (prevProgress != response.data.progress || prevState != response.data.state ? 1 : i);
} }
} }
async checkUrl(url) { async checkUrl(url) {
return await axios.head(url, {headers: {'Cache-Control': 'no-cache'}}); let fileExists = false;
} try {
await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
async loadCachedBook(url, callback) { fileExists = true;
const response = await axios.head(url); } catch (e) {
//
let estSize = 1000000;
if (response.headers['content-length']) {
estSize = response.headers['content-length'];
} }
//восстановим при необходимости файл на сервере из удаленного облака
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}); callback({state: 'loading', progress: 0});
//получение размера файла
let fileExists = false;
if (estSize < 0) {
try {
response = await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
if (response.headers['content-length']) {
estSize = response.headers['content-length'];
}
fileExists = true;
} catch (e) {
//
}
}
//восстановим при необходимости файл на сервере из удаленного облака
if (restore && !fileExists) {
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);
}
if (response.size && estSize < 0) {
estSize = response.size;
}
}
//получение файла
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;
@@ -77,7 +162,7 @@ class Reader {
callback({progress: Math.round((progress.loaded*100)/estSize)}); callback({progress: Math.round((progress.loaded*100)/estSize)});
} }
} }
//загрузка
return await axios.get(url, options); return await axios.get(url, options);
} }

View File

@@ -112,7 +112,7 @@ class LoaderPage extends Vue {
submitUrl() { submitUrl() {
if (this.bookUrl) { if (this.bookUrl) {
this.$emit('load-book', {url: this.bookUrl}); this.$emit('load-book', {url: this.bookUrl, force: true});
this.bookUrl = ''; this.bookUrl = '';
} }
} }

View File

@@ -1,4 +1,15 @@
export const versionHistory = [ export const versionHistory = [
{
showUntil: '2020-01-19',
header: '0.8.2 (2020-01-20)',
content:
`
<ul>
<li>внутренние оптимизации</li>
</ul>
`
},
{ {
showUntil: '2020-01-06', showUntil: '2020-01-06',
header: '0.8.1 (2020-01-07)', header: '0.8.1 (2020-01-07)',

86
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "Liberama", "name": "Liberama",
"version": "0.8.1", "version": "0.8.2",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -1725,6 +1725,11 @@
} }
} }
}, },
"base-64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
},
"base-x": { "base-x": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.7.tgz", "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.7.tgz",
@@ -5719,6 +5724,11 @@
"parse-passwd": "^1.0.0" "parse-passwd": "^1.0.0"
} }
}, },
"hot-patcher": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-0.5.0.tgz",
"integrity": "sha512-2Uu2W0s8+dnqXzdlg0MRsRzPoDCs1wVjOGSyMRRaMzLDX4bgHw6xDYKccsWafXPPxQpkQfEjgW6+17pwcg60bw=="
},
"hsl-regex": { "hsl-regex": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz",
@@ -6902,6 +6912,11 @@
} }
} }
}, },
"merge": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz",
"integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ=="
},
"merge-descriptors": { "merge-descriptors": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@@ -7882,6 +7897,11 @@
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true "dev": true
}, },
"path-posix": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz",
"integrity": "sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8="
},
"path-to-regexp": { "path-to-regexp": {
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@@ -10423,6 +10443,11 @@
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
"dev": true "dev": true
}, },
"querystringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz",
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA=="
},
"randombytes": { "randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -10709,6 +10734,11 @@
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true "dev": true
}, },
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"resize-observer-polyfill": { "resize-observer-polyfill": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
@@ -12489,6 +12519,11 @@
} }
} }
}, },
"url-join": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
},
"url-loader": { "url-loader": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz", "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz",
@@ -12508,6 +12543,15 @@
} }
} }
}, },
"url-parse": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
"integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
"requires": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"url-parse-lax": { "url-parse-lax": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
@@ -12737,6 +12781,32 @@
"neo-async": "^2.5.0" "neo-async": "^2.5.0"
} }
}, },
"webdav": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/webdav/-/webdav-2.10.1.tgz",
"integrity": "sha512-3UfnjGTAqSM9MW3Rpt1KrY1KneYK0wPCFryHTncqw1OP1pyiniT3uYhVpgmH6za/TkWOfnTnKCDKhwrLJFdzow==",
"requires": {
"axios": "^0.19.0",
"base-64": "^0.1.0",
"hot-patcher": "^0.5.0",
"merge": "^1.2.1",
"minimatch": "^3.0.4",
"path-posix": "^1.0.0",
"url-join": "^4.0.1",
"url-parse": "^1.4.7",
"xml2js": "^0.4.19"
},
"dependencies": {
"axios": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.1.tgz",
"integrity": "sha512-Yl+7nfreYKaLRvAvjNPkvfjnQHJM1yLBY3zhqAwcJSwR/6ETkanUgylgtIvkvz0xJ+p/vZuNw8X7Hnb7Whsbpw==",
"requires": {
"follow-redirects": "1.5.10"
}
}
}
},
"webpack": { "webpack": {
"version": "4.40.2", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.40.2.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.40.2.tgz",
@@ -13005,6 +13075,20 @@
"mkdirp": "^0.5.1" "mkdirp": "^0.5.1"
} }
}, },
"xml2js": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
"requires": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
}
},
"xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
},
"xtend": { "xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "Liberama", "name": "Liberama",
"version": "0.8.1", "version": "0.8.2",
"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",
@@ -84,6 +84,7 @@
"vue-router": "^3.1.3", "vue-router": "^3.1.3",
"vuex": "^3.1.1", "vuex": "^3.1.1",
"vuex-persistedstate": "^2.5.4", "vuex-persistedstate": "^2.5.4",
"webdav": "^2.10.1",
"zip-stream": "^2.1.2" "zip-stream": "^2.1.2"
} }
} }

View File

@@ -21,7 +21,7 @@ module.exports = {
maxTempPublicDirSize: 512*1024*1024,//512Мб maxTempPublicDirSize: 512*1024*1024,//512Мб
maxUploadPublicDirSize: 200*1024*1024,//100Мб maxUploadPublicDirSize: 200*1024*1024,//100Мб
useExternalBookConverter: false, useExternalBookConverter: false,
db: [ db: [
{ {
@@ -45,5 +45,14 @@ module.exports = {
}, },
], ],
remoteWebDavStorage: false,
/*
remoteWebDavStorage: {
url: '127.0.0.1:1900',
username: '',
password: '',
},
*/
}; };

View File

@@ -10,6 +10,7 @@ const propsToSave = [
'useExternalBookConverter', 'useExternalBookConverter',
'servers', 'servers',
'remoteWebDavStorage',
]; ];
let instance = null; let instance = null;

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ const unbzip2Stream = require('unbzip2-stream');
const tar = require('tar-fs'); const tar = require('tar-fs');
const ZipStreamer = require('./ZipStreamer'); const ZipStreamer = require('./ZipStreamer');
const appLogger = new (require('./AppLogger'))();//singleton
const utils = require('./utils'); const utils = require('./utils');
const FileDetector = require('./FileDetector'); const FileDetector = require('./FileDetector');
@@ -189,9 +190,9 @@ class FileDecompressor {
}); });
} }
async gzipFile(inputFile, outputFile) { async gzipFile(inputFile, outputFile, level = 1) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const gzip = zlib.createGzip({level: 1}); const gzip = zlib.createGzip({level});
const input = fs.createReadStream(inputFile); const input = fs.createReadStream(inputFile);
const output = fs.createWriteStream(outputFile); const output = fs.createWriteStream(outputFile);
@@ -208,7 +209,21 @@ class FileDecompressor {
const outFilename = `${outDir}/${hash}`; const outFilename = `${outDir}/${hash}`;
if (!await fs.pathExists(outFilename)) { if (!await fs.pathExists(outFilename)) {
await this.gzipFile(filename, outFilename); await this.gzipFile(filename, outFilename, 1);
// переупакуем через некоторое время на максималках
const filenameCopy = `${filename}.copy`;
await fs.copy(filename, filenameCopy);
(async() => {
await utils.sleep(5000);
const filenameGZ = `${filename}.gz`;
await this.gzipFile(filenameCopy, filenameGZ, 9);
await fs.move(filenameGZ, outFilename, {overwrite: true});
await fs.remove(filenameCopy);
})().catch((e) => { if (appLogger.inited) appLogger.log(LM_ERR, `FileDecompressor.gzipFileIfNotExists: ${e.message}`) });
} else { } else {
await utils.touchFile(outFilename); await utils.touchFile(outFilename);
} }

View File

@@ -5,10 +5,13 @@ const WorkerState = require('../WorkerState');//singleton
const FileDownloader = require('../FileDownloader'); const FileDownloader = require('../FileDownloader');
const FileDecompressor = require('../FileDecompressor'); const FileDecompressor = require('../FileDecompressor');
const BookConverter = require('./BookConverter'); const BookConverter = require('./BookConverter');
const RemoteWebDavStorage = require('../RemoteWebDavStorage');
const utils = require('../utils'); const utils = require('../utils');
const log = new (require('../AppLogger'))().log;//singleton const log = new (require('../AppLogger'))().log;//singleton
const cleanDirPeriod = 60*60*1000;//1 раз в час
let instance = null; let instance = null;
//singleton //singleton
@@ -28,8 +31,15 @@ class ReaderWorker {
this.decomp = new FileDecompressor(); this.decomp = new FileDecompressor();
this.bookConverter = new BookConverter(this.config); this.bookConverter = new BookConverter(this.config);
this.periodicCleanDir(this.config.tempPublicDir, this.config.maxTempPublicDirSize, 60*60*1000);//1 раз в час this.remoteWebDavStorage = false;
this.periodicCleanDir(this.config.uploadDir, this.config.maxUploadPublicDirSize, 60*60*1000);//1 раз в час if (config.remoteWebDavStorage) {
this.remoteWebDavStorage = new RemoteWebDavStorage(
Object.assign({maxContentLength: config.maxUploadFileSize}, config.remoteWebDavStorage)
);
}
this.periodicCleanDir(this.config.tempPublicDir, this.config.maxTempPublicDirSize, cleanDirPeriod);
this.periodicCleanDir(this.config.uploadDir, this.config.maxUploadPublicDirSize, cleanDirPeriod);
instance = this; instance = this;
} }
@@ -39,7 +49,6 @@ class ReaderWorker {
async loadBook(opts, wState) { async loadBook(opts, wState) {
const url = opts.url; const url = opts.url;
let errMes = '';
let decompDir = ''; let decompDir = '';
let downloadedFilename = ''; let downloadedFilename = '';
let isUploaded = false; let isUploaded = false;
@@ -87,17 +96,31 @@ class ReaderWorker {
}); });
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256 //сжимаем файл в tmp, если там уже нет с тем же именем-sha256
const compFilename = await this.decomp.gzipFileIfNotExists(convertFilename, `${this.config.tempPublicDir}`); const compFilename = await this.decomp.gzipFileIfNotExists(convertFilename, this.config.tempPublicDir);
const stat = await fs.stat(compFilename);
wState.set({progress: 100}); wState.set({progress: 100});
//finish //finish
const finishFilename = path.basename(compFilename); const finishFilename = path.basename(compFilename);
wState.finish({path: `/tmp/${finishFilename}`}); wState.finish({path: `/tmp/${finishFilename}`, size: stat.size});
//лениво сохраним compFilename в удаленном хранилище
if (this.remoteWebDavStorage) {
(async() => {
await utils.sleep(20*1000);
try {
//log(`remoteWebDavStorage.putFile ${path.basename(compFilename)}`);
await this.remoteWebDavStorage.putFile(compFilename);
} catch (e) {
log(LM_ERR, e.stack);
}
})();
}
} catch (e) { } catch (e) {
log(LM_ERR, e.stack); log(LM_ERR, e.stack);
wState.set({state: 'error', error: (errMes ? errMes : e.message)}); wState.set({state: 'error', error: e.message});
} finally { } finally {
//clean //clean
if (decompDir) if (decompDir)
@@ -133,6 +156,41 @@ class ReaderWorker {
return `file://${hash}`; return `file://${hash}`;
} }
restoreCachedFile(filename) {
const workerId = this.workerState.generateWorkerId();
const wState = this.workerState.getControl(workerId);
wState.set({state: 'start'});
(async() => {
try {
wState.set({state: 'download', step: 1, totalSteps: 1, path: filename, progress: 0});
const basename = path.basename(filename);
const targetName = `${this.config.tempPublicDir}/${basename}`;
if (!await fs.pathExists(targetName)) {
let found = false;
if (this.remoteWebDavStorage) {
found = await this.remoteWebDavStorage.getFileSuccess(targetName);
}
if (!found) {
throw new Error('404 Файл не найден');
}
}
const stat = await fs.stat(targetName);
wState.finish({path: `/tmp/${basename}`, size: stat.size, progress: 100});
} catch (e) {
if (e.message.indexOf('404') < 0)
log(LM_ERR, e.stack);
wState.set({state: 'error', error: e.message});
}
})();
return workerId;
}
async periodicCleanDir(dir, maxSize, timeout) { async periodicCleanDir(dir, maxSize, timeout) {
try { try {
const list = await fs.readdir(dir); const list = await fs.readdir(dir);
@@ -153,7 +211,16 @@ class ReaderWorker {
let i = 0; let i = 0;
while (i < files.length && size > maxSize) { while (i < files.length && size > maxSize) {
const file = files[i]; const file = files[i];
await fs.remove(`${dir}/${file.name}`); const oldFile = `${dir}/${file.name}`;
if (this.remoteWebDavStorage) {
try {
//log(`remoteWebDavStorage.putFile ${path.basename(oldFile)}`);
await this.remoteWebDavStorage.putFile(oldFile);
} catch (e) {
log(LM_ERR, e.stack);
}
}
await fs.remove(oldFile);
size -= file.stat.size; size -= file.stat.size;
i++; i++;
} }

View File

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

View File

@@ -28,7 +28,9 @@ function initRoutes(app, config) {
['POST', '/api/reader/load-book', reader.loadBook.bind(reader), [aAll], {}], ['POST', '/api/reader/load-book', reader.loadBook.bind(reader), [aAll], {}],
['POST', '/api/reader/storage', reader.storage.bind(reader), [aAll], {}], ['POST', '/api/reader/storage', reader.storage.bind(reader), [aAll], {}],
['POST', '/api/reader/upload-file', [upload.single('file'), reader.uploadFile.bind(reader)], [aAll], {}], ['POST', '/api/reader/upload-file', [upload.single('file'), reader.uploadFile.bind(reader)], [aAll], {}],
['POST', '/api/reader/restore-cached-file', reader.restoreCachedFile.bind(reader), [aAll], {}],
['POST', '/api/worker/get-state', worker.getState.bind(worker), [aAll], {}], ['POST', '/api/worker/get-state', worker.getState.bind(worker), [aAll], {}],
['POST', '/api/worker/get-state-finish', worker.getStateFinish.bind(worker), [aAll], {}],
]; ];
//to app //to app