-
diff --git a/client/quasar.js b/client/quasar.js
index 925a20f..9acf6b1 100644
--- a/client/quasar.js
+++ b/client/quasar.js
@@ -17,7 +17,7 @@ import {QBtn} from 'quasar/src/components/btn';
import {QBtnToggle} from 'quasar/src/components/btn-toggle';
import {QIcon} from 'quasar/src/components/icon';
//import {QSlider} from 'quasar/src/components/slider';
-//import {QTabs, QTab} from 'quasar/src/components/tabs';
+import {QTabs, QTab} from 'quasar/src/components/tabs';
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
//import {QSeparator} from 'quasar/src/components/separator';
//import {QList} from 'quasar/src/components/item';
@@ -52,7 +52,7 @@ const components = {
QBtnToggle,
QIcon,
//QSlider,
- //QTabs, QTab,
+ QTabs, QTab,
//QTabPanels, QTabPanel,
//QSeparator,
//QList,
diff --git a/client/share/utils.js b/client/share/utils.js
index f48aeb1..573cb6f 100644
--- a/client/share/utils.js
+++ b/client/share/utils.js
@@ -87,18 +87,6 @@ export async function copyTextToClipboard(text) {
return result;
}
-export function makeValidFilename(filename, repl = '_') {
- let f = filename.replace(/[\x00\\/:*"<>|]/g, repl); // eslint-disable-line no-control-regex
- f = f.trim();
- while (f.length && (f[f.length - 1] == '.' || f[f.length - 1] == '_')) {
- f = f.substring(0, f.length - 1);
- }
-
- if (f)
- return f;
- else
- throw new Error('Invalid filename');
-}
/*
export function formatDate(d, format = 'normal') {
switch (format) {
diff --git a/client/store/root.js b/client/store/root.js
index eb89ec2..6187bbe 100644
--- a/client/store/root.js
+++ b/client/store/root.js
@@ -9,6 +9,7 @@ const state = {
expandedSeries: [],
showCounts: true,
showRates: true,
+ showInfo: true,
showGenres: true,
showDates: false,
showDeleted: false,
diff --git a/package-lock.json b/package-lock.json
index 52bb9f2..d61ac87 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,19 +1,21 @@
{
"name": "inpx-web",
- "version": "1.1.4",
+ "version": "1.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "inpx-web",
- "version": "1.1.4",
+ "version": "1.2.0",
"hasInstallScript": true,
"license": "CC0-1.0",
"dependencies": {
"@quasar/extras": "^1.15.0",
"axios": "^0.27.2",
+ "chardet": "^1.5.0",
"express": "^4.18.1",
"fs-extra": "^10.1.0",
+ "iconv-lite": "^0.6.3",
"jembadb": "^5.0.2",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
@@ -2645,6 +2647,17 @@
"ms": "2.0.0"
}
},
+ "node_modules/body-parser/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/body-parser/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -2809,6 +2822,11 @@
"node": ">=4"
}
},
+ "node_modules/chardet": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-1.5.0.tgz",
+ "integrity": "sha512-Nj3VehbbFs/1ZnJJJaL3ztEf3Nu5Fs6YV/NBs6lyz/iDDHUU+X9QNk5QgPy1/5Rjtb/cGVf+NyazP7kVEJqKRg=="
+ },
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
@@ -4781,11 +4799,11 @@
}
},
"node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
@@ -7011,6 +7029,17 @@
"node": ">= 0.8"
}
},
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -10718,6 +10747,14 @@
"ms": "2.0.0"
}
},
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -10832,6 +10869,11 @@
"supports-color": "^5.3.0"
}
},
+ "chardet": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-1.5.0.tgz",
+ "integrity": "sha512-Nj3VehbbFs/1ZnJJJaL3ztEf3Nu5Fs6YV/NBs6lyz/iDDHUU+X9QNk5QgPy1/5Rjtb/cGVf+NyazP7kVEJqKRg=="
+ },
"chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
@@ -12311,11 +12353,11 @@
}
},
"iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"requires": {
- "safer-buffer": ">= 2.1.2 < 3"
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
}
},
"icss-utils": {
@@ -13869,6 +13911,14 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
}
}
},
diff --git a/package.json b/package.json
index 263460f..5015b26 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "inpx-web",
- "version": "1.1.4",
+ "version": "1.2.0",
"author": "Book Pauk
",
"license": "CC0-1.0",
"repository": "bookpauk/inpx-web",
@@ -51,8 +51,10 @@
"dependencies": {
"@quasar/extras": "^1.15.0",
"axios": "^0.27.2",
+ "chardet": "^1.5.0",
"express": "^4.18.1",
"fs-extra": "^10.1.0",
+ "iconv-lite": "^0.6.3",
"jembadb": "^5.0.2",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
diff --git a/server/controllers/WebSocketController.js b/server/controllers/WebSocketController.js
index 0dac9f6..8f9ed44 100644
--- a/server/controllers/WebSocketController.js
+++ b/server/controllers/WebSocketController.js
@@ -84,6 +84,8 @@ class WebSocketController {
await this.getGenreTree(req, ws); break;
case 'get-book-link':
await this.getBookLink(req, ws); break;
+ case 'get-book-info':
+ await this.getBookInfo(req, ws); break;
case 'get-inpx-file':
await this.getInpxFile(req, ws); break;
@@ -163,12 +165,19 @@ class WebSocketController {
}
async getBookLink(req, ws) {
- if (!utils.hasProp(req, 'bookPath'))
- throw new Error(`bookPath is empty`);
- if (!utils.hasProp(req, 'downFileName'))
- throw new Error(`downFileName is empty`);
+ if (!utils.hasProp(req, 'bookId'))
+ throw new Error(`bookId is empty`);
- const result = await this.webWorker.getBookLink({bookPath: req.bookPath, downFileName: req.downFileName});
+ const result = await this.webWorker.getBookLink(req.bookId);
+
+ this.send(result, req, ws);
+ }
+
+ async getBookInfo(req, ws) {
+ if (!utils.hasProp(req, 'bookId'))
+ throw new Error(`bookId is empty`);
+
+ const result = await this.webWorker.getBookInfo(req.bookId);
this.send(result, req, ws);
}
diff --git a/server/core/WebWorker.js b/server/core/WebWorker.js
index 05ee501..c862b0d 100644
--- a/server/core/WebWorker.js
+++ b/server/core/WebWorker.js
@@ -15,6 +15,7 @@ const ayncExit = new (require('./AsyncExit'))();
const log = new (require('./AppLogger'))().log;//singleton
const utils = require('./utils');
const genreTree = require('./genres');
+const Fb2Helper = require('./fb2/Fb2Helper');
//server states
const ssNormal = 'normal';
@@ -44,6 +45,7 @@ class WebWorker {
}
this.inpxHashCreator = new InpxHashCreator(config);
+ this.fb2Helper = new Fb2Helper();
this.inpxFileHash = '';
this.wState = this.workerState.getControl('server_state');
@@ -400,17 +402,36 @@ class WebWorker {
return link;
}
- async getBookLink(params) {
+ async getBookLink(bookId) {
this.checkMyState();
- const {bookPath, downFileName} = params;
-
try {
const db = this.db;
let link = '';
+ //найдем bookPath и downFileName
+ let rows = await db.select({table: 'book', where: `@@id(${db.esc(bookId)})`});
+ if (!rows.length)
+ throw new Error('404 Файл не найден');
+
+ const book = rows[0];
+ let downFileName = book.file;
+ const author = book.author.split(',');
+ const at = [author[0], book.title];
+ downFileName = utils.makeValidFileNameOrEmpty(at.filter(r => r).join(' - '))
+ || utils.makeValidFileNameOrEmpty(at[0])
+ || utils.makeValidFileNameOrEmpty(at[1])
+ || downFileName;
+ downFileName = downFileName.substring(0, 100);
+
+ const ext = `.${book.ext}`;
+ if (downFileName.substring(downFileName.length - ext.length) != ext)
+ downFileName += ext;
+
+ const bookPath = `${book.folder}/${book.file}${ext}`;
+
//найдем хеш
- const rows = await db.select({table: 'file_hash', where: `@@id(${db.esc(bookPath)})`});
+ rows = await db.select({table: 'file_hash', where: `@@id(${db.esc(bookPath)})`});
if (rows.length) {//хеш найден по bookPath
const hash = rows[0].hash;
const bookFile = `${this.config.filesDir}/${hash}`;
@@ -428,7 +449,7 @@ class WebWorker {
if (!link)
throw new Error('404 Файл не найден');
- return {link};
+ return {link, bookPath, downFileName};
} catch(e) {
log(LM_ERR, `getBookLink error: ${e.message}`);
if (e.message.indexOf('ENOENT') >= 0)
@@ -437,48 +458,70 @@ class WebWorker {
}
}
- /*
- async restoreBookFile(publicPath) {
+ async getBookInfo(bookId) {
this.checkMyState();
try {
const db = this.db;
- const hash = path.basename(publicPath);
- //найдем bookPath и downFileName
- const rows = await db.select({table: 'file_hash', where: `@@id(${db.esc(hash)})`});
- if (rows.length) {//нашли по хешу
- const rec = rows[0];
- await this.restoreBook(rec.bookPath, rec.downFileName);
+ let bookInfo = await this.getBookLink(bookId);
+ const hash = path.basename(bookInfo.link);
+ const bookFile = `${this.config.filesDir}/${hash}`;
+ const bookFileInfo = `${bookFile}.info`;
- return rec.downFileName;
- } else {//bookPath не найден
- throw new Error('404 Файл не найден');
+ const restoreBookInfo = async() => {
+ const result = {};
+
+ const rows = await db.select({table: 'book', where: `@@id(${db.esc(bookId)})`});
+ const book = rows[0];
+
+ result.book = book;
+ result.cover = '';
+ result.fb2 = false;
+
+ if (book.ext == 'fb2') {
+ const {fb2, cover, coverExt} = await this.fb2Helper.getDescAndCover(bookFile);
+ result.fb2 = fb2;
+
+ if (cover) {
+ result.cover = `${this.config.filesPathStatic}/${hash}${coverExt}`;
+ await fs.writeFile(`${bookFile}${coverExt}`, cover);
+ }
+ }
+
+ return result;
+ };
+
+ if (!await fs.pathExists(bookFileInfo)) {
+ Object.assign(bookInfo, await restoreBookInfo());
+ await fs.writeFile(bookFileInfo, JSON.stringify(bookInfo, null, 2));
+ } else {
+ await utils.touchFile(bookFileInfo);
+ const info = await fs.readFile(bookFileInfo, 'utf-8');
+ const tmpInfo = JSON.parse(info);
+
+ //проверим существование файла обложки, восстановим если нету
+ let coverFile = '';
+ if (tmpInfo.cover)
+ coverFile = `${this.config.publicFilesDir}${tmpInfo.cover}`;
+
+ if (coverFile && !await fs.pathExists(coverFile)) {
+ Object.assign(bookInfo, await restoreBookInfo());
+ await fs.writeFile(bookFileInfo, JSON.stringify(bookInfo, null, 2));
+ } else {
+ bookInfo = tmpInfo;
+ }
}
+
+ return {bookInfo};
} catch(e) {
- log(LM_ERR, `restoreBookFile error: ${e.message}`);
+ log(LM_ERR, `getBookInfo error: ${e.message}`);
if (e.message.indexOf('ENOENT') >= 0)
throw new Error('404 Файл не найден');
throw e;
}
}
- async getDownFileName(publicPath) {
- this.checkMyState();
-
- const db = this.db;
- const hash = path.basename(publicPath);
-
- //найдем downFileName
- const rows = await db.select({table: 'file_hash', where: `@@id(${db.esc(hash)})`});
- if (rows.length) {//downFileName найден по хешу
- return rows[0].downFileName;
- } else {//bookPath не найден
- throw new Error('404 Файл не найден');
- }
- }
- */
-
async getInpxFile(params) {
let data = null;
if (params.inpxFileHash && this.inpxFileHash && params.inpxFileHash === this.inpxFileHash) {
diff --git a/server/core/fb2/Fb2Helper.js b/server/core/fb2/Fb2Helper.js
new file mode 100644
index 0000000..e70e30d
--- /dev/null
+++ b/server/core/fb2/Fb2Helper.js
@@ -0,0 +1,103 @@
+const fs = require('fs-extra');
+const iconv = require('iconv-lite');
+const textUtils = require('./textUtils');
+
+const Fb2Parser = require('../fb2/Fb2Parser');
+const utils = require('../utils');
+
+class Fb2Helper {
+ checkEncoding(data) {
+ //Корректируем кодировку UTF-16
+ let encoding = textUtils.getEncoding(data);
+ if (encoding.indexOf('UTF-16') == 0) {
+ data = Buffer.from(iconv.decode(data, encoding));
+ encoding = 'utf-8';
+ }
+
+ //Корректируем пробелы, всякие файлы попадаются :(
+ if (data[0] == 32) {
+ data = Buffer.from(data.toString().trim());
+ }
+
+ //Окончательно корректируем кодировку
+ let result = data;
+
+ let left = data.indexOf('= 0) {
+ const right = data.indexOf('?>', left);
+ if (right >= 0) {
+ const head = data.slice(left, right + 2).toString();
+ const m = head.match(/encoding=['"](.*?)['"]/);
+ if (m) {
+ let enc = m[1].toLowerCase();
+ if (enc != 'utf-8') {
+ //enc может не соответсвовать реальной кодировке файла, поэтому:
+ if (encoding.indexOf('ISO-8859') >= 0) {
+ encoding = enc;
+ }
+
+ result = iconv.decode(data, encoding);
+ result = Buffer.from(result.toString().replace(m[0], `encoding="utf-8"`));
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ async getDescAndCover(bookFile) {
+ let data = await fs.readFile(bookFile);
+ data = await utils.gunzipBuffer(data);
+
+ data = this.checkEncoding(data);
+
+ const parser = new Fb2Parser();
+
+ parser.fromString(data.toString(), {
+ lowerCase: true,
+ pickNode: route => route.indexOf('fictionbook/body') !== 0,
+ });
+
+ const desc = parser.$$('description').toObject();
+ const coverImage = parser.inspector(desc).$('description/title-info/coverpage/image');
+
+ let cover = null;
+ let coverExt = '';
+ if (coverImage) {
+ const coverAttrs = coverImage.attrs();
+ const href = coverAttrs[`${parser.xlinkNS}:href`];
+ let coverType = coverAttrs['content-type'];
+ coverType = (coverType == 'image/jpg' || coverType == 'application/octet-stream' ? 'image/jpeg' : coverType);
+ coverExt = (coverType == 'image/png' ? '.png' : '.jpg');
+
+ if (href) {
+ const binaryId = (href[0] == '#' ? href.substring(1) : href);
+
+ //найдем нужный image
+ parser.$$('binary').eachSelf(node => {
+ let attrs = node.attrs();
+ if (!attrs)
+ return;
+ attrs = Object.fromEntries(attrs);
+
+ if (attrs.id === binaryId) {
+ const textNode = new Fb2Parser(node.value);
+ const base64 = textNode.$self('*TEXT').value;
+
+ cover = (base64 ? Buffer.from(base64, 'base64') : null);
+ }
+ });
+ }
+ }
+
+ parser.remove('binary');
+ return {fb2: parser.toObject(), cover, coverExt};
+ }
+}
+
+module.exports = Fb2Helper;
\ No newline at end of file
diff --git a/server/core/fb2/Fb2Parser.js b/server/core/fb2/Fb2Parser.js
new file mode 100644
index 0000000..70e2815
--- /dev/null
+++ b/server/core/fb2/Fb2Parser.js
@@ -0,0 +1,297 @@
+const XmlParser = require('../xml/XmlParser');
+
+class Fb2Parser extends XmlParser {
+ get xlinkNS() {
+ if (!this._xlinkNS) {
+ const rootAttrs = this.$self().attrs();
+ let ns = 'l';
+ for (const [key, value] of rootAttrs) {
+ if (value == 'http://www.w3.org/1999/xlink') {
+ ns = key.split(':')[1] || ns;
+ break;
+ }
+ }
+
+ this._xlinkNS = ns;
+ }
+
+ return this._xlinkNS;
+ }
+
+ bookInfo(fb2Object) {
+ const result = {};
+
+ if (!fb2Object)
+ fb2Object = this.toObject();
+
+ const desc = this.inspector(fb2Object).$('fictionbook/description');
+
+ if (!desc)
+ return result;
+
+ const parseAuthors = (node, tagName) => {
+ const authors = [];
+ for (const a of node.$$(tagName)) {
+ let names = [];
+ names.push(a.text('last-name'));
+ names.push(a.text('first-name'));
+ names.push(a.text('middle-name'));
+ names = names.filter(n => n);
+ if (!names.length)
+ names.push(a.text('nickname'));
+
+ authors.push(names.join(' '));
+ }
+
+ return authors;
+ }
+
+ const parseSequence = (node, tagName) => {
+ const sequence = [];
+ for (const s of node.$$(tagName)) {
+ const seqAttrs = s.attrs() || {};
+ const name = seqAttrs['name'] || null;
+ const num = seqAttrs['number'] || null;
+ const lang = seqAttrs['xml:lang'] || null;
+
+ sequence.push({name, num, lang});
+ }
+
+ return sequence;
+ }
+
+ const parseTitleInfo = (titleInfo) => {
+ const info = {};
+
+ info.genre = [];
+ for (const g of titleInfo.$$('genre'))
+ info.genre.push(g.text());
+
+ info.author = parseAuthors(titleInfo, 'author');
+
+ info.bookTitle = titleInfo.text('book-title');
+
+ //annotation как Object
+ info.annotation = titleInfo.$('annotation') && titleInfo.$('annotation').value;
+ info.annotationXml = null;
+ info.annotationHtml = null;
+ if (info.annotation) {
+ //annotation как кусок xml
+ info.annotationXml = (new XmlParser()).fromObject(info.annotation).toString({noHeader: true});
+
+ //annotation как html
+ info.annotationHtml = this.toHtml(info.annotationXml);
+ }
+
+ info.keywords = titleInfo.text('keywords');
+ info.date = titleInfo.text('date');
+ info.coverpage = titleInfo.$('coverpage') && titleInfo.$('coverpage').value;
+ info.lang = titleInfo.text('lang');
+ info.srcLang = titleInfo.text('src-lang');
+
+ info.translator = parseAuthors(titleInfo, 'translator');
+
+ info.sequence = parseSequence(titleInfo, 'sequence');
+
+ return info;
+ }
+
+ //title-info
+ const titleInfo = desc.$('title-info');
+ if (titleInfo) {
+ result.titleInfo = parseTitleInfo(titleInfo);
+ }
+
+ //src-title-info
+ const srcTitleInfo = desc.$('src-title-info');
+ if (srcTitleInfo) {
+ result.srcTitleInfo = parseTitleInfo(srcTitleInfo);
+ }
+
+ //document-info
+ const documentInfo = desc.$('document-info');
+ if (documentInfo) {
+ const info = {};
+
+ info.author = parseAuthors(documentInfo, 'author');
+ info.programUsed = documentInfo.text('program-used');
+ info.date = documentInfo.text('date');
+
+ info.srcUrl = [];
+ for (const url of documentInfo.$$('src-url'))
+ info.srcUrl.push(url.text());
+
+ info.srcOcr = documentInfo.text('src-ocr');
+ info.id = documentInfo.text('id');
+ info.version = documentInfo.text('version');
+
+ //аналогично annotation
+ info.history = documentInfo.$('history') && documentInfo.$('history').value;
+ info.historyXml = null;
+ info.historyHtml = null;
+ if (info.history) {
+ //history как кусок xml
+ info.historyXml = (new XmlParser()).fromObject(info.history).toString({noHeader: true});
+
+ //history как html
+ info.historyHtml = this.toHtml(info.historyXml);
+ }
+
+ info.publisher = parseAuthors(documentInfo, 'publisher');
+
+ result.documentInfo = info;
+ }
+
+ //publish-info
+ const publishInfo = desc.$('publish-info');
+ if (publishInfo) {
+ const info = {};
+
+ info.bookName = publishInfo.text('book-name');
+ info.publisher = publishInfo.text('publisher');
+ info.city = publishInfo.text('city');
+ info.year = publishInfo.text('year');
+ info.isbn = publishInfo.text('isbn');
+ info.sequence = parseSequence(publishInfo, 'sequence');
+
+ result.publishInfo = info;
+ }
+
+ return result;
+ }
+
+ bookInfoList(fb2Object, options = {}) {
+ let {
+ correctMapping = false,
+ valueToString = false,
+ } = options;
+
+ if (!correctMapping)
+ correctMapping = mapping => mapping;
+
+ const myValueToString = (value, nodePath, origVTS) => {//eslint-disable-line no-unused-vars
+ if (nodePath == 'titleInfo/sequence'
+ || nodePath == 'srcTitleInfo/sequence'
+ || nodePath == 'publishInfo/sequence')
+ return value.map(v => [v.name, v.num].filter(s => s).join(' #')).join(', ');
+
+ if (typeof(value) === 'string') {
+ return value;
+ } else if (Array.isArray(value)) {
+ return value.join(', ');
+ } else if (typeof(value) === 'object') {
+ return JSON.stringify(value);
+ }
+
+ return value;
+ };
+
+ if (!valueToString)
+ valueToString = myValueToString;
+
+ let mapping = [
+ {name: 'titleInfo', label: 'Общая информация', value: [
+ {name: 'author', label: 'Автор(ы)'},
+ {name: 'bookTitle', label: 'Название'},
+ {name: 'sequence', label: 'Серия'},
+ {name: 'genre', label: 'Жанр'},
+
+ {name: 'date', label: 'Дата'},
+ {name: 'lang', label: 'Язык книги'},
+ {name: 'srcLang', label: 'Язык оригинала'},
+ {name: 'translator', label: 'Переводчик(и)'},
+ {name: 'keywords', label: 'Ключевые слова'},
+ ]},
+ {name: 'srcTitleInfo', label: 'Информация о произведении на языке оригинала', value: [
+ {name: 'author', label: 'Автор(ы)'},
+ {name: 'bookTitle', label: 'Название'},
+ {name: 'sequence', label: 'Серия'},
+ {name: 'genre', label: 'Жанр'},
+
+ {name: 'date', label: 'Дата'},
+ {name: 'lang', label: 'Язык книги'},
+ {name: 'srcLang', label: 'Язык оригинала'},
+ {name: 'translator', label: 'Переводчик(и)'},
+ {name: 'keywords', label: 'Ключевые слова'},
+ ]},
+ {name: 'publishInfo', label: 'Издательская информация', value: [
+ {name: 'bookName', label: 'Название'},
+ {name: 'publisher', label: 'Издательство'},
+ {name: 'city', label: 'Город'},
+ {name: 'year', label: 'Год'},
+ {name: 'isbn', label: 'ISBN'},
+ {name: 'sequence', label: 'Серия'},
+ ]},
+ {name: 'documentInfo', label: 'Информация о документе (OCR)', value: [
+ {name: 'author', label: 'Автор(ы)'},
+ {name: 'programUsed', label: 'Программа'},
+ {name: 'date', label: 'Дата'},
+ //srcUrl = []
+ {name: 'id', label: 'ID'},
+ {name: 'version', label: 'Версия'},
+ {name: 'srcOcr', label: 'Автор источника'},
+ {name: 'historyHtml', label: 'История'},
+ {name: 'publisher', label: 'Правообладатели'},
+ ]},
+ ];
+
+ mapping = correctMapping(mapping);
+ const bookInfo = this.bookInfo(fb2Object);
+
+ //заполняем mapping
+ let result = [];
+ for (const item of mapping) {
+ const itemOut = {name: item.name, label: item.label, value: []};
+ const info = bookInfo[item.name];
+ if (!info)
+ continue;
+
+ for (const subItem of item.value) {
+ if (info[subItem.name] !== null) {
+ const subItemOut = {
+ name: subItem.name,
+ label: subItem.label,
+ value: valueToString(info[subItem.name], `${item.name}/${subItem.name}`, myValueToString),
+ };
+
+ if (subItemOut.value)
+ itemOut.value.push(subItemOut);
+ }
+ }
+
+ if (itemOut.value.length)
+ result.push(itemOut);
+ }
+
+ return result;
+ }
+
+ toHtml(xmlString) {
+ const substs = {
+ '': '',
+ '
': '',
+ '': '
',
+ '': '',
+ '': '',
+ '': '',
+ '': '',
+ '': '
',
+ '': '',
+ '': '
',
+ '': '',
+ '': '',
+ '': '',
+ '': '',
+ };
+
+ for (const [tag, s] of Object.entries(substs)) {
+ const r = new RegExp(`${tag}`, 'g');
+ xmlString = xmlString.replace(r, s);
+ }
+
+ return xmlString;
+ }
+}
+
+module.exports = Fb2Parser;
\ No newline at end of file
diff --git a/server/core/fb2/textUtils.js b/server/core/fb2/textUtils.js
new file mode 100644
index 0000000..ef05606
--- /dev/null
+++ b/server/core/fb2/textUtils.js
@@ -0,0 +1,130 @@
+const chardet = require('chardet');
+
+function getEncoding(buf) {
+ let selected = getEncodingLite(buf);
+
+ if (selected == 'ISO-8859-5' && buf.length > 10) {
+ const charsetAll = chardet.analyse(buf.slice(0, 20000));
+ for (const charset of charsetAll) {
+ if (charset.name.indexOf('ISO-8859') < 0) {
+ selected = charset.name;
+ break;
+ }
+ }
+ }
+
+ return selected;
+}
+
+
+function getEncodingLite(buf, returnAll) {
+ const lowerCase = 3;
+ const upperCase = 1;
+
+ const codePage = {
+ 'k': 'koi8-r',
+ 'w': 'Windows-1251',
+ 'd': 'cp866',
+ 'i': 'ISO-8859-5',
+ 'm': 'maccyrillic',
+ 'u': 'utf-8',
+ };
+
+ let charsets = {
+ 'k': 0,
+ 'w': 0,
+ 'd': 0,
+ 'i': 0,
+ 'm': 0,
+ 'u': 0,
+ };
+
+ const len = buf.length;
+ const blockSize = (len > 5*3000 ? 3000 : len);
+ let counter = 0;
+ let i = 0;
+ let totalChecked = 0;
+ while (i < len) {
+ const char = buf[i];
+ const nextChar = (i < len - 1 ? buf[i + 1] : 0);
+ totalChecked++;
+ i++;
+ //non-russian characters
+ if (char < 128 || char > 256)
+ continue;
+ //UTF-8
+ if ((char == 208 || char == 209) && nextChar >= 128 && nextChar <= 190)
+ charsets['u'] += lowerCase;
+ else {
+ //CP866
+ if ((char > 159 && char < 176) || (char > 223 && char < 242)) charsets['d'] += lowerCase;
+ if ((char > 127 && char < 160)) charsets['d'] += upperCase;
+
+ //KOI8-R
+ if ((char > 191 && char < 223)) charsets['k'] += lowerCase;
+ if ((char > 222 && char < 256)) charsets['k'] += upperCase;
+
+ //WIN-1251
+ if (char > 223 && char < 256) charsets['w'] += lowerCase;
+ if (char > 191 && char < 224) charsets['w'] += upperCase;
+
+ //MAC
+ if (char > 221 && char < 255) charsets['m'] += lowerCase;
+ if (char > 127 && char < 160) charsets['m'] += upperCase;
+
+ //ISO-8859-5
+ if (char > 207 && char < 240) charsets['i'] += lowerCase;
+ if (char > 175 && char < 208) charsets['i'] += upperCase;
+ }
+
+ counter++;
+
+ if (counter > blockSize) {
+ counter = 0;
+ i += Math.round(len/2 - 2*blockSize);
+ }
+ }
+
+ let sorted = Object.keys(charsets).map(function(key) {
+ return { codePage: codePage[key], c: charsets[key], totalChecked };
+ });
+
+ sorted.sort((a, b) => b.c - a.c);
+
+ if (returnAll)
+ return sorted;
+ else if (sorted[0].c > 0 && sorted[0].c > sorted[0].totalChecked/2)
+ return sorted[0].codePage;
+ else
+ return 'ISO-8859-5';
+}
+
+function checkIfText(buf) {
+ const enc = getEncodingLite(buf, true);
+ if (enc[0].c > enc[0].totalChecked*0.9)
+ return true;
+
+ let spaceCount = 0;
+ let crCount = 0;
+ let lfCount = 0;
+ for (let i = 0; i < buf.length; i++) {
+ if (buf[i] == 32)
+ spaceCount++;
+ if (buf[i] == 13)
+ crCount++;
+ if (buf[i] == 10)
+ lfCount++;
+ }
+
+ const spaceFreq = spaceCount/(buf.length + 1);
+ const crFreq = crCount/(buf.length + 1);
+ const lfFreq = lfCount/(buf.length + 1);
+
+ return (buf.length < 1000 || spaceFreq > 0.1 || crFreq > 0.03 || lfFreq > 0.03);
+}
+
+module.exports = {
+ getEncoding,
+ getEncodingLite,
+ checkIfText,
+}
\ No newline at end of file
diff --git a/server/core/utils.js b/server/core/utils.js
index 747b524..856d334 100644
--- a/server/core/utils.js
+++ b/server/core/utils.js
@@ -115,10 +115,65 @@ function gzipFile(inputFile, outputFile, level = 1) {
});
}
+function gunzipFile(inputFile, outputFile) {
+ return new Promise((resolve, reject) => {
+ const gzip = zlib.createGunzip();
+ const input = fs.createReadStream(inputFile);
+ const output = fs.createWriteStream(outputFile);
+
+ input.on('error', reject)
+ .pipe(gzip).on('error', reject)
+ .pipe(output).on('error', reject)
+ .on('finish', (err) => {
+ if (err) reject(err);
+ else resolve();
+ });
+ });
+}
+
+function gzipBuffer(buf) {
+ return new Promise((resolve, reject) => {
+ zlib.gzip(buf, {level: 1}, (err, result) => {
+ if (err) reject(err);
+ resolve(result);
+ });
+ });
+}
+
+function gunzipBuffer(buf) {
+ return new Promise((resolve, reject) => {
+ zlib.gunzip(buf, (err, result) => {
+ if (err) reject(err);
+ resolve(result);
+ });
+ });
+}
+
function toUnixPath(dir) {
return dir.replace(/\\/g, '/');
}
+function makeValidFileName(fileName, repl = '_') {
+ let f = fileName.replace(/[\x00\\/:*"<>|]/g, repl); // eslint-disable-line no-control-regex
+ f = f.trim();
+ while (f.length && (f[f.length - 1] == '.' || f[f.length - 1] == '_')) {
+ f = f.substring(0, f.length - 1);
+ }
+
+ if (f)
+ return f;
+ else
+ throw new Error('Invalid filename');
+}
+
+function makeValidFileNameOrEmpty(fileName) {
+ try {
+ return makeValidFileName(fileName);
+ } catch(e) {
+ return '';
+ }
+}
+
module.exports = {
sleep,
processLoop,
@@ -132,5 +187,10 @@ module.exports = {
intersectSet,
randomHexString,
gzipFile,
+ gunzipFile,
+ gzipBuffer,
+ gunzipBuffer,
toUnixPath,
+ makeValidFileName,
+ makeValidFileNameOrEmpty,
};
\ No newline at end of file
diff --git a/server/core/xml/ObjectInspector.js b/server/core/xml/ObjectInspector.js
new file mode 100644
index 0000000..0adc188
--- /dev/null
+++ b/server/core/xml/ObjectInspector.js
@@ -0,0 +1,109 @@
+class ObjectInspector {
+ constructor(raw = null) {
+ this.raw = raw;
+ }
+
+ makeSelector(selector) {
+ const result = [];
+ selector = selector.trim();
+
+ //последний индекс не учитывется, только если не задан явно
+ if (selector && selector[selector.length - 1] == ']')
+ selector += '/';
+
+ const levels = selector.split('/');
+
+ for (const level of levels) {
+ const [name, indexPart] = level.split('[');
+ let index = 0;
+ if (indexPart) {
+ const i = indexPart.indexOf(']');
+ index = parseInt(indexPart.substring(0, i), 10) || 0;
+ }
+
+ result.push({name, index});
+ }
+
+ if (result.length);
+ result[result.length - 1].last = true;
+
+ return result;
+ }
+
+ select(selector = '') {
+ selector = this.makeSelector(selector);
+
+ let raw = this.raw;
+ for (const s of selector) {
+ if (s.name) {
+ if (typeof(raw) === 'object' && !Array.isArray(raw))
+ raw = raw[s.name];
+ else
+ raw = null;
+ }
+
+ if (raw !== null && !s.last) {
+ if (Array.isArray(raw))
+ raw = raw[s.index];
+ else if (s.index > 0)
+ raw = null;
+ }
+
+ if (raw === undefined || raw === null) {
+ raw = null;
+ break;
+ }
+ }
+
+ if (raw === null)
+ return [];
+
+ raw = (Array.isArray(raw) ? raw : [raw]);
+
+ const result = [];
+ for (const r of raw)
+ result.push(new ObjectInspector(r));
+
+ return result;
+ }
+
+ $$(selector) {
+ return this.select(selector);
+ }
+
+ $(selector) {
+ const res = this.select(selector);
+ return (res !== null && res.length ? res[0] : null);
+ }
+
+ get value() {
+ return this.raw;
+ }
+
+ v(selector = '') {
+ const res = this.$(selector);
+ return (res ? res.value : null);
+ }
+
+ text(selector = '') {
+ const res = this.$(`${selector}/*TEXT`);
+ return (res ? res.value : null);
+ }
+
+ comment(selector = '') {
+ const res = this.$(`${selector}/*COMMENT`);
+ return (res ? res.value : null);
+ }
+
+ cdata(selector = '') {
+ const res = this.$(`${selector}/*CDATA`);
+ return (res ? res.value : null);
+ }
+
+ attrs(selector = '') {
+ const res = this.$(`${selector}/*ATTRS`);
+ return (res ? res.value : null);
+ }
+}
+
+module.exports = ObjectInspector;
\ No newline at end of file
diff --git a/server/core/xml/XmlParser.js b/server/core/xml/XmlParser.js
new file mode 100644
index 0000000..26da7e7
--- /dev/null
+++ b/server/core/xml/XmlParser.js
@@ -0,0 +1,771 @@
+const sax = require('./sax');
+const ObjectInspector = require('./ObjectInspector');
+
+//node types
+const NODE = 1;
+const TEXT = 2;
+const CDATA = 3;
+const COMMENT = 4;
+
+const name2type = {
+ 'NODE': NODE,
+ 'TEXT': TEXT,
+ 'CDATA': CDATA,
+ 'COMMENT': COMMENT,
+};
+
+const type2name = {
+ [NODE]: 'NODE',
+ [TEXT]: 'TEXT',
+ [CDATA]: 'CDATA',
+ [COMMENT]: 'COMMENT',
+};
+
+class NodeBase {
+ makeSelectorObj(selectorString) {
+ const result = {all: false, before: false, type: 0, name: ''};
+
+ if (selectorString === '') {
+ result.before = true;
+ } else if (selectorString === '*') {
+ result.all = true;
+ } else if (selectorString[0] === '*') {
+ const typeName = selectorString.substring(1);
+ result.type = name2type[typeName];
+ if (!result.type)
+ throw new Error(`Unknown selector type: ${typeName}`);
+ } else {
+ result.name = selectorString;
+ }
+
+ return result;
+ }
+
+ checkNode(rawNode, selectorObj) {
+ return selectorObj.all || selectorObj.before
+ || (selectorObj.type && rawNode[0] === selectorObj.type)
+ || (rawNode[0] === NODE && rawNode[1] === selectorObj.name);
+ }
+
+ findNodeIndex(nodes, selectorObj) {
+ for (let i = 0; i < nodes.length; i++)
+ if (this.checkNode(nodes[i], selectorObj))
+ return i;
+ }
+
+ rawAdd(nodes, rawNode, selectorObj) {
+ if (selectorObj.all) {
+ nodes.push(rawNode);
+ } else if (selectorObj.before) {
+ nodes.unshift(rawNode);
+ } else {
+ const index = this.findNodeIndex(nodes, selectorObj);
+ if (index >= 0)
+ nodes.splice(index, 0, rawNode);
+ else
+ nodes.push(rawNode);
+ }
+ }
+
+ rawRemove(nodes, selectorObj) {
+ if (selectorObj.before)
+ return;
+
+ for (let i = nodes.length - 1; i >= 0; i--) {
+ if (this.checkNode(nodes[i], selectorObj))
+ nodes.splice(i, 1);
+ }
+ }
+}
+
+class NodeObject extends NodeBase {
+ constructor(raw = null) {
+ super();
+
+ if (raw)
+ this.raw = raw;
+ else
+ this.raw = [];
+ }
+
+ get type() {
+ return this.raw[0] || null;
+ }
+
+ get name() {
+ if (this.type === NODE)
+ return this.raw[1] || null;
+
+ return null;
+ }
+
+ set name(value) {
+ if (this.type === NODE)
+ this.raw[1] = value;
+ }
+
+ attrs(key, value) {
+ if (this.type !== NODE)
+ return null;
+
+ let map = null;
+
+ if (key instanceof Map) {
+ map = key;
+ this.raw[2] = Array.from(map);
+ } else if (Array.isArray(this.raw[2])) {
+ map = new Map(this.raw[2]);
+ if (key) {
+ map.set(key, value);
+ this.raw[2] = Array.from(map);
+ }
+ }
+
+ return map;
+ }
+
+ get value() {
+ switch (this.type) {
+ case NODE:
+ return this.raw[3] || null;
+ case TEXT:
+ case CDATA:
+ case COMMENT:
+ return this.raw[1] || null;
+ }
+
+ return null;
+ }
+
+ set value(v) {
+ switch (this.type) {
+ case NODE:
+ this.raw[3] = v;
+ break;
+ case TEXT:
+ case CDATA:
+ case COMMENT:
+ this.raw[1] = v;
+ }
+ }
+
+ add(node, after = '*') {
+ if (this.type !== NODE)
+ return;
+
+ const selectorObj = this.makeSelectorObj(after);
+
+ if (!Array.isArray(this.raw[3]))
+ this.raw[3] = [];
+
+ if (Array.isArray(node)) {
+ for (const node_ of node)
+ this.rawAdd(this.raw[3], node_.raw, selectorObj);
+ } else {
+ this.rawAdd(this.raw[3], node.raw, selectorObj);
+ }
+
+ return this;
+ }
+
+ remove(selector = '') {
+ if (this.type !== NODE || !this.raw[3])
+ return;
+
+ const selectorObj = this.makeSelectorObj(selector);
+
+ this.rawRemove(this.raw[3], selectorObj);
+ if (!this.raw[3].length)
+ this.raw[3] = null;
+
+ return this;
+ }
+
+ each(callback) {
+ if (this.type !== NODE || !this.raw[3])
+ return;
+
+ for (const n of this.raw[3]) {
+ if (callback(new NodeObject(n)) === false)
+ break;
+ }
+
+ return this;
+ }
+
+ eachDeep(callback) {
+ if (this.type !== NODE || !this.raw[3])
+ return;
+
+ const deep = (nodes, route = '') => {
+ for (const n of nodes) {
+ const node = new NodeObject(n);
+
+ if (callback(node, route) === false)
+ return false;
+
+ if (node.type === NODE && node.value) {
+ if (deep(node.value, `${route}${route ? '/' : ''}${node.name}`) === false)
+ return false;
+ }
+ }
+ }
+
+ deep(this.raw[3]);
+
+ return this;
+ }
+}
+
+class XmlParser extends NodeBase {
+ constructor(rawNodes = []) {
+ super();
+
+ this.NODE = NODE;
+ this.TEXT = TEXT;
+ this.CDATA = CDATA;
+ this.COMMENT = COMMENT;
+
+ this.rawNodes = rawNodes;
+ }
+
+ get count() {
+ return this.rawNodes.length;
+ }
+
+ nodeObject(node) {
+ return new NodeObject(node);
+ }
+
+ newParser(nodes) {
+ return new XmlParser(nodes);
+ }
+
+ checkType(type) {
+ if (!type2name[type])
+ throw new Error(`Invalid type: ${type}`);
+ }
+
+ createTypedNode(type, nameOrValue, attrs = null, value = null) {
+ this.checkType(type);
+ switch (type) {
+ case NODE:
+ if (!nameOrValue || typeof(nameOrValue) !== 'string')
+ throw new Error('Node name must be non-empty string');
+ return new NodeObject([type, nameOrValue, attrs, value]);
+ case TEXT:
+ case CDATA:
+ case COMMENT:
+ if (typeof(nameOrValue) !== 'string')
+ throw new Error('Node value must be of type string');
+ return new NodeObject([type, nameOrValue]);
+ }
+ }
+
+ createNode(name, attrs = null, value = null) {
+ return this.createTypedNode(NODE, name, attrs, value);
+ }
+
+ createText(value = null) {
+ return this.createTypedNode(TEXT, value);
+ }
+
+ createCdata(value = null) {
+ return this.createTypedNode(CDATA, value);
+ }
+
+ createComment(value = null) {
+ return this.createTypedNode(COMMENT, value);
+ }
+
+ add(node, after = '*') {
+ const selectorObj = this.makeSelectorObj(after);
+
+ for (const n of this.rawNodes) {
+ if (n && n[0] === NODE) {
+ if (!Array.isArray(n[3]))
+ n[3] = [];
+
+ if (Array.isArray(node)) {
+ for (const node_ of node)
+ this.rawAdd(n[3], node_.raw, selectorObj);
+ } else {
+ this.rawAdd(n[3], node.raw, selectorObj);
+ }
+ }
+ }
+
+ return this;
+ }
+
+ addRoot(node, after = '*') {
+ const selectorObj = this.makeSelectorObj(after);
+
+ if (Array.isArray(node)) {
+ for (const node_ of node)
+ this.rawAdd(this.rawNodes, node_.raw, selectorObj);
+ } else {
+ this.rawAdd(this.rawNodes, node.raw, selectorObj);
+ }
+
+ return this;
+ }
+
+ remove(selector = '') {
+ const selectorObj = this.makeSelectorObj(selector);
+
+ for (const n of this.rawNodes) {
+ if (n && n[0] === NODE && Array.isArray(n[3])) {
+ this.rawRemove(n[3], selectorObj);
+ if (!n[3].length)
+ n[3] = null;
+ }
+ }
+
+ return this;
+ }
+
+ removeRoot(selector = '') {
+ const selectorObj = this.makeSelectorObj(selector);
+
+ this.rawRemove(this.rawNodes, selectorObj);
+
+ return this;
+ }
+
+ each(callback, self = false) {
+ if (self) {
+ for (const n of this.rawNodes) {
+ if (callback(new NodeObject(n)) === false)
+ return this;
+ }
+ } else {
+ for (const n of this.rawNodes) {
+ if (n[0] === NODE && n[3]) {
+ for (const nn of n[3])
+ if (callback(new NodeObject(nn)) === false)
+ return this;
+ }
+ }
+ }
+
+ return this;
+ }
+
+ eachSelf(callback) {
+ return this.each(callback, true);
+ }
+
+ eachDeep(callback, self = false) {
+ const deep = (nodes, route = '') => {
+ for (const n of nodes) {
+ const node = new NodeObject(n);
+
+ if (callback(node, route) === false)
+ return false;
+
+ if (node.type === NODE && node.value) {
+ if (deep(node.value, `${route}${route ? '/' : ''}${node.name}`) === false)
+ return false;
+ }
+ }
+ }
+
+ if (self) {
+ deep(this.rawNodes);
+ } else {
+ for (const n of this.rawNodes) {
+ if (n[0] === NODE && n[3])
+ if (deep(n[3]) === false)
+ break;
+ }
+ }
+
+ return this;
+ }
+
+ eachDeepSelf(callback) {
+ return this.eachDeep(callback, true);
+ }
+
+ rawSelect(nodes, selectorObj, callback) {
+ for (const n of nodes)
+ if (this.checkNode(n, selectorObj))
+ callback(n);
+
+ return this;
+ }
+
+ select(selector = '', self = false) {
+ let newRawNodes = [];
+
+ if (selector.indexOf('/') >= 0) {
+ const selectors = selector.split('/');
+ let res = this;
+ for (const sel of selectors) {
+ res = res.select(sel, self);
+ self = false;
+ }
+
+ newRawNodes = res.rawNodes;
+ } else {
+ const selectorObj = this.makeSelectorObj(selector);
+
+ if (self) {
+ this.rawSelect(this.rawNodes, selectorObj, (node) => {
+ newRawNodes.push(node);
+ })
+ } else {
+ for (const n of this.rawNodes) {
+ if (n && n[0] === NODE && Array.isArray(n[3])) {
+ this.rawSelect(n[3], selectorObj, (node) => {
+ newRawNodes.push(node);
+ })
+ }
+ }
+ }
+ }
+
+ return new XmlParser(newRawNodes);
+ }
+
+ $$(selector, self) {
+ return this.select(selector, self);
+ }
+
+ $$self(selector) {
+ return this.select(selector, true);
+ }
+
+ selectFirst(selector, self) {
+ const result = this.select(selector, self);
+ const node = (result.count ? result.rawNodes[0] : null);
+ return new NodeObject(node);
+ }
+
+ $(selector, self) {
+ return this.selectFirst(selector, self);
+ }
+
+ $self(selector) {
+ return this.selectFirst(selector, true);
+ }
+
+ toJson(options = {}) {
+ const {format = false} = options;
+
+ if (format)
+ return JSON.stringify(this.rawNodes, null, 2);
+ else
+ return JSON.stringify(this.rawNodes);
+ }
+
+ fromJson(jsonString) {
+ const parsed = JSON.parse(jsonString);
+ if (!Array.isArray(parsed))
+ throw new Error('JSON parse error: root element must be array');
+
+ this.rawNodes = parsed;
+
+ return this;
+ }
+
+ toString(options = {}) {
+ const {
+ encoding = 'utf-8',
+ format = false,
+ noHeader = false,
+ expandEmpty = false
+ } = options;
+
+ let deepType = 0;
+ let out = '';
+ if (!noHeader)
+ out += ``;
+
+ const nodesToString = (nodes, depth = 0) => {
+ let result = '';
+
+ const indent = '\n' + ' '.repeat(depth);
+ let lastType = 0;
+
+ for (const n of nodes) {
+ const node = new NodeObject(n);
+
+ let open = '';
+ let body = '';
+ let close = '';
+
+ if (node.type === NODE) {
+ if (!node.name)
+ continue;
+
+ let attrs = '';
+
+ const nodeAttrs = node.attrs();
+ if (nodeAttrs) {
+ for (const [attrName, attrValue] of nodeAttrs) {
+ if (typeof(attrValue) === 'string')
+ attrs += ` ${attrName}="${attrValue}"`;
+ else
+ if (attrValue)
+ attrs += ` ${attrName}`;
+ }
+ }
+
+ if (node.value)
+ body = nodesToString(node.value, depth + 2);
+
+ if (!body && !expandEmpty) {
+ open = (format && lastType !== TEXT ? indent : '');
+ open += `<${node.name}${attrs}/>`;
+ } else {
+ open = (format && lastType !== TEXT ? indent : '');
+ open += `<${node.name}${attrs}>`;
+
+ close = (format && deepType && deepType !== TEXT ? indent : '');
+ close += `${node.name}>`;
+ }
+ } else if (node.type === TEXT) {
+ body = node.value || '';
+ } else if (node.type === CDATA) {
+ body = (format && lastType !== TEXT ? indent : '');
+ body += ``;
+ } else if (node.type === COMMENT) {
+ body = (format && lastType !== TEXT ? indent : '');
+ body += ``;
+ }
+
+ result += `${open}${body}${close}`;
+ lastType = node.type;
+ }
+
+ deepType = lastType;
+ return result;
+ }
+
+ out += nodesToString(this.rawNodes) + (format ? '\n' : '');
+
+ return out;
+ }
+
+ fromString(xmlString, options = {}) {
+ const {
+ lowerCase = false,
+ whiteSpace = false,
+ pickNode = false,
+ } = options;
+
+ const parsed = [];
+ const root = this.createNode('root', null, parsed);//fake node
+ let node = root;
+
+ let route = '';
+ let routeStack = [];
+ let ignoreNode = false;
+
+ const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
+ if (tag == '?xml')
+ return;
+
+ if (!ignoreNode && pickNode) {
+ route += `${route ? '/' : ''}${tag}`;
+ ignoreNode = !pickNode(route);
+ }
+
+ let newNode = node;
+ if (!ignoreNode)
+ newNode = this.createNode(tag);
+
+ routeStack.push({tag, route, ignoreNode, node: newNode});
+
+ if (ignoreNode)
+ return;
+
+ if (tail && tail.trim() !== '') {
+ const parsedAttrs = sax.getAttrsSync(tail, lowerCase);
+ const attrs = new Map();
+ for (const attr of parsedAttrs.values()) {
+ attrs.set(attr.fn, attr.value);
+ }
+
+ if (attrs.size)
+ newNode.attrs(attrs);
+ }
+
+ if (!node.value)
+ node.value = [];
+ node.value.push(newNode.raw);
+ node = newNode;
+ };
+
+ const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
+ if (routeStack.length && routeStack[routeStack.length - 1].tag === tag) {
+ routeStack.pop();
+
+ if (routeStack.length) {
+ const last = routeStack[routeStack.length - 1];
+ route = last.route;
+ ignoreNode = last.ignoreNode;
+ node = last.node;
+ } else {
+ route = '';
+ ignoreNode = false;
+ node = root;
+ }
+ }
+ }
+
+ const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
+ if (ignoreNode || (pickNode && !pickNode(`${route}/*TEXT`)))
+ return;
+
+ if (!whiteSpace && text.trim() == '')
+ return;
+
+ if (!node.value)
+ node.value = [];
+
+ node.value.push(this.createText(text).raw);
+ };
+
+ const onCdata = (tagData, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
+ if (ignoreNode || (pickNode && !pickNode(`${route}/*CDATA`)))
+ return;
+
+ if (!node.value)
+ node.value = [];
+
+ node.value.push(this.createCdata(tagData).raw);
+ }
+
+ const onComment = (tagData, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
+ if (ignoreNode || (pickNode && !pickNode(`${route}/*COMMENT`)))
+ return;
+
+ if (!node.value)
+ node.value = [];
+
+ node.value.push(this.createComment(tagData).raw);
+ }
+
+ sax.parseSync(xmlString, {
+ onStartNode, onEndNode, onTextNode, onCdata, onComment, lowerCase
+ });
+
+ this.rawNodes = parsed;
+
+ return this;
+ }
+
+ toObject(options = {}) {
+ const {
+ compactText = false
+ } = options;
+
+ const nodesToObject = (nodes) => {
+ const result = {};
+
+ for (const n of nodes) {
+ const node = new NodeObject(n);
+
+ if (node.type === NODE) {
+ if (!node.name)
+ continue;
+
+ let newNode = {};
+
+ const nodeAttrs = node.attrs();
+ if (nodeAttrs)
+ newNode['*ATTRS'] = Object.fromEntries(nodeAttrs);
+
+ if (node.value) {
+ Object.assign(newNode, nodesToObject(node.value));
+
+ //схлопывание текстового узла до string
+ if (compactText
+ && !Array.isArray(newNode)
+ && Object.prototype.hasOwnProperty.call(newNode, '*TEXT')
+ && Object.keys(newNode).length === 1) {
+ newNode = newNode['*TEXT'];
+ }
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(result, node.name)) {
+ result[node.name] = newNode;
+ } else {
+ if (!Array.isArray(result[node.name])) {
+ result[node.name] = [result[node.name]];
+ }
+
+ result[node.name].push(newNode);
+ }
+ } else if (node.type === TEXT) {
+ if (!result['*TEXT'])
+ result['*TEXT'] = '';
+ result['*TEXT'] += node.value || '';
+ } else if (node.type === CDATA) {
+ if (!result['*CDATA'])
+ result['*CDATA'] = '';
+ result['*CDATA'] += node.value || '';
+ } else if (node.type === COMMENT) {
+ if (!result['*COMMENT'])
+ result['*COMMENT'] = '';
+ result['*COMMENT'] += node.value || '';
+ }
+ }
+
+ return result;
+ }
+
+ return nodesToObject(this.rawNodes);
+ }
+
+ fromObject(xmlObject) {
+ const objectToNodes = (obj) => {
+ const result = [];
+
+ for (const [tag, objNode] of Object.entries(obj)) {
+ if (tag === '*TEXT') {
+ result.push(this.createText(objNode).raw);
+ } else if (tag === '*CDATA') {
+ result.push(this.createCdata(objNode).raw);
+ } else if (tag === '*COMMENT') {
+ result.push(this.createComment(objNode).raw);
+ } else if (tag === '*ATTRS') {
+ //пропускаем
+ } else {
+ if (typeof(objNode) === 'string') {
+ result.push(this.createNode(tag, null, [this.createText(objNode).raw]).raw);
+ } else if (Array.isArray(objNode)) {
+ for (const n of objNode) {
+ if (typeof(n) === 'string') {
+ result.push(this.createNode(tag, null, [this.createText(n).raw]).raw);
+ } else if (typeof(n) === 'object') {
+ result.push(this.createNode(tag, (n['*ATTRS'] ? Object.entries(n['*ATTRS']) : null), objectToNodes(n)).raw);
+ }
+ }
+
+ } else if (typeof(objNode) === 'object') {
+ result.push(this.createNode(tag, (objNode['*ATTRS'] ? Object.entries(objNode['*ATTRS']) : null), objectToNodes(objNode)).raw);
+ }
+ }
+ }
+
+ return result;
+ };
+
+ this.rawNodes = objectToNodes(xmlObject);
+
+ return this;
+ }
+
+ inspector(obj) {
+ if (!obj)
+ obj = this.toObject();
+
+ return new ObjectInspector(obj);
+ }
+}
+
+module.exports = XmlParser;
\ No newline at end of file
diff --git a/server/core/xml/sax.js b/server/core/xml/sax.js
new file mode 100644
index 0000000..d429368
--- /dev/null
+++ b/server/core/xml/sax.js
@@ -0,0 +1,367 @@
+function parseSync(xstr, options) {
+ const dummy = () => {};
+ let {onStartNode: _onStartNode = dummy,
+ onEndNode: _onEndNode = dummy,
+ onTextNode: _onTextNode = dummy,
+ onCdata: _onCdata = dummy,
+ onComment: _onComment = dummy,
+ onProgress: _onProgress = dummy,
+ innerCut = new Set(),
+ lowerCase = true,
+ } = options;
+
+ let i = 0;
+ const len = xstr.length;
+ const progStep = len/20;
+ let nextProg = 0;
+
+ let cutCounter = 0;
+ let cutTag = '';
+ let inCdata;
+ let inComment;
+ let leftData = 0;
+ while (i < len) {
+ inCdata = false;
+ inComment = false;
+ let singleTag = false;
+
+ let left = xstr.indexOf('<', i);
+ if (left < 0)
+ break;
+ leftData = left;
+
+ if (left < len - 2 && xstr[left + 1] == '!') {
+ if (xstr[left + 2] == '-') {
+ const leftComment = xstr.indexOf('', leftData + 1);
+ if (rightData < 0)
+ break;
+ right = rightData + 2;
+ } else {
+ rightData = xstr.indexOf('>', leftData + 1);
+ if (rightData < 0)
+ break;
+ right = rightData;
+ if (xstr[right - 1] === '/') {
+ singleTag = true;
+ rightData--;
+ }
+ }
+
+ let tagData = xstr.substr(leftData + 1, rightData - leftData - 1);
+
+ if (inCdata) {
+ _onCdata(tagData, cutCounter, cutTag);
+ } else if (inComment) {
+ _onComment(tagData, cutCounter, cutTag);
+ } else {
+ let tag = '';
+ let tail = '';
+ const firstSpace = tagData.indexOf(' ');
+ if (firstSpace >= 0) {
+ tail = tagData.substr(firstSpace);
+ tag = tagData.substr(0, firstSpace);
+ } else {
+ tag = tagData;
+ }
+ if (lowerCase)
+ tag = tag.toLowerCase();
+
+ if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
+ if (!cutCounter)
+ cutTag = tag;
+ cutCounter++;
+ }
+
+ let endTag = (singleTag ? tag : '');
+ if (tag === '' || tag[0] !== '/') {
+ _onStartNode(tag, tail, singleTag, cutCounter, cutTag);
+ } else {
+ endTag = tag.substr(1);
+ }
+
+ if (endTag)
+ _onEndNode(endTag, tail, singleTag, cutCounter, cutTag);
+
+ if (cutTag === endTag) {
+ cutCounter = (cutCounter > 0 ? cutCounter - 1 : 0);
+ if (!cutCounter)
+ cutTag = '';
+ }
+ }
+
+ if (right >= nextProg) {
+ _onProgress(Math.round(right/(len + 1)*100));
+ nextProg += progStep;
+ }
+ i = right + 1;
+ }
+
+ if (i < len) {
+ if (inCdata) {
+ _onCdata(xstr.substr(leftData + 1, len - leftData - 1), cutCounter, cutTag);
+ } else if (inComment) {
+ _onComment(xstr.substr(leftData + 1, len - leftData - 1), cutCounter, cutTag);
+ } else {
+ _onTextNode(xstr.substr(i, len - i), cutCounter, cutTag);
+ }
+ }
+
+ _onProgress(100);
+}
+
+//асинхронная копия parseSync
+//делается заменой "_on" => "await _on" после while
+async function parse(xstr, options) {
+ const dummy = () => {};
+ let {onStartNode: _onStartNode = dummy,
+ onEndNode: _onEndNode = dummy,
+ onTextNode: _onTextNode = dummy,
+ onCdata: _onCdata = dummy,
+ onComment: _onComment = dummy,
+ onProgress: _onProgress = dummy,
+ innerCut = new Set(),
+ lowerCase = true,
+ } = options;
+
+ let i = 0;
+ const len = xstr.length;
+ const progStep = len/20;
+ let nextProg = 0;
+
+ let cutCounter = 0;
+ let cutTag = '';
+ let inCdata;
+ let inComment;
+ let leftData = 0;
+ while (i < len) {
+ inCdata = false;
+ inComment = false;
+ let singleTag = false;
+
+ let left = xstr.indexOf('<', i);
+ if (left < 0)
+ break;
+ leftData = left;
+
+ if (left < len - 2 && xstr[left + 1] == '!') {
+ if (xstr[left + 2] == '-') {
+ const leftComment = xstr.indexOf('', leftData + 1);
+ if (rightData < 0)
+ break;
+ right = rightData + 2;
+ } else {
+ rightData = xstr.indexOf('>', leftData + 1);
+ if (rightData < 0)
+ break;
+ right = rightData;
+ if (xstr[right - 1] === '/') {
+ singleTag = true;
+ rightData--;
+ }
+ }
+
+ let tagData = xstr.substr(leftData + 1, rightData - leftData - 1);
+
+ if (inCdata) {
+ await _onCdata(tagData, cutCounter, cutTag);
+ } else if (inComment) {
+ await _onComment(tagData, cutCounter, cutTag);
+ } else {
+ let tag = '';
+ let tail = '';
+ const firstSpace = tagData.indexOf(' ');
+ if (firstSpace >= 0) {
+ tail = tagData.substr(firstSpace);
+ tag = tagData.substr(0, firstSpace);
+ } else {
+ tag = tagData;
+ }
+ if (lowerCase)
+ tag = tag.toLowerCase();
+
+ if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
+ if (!cutCounter)
+ cutTag = tag;
+ cutCounter++;
+ }
+
+ let endTag = (singleTag ? tag : '');
+ if (tag === '' || tag[0] !== '/') {
+ await _onStartNode(tag, tail, singleTag, cutCounter, cutTag);
+ } else {
+ endTag = tag.substr(1);
+ }
+
+ if (endTag)
+ await _onEndNode(endTag, tail, singleTag, cutCounter, cutTag);
+
+ if (cutTag === endTag) {
+ cutCounter = (cutCounter > 0 ? cutCounter - 1 : 0);
+ if (!cutCounter)
+ cutTag = '';
+ }
+ }
+
+ if (right >= nextProg) {
+ await _onProgress(Math.round(right/(len + 1)*100));
+ nextProg += progStep;
+ }
+ i = right + 1;
+ }
+
+ if (i < len) {
+ if (inCdata) {
+ await _onCdata(xstr.substr(leftData + 1, len - leftData - 1), cutCounter, cutTag);
+ } else if (inComment) {
+ await _onComment(xstr.substr(leftData + 1, len - leftData - 1), cutCounter, cutTag);
+ } else {
+ await _onTextNode(xstr.substr(i, len - i), cutCounter, cutTag);
+ }
+ }
+
+ await _onProgress(100);
+}
+
+function getAttrsSync(tail, lowerCase = true) {
+ let result = new Map();
+ let name = '';
+ let value = '';
+ let vOpen = '';
+ let inName = false;
+ let inValue = false;
+ let waitValue = false;
+ let waitEq = true;
+
+ const pushResult = () => {
+ if (waitEq)
+ value = true;
+ if (lowerCase)
+ name = name.toLowerCase();
+ if (name != '') {
+ const fn = name;
+ let ns = '';
+ if (fn.indexOf(':') >= 0) {
+ [ns, name] = fn.split(':');
+ }
+
+ result.set(fn, {value, ns, name, fn});
+ }
+ name = '';
+ value = '';
+ vOpen = '';
+ inName = false;
+ inValue = false;
+ waitValue = false;
+ waitEq = true;
+ };
+
+ tail = tail.replace(/[\t\n\r]/g, ' ');
+ for (let i = 0; i < tail.length; i++) {
+ const c = tail.charAt(i);
+ if (c == ' ') {
+ if (inValue) {
+ if (vOpen == '"')
+ value += c;
+ else
+ pushResult();
+ } else if (inName) {
+ inName = false;
+ }
+ } else if (!inValue && c == '=') {
+ waitEq = false;
+ waitValue = true;
+ inName = false;
+ } else if (c == '"') {
+ if (inValue) {
+ pushResult();
+ } else if (waitValue) {
+ inValue = true;
+ vOpen = '"';
+ }
+ } else if (inValue) {
+ value += c;
+ } else if (inName) {
+ name += c;
+ } else if (waitEq) {
+ pushResult();
+ inName = true;
+ name = c;
+ } else if (waitValue) {
+ waitValue = false;
+ inValue = true;
+ vOpen = ' ';
+ value = c;
+ } else {
+ inName = true;
+ name = c;
+ }
+ }
+ if (name != '')
+ pushResult();
+
+ return result;
+}
+
+module.exports = {
+ parseSync,
+ getAttrsSync,
+ parse
+}
\ No newline at end of file
diff --git a/server/index.js b/server/index.js
index 99383c1..15e387f 100644
--- a/server/index.js
+++ b/server/index.js
@@ -189,32 +189,31 @@ function initStatic(app, config) {
return next();
}
- if (path.extname(req.path) == '.json')
- return next();
+ if (path.extname(req.path) == '') {
+ const bookFile = `${config.publicFilesDir}${req.path}`;
+ const bookFileDesc = `${bookFile}.json`;
- const bookFile = `${config.publicFilesDir}${req.path}`;
- const bookFileDesc = `${bookFile}.json`;
+ let downFileName = '';
+ //восстановим из json-файла описания
+ try {
+ if (await fs.pathExists(bookFile) && await fs.pathExists(bookFileDesc)) {
+ await utils.touchFile(bookFile);
+ await utils.touchFile(bookFileDesc);
- let downFileName = '';
- //восстановим из json-файла описания
- try {
- if (await fs.pathExists(bookFile) && await fs.pathExists(bookFileDesc)) {
- await utils.touchFile(bookFile);
- await utils.touchFile(bookFileDesc);
-
- let desc = await fs.readFile(bookFileDesc, 'utf8');
- desc = JSON.parse(desc);
- downFileName = desc.downFileName;
- } else {
- await fs.remove(bookFile);
- await fs.remove(bookFileDesc);
+ let desc = await fs.readFile(bookFileDesc, 'utf8');
+ desc = JSON.parse(desc);
+ downFileName = desc.downFileName;
+ } else {
+ await fs.remove(bookFile);
+ await fs.remove(bookFileDesc);
+ }
+ } catch(e) {
+ log(LM_ERR, e.message);
}
- } catch(e) {
- log(LM_ERR, e.message);
- }
- if (downFileName)
- res.downFileName = downFileName;
+ if (downFileName)
+ res.downFileName = downFileName;
+ }
return next();
});