├── server ├── config │ ├── application_env │ ├── development.js │ ├── production.js │ ├── base.js │ └── index.js ├── core │ ├── LibSharedStorage │ │ └── index.js │ ├── Reader │ │ └── BookConverter │ │ │ ├── ConvertDoc.js │ │ │ ├── ConvertRtf.js │ │ │ ├── ConvertMobi.js │ │ │ ├── ConvertEpub.js │ │ │ ├── ConvertFb3.js │ │ │ ├── ConvertDocX.js │ │ │ ├── index.js │ │ │ ├── ConvertFb2.js │ │ │ ├── ConvertJpegPng.js │ │ │ ├── ConvertSites.js │ │ │ ├── textUtils.js │ │ │ ├── ConvertPdfImages.js │ │ │ └── ConvertBase.js │ ├── Zip │ │ ├── ZipReader.js │ │ └── ZipStreamer.js │ ├── WorkerState.js │ ├── AppLogger.js │ ├── RemoteStorage.js │ ├── RemoteWebDavStorage.js │ ├── AsyncExit.js │ ├── LimitedQueue.js │ ├── utils.js │ ├── FileDownloader.js │ └── xmlParser.js ├── controllers │ ├── BaseController.js │ ├── index.js │ ├── MiscController.js │ ├── WorkerController.js │ ├── ReaderController.js │ └── BookUpdateCheckerController.js ├── db │ └── jembaMigrations │ │ ├── reader-storage │ │ ├── index.js │ │ └── 001-create.js │ │ ├── book-update-server │ │ ├── index.js │ │ └── 001-create.js │ │ ├── index.js │ │ └── app │ │ ├── index.js │ │ ├── 001-create.js │ │ └── 002-create.js ├── createWebApp.js └── dev.js ├── docs ├── omnireader.ru │ ├── old │ │ ├── info.txt │ │ ├── robots.txt │ │ ├── config │ │ │ ├── config.js │ │ │ └── config.php │ │ ├── f.php │ │ ├── txt │ │ │ └── .htaccess │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── js │ │ │ ├── bpr319.js │ │ │ ├── load.gif │ │ │ ├── bpricon.gif │ │ │ ├── colo58.png │ │ │ └── stylex.css │ │ ├── test.php │ │ ├── apple-touch-icon.png │ │ ├── apple-touch-icon-precomposed.png │ │ ├── .htaccess │ │ └── parser.php │ ├── stop_server.sh │ ├── deploy.sh │ ├── start_server.sh │ ├── cron_server.sh │ ├── omnireader_http │ ├── README.md │ └── omnireader ├── assets │ ├── face.jpg │ └── reader.jpg ├── beta │ ├── run_server.sh │ ├── deploy.sh │ ├── beta.omnireader_http │ ├── beta.liberama │ └── beta.omnireader └── liberama.top │ └── liberama ├── makefile ├── client ├── assets │ ├── robots.txt │ ├── favicon.ico │ ├── apple-touch-icon.png │ ├── apple-touch-icon-precomposed.png │ └── sw-register.js ├── components │ ├── fonts │ │ ├── arimo.woff2 │ │ ├── avrile.ttf │ │ ├── avrile.woff │ │ ├── geo_1.ttf │ │ ├── geo_1.woff │ │ ├── roboto.ttf │ │ ├── roboto.woff │ │ ├── rubik.woff2 │ │ ├── open-sans.ttf │ │ ├── open-sans.woff │ │ ├── reader-default.ttf │ │ └── reader-default.woff │ ├── Reader │ │ ├── TextPage │ │ │ ├── images │ │ │ │ ├── paper1.jpg │ │ │ │ ├── paper10.png │ │ │ │ ├── paper11.png │ │ │ │ ├── paper12.png │ │ │ │ ├── paper13.png │ │ │ │ ├── paper14.png │ │ │ │ ├── paper15.png │ │ │ │ ├── paper16.png │ │ │ │ ├── paper17.png │ │ │ │ ├── paper2.jpg │ │ │ │ ├── paper3.jpg │ │ │ │ ├── paper4.jpg │ │ │ │ ├── paper5.jpg │ │ │ │ ├── paper6.jpg │ │ │ │ ├── paper7.jpg │ │ │ │ ├── paper8.jpg │ │ │ │ └── paper9.jpg │ │ │ └── TextPage.css │ │ ├── SettingsPage │ │ │ ├── ViewTab │ │ │ │ ├── helper.js │ │ │ │ ├── ViewTab.vue │ │ │ │ └── defPalette.js │ │ │ ├── ResetTab │ │ │ │ └── ResetTab.vue │ │ │ ├── KeysTab │ │ │ │ └── KeysTab.vue │ │ │ ├── ToolBarTab │ │ │ │ └── ToolBarTab.vue │ │ │ └── PageMoveTab │ │ │ │ └── PageMoveTab.vue │ │ ├── share │ │ │ ├── clickMap.js │ │ │ ├── wallpaperStorage.js │ │ │ └── coversStorage.js │ │ ├── HelpPage │ │ │ ├── DonateHelpPage │ │ │ │ └── DonateHelpPage.vue │ │ │ ├── HotkeysHelpPage │ │ │ │ └── HotkeysHelpPage.vue │ │ │ ├── VersionHistoryPage │ │ │ │ └── VersionHistoryPage.vue │ │ │ ├── MouseHelpPage │ │ │ │ └── MouseHelpPage.vue │ │ │ ├── HelpPage.vue │ │ │ └── CommonHelpPage │ │ │ │ └── CommonHelpPage.vue │ │ ├── ClickMapPage │ │ │ └── ClickMapPage.vue │ │ ├── SetPositionPage │ │ │ └── SetPositionPage.vue │ │ ├── ProgressPage │ │ │ └── ProgressPage.vue │ │ ├── CopyTextPage │ │ │ └── CopyTextPage.vue │ │ ├── LoaderPage │ │ │ └── PasteTextPage │ │ │ │ └── PasteTextPage.vue │ │ └── LibsPage │ │ │ └── LibsPage.vue │ ├── ExternalLibs │ │ └── linkUtils.js │ ├── share │ │ ├── Dialog.vue │ │ └── Notify.vue │ └── vueComponent.js ├── api │ ├── webSocketConnection.js │ └── misc.js ├── main.js ├── store │ ├── modules │ │ ├── fonts │ │ │ ├── fonts2list.js │ │ │ └── fonts.json │ │ └── config.js │ ├── root.js │ └── index.js ├── index.html.template ├── share │ ├── dynamicCss.js │ ├── cryptoUtils.js │ └── LockQueue.js ├── router.js └── quasar.js ├── .gitignore ├── nodemon.json ├── .babelrc ├── .eslintrc └── package.json /server/config/application_env: -------------------------------------------------------------------------------- 1 | development -------------------------------------------------------------------------------- /docs/omnireader.ru/old/info.txt: -------------------------------------------------------------------------------- 1 | omnireader.ru -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | 3 | %: 4 | @npm run $@ -------------------------------------------------------------------------------- /client/assets/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /?*url= 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /server/.liberama* 3 | /dist 4 | dev*.sh 5 | 6 | -------------------------------------------------------------------------------- /docs/omnireader.ru/old/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /?*url= 3 | -------------------------------------------------------------------------------- /docs/assets/face.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/docs/assets/face.jpg -------------------------------------------------------------------------------- /docs/assets/reader.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/docs/assets/reader.jpg -------------------------------------------------------------------------------- /client/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/assets/favicon.ico -------------------------------------------------------------------------------- /docs/omnireader.ru/stop_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo service cron stop 4 | sudo killall liberama -------------------------------------------------------------------------------- /docs/omnireader.ru/old/config/config.js: -------------------------------------------------------------------------------- 1 | siteroot = 'http://old.omnireader.ru/'; 2 | doRedirect = ''; 3 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["node_modules", ".git", "client/**/*.*"], 3 | 4 | "ext": "js json" 5 | } -------------------------------------------------------------------------------- /docs/omnireader.ru/old/f.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/docs/omnireader.ru/old/f.php -------------------------------------------------------------------------------- /server/core/LibSharedStorage/index.js: -------------------------------------------------------------------------------- 1 | class LibSharedStorage { 2 | } 3 | 4 | module.exports = LibSharedStorage; -------------------------------------------------------------------------------- /docs/omnireader.ru/old/txt/.htaccess: -------------------------------------------------------------------------------- 1 | AddType 'text/plain; charset=windows-1251' .txz .txt 2 | AddEncoding gzip .txz -------------------------------------------------------------------------------- /client/assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /client/components/fonts/arimo.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/fonts/arimo.woff2 -------------------------------------------------------------------------------- /client/components/fonts/avrile.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/fonts/avrile.ttf -------------------------------------------------------------------------------- /client/components/fonts/avrile.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/fonts/avrile.woff -------------------------------------------------------------------------------- /client/components/fonts/geo_1.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/fonts/geo_1.ttf -------------------------------------------------------------------------------- /client/components/fonts/geo_1.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/fonts/geo_1.woff -------------------------------------------------------------------------------- /client/components/fonts/roboto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/fonts/roboto.ttf -------------------------------------------------------------------------------- /client/components/fonts/roboto.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/fonts/roboto.woff -------------------------------------------------------------------------------- /client/components/fonts/rubik.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/fonts/rubik.woff2 -------------------------------------------------------------------------------- /docs/beta/run_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo -H -u www-data bash -c "cd /var/www; /home/beta.liberama/liberama" 4 | -------------------------------------------------------------------------------- /docs/omnireader.ru/old/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/docs/omnireader.ru/old/favicon.ico -------------------------------------------------------------------------------- /docs/omnireader.ru/old/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/docs/omnireader.ru/old/index.html -------------------------------------------------------------------------------- /docs/omnireader.ru/old/js/bpr319.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/docs/omnireader.ru/old/js/bpr319.js -------------------------------------------------------------------------------- /docs/omnireader.ru/old/js/load.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/docs/omnireader.ru/old/js/load.gif -------------------------------------------------------------------------------- /client/components/fonts/open-sans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/fonts/open-sans.ttf -------------------------------------------------------------------------------- /docs/omnireader.ru/old/js/bpricon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/docs/omnireader.ru/old/js/bpricon.gif -------------------------------------------------------------------------------- /docs/omnireader.ru/old/js/colo58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/docs/omnireader.ru/old/js/colo58.png -------------------------------------------------------------------------------- /docs/omnireader.ru/old/js/stylex.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/docs/omnireader.ru/old/js/stylex.css -------------------------------------------------------------------------------- /client/components/fonts/open-sans.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/fonts/open-sans.woff -------------------------------------------------------------------------------- /docs/beta/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm run build:linux 4 | sudo -u www-data cp -r ../../dist/linux/* /home/beta.liberama 5 | -------------------------------------------------------------------------------- /client/components/fonts/reader-default.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/fonts/reader-default.ttf -------------------------------------------------------------------------------- /docs/omnireader.ru/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm run build:linux 4 | sudo -u www-data cp -r ../../dist/linux/* /home/liberama 5 | -------------------------------------------------------------------------------- /docs/omnireader.ru/old/test.php: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /client/components/fonts/reader-default.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/fonts/reader-default.woff -------------------------------------------------------------------------------- /docs/omnireader.ru/old/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/docs/omnireader.ru/old/apple-touch-icon.png -------------------------------------------------------------------------------- /client/assets/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/assets/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /client/api/webSocketConnection.js: -------------------------------------------------------------------------------- 1 | import WebSocketConnection from '../../server/core/WebSocketConnection'; 2 | 3 | export default new WebSocketConnection(); -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper1.jpg -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper10.png -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper11.png -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper12.png -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper13.png -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper14.png -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper15.png -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper16.png -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper17.png -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper2.jpg -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper3.jpg -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper4.jpg -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper5.jpg -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper6.jpg -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper7.jpg -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper8.jpg -------------------------------------------------------------------------------- /client/components/Reader/TextPage/images/paper9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/client/components/Reader/TextPage/images/paper9.jpg -------------------------------------------------------------------------------- /server/config/development.js: -------------------------------------------------------------------------------- 1 | const base = require('./base'); 2 | 3 | module.exports = Object.assign({}, base, { 4 | branch: 'development', 5 | }); 6 | -------------------------------------------------------------------------------- /docs/omnireader.ru/old/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookpauk/liberama/HEAD/docs/omnireader.ru/old/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /docs/omnireader.ru/old/config/config.php: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /client/assets/sw-register.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | if('serviceWorker' in navigator) { 3 | navigator.serviceWorker.register('/service-worker.js'); 4 | } 5 | })(); 6 | -------------------------------------------------------------------------------- /docs/omnireader.ru/start_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama >/dev/null & disown" 4 | sudo service cron start 5 | -------------------------------------------------------------------------------- /server/controllers/BaseController.js: -------------------------------------------------------------------------------- 1 | class BaseController { 2 | constructor(config) { 3 | this.config = config; 4 | } 5 | } 6 | 7 | module.exports = BaseController; -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [['@babel/preset-env', { "targets": { "esmodules": true } }]], 3 | "plugins": [ 4 | ["@babel/plugin-proposal-decorators", { "legacy": true }] 5 | ] 6 | } -------------------------------------------------------------------------------- /server/db/jembaMigrations/reader-storage/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | table: 'migration1', 3 | data: [ 4 | {id: 1, name: 'create', data: require('./001-create')} 5 | ] 6 | } -------------------------------------------------------------------------------- /server/db/jembaMigrations/book-update-server/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | table: 'migration1', 3 | data: [ 4 | {id: 1, name: 'create', data: require('./001-create')} 5 | ] 6 | } -------------------------------------------------------------------------------- /docs/omnireader.ru/old/.htaccess: -------------------------------------------------------------------------------- 1 | #RewriteEngine On 2 | #RewriteCond %{HTTP_HOST} ^www.bookpauk.ru$ [NC] 3 | #RewriteRule ^(.*)$ http://bookpauk.ru/$1 [R=301,L] 4 | 5 | Options None 6 | Options +ExecCGI 7 | -------------------------------------------------------------------------------- /server/db/jembaMigrations/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'app': require('./app'), 3 | 'reader-storage': require('./reader-storage'), 4 | 'book-update-server': require('./book-update-server'), 5 | }; 6 | -------------------------------------------------------------------------------- /server/db/jembaMigrations/app/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | table: 'migration1', 3 | data: [ 4 | {id: 1, name: 'create', data: require('./001-create')}, 5 | {id: 2, name: 'create', data: require('./002-create')}, 6 | ] 7 | } -------------------------------------------------------------------------------- /docs/omnireader.ru/cron_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! pgrep -x "liberama" > /dev/null ; then 4 | sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama >/dev/null" 5 | else 6 | echo "Process 'liberama' already running" 7 | fi 8 | 9 | -------------------------------------------------------------------------------- /server/db/jembaMigrations/app/001-create.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: [ 3 | ['create', { 4 | table: 'remote_sent' 5 | }], 6 | ], 7 | down: [ 8 | ['drop', { 9 | table: 'remote_sent' 10 | }], 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /server/controllers/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | MiscController: require('./MiscController'), 3 | ReaderController: require('./ReaderController'), 4 | WorkerController: require('./WorkerController'), 5 | WebSocketController: require('./WebSocketController'), 6 | BookUpdateCheckerController: require('./BookUpdateCheckerController'), 7 | } -------------------------------------------------------------------------------- /client/components/Reader/SettingsPage/ViewTab/helper.js: -------------------------------------------------------------------------------- 1 | const hex = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/; 2 | 3 | export function colorPanStyle(bgColor) { 4 | return `width: 30px; height: 30px; border: 1px solid black; border-radius: 4px; background-color: ${bgColor}`; 5 | } 6 | 7 | export function isHexColor(value) { 8 | return hex.test(value); 9 | } 10 | -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | 3 | import router from './router'; 4 | import store from './store'; 5 | import q from './quasar'; 6 | 7 | import App from './components/App.vue'; 8 | 9 | const app = createApp(App); 10 | 11 | app.use(router); 12 | app.use(store); 13 | app.use(q.quasar, q.options); 14 | q.init(); 15 | 16 | app.mount('#app'); 17 | -------------------------------------------------------------------------------- /server/db/jembaMigrations/reader-storage/001-create.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: [ 3 | //CREATE TABLE storage (id TEXT PRIMARY KEY, rev INTEGER, time INTEGER, data TEXT); 4 | ['create', { 5 | table: 'storage' 6 | }], 7 | ], 8 | down: [ 9 | ['drop', { 10 | table: 'storage' 11 | }], 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /client/components/Reader/share/clickMap.js: -------------------------------------------------------------------------------- 1 | export const clickMap = { 2 | 33: {30: 'PgUp', 100: 'PgDown'}, 3 | 67: {30: 'Up', 70: 'Menu', 100: 'Down'}, 4 | 100: {30: 'PgUp', 100: 'PgDown'} 5 | }; 6 | 7 | export const clickMapText = { 8 | 'PgUp': 'Страницу назад', 9 | 'PgDown': 'Страницу вперед', 10 | 'Up': 'Строку назад', 11 | 'Down': 'Строку вперед', 12 | 'Menu': 'Показать или скрыть панель', 13 | }; 14 | -------------------------------------------------------------------------------- /client/store/modules/fonts/fonts2list.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | 3 | async function main() { 4 | const webfonts = await fs.readFile('webfonts.json'); 5 | let fonts = JSON.parse(webfonts); 6 | 7 | fonts = fonts.items.filter(item => item.subsets.includes('cyrillic')); 8 | fonts = fonts.map(item => item.family); 9 | fonts.sort(); 10 | 11 | await fs.writeFile('fonts.json', JSON.stringify(fonts)); 12 | } 13 | main(); -------------------------------------------------------------------------------- /client/store/root.js: -------------------------------------------------------------------------------- 1 | // initial state 2 | const state = { 3 | apiError: null, 4 | }; 5 | 6 | // getters 7 | const getters = {}; 8 | 9 | // actions 10 | const actions = {}; 11 | 12 | // mutations 13 | const mutations = { 14 | setApiError(state, value) { 15 | state.apiError = value; 16 | }, 17 | }; 18 | 19 | export default { 20 | namespaced: true, 21 | state, 22 | getters, 23 | actions, 24 | mutations 25 | }; 26 | -------------------------------------------------------------------------------- /server/config/production.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const base = require('./base'); 3 | 4 | const execDir = path.dirname(process.execPath); 5 | 6 | module.exports = Object.assign({}, base, { 7 | branch: 'production', 8 | 9 | execDir, 10 | 11 | servers: [ 12 | { 13 | serverName: '1', 14 | mode: 'reader', 15 | ip: '0.0.0.0', 16 | port: '44080', 17 | }, 18 | ], 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /client/store/modules/config.js: -------------------------------------------------------------------------------- 1 | // initial state 2 | const state = { 3 | name: null, 4 | version: null, 5 | mode: null, 6 | }; 7 | 8 | // getters 9 | const getters = {}; 10 | 11 | // actions 12 | const actions = {}; 13 | 14 | // mutations 15 | const mutations = { 16 | setConfig(state, value) { 17 | Object.assign(state, value); 18 | }, 19 | }; 20 | 21 | export default { 22 | namespaced: true, 23 | state, 24 | getters, 25 | actions, 26 | mutations 27 | }; 28 | -------------------------------------------------------------------------------- /client/index.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /client/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex'; 2 | //import createPersistedState from 'vuex-persistedstate'; 3 | import VuexPersistence from 'vuex-persist'; 4 | 5 | import root from './root.js'; 6 | import config from './modules/config'; 7 | import reader from './modules/reader'; 8 | 9 | const debug = process.env.NODE_ENV !== 'production'; 10 | 11 | const vuexLocal = new VuexPersistence(); 12 | 13 | export default createStore(Object.assign({}, root, { 14 | modules: { 15 | config, 16 | reader, 17 | }, 18 | strict: debug, 19 | plugins: [vuexLocal.plugin] 20 | })); 21 | -------------------------------------------------------------------------------- /server/controllers/MiscController.js: -------------------------------------------------------------------------------- 1 | const BaseController = require('./BaseController'); 2 | const _ = require('lodash'); 3 | 4 | class MiscController extends BaseController { 5 | async getConfig(req, res) { 6 | if (Array.isArray(req.body.params)) { 7 | const paramsSet = new Set(req.body.params); 8 | 9 | return _.pick(this.config, this.config.webConfigParams.filter(x => paramsSet.has(x))); 10 | } 11 | //bad request 12 | res.status(400).send({error: 'params is not an array'}); 13 | return false; 14 | } 15 | 16 | } 17 | 18 | module.exports = MiscController; 19 | -------------------------------------------------------------------------------- /client/share/dynamicCss.js: -------------------------------------------------------------------------------- 1 | class DynamicCss { 2 | constructor() { 3 | this.cssNodes = {}; 4 | } 5 | 6 | replace(name, cssText) { 7 | const style = document.createElement('style'); 8 | style.type = 'text/css'; 9 | style.innerHTML = cssText; 10 | 11 | const parent = document.getElementsByTagName('head')[0]; 12 | 13 | if (this.cssNodes[name]) { 14 | parent.removeChild(this.cssNodes[name]); 15 | delete this.cssNodes[name]; 16 | } 17 | 18 | this.cssNodes[name] = parent.appendChild(style); 19 | } 20 | } 21 | 22 | export default new DynamicCss(); -------------------------------------------------------------------------------- /server/db/jembaMigrations/app/002-create.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: [ 3 | ['create', { 4 | /*{ 5 | id, // book URL 6 | queryTime: Number, 7 | checkTime: Number, // 0 - never checked 8 | modTime: String, 9 | size: Number, 10 | checkSum: String, //sha256 11 | state: Number, // 0 - not processing, 1 - processing 12 | error: String, 13 | }*/ 14 | table: 'buc' 15 | }], 16 | ], 17 | down: [ 18 | ['drop', { 19 | table: 'buc' 20 | }], 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /client/api/misc.js: -------------------------------------------------------------------------------- 1 | import wsc from './webSocketConnection'; 2 | 3 | class Misc { 4 | async loadConfig(_configHash) { 5 | 6 | const query = { 7 | params: [ 8 | 'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 9 | 'acceptFileExt', 'bucEnabled', 'branch', 'networkLibraryLink', 'restricted' 10 | ], 11 | _configHash, 12 | }; 13 | 14 | const config = await wsc.message(await wsc.send(Object.assign({action: 'get-config'}, query))); 15 | if (config.error) 16 | throw new Error(config.error); 17 | 18 | return config; 19 | } 20 | } 21 | 22 | export default new Misc(); -------------------------------------------------------------------------------- /client/share/cryptoUtils.js: -------------------------------------------------------------------------------- 1 | //WebCrypto API (crypto.subtle) не работает без https, поэтому приходится извращаться через sjcl 2 | import sjclWrapper from './sjclWrapper'; 3 | 4 | //не менять 5 | const iv = 'B6E2XejNh2dS'; 6 | const salt = 'Liberama project is awesome'; 7 | 8 | export function aesEncrypt(data, password) { 9 | return sjclWrapper.codec.bytes.fromBits( 10 | sjclWrapper.encryptArray( 11 | password, sjclWrapper.codec.bytes.toBits(data), {iv, salt} 12 | ).ct 13 | ); 14 | } 15 | 16 | export function aesDecrypt(data, password) { 17 | return sjclWrapper.codec.bytes.fromBits( 18 | sjclWrapper.decryptArray( 19 | password, {ct: sjclWrapper.codec.bytes.toBits(data)}, {iv, salt} 20 | ) 21 | ); 22 | } 23 | 24 | export function sha256(str) { 25 | return sjclWrapper.codec.bytes.fromBits(sjclWrapper.hash.sha256.hash(str)); 26 | } 27 | -------------------------------------------------------------------------------- /server/db/jembaMigrations/book-update-server/001-create.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: [ 3 | ['create', { 4 | /*{ 5 | id, // book URL 6 | queryTime: Number, 7 | checkTime: Number, // 0 - never checked 8 | modTime: String, 9 | size: Number, 10 | checkSum: String, //sha256 11 | state: Number, // 0 - not processing, 1 - processing 12 | error: String, 13 | }*/ 14 | table: 'buc', 15 | flag: [ 16 | {name: 'notProcessing', check: `(r) => r.state === 0`}, 17 | ], 18 | index: [ 19 | {field: 'queryTime', type: 'number'}, 20 | {field: 'checkTime', type: 'number'}, 21 | ] 22 | }], 23 | ], 24 | down: [ 25 | ['drop', { 26 | table: 'buc' 27 | }], 28 | ] 29 | }; 30 | -------------------------------------------------------------------------------- /server/controllers/WorkerController.js: -------------------------------------------------------------------------------- 1 | const BaseController = require('./BaseController'); 2 | const WorkerState = require('../core/WorkerState');//singleton 3 | const utils = require('../core/utils'); 4 | 5 | class WorkerController extends BaseController { 6 | constructor(config) { 7 | super(config); 8 | this.workerState = new WorkerState(); 9 | } 10 | 11 | async getState(req, res) { 12 | const request = req.body; 13 | let error = ''; 14 | try { 15 | if (!request.workerId) 16 | throw new Error(`key 'workerId' is wrong`); 17 | 18 | const state = this.workerState.getState(request.workerId); 19 | 20 | return (state ? state : {}); 21 | } catch (e) { 22 | error = e.message; 23 | } 24 | //bad request 25 | res.status(400).send({error}); 26 | return false; 27 | } 28 | 29 | } 30 | 31 | module.exports = WorkerController; 32 | -------------------------------------------------------------------------------- /server/createWebApp.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | 3 | const webApp = require('../dist/public.json'); 4 | const ZipReader = require('./core/Zip/ZipReader'); 5 | 6 | module.exports = async(config) => { 7 | const verFile = `${config.publicDir}/version.txt`; 8 | const zipFile = `${config.tempDir}/public.zip`; 9 | 10 | if (await fs.pathExists(verFile)) { 11 | const curPublicVersion = await fs.readFile(verFile, 'utf8'); 12 | if (curPublicVersion == config.version + config.rootPathStatic) 13 | return; 14 | } 15 | 16 | await fs.remove(config.publicDir); 17 | 18 | //извлекаем новый webApp 19 | await fs.writeFile(zipFile, webApp.data, {encoding: 'base64'}); 20 | const zipReader = new ZipReader(); 21 | await zipReader.open(zipFile); 22 | 23 | try { 24 | await zipReader.extractAllToDir(config.publicDir); 25 | } finally { 26 | await zipReader.close(); 27 | } 28 | 29 | await fs.writeFile(verFile, config.version + config.rootPathStatic); 30 | await fs.remove(zipFile); 31 | }; -------------------------------------------------------------------------------- /client/components/Reader/SettingsPage/ResetTab/ResetTab.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 39 | 40 | 42 | -------------------------------------------------------------------------------- /client/components/Reader/share/wallpaperStorage.js: -------------------------------------------------------------------------------- 1 | import localForage from 'localforage'; 2 | //import _ from 'lodash'; 3 | 4 | const wpStore = localForage.createInstance({ 5 | name: 'wallpaperStorage' 6 | }); 7 | 8 | class WallpaperStorage { 9 | constructor() { 10 | this.cachedKeys = []; 11 | } 12 | 13 | async init() { 14 | this.cachedKeys = await wpStore.keys(); 15 | } 16 | 17 | async getLength() { 18 | return await wpStore.length(); 19 | } 20 | 21 | async setData(key, data) { 22 | await wpStore.setItem(key, data); 23 | this.cachedKeys = await wpStore.keys(); 24 | } 25 | 26 | async getData(key) { 27 | return await wpStore.getItem(key); 28 | } 29 | 30 | async removeData(key) { 31 | await wpStore.removeItem(key); 32 | this.cachedKeys = await wpStore.keys(); 33 | } 34 | 35 | async getKeys() { 36 | return await wpStore.keys(); 37 | } 38 | 39 | keyExists(key) {//не асинхронная 40 | return this.cachedKeys.includes(key); 41 | } 42 | } 43 | 44 | export default new WallpaperStorage(); -------------------------------------------------------------------------------- /server/core/Reader/BookConverter/ConvertDoc.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | 4 | const ConvertDocX = require('./ConvertDocX'); 5 | 6 | class ConvertDoc extends ConvertDocX { 7 | check(data, opts) { 8 | const {inputFiles} = opts; 9 | 10 | return this.config.useExternalBookConverter && 11 | inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'msi'; 12 | } 13 | 14 | async run(data, opts) { 15 | if (!this.check(data, opts)) 16 | return false; 17 | await this.checkExternalConverterPresent(); 18 | 19 | const {inputFiles, callback, abort} = opts; 20 | 21 | const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`; 22 | const docFile = `${outFile}.doc`; 23 | const docxFile = `${outFile}.docx`; 24 | const fb2File = `${outFile}.fb2`; 25 | 26 | await fs.copy(inputFiles.sourceFile, docFile); 27 | await this.execConverter(this.sofficePath, ['--headless', '--convert-to', 'docx', '--outdir', inputFiles.filesDir, docFile], null, abort); 28 | 29 | return await super.convert(docxFile, fb2File, callback, abort); 30 | } 31 | } 32 | 33 | module.exports = ConvertDoc; 34 | -------------------------------------------------------------------------------- /server/core/Reader/BookConverter/ConvertRtf.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | 4 | const ConvertDocX = require('./ConvertDocX'); 5 | 6 | class ConvertRtf extends ConvertDocX { 7 | check(data, opts) { 8 | const {inputFiles} = opts; 9 | 10 | return this.config.useExternalBookConverter && 11 | inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'rtf'; 12 | } 13 | 14 | async run(data, opts) { 15 | if (!this.check(data, opts)) 16 | return false; 17 | await this.checkExternalConverterPresent(); 18 | 19 | const {inputFiles, callback, abort} = opts; 20 | 21 | const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`; 22 | const rtfFile = `${outFile}.rtf`; 23 | const docxFile = `${outFile}.docx`; 24 | const fb2File = `${outFile}.fb2`; 25 | 26 | await fs.copy(inputFiles.sourceFile, rtfFile); 27 | await this.execConverter(this.sofficePath, ['--headless', '--convert-to', 'docx', '--outdir', inputFiles.filesDir, rtfFile], null, abort); 28 | 29 | return await super.convert(docxFile, fb2File, callback, abort); 30 | } 31 | } 32 | 33 | module.exports = ConvertRtf; 34 | -------------------------------------------------------------------------------- /server/core/Reader/BookConverter/ConvertMobi.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | 4 | const ConvertBase = require('./ConvertBase'); 5 | 6 | class ConvertMobi extends ConvertBase { 7 | async check(data, opts) { 8 | const {inputFiles} = opts; 9 | 10 | return (this.config.useExternalBookConverter && 11 | inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'mobi'); 12 | } 13 | 14 | async run(data, opts) { 15 | if (!await this.check(data, opts)) 16 | return false; 17 | await this.checkExternalConverterPresent(); 18 | 19 | const {inputFiles, callback, abort} = opts; 20 | 21 | const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`; 22 | const mobiFile = `${outFile}.mobi`; 23 | const fb2File = `${outFile}.fb2`; 24 | 25 | await fs.copy(inputFiles.sourceFile, mobiFile); 26 | 27 | let perc = 0; 28 | await this.execConverter(this.calibrePath, [mobiFile, fb2File, '-vv'], () => { 29 | perc = (perc < 100 ? perc + 1 : 50); 30 | callback(perc); 31 | }, abort); 32 | 33 | return await fs.readFile(fb2File); 34 | } 35 | } 36 | 37 | module.exports = ConvertMobi; 38 | -------------------------------------------------------------------------------- /client/components/ExternalLibs/linkUtils.js: -------------------------------------------------------------------------------- 1 | export function addProtocol(url) { 2 | if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0)) 3 | return 'http://' + url; 4 | return url; 5 | } 6 | 7 | export function removeProtocol(url) { 8 | return url.replace(/(^\w+:|^)\/\//, ''); 9 | } 10 | 11 | export function getOrigin(url) { 12 | const parsed = new URL(url); 13 | return parsed.origin; 14 | } 15 | 16 | export function removeOrigin(url) { 17 | const parsed = new URL(url); 18 | const result = url.substring(parsed.origin.length); 19 | return (result ? result : '/'); 20 | } 21 | 22 | export function getRootIndexByUrl(groups, url) { 23 | const origin = getOrigin(url); 24 | for (let i = 0; i < groups.length; i++) { 25 | if (groups[i].r == origin) 26 | return i; 27 | } 28 | return -1; 29 | } 30 | 31 | export function getSafeRootIndexByUrl(groups, url) { 32 | let index = -1; 33 | try { 34 | index = getRootIndexByUrl(groups, url); 35 | } catch(e) { 36 | // 37 | } 38 | return index; 39 | } 40 | 41 | export function getListItemByLink(list, link) { 42 | for (const item of list) { 43 | if (item.l == link) 44 | return item; 45 | } 46 | return null; 47 | } 48 | 49 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "vue-eslint-parser", 3 | "parserOptions": { 4 | "parser": "@babel/eslint-parser", 5 | "sourceType": "module" 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:vue/recommended" 10 | ], 11 | "plugins": [ 12 | "@babel" 13 | ], 14 | "env": { 15 | "es6": true, 16 | "browser": true, 17 | "node": true 18 | }, 19 | "globals": { 20 | "LM_OK": false, 21 | "LM_INFO": false, 22 | "LM_WARN": false, 23 | "LM_ERR": false, 24 | "LM_FATAL": false, 25 | "LM_TOTAL": false 26 | }, 27 | "rules": { 28 | "vue/html-indent": ["warn", 4, { 29 | "alignAttributesVertically": false 30 | }], 31 | "vue/max-attributes-per-line": "off", 32 | "vue/html-self-closing": "off", 33 | "vue/no-v-html": "off", 34 | "vue/no-v-model-argument": "off", 35 | 36 | "strict": 0, 37 | "indent": [0, 4, { 38 | "SwitchCase": 1 39 | }], 40 | "space-before-function-paren": [2, "never"], 41 | "valid-jsdoc": [2, { 42 | "requireReturn": false, 43 | "prefer": { 44 | "returns": "return" 45 | } 46 | }], 47 | "require-jsdoc": 0, 48 | "max-len": [1, 200, 4, { 49 | "ignoreComments": true, 50 | "ignoreUrls": true 51 | }], 52 | "no-console": off 53 | } 54 | } -------------------------------------------------------------------------------- /client/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router'; 2 | import _ from 'lodash'; 3 | 4 | //немедленная загрузка 5 | //import Reader from './components/Reader/Reader.vue'; 6 | const Reader = () => import('./components/Reader/Reader.vue'); 7 | const ExternalLibs = () => import('./components/ExternalLibs/ExternalLibs.vue'); 8 | 9 | const myRoutes = [ 10 | ['/', null, null, '/reader'], 11 | ['/reader', Reader], 12 | ['/external-libs', ExternalLibs], 13 | ['/:pathMatch(.*)*', null, null, '/reader'], 14 | ]; 15 | 16 | let routes = {}; 17 | 18 | for (let route of myRoutes) { 19 | const [path, component, name, redirect] = route; 20 | let cleanRoute = _.pickBy({path, component, name, redirect}, _.identity); 21 | 22 | let parts = cleanRoute.path.split('~'); 23 | let f = routes; 24 | for (let part of parts) { 25 | const curRoute = _.assign({}, cleanRoute, { path: part }); 26 | 27 | if (!f.children) 28 | f.children = []; 29 | let r = f.children; 30 | 31 | f = _.find(r, {path: part}); 32 | if (!f) { 33 | r.push(curRoute); 34 | f = curRoute; 35 | } 36 | } 37 | } 38 | routes = routes.children; 39 | 40 | export default createRouter({ 41 | history: createWebHashHistory(), 42 | routes 43 | }); 44 | -------------------------------------------------------------------------------- /server/dev.js: -------------------------------------------------------------------------------- 1 | const log = new (require('./core/AppLogger'))().log;//singleton 2 | 3 | function webpackDevMiddleware(app) { 4 | const webpack = require('webpack'); 5 | const wpConfig = require('../build/webpack.dev.config'); 6 | 7 | const compiler = webpack(wpConfig); 8 | const devMiddleware = require('webpack-dev-middleware'); 9 | app.use(devMiddleware(compiler, { 10 | publicPath: wpConfig.output.publicPath, 11 | stats: {colors: true} 12 | })); 13 | 14 | let hotMiddleware = require('webpack-hot-middleware'); 15 | app.use(hotMiddleware(compiler, { 16 | log: log 17 | })); 18 | } 19 | 20 | function logQueries(app) { 21 | app.use(function(req, res, next) { 22 | const start = Date.now(); 23 | log(`${req.method} ${req.originalUrl} ${JSON.stringify(req.body).substr(0, 4000)}`); 24 | //log(`${JSON.stringify(req.headers, null, 2)}`) 25 | res.once('finish', () => { 26 | log(`${Date.now() - start}ms`); 27 | }); 28 | next(); 29 | }); 30 | } 31 | 32 | function logErrors(app) { 33 | app.use(function(err, req, res, next) {// eslint-disable-line no-unused-vars 34 | log(LM_ERR, err.stack); 35 | res.status(500).send(err.stack); 36 | }); 37 | } 38 | 39 | module.exports = { 40 | webpackDevMiddleware, 41 | logQueries, 42 | logErrors 43 | }; -------------------------------------------------------------------------------- /docs/beta/beta.omnireader_http: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name beta.omnireader.ru b.beta.omnireader.ru; 4 | set $liberama http://127.0.0.1:34081; 5 | 6 | client_max_body_size 50m; 7 | proxy_read_timeout 1h; 8 | 9 | gzip on; 10 | gzip_min_length 1024; 11 | gzip_proxied expired no-cache no-store private auth; 12 | gzip_types *; 13 | 14 | location @liberama { 15 | proxy_pass $liberama; 16 | } 17 | 18 | location /api { 19 | proxy_pass $liberama; 20 | } 21 | 22 | location /ws { 23 | proxy_pass $liberama; 24 | proxy_http_version 1.1; 25 | proxy_set_header Upgrade $http_upgrade; 26 | proxy_set_header Connection "upgrade"; 27 | proxy_read_timeout 600s; 28 | } 29 | 30 | location /tmp { 31 | root /home/beta.liberama/.liberama/public-files; 32 | 33 | types { } default_type "application/xml; charset=utf-8"; 34 | add_header Content-Encoding gzip; 35 | try_files $uri @liberama; 36 | } 37 | 38 | location /upload { 39 | root /home/beta.liberama/.liberama/public-files; 40 | 41 | try_files $uri @liberama; 42 | } 43 | 44 | location / { 45 | root /home/beta.liberama/.liberama/public; 46 | 47 | location ~* \.(?:manifest|appcache|html)$ { 48 | expires -1; 49 | } 50 | } 51 | } 52 | 53 | server { 54 | listen 80; 55 | server_name beta.omnireader.ru; 56 | 57 | return 301 https://$host$request_uri; 58 | } 59 | -------------------------------------------------------------------------------- /client/share/LockQueue.js: -------------------------------------------------------------------------------- 1 | class LockQueue { 2 | constructor(queueSize) { 3 | this.queueSize = queueSize; 4 | this.freed = true; 5 | this.waitingQueue = []; 6 | } 7 | 8 | //async 9 | get(take = true) { 10 | return new Promise((resolve, reject) => { 11 | if (this.freed) { 12 | if (take) 13 | this.freed = false; 14 | resolve(); 15 | return; 16 | } 17 | 18 | if (this.waitingQueue.length < this.queueSize) { 19 | this.waitingQueue.push({resolve, reject}); 20 | } else { 21 | reject(new Error('Lock queue is too long')); 22 | } 23 | }); 24 | } 25 | 26 | ret() { 27 | if (this.waitingQueue.length) { 28 | this.waitingQueue.shift().resolve(); 29 | } else { 30 | this.freed = true; 31 | } 32 | } 33 | 34 | //async 35 | wait() { 36 | return this.get(false); 37 | } 38 | 39 | retAll() { 40 | while (this.waitingQueue.length) { 41 | this.waitingQueue.shift().resolve(); 42 | } 43 | } 44 | 45 | errAll(error = 'rejected') { 46 | while (this.waitingQueue.length) { 47 | this.waitingQueue.shift().reject(new Error(error)); 48 | } 49 | } 50 | 51 | } 52 | 53 | export default LockQueue; -------------------------------------------------------------------------------- /client/components/Reader/HelpPage/DonateHelpPage/DonateHelpPage.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 38 | 39 | 53 | -------------------------------------------------------------------------------- /server/core/Zip/ZipReader.js: -------------------------------------------------------------------------------- 1 | const StreamUnzip = require('./node_stream_zip_changed'); 2 | //const StreamUnzip = require('node-stream-zip'); 3 | 4 | class ZipReader { 5 | constructor() { 6 | this.zip = null; 7 | } 8 | 9 | checkState() { 10 | if (!this.zip) 11 | throw new Error('Zip closed'); 12 | } 13 | 14 | async open(zipFile, zipEntries = true) { 15 | if (this.zip) 16 | throw new Error('Zip file is already open'); 17 | 18 | const zip = new StreamUnzip.async({file: zipFile, skipEntryNameValidation: true}); 19 | 20 | if (zipEntries) 21 | this.zipEntries = await zip.entries(); 22 | 23 | this.zip = zip; 24 | } 25 | 26 | get entries() { 27 | this.checkState(); 28 | 29 | return this.zipEntries; 30 | } 31 | 32 | async extractToBuf(entryFilePath) { 33 | this.checkState(); 34 | 35 | return await this.zip.entryData(entryFilePath); 36 | } 37 | 38 | async extractToFile(entryFilePath, outputFile) { 39 | this.checkState(); 40 | 41 | await this.zip.extract(entryFilePath, outputFile); 42 | } 43 | 44 | async extractAllToDir(outputDir) { 45 | this.checkState(); 46 | 47 | await this.zip.extract(null, outputDir); 48 | } 49 | 50 | async close() { 51 | if (this.zip) { 52 | await this.zip.close(); 53 | this.zip = null; 54 | this.zipEntries = undefined; 55 | } 56 | } 57 | } 58 | 59 | module.exports = ZipReader; -------------------------------------------------------------------------------- /client/components/Reader/HelpPage/HotkeysHelpPage/HotkeysHelpPage.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 48 | 49 | 55 | -------------------------------------------------------------------------------- /client/store/modules/fonts/fonts.json: -------------------------------------------------------------------------------- 1 | ["Alegreya","Alegreya SC","Alegreya Sans","Alegreya Sans SC","Alice","Amatic SC","Andika","Anonymous Pro","Arimo","Arsenal","Bad Script","Balsamiq Sans","Bellota","Bellota Text","Bitter","Caveat","Comfortaa","Commissioner","Cormorant","Cormorant Garamond","Cormorant Infant","Cormorant SC","Cormorant Unicase","Cousine","Cuprum","Didact Gothic","EB Garamond","El Messiri","Exo 2","Fira Code","Fira Mono","Fira Sans","Fira Sans Condensed","Fira Sans Extra Condensed","Forum","Gabriela","Hachi Maru Pop","IBM Plex Mono","IBM Plex Sans","IBM Plex Serif","Inter","Istok Web","JetBrains Mono","Jost","Jura","Kelly Slab","Kosugi","Kosugi Maru","Kurale","Ledger","Literata","Lobster","Lora","M PLUS 1p","M PLUS Rounded 1c","Manrope","Marck Script","Marmelad","Merriweather","Montserrat","Montserrat Alternates","Neucha","Noto Sans","Noto Serif","Nunito","Old Standard TT","Open Sans","Open Sans Condensed","Oranienbaum","Oswald","PT Mono","PT Sans","PT Sans Caption","PT Sans Narrow","PT Serif","PT Serif Caption","Pacifico","Pangolin","Pattaya","Philosopher","Piazzolla","Play","Playfair Display","Playfair Display SC","Podkova","Poiret One","Prata","Press Start 2P","Prosto One","Raleway","Roboto","Roboto Condensed","Roboto Mono","Roboto Slab","Rubik","Rubik Mono One","Ruda","Ruslan Display","Russo One","Sawarabi Gothic","Scada","Seymour One","Source Code Pro","Source Sans Pro","Source Serif Pro","Spectral","Spectral SC","Stalinist One","Tenor Sans","Tinos","Ubuntu","Ubuntu Condensed","Ubuntu Mono","Underdog","Viaoda Libre","Vollkorn","Vollkorn SC","Yanone Kaffeesatz","Yeseva One"] -------------------------------------------------------------------------------- /client/components/Reader/share/coversStorage.js: -------------------------------------------------------------------------------- 1 | import localForage from 'localforage'; 2 | //import _ from 'lodash'; 3 | import * as utils from '../../../share/utils'; 4 | 5 | const maxDataSize = 100*1024*1024; 6 | 7 | const coversStore = localForage.createInstance({ 8 | name: 'coversStorage' 9 | }); 10 | 11 | class CoversStorage { 12 | constructor() { 13 | } 14 | 15 | async init() { 16 | this.cleanCovers(); //no await 17 | } 18 | 19 | async setData(key, data) { 20 | await coversStore.setItem(key, {addTime: Date.now(), data}); 21 | } 22 | 23 | async getData(key) { 24 | const item = await coversStore.getItem(key); 25 | return (item ? item.data : undefined); 26 | } 27 | 28 | async removeData(key) { 29 | await coversStore.removeItem(key); 30 | } 31 | 32 | async cleanCovers() { 33 | await utils.sleep(10000); 34 | 35 | while (1) {// eslint-disable-line no-constant-condition 36 | let size = 0; 37 | let min = Date.now(); 38 | let toDel = null; 39 | for (const key of (await coversStore.keys())) { 40 | const item = await coversStore.getItem(key); 41 | 42 | size += item.data.length; 43 | 44 | if (item.addTime < min) { 45 | toDel = key; 46 | min = item.addTime; 47 | } 48 | } 49 | 50 | 51 | if (size > maxDataSize && toDel) { 52 | await this.removeData(toDel); 53 | } else { 54 | break; 55 | } 56 | } 57 | } 58 | 59 | } 60 | 61 | export default new CoversStorage(); -------------------------------------------------------------------------------- /docs/omnireader.ru/omnireader_http: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name omnireader.ru b.omnireader.ru; 4 | set $liberama http://127.0.0.1:44081; 5 | 6 | client_max_body_size 50m; 7 | proxy_read_timeout 1h; 8 | 9 | gzip on; 10 | gzip_min_length 1024; 11 | gzip_proxied expired no-cache no-store private auth; 12 | gzip_types *; 13 | 14 | location @liberama { 15 | proxy_pass $liberama; 16 | } 17 | 18 | location /api { 19 | proxy_pass $liberama; 20 | } 21 | 22 | location /ws { 23 | proxy_pass $liberama; 24 | proxy_http_version 1.1; 25 | proxy_set_header Upgrade $http_upgrade; 26 | proxy_set_header Connection "upgrade"; 27 | } 28 | 29 | location /tmp { 30 | root /home/liberama/.liberama/public-files; 31 | 32 | types { } default_type "application/xml; charset=utf-8"; 33 | add_header Content-Encoding gzip; 34 | try_files $uri @liberama; 35 | } 36 | 37 | location /upload { 38 | root /home/liberama/.liberama/public-files; 39 | 40 | try_files $uri @liberama; 41 | } 42 | 43 | location / { 44 | root /home/liberama/.liberama/public; 45 | 46 | location ~* \.(?:manifest|appcache|html)$ { 47 | expires -1; 48 | } 49 | } 50 | } 51 | 52 | server { 53 | listen 80; 54 | server_name old.omnireader.ru; 55 | 56 | client_max_body_size 50m; 57 | 58 | gzip on; 59 | gzip_min_length 1024; 60 | gzip_proxied expired no-cache no-store private auth; 61 | gzip_types *; 62 | 63 | root /home/oldreader; 64 | 65 | index index.html; 66 | 67 | # Обработка php файлов с помощью fpm 68 | location ~ \.php$ { 69 | try_files $uri =404; 70 | include /etc/nginx/fastcgi.conf; 71 | fastcgi_pass unix:/run/php/php7.4-fpm.sock; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /server/core/Reader/BookConverter/ConvertEpub.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | 4 | const ConvertBase = require('./ConvertBase'); 5 | 6 | class ConvertEpub extends ConvertBase { 7 | async check(data, opts) { 8 | const {inputFiles} = opts; 9 | 10 | if (this.config.useExternalBookConverter && 11 | inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'zip') { 12 | //ищем файл 'mimetype' 13 | for (const file of inputFiles.files) { 14 | if (file.path == 'mimetype') { 15 | const mt = await fs.readFile(`${inputFiles.filesDir}/${file.path}`); 16 | if (mt.toString().trim() == 'application/epub+zip') 17 | return true; 18 | break; 19 | } 20 | } 21 | } 22 | 23 | return false; 24 | } 25 | 26 | async run(data, opts) { 27 | if (!await this.check(data, opts)) 28 | return false; 29 | await this.checkExternalConverterPresent(); 30 | 31 | const {inputFiles, callback, abort} = opts; 32 | 33 | const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`; 34 | const epubFile = `${outFile}.epub`; 35 | const fb2File = `${outFile}.fb2`; 36 | 37 | await fs.copy(inputFiles.sourceFile, epubFile); 38 | 39 | let perc = 0; 40 | await this.execConverter(this.calibrePath, [epubFile, fb2File, '-vv'], () => { 41 | perc = (perc < 100 ? perc + 1 : 50); 42 | callback(perc); 43 | }, abort); 44 | 45 | return await fs.readFile(fb2File); 46 | } 47 | } 48 | 49 | module.exports = ConvertEpub; 50 | -------------------------------------------------------------------------------- /server/core/WorkerState.js: -------------------------------------------------------------------------------- 1 | const utils = require('./utils'); 2 | 3 | const cleanInterval = 3600; //sec 4 | const cleanAfterLastModified = cleanInterval - 60; //sec 5 | 6 | let instance = null; 7 | 8 | //singleton 9 | class WorkerState { 10 | constructor() { 11 | if (!instance) { 12 | this.states = {}; 13 | this.cleanStates(); 14 | instance = this; 15 | } 16 | 17 | return instance; 18 | } 19 | 20 | generateWorkerId() { 21 | return utils.randomHexString(20); 22 | } 23 | 24 | getControl(workerId) { 25 | return { 26 | set: state => this.setState(workerId, state), 27 | finish: state => this.finishState(workerId, state), 28 | get: () => this.getState(workerId), 29 | }; 30 | } 31 | 32 | setState(workerId, state) { 33 | this.states[workerId] = Object.assign({}, this.states[workerId], state, { 34 | workerId, 35 | lastModified: Date.now() 36 | }); 37 | } 38 | 39 | finishState(workerId, state) { 40 | this.states[workerId] = Object.assign({}, this.states[workerId], state, { 41 | workerId, 42 | state: 'finish', 43 | lastModified: Date.now() 44 | }); 45 | } 46 | 47 | getState(workerId) { 48 | return this.states[workerId]; 49 | } 50 | 51 | cleanStates() { 52 | const now = Date.now(); 53 | for (let workerID in this.states) { 54 | if ((now - this.states[workerID].lastModified) >= cleanAfterLastModified*1000) { 55 | delete this.states[workerID]; 56 | } 57 | } 58 | setTimeout(this.cleanStates.bind(this), cleanInterval*1000); 59 | } 60 | } 61 | 62 | module.exports = WorkerState; -------------------------------------------------------------------------------- /server/core/Reader/BookConverter/ConvertFb3.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | 3 | const ConvertHtml = require('./ConvertHtml'); 4 | 5 | class ConvertFb3 extends ConvertHtml { 6 | async check(data, opts) { 7 | const {inputFiles} = opts; 8 | if (this.config.useExternalBookConverter && 9 | inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'zip') { 10 | //ищем файл '[Content_Types].xml' 11 | for (const file of inputFiles.files) { 12 | if (file.path == '[Content_Types].xml') { 13 | const contentTypes = await fs.readFile(`${inputFiles.filesDir}/${file.path}`, 'utf8'); 14 | return contentTypes.indexOf('/fb3/body.xml') >= 0; 15 | } 16 | } 17 | } 18 | 19 | return false; 20 | } 21 | 22 | getTitle(text) { 23 | let title = ''; 24 | const m = text.match(/([\s\S]*?)<\/title>/); 25 | if (m) 26 | title = m[1]; 27 | 28 | return title.trim(); 29 | } 30 | 31 | async run(data, opts) { 32 | if (!(await this.check(data, opts))) 33 | return false; 34 | 35 | const {inputFiles} = opts; 36 | 37 | let text = await fs.readFile(`${inputFiles.filesDir}/fb3/body.xml`, 'utf8'); 38 | 39 | const title = this.getTitle(text) 40 | .replace(/<\/?p>/g, '') 41 | ; 42 | text = `<fb2-title>${title}</fb2-title>` + text 43 | .replace(/<title>/g, '<br><b>') 44 | .replace(/<\/title>/g, '</b><br>') 45 | .replace(/<subtitle>/g, '<br><br><fb2-subtitle>') 46 | .replace(/<\/subtitle>/g, '</fb2-subtitle>') 47 | ; 48 | return await super.run(Buffer.from(text), {skipHtmlCheck: true}); 49 | } 50 | } 51 | 52 | module.exports = ConvertFb3; 53 | -------------------------------------------------------------------------------- /server/core/Reader/BookConverter/ConvertDocX.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | 4 | const ConvertBase = require('./ConvertBase'); 5 | 6 | class ConvertDocX extends ConvertBase { 7 | async check(data, opts) { 8 | const {inputFiles} = opts; 9 | if (this.config.useExternalBookConverter && 10 | inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'zip') { 11 | //ищем файл '[Content_Types].xml' 12 | for (const file of inputFiles.files) { 13 | if (file.path == '[Content_Types].xml') { 14 | const contentTypes = await fs.readFile(`${inputFiles.filesDir}/${file.path}`, 'utf8'); 15 | return contentTypes.indexOf('/word/document.xml') >= 0; 16 | } 17 | } 18 | } 19 | 20 | return false; 21 | } 22 | 23 | async convert(docxFile, fb2File, callback, abort) { 24 | let perc = 0; 25 | await this.execConverter(this.calibrePath, [docxFile, fb2File, '-vv'], () => { 26 | perc = (perc < 100 ? perc + 1 : 50); 27 | callback(perc); 28 | }, abort); 29 | 30 | return await fs.readFile(fb2File); 31 | } 32 | 33 | async run(data, opts) { 34 | if (!(await this.check(data, opts))) 35 | return false; 36 | await this.checkExternalConverterPresent(); 37 | 38 | const {inputFiles, callback, abort} = opts; 39 | 40 | const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`; 41 | const docxFile = `${outFile}.docx`; 42 | const fb2File = `${outFile}.fb2`; 43 | 44 | await fs.copy(inputFiles.sourceFile, docxFile); 45 | 46 | return await this.convert(docxFile, fb2File, callback, abort); 47 | } 48 | } 49 | 50 | module.exports = ConvertDocX; 51 | -------------------------------------------------------------------------------- /server/core/AppLogger.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const Logger = require('./Logger'); 3 | 4 | let instance = null; 5 | 6 | //singleton 7 | class AppLogger { 8 | constructor() { 9 | if (!instance) { 10 | this.inited = false; 11 | this.logFileName = ''; 12 | this.errLogFileName = ''; 13 | this.fatalLogFileName = ''; 14 | 15 | instance = this; 16 | } 17 | 18 | return instance; 19 | } 20 | 21 | async init(config) { 22 | if (this.inited) 23 | throw new Error('already inited'); 24 | 25 | let loggerParams = null; 26 | 27 | if (config.loggingEnabled) { 28 | await fs.ensureDir(config.logDir); 29 | 30 | this.logFileName = `${config.logDir}/${config.name}.log`; 31 | this.errLogFileName = `${config.logDir}/${config.name}.err.log`; 32 | this.fatalLogFileName = `${config.logDir}/${config.name}.fatal.log`; 33 | 34 | loggerParams = [ 35 | {log: 'ConsoleLog'}, 36 | {log: 'FileLog', fileName: this.logFileName}, 37 | {log: 'FileLog', fileName: this.errLogFileName, exclude: [LM_OK, LM_INFO, LM_TOTAL]}, 38 | {log: 'FileLog', fileName: this.fatalLogFileName, exclude: [LM_OK, LM_INFO, LM_WARN, LM_ERR, LM_TOTAL]},//LM_FATAL only 39 | ]; 40 | } else { 41 | loggerParams = [ 42 | {log: 'ConsoleLog'}, 43 | ]; 44 | } 45 | 46 | this._logger = new Logger(loggerParams); 47 | 48 | this.inited = true; 49 | return this.logger; 50 | } 51 | 52 | get logger() { 53 | if (!this.inited) 54 | throw new Error('not inited'); 55 | return this._logger; 56 | } 57 | 58 | get log() { 59 | const l = this.logger; 60 | return l.log.bind(l); 61 | } 62 | } 63 | 64 | module.exports = AppLogger; 65 | -------------------------------------------------------------------------------- /client/components/share/Dialog.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <q-dialog v-model="active" no-route-dismiss @show="onShow" @hide="onHide"> 3 | <div class="column bg-dialog no-wrap"> 4 | <div class="header row"> 5 | <div class="caption col row items-center q-ml-md"> 6 | <slot name="header"></slot> 7 | </div> 8 | <div class="close-icon column justify-center items-center"> 9 | <q-btn v-close-popup flat round dense> 10 | <q-icon name="la la-times" size="18px"></q-icon> 11 | </q-btn> 12 | </div> 13 | </div> 14 | 15 | <div class="col q-mx-md"> 16 | <slot></slot> 17 | </div> 18 | 19 | <div class="row justify-end q-pa-md"> 20 | <slot name="footer"></slot> 21 | </div> 22 | </div> 23 | </q-dialog> 24 | </template> 25 | 26 | <script> 27 | //----------------------------------------------------------------------------- 28 | import vueComponent from '../vueComponent.js'; 29 | import * as utils from '../../share/utils'; 30 | 31 | class Dialog { 32 | _props = { 33 | modelValue: Boolean, 34 | }; 35 | 36 | shown = false; 37 | 38 | get active() { 39 | return this.modelValue; 40 | } 41 | 42 | set active(value) { 43 | this.$emit('update:modelValue', value); 44 | } 45 | 46 | onShow() { 47 | this.shown = true; 48 | } 49 | 50 | onHide() { 51 | this.shown = false; 52 | } 53 | 54 | async waitShown() { 55 | let i = 100; 56 | while (!this.shown && i > 0) { 57 | await utils.sleep(10); 58 | i--; 59 | } 60 | } 61 | } 62 | 63 | export default vueComponent(Dialog); 64 | //----------------------------------------------------------------------------- 65 | </script> 66 | 67 | <style scoped> 68 | .header { 69 | height: 50px; 70 | } 71 | 72 | .caption { 73 | font-size: 110%; 74 | overflow: hidden; 75 | } 76 | 77 | .close-icon { 78 | width: 50px; 79 | } 80 | </style> -------------------------------------------------------------------------------- /client/components/Reader/TextPage/TextPage.css: -------------------------------------------------------------------------------- 1 | @keyframes page1-animation-thaw { 2 | 0% { opacity: 0; } 3 | 100% { opacity: 1; } 4 | } 5 | 6 | @keyframes page2-animation-thaw { 7 | 0% { opacity: 1; } 8 | 100% { opacity: 0; } 9 | } 10 | 11 | .paper1 { 12 | background: url("images/paper1.jpg") center; 13 | background-size: 100% 100%; 14 | } 15 | 16 | .paper2 { 17 | background: url("images/paper2.jpg") center; 18 | background-size: 100% 100%; 19 | } 20 | 21 | .paper3 { 22 | background: url("images/paper3.jpg") center; 23 | background-size: 100% 100%; 24 | } 25 | 26 | .paper4 { 27 | background: url("images/paper4.jpg") center; 28 | background-size: 100% 100%; 29 | } 30 | 31 | .paper5 { 32 | background: url("images/paper5.jpg") center; 33 | background-size: 100% 100%; 34 | } 35 | 36 | .paper6 { 37 | background: url("images/paper6.jpg") center; 38 | background-size: 100% 100%; 39 | } 40 | 41 | .paper7 { 42 | background: url("images/paper7.jpg") center; 43 | background-size: 100% 100%; 44 | } 45 | 46 | .paper8 { 47 | background: url("images/paper8.jpg") center; 48 | background-size: 100% 100%; 49 | } 50 | 51 | .paper9 { 52 | background: url("images/paper9.jpg"); 53 | } 54 | 55 | .paper10 { 56 | background: url("images/paper10.png") center; 57 | background-size: 100% 100%; 58 | } 59 | 60 | .paper11 { 61 | background: url("images/paper11.png") center; 62 | background-size: 100% 100%; 63 | } 64 | 65 | .paper12 { 66 | background: url("images/paper12.png") center; 67 | background-size: 100% 100%; 68 | } 69 | 70 | .paper13 { 71 | background: url("images/paper13.png") center; 72 | background-size: 100% 100%; 73 | } 74 | 75 | .paper14 { 76 | background: url("images/paper14.png") center; 77 | background-size: 100% 100%; 78 | } 79 | 80 | .paper15 { 81 | background: url("images/paper15.png") center; 82 | background-size: 100% 100%; 83 | } 84 | 85 | .paper16 { 86 | background: url("images/paper16.png") center; 87 | background-size: 100% 100%; 88 | } 89 | 90 | .paper17 { 91 | background: url("images/paper17.png") center; 92 | background-size: 100% 100%; 93 | } 94 | -------------------------------------------------------------------------------- /client/components/share/Notify.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="hidden"></div> 3 | </template> 4 | 5 | <script> 6 | //----------------------------------------------------------------------------- 7 | import vueComponent from '../vueComponent.js'; 8 | 9 | class Notify { 10 | notify(opts) { 11 | let { 12 | caption = null, 13 | captionColor = 'black', 14 | color = 'positive', 15 | icon = '', 16 | iconColor = 'white', 17 | message = '', 18 | messageColor = 'black', 19 | position = 'top-right', 20 | } = opts; 21 | 22 | caption = (caption ? `<div style="font-size: 120%; color: ${captionColor}"><b>${caption}</b></div><br>` : ''); 23 | return this.$q.notify({ 24 | position, 25 | color, 26 | textColor: iconColor, 27 | icon, 28 | actions: [{icon: 'la la-times notify-button-icon', color: 'black'}], 29 | html: true, 30 | 31 | message: 32 | `<div style="max-width: 350px"> 33 | ${caption} 34 | <div style="color: ${messageColor}; overflow-wrap: break-word; word-wrap: break-word;">${message}</div> 35 | </div>` 36 | }); 37 | } 38 | 39 | success(message, caption, options) { 40 | this.notify(Object.assign({color: 'positive', icon: 'la la-check-circle', message, caption}, options)); 41 | } 42 | 43 | warning(message, caption, options) { 44 | this.notify(Object.assign({color: 'warning', icon: 'la la-exclamation-circle', message, caption}, options)); 45 | } 46 | 47 | error(message, caption, options) { 48 | this.notify(Object.assign({color: 'negative', icon: 'la la-exclamation-circle', messageColor: 'yellow', captionColor: 'white', message, caption}, options)); 49 | } 50 | 51 | info(message, caption, options) { 52 | this.notify(Object.assign({color: 'info', icon: 'la la-bell', message, caption}, options)); 53 | } 54 | } 55 | 56 | export default vueComponent(Notify); 57 | //----------------------------------------------------------------------------- 58 | </script> 59 | -------------------------------------------------------------------------------- /client/components/Reader/HelpPage/VersionHistoryPage/VersionHistoryPage.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div id="versionHistoryPage" class="page"> 3 | <span class="text-h6 text-bold">История версий:</span> 4 | <br><br> 5 | 6 | <span v-for="(item, index) in versionHeader" :key="index" class="clickable" @click="showRelease(item)"> 7 | <p> 8 | {{ item }} 9 | </p> 10 | </span> 11 | 12 | <br> 13 | 14 | <div v-for="item in versionContent" :id="item.key" :key="item.key"> 15 | <span v-html="item.content"></span> 16 | <br> 17 | </div> 18 | </div> 19 | </template> 20 | 21 | <script> 22 | //----------------------------------------------------------------------------- 23 | import vueComponent from '../../../vueComponent.js'; 24 | 25 | import {versionHistory} from '../../versionHistory'; 26 | 27 | class VersionHistoryPage { 28 | versionHeader = []; 29 | versionContent = []; 30 | 31 | created() { 32 | } 33 | 34 | mounted() { 35 | let vh = []; 36 | for (const v of versionHistory) { 37 | vh.push(`${v.version} (${v.releaseDate})`); 38 | } 39 | this.versionHeader = vh; 40 | 41 | let vc = []; 42 | for (const v of versionHistory) { 43 | let header = `${v.version} (${v.releaseDate})`; 44 | vc.push({key: header, content: 'Версия ' + header + v.content}); 45 | } 46 | this.versionContent = vc; 47 | } 48 | 49 | showRelease(id) { 50 | let el = document.getElementById(id); 51 | if (el) { 52 | document.getElementById('versionHistoryPage').scrollTop = el.offsetTop; 53 | } 54 | } 55 | } 56 | 57 | export default vueComponent(VersionHistoryPage); 58 | //----------------------------------------------------------------------------- 59 | </script> 60 | 61 | <style scoped> 62 | .page { 63 | padding: 15px; 64 | overflow-y: auto; 65 | font-size: 120%; 66 | line-height: 130%; 67 | position: relative; 68 | } 69 | 70 | p { 71 | line-height: 15px; 72 | } 73 | 74 | .clickable { 75 | color: var(--text-anchor-color); 76 | text-decoration: underline; 77 | cursor: pointer; 78 | } 79 | </style> 80 | -------------------------------------------------------------------------------- /client/components/Reader/SettingsPage/KeysTab/KeysTab.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="fit column"> 3 | <div class="bg-menu-1 row"> 4 | <q-tabs 5 | v-model="selectedTab" 6 | active-color="app" 7 | active-bg-color="app" 8 | indicator-color="bg-app" 9 | dense 10 | no-caps 11 | class="bg-menu-2 text-menu" 12 | > 13 | <q-tab name="mouse" label="Мышь/тачскрин" /> 14 | <q-tab name="keyboard" label="Клавиатура" /> 15 | </q-tabs> 16 | </div> 17 | 18 | <div class="q-mb-sm" /> 19 | 20 | <div class="col sets-tab-panel"> 21 | <div v-if="selectedTab == 'mouse'"> 22 | <div class="sets-item row"> 23 | <div class="sets-label label"></div> 24 | <div class="col row"> 25 | <q-checkbox v-model="form.clickControl" size="xs" label="Включить управление кликом" /> 26 | </div> 27 | </div> 28 | </div> 29 | 30 | <div v-if="selectedTab == 'keyboard'"> 31 | <div class="sets-item row"> 32 | <UserHotKeys v-model="form.userHotKeys" /> 33 | </div> 34 | </div> 35 | </div> 36 | </div> 37 | </template> 38 | 39 | <script> 40 | //----------------------------------------------------------------------------- 41 | import vueComponent from '../../../vueComponent.js'; 42 | 43 | import UserHotKeys from './UserHotKeys/UserHotKeys.vue'; 44 | 45 | const componentOptions = { 46 | components: { 47 | UserHotKeys, 48 | }, 49 | }; 50 | class KeysTab { 51 | _options = componentOptions; 52 | _props = { 53 | form: Object, 54 | }; 55 | 56 | selectedTab = 'mouse'; 57 | 58 | created() { 59 | } 60 | 61 | mounted() { 62 | } 63 | 64 | get mode() { 65 | return this.$store.state.config.mode; 66 | } 67 | } 68 | 69 | export default vueComponent(KeysTab); 70 | //----------------------------------------------------------------------------- 71 | </script> 72 | 73 | <style scoped> 74 | .label { 75 | width: 110px; 76 | } 77 | 78 | </style> 79 | -------------------------------------------------------------------------------- /server/core/Reader/BookConverter/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const FileDetector = require('../../FileDetector'); 3 | 4 | //порядок важен 5 | const convertClassFactory = [ 6 | require('./ConvertJpegPng'), 7 | require('./ConvertEpub'), 8 | require('./ConvertDjvu'), 9 | require('./ConvertPdf'), 10 | require('./ConvertPdfImages'), 11 | require('./ConvertRtf'), 12 | require('./ConvertDocX'), 13 | require('./ConvertFb3'), 14 | require('./ConvertDoc'), 15 | require('./ConvertMobi'), 16 | require('./ConvertFb2'), 17 | require('./ConvertSamlib'), 18 | require('./ConvertSites'), 19 | require('./ConvertHtml'), 20 | ]; 21 | 22 | class BookConverter { 23 | constructor(config) { 24 | this.detector = new FileDetector(); 25 | 26 | this.convertFactory = []; 27 | for (const convertClass of convertClassFactory) { 28 | this.convertFactory.push(new convertClass(config)); 29 | } 30 | } 31 | 32 | async convertToFb2(inputFiles, outputFile, opts, callback, abort) { 33 | if (abort && abort()) 34 | throw new Error('abort'); 35 | 36 | const selectedFileType = await this.detector.detectFile(inputFiles.selectedFile); 37 | const data = await fs.readFile(inputFiles.selectedFile); 38 | 39 | const convertOpts = Object.assign({}, opts, {inputFiles, callback, abort, dataType: selectedFileType}); 40 | let result = false; 41 | for (const convert of this.convertFactory) { 42 | result = await convert.run(data, convertOpts); 43 | if (result) { 44 | await fs.writeFile(outputFile, result); 45 | break; 46 | } 47 | } 48 | 49 | if (!result && inputFiles.nesting) { 50 | result = await this.convertToFb2(inputFiles.nesting, outputFile, opts, callback, abort); 51 | } 52 | 53 | if (!result) { 54 | if (selectedFileType) 55 | throw new Error(`Этот формат файла не поддерживается: ${selectedFileType.mime}`); 56 | else { 57 | throw new Error(`Не удалось определить формат файла: ${opts.url}`); 58 | } 59 | } 60 | 61 | callback(100); 62 | return result; 63 | } 64 | } 65 | 66 | module.exports = BookConverter; -------------------------------------------------------------------------------- /client/components/Reader/ClickMapPage/ClickMapPage.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div ref="page" class="map-page"> 3 | <div class="content" v-html="mapHtml"></div> 4 | </div> 5 | </template> 6 | 7 | <script> 8 | //----------------------------------------------------------------------------- 9 | import vueComponent from '../../vueComponent.js'; 10 | 11 | import {sleep} from '../../../share/utils'; 12 | import {clickMap, clickMapText} from '../share/clickMap'; 13 | 14 | class ClickMapPage { 15 | fontSize = '200%'; 16 | 17 | created() { 18 | } 19 | 20 | get mapHtml() { 21 | let result = '<div style="flex: 1; display: flex;">'; 22 | 23 | let px = 0; 24 | for (const x in clickMap) { 25 | let div = `<div style="display: flex; flex-direction: column; width: ${x - px}%;">`; 26 | 27 | let py = 0; 28 | for (const y in clickMap[x]) { 29 | const text = clickMapText[clickMap[x][y]].split(' '); 30 | let divText = ''; 31 | for (const t of text) 32 | divText += `<span>${t}</span>`; 33 | div += `<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; ` + 34 | `height: ${y - py}%; border: 1px solid white; font-size: ${this.fontSize}; line-height: 100%;">${divText}</div>`; 35 | py = y; 36 | } 37 | 38 | div += '</div>'; 39 | px = x; 40 | result += div; 41 | } 42 | 43 | result += '</div>'; 44 | return result; 45 | } 46 | 47 | async slowDisappear() { 48 | const page = this.$refs.page; 49 | page.style.animation = 'click-map-disappear 5s ease-in 1'; 50 | await sleep(5000); 51 | } 52 | } 53 | 54 | export default vueComponent(ClickMapPage); 55 | //----------------------------------------------------------------------------- 56 | </script> 57 | 58 | <style scoped> 59 | .map-page { 60 | position: absolute; 61 | width: 100%; 62 | height: 100%; 63 | z-index: 19; 64 | background-color: rgba(0, 0, 0, 0.9); 65 | color: white; 66 | display: flex; 67 | } 68 | 69 | .content { 70 | flex: 1; 71 | display: flex; 72 | } 73 | </style> 74 | <style> 75 | @keyframes click-map-disappear { 76 | 0% { opacity: 0.9; } 77 | 100% { opacity: 0; } 78 | } 79 | </style> 80 | -------------------------------------------------------------------------------- /client/components/Reader/SettingsPage/ViewTab/ViewTab.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="fit column"> 3 | <q-tabs 4 | v-model="selectedTab" 5 | active-color="app" 6 | active-bg-color="app" 7 | indicator-color="bg-app" 8 | dense 9 | no-caps 10 | class="no-mp bg-menu-2 text-menu" 11 | > 12 | <q-tab name="mode" label="Режим" /> 13 | <q-tab name="color" label="Цвет" /> 14 | <q-tab name="font" label="Шрифт" /> 15 | <q-tab name="text" label="Текст" /> 16 | <q-tab name="status" label="Строка статуса" /> 17 | </q-tabs> 18 | 19 | <div class="q-mb-sm" /> 20 | 21 | <div class="col sets-tab-panel"> 22 | <Mode v-if="selectedTab == 'mode'" :form="form" @tab-event="tabEvent" /> 23 | <Color v-if="selectedTab == 'color'" :form="form" /> 24 | <Font v-if="selectedTab == 'font'" :form="form" /> 25 | <Text v-if="selectedTab == 'text'" :form="form" /> 26 | <Status v-if="selectedTab == 'status'" :form="form" /> 27 | </div> 28 | </div> 29 | </template> 30 | 31 | <script> 32 | //----------------------------------------------------------------------------- 33 | import vueComponent from '../../../vueComponent.js'; 34 | 35 | import Mode from './Mode/Mode.vue'; 36 | import Color from './Color/Color.vue'; 37 | import Font from './Font/Font.vue'; 38 | import Text from './Text/Text.vue'; 39 | import Status from './Status/Status.vue'; 40 | 41 | const componentOptions = { 42 | components: { 43 | Mode, 44 | Color, 45 | Font, 46 | Text, 47 | Status, 48 | }, 49 | }; 50 | class ViewTab { 51 | _options = componentOptions; 52 | _props = { 53 | form: Object, 54 | }; 55 | 56 | selectedTab = 'mode'; 57 | 58 | created() { 59 | } 60 | 61 | mounted() { 62 | } 63 | 64 | tabEvent(event) { 65 | if (!event || !event.action) 66 | return; 67 | 68 | switch (event.action) { 69 | case 'night-mode': this.$emit('tab-event', {action: 'night-mode'}); break; 70 | } 71 | } 72 | } 73 | 74 | export default vueComponent(ViewTab); 75 | //----------------------------------------------------------------------------- 76 | </script> 77 | 78 | <style scoped> 79 | .label { 80 | width: 75px; 81 | } 82 | 83 | </style> 84 | -------------------------------------------------------------------------------- /client/components/vueComponent.js: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue'; 2 | import _ from 'lodash'; 3 | 4 | export default function(componentClass) { 5 | const comp = {}; 6 | const obj = new componentClass(); 7 | 8 | //data, options, props 9 | const data = {}; 10 | for (const prop of Object.getOwnPropertyNames(obj)) { 11 | if (['_options', '_props'].includes(prop)) {//meta props 12 | if (prop === '_options') { 13 | const options = obj[prop]; 14 | for (const optName of ['components', 'watch', 'emits']) { 15 | if (options[optName]) { 16 | comp[optName] = options[optName]; 17 | } 18 | } 19 | } else if (prop === '_props') { 20 | comp.props = obj[prop]; 21 | } 22 | } else {//usual prop 23 | data[prop] = obj[prop]; 24 | } 25 | } 26 | comp.data = () => _.cloneDeep(data); 27 | 28 | //methods 29 | const methods = {}; 30 | const computed = {}; 31 | 32 | let classProto = Object.getPrototypeOf(obj); 33 | while (classProto) { 34 | const classMethods = Object.getOwnPropertyNames(classProto); 35 | for (const method of classMethods) { 36 | const desc = Object.getOwnPropertyDescriptor(classProto, method); 37 | if (desc.get) {//has getter, computed 38 | if (!computed[method]) { 39 | computed[method] = {get: desc.get}; 40 | if (desc.set) 41 | computed[method].set = desc.set; 42 | } 43 | } else if ( ['beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated', 44 | 'deactivated', 'beforeUnmount', 'unmounted', 'errorCaptured', 'renderTracked', 'renderTriggered', 45 | 'setup'].includes(method) ) {//life cycle hooks 46 | if (!comp[method]) 47 | comp[method] = obj[method]; 48 | } else if (method !== 'constructor') {//usual 49 | if (!methods[method]) 50 | methods[method] = obj[method]; 51 | } 52 | } 53 | 54 | classProto = Object.getPrototypeOf(classProto); 55 | } 56 | comp.methods = methods; 57 | comp.computed = computed; 58 | 59 | //console.log(comp); 60 | return defineComponent(comp); 61 | } 62 | -------------------------------------------------------------------------------- /client/components/Reader/SettingsPage/ToolBarTab/ToolBarTab.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="fit sets-tab-panel"> 3 | <div class="sets-part-header"> 4 | Отображение 5 | </div> 6 | 7 | <div class="item row no-wrap"> 8 | <div class="sets-label label"></div> 9 | <q-checkbox v-model="form.toolBarMultiLine" size="xs" label="Многострочная панель"> 10 | <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%"> 11 | Размещать кнопки на панели в несколько рядов, если они не помещаются в одну строку 12 | </q-tooltip> 13 | </q-checkbox> 14 | </div> 15 | 16 | <div class="item row no-wrap"> 17 | <div class="sets-label label"></div> 18 | <q-checkbox v-model="form.toolBarHideOnScroll" size="xs" label="Скрывать/показывать панель при прокрутке"> 19 | <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%"> 20 | Скрывать/показывть панель при прокрутке текста вперед/назад 21 | </q-tooltip> 22 | </q-checkbox> 23 | </div> 24 | 25 | <div class="sets-part-header"> 26 | Показывать кнопки 27 | </div> 28 | 29 | <div v-for="item in rstore.toolButtons" :key="item.name"> 30 | <div class="sets-item row no-wrap"> 31 | <div class="sets-label label"></div> 32 | <q-checkbox v-model="form.showToolButton[item.name]" size="xs" :label="rstore.readerActions[item.name]" /> 33 | </div> 34 | </div> 35 | </div> 36 | </template> 37 | 38 | <script> 39 | //----------------------------------------------------------------------------- 40 | import vueComponent from '../../../vueComponent.js'; 41 | 42 | import rstore from '../../../../store/modules/reader'; 43 | 44 | const componentOptions = { 45 | watch: { 46 | }, 47 | }; 48 | class ToolBarTab { 49 | _options = componentOptions; 50 | _props = { 51 | form: Object, 52 | }; 53 | 54 | rstore = rstore; 55 | 56 | created() { 57 | } 58 | 59 | mounted() { 60 | } 61 | 62 | get mode() { 63 | return this.$store.state.config.mode; 64 | } 65 | } 66 | 67 | export default vueComponent(ToolBarTab); 68 | //----------------------------------------------------------------------------- 69 | </script> 70 | 71 | <style scoped> 72 | .label { 73 | width: 75px; 74 | } 75 | 76 | </style> 77 | -------------------------------------------------------------------------------- /server/config/base.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pckg = require('../../package.json'); 3 | 4 | const execDir = path.resolve(__dirname, '..'); 5 | 6 | module.exports = { 7 | branch: 'unknown', 8 | version: pckg.version, 9 | name: pckg.name, 10 | 11 | execDir, 12 | 13 | loggingEnabled: true, 14 | 15 | maxUploadFileSize: 50*1024*1024,//50Мб 16 | maxTempPublicDirSize: 512*1024*1024,//512Мб + 20% квота если проблема с remoteWebDavStorage 17 | maxUploadPublicDirSize: 200*1024*1024,//100Мб 18 | 19 | useExternalBookConverter: false, 20 | acceptFileExt: '.fb2, .fb3, .html, .txt, .zip, .bz2, .gz, .rar, .epub, .mobi, .rtf, .doc, .docx, .pdf, .djvu, .jpg, .jpeg, .png', 21 | restricted: {}, 22 | webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'bucEnabled', 'branch', 'networkLibraryLink', 'restricted'], 23 | 24 | jembaDb: [ 25 | { 26 | serverMode: ['reader', 'omnireader', 'liberama'], 27 | dbName: 'app', 28 | thread: true, 29 | openAll: true, 30 | }, 31 | { 32 | serverMode: ['reader', 'omnireader', 'liberama'], 33 | dbName: 'reader-storage', 34 | thread: true, 35 | openAll: true, 36 | }, 37 | { 38 | serverMode: 'book_update_checker', 39 | dbName: 'book-update-server', 40 | thread: true, 41 | openAll: true, 42 | }, 43 | ], 44 | 45 | servers: [ 46 | { 47 | serverName: '1', 48 | mode: 'reader', //'reader', 'omnireader', 'liberama', 'book_update_checker' 49 | ip: '0.0.0.0', 50 | port: '33080', 51 | }, 52 | /*{ 53 | serverName: '2', 54 | mode: 'book_update_checker', 55 | isHttps: true, 56 | keysFile: 'server', 57 | ip: '0.0.0.0', 58 | port: '33443', 59 | accessToken: '', 60 | shciForHost: { 61 | 'samlib.ru': 300000 62 | }, 63 | }*/ 64 | ], 65 | 66 | remoteStorage: false, 67 | /* 68 | remoteStorage: { 69 | url: 'wss://127.0.0.1:11900', 70 | accessToken: '', 71 | }, 72 | */ 73 | bucEnabled: false, 74 | bucServer: false, 75 | /* 76 | bucServer: { 77 | url: 'wss://127.0.0.1:33443', 78 | accessToken: '', 79 | } 80 | */ 81 | networkLibraryLink: '', 82 | }; 83 | 84 | -------------------------------------------------------------------------------- /client/components/Reader/HelpPage/MouseHelpPage/MouseHelpPage.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="page"> 3 | <span class="text-h6 text-bold">Управление с помощью мыши/тачскрина:</span> 4 | <ul> 5 | <li><b>ЛКМ/ТАЧ</b> по экрану в одну из областей - активация действия:</li> 6 | <div class="click-map-page"> 7 | <ClickMapPage ref="clickMapPage"></ClickMapPage> 8 | </div> 9 | 10 | <li><b>ПКМ</b> - показать/скрыть панель управления</li> 11 | <li><b>СКМ</b> - вкл./выкл. плавный скроллинг текста</li> 12 | <br> 13 | <li>Жесты для тачскрина:</li> 14 | <ul> 15 | <li style="list-style-type: square"> 16 | от центра вверх/двойной тап по центру: на весь экран 17 | </li> 18 | <li style="list-style-type: square"> 19 | от центра вниз: плавный скроллинг 20 | </li> 21 | <li style="list-style-type: square"> 22 | от центра вправо: увеличить скорость скроллинга 23 | </li> 24 | <li style="list-style-type: square"> 25 | от центра влево: уменьшить скорость скроллинга 26 | </li> 27 | </ul> 28 | </ul> 29 | * Для управления с помощью мыши/тачскрина необходимо установить галочку "Включить управление кликом" в настройках 30 | </div> 31 | </template> 32 | 33 | <script> 34 | //----------------------------------------------------------------------------- 35 | import vueComponent from '../../../vueComponent.js'; 36 | 37 | import ClickMapPage from '../../ClickMapPage/ClickMapPage.vue'; 38 | 39 | const componentOptions = { 40 | components: { 41 | ClickMapPage, 42 | }, 43 | }; 44 | class MouseHelpPage { 45 | _options = componentOptions; 46 | 47 | created() { 48 | } 49 | 50 | mounted() { 51 | this.$refs.clickMapPage.$el.style.fontSize = '50%'; 52 | this.$refs.clickMapPage.$el.style.backgroundColor = '#478355'; 53 | } 54 | } 55 | 56 | export default vueComponent(MouseHelpPage); 57 | //----------------------------------------------------------------------------- 58 | </script> 59 | 60 | <style scoped> 61 | .page { 62 | padding: 15px; 63 | overflow-y: auto; 64 | font-size: 120%; 65 | line-height: 130%; 66 | } 67 | 68 | .click-map-page { 69 | position: relative; 70 | width: 400px; 71 | height: 400px; 72 | margin: 10px 0 10px 0; 73 | } 74 | </style> 75 | -------------------------------------------------------------------------------- /client/components/Reader/SettingsPage/ViewTab/defPalette.js: -------------------------------------------------------------------------------- 1 | const defPalette = [ 2 | 'rgb(255,204,204)', 'rgb(255,230,204)', 'rgb(255,255,204)', 'rgb(204,255,204)', 'rgb(204,255,230)', 3 | 'rgb(204,255,255)', 'rgb(204,230,255)', 'rgb(204,204,255)', 'rgb(230,204,255)', 'rgb(255,204,255)', 4 | 'rgb(255,153,153)', 'rgb(255,204,153)', 'rgb(255,255,153)', 'rgb(153,255,153)', 'rgb(153,255,204)', 5 | 'rgb(153,255,255)', 'rgb(153,204,255)', 'rgb(153,153,255)', 'rgb(204,153,255)', 'rgb(255,153,255)', 6 | 'rgb(255,102,102)', 'rgb(255,179,102)', 'rgb(255,255,102)', 'rgb(102,255,102)', 'rgb(102,255,179)', 7 | 'rgb(102,255,255)', 'rgb(102,179,255)', 'rgb(102,102,255)', 'rgb(179,102,255)', 'rgb(255,102,255)', 8 | 'rgb(255,51,51)', 'rgb(255,153,51)', 'rgb(255,255,51)', 'rgb(51,255,51)', 'rgb(51,255,153)', 'rgb(51,255,255)', 'rgb(51,153,255)', 'rgb(51,51,255)', 'rgb(153,51,255)', 'rgb(255,51,255)', 9 | 'rgb(255,0,0)', 'rgb(255,128,0)', 'rgb(255,255,0)', 'rgb(0,255,0)', 'rgb(0,255,128)', 'rgb(0,255,255)', 'rgb(0,128,255)', 'rgb(0,0,255)', 'rgb(128,0,255)', 'rgb(255,0,255)', 10 | 'rgb(245,0,0)', 'rgb(245,123,0)', 'rgb(245,245,0)', 'rgb(0,245,0)', 'rgb(0,245,123)', 'rgb(0,245,245)', 'rgb(0,123,245)', 'rgb(0,0,245)', 'rgb(123,0,245)', 'rgb(245,0,245)', 11 | 'rgb(214,0,0)', 'rgb(214,108,0)', 'rgb(214,214,0)', 'rgb(0,214,0)', 'rgb(0,214,108)', 'rgb(0,214,214)', 'rgb(0,108,214)', 'rgb(0,0,214)', 'rgb(108,0,214)', 'rgb(214,0,214)', 12 | 'rgb(163,0,0)', 'rgb(163,82,0)', 'rgb(163,163,0)', 'rgb(0,163,0)', 'rgb(0,163,82)', 'rgb(0,163,163)', 'rgb(0,82,163)', 'rgb(0,0,163)', 'rgb(82,0,163)', 'rgb(163,0,163)', 13 | 'rgb(92,0,0)', 'rgb(92,46,0)', 'rgb(92,92,0)', 'rgb(0,92,0)', 'rgb(0,92,46)', 'rgb(0,92,92)', 'rgb(0,46,92)', 'rgb(0,0,92)', 'rgb(46,0,92)', 'rgb(92,0,92)', 14 | 'rgb(255,255,255)', 'rgb(205,205,205)', 'rgb(178,178,178)', 'rgb(153,153,153)', 'rgb(127,127,127)', 'rgb(102,102,102)', 'rgb(76,76,76)', 'rgb(51,51,51)', 'rgb(25,25,25)', 'rgb(0,0,0)' 15 | ]; 16 | 17 | export default { 18 | predefinePalette: defPalette, 19 | 20 | predefineTextColors: defPalette.concat([ 21 | '#ffffff', 22 | '#000000', 23 | '#202020', 24 | '#323232', 25 | '#aaaaaa', 26 | '#00c0c0', 27 | '#ebe2c9', 28 | '#cfdc99', 29 | '#478355', 30 | '#909080', 31 | ]), 32 | 33 | predefineBackgroundColors: defPalette.concat([ 34 | '#ffffff', 35 | '#000000', 36 | '#202020', 37 | '#ebe2c9', 38 | '#cfdc99', 39 | '#478355', 40 | '#a6caf0', 41 | '#909080', 42 | '#808080', 43 | '#c8c8c8', 44 | ]), 45 | }; -------------------------------------------------------------------------------- /server/core/Reader/BookConverter/ConvertFb2.js: -------------------------------------------------------------------------------- 1 | const ConvertBase = require('./ConvertBase'); 2 | const iconv = require('iconv-lite'); 3 | const textUtils = require('./textUtils'); 4 | 5 | class ConvertFb2 extends ConvertBase { 6 | check(data, opts) { 7 | const {dataType} = opts; 8 | 9 | return ( 10 | ( (dataType && dataType.ext == 'xml') || this.isDataXml(data) ) && 11 | data.toString().indexOf('<FictionBook') >= 0 12 | ); 13 | } 14 | 15 | async run(data, opts) { 16 | let newData = data.slice(0, 1024); 17 | 18 | //Корректируем кодировку для проверки, 16-битные кодировки должны стать utf-8 19 | const encoding = textUtils.getEncoding(newData); 20 | if (encoding.indexOf('UTF-16') == 0) { 21 | newData = Buffer.from(iconv.decode(newData, encoding)); 22 | } 23 | 24 | //Проверяем 25 | if (!this.check(newData, opts)) 26 | return false; 27 | 28 | //Корректируем кодировку всего объема 29 | newData = data; 30 | if (encoding.indexOf('UTF-16') == 0) { 31 | newData = Buffer.from(iconv.decode(newData, encoding)); 32 | } 33 | 34 | //Корректируем пробелы, всякие файлы попадаются :( 35 | if (newData[0] == 32) { 36 | newData = Buffer.from(newData.toString().trim()); 37 | } 38 | 39 | //Окончательно корректируем кодировку 40 | return this.checkEncoding(newData); 41 | } 42 | 43 | checkEncoding(data) { 44 | let result = data; 45 | 46 | let q = '"'; 47 | let left = data.indexOf('<?xml version="1.0"'); 48 | if (left < 0) { 49 | left = data.indexOf('<?xml version=\'1.0\''); 50 | q = '\''; 51 | } 52 | 53 | if (left >= 0) { 54 | const right = data.indexOf('?>', left); 55 | if (right >= 0) { 56 | const head = data.slice(left, right + 2).toString(); 57 | const m = head.match(/encoding=['"](.*?)['"]/); 58 | if (m) { 59 | let encoding = m[1].toLowerCase(); 60 | if (encoding != 'utf-8') { 61 | //encoding может не соответсвовать реальной кодировке файла, поэтому: 62 | let calcEncoding = textUtils.getEncoding(data); 63 | if (calcEncoding.indexOf('ISO-8859') >= 0) { 64 | calcEncoding = encoding; 65 | } 66 | 67 | result = iconv.decode(data, calcEncoding); 68 | result = Buffer.from(result.toString().replace(m[0], `encoding=${q}utf-8${q}`)); 69 | } 70 | } 71 | } 72 | } 73 | 74 | return result; 75 | } 76 | } 77 | 78 | module.exports = ConvertFb2; 79 | -------------------------------------------------------------------------------- /client/components/Reader/SetPositionPage/SetPositionPage.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close"> 3 | <template #header> 4 | Установить позицию 5 | </template> 6 | 7 | <div class="col column justify-center"> 8 | <div id="set-position-slider" class="slider q-px-md column justify-center"> 9 | <q-slider 10 | v-model="sliderValue" 11 | thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0" 12 | 13 | :max="sliderMax" 14 | label 15 | :label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)" 16 | color="primary" 17 | /> 18 | </div> 19 | </div> 20 | </Window> 21 | </template> 22 | 23 | <script> 24 | //----------------------------------------------------------------------------- 25 | import vueComponent from '../../vueComponent.js'; 26 | 27 | import Window from '../../share/Window.vue'; 28 | 29 | const componentOptions = { 30 | components: { 31 | Window, 32 | }, 33 | watch: { 34 | sliderValue: function(newValue) { 35 | if (this.initialized) 36 | this.$emit('book-pos-changed', {bookPos: newValue}); 37 | }, 38 | }, 39 | }; 40 | class SetPositionPage { 41 | _options = componentOptions; 42 | 43 | sliderValue = null; 44 | sliderMax = null; 45 | 46 | created() { 47 | this.commit = this.$store.commit; 48 | this.reader = this.$store.state.reader; 49 | this.initialized = false; 50 | } 51 | 52 | init(sliderValue, sliderMax) { 53 | this.$refs.window.init(); 54 | 55 | this.sliderMax = sliderMax; 56 | this.sliderValue = sliderValue; 57 | this.initialized = true; 58 | } 59 | 60 | close() { 61 | this.$emit('set-position-toggle'); 62 | } 63 | 64 | keyHook(event) { 65 | if (event.type == 'keydown') { 66 | const action = this.$root.readerActionByKeyEvent(event); 67 | if (event.key == 'Escape' || action == 'setPosition') { 68 | this.close(); 69 | } 70 | } 71 | return true; 72 | } 73 | } 74 | 75 | export default vueComponent(SetPositionPage); 76 | //----------------------------------------------------------------------------- 77 | </script> 78 | 79 | <style scoped> 80 | .slider { 81 | margin: 0 20px 0 20px; 82 | height: 35px; 83 | background-color: var(--bg-input-color); 84 | border-radius: 15px; 85 | } 86 | </style> 87 | 88 | <style> 89 | #set-position-slider .q-slider__thumb path { 90 | fill: white !important; 91 | stroke: blue !important; 92 | stroke-width: 2 !important; 93 | } 94 | 95 | </style> -------------------------------------------------------------------------------- /docs/beta/beta.liberama: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; # managed by Certbot 3 | ssl_certificate /etc/letsencrypt/live/beta.liberama.top/fullchain.pem; # managed by Certbot 4 | ssl_certificate_key /etc/letsencrypt/live/beta.liberama.top/privkey.pem; # managed by Certbot 5 | include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 6 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot 7 | 8 | server_name beta.liberama.top; 9 | set $liberama http://127.0.0.1:34082; 10 | 11 | client_max_body_size 50m; 12 | proxy_read_timeout 1h; 13 | 14 | gzip on; 15 | gzip_min_length 1024; 16 | gzip_proxied expired no-cache no-store private auth; 17 | gzip_types *; 18 | 19 | location @liberama { 20 | proxy_pass $liberama; 21 | } 22 | 23 | location /api { 24 | proxy_pass $liberama; 25 | } 26 | 27 | location /ws { 28 | proxy_pass $liberama; 29 | proxy_http_version 1.1; 30 | proxy_set_header Upgrade $http_upgrade; 31 | proxy_set_header Connection "upgrade"; 32 | proxy_read_timeout 600s; 33 | } 34 | 35 | location / { 36 | root /home/beta.liberama/public; 37 | 38 | location /tmp { 39 | types { } default_type "application/xml; charset=utf-8"; 40 | add_header Content-Encoding gzip; 41 | try_files $uri @liberama; 42 | } 43 | 44 | location /upload { 45 | try_files $uri @liberama; 46 | } 47 | 48 | location ~* \.(?:manifest|appcache|html)$ { 49 | expires -1; 50 | } 51 | } 52 | } 53 | 54 | server { 55 | listen 80; 56 | server_name beta.liberama.top; 57 | 58 | return 301 https://$host$request_uri; 59 | } 60 | 61 | server { 62 | listen 80; 63 | server_name b.beta.liberama.top; 64 | set $liberama http://127.0.0.1:34082; 65 | 66 | client_max_body_size 50m; 67 | proxy_read_timeout 1h; 68 | 69 | gzip on; 70 | gzip_min_length 1024; 71 | gzip_proxied expired no-cache no-store private auth; 72 | gzip_types *; 73 | 74 | location @liberama { 75 | proxy_pass $liberama; 76 | } 77 | 78 | location /api { 79 | proxy_pass $liberama; 80 | } 81 | 82 | location /ws { 83 | proxy_pass $liberama; 84 | proxy_http_version 1.1; 85 | proxy_set_header Upgrade $http_upgrade; 86 | proxy_set_header Connection "upgrade"; 87 | proxy_read_timeout 600s; 88 | } 89 | 90 | location /tmp { 91 | root /home/beta.liberama/.liberama/public-files; 92 | 93 | types { } default_type "application/xml; charset=utf-8"; 94 | add_header Content-Encoding gzip; 95 | try_files $uri @liberama; 96 | } 97 | 98 | location /upload { 99 | root /home/beta.liberama/.liberama/public-files; 100 | 101 | try_files $uri @liberama; 102 | } 103 | 104 | location / { 105 | root /home/beta.liberama/.liberama/public; 106 | 107 | location ~* \.(?:manifest|appcache|html)$ { 108 | expires -1; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /server/controllers/ReaderController.js: -------------------------------------------------------------------------------- 1 | const BaseController = require('./BaseController'); 2 | const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton 3 | const JembaReaderStorage = require('../core/Reader/JembaReaderStorage');//singleton 4 | const WorkerState = require('../core/WorkerState');//singleton 5 | 6 | class ReaderController extends BaseController { 7 | constructor(config) { 8 | super(config); 9 | this.readerStorage = new JembaReaderStorage(); 10 | this.readerWorker = new ReaderWorker(config); 11 | this.workerState = new WorkerState(); 12 | } 13 | 14 | async loadBook(req, res) { 15 | const request = req.body; 16 | let error = ''; 17 | try { 18 | if (!request.url) 19 | throw new Error(`key 'url' is empty`); 20 | const workerId = this.readerWorker.loadBookUrl({ 21 | url: request.url, 22 | enableSitesFilter: (request.hasOwnProperty('enableSitesFilter') ? request.enableSitesFilter : true), 23 | skipHtmlCheck: (request.hasOwnProperty('skipHtmlCheck') ? request.skipHtmlCheck : false), 24 | isText: (request.hasOwnProperty('isText') ? request.isText : false), 25 | uploadFileName: (request.hasOwnProperty('uploadFileName') ? request.uploadFileName : false), 26 | djvuQuality: (request.hasOwnProperty('djvuQuality') ? request.djvuQuality : false), 27 | pdfAsText: (request.hasOwnProperty('pdfAsText') ? request.pdfAsText : false), 28 | pdfQuality: (request.hasOwnProperty('pdfQuality') ? request.pdfQuality : false), 29 | }); 30 | const state = this.workerState.getState(workerId); 31 | return (state ? state : {}); 32 | } catch (e) { 33 | error = e.message; 34 | } 35 | //bad request 36 | res.status(400).send({error}); 37 | return false; 38 | } 39 | 40 | async storage(req, res) { 41 | const request = req.body; 42 | let error = ''; 43 | try { 44 | if (!request.action) 45 | throw new Error(`key 'action' is empty`); 46 | if (!request.items || Array.isArray(request.data)) 47 | throw new Error(`key 'items' is empty`); 48 | 49 | return await this.readerStorage.doAction(request); 50 | } catch (e) { 51 | error = e.message; 52 | } 53 | //error 54 | res.status(500).send({error}); 55 | return false; 56 | } 57 | 58 | async uploadFile(req, res) { 59 | const file = req.file; 60 | let error = ''; 61 | try { 62 | const url = await this.readerWorker.saveFile(file); 63 | return {url}; 64 | } catch (e) { 65 | error = e.message; 66 | } 67 | //bad request 68 | res.status(400).send({error}); 69 | return false; 70 | } 71 | } 72 | 73 | module.exports = ReaderController; 74 | -------------------------------------------------------------------------------- /docs/beta/beta.omnireader: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; # managed by Certbot 3 | ssl_certificate /etc/letsencrypt/live/beta.omnireader.ru/fullchain.pem; # managed by Certbot 4 | ssl_certificate_key /etc/letsencrypt/live/beta.omnireader.ru/privkey.pem; # managed by Certbot 5 | include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 6 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot 7 | 8 | server_name beta.omnireader.ru; 9 | set $liberama http://127.0.0.1:34081; 10 | 11 | client_max_body_size 50m; 12 | proxy_read_timeout 1h; 13 | 14 | gzip on; 15 | gzip_min_length 1024; 16 | gzip_proxied expired no-cache no-store private auth; 17 | gzip_types *; 18 | 19 | location @liberama { 20 | proxy_pass $liberama; 21 | } 22 | 23 | location /api { 24 | proxy_pass $liberama; 25 | } 26 | 27 | location /ws { 28 | proxy_pass $liberama; 29 | proxy_http_version 1.1; 30 | proxy_set_header Upgrade $http_upgrade; 31 | proxy_set_header Connection "upgrade"; 32 | proxy_read_timeout 600s; 33 | } 34 | 35 | location /tmp { 36 | root /home/beta.liberama/.liberama/public-files; 37 | 38 | types { } default_type "application/xml; charset=utf-8"; 39 | add_header Content-Encoding gzip; 40 | try_files $uri @liberama; 41 | } 42 | 43 | location /upload { 44 | root /home/beta.liberama/.liberama/public-files; 45 | 46 | try_files $uri @liberama; 47 | } 48 | 49 | location / { 50 | root /home/beta.liberama/.liberama/public; 51 | 52 | location ~* \.(?:manifest|appcache|html)$ { 53 | expires -1; 54 | } 55 | } 56 | } 57 | 58 | server { 59 | listen 80; 60 | server_name beta.omnireader.ru; 61 | 62 | return 301 https://$host$request_uri; 63 | } 64 | 65 | server { 66 | listen 80; 67 | server_name b.beta.omnireader.ru; 68 | set $liberama http://127.0.0.1:34081; 69 | 70 | client_max_body_size 50m; 71 | proxy_read_timeout 1h; 72 | 73 | gzip on; 74 | gzip_min_length 1024; 75 | gzip_proxied expired no-cache no-store private auth; 76 | gzip_types *; 77 | 78 | location @liberama { 79 | proxy_pass $liberama; 80 | } 81 | 82 | location /api { 83 | proxy_pass $liberama; 84 | } 85 | 86 | location /ws { 87 | proxy_pass $liberama; 88 | proxy_http_version 1.1; 89 | proxy_set_header Upgrade $http_upgrade; 90 | proxy_set_header Connection "upgrade"; 91 | proxy_read_timeout 600s; 92 | } 93 | 94 | location /tmp { 95 | root /home/beta.liberama/.liberama/public-files; 96 | 97 | types { } default_type "application/xml; charset=utf-8"; 98 | add_header Content-Encoding gzip; 99 | try_files $uri @liberama; 100 | } 101 | 102 | location /upload { 103 | root /home/beta.liberama/.liberama/public-files; 104 | 105 | try_files $uri @liberama; 106 | } 107 | 108 | location / { 109 | root /home/beta.liberama/.liberama/public; 110 | 111 | location ~* \.(?:manifest|appcache|html)$ { 112 | expires -1; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /server/core/RemoteStorage.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | 4 | const WebSocketConnection = require('./WebSocketConnection'); 5 | 6 | class RemoteStorage { 7 | constructor(config) { 8 | this.config = Object.assign({}, config); 9 | this.config.maxContentLength = this.config.maxContentLength || 10*1024*1024; 10 | 11 | this.accessToken = this.config.accessToken; 12 | 13 | this.wsc = new WebSocketConnection(config.url, 10, 30, {rejectUnauthorized: false}); 14 | } 15 | 16 | async wsRequest(query) { 17 | const response = await this.wsc.message( 18 | await this.wsc.send(Object.assign({accessToken: this.accessToken}, query), 600), 19 | 600 20 | ); 21 | if (response.error) 22 | throw new Error(response.error); 23 | return response; 24 | } 25 | 26 | async wsStat(fileName) { 27 | return await this.wsRequest({action: 'get-stat', fileName}); 28 | } 29 | 30 | async wsGetFile(fileName) { 31 | return this.wsRequest({action: 'get-file', fileName}); 32 | } 33 | 34 | async wsPutFile(fileName, data) {//data base64 encoded string 35 | return this.wsRequest({action: 'put-file', fileName, data}); 36 | } 37 | 38 | async wsDelFile(fileName) { 39 | return this.wsRequest({action: 'del-file', fileName}); 40 | } 41 | 42 | makeRemoteFileName(fileName, dir = '') { 43 | const base = path.basename(fileName); 44 | if (base.length > 3) { 45 | return `${dir}/${base.substr(0, 3)}/${base}`; 46 | } else { 47 | return `${dir}/${base}`; 48 | } 49 | } 50 | 51 | async putFile(fileName, dir = '') { 52 | if (!await fs.pathExists(fileName)) { 53 | throw new Error(`File not found: ${fileName}`); 54 | } 55 | 56 | const remoteFilename = this.makeRemoteFileName(fileName, dir); 57 | 58 | try { 59 | const localStat = await fs.stat(fileName); 60 | let remoteStat = await this.wsStat(remoteFilename); 61 | remoteStat = remoteStat.stat; 62 | 63 | if (remoteStat.isFile && localStat.size == remoteStat.size) { 64 | return; 65 | } 66 | 67 | await this.wsDelFile(remoteFilename); 68 | } catch (e) { 69 | // 70 | } 71 | 72 | const data = await fs.readFile(fileName, 'base64'); 73 | await this.wsPutFile(remoteFilename, data); 74 | } 75 | 76 | async getFile(fileName, dir = '') { 77 | if (await fs.pathExists(fileName)) { 78 | return; 79 | } 80 | 81 | const remoteFilename = this.makeRemoteFileName(fileName, dir); 82 | 83 | const response = await this.wsGetFile(remoteFilename); 84 | await fs.writeFile(fileName, response.data, 'base64'); 85 | } 86 | 87 | async getFileSuccess(filename, dir = '') { 88 | try { 89 | await this.getFile(filename, dir); 90 | return true; 91 | } catch (e) { 92 | // 93 | } 94 | return false; 95 | } 96 | } 97 | 98 | module.exports = RemoteStorage; -------------------------------------------------------------------------------- /docs/omnireader.ru/README.md: -------------------------------------------------------------------------------- 1 | ## Разворачивание сервера OmniReader в Ubuntu 20.04: 2 | 3 | ### git, clone 4 | ``` 5 | cd ~ 6 | sudo apt install ssh git zip 7 | git clone https://github.com/bookpauk/liberama 8 | ``` 9 | 10 | ### node.js 11 | ``` 12 | sudo apt install -y curl 13 | curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash - 14 | sudo apt install -y nodejs 15 | ``` 16 | 17 | ### install packages 18 | ``` 19 | cd liberama 20 | npm i 21 | cd docs/omnireader.ru 22 | ``` 23 | 24 | ### create public dir 25 | ``` 26 | sudo mkdir /home/liberama 27 | sudo chown www-data.www-data /home/liberama 28 | ``` 29 | 30 | ### external converter `calibre` 31 | #### download from https://download.calibre-ebook.com/ 32 | ``` 33 | wget "https://download.calibre-ebook.com/5.29.0/calibre-5.29.0-x86_64.txz" 34 | sudo -u www-data mkdir -p /home/liberama/.liberama/calibre 35 | sudo -u www-data tar xvf calibre-5.29.0-x86_64.txz -C /home/liberama/.liberama/calibre 36 | ``` 37 | 38 | ### external converters 39 | ``` 40 | sudo apt install rar libreoffice poppler-utils djvulibre-bin libtiff-tools graphicsmagick-imagemagick-compat 41 | ``` 42 | 43 | ### nginx, server config 44 | #### Для своего домена необходимо будет подправить docs/omnireader.ru/omnireader и docs/omnireader.ru/omnireader_http 45 | Сначала настроим для HTTP: 46 | ``` 47 | sudo apt install nginx 48 | sudo cp ./omnireader_http /etc/nginx/sites-available/omnireader 49 | sudo ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader 50 | sudo rm /etc/nginx/sites-enabled/default 51 | sudo service nginx reload 52 | sudo chown -R www-data.www-data /var/www 53 | ``` 54 | 55 | ### certbot 56 | #### Следовать инструкции установки certbot https://certbot.eff.org/instructions?ws=nginx&os=ubuntu-20 57 | После установки сертификата, можно использовать конфиг для nginx c ssl: 58 | ``` 59 | sudo cp ./omnireader /etc/nginx/sites-available/omnireader 60 | sudo service nginx reload 61 | 62 | ``` 63 | 64 | ### old.omnireader 65 | #### Старая версия omnireader на базе PHP, можно не устанавливать 66 | ``` 67 | sudo apt install php7.4 php7.4-curl php7.4-mbstring php7.4-fpm 68 | sudo service php7.4-fpm restart 69 | 70 | sudo mkdir /home/oldreader 71 | sudo chown www-data.www-data /home/oldreader 72 | sudo -u www-data cp -r ./old/* /home/oldreader 73 | ``` 74 | 75 | ## Запуск по крону 76 | ``` 77 | * * * * * ~/liberama/docs/omnireader.ru/cron_server.sh >>~/liberama_cron.log 2>&1 78 | ``` 79 | 80 | ## Деплой и запуск 81 | ``` 82 | ./stop_server.sh 83 | ./deploy.sh 84 | ./start_server.sh 85 | ``` 86 | После первого запуска будет создан конфигурационный файл `/home/liberama/data/config.json`. 87 | 88 | Необходимо переключить приложение в режим `omnireader`, отредактировав опцию `servers`: 89 | ``` 90 | "servers": [ 91 | { 92 | "serverName": "1", 93 | "mode": "omnireader", 94 | "ip": "0.0.0.0", 95 | "port": "44081" 96 | } 97 | ] 98 | ``` 99 | Для использования установленных внешних конвертеров можно также поправить: 100 | ``` 101 | "useExternalBookConverter": true, 102 | ``` 103 | и перезапустить сервер. -------------------------------------------------------------------------------- /client/components/Reader/SettingsPage/PageMoveTab/PageMoveTab.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="fit sets-tab-panel"> 3 | <!----------------------------------------------> 4 | <div class="sets-part-header"> 5 | Анимация 6 | </div> 7 | 8 | <div class="sets-item row"> 9 | <div class="sets-label label"> 10 | Тип 11 | </div> 12 | <q-select 13 | v-model="form.pageChangeAnimation" bg-color="input" class="col-left" :options="pageChangeAnimationOptions" 14 | dropdown-icon="la la-angle-down la-sm" 15 | outlined dense emit-value map-options 16 | /> 17 | </div> 18 | 19 | <div class="sets-item row"> 20 | <div class="sets-label label"> 21 | Скорость 22 | </div> 23 | <NumInput v-model="form.pageChangeAnimationSpeed" bg-color="input" class="col-left" :min="0" :max="100" :disable="form.pageChangeAnimation == ''" /> 24 | </div> 25 | 26 | <!----------------------------------------------> 27 | <div class="sets-part-header"> 28 | Другое 29 | </div> 30 | 31 | <div class="sets-item row"> 32 | <div class="sets-label label"> 33 | Страница 34 | </div> 35 | <q-checkbox v-model="form.keepLastToFirst" size="xs" label="Переносить последнюю строку"> 36 | <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%"> 37 | Переносить последнюю строку страницы<br> 38 | в начало следующей при листании 39 | </q-tooltip> 40 | </q-checkbox> 41 | </div> 42 | </div> 43 | </template> 44 | 45 | <script> 46 | //----------------------------------------------------------------------------- 47 | import vueComponent from '../../../vueComponent.js'; 48 | import NumInput from '../../../share/NumInput.vue'; 49 | 50 | const componentOptions = { 51 | components: { 52 | NumInput, 53 | }, 54 | }; 55 | class PageMoveTab { 56 | _options = componentOptions; 57 | _props = { 58 | form: Object, 59 | }; 60 | 61 | created() { 62 | } 63 | 64 | mounted() { 65 | } 66 | 67 | get pageChangeAnimationOptions() { 68 | let result = [ 69 | {label: 'Нет', value: ''}, 70 | {label: 'Вверх-вниз', value: 'downShift'}, 71 | (!this.form.dualPageMode ? {label: 'Вправо-влево', value: 'rightShift'} : null), 72 | {label: 'Протаивание', value: 'thaw'}, 73 | {label: 'Мерцание', value: 'blink'}, 74 | {label: 'Вращение', value: 'rotate'}, 75 | (this.form.wallpaper == '' && !this.form.dualPageMode ? {label: 'Листание', value: 'flip'} : null), 76 | ]; 77 | 78 | result = result.filter(v => v); 79 | 80 | return result; 81 | } 82 | } 83 | 84 | export default vueComponent(PageMoveTab); 85 | //----------------------------------------------------------------------------- 86 | </script> 87 | 88 | <style scoped> 89 | .label { 90 | width: 110px; 91 | } 92 | 93 | .col-left { 94 | width: 150px; 95 | } 96 | </style> 97 | -------------------------------------------------------------------------------- /docs/omnireader.ru/old/parser.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | function parseHtml($data, $remove_tags = FALSE) { 4 | $substs = array( 5 | //html 6 | 'TD' => chr(9), 7 | 'TH' => chr(9), 8 | 'TR' => chr(13) . chr(10) . '<P>', 9 | 'BR' => chr(13) . chr(10) . '<P>', 10 | 'BR/' => chr(13) . chr(10) . '<P>', 11 | 'DD' => chr(13) . chr(10) . '<P>', 12 | 'P' => chr(13) . chr(10) . '<P>', 13 | 'HR' => chr(13) . chr(10), 14 | 'LI' => chr(13) . chr(10), 15 | 'OL' => chr(13) . chr(10), 16 | '/OL' => chr(13) . chr(10), 17 | 'TABLE' => chr(13) . chr(10), 18 | '/TABLE' => chr(13) . chr(10), 19 | 'TITLE' => '<br> ', 20 | '/TITLE' => '<br> ', 21 | 'UL' => chr(13) . chr(10) . ' ', 22 | '/UL' => chr(13) . chr(10), 23 | 24 | // fb2 25 | 'EMPTY-LINE/' => '<P> ', 26 | 'STANZA' => '<P> ', 27 | 'V' => '<P>', 28 | '/POEM' => '<P> ', 29 | 'SUBTITLE' => '<br> <P>', 30 | '/SUBTITLE' => '<br> ', 31 | ); 32 | 33 | $inner_cut = array( 34 | 'HEAD' => 1, 35 | 'SCRIPT' => 1, 36 | 'STYLE' => 1, 37 | //fb2 38 | 'BINARY' => 1, 39 | 'DESCRIPTION' => 1, 40 | ); 41 | 42 | if ($remove_tags) 43 | $substs = $inner_cut = array(); 44 | 45 | 46 | $data = str_replace(' ', ' ', $data); 47 | 48 | $i = 0; 49 | $len = strlen($data); 50 | $out = ''; 51 | $cut_counter = 0; 52 | $cut_tag = ''; 53 | while ($i < $len) { 54 | $left = strpos($data, '<', $i); 55 | if ($left !== FALSE) { 56 | $right = strpos($data, '>', $left + 1); 57 | if ($right !== FALSE) { 58 | $tag = trim(substr($data, $left + 1, $right - $left - 1)); 59 | $first_space = strpos($tag, ' '); 60 | if ($first_space !== FALSE) 61 | $tag = substr($tag, 0, $first_space); 62 | $tag = strtoupper($tag); 63 | 64 | if (!$cut_counter) { 65 | $out .= substr($data, $i, $left - $i); 66 | if (isset($substs[$tag])) 67 | $out .= $substs[$tag]; 68 | } 69 | 70 | if (isset($inner_cut[$tag]) && (!$cut_counter || $cut_tag == $tag)) 71 | { 72 | if (!$cut_counter) 73 | $cut_tag = $tag; 74 | $cut_counter++; 75 | } 76 | if ($tag != '' && $tag[0] == '/' && $cut_tag == substr($tag, 1)) { 77 | $cut_counter = ($cut_counter > 0) ? $cut_counter - 1 : 0; 78 | if (!$cut_counter) 79 | $cut_tag = ''; 80 | } 81 | //$close_tag = substr($tag, 1); 82 | //$out .= "<br>$cut_counter, $cut_tag == $close_tag"; 83 | 84 | $i = $right + 1; 85 | } else 86 | break; 87 | } 88 | else 89 | break; 90 | } 91 | if ($i < $len && !$cut_counter) 92 | $out .= substr($data, $i, $len - $i); 93 | return $out; 94 | } 95 | 96 | ?> 97 | -------------------------------------------------------------------------------- /client/quasar.js: -------------------------------------------------------------------------------- 1 | import 'quasar/dist/quasar.css'; 2 | //import Quasar from 'quasar/dist/quasar.umd.prod.js'; 3 | 4 | import Quasar from 'quasar/src/vue-plugin.js'; 5 | //config 6 | const config = {}; 7 | 8 | //components 9 | //import {QLayout} from 'quasar/src/components/layout'; 10 | //import {QPageContainer, QPage} from 'quasar/src/components/page'; 11 | //import {QDrawer} from 'quasar/src/components/drawer'; 12 | 13 | import {QCircularProgress} from 'quasar/src/components/circular-progress'; 14 | import {QInput} from 'quasar/src/components/input'; 15 | import {QBtn} from 'quasar/src/components/btn'; 16 | import {QBtnGroup} from 'quasar/src/components/btn-group'; 17 | import {QBtnToggle} from 'quasar/src/components/btn-toggle'; 18 | import {QIcon} from 'quasar/src/components/icon'; 19 | import {QSlider} from 'quasar/src/components/slider'; 20 | import {QTabs, QTab} from 'quasar/src/components/tabs'; 21 | //import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels'; 22 | import {QSeparator} from 'quasar/src/components/separator'; 23 | //import {QList} from 'quasar/src/components/item'; 24 | import {QItem, QItemSection, QItemLabel} from 'quasar/src/components/item'; 25 | import {QTooltip} from 'quasar/src/components/tooltip'; 26 | import {QSpinner} from 'quasar/src/components/spinner'; 27 | import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table'; 28 | import {QCheckbox} from 'quasar/src/components/checkbox'; 29 | import {QSelect} from 'quasar/src/components/select'; 30 | import {QColor} from 'quasar/src/components/color'; 31 | import {QPopupProxy} from 'quasar/src/components/popup-proxy'; 32 | import {QDialog} from 'quasar/src/components/dialog'; 33 | import {QChip} from 'quasar/src/components/chip'; 34 | import {QTree} from 'quasar/src/components/tree'; 35 | import {QVirtualScroll} from 'quasar/src/components/virtual-scroll'; 36 | 37 | //import {QExpansionItem} from 'quasar/src/components/expansion-item'; 38 | 39 | const components = { 40 | //QLayout, 41 | //QPageContainer, QPage, 42 | //QDrawer, 43 | 44 | QCircularProgress, 45 | QInput, 46 | QBtn, 47 | QBtnGroup, 48 | QBtnToggle, 49 | QIcon, 50 | QSlider, 51 | QTabs, QTab, 52 | //QTabPanels, QTabPanel, 53 | QSeparator, 54 | //QList, 55 | QItem, QItemSection, QItemLabel, 56 | QTooltip, 57 | QSpinner, 58 | QTable, QTh, QTr, QTd, 59 | QCheckbox, 60 | QSelect, 61 | QColor, 62 | QPopupProxy, 63 | QDialog, 64 | QChip, 65 | QTree, 66 | //QExpansionItem, 67 | QVirtualScroll, 68 | }; 69 | 70 | //directives 71 | import Ripple from 'quasar/src/directives/Ripple'; 72 | import ClosePopup from 'quasar/src/directives/ClosePopup'; 73 | 74 | const directives = {Ripple, ClosePopup}; 75 | 76 | //plugins 77 | import AppFullscreen from 'quasar/src/plugins/AppFullscreen'; 78 | import Notify from 'quasar/src/plugins/Notify'; 79 | 80 | const plugins = { 81 | AppFullscreen, 82 | Notify, 83 | }; 84 | 85 | //icons 86 | //import '@quasar/extras/fontawesome-v5/fontawesome-v5.css'; 87 | //import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js' 88 | 89 | import '@quasar/extras/line-awesome/line-awesome.css'; 90 | import lineAwesome from 'quasar/icon-set/line-awesome.js' 91 | 92 | export default { 93 | quasar: Quasar, 94 | options: { config, components, directives, plugins }, 95 | init: () => { 96 | Quasar.iconSet.set(lineAwesome); 97 | } 98 | }; -------------------------------------------------------------------------------- /client/components/Reader/HelpPage/HelpPage.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <Window style="z-index: 200" @close="close"> 3 | <template #header> 4 | Справка 5 | </template> 6 | 7 | <div class="col column" style="min-width: 600px"> 8 | <div class="bg-menu-1 row"> 9 | <q-tabs 10 | v-model="selectedTab" 11 | active-color="app" 12 | active-bg-color="app" 13 | indicator-color="bg-app" 14 | dense 15 | no-caps 16 | inline-label 17 | class="bg-menu-2 text-menu" 18 | > 19 | <q-tab v-for="btn in buttons" :key="btn.value" :name="btn.value" :label="btn.label" /> 20 | </q-tabs> 21 | </div> 22 | 23 | <keep-alive> 24 | <component :is="activePage" ref="page" class="col"></component> 25 | </keep-alive> 26 | </div> 27 | </Window> 28 | </template> 29 | 30 | <script> 31 | //----------------------------------------------------------------------------- 32 | import vueComponent from '../../vueComponent.js'; 33 | 34 | import Window from '../../share/Window.vue'; 35 | import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue'; 36 | import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue'; 37 | import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue'; 38 | import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue'; 39 | import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue'; 40 | 41 | const pages = { 42 | 'CommonHelpPage': CommonHelpPage, 43 | 'HotkeysHelpPage': HotkeysHelpPage, 44 | 'MouseHelpPage': MouseHelpPage, 45 | 'VersionHistoryPage': VersionHistoryPage, 46 | 'DonateHelpPage': DonateHelpPage, 47 | }; 48 | 49 | const tabs = [ 50 | ['CommonHelpPage', 'Общее'], 51 | ['MouseHelpPage', 'Мышь/тачскрин'], 52 | ['HotkeysHelpPage', 'Клавиатура'], 53 | ['VersionHistoryPage', 'История версий'], 54 | //['DonateHelpPage', 'Помочь проекту'], 55 | ]; 56 | 57 | const componentOptions = { 58 | components: Object.assign({ Window }, pages), 59 | }; 60 | class HelpPage { 61 | _options = componentOptions; 62 | 63 | selectedTab = 'CommonHelpPage'; 64 | 65 | close() { 66 | this.$emit('do-action', {action: 'help'}); 67 | } 68 | 69 | get activePage() { 70 | if (pages[this.selectedTab]) 71 | return pages[this.selectedTab]; 72 | return null; 73 | } 74 | 75 | get buttons() { 76 | let result = []; 77 | for (const tab of tabs) 78 | result.push({label: tab[1], value: tab[0]}); 79 | return result; 80 | } 81 | 82 | activateDonateHelpPage() { 83 | this.selectedTab = 'DonateHelpPage'; 84 | } 85 | 86 | activateVersionHistoryHelpPage() { 87 | this.selectedTab = 'VersionHistoryPage'; 88 | } 89 | 90 | keyHook(event) { 91 | if (event.type == 'keydown' && event.key == 'Escape') { 92 | this.close(); 93 | } 94 | return true; 95 | } 96 | } 97 | 98 | export default vueComponent(HelpPage); 99 | //----------------------------------------------------------------------------- 100 | </script> 101 | 102 | <style scoped> 103 | </style> 104 | -------------------------------------------------------------------------------- /client/components/Reader/ProgressPage/ProgressPage.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div v-show="visible" class="column justify-center items-center" style="background-color: rgba(0, 0, 0, 0.8); z-index: 100;"> 3 | <div class="column justify-start items-center" style="height: 250px"> 4 | <q-circular-progress 5 | show-value 6 | instant-feedback 7 | font-size="13px" 8 | :value="percentage" 9 | size="100px" 10 | :thickness="0.11" 11 | color="green-7" 12 | track-color="grey-4" 13 | class="q-ma-md" 14 | > 15 | <span class="text-yellow">{{ percentage }}%</span> 16 | </q-circular-progress> 17 | 18 | <div> 19 | <span class="text-yellow">{{ text }}</span> 20 | <q-icon :style="iconStyle" color="yellow" name="la la-slash" size="20px" /> 21 | </div> 22 | </div> 23 | </div> 24 | </template> 25 | 26 | <script> 27 | //----------------------------------------------------------------------------- 28 | import vueComponent from '../../vueComponent.js'; 29 | 30 | import * as utils from '../../../share/utils'; 31 | 32 | const ruMessage = { 33 | 'start': ' ', 34 | 'finish': ' ', 35 | 'error': ' ', 36 | 'queue': 'очередь', 37 | 'download': 'скачивание', 38 | 'decompress': 'распаковка', 39 | 'convert': 'конвертирование', 40 | 'loading': 'загрузка', 41 | 'parse': 'обработка', 42 | 'upload': 'отправка', 43 | }; 44 | 45 | class ProgressPage { 46 | text = ''; 47 | totalSteps = 1; 48 | step = 1; 49 | progress = 0; 50 | visible = false; 51 | iconStyle = ''; 52 | 53 | show() { 54 | this.text = ''; 55 | this.totalSteps = 1; 56 | this.step = 1; 57 | this.progress = 0; 58 | this.iconAngle = 0; 59 | this.ani = false; 60 | 61 | this.visible = true; 62 | } 63 | 64 | hide() { 65 | this.visible = false; 66 | this.text = ''; 67 | this.iconAngle = 0; 68 | } 69 | 70 | setState(state) { 71 | if (state.state) { 72 | if (state.state == 'queue') { 73 | this.text = (state.place ? 'Номер в очереди: ' + state.place : ''); 74 | } else { 75 | this.text = (ruMessage[state.state] ? ruMessage[state.state] : state.state); 76 | } 77 | } 78 | this.step = (state.step ? state.step : this.step); 79 | this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps); 80 | this.progress = state.progress || 0; 81 | 82 | if (!this.ani) { 83 | (async() => { 84 | this.ani = true; 85 | this.iconAngle += 30; 86 | this.iconStyle = `transform: rotate(${this.iconAngle}deg); transition: 150ms linear`; 87 | await utils.sleep(150); 88 | this.ani = false; 89 | })(); 90 | } 91 | } 92 | 93 | get percentage() { 94 | return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100); 95 | } 96 | } 97 | 98 | export default vueComponent(ProgressPage); 99 | //----------------------------------------------------------------------------- 100 | </script> 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liberama", 3 | "version": "1.2.8", 4 | "author": "Book Pauk <bookpauk@gmail.com>", 5 | "license": "CC0-1.0", 6 | "repository": "bookpauk/liberama", 7 | "engines": { 8 | "node": ">=16.16.0" 9 | }, 10 | "scripts": { 11 | "dev": "nodemon --inspect --ignore server/.liberama --ignore client --exec 'node server'", 12 | "build:client": "webpack --config build/webpack.prod.config.js", 13 | "build:linux": "npm run build:client && node build/prepkg.js linux && pkg -t node16-linux-x64 -C GZip -o dist/linux/liberama .", 14 | "build:linux-arm64": "npm run build:client && node build/prepkg.js linux-arm64 && pkg -t node16-linuxstatic-arm64 -C GZip -o dist/linux-arm64/liberama .", 15 | "build:win": "npm run build:client && node build/prepkg.js win && pkg -t node16-win-x64 -C GZip -o dist/win/liberama .", 16 | "build:macos": "npm run build:client && node build/prepkg.js macos && pkg -t node16-macos-x64 -C GZip -o dist/macos/liberama .", 17 | "lint": "eslint --ext=.js,.vue client server", 18 | "build:client-dev": "webpack --config build/webpack.dev.config.js", 19 | "build:all": "npm run build:linux && npm run build:win && npm run build:macos && npm run build:linux-arm64", 20 | "release": "npm run build:all && node build/release.js", 21 | "postinstall": "npm run build:client-dev" 22 | }, 23 | "bin": "server/index.js", 24 | "pkg": { 25 | "scripts": "server/config/*.js" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.23.5", 29 | "@babel/eslint-parser": "^7.23.3", 30 | "@babel/eslint-plugin": "^7.23.5", 31 | "@babel/plugin-proposal-decorators": "^7.23.5", 32 | "@babel/preset-env": "^7.23.5", 33 | "@vue/compiler-sfc": "^3.2.22", 34 | "babel-loader": "^9.1.3", 35 | "copy-webpack-plugin": "^11.0.0", 36 | "css-loader": "^6.8.1", 37 | "css-minimizer-webpack-plugin": "^4.2.2", 38 | "eslint": "^8.55.0", 39 | "eslint-plugin-vue": "^9.19.2", 40 | "html-webpack-plugin": "^5.5.4", 41 | "mini-css-extract-plugin": "^2.7.6", 42 | "pkg": "^5.8.1", 43 | "showdown": "^2.1.0", 44 | "terser-webpack-plugin": "^5.3.9", 45 | "vue-eslint-parser": "^9.3.2", 46 | "vue-loader": "^17.3.1", 47 | "vue-style-loader": "^4.1.3", 48 | "webpack": "^5.89.0", 49 | "webpack-cli": "^5.1.4", 50 | "webpack-dev-middleware": "^6.1.1", 51 | "webpack-hot-middleware": "^2.25.4", 52 | "webpack-merge": "^5.10.0", 53 | "workbox-webpack-plugin": "^6.6.0" 54 | }, 55 | "dependencies": { 56 | "@quasar/extras": "^1.16.9", 57 | "@vue/compat": "^3.3.10", 58 | "axios": "^0.27.2", 59 | "base-x": "^4.0.0", 60 | "chardet": "^1.6.0", 61 | "compression": "^1.7.4", 62 | "dayjs": "^1.11.10", 63 | "express": "^4.18.2", 64 | "fg-loadcss": "^3.1.0", 65 | "fs-extra": "^10.1.0", 66 | "he": "^1.2.0", 67 | "iconv-lite": "^0.6.3", 68 | "jembadb": "^5.1.7", 69 | "localforage": "^1.10.0", 70 | "lodash": "^4.17.21", 71 | "minimist": "^1.2.8", 72 | "multer": "^1.4.5-lts.1", 73 | "pako": "^2.1.0", 74 | "path-browserify": "^1.0.1", 75 | "pidusage": "^3.0.2", 76 | "quasar": "^2.14.1", 77 | "safe-buffer": "^5.2.1", 78 | "sanitize-html": "^2.11.0", 79 | "sjcl": "^1.0.8", 80 | "tar-fs": "^2.1.1", 81 | "unbzip2-stream": "^1.4.3", 82 | "vue": "^3.2.37", 83 | "vue-router": "^4.2.5", 84 | "vuex": "^4.1.0", 85 | "vuex-persist": "^3.1.3", 86 | "webdav": "^4.11.3", 87 | "ws": "^8.14.2", 88 | "zip-stream": "^4.1.1" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /server/core/RemoteWebDavStorage.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | 4 | const { createClient } = require('webdav'); 5 | 6 | class RemoteWebDavStorage { 7 | constructor(config) { 8 | this.config = Object.assign({}, config); 9 | this.config.maxContentLength = this.config.maxContentLength || 10*1024*1024; 10 | this.config.maxBodyLength = this.config.maxContentLength; 11 | this.wdc = createClient(config.url, this.config); 12 | } 13 | 14 | _convertStat(data) { 15 | return { 16 | isDirectory: function() { 17 | return data.type === "directory"; 18 | }, 19 | isFile: function() { 20 | return data.type === "file"; 21 | }, 22 | mtime: (new Date(data.lastmod)).getTime(), 23 | name: data.basename, 24 | size: data.size || 0 25 | }; 26 | } 27 | 28 | async stat(filename) { 29 | const stat = await this.wdc.stat(filename); 30 | return this._convertStat(stat); 31 | } 32 | 33 | async writeFile(filename, data) { 34 | return await this.wdc.putFileContents(filename, data) 35 | } 36 | 37 | async unlink(filename) { 38 | return await this.wdc.deleteFile(filename); 39 | } 40 | 41 | async readFile(filename) { 42 | return await this.wdc.getFileContents(filename) 43 | } 44 | 45 | async mkdir(dirname) { 46 | return await this.wdc.createDirectory(dirname); 47 | } 48 | 49 | async putFile(filename, dir = '') { 50 | if (!await fs.pathExists(filename)) { 51 | throw new Error(`File not found: ${filename}`); 52 | } 53 | 54 | const base = path.basename(filename); 55 | let remoteFilename = `${dir}/${base}`; 56 | 57 | if (base.length > 3) { 58 | const remoteDir = `${dir}/${base.substr(0, 3)}`; 59 | try { 60 | await this.mkdir(remoteDir); 61 | } catch (e) { 62 | // 63 | } 64 | remoteFilename = `${remoteDir}/${base}`; 65 | } 66 | 67 | try { 68 | const localStat = await fs.stat(filename); 69 | const remoteStat = await this.stat(remoteFilename); 70 | if (remoteStat.isFile && localStat.size == remoteStat.size) { 71 | return; 72 | } 73 | await this.unlink(remoteFilename); 74 | } catch (e) { 75 | // 76 | } 77 | 78 | const data = await fs.readFile(filename); 79 | await this.writeFile(remoteFilename, data); 80 | } 81 | 82 | async getFile(filename, dir = '') { 83 | if (await fs.pathExists(filename)) { 84 | return; 85 | } 86 | 87 | const base = path.basename(filename); 88 | let remoteFilename = `${dir}/${base}`; 89 | if (base.length > 3) { 90 | remoteFilename = `${dir}/${base.substr(0, 3)}/${base}`; 91 | } 92 | 93 | const data = await this.readFile(remoteFilename); 94 | await fs.writeFile(filename, data); 95 | } 96 | 97 | async getFileSuccess(filename, dir = '') { 98 | try { 99 | await this.getFile(filename, dir); 100 | return true; 101 | } catch (e) { 102 | // 103 | } 104 | return false; 105 | } 106 | } 107 | 108 | module.exports = RemoteWebDavStorage; -------------------------------------------------------------------------------- /server/core/AsyncExit.js: -------------------------------------------------------------------------------- 1 | const defaultTimeout = 15*1000;//15 sec 2 | const exitSignals = ['SIGINT', 'SIGTERM', 'SIGBREAK', 'SIGHUP', 'uncaughtException']; 3 | 4 | //singleton 5 | let instance = null; 6 | 7 | class AsyncExit { 8 | constructor(signals = exitSignals, codeOnSignal = 2) { 9 | if (!instance) { 10 | this.onSignalCallbacks = new Map(); 11 | this.callbacks = new Map(); 12 | this.afterCallbacks = new Map(); 13 | this.exitTimeout = defaultTimeout; 14 | 15 | this._init(signals, codeOnSignal); 16 | 17 | instance = this; 18 | } 19 | 20 | return instance; 21 | } 22 | 23 | _init(signals, codeOnSignal) { 24 | const runSingalCallbacks = async(signal, err, origin) => { 25 | if (!this.onSignalCallbacks.size) { 26 | console.error(`Uncaught signal "${signal}" received, error: "${(err.stack ? err.stack : err)}"`); 27 | } 28 | 29 | for (const signalCallback of this.onSignalCallbacks.keys()) { 30 | try { 31 | await signalCallback(signal, err, origin); 32 | } catch(e) { 33 | console.error(e); 34 | } 35 | } 36 | }; 37 | 38 | for (const signal of signals) { 39 | process.once(signal, async(err, origin) => { 40 | await runSingalCallbacks(signal, err, origin); 41 | this.exit(codeOnSignal); 42 | }); 43 | } 44 | } 45 | 46 | onSignal(signalCallback) { 47 | if (!this.onSignalCallbacks.has(signalCallback)) { 48 | this.onSignalCallbacks.set(signalCallback, true); 49 | } 50 | } 51 | 52 | add(exitCallback) { 53 | if (!this.callbacks.has(exitCallback)) { 54 | this.callbacks.set(exitCallback, true); 55 | } 56 | } 57 | 58 | addAfter(exitCallback) { 59 | if (!this.afterCallbacks.has(exitCallback)) { 60 | this.afterCallbacks.set(exitCallback, true); 61 | } 62 | } 63 | 64 | remove(exitCallback) { 65 | if (this.callbacks.has(exitCallback)) { 66 | this.callbacks.delete(exitCallback); 67 | } 68 | if (this.afterCallbacks.has(exitCallback)) { 69 | this.afterCallbacks.delete(exitCallback); 70 | } 71 | } 72 | 73 | setExitTimeout(timeout) { 74 | this.exitTimeout = timeout; 75 | } 76 | 77 | exit(code = 0) { 78 | if (this.exiting) 79 | return; 80 | 81 | this.exiting = true; 82 | 83 | const timer = setTimeout(() => { process.exit(code); }, this.exitTimeout); 84 | 85 | (async() => { 86 | for (const exitCallback of this.callbacks.keys()) { 87 | try { 88 | await exitCallback(); 89 | } catch(e) { 90 | console.error(e); 91 | } 92 | } 93 | 94 | for (const exitCallback of this.afterCallbacks.keys()) { 95 | try { 96 | await exitCallback(); 97 | } catch(e) { 98 | console.error(e); 99 | } 100 | } 101 | 102 | clearTimeout(timer); 103 | //console.log('Exited gracefully'); 104 | process.exit(code); 105 | })(); 106 | } 107 | } 108 | 109 | module.exports = AsyncExit; 110 | -------------------------------------------------------------------------------- /docs/omnireader.ru/omnireader: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; # managed by Certbot 3 | ssl_certificate /etc/letsencrypt/live/omnireader.ru/fullchain.pem; # managed by Certbot 4 | ssl_certificate_key /etc/letsencrypt/live/omnireader.ru/privkey.pem; # managed by Certbot 5 | include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 6 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot 7 | 8 | server_name omnireader.ru; 9 | set $liberama http://127.0.0.1:44081; 10 | 11 | client_max_body_size 100m; 12 | proxy_read_timeout 1h; 13 | 14 | gzip on; 15 | gzip_min_length 1024; 16 | gzip_proxied expired no-cache no-store private auth; 17 | gzip_types *; 18 | 19 | location @liberama { 20 | proxy_pass $liberama; 21 | } 22 | 23 | location /api { 24 | proxy_pass $liberama; 25 | } 26 | 27 | location /ws { 28 | proxy_pass $liberama; 29 | proxy_http_version 1.1; 30 | proxy_set_header Upgrade $http_upgrade; 31 | proxy_set_header Connection "upgrade"; 32 | proxy_read_timeout 600s; 33 | } 34 | 35 | location /tmp { 36 | root /home/liberama/.liberama/public-files; 37 | 38 | types { } default_type "application/xml; charset=utf-8"; 39 | add_header Content-Encoding gzip; 40 | try_files $uri @liberama; 41 | } 42 | 43 | location /upload { 44 | root /home/liberama/.liberama/public-files; 45 | 46 | try_files $uri @liberama; 47 | } 48 | 49 | location / { 50 | root /home/liberama/.liberama/public; 51 | 52 | location ~* \.(?:manifest|appcache|html)$ { 53 | expires -1; 54 | } 55 | } 56 | } 57 | 58 | server { 59 | listen 80; 60 | server_name b.omnireader.ru; 61 | set $liberama http://127.0.0.1:44081; 62 | 63 | client_max_body_size 50m; 64 | proxy_read_timeout 1h; 65 | 66 | gzip on; 67 | gzip_min_length 1024; 68 | gzip_proxied expired no-cache no-store private auth; 69 | gzip_types *; 70 | 71 | location @liberama { 72 | proxy_pass $liberama; 73 | } 74 | 75 | location /api { 76 | proxy_pass $liberama; 77 | } 78 | 79 | location /ws { 80 | proxy_pass $liberama; 81 | proxy_http_version 1.1; 82 | proxy_set_header Upgrade $http_upgrade; 83 | proxy_set_header Connection "upgrade"; 84 | } 85 | 86 | location /tmp { 87 | root /home/liberama/.liberama/public-files; 88 | 89 | types { } default_type "application/xml; charset=utf-8"; 90 | add_header Content-Encoding gzip; 91 | try_files $uri @liberama; 92 | } 93 | 94 | location /upload { 95 | root /home/liberama/.liberama/public-files; 96 | 97 | try_files $uri @liberama; 98 | } 99 | 100 | location / { 101 | root /home/liberama/.liberama/public; 102 | 103 | location ~* \.(?:manifest|appcache|html)$ { 104 | expires -1; 105 | } 106 | } 107 | } 108 | 109 | server { 110 | listen 80; 111 | server_name omnireader.ru; 112 | 113 | return 301 https://$host$request_uri; 114 | } 115 | 116 | server { 117 | listen 80; 118 | server_name old.omnireader.ru; 119 | 120 | client_max_body_size 100m; 121 | 122 | gzip on; 123 | gzip_min_length 1024; 124 | gzip_proxied expired no-cache no-store private auth; 125 | gzip_types *; 126 | 127 | root /home/oldreader; 128 | 129 | index index.html; 130 | 131 | # Обработка php файлов с помощью fpm 132 | location ~ \.php$ { 133 | try_files $uri =404; 134 | include /etc/nginx/fastcgi.conf; 135 | fastcgi_pass unix:/run/php/php7.4-fpm.sock; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /server/core/Reader/BookConverter/ConvertJpegPng.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | //const utils = require('../../utils'); 4 | 5 | const ConvertBase = require('./ConvertBase'); 6 | 7 | class ConvertJpegPng extends ConvertBase { 8 | check(data, opts) { 9 | const {inputFiles} = opts; 10 | 11 | return this.config.useExternalBookConverter && 12 | inputFiles.sourceFileType && 13 | (inputFiles.sourceFileType.ext == 'jpg' || inputFiles.sourceFileType.ext == 'png' ); 14 | } 15 | 16 | async run(data, opts) { 17 | const {inputFiles, uploadFileName, imageFiles} = opts; 18 | 19 | if (!imageFiles) { 20 | if (!this.check(data, opts)) 21 | return false; 22 | } 23 | 24 | let files = []; 25 | if (imageFiles) { 26 | files = imageFiles; 27 | } else { 28 | const imageFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}.${inputFiles.sourceFileType.ext}`; 29 | await fs.copy(inputFiles.sourceFile, imageFile); 30 | files.push({src: imageFile}); 31 | } 32 | 33 | //читаем изображения 34 | const limitSize = 2*this.config.maxUploadFileSize; 35 | let imagesSize = 0; 36 | 37 | const loadImage = async(image) => { 38 | const src = path.parse(image.src); 39 | let type = 'unknown'; 40 | switch (src.ext) { 41 | case '.jpg': type = 'image/jpeg'; break; 42 | case '.png': type = 'image/png'; break; 43 | } 44 | if (type != 'unknown') { 45 | image.data = (await fs.readFile(image.src)).toString('base64'); 46 | image.type = type; 47 | image.name = src.base; 48 | 49 | imagesSize += image.data.length; 50 | if (imagesSize > limitSize) { 51 | throw new Error(`Файл для конвертирования слишком большой|FORLOG| imagesSize: ${imagesSize} > ${limitSize}`); 52 | } 53 | } 54 | } 55 | 56 | let images = []; 57 | let loading = []; 58 | files.forEach(img => { 59 | images.push(img); 60 | loading.push(loadImage(img)); 61 | }); 62 | 63 | await Promise.all(loading); 64 | 65 | //формируем fb2 66 | let titleInfo = {}; 67 | let desc = {_n: 'description', 'title-info': titleInfo}; 68 | let pars = []; 69 | let body = {_n: 'body', section: {_a: [pars]}}; 70 | let binary = []; 71 | let fb2 = [desc, body, binary]; 72 | 73 | let title = ''; 74 | if (uploadFileName) 75 | title = uploadFileName; 76 | 77 | titleInfo['book-title'] = title; 78 | 79 | for (const image of images) { 80 | if (image.type) { 81 | const img = {_n: 'binary', _attrs: {id: image.name, 'content-type': image.type}, _t: image.data}; 82 | binary.push(img); 83 | 84 | const attrs = {'l:href': `#${image.name}`}; 85 | if (image.alt) { 86 | image.alt = (image.alt.length > 256 ? image.alt.substring(0, 256) : image.alt); 87 | attrs.alt = image.alt; 88 | } 89 | 90 | pars.push({_n: 'p', _t: ''}); 91 | pars.push({_n: 'image', _attrs: attrs}); 92 | } 93 | } 94 | pars.push({_n: 'p', _t: ''}); 95 | 96 | return this.formatFb2(fb2); 97 | } 98 | } 99 | 100 | module.exports = ConvertJpegPng; 101 | -------------------------------------------------------------------------------- /server/core/Reader/BookConverter/ConvertSites.js: -------------------------------------------------------------------------------- 1 | const URL = require('url').URL; 2 | 3 | const ConvertHtml = require('./ConvertHtml'); 4 | 5 | const sitesFilter = { 6 | 'www.fanfiction.net': { 7 | converter: 'cutter', 8 | begin: `<div class='storytext xcontrast_txt nocopy' id='storytext'>`, 9 | end: `<div style='height:5px'></div><div style='clear:both;text-align:right;'>`, 10 | }, 11 | 'archiveofourown.org': { 12 | converter: 'cutter', 13 | begin: `<!-- BEGIN section where work skin applies -->`, 14 | end: `<!-- END work skin -->`, 15 | }, 16 | 'flibusta.is': { 17 | converter: 'flibusta' 18 | }, 19 | }; 20 | 21 | class ConvertSites extends ConvertHtml { 22 | check(data, opts) { 23 | const {url, dataType} = opts; 24 | 25 | const parsedUrl = new URL(url); 26 | if (dataType && dataType.ext == 'html') { 27 | if (sitesFilter[parsedUrl.hostname]) 28 | return {hostname: parsedUrl.hostname}; 29 | } 30 | 31 | return false; 32 | } 33 | 34 | async run(data, opts) { 35 | if (!opts.enableSitesFilter) 36 | return false; 37 | 38 | const checkResult = this.check(data, opts); 39 | if (!checkResult) 40 | return false; 41 | 42 | const {hostname} = checkResult; 43 | 44 | let text = this.decode(data).toString(); 45 | 46 | text = this[sitesFilter[hostname].converter](text, sitesFilter[hostname]); 47 | 48 | if (text === false) 49 | return false; 50 | 51 | return await super.run(Buffer.from(text), {skipHtmlCheck: true}); 52 | } 53 | 54 | getTitle(text) { 55 | let title = ''; 56 | const m = text.match(/<title>([\s\S]*?)<\/title>/); 57 | if (m) 58 | title = m[1]; 59 | 60 | return title.trim(); 61 | } 62 | 63 | cutter(text, opts) { 64 | const title = `<title>${this.getTitle(text)}`; 65 | const l = text.indexOf(opts.begin) + opts.begin.length; 66 | const r = text.indexOf(opts.end); 67 | if (l < 0 || r < 0 || r <= l) 68 | return false; 69 | 70 | return text.substring(l, r) + title; 71 | } 72 | 73 | flibusta(text) { 74 | let author = ''; 75 | let m = text.match(/- ([\s\S]*?)<\/a>/); 76 | if (m) 77 | author = m[1]; 78 | 79 | let book = this.getTitle(text); 80 | book = book.replace(' (fb2) | Флибуста', ''); 81 | 82 | const title = `${author}${(author ? ' - ' : '')}${book}`; 83 | 84 | let begin = '

'; 85 | if (text.indexOf(begin) <= 0) 86 | begin = '

'; 87 | 88 | const end = '

') 98 | .replace(/

/g, '

') 99 | .replace(/
/g, '

') 100 | .replace(/

/g, '

') 101 | .replace(/
/g, '

') 102 | .replace(/<\/h3>/g, '
') 103 | .replace(/<\/h5>/g, '
') 104 | .replace(/
/g, '
') 105 | .replace(/
/g, '
') 106 | + title; 107 | } 108 | } 109 | 110 | module.exports = ConvertSites; 111 | -------------------------------------------------------------------------------- /client/components/Reader/CopyTextPage/CopyTextPage.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 112 | 113 | 127 | -------------------------------------------------------------------------------- /server/config/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const path = require('path'); 3 | const fs = require('fs-extra'); 4 | 5 | const branchFilename = __dirname + '/application_env'; 6 | 7 | const propsToSave = [ 8 | 'maxUploadFileSize', 9 | 'maxTempPublicDirSize', 10 | 'maxUploadPublicDirSize', 11 | 'useExternalBookConverter', 12 | 13 | 'servers', 14 | 'remoteStorage', 15 | 'bucEnabled', 16 | 'bucServer', 17 | 'networkLibraryLink', 18 | ]; 19 | 20 | let instance = null; 21 | 22 | //singleton 23 | class ConfigManager { 24 | constructor() { 25 | if (!instance) { 26 | this.inited = false; 27 | 28 | instance = this; 29 | } 30 | 31 | return instance; 32 | } 33 | 34 | async init(dataDir) { 35 | if (this.inited) 36 | throw new Error('already inited'); 37 | 38 | this.branch = 'production'; 39 | try { 40 | await fs.access(branchFilename); 41 | this.branch = (await fs.readFile(branchFilename, 'utf8')).trim(); 42 | } catch (err) { 43 | // 44 | } 45 | 46 | process.env.NODE_ENV = this.branch; 47 | 48 | this.branchConfigFile = __dirname + `/${this.branch}.js`; 49 | const config = require(this.branchConfigFile); 50 | 51 | if (dataDir) { 52 | config.dataDir = path.resolve(dataDir); 53 | } else { 54 | config.dataDir = `${config.execDir}/.${config.name}`; 55 | } 56 | 57 | await fs.ensureDir(config.dataDir); 58 | this._userConfigFile = `${config.dataDir}/config.json`; 59 | this._restrictedFile = `${config.dataDir}/restricted.json`; 60 | this._config = config; 61 | 62 | this.inited = true; 63 | } 64 | 65 | get config() { 66 | if (!this.inited) 67 | throw new Error('not inited'); 68 | return _.cloneDeep(this._config); 69 | } 70 | 71 | set config(value) { 72 | Object.assign(this._config, value); 73 | } 74 | 75 | get userConfigFile() { 76 | return this._userConfigFile; 77 | } 78 | 79 | get restrictedFile() { 80 | return this._restrictedFile; 81 | } 82 | 83 | set userConfigFile(value) { 84 | if (value) 85 | this._userConfigFile = value; 86 | } 87 | 88 | async load() { 89 | try { 90 | if (!this.inited) 91 | throw new Error('not inited'); 92 | 93 | if (await fs.pathExists(this.userConfigFile)) { 94 | const data = JSON.parse(await fs.readFile(this.userConfigFile, 'utf8')); 95 | const config = _.pick(data, propsToSave); 96 | 97 | this.config = config; 98 | 99 | //сохраним конфиг, если не все атрибуты присутствуют в файле конфига 100 | for (const prop of propsToSave) 101 | if (!Object.prototype.hasOwnProperty.call(config, prop)) { 102 | await this.save(); 103 | break; 104 | } 105 | } else { 106 | await this.save(); 107 | } 108 | 109 | if (await fs.pathExists(this.restrictedFile)) { 110 | const data = JSON.parse(await fs.readFile(this.restrictedFile, 'utf8')); 111 | 112 | this.config = {restricted: data}; 113 | } 114 | } catch(e) { 115 | throw new Error(`Error while loading "${this.userConfigFile}": ${e.message}`); 116 | } 117 | } 118 | 119 | async save() { 120 | if (!this.inited) 121 | throw new Error('not inited'); 122 | 123 | const dataToSave = _.pick(this._config, propsToSave); 124 | await fs.writeFile(this.userConfigFile, JSON.stringify(dataToSave, null, 4)); 125 | } 126 | } 127 | 128 | module.exports = ConfigManager; -------------------------------------------------------------------------------- /server/core/LimitedQueue.js: -------------------------------------------------------------------------------- 1 | class LimitedQueue { 2 | constructor(enqueueAfter = 10, size = 100, timeout = 60*60*1000) {//timeout в ms 3 | this.size = size; 4 | this.timeout = timeout; 5 | 6 | this.abortCount = 0; 7 | this.enqueueAfter = enqueueAfter; 8 | this.freed = enqueueAfter; 9 | this.listeners = []; 10 | } 11 | 12 | _addListener(listener) { 13 | this.listeners.push(listener); 14 | } 15 | 16 | //отсылаем сообщение первому ожидающему и удаляем его из списка 17 | _emitFree() { 18 | if (this.listeners.length > 0) { 19 | let listener = this.listeners.shift(); 20 | listener.onFree(); 21 | 22 | for (let i = 0; i < this.listeners.length; i++) { 23 | this.listeners[i].onPlaceChange(i + 1); 24 | } 25 | } 26 | } 27 | 28 | get(onPlaceChange) { 29 | return new Promise((resolve, reject) => { 30 | if (this.destroyed) 31 | reject(new Error('destroyed')); 32 | 33 | const take = () => { 34 | if (this.freed <= 0) 35 | throw new Error('Ошибка получения ресурсов в очереди ожидания'); 36 | 37 | this.freed--; 38 | this.resetTimeout(); 39 | 40 | let aCount = this.abortCount; 41 | return { 42 | ret: () => { 43 | if (aCount == this.abortCount) { 44 | this.freed++; 45 | this._emitFree(); 46 | aCount = -1; 47 | this.resetTimeout(); 48 | } 49 | }, 50 | abort: () => { 51 | return (aCount != this.abortCount); 52 | }, 53 | resetTimeout: this.resetTimeout.bind(this) 54 | }; 55 | }; 56 | 57 | if (this.freed > 0) { 58 | resolve(take()); 59 | } else { 60 | if (this.listeners.length < this.size) { 61 | this._addListener({ 62 | onFree: () => { 63 | resolve(take()); 64 | }, 65 | onError: (err) => { 66 | reject(err); 67 | }, 68 | onPlaceChange: (i) => { 69 | if (onPlaceChange) 70 | onPlaceChange(i); 71 | } 72 | }); 73 | if (onPlaceChange) 74 | onPlaceChange(this.listeners.length); 75 | } else { 76 | reject(new Error('Превышен размер очереди ожидания')); 77 | } 78 | } 79 | }); 80 | } 81 | 82 | resetTimeout() { 83 | if (this.timer) 84 | clearTimeout(this.timer); 85 | this.timer = setTimeout(() => { this.clean(); }, this.timeout); 86 | } 87 | 88 | clean() { 89 | this.timer = null; 90 | 91 | if (this.freed < this.enqueueAfter) { 92 | this.abortCount++; 93 | //чистка listeners 94 | for (const listener of this.listeners) { 95 | listener.onError('Время ожидания в очереди истекло'); 96 | } 97 | this.listeners = []; 98 | 99 | this.freed = this.enqueueAfter; 100 | } 101 | } 102 | 103 | destroy() { 104 | if (this.timer) { 105 | clearTimeout(this.timer); 106 | this.timer = null; 107 | } 108 | 109 | for (const listener of this.listeners) { 110 | listener.onError('destroy'); 111 | } 112 | this.listeners = []; 113 | this.abortCount++; 114 | 115 | this.destroyed = true; 116 | } 117 | } 118 | 119 | module.exports = LimitedQueue; -------------------------------------------------------------------------------- /client/components/Reader/LoaderPage/PasteTextPage/PasteTextPage.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 108 | 109 | 140 | -------------------------------------------------------------------------------- /server/core/Reader/BookConverter/textUtils.js: -------------------------------------------------------------------------------- 1 | const chardet = require('chardet'); 2 | 3 | function getEncoding(buf) { 4 | let selected = getEncodingLite(buf); 5 | 6 | if (selected == 'ISO-8859-5' && buf.length > 10) { 7 | const charsetAll = chardet.analyse(buf.slice(0, 20000)); 8 | for (const charset of charsetAll) { 9 | if (charset.name.indexOf('ISO-8859') < 0) { 10 | selected = charset.name; 11 | break; 12 | } 13 | } 14 | } 15 | 16 | return selected; 17 | } 18 | 19 | 20 | function getEncodingLite(buf, returnAll) { 21 | const lowerCase = 3; 22 | const upperCase = 1; 23 | 24 | const codePage = { 25 | 'k': 'koi8-r', 26 | 'w': 'Windows-1251', 27 | 'd': 'cp866', 28 | 'i': 'ISO-8859-5', 29 | 'm': 'maccyrillic', 30 | 'u': 'utf-8', 31 | }; 32 | 33 | let charsets = { 34 | 'k': 0, 35 | 'w': 0, 36 | 'd': 0, 37 | 'i': 0, 38 | 'm': 0, 39 | 'u': 0, 40 | }; 41 | 42 | const len = buf.length; 43 | const blockSize = (len > 5*3000 ? 3000 : len); 44 | let counter = 0; 45 | let i = 0; 46 | let totalChecked = 0; 47 | while (i < len) { 48 | const char = buf[i]; 49 | const nextChar = (i < len - 1 ? buf[i + 1] : 0); 50 | totalChecked++; 51 | i++; 52 | //non-russian characters 53 | if (char < 128 || char > 256) 54 | continue; 55 | //UTF-8 56 | if ((char == 208 || char == 209) && nextChar >= 128 && nextChar <= 190) 57 | charsets['u'] += lowerCase; 58 | else { 59 | //CP866 60 | if ((char > 159 && char < 176) || (char > 223 && char < 242)) charsets['d'] += lowerCase; 61 | if ((char > 127 && char < 160)) charsets['d'] += upperCase; 62 | 63 | //KOI8-R 64 | if ((char > 191 && char < 223)) charsets['k'] += lowerCase; 65 | if ((char > 222 && char < 256)) charsets['k'] += upperCase; 66 | 67 | //WIN-1251 68 | if (char > 223 && char < 256) charsets['w'] += lowerCase; 69 | if (char > 191 && char < 224) charsets['w'] += upperCase; 70 | 71 | //MAC 72 | if (char > 221 && char < 255) charsets['m'] += lowerCase; 73 | if (char > 127 && char < 160) charsets['m'] += upperCase; 74 | 75 | //ISO-8859-5 76 | if (char > 207 && char < 240) charsets['i'] += lowerCase; 77 | if (char > 175 && char < 208) charsets['i'] += upperCase; 78 | } 79 | 80 | counter++; 81 | 82 | if (counter > blockSize) { 83 | counter = 0; 84 | i += Math.round(len/2 - 2*blockSize); 85 | } 86 | } 87 | 88 | let sorted = Object.keys(charsets).map(function(key) { 89 | return { codePage: codePage[key], c: charsets[key], totalChecked }; 90 | }); 91 | 92 | sorted.sort((a, b) => b.c - a.c); 93 | 94 | if (returnAll) 95 | return sorted; 96 | else if (sorted[0].c > 0 && sorted[0].c > sorted[0].totalChecked/2) 97 | return sorted[0].codePage; 98 | else 99 | return 'ISO-8859-5'; 100 | } 101 | 102 | function checkIfText(buf) { 103 | const enc = getEncodingLite(buf, true); 104 | if (enc[0].c > enc[0].totalChecked*0.9) 105 | return true; 106 | 107 | let spaceCount = 0; 108 | let crCount = 0; 109 | let lfCount = 0; 110 | for (let i = 0; i < buf.length; i++) { 111 | if (buf[i] == 32) 112 | spaceCount++; 113 | if (buf[i] == 13) 114 | crCount++; 115 | if (buf[i] == 10) 116 | lfCount++; 117 | } 118 | 119 | const spaceFreq = spaceCount/(buf.length + 1); 120 | const crFreq = crCount/(buf.length + 1); 121 | const lfFreq = lfCount/(buf.length + 1); 122 | 123 | return (buf.length < 1000 || spaceFreq > 0.1 || crFreq > 0.03 || lfFreq > 0.03); 124 | } 125 | 126 | module.exports = { 127 | getEncoding, 128 | getEncodingLite, 129 | checkIfText, 130 | } -------------------------------------------------------------------------------- /client/components/Reader/HelpPage/CommonHelpPage/CommonHelpPage.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 78 | 79 | 94 | -------------------------------------------------------------------------------- /server/controllers/BookUpdateCheckerController.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | //const _ = require('lodash'); 3 | 4 | const BUCServer = require('../core/BookUpdateChecker/BUCServer'); 5 | const log = new (require('../core/AppLogger'))().log;//singleton 6 | //const utils = require('../core/utils'); 7 | 8 | const cleanPeriod = 1*60*1000;//1 минута 9 | const closeSocketOnIdle = 5*60*1000;//5 минут 10 | 11 | class BookUpdateCheckerController { 12 | constructor(wss, config) { 13 | this.config = config; 14 | this.isDevelopment = (config.branch == 'development'); 15 | 16 | this.accessToken = config.accessToken; 17 | this.bucServer = new BUCServer(config); 18 | 19 | this.wss = wss; 20 | 21 | wss.on('connection', (ws) => { 22 | ws.on('message', (message) => { 23 | this.onMessage(ws, message.toString()); 24 | }); 25 | 26 | ws.on('error', (err) => { 27 | log(LM_ERR, err); 28 | }); 29 | }); 30 | 31 | setTimeout(() => { this.periodicClean(); }, cleanPeriod); 32 | } 33 | 34 | periodicClean() { 35 | try { 36 | const now = Date.now(); 37 | this.wss.clients.forEach((ws) => { 38 | if (!ws.lastActivity || now - ws.lastActivity > closeSocketOnIdle - 50) { 39 | ws.terminate(); 40 | } 41 | }); 42 | } finally { 43 | setTimeout(() => { this.periodicClean(); }, cleanPeriod); 44 | } 45 | } 46 | 47 | async onMessage(ws, message) { 48 | let req = {}; 49 | try { 50 | if (this.isDevelopment) { 51 | log(`BUC-WebSocket-IN: ${message.substr(0, 4000)}`); 52 | } 53 | 54 | req = JSON.parse(message); 55 | 56 | ws.lastActivity = Date.now(); 57 | 58 | //pong for WebSocketConnection 59 | this.send({_rok: 1}, req, ws); 60 | 61 | if (req.accessToken !== this.accessToken) 62 | throw new Error('Access denied'); 63 | 64 | switch (req.action) { 65 | case 'test': 66 | await this.test(req, ws); break; 67 | case 'get-buc': 68 | await this.getBuc(req, ws); break; 69 | case 'update-buc': 70 | await this.updateBuc(req, ws); break; 71 | 72 | default: 73 | throw new Error(`Action not found: ${req.action}`); 74 | } 75 | } catch (e) { 76 | this.send({error: e.message}, req, ws); 77 | } 78 | } 79 | 80 | send(res, req, ws) { 81 | if (ws.readyState == WebSocket.OPEN) { 82 | ws.lastActivity = Date.now(); 83 | let r = res; 84 | if (req.requestId) 85 | r = Object.assign({requestId: req.requestId}, r); 86 | 87 | const message = JSON.stringify(r); 88 | ws.send(message); 89 | 90 | if (this.isDevelopment) { 91 | log(`BUC-WebSocket-OUT: ${message.substr(0, 4000)}`); 92 | } 93 | 94 | } 95 | } 96 | 97 | //Actions ------------------------------------------------------------------ 98 | async test(req, ws) { 99 | this.send({message: 'Liberama project is awesome'}, req, ws); 100 | } 101 | 102 | async getBuc(req, ws) { 103 | if (!req.fromCheckTime) 104 | throw new Error(`key 'fromCheckTime' is empty`); 105 | 106 | await this.bucServer.getBuc(req.fromCheckTime, (rows) => { 107 | this.send({state: 'get', rows}, req, ws); 108 | }); 109 | 110 | this.send({state: 'finish'}, req, ws); 111 | } 112 | 113 | async updateBuc(req, ws) { 114 | if (!req.bookUrls) 115 | throw new Error(`key 'bookUrls' is empty`); 116 | 117 | if (!Array.isArray(req.bookUrls)) 118 | throw new Error(`key 'bookUrls' must be array`); 119 | 120 | await this.bucServer.updateBuc(req.bookUrls); 121 | 122 | this.send({state: 'success'}, req, ws); 123 | } 124 | } 125 | 126 | module.exports = BookUpdateCheckerController; 127 | -------------------------------------------------------------------------------- /docs/liberama.top/liberama: -------------------------------------------------------------------------------- 1 | server { 2 | server_name _; 3 | listen 80 default_server; 4 | listen 443 ssl default_server; 5 | 6 | #openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt 7 | ssl_certificate /etc/nginx/ssl/nginx.crt; 8 | ssl_certificate_key /etc/nginx/ssl/nginx.key; 9 | return 403; 10 | } 11 | 12 | server { 13 | listen 443 ssl; # managed by Certbot 14 | ssl_certificate /etc/letsencrypt/live/liberama.top/fullchain.pem; # managed by Certbot 15 | ssl_certificate_key /etc/letsencrypt/live/liberama.top/privkey.pem; # managed by Certbot 16 | include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 17 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot 18 | 19 | server_name liberama.top; 20 | set $liberama http://127.0.0.1:55081; 21 | 22 | client_max_body_size 100m; 23 | proxy_read_timeout 1h; 24 | 25 | gzip on; 26 | gzip_min_length 1024; 27 | gzip_proxied expired no-cache no-store private auth; 28 | gzip_types *; 29 | 30 | location @liberama { 31 | proxy_pass $liberama; 32 | } 33 | 34 | location /api { 35 | proxy_pass $liberama; 36 | } 37 | 38 | location /ws { 39 | proxy_pass $liberama; 40 | proxy_http_version 1.1; 41 | proxy_set_header Upgrade $http_upgrade; 42 | proxy_set_header Connection "upgrade"; 43 | proxy_read_timeout 600s; 44 | } 45 | 46 | location /tmp { 47 | root /home/liberama/.liberama/public-files; 48 | 49 | types { } default_type "application/xml; charset=utf-8"; 50 | add_header Content-Encoding gzip; 51 | try_files $uri @liberama; 52 | } 53 | 54 | location /upload { 55 | root /home/liberama/.liberama/public-files; 56 | 57 | try_files $uri @liberama; 58 | } 59 | 60 | location / { 61 | root /home/liberama/.liberama/public; 62 | 63 | location ~* \.(?:manifest|appcache|html)$ { 64 | expires -1; 65 | } 66 | } 67 | } 68 | 69 | server { 70 | listen 80; 71 | server_name liberama.top; 72 | 73 | return 301 https://$host$request_uri; 74 | } 75 | 76 | server { 77 | listen 80; 78 | server_name b.liberama.top; 79 | set $liberama http://127.0.0.1:55081; 80 | 81 | client_max_body_size 100m; 82 | proxy_read_timeout 1h; 83 | 84 | gzip on; 85 | gzip_min_length 1024; 86 | gzip_proxied expired no-cache no-store private auth; 87 | gzip_types *; 88 | 89 | location @liberama { 90 | proxy_pass $liberama; 91 | } 92 | 93 | location /api { 94 | proxy_pass $liberama; 95 | } 96 | 97 | location /ws { 98 | proxy_pass $liberama; 99 | proxy_http_version 1.1; 100 | proxy_set_header Upgrade $http_upgrade; 101 | proxy_set_header Connection "upgrade"; 102 | proxy_read_timeout 600s; 103 | } 104 | 105 | location /tmp { 106 | root /home/liberama/.liberama/public-files; 107 | 108 | types { } default_type "application/xml; charset=utf-8"; 109 | add_header Content-Encoding gzip; 110 | try_files $uri @liberama; 111 | } 112 | 113 | location /upload { 114 | root /home/liberama/.liberama/public-files; 115 | 116 | try_files $uri @liberama; 117 | } 118 | 119 | location / { 120 | root /home/liberama/.liberama/public; 121 | 122 | location ~* \.(?:manifest|appcache|html)$ { 123 | expires -1; 124 | } 125 | } 126 | } 127 | 128 | server { 129 | listen 23480; 130 | server_name flibusta_proxy; 131 | 132 | valid_referers liberama.top b.liberama.top; 133 | 134 | if ($invalid_referer) { 135 | return 403; 136 | } 137 | 138 | location / { 139 | proxy_pass http://flibusta.is; 140 | proxy_redirect http://static.flibusta.is:443 http://b.liberama.top:23481; 141 | } 142 | } 143 | 144 | server { 145 | listen 23481; 146 | server_name flibusta_proxy_static; 147 | 148 | valid_referers liberama.top b.liberama.top; 149 | 150 | if ($invalid_referer) { 151 | return 403; 152 | } 153 | 154 | location / { 155 | proxy_pass http://static.flibusta.is:443; 156 | proxy_set_header Referer ""; 157 | } 158 | } 159 | 160 | server { 161 | listen 23580; 162 | server_name fw_proxy; 163 | 164 | valid_referers liberama.top b.liberama.top; 165 | 166 | if ($invalid_referer) { 167 | return 403; 168 | } 169 | 170 | location / { 171 | proxy_pass http://fantasy-worlds.org; 172 | proxy_hide_header x-frame-options; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /server/core/Zip/ZipStreamer.js: -------------------------------------------------------------------------------- 1 | /*const fs = require('fs-extra'); 2 | const path = require('path'); 3 | 4 | const zipStream = require('zip-stream');*/ 5 | const StreamUnzip = require('./node_stream_zip_changed'); 6 | 7 | class ZipStreamer { 8 | constructor() { 9 | } 10 | 11 | //TODO: сделать рекурсивный обход директорий, пока только файлы 12 | //files = ['filename', 'dirname/'] 13 | /* zip-stream 2.1.3 => 4.1.0 Актуализировать! 14 | pack(zipFile, files, options, entryCallback) { 15 | return new Promise((resolve, reject) => { (async() => { 16 | entryCallback = (entryCallback ? entryCallback : () => {}); 17 | const zip = new zipStream(options); 18 | 19 | const outputStream = fs.createWriteStream(zipFile); 20 | 21 | outputStream.on('error', reject); 22 | outputStream.on('finish', async() => { 23 | let file = {path: zipFile}; 24 | try { 25 | file.size = (await fs.stat(zipFile)).size; 26 | } catch (e) { 27 | reject(e); 28 | } 29 | resolve(file); 30 | }); 31 | 32 | zip.on('error', reject); 33 | zip.pipe(outputStream); 34 | 35 | const zipAddEntry = (filename) => { 36 | return new Promise((resolve, reject) => { 37 | const basename = path.basename(filename); 38 | const source = fs.createReadStream(filename); 39 | 40 | zip.entry(source, {name: basename}, (err, entry) => { 41 | if (err) reject(err); 42 | resolve(entry); 43 | }); 44 | }); 45 | }; 46 | 47 | for (const filename of files) { 48 | const entry = await zipAddEntry(filename); 49 | entryCallback({path: entry.name, size: entry.size, compressedSize: entry.csize}); 50 | } 51 | 52 | zip.finish(); 53 | })().catch(reject); }); 54 | } 55 | */ 56 | 57 | unpack(zipFile, outputDir, options, entryCallback) { 58 | return new Promise((resolve, reject) => { 59 | entryCallback = (entryCallback ? entryCallback : () => {}); 60 | const { 61 | limitFileSize = 0, 62 | limitFileCount = 0, 63 | decodeEntryNameCallback = false, 64 | } = options; 65 | 66 | const unzip = new StreamUnzip({file: zipFile, skipEntryNameValidation: true}); 67 | 68 | unzip.on('error', reject); 69 | 70 | let files = []; 71 | unzip.on('extract', (en) => { 72 | const entry = {path: en.name, size: en.size, compressedSize: en.compressedSize}; 73 | entryCallback(entry); 74 | files.push(entry); 75 | }); 76 | 77 | unzip.on('ready', () => { 78 | if (limitFileCount || limitFileSize || decodeEntryNameCallback) { 79 | const entries = Object.values(unzip.entries()); 80 | if (limitFileCount && entries.length > limitFileCount) { 81 | reject(new Error('Слишком много файлов')); 82 | return; 83 | } 84 | 85 | for (const entry of entries) { 86 | if (limitFileSize && !entry.isDirectory && entry.size > limitFileSize) { 87 | reject(new Error('Файл слишком большой')); 88 | return; 89 | } 90 | 91 | if (decodeEntryNameCallback) { 92 | entry.name = (decodeEntryNameCallback(entry.nameRaw)).toString(); 93 | } 94 | } 95 | } 96 | 97 | unzip.extract(null, outputDir, (err) => { 98 | if (err) { 99 | reject(err); 100 | return; 101 | } 102 | try { 103 | unzip.close(); 104 | resolve(files); 105 | } catch (e) { 106 | reject(e); 107 | } 108 | }); 109 | }); 110 | }); 111 | } 112 | 113 | } 114 | 115 | module.exports = ZipStreamer; -------------------------------------------------------------------------------- /server/core/Reader/BookConverter/ConvertPdfImages.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const utils = require('../../utils'); 4 | 5 | const sax = require('../../sax'); 6 | 7 | const ConvertJpegPng = require('./ConvertJpegPng'); 8 | 9 | class ConvertPdfImages extends ConvertJpegPng { 10 | check(data, opts) { 11 | const {inputFiles} = opts; 12 | 13 | return this.config.useExternalBookConverter && 14 | inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'pdf'; 15 | } 16 | 17 | async run(data, opts) { 18 | if (!this.check(data, opts)) 19 | return false; 20 | 21 | let {inputFiles, callback, abort, pdfQuality} = opts; 22 | 23 | pdfQuality = (pdfQuality && pdfQuality <= 100 && pdfQuality >= 10 ? pdfQuality : 20); 24 | 25 | const pdftoppmPath = '/usr/bin/pdftoppm'; 26 | if (!await fs.pathExists(pdftoppmPath)) 27 | throw new Error('Внешний конвертер pdftoppm не найден'); 28 | 29 | const pdftohtmlPath = '/usr/bin/pdftohtml'; 30 | if (!await fs.pathExists(pdftohtmlPath)) 31 | throw new Error('Внешний конвертер pdftohtml не найден'); 32 | 33 | const inpFile = inputFiles.sourceFile; 34 | const dir = `${inputFiles.filesDir}/`; 35 | const outBasename = `${dir}${utils.randomHexString(10)}`; 36 | const outFile = `${outBasename}.tmp`; 37 | 38 | //конвертируем в jpeg 39 | let perc = 0; 40 | await this.execConverter(pdftoppmPath, ['-jpeg', '-jpegopt', `quality=${pdfQuality},progressive=y`, inpFile, outFile], () => { 41 | perc = (perc < 100 ? perc + 1 : 40); 42 | callback(perc); 43 | }, abort); 44 | 45 | const limitSize = 2*this.config.maxUploadFileSize; 46 | let jpgFilesSize = 0; 47 | 48 | //ищем изображения 49 | let files = []; 50 | await utils.findFiles(async(file) => { 51 | if (path.extname(file) == '.jpg') { 52 | jpgFilesSize += (await fs.stat(file)).size; 53 | if (jpgFilesSize > limitSize) { 54 | throw new Error(`Файл для конвертирования слишком большой|FORLOG| jpgFilesSize: ${jpgFilesSize} > ${limitSize}`); 55 | } 56 | 57 | files.push({name: file, base: path.basename(file)}); 58 | } 59 | }, dir); 60 | 61 | files.sort((a, b) => a.base.localeCompare(b.base)); 62 | 63 | //схема документа (outline) 64 | const outXml = `${outBasename}.xml`; 65 | await this.execConverter(pdftohtmlPath, ['-nodrm', '-i', '-c', '-s', '-xml', inpFile, outXml], null, abort); 66 | const outline = []; 67 | 68 | let inOutline = 0; 69 | let inItem = false; 70 | let pageNum = 0; 71 | 72 | const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars 73 | if (inOutline > 0 && inItem && pageNum) { 74 | outline[pageNum] = text; 75 | } 76 | }; 77 | 78 | const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars 79 | if (tag == 'outline') 80 | inOutline++; 81 | 82 | if (inOutline > 0 && tag == 'item') { 83 | const attrs = sax.getAttrsSync(tail); 84 | pageNum = (attrs.page && attrs.page.value ? attrs.page.value : 0); 85 | inItem = true; 86 | } 87 | }; 88 | 89 | const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars 90 | if (tag == 'outline') 91 | inOutline--; 92 | if (tag == 'item') 93 | inItem = false; 94 | }; 95 | 96 | const dataXml = await fs.readFile(outXml); 97 | const buf = this.decode(dataXml).toString(); 98 | sax.parseSync(buf, { 99 | onStartNode, onEndNode, onTextNode 100 | }); 101 | 102 | 103 | await utils.sleep(100); 104 | //формируем список файлов 105 | let i = 0; 106 | const imageFiles = files.map(f => { 107 | i++; 108 | let alt = (outline[i] ? outline[i] : ''); 109 | return {src: f.name, alt}; 110 | }); 111 | return await super.run(data, Object.assign({}, opts, {imageFiles})); 112 | } 113 | } 114 | 115 | module.exports = ConvertPdfImages; 116 | -------------------------------------------------------------------------------- /server/core/utils.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | const crypto = require('crypto'); 5 | const baseX = require('base-x'); 6 | const pidusage = require('pidusage'); 7 | 8 | const BASE36 = '0123456789abcdefghijklmnopqrstuvwxyz'; 9 | const bs36 = baseX(BASE36); 10 | 11 | function toBase36(data) { 12 | return bs36.encode(Buffer.from(data)); 13 | } 14 | 15 | function fromBase36(data) { 16 | return Buffer.from(bs36.decode(data)); 17 | } 18 | 19 | function bufferRemoveZeroes(buf) { 20 | const i = buf.indexOf(0); 21 | if (i >= 0) { 22 | return buf.slice(0, i); 23 | } 24 | return buf; 25 | } 26 | 27 | function getFileHash(filename, hashName, enc) { 28 | return new Promise((resolve, reject) => { 29 | const hash = crypto.createHash(hashName); 30 | const rs = fs.createReadStream(filename); 31 | rs.on('error', reject); 32 | rs.on('data', chunk => hash.update(chunk)); 33 | rs.on('end', () => resolve(hash.digest(enc))); 34 | }); 35 | } 36 | 37 | function getBufHash(buf, hashName, enc) { 38 | const hash = crypto.createHash(hashName); 39 | hash.update(buf); 40 | return hash.digest(enc); 41 | } 42 | 43 | function sleep(ms) { 44 | return new Promise(resolve => setTimeout(resolve, ms)); 45 | } 46 | 47 | function toUnixTime(time) { 48 | return parseInt(time/1000); 49 | } 50 | 51 | function randomHexString(len) { 52 | return crypto.randomBytes(len).toString('hex') 53 | } 54 | 55 | async function touchFile(filename) { 56 | await fs.utimes(filename, Date.now()/1000, Date.now()/1000); 57 | } 58 | 59 | function spawnProcess(cmd, opts) { 60 | let {args, killAfter, onData, onUsage, onUsageInterval, abort} = opts; 61 | killAfter = (killAfter ? killAfter : 120);//seconds 62 | onData = (onData ? onData : () => {}); 63 | args = (args ? args : []); 64 | onUsageInterval = (onUsageInterval ? onUsageInterval : 30);//seconds 65 | 66 | return new Promise((resolve, reject) => { (async() => { 67 | let resolved = false; 68 | const proc = spawn(cmd, args, {detached: true}); 69 | 70 | let stdout = ''; 71 | proc.stdout.on('data', (data) => { 72 | stdout += data; 73 | onData(data); 74 | }); 75 | 76 | let stderr = ''; 77 | proc.stderr.on('data', (data) => { 78 | stderr += data; 79 | onData(data); 80 | }); 81 | 82 | proc.on('close', (code) => { 83 | resolved = true; 84 | resolve({status: 'close', code, stdout, stderr}); 85 | }); 86 | 87 | proc.on('error', (error) => { 88 | reject({status: 'error', error, stdout, stderr}); 89 | }); 90 | 91 | //ждем процесс, контролируем его работу раз в секунду 92 | let onUsageCounter = onUsageInterval; 93 | while (!resolved) { 94 | await sleep(1000); 95 | 96 | onUsageCounter--; 97 | if (onUsage && onUsageCounter <= 0) { 98 | const stats = await pidusage(proc.pid); 99 | onUsage(stats); 100 | onUsageCounter = onUsageInterval; 101 | } 102 | 103 | killAfter--; 104 | if (killAfter <= 0 || (abort && abort())) { 105 | process.kill(proc.pid); 106 | if (killAfter <= 0) { 107 | reject({status: 'killed', stdout, stderr}); 108 | } else { 109 | reject({status: 'abort', stdout, stderr}); 110 | } 111 | break; 112 | } 113 | } 114 | })().catch(reject); }); 115 | } 116 | 117 | async function findFiles(callback, dir) { 118 | if (!(callback && dir)) 119 | return; 120 | let result = true; 121 | const files = await fs.readdir(dir, { withFileTypes: true }); 122 | 123 | for (const file of files) { 124 | const found = path.resolve(dir, file.name); 125 | if (file.isDirectory()) 126 | result = await findFiles(callback, found); 127 | else 128 | await callback(found); 129 | } 130 | return result; 131 | } 132 | 133 | module.exports = { 134 | toBase36, 135 | fromBase36, 136 | bufferRemoveZeroes, 137 | getFileHash, 138 | getBufHash, 139 | sleep, 140 | toUnixTime, 141 | randomHexString, 142 | touchFile, 143 | spawnProcess, 144 | findFiles 145 | }; -------------------------------------------------------------------------------- /server/core/Reader/BookConverter/ConvertBase.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const iconv = require('iconv-lite'); 3 | const he = require('he'); 4 | 5 | const LimitedQueue = require('../../LimitedQueue'); 6 | const textUtils = require('./textUtils'); 7 | const utils = require('../../utils'); 8 | const xmlParser = require('../../xmlParser'); 9 | 10 | const queue = new LimitedQueue(3, 20, 2*60*1000);//2 минуты ожидание подвижек 11 | 12 | class ConvertBase { 13 | constructor(config) { 14 | this.config = config; 15 | 16 | this.calibrePath = `${config.dataDir}/calibre/ebook-convert`; 17 | this.sofficePath = '/usr/bin/soffice'; 18 | } 19 | 20 | async run(data, opts) {// eslint-disable-line no-unused-vars 21 | //override 22 | } 23 | 24 | async checkExternalConverterPresent() { 25 | if (!await fs.pathExists(this.calibrePath)) 26 | throw new Error('Внешний конвертер calibre не найден'); 27 | 28 | if (!await fs.pathExists(this.sofficePath)) 29 | throw new Error('Внешний конвертер LibreOffice не найден'); 30 | } 31 | 32 | async execConverter(path, args, onData, abort) { 33 | onData = (onData ? onData : () => {}); 34 | 35 | let q = null; 36 | try { 37 | q = await queue.get(() => {onData();}); 38 | } catch (e) { 39 | throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.'); 40 | } 41 | 42 | abort = (abort ? abort : () => false); 43 | const myAbort = () => { 44 | return q.abort() || abort(); 45 | } 46 | 47 | try { 48 | if (myAbort()) 49 | throw new Error('abort'); 50 | 51 | const result = await utils.spawnProcess(path, { 52 | killAfter: 3600,//1 час 53 | args, 54 | onData: (data) => { 55 | if (queue.freed > 0) 56 | q.resetTimeout(); 57 | onData(data); 58 | }, 59 | //будем периодически проверять работу конвертера и если очереди нет, то разрешаем работу пинком onData 60 | onUsage: (stats) => { 61 | if (queue.freed > 0 && stats.cpu >= 10) { 62 | q.resetTimeout(); 63 | onData('.'); 64 | } 65 | }, 66 | onUsageInterval: 10, 67 | abort: myAbort 68 | }); 69 | if (result.code != 0) { 70 | const error = `${result.code}|FORLOG|, exec: ${path}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`; 71 | throw new Error(`Внешний конвертер завершился с ошибкой: ${error}`); 72 | } 73 | return result; 74 | } catch(e) { 75 | if (e.status == 'killed') { 76 | throw new Error('Слишком долгое ожидание конвертера'); 77 | } else if (e.status == 'abort') { 78 | throw new Error('abort'); 79 | } else if (e.status == 'error') { 80 | throw new Error(e.error); 81 | } else { 82 | throw new Error(e); 83 | } 84 | } finally { 85 | q.ret(); 86 | } 87 | } 88 | 89 | decode(data) { 90 | let selected = textUtils.getEncoding(data); 91 | 92 | if (selected.toLowerCase() != 'utf-8') 93 | return iconv.decode(data, selected); 94 | else 95 | return data; 96 | } 97 | 98 | repSpaces(text) { 99 | return text.replace(/ |[\t\n\r]/g, ' '); 100 | } 101 | 102 | escapeEntities(text) { 103 | return he.escape(he.decode(text.replace(/ /g, ' '))); 104 | } 105 | 106 | isDataXml(data) { 107 | const str = data.slice(0, 100).toString().trim(); 108 | return (str.indexOf('\s*?<\/p>/g, ''); 120 | } 121 | } 122 | 123 | module.exports = ConvertBase; -------------------------------------------------------------------------------- /server/core/FileDownloader.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | const axios = require('axios'); 3 | const utils = require('./utils'); 4 | 5 | const userAgent = 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0'; 6 | 7 | class FileDownloader { 8 | constructor(limitDownloadSize = 0) { 9 | this.limitDownloadSize = limitDownloadSize; 10 | } 11 | 12 | async load(url, opts, callback, abort) { 13 | let errMes = ''; 14 | 15 | let options = { 16 | headers: { 17 | 'accept-encoding': 'gzip, compress, deflate', 18 | 'user-agent': userAgent, 19 | }, 20 | httpsAgent: new https.Agent({ 21 | rejectUnauthorized: false // решение проблемы 'unable to verify the first certificate' для некоторых сайтов с валидным сертификатом 22 | }), 23 | responseType: 'stream', 24 | }; 25 | if (opts) 26 | options = Object.assign({}, opts, options); 27 | 28 | if (!options.timeout) 29 | options.timeout = 300*1000;//5 min 30 | 31 | try { 32 | const res = await axios.get(url, options); 33 | 34 | let estSize = 0; 35 | if (res.headers['content-length']) { 36 | estSize = res.headers['content-length']; 37 | } 38 | 39 | if (this.limitDownloadSize && estSize > this.limitDownloadSize) { 40 | throw new Error('Файл слишком большой'); 41 | } 42 | 43 | let prevProg = 0; 44 | let transferred = 0; 45 | 46 | const download = this.streamToBuffer(res.data, (chunk) => { 47 | transferred += chunk.length; 48 | if (this.limitDownloadSize) { 49 | if (transferred > this.limitDownloadSize) { 50 | errMes = 'Файл слишком большой'; 51 | res.request.abort(); 52 | } 53 | } 54 | 55 | let prog = 0; 56 | if (estSize) 57 | prog = Math.round(transferred/estSize*100); 58 | else 59 | prog = Math.round(transferred/(transferred + 200000)*100); 60 | 61 | if (prog != prevProg && callback) 62 | callback(prog); 63 | prevProg = prog; 64 | 65 | if (abort && abort()) { 66 | errMes = 'abort'; 67 | res.request.abort(); 68 | } 69 | }); 70 | 71 | return await download; 72 | } catch (error) { 73 | errMes = (errMes ? errMes : error.message); 74 | throw new Error(errMes); 75 | } 76 | } 77 | 78 | async head(url) { 79 | const options = { 80 | headers: { 81 | 'user-agent': userAgent, 82 | }, 83 | timeout: 10*1000, 84 | }; 85 | 86 | const res = await axios.head(url, options); 87 | return res.headers; 88 | } 89 | 90 | streamToBuffer(stream, progress, timeout = 30*1000) { 91 | return new Promise((resolve, reject) => { 92 | 93 | if (!progress) 94 | progress = () => {}; 95 | 96 | const _buf = []; 97 | let resolved = false; 98 | let timer = 0; 99 | 100 | stream.on('data', (chunk) => { 101 | timer = 0; 102 | _buf.push(chunk); 103 | progress(chunk); 104 | }); 105 | stream.on('end', () => { 106 | resolved = true; 107 | timer = timeout; 108 | resolve(Buffer.concat(_buf)); 109 | }); 110 | stream.on('error', (err) => { 111 | reject(err); 112 | }); 113 | stream.on('aborted', () => { 114 | reject(new Error('aborted')); 115 | }); 116 | 117 | //бодяга с timer и timeout, чтобы гарантировать отсутствие зависания по каким-либо причинам 118 | (async() => { 119 | while (timer < timeout) { 120 | await utils.sleep(1000); 121 | timer += 1000; 122 | } 123 | if (!resolved) 124 | reject(new Error('FileDownloader: timed out')) 125 | })(); 126 | }); 127 | } 128 | } 129 | 130 | module.exports = FileDownloader; 131 | -------------------------------------------------------------------------------- /server/core/xmlParser.js: -------------------------------------------------------------------------------- 1 | const sax = require('./sax'); 2 | 3 | function formatXml(xmlParsed, encoding = 'utf-8', textFilterFunc) { 4 | let out = ``; 5 | out += formatXmlNode(xmlParsed, textFilterFunc); 6 | return out; 7 | } 8 | 9 | function formatXmlNode(node, textFilterFunc) { 10 | textFilterFunc = (textFilterFunc ? textFilterFunc : text => text); 11 | 12 | const formatNode = (node, name) => { 13 | let out = ''; 14 | 15 | if (Array.isArray(node)) { 16 | for (const n of node) { 17 | out += formatNode(n); 18 | } 19 | } else if (typeof node == 'string') { 20 | if (name) 21 | out += `<${name}>${textFilterFunc(node)}`; 22 | else 23 | out += textFilterFunc(node); 24 | } else { 25 | if (node._n) 26 | name = node._n; 27 | 28 | let attrs = ''; 29 | if (node._attrs) { 30 | for (let attrName in node._attrs) { 31 | attrs += ` ${attrName}="${node._attrs[attrName]}"`; 32 | } 33 | } 34 | 35 | let tOpen = ''; 36 | let tBody = ''; 37 | let tClose = ''; 38 | if (name) 39 | tOpen += `<${name}${attrs}>`; 40 | if (node.hasOwnProperty('_t')) 41 | tBody += textFilterFunc(node._t); 42 | 43 | for (let nodeName in node) { 44 | if (nodeName && nodeName[0] == '_' && nodeName != '_a') 45 | continue; 46 | 47 | const n = node[nodeName]; 48 | tBody += formatNode(n, nodeName); 49 | } 50 | 51 | if (name) 52 | tClose += ``; 53 | 54 | out += `${tOpen}${tBody}${tClose}`; 55 | } 56 | return out; 57 | } 58 | 59 | return formatNode(node); 60 | } 61 | 62 | function parseXml(xmlString, lowerCase = true) { 63 | let result = {}; 64 | let node = result; 65 | 66 | const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars 67 | node._t = text; 68 | }; 69 | 70 | const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars 71 | if (tag == '?xml') 72 | return; 73 | 74 | const newNode = {_n: tag, _p: node}; 75 | 76 | if (tail) { 77 | const parsedAttrs = sax.getAttrsSync(tail, lowerCase); 78 | const atKeys = Object.keys(parsedAttrs); 79 | if (atKeys.length) { 80 | const attrs = {}; 81 | for (let i = 0; i < atKeys.length; i++) { 82 | const attrName = atKeys[i]; 83 | attrs[parsedAttrs[attrName].fn] = parsedAttrs[attrName].value; 84 | } 85 | 86 | newNode._attrs = attrs; 87 | } 88 | } 89 | 90 | if (!node._a) 91 | node._a = []; 92 | node._a.push(newNode); 93 | node = newNode; 94 | }; 95 | 96 | const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars 97 | if (node._p && node._n == tag) 98 | node = node._p; 99 | }; 100 | 101 | sax.parseSync(xmlString, { 102 | onStartNode, onEndNode, onTextNode, lowerCase 103 | }); 104 | 105 | if (result._a) 106 | result = result._a[0]; 107 | 108 | return result; 109 | } 110 | 111 | function simplifyXmlParsed(node) { 112 | 113 | const simplifyNodeArray = (a) => { 114 | const result = {}; 115 | 116 | for (let i = 0; i < a.length; i++) { 117 | const child = a[i]; 118 | if (child._n && !result[child._n]) { 119 | result[child._n] = {}; 120 | if (child._a) { 121 | result[child._n] = simplifyNodeArray(child._a); 122 | } 123 | if (child._t) { 124 | result[child._n]._t = child._t; 125 | } 126 | if (child._attrs) { 127 | result[child._n]._attrs = child._attrs; 128 | } 129 | } 130 | } 131 | 132 | return result; 133 | }; 134 | 135 | return simplifyNodeArray([node]); 136 | } 137 | 138 | module.exports = { 139 | formatXml, 140 | formatXmlNode, 141 | parseXml, 142 | simplifyXmlParsed 143 | } -------------------------------------------------------------------------------- /client/components/Reader/LibsPage/LibsPage.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 138 | 139 | 145 | --------------------------------------------------------------------------------