diff --git a/cps.py b/cps.py index 184bee0a..f71c60c4 100755 --- a/cps.py +++ b/cps.py @@ -17,14 +17,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os +from __future__ import absolute_import, division, print_function, unicode_literals import sys +import os + -base_path = os.path.dirname(os.path.abspath(__file__)) # Insert local directories into path -sys.path.append(base_path) -sys.path.append(os.path.join(base_path, 'cps')) -sys.path.append(os.path.join(base_path, 'vendor')) +sys.path.append(os.path.join(sys.path[0], 'vendor')) + from cps import create_app from cps.opds import opds diff --git a/cps/__init__.py b/cps/__init__.py index dfe72e5c..3b35570b 100755 --- a/cps/__init__.py +++ b/cps/__init__.py @@ -20,29 +20,28 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -__all__ =['app'] -import mimetypes -from flask import Flask, request, g -from flask_login import LoginManager -from flask_babel import Babel -import cache_buster -from reverseproxy import ReverseProxied -import logging -from logging.handlers import RotatingFileHandler -from flask_principal import Principal -from babel.core import UnknownLocaleError -from babel import Locale as LC -from babel import negotiate_locale -import os -import ub +from __future__ import division, print_function, unicode_literals import sys -from ub import Config, Settings +import os +import mimetypes try: import cPickle except ImportError: import pickle as cPickle +from babel import Locale as LC +from babel import negotiate_locale +from babel.core import UnknownLocaleError +from flask import Flask, request, g +from flask_login import LoginManager +from flask_babel import Babel +from flask_principal import Principal + +from . import logger, cache_buster, ub +from .constants import TRANSLATIONS_DIR as _TRANSLATIONS_DIR +from .reverseproxy import ReverseProxied + mimetypes.init() mimetypes.add_type('application/xhtml+xml', '.xhtml') @@ -70,12 +69,11 @@ lm.anonymous_user = ub.Anonymous ub.init_db() -config = Config() - +config = ub.Config() from . import db try: - with open(os.path.join(config.get_main_dir, 'cps/translations/iso639.pickle'), 'rb') as f: + with open(os.path.join(_TRANSLATIONS_DIR, 'iso639.pickle'), 'rb') as f: language_table = cPickle.load(f) except cPickle.UnpicklingError as error: # app.logger.error("Can't read file cps/translations/iso639.pickle: %s", error) @@ -91,24 +89,14 @@ from .server import server Server = server() babel = Babel() +log = logger.create() + def create_app(): app.wsgi_app = ReverseProxied(app.wsgi_app) cache_buster.init_cache_busting(app) - formatter = logging.Formatter( - "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s") - try: - file_handler = RotatingFileHandler(config.get_config_logfile(), maxBytes=50000, backupCount=2) - except IOError: - file_handler = RotatingFileHandler(os.path.join(config.get_main_dir, "calibre-web.log"), - maxBytes=50000, backupCount=2) - # ToDo: reset logfile value in config class - file_handler.setFormatter(formatter) - app.logger.addHandler(file_handler) - app.logger.setLevel(config.config_log_level) - - app.logger.info('Starting Calibre Web...') + log.info('Starting Calibre Web...') Principal(app) lm.init_app(app) app.secret_key = os.getenv('SECRET_KEY', 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT') @@ -132,7 +120,7 @@ def get_locale(): try: preferred.append(str(LC.parse(x.replace('-', '_')))) except (UnknownLocaleError, ValueError) as e: - app.logger.debug("Could not parse locale: %s", e) + log.warning('Could not parse locale "%s": %s', x, e) preferred.append('en') return negotiate_locale(preferred, translations) @@ -145,3 +133,6 @@ def get_timezone(): from .updater import Updater updater_thread = Updater() + + +__all__ = ['app'] diff --git a/cps/about.py b/cps/about.py index 59623f16..bc7b0e8a 100644 --- a/cps/about.py +++ b/cps/about.py @@ -21,29 +21,30 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from flask import Blueprint -from flask_login import login_required -from . import db +from __future__ import division, print_function, unicode_literals import sys -from .uploader import get_versions -from babel import __version__ as babelVersion -from sqlalchemy import __version__ as sqlalchemyVersion -from flask_principal import __version__ as flask_principalVersion -from iso639 import __version__ as iso639Version -from pytz import __version__ as pytzVersion -from flask import __version__ as flaskVersion -from werkzeug import __version__ as werkzeugVersion -from jinja2 import __version__ as jinja2Version -from .converter import versioncheck -from flask_babel import gettext as _ -from cps import Server import requests -from .web import render_title_template +from flask import Blueprint +from flask import __version__ as flaskVersion +from flask_babel import gettext as _ +from flask_principal import __version__ as flask_principalVersion +from flask_login import login_required try: from flask_login import __version__ as flask_loginVersion except ImportError: from flask_login.__about__ import __version__ as flask_loginVersion +from werkzeug import __version__ as werkzeugVersion + +from babel import __version__ as babelVersion +from jinja2 import __version__ as jinja2Version +from pytz import __version__ as pytzVersion +from sqlalchemy import __version__ as sqlalchemyVersion + +from . import db, converter, Server, uploader +from .isoLanguages import __version__ as iso639Version +from .web import render_title_template + about = Blueprint('about', __name__) @@ -55,7 +56,7 @@ def stats(): authors = db.session.query(db.Authors).count() categorys = db.session.query(db.Tags).count() series = db.session.query(db.Series).count() - versions = get_versions() + versions = uploader.get_versions() versions['Babel'] = 'v' + babelVersion versions['Sqlalchemy'] = 'v' + sqlalchemyVersion versions['Werkzeug'] = 'v' + werkzeugVersion @@ -69,7 +70,7 @@ def stats(): versions['Requests'] = 'v' + requests.__version__ versions['pySqlite'] = 'v' + db.engine.dialect.dbapi.version versions['Sqlite'] = 'v' + db.engine.dialect.dbapi.sqlite_version - versions.update(versioncheck()) + versions.update(converter.versioncheck()) versions.update(Server.getNameVersion()) versions['Python'] = sys.version return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=versions, diff --git a/cps/admin.py b/cps/admin.py index 305dd9e3..85088336 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -21,29 +21,32 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, print_function, unicode_literals import os -from flask import Blueprint, flash, redirect, url_for -from flask import abort, request, make_response -from flask_login import login_required, current_user, logout_user -from .web import admin_required, render_title_template, before_request, unconfigured, \ - login_required_if_no_ano -from . import db, ub, Server, get_locale, config, app, updater_thread, babel import json -from datetime import datetime, timedelta import time -from babel.dates import format_datetime -from flask_babel import gettext as _ -from babel import Locale as LC -from sqlalchemy.exc import IntegrityError -from .gdriveutils import is_gdrive_ready, gdrive_support, downloadFile, deleteDatabaseOnChange, listRootFolders -from .helper import speaking_language, check_valid_domain, check_unrar, send_test_mail, generate_random_password, \ - send_registration_mail -from werkzeug.security import generate_password_hash +from datetime import datetime, timedelta try: from imp import reload except ImportError: pass +from babel import Locale as LC +from babel.dates import format_datetime +from flask import Blueprint, flash, redirect, url_for, abort, request, make_response +from flask_login import login_required, current_user, logout_user +from flask_babel import gettext as _ +from sqlalchemy import and_ +from sqlalchemy.exc import IntegrityError +from werkzeug.security import generate_password_hash + +from . import constants, logger +from . import db, ub, Server, get_locale, config, updater_thread, babel, gdriveutils +from .helper import speaking_language, check_valid_domain, check_unrar, send_test_mail, generate_random_password, \ + send_registration_mail +from .gdriveutils import is_gdrive_ready, gdrive_support, downloadFile, deleteDatabaseOnChange, listRootFolders +from .web import admin_required, render_title_template, before_request, unconfigured, login_required_if_no_ano + feature_support = dict() try: from goodreads.client import GoodreadsClient @@ -51,11 +54,11 @@ try: except ImportError: feature_support['goodreads'] = False -try: - import rarfile - feature_support['rar'] = True -except ImportError: - feature_support['rar'] = False +# try: +# import rarfile +# feature_support['rar'] = True +# except ImportError: +# feature_support['rar'] = False try: import ldap @@ -70,8 +73,10 @@ except ImportError: feature_support['oauth'] = False oauth_check = {} + feature_support['gdrive'] = gdrive_support admi = Blueprint('admin', __name__) +log = logger.create() @admi.route("/admin") @@ -174,7 +179,7 @@ def view_configuration(): if "config_mature_content_tags" in to_save: content.config_mature_content_tags = to_save["config_mature_content_tags"].strip() if "Show_mature_content" in to_save: - content.config_default_show = content.config_default_show + ub.MATURE_CONTENT + content.config_default_show |= constants.MATURE_CONTENT if "config_authors_max" in to_save: content.config_authors_max = int(to_save["config_authors_max"]) @@ -182,26 +187,26 @@ def view_configuration(): # Default user configuration content.config_default_role = 0 if "admin_role" in to_save: - content.config_default_role = content.config_default_role + ub.ROLE_ADMIN + content.config_default_role |= constants.ROLE_ADMIN if "download_role" in to_save: - content.config_default_role = content.config_default_role + ub.ROLE_DOWNLOAD + content.config_default_role |= constants.ROLE_DOWNLOAD if "viewer_role" in to_save: - content.config_default_role = content.config_default_role + ub.ROLE_VIEWER + content.config_default_role |= constants.ROLE_VIEWER if "upload_role" in to_save: - content.config_default_role = content.config_default_role + ub.ROLE_UPLOAD + content.config_default_role |= constants.ROLE_UPLOAD if "edit_role" in to_save: - content.config_default_role = content.config_default_role + ub.ROLE_EDIT + content.config_default_role |= constants.ROLE_EDIT if "delete_role" in to_save: - content.config_default_role = content.config_default_role + ub.ROLE_DELETE_BOOKS + content.config_default_role |= constants.ROLE_DELETE_BOOKS if "passwd_role" in to_save: - content.config_default_role = content.config_default_role + ub.ROLE_PASSWD + content.config_default_role |= constants.ROLE_PASSWD if "edit_shelf_role" in to_save: - content.config_default_role = content.config_default_role + ub.ROLE_EDIT_SHELFS + content.config_default_role |= constants.ROLE_EDIT_SHELFS val = 0 for key,v in to_save.items(): if key.startswith('show'): - val += int(key[5:]) + val |= int(key[5:]) content.config_default_show = val ub.session.commit() @@ -215,9 +220,9 @@ def view_configuration(): # stop Server Server.setRestartTyp(True) Server.stopServer() - app.logger.info('Reboot required, restarting') + log.info('Reboot required, restarting') readColumn = db.session.query(db.Custom_Columns)\ - .filter(db.and_(db.Custom_Columns.datatype == 'bool',db.Custom_Columns.mark_for_delete == 0)).all() + .filter(and_(db.Custom_Columns.datatype == 'bool',db.Custom_Columns.mark_for_delete == 0)).all() return render_title_template("config_view_edit.html", conf=config, readColumns=readColumn, title=_(u"UI Configuration"), page="uiconfig") @@ -294,10 +299,10 @@ def configuration_helper(origin): if not feature_support['gdrive']: gdriveError = _('Import of optional Google Drive requirements missing') else: - if not os.path.isfile(os.path.join(config.get_main_dir, 'client_secrets.json')): + if not os.path.isfile(gdriveutils.CLIENT_SECRETS): gdriveError = _('client_secrets.json is missing or not readable') else: - with open(os.path.join(config.get_main_dir, 'client_secrets.json'), 'r') as settings: + with open(gdriveutils.CLIENT_SECRETS, 'r') as settings: filedata = json.load(settings) if 'web' not in filedata: gdriveError = _('client_secrets.json is not configured for web application') @@ -309,13 +314,13 @@ def configuration_helper(origin): content.config_calibre_dir = to_save["config_calibre_dir"] db_change = True # Google drive setup - if not os.path.isfile(os.path.join(config.get_main_dir, 'settings.yaml')): + if not os.path.isfile(gdriveutils.SETTINGS_YAML): content.config_use_google_drive = False if "config_use_google_drive" in to_save and not content.config_use_google_drive and not gdriveError: if filedata: if filedata['web']['redirect_uris'][0].endswith('/'): filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-1] - with open(os.path.join(config.get_main_dir, 'settings.yaml'), 'w') as f: + with open(gdriveutils.SETTINGS_YAML, 'w') as f: yaml = "client_config_backend: settings\nclient_config_file: %(client_file)s\n" \ "client_config:\n" \ " client_id: %(client_id)s\n client_secret: %(client_secret)s\n" \ @@ -323,11 +328,11 @@ def configuration_helper(origin): "save_credentials_backend: file\nsave_credentials_file: %(credential)s\n\n" \ "get_refresh_token: True\n\noauth_scope:\n" \ " - https://www.googleapis.com/auth/drive\n" - f.write(yaml % {'client_file': os.path.join(config.get_main_dir, 'client_secrets.json'), + f.write(yaml % {'client_file': gdriveutils.CLIENT_SECRETS, 'client_id': filedata['web']['client_id'], 'client_secret': filedata['web']['client_secret'], 'redirect_uri': filedata['web']['redirect_uris'][0], - 'credential': os.path.join(config.get_main_dir, 'gdrive_credentials')}) + 'credential': gdriveutils.CREDENTIALS}) else: flash(_(u'client_secrets.json is not configured for web application'), category="error") return render_title_template("config_edit.html", config=config, origin=origin, @@ -397,7 +402,7 @@ def configuration_helper(origin): gdriveError=gdriveError, feature_support=feature_support, title=_(u"Basic Configuration"), page="config") else: - content.config_login_type = ub.LOGIN_LDAP + content.config_login_type = constants.LOGIN_LDAP content.config_ldap_provider_url = to_save["config_ldap_provider_url"] content.config_ldap_dn = to_save["config_ldap_dn"] db_change = True @@ -425,7 +430,7 @@ def configuration_helper(origin): gdriveError=gdriveError, feature_support=feature_support, title=_(u"Basic Configuration"), page="config") else: - content.config_login_type = ub.LOGIN_OAUTH_GITHUB + content.config_login_type = constants.LOGIN_OAUTH_GITHUB content.config_github_oauth_client_id = to_save["config_github_oauth_client_id"] content.config_github_oauth_client_secret = to_save["config_github_oauth_client_secret"] reboot_required = True @@ -439,31 +444,25 @@ def configuration_helper(origin): gdriveError=gdriveError, feature_support=feature_support, title=_(u"Basic Configuration"), page="config") else: - content.config_login_type = ub.LOGIN_OAUTH_GOOGLE + content.config_login_type = constants.LOGIN_OAUTH_GOOGLE content.config_google_oauth_client_id = to_save["config_google_oauth_client_id"] content.config_google_oauth_client_secret = to_save["config_google_oauth_client_secret"] reboot_required = True if "config_login_type" in to_save and to_save["config_login_type"] == "0": - content.config_login_type = ub.LOGIN_STANDARD + content.config_login_type = constants.LOGIN_STANDARD if "config_log_level" in to_save: content.config_log_level = int(to_save["config_log_level"]) if content.config_logfile != to_save["config_logfile"]: # check valid path, only path or file - if os.path.dirname(to_save["config_logfile"]): - if os.path.exists(os.path.dirname(to_save["config_logfile"])) and \ - os.path.basename(to_save["config_logfile"]) and not os.path.isdir(to_save["config_logfile"]): - content.config_logfile = to_save["config_logfile"] - else: + if not logger.is_valid_logfile(to_save["config_logfile"]): ub.session.commit() flash(_(u'Logfile location is not valid, please enter correct path'), category="error") return render_title_template("config_edit.html", config=config, origin=origin, gdriveError=gdriveError, feature_support=feature_support, title=_(u"Basic Configuration"), page="config") - else: - content.config_logfile = to_save["config_logfile"] - reboot_required = True + content.config_logfile = to_save["config_logfile"] # Rarfile Content configuration if "config_rarfile_location" in to_save and to_save['config_rarfile_location'] is not u"": @@ -485,7 +484,6 @@ def configuration_helper(origin): ub.session.commit() flash(_(u"Calibre-Web configuration updated"), category="success") config.loadSettings() - app.logger.setLevel(config.config_log_level) except Exception as e: flash(e, category="error") return render_title_template("config_edit.html", config=config, origin=origin, @@ -502,7 +500,7 @@ def configuration_helper(origin): # stop Server Server.setRestartTyp(True) Server.stopServer() - app.logger.info('Reboot required, restarting') + log.info('Reboot required, restarting') if origin: success = True if is_gdrive_ready() and feature_support['gdrive'] is True: # and config.config_use_google_drive == True: @@ -536,23 +534,23 @@ def new_user(): content.sidebar_view = val if "show_detail_random" in to_save: - content.sidebar_view += ub.DETAIL_RANDOM + content.sidebar_view |= constants.DETAIL_RANDOM content.role = 0 if "admin_role" in to_save: - content.role = content.role + ub.ROLE_ADMIN + content.role |= constants.ROLE_ADMIN if "download_role" in to_save: - content.role = content.role + ub.ROLE_DOWNLOAD + content.role |= constants.ROLE_DOWNLOAD if "upload_role" in to_save: - content.role = content.role + ub.ROLE_UPLOAD + content.role |= constants.ROLE_UPLOAD if "edit_role" in to_save: - content.role = content.role + ub.ROLE_EDIT + content.role |= constants.ROLE_EDIT if "delete_role" in to_save: - content.role = content.role + ub.ROLE_DELETE_BOOKS + content.role |= constants.ROLE_DELETE_BOOKS if "passwd_role" in to_save: - content.role = content.role + ub.ROLE_PASSWD + content.role |= constants.ROLE_PASSWD if "edit_shelf_role" in to_save: - content.role = content.role + ub.ROLE_EDIT_SHELFS + content.role |= constants.ROLE_EDIT_SHELFS if not to_save["nickname"] or not to_save["email"] or not to_save["password"]: flash(_(u"Please fill out all fields!"), category="error") return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, @@ -576,7 +574,7 @@ def new_user(): else: content.role = config.config_default_role content.sidebar_view = config.config_default_show - content.mature_content = bool(config.config_default_show & ub.MATURE_CONTENT) + content.mature_content = bool(config.config_default_show & constants.MATURE_CONTENT) return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, languages=languages, title=_(u"Add new user"), page="newuser", registered_oauth=oauth_check) @@ -642,58 +640,58 @@ def edit_user(user_id): if "password" in to_save and to_save["password"]: content.password = generate_password_hash(to_save["password"]) - if "admin_role" in to_save and not content.role_admin(): - content.role = content.role + ub.ROLE_ADMIN - elif "admin_role" not in to_save and content.role_admin(): - content.role = content.role - ub.ROLE_ADMIN - - if "download_role" in to_save and not content.role_download(): - content.role = content.role + ub.ROLE_DOWNLOAD - elif "download_role" not in to_save and content.role_download(): - content.role = content.role - ub.ROLE_DOWNLOAD - - if "viewer_role" in to_save and not content.role_viewer(): - content.role = content.role + ub.ROLE_VIEWER - elif "viewer_role" not in to_save and content.role_viewer(): - content.role = content.role - ub.ROLE_VIEWER - - if "upload_role" in to_save and not content.role_upload(): - content.role = content.role + ub.ROLE_UPLOAD - elif "upload_role" not in to_save and content.role_upload(): - content.role = content.role - ub.ROLE_UPLOAD - - if "edit_role" in to_save and not content.role_edit(): - content.role = content.role + ub.ROLE_EDIT - elif "edit_role" not in to_save and content.role_edit(): - content.role = content.role - ub.ROLE_EDIT - - if "delete_role" in to_save and not content.role_delete_books(): - content.role = content.role + ub.ROLE_DELETE_BOOKS - elif "delete_role" not in to_save and content.role_delete_books(): - content.role = content.role - ub.ROLE_DELETE_BOOKS - - if "passwd_role" in to_save and not content.role_passwd(): - content.role = content.role + ub.ROLE_PASSWD - elif "passwd_role" not in to_save and content.role_passwd(): - content.role = content.role - ub.ROLE_PASSWD - - if "edit_shelf_role" in to_save and not content.role_edit_shelfs(): - content.role = content.role + ub.ROLE_EDIT_SHELFS - elif "edit_shelf_role" not in to_save and content.role_edit_shelfs(): - content.role = content.role - ub.ROLE_EDIT_SHELFS - - val = [int(k[5:]) for k, __ in to_save.items() if k.startswith('show')] + if "admin_role" in to_save: + content.role |= constants.ROLE_ADMIN + else: + content.role &= ~constants.ROLE_ADMIN + + if "download_role" in to_save: + content.role |= constants.ROLE_DOWNLOAD + else: + content.role &= ~constants.ROLE_DOWNLOAD + + if "viewer_role" in to_save: + content.role |= constants.ROLE_VIEWER + else: + content.role &= ~constants.ROLE_VIEWER + + if "upload_role" in to_save: + content.role |= constants.ROLE_UPLOAD + else: + content.role &= ~constants.ROLE_UPLOAD + + if "edit_role" in to_save: + content.role |= constants.ROLE_EDIT + else: + content.role &= ~constants.ROLE_EDIT + + if "delete_role" in to_save: + content.role |= constants.ROLE_DELETE_BOOKS + else: + content.role &= ~constants.ROLE_DELETE_BOOKS + + if "passwd_role" in to_save: + content.role |= constants.ROLE_PASSWD + else: + content.role &= ~constants.ROLE_PASSWD + + if "edit_shelf_role" in to_save: + content.role |= constants.ROLE_EDIT_SHELFS + else: + content.role &= ~constants.ROLE_EDIT_SHELFS + + val = [int(k[5:]) for k, __ in to_save.items() if k.startswith('show_')] sidebar = ub.get_sidebar_config() for element in sidebar: if element['visibility'] in val and not content.check_visibility(element['visibility']): - content.sidebar_view += element['visibility'] + content.sidebar_view |= element['visibility'] elif not element['visibility'] in val and content.check_visibility(element['visibility']): - content.sidebar_view -= element['visibility'] + content.sidebar_view &= ~element['visibility'] - if "Show_detail_random" in to_save and not content.show_detail_random(): - content.sidebar_view += ub.DETAIL_RANDOM - elif "Show_detail_random" not in to_save and content.show_detail_random(): - content.sidebar_view -= ub.DETAIL_RANDOM + if "Show_detail_random" in to_save: + content.sidebar_view |= constants.DETAIL_RANDOM + else: + content.sidebar_view &= ~constants.DETAIL_RANDOM content.mature_content = "Show_mature_content" in to_save diff --git a/cps/cache_buster.py b/cps/cache_buster.py index edd73cec..02aa7187 100644 --- a/cps/cache_buster.py +++ b/cps/cache_buster.py @@ -17,8 +17,14 @@ # Inspired by https://github.com/ChrisTM/Flask-CacheBust # Uses query strings so CSS font files are found without having to resort to absolute URLs -import hashlib +from __future__ import division, print_function, unicode_literals import os +import hashlib + +from . import logger + + +log = logger.create() def init_cache_busting(app): @@ -34,7 +40,7 @@ def init_cache_busting(app): hash_table = {} # map of file hashes - app.logger.debug('Computing cache-busting values...') + log.debug('Computing cache-busting values...') # compute file hashes for dirpath, __, filenames in os.walk(static_folder): for filename in filenames: @@ -47,7 +53,7 @@ def init_cache_busting(app): file_path = rooted_filename.replace(static_folder, "") file_path = file_path.replace("\\", "/") # Convert Windows path to web path hash_table[file_path] = file_hash - app.logger.debug('Finished computing cache-busting values') + log.debug('Finished computing cache-busting values') def bust_filename(filename): return hash_table.get(filename, "") diff --git a/cps/cli.py b/cps/cli.py index 0495b313..5d304ad0 100644 --- a/cps/cli.py +++ b/cps/cli.py @@ -18,9 +18,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import argparse -import os +from __future__ import division, print_function, unicode_literals import sys +import os +import argparse + +from .constants import CONFIG_DIR as _CONFIG_DIR + parser = argparse.ArgumentParser(description='Calibre Web is a web app' ' providing a interface for browsing, reading and downloading eBooks\n', prog='cps.py') @@ -33,17 +37,8 @@ parser.add_argument('-k', metavar='path', parser.add_argument('-v', action='store_true', help='shows version number and exits Calibre-web') args = parser.parse_args() -generalPath = os.path.normpath(os.getenv("CALIBRE_DBPATH", - os.path.dirname(os.path.realpath(__file__)) + os.sep + ".." + os.sep)) -if args.p: - settingspath = args.p -else: - settingspath = os.path.join(generalPath, "app.db") - -if args.g: - gdpath = args.g -else: - gdpath = os.path.join(generalPath, "gdrive.db") +settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db") +gdpath = args.g or os.path.join(_CONFIG_DIR, "gdrive.db") certfilepath = None keyfilepath = None diff --git a/cps/comic.py b/cps/comic.py index b68e638d..738b2a89 100755 --- a/cps/comic.py +++ b/cps/comic.py @@ -17,17 +17,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, print_function, unicode_literals import os -from constants import BookMeta -from cps import app -from iso639 import languages as isoLanguages + +from . import logger, isoLanguages +from .constants import BookMeta + + +log = logger.create() try: from comicapi.comicarchive import ComicArchive, MetaDataStyle use_comic_meta = True except ImportError as e: - app.logger.warning('cannot import comicapi, extracting comic metadata will not work: %s', e) + log.warning('cannot import comicapi, extracting comic metadata will not work: %s', e) import zipfile import tarfile use_comic_meta = False diff --git a/cps/constants.py b/cps/constants.py index 9e89f70f..ed073644 100644 --- a/cps/constants.py +++ b/cps/constants.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) -# Copyright (C) 2019 OzzieIsaacs +# Copyright (C) 2019 OzzieIsaacs, pwr # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,10 +17,101 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, print_function, unicode_literals +import sys +import os from collections import namedtuple + +BASE_DIR = sys.path[0] +STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static') +TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates') +TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations') +CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', BASE_DIR) + + +ROLE_USER = 0 << 0 +ROLE_ADMIN = 1 << 0 +ROLE_DOWNLOAD = 1 << 1 +ROLE_UPLOAD = 1 << 2 +ROLE_EDIT = 1 << 3 +ROLE_PASSWD = 1 << 4 +ROLE_ANONYMOUS = 1 << 5 +ROLE_EDIT_SHELFS = 1 << 6 +ROLE_DELETE_BOOKS = 1 << 7 +ROLE_VIEWER = 1 << 8 + +ALL_ROLES = { + "admin_role": ROLE_ADMIN, + "download_role": ROLE_DOWNLOAD, + "upload_role": ROLE_UPLOAD, + "edit_role": ROLE_EDIT, + "passwd_role": ROLE_PASSWD, + "edit_shelf_role": ROLE_EDIT_SHELFS, + "delete_role": ROLE_DELETE_BOOKS, + "viewer_role": ROLE_VIEWER, + } + +DETAIL_RANDOM = 1 << 0 +SIDEBAR_LANGUAGE = 1 << 1 +SIDEBAR_SERIES = 1 << 2 +SIDEBAR_CATEGORY = 1 << 3 +SIDEBAR_HOT = 1 << 4 +SIDEBAR_RANDOM = 1 << 5 +SIDEBAR_AUTHOR = 1 << 6 +SIDEBAR_BEST_RATED = 1 << 7 +SIDEBAR_READ_AND_UNREAD = 1 << 8 +SIDEBAR_RECENT = 1 << 9 +SIDEBAR_SORTED = 1 << 10 +MATURE_CONTENT = 1 << 11 +SIDEBAR_PUBLISHER = 1 << 12 +SIDEBAR_RATING = 1 << 13 +SIDEBAR_FORMAT = 1 << 14 + +ADMIN_USER_ROLES = (ROLE_VIEWER << 1) - 1 - (ROLE_ANONYMOUS | ROLE_EDIT_SHELFS) +ADMIN_USER_SIDEBAR = (SIDEBAR_FORMAT << 1) - 1 + +UPDATE_STABLE = 0 << 0 +AUTO_UPDATE_STABLE = 1 << 0 +UPDATE_NIGHTLY = 1 << 1 +AUTO_UPDATE_NIGHTLY = 1 << 2 + +LOGIN_STANDARD = 0 +LOGIN_LDAP = 1 +LOGIN_OAUTH_GITHUB = 2 +LOGIN_OAUTH_GOOGLE = 3 + + +DEFAULT_PASSWORD = "admin123" +DEFAULT_PORT = 8083 +try: + env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT) + DEFAULT_PORT = int(env_CALIBRE_PORT) +except ValueError: + print('Environment variable CALIBRE_PORT has invalid value (%s), faling back to default (8083)' % env_CALIBRE_PORT) +del env_CALIBRE_PORT + + +EXTENSIONS_AUDIO = {'mp3', 'm4a', 'm4b'} +EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt'} +EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx', + 'fb2', 'html', 'rtf', 'odt', 'mp3', 'm4a', 'm4b'} +# EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] + +# (['rar','cbr'] if feature_support['rar'] else [])) + + +def has_flag(value, bit_flag): + return bit_flag == (bit_flag & (value or 0)) + + """ :rtype: BookMeta """ BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' 'series_id, languages') + +STABLE_VERSION = {'version': '0.6.4 Beta'} + +# clean-up the module namespace +del sys, os, namedtuple + diff --git a/cps/converter.py b/cps/converter.py index d538619d..a2eb572d 100644 --- a/cps/converter.py +++ b/cps/converter.py @@ -17,14 +17,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - +from __future__ import division, print_function, unicode_literals import os -# import subprocess -import ub import re + from flask_babel import gettext as _ -from subproc_wrapper import process_open + from . import config +from .subproc_wrapper import process_open def versionKindle(): diff --git a/cps/db.py b/cps/db.py index 07ff54f7..ab9931da 100755 --- a/cps/db.py +++ b/cps/db.py @@ -18,16 +18,20 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from sqlalchemy import * -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import * +from __future__ import division, print_function, unicode_literals +import sys import os import re import ast -from . import config -import ub -import sys -import unidecode + +from sqlalchemy import create_engine +from sqlalchemy import Table, Column, ForeignKey +from sqlalchemy import String, Integer, Boolean +from sqlalchemy.orm import relationship, sessionmaker, scoped_session +from sqlalchemy.ext.declarative import declarative_base + +from . import config, ub + session = None cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series'] @@ -385,7 +389,7 @@ def setup_db(): ccdict = {'__tablename__': 'custom_column_' + str(row.id), 'id': Column(Integer, primary_key=True), 'value': Column(String)} - cc_classes[row.id] = type('Custom_Column_' + str(row.id), (Base,), ccdict) + cc_classes[row.id] = type(str('Custom_Column_' + str(row.id)), (Base,), ccdict) for cc_id in cc_ids: if (cc_id[1] == 'bool') or (cc_id[1] == 'int'): diff --git a/cps/editbooks.py b/cps/editbooks.py index 0cab9e23..15ee1661 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -21,28 +21,25 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# opds routing functions -from . import config, language_table, get_locale, app, ub, global_WorkerThread, db -from flask import request, flash, redirect, url_for, abort, Markup, Response -from flask import Blueprint -import datetime +from __future__ import division, print_function, unicode_literals import os +import datetime import json -from flask_babel import gettext as _ +from shutil import move, copyfile from uuid import uuid4 -from . import helper -from .helper import order_authors, common_filters + +from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response +from flask_babel import gettext as _ from flask_login import current_user -from .web import login_required_if_no_ano, render_title_template, edit_required, \ - upload_required, login_required, EXTENSIONS_UPLOAD -from . import gdriveutils -from shutil import move, copyfile -from . import uploader -from iso639 import languages as isoLanguages -editbook = Blueprint('editbook', __name__) +from . import constants, logger, isoLanguages, gdriveutils, uploader, helper +from . import config, get_locale, db, ub, global_WorkerThread, language_table +from .helper import order_authors, common_filters +from .web import login_required_if_no_ano, render_title_template, edit_required, upload_required, login_required + -EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt'} +editbook = Blueprint('editbook', __name__) +log = logger.create() # Modifies different Database objects, first check if elements have to be added to database, than check @@ -201,7 +198,7 @@ def delete_book(book_id, book_format): db.session.commit() else: # book not found - app.logger.info('Book with id "'+str(book_id)+'" could not be deleted') + log.error('Book with id "%s" could not be deleted: not found', book_id) if book_format: return redirect(url_for('editbook.edit_book', book_id=book_id)) else: @@ -231,16 +228,16 @@ def render_edit_book(book_id): valid_source_formats=list() if config.config_ebookconverter == 2: for file in book.data: - if file.format.lower() in EXTENSIONS_CONVERT: + if file.format.lower() in constants.EXTENSIONS_CONVERT: valid_source_formats.append(file.format.lower()) # Determine what formats don't already exist - allowed_conversion_formats = EXTENSIONS_CONVERT.copy() + allowed_conversion_formats = constants.EXTENSIONS_CONVERT.copy() for file in book.data: try: allowed_conversion_formats.remove(file.format.lower()) except Exception: - app.logger.warning(file.format.lower() + ' already removed from list.') + log.warning('%s already removed from list.', file.format.lower()) return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc, title=_(u"edit metadata"), page="editbook", @@ -321,7 +318,7 @@ def upload_single_file(request, book, book_id): if requested_file.filename != '': if '.' in requested_file.filename: file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() - if file_ext not in EXTENSIONS_UPLOAD: + if file_ext not in constants.EXTENSIONS_UPLOAD: flash(_("File extension '%(ext)s' is not allowed to be uploaded to this server", ext=file_ext), category="error") return redirect(url_for('web.show_book', book_id=book.id)) @@ -352,7 +349,7 @@ def upload_single_file(request, book, book_id): # Format entry already exists, no need to update the database if is_format: - app.logger.info('Book format already existing') + log.warning('Book format %s already existing', file_ext.upper()) else: db_format = db.Data(book_id, file_ext.upper(), file_size, file_name) db.session.add(db_format) @@ -530,7 +527,7 @@ def edit_book(book_id): res = list(language_table[get_locale()].keys())[invers_lang_table.index(lang)] input_l.append(res) except ValueError: - app.logger.error('%s is not a valid language' % lang) + log.error('%s is not a valid language', lang) flash(_(u"%(langname)s is not a valid language", langname=lang), category="error") modify_database_object(input_l, book.languages, db.Languages, db.session, 'languages') @@ -569,7 +566,7 @@ def edit_book(book_id): flash(error, category="error") return render_edit_book(book_id) except Exception as e: - app.logger.exception(e) + log.exception(e) db.session.rollback() flash(_("Error editing book, please check logfile for details"), category="error") return redirect(url_for('web.show_book', book_id=book.id)) @@ -590,7 +587,7 @@ def upload(): # check if file extension is correct if '.' in requested_file.filename: file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() - if file_ext not in EXTENSIONS_UPLOAD: + if file_ext not in constants.EXTENSIONS_UPLOAD: flash( _("File extension '%(ext)s' is not allowed to be uploaded to this server", ext=file_ext), category="error") @@ -631,7 +628,7 @@ def upload(): if meta.cover is None: has_cover = 0 - copyfile(os.path.join(config.get_main_dir, "cps/static/generic_cover.jpg"), + copyfile(os.path.join(constants.STATIC_DIR, 'generic_cover.jpg'), os.path.join(filepath, "cover.jpg")) else: has_cover = 1 @@ -741,9 +738,7 @@ def convert_bookformat(book_id): flash(_(u"Source or destination format for conversion missing"), category="error") return redirect(request.environ["HTTP_REFERER"]) - app.logger.debug('converting: book id: ' + str(book_id) + - ' from: ' + request.form['book_format_from'] + - ' to: ' + request.form['book_format_to']) + log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to) rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(), book_format_to.upper(), current_user.nickname) diff --git a/cps/epub.py b/cps/epub.py index 68325089..d9129646 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -17,11 +17,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, print_function, unicode_literals +import os import zipfile from lxml import etree -import os + +from . import isoLanguages from .constants import BookMeta -import isoLanguages def extractCover(zipFile, coverFile, coverpath, tmp_file_name): @@ -125,7 +127,7 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension): else: title = epub_metadata['title'] - return uploader.BookMeta( + return BookMeta( file_path=tmp_file_path, extension=original_file_extension, title=title.encode('utf-8').decode('utf-8'), diff --git a/cps/fb2.py b/cps/fb2.py index 4f113383..cd61b511 100644 --- a/cps/fb2.py +++ b/cps/fb2.py @@ -17,7 +17,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, print_function, unicode_literals from lxml import etree + from .constants import BookMeta diff --git a/cps/gdrive.py b/cps/gdrive.py index b4749aa1..196b9dac 100644 --- a/cps/gdrive.py +++ b/cps/gdrive.py @@ -20,26 +20,31 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals import os -from flask import Blueprint -from . import gdriveutils -from flask import flash, request, redirect, url_for, abort -from flask_babel import gettext as _ -from . import app, config, ub, db -from flask_login import login_required +import hashlib import json +import tempfile from uuid import uuid4 from time import time -import tempfile from shutil import move, copyfile -from .web import admin_required + +from flask import Blueprint, flash, request, redirect, url_for, abort +from flask_babel import gettext as _ +from flask_login import login_required try: from googleapiclient.errors import HttpError except ImportError: pass +from . import logger, gdriveutils, config, ub, db +from .web import admin_required + + gdrive = Blueprint('gdrive', __name__) +log = logger.create() current_milli_time = lambda: int(round(time() * 1000)) @@ -66,10 +71,10 @@ def google_drive_callback(): abort(403) try: credentials = gdriveutils.Gauth.Instance().auth.flow.step2_exchange(auth_code) - with open(os.path.join(config.get_main_dir,'gdrive_credentials'), 'w') as f: + with open(gdriveutils.CREDENTIALS, 'w') as f: f.write(credentials.to_json()) except ValueError as error: - app.logger.error(error) + log.error(error) return redirect(url_for('admin.configuration')) @@ -78,7 +83,7 @@ def google_drive_callback(): @admin_required def watch_gdrive(): if not config.config_google_drive_watch_changes_response: - with open(os.path.join(config.get_main_dir,'client_secrets.json'), 'r') as settings: + with open(gdriveutils.CLIENT_SECRETS, 'r') as settings: filedata = json.load(settings) if filedata['web']['redirect_uris'][0].endswith('/'): filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-((len('/gdrive/callback')+1))] @@ -126,7 +131,7 @@ def revoke_watch_gdrive(): @gdrive.route("/gdrive/watch/callback", methods=['GET', 'POST']) def on_received_watch_confirmation(): - app.logger.debug(request.headers) + log.debug('%r', request.headers) if request.headers.get('X-Goog-Channel-Token') == gdrive_watch_callback_token \ and request.headers.get('X-Goog-Resource-State') == 'change' \ and request.data: @@ -134,27 +139,26 @@ def on_received_watch_confirmation(): data = request.data def updateMetaData(): - app.logger.info('Change received from gdrive') - app.logger.debug(data) + log.info('Change received from gdrive') + log.debug('%r', data) try: j = json.loads(data) - app.logger.info('Getting change details') + log.info('Getting change details') response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id']) - app.logger.debug(response) + log.debug('%r', response) if response: dbpath = os.path.join(config.config_calibre_dir, "metadata.db") if not response['deleted'] and response['file']['title'] == 'metadata.db' and response['file']['md5Checksum'] != hashlib.md5(dbpath): tmpDir = tempfile.gettempdir() - app.logger.info('Database file updated') + log.info('Database file updated') copyfile(dbpath, os.path.join(tmpDir, "metadata.db_" + str(current_milli_time()))) - app.logger.info('Backing up existing and downloading updated metadata.db') + log.info('Backing up existing and downloading updated metadata.db') gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmpDir, "tmp_metadata.db")) - app.logger.info('Setting up new DB') + log.info('Setting up new DB') # prevent error on windows, as os.rename does on exisiting files move(os.path.join(tmpDir, "tmp_metadata.db"), dbpath) db.setup_db() except Exception as e: - app.logger.info(e.message) - app.logger.exception(e) + log.exception(e) updateMetaData() return '' diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py index c4b32e95..705966b2 100644 --- a/cps/gdriveutils.py +++ b/cps/gdriveutils.py @@ -17,23 +17,35 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, print_function, unicode_literals +import os +import shutil + +from flask import Response, stream_with_context +from sqlalchemy import create_engine +from sqlalchemy import Column, UniqueConstraint +from sqlalchemy import String, Integer +from sqlalchemy.orm import sessionmaker, scoped_session +from sqlalchemy.ext.declarative import declarative_base + try: from pydrive.auth import GoogleAuth from pydrive.drive import GoogleDrive - from pydrive.auth import RefreshError, InvalidConfigError + from pydrive.auth import RefreshError from apiclient import errors gdrive_support = True except ImportError: gdrive_support = False -import os -from . import config, app -import cli -import shutil -from flask import Response, stream_with_context -from sqlalchemy import * -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import * +from . import logger, cli, config +from .constants import BASE_DIR as _BASE_DIR + + +SETTINGS_YAML = os.path.join(_BASE_DIR, 'settings.yaml') +CREDENTIALS = os.path.join(_BASE_DIR, 'gdrive_credentials') +CLIENT_SECRETS = os.path.join(_BASE_DIR, 'client_secrets.json') + +log = logger.create() class Singleton: @@ -78,7 +90,7 @@ class Singleton: @Singleton class Gauth: def __init__(self): - self.auth = GoogleAuth(settings_file=os.path.join(config.get_main_dir,'settings.yaml')) + self.auth = GoogleAuth(settings_file=SETTINGS_YAML) @Singleton @@ -87,8 +99,7 @@ class Gdrive: self.drive = getDrive(gauth=Gauth.Instance().auth) def is_gdrive_ready(): - return os.path.exists(os.path.join(config.get_main_dir, 'settings.yaml')) and \ - os.path.exists(os.path.join(config.get_main_dir, 'gdrive_credentials')) + return os.path.exists(SETTINGS_YAML) and os.path.exists(CREDENTIALS) engine = create_engine('sqlite:///{0}'.format(cli.gdpath), echo=False) @@ -150,17 +161,17 @@ migrate() def getDrive(drive=None, gauth=None): if not drive: if not gauth: - gauth = GoogleAuth(settings_file=os.path.join(config.get_main_dir,'settings.yaml')) + gauth = GoogleAuth(settings_file=SETTINGS_YAML) # Try to load saved client credentials - gauth.LoadCredentialsFile(os.path.join(config.get_main_dir,'gdrive_credentials')) + gauth.LoadCredentialsFile(CREDENTIALS) if gauth.access_token_expired: # Refresh them if expired try: gauth.Refresh() except RefreshError as e: - app.logger.error("Google Drive error: " + e.message) + log.error("Google Drive error: %s", e) except Exception as e: - app.logger.exception(e) + log.exception(e) else: # Initialize the saved creds gauth.Authorize() @@ -170,7 +181,7 @@ def getDrive(drive=None, gauth=None): try: drive.auth.Refresh() except RefreshError as e: - app.logger.error("Google Drive error: " + e.message) + log.error("Google Drive error: %s", e) return drive def listRootFolders(): @@ -207,7 +218,7 @@ def getEbooksFolderId(drive=None): try: gDriveId.gdrive_id = getEbooksFolder(drive)['id'] except Exception: - app.logger.error('Error gDrive, root ID not found') + log.error('Error gDrive, root ID not found') gDriveId.path = '/' session.merge(gDriveId) session.commit() @@ -447,10 +458,10 @@ def getChangeById (drive, change_id): change = drive.auth.service.changes().get(changeId=change_id).execute() return change except (errors.HttpError) as error: - app.logger.info(error.message) + log.error(error) return None except Exception as e: - app.logger.info(e) + log.error(e) return None @@ -520,6 +531,6 @@ def do_gdrive_download(df, headers): if resp.status == 206: yield content else: - app.logger.info('An error occurred: %s' % resp) + log.warning('An error occurred: %s', resp) return return Response(stream_with_context(stream()), headers=headers) diff --git a/cps/helper.py b/cps/helper.py index 2ff6515d..cc309780 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -18,40 +18,30 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - -from . import config, global_WorkerThread, get_locale, db, mimetypes -from flask import current_app as app -from tempfile import gettempdir +from __future__ import division, print_function, unicode_literals import sys -import io import os +import io +import json +import mimetypes +import random import re -import unicodedata -from .worker import STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS, TASK_EMAIL, TASK_CONVERT, TASK_UPLOAD, \ - TASK_CONVERT_ANY +import requests +import shutil import time +import unicodedata +from datetime import datetime +from functools import reduce +from tempfile import gettempdir + +from babel import Locale as LC +from babel.core import UnknownLocaleError +from babel.dates import format_datetime from flask import send_from_directory, make_response, redirect, abort from flask_babel import gettext as _ from flask_login import current_user -from babel.dates import format_datetime -from babel.core import UnknownLocaleError -from datetime import datetime -from babel import Locale as LC -import shutil -import requests -from sqlalchemy.sql.expression import true, and_, false, text, func -from iso639 import languages as isoLanguages -from pagination import Pagination +from sqlalchemy.sql.expression import true, false, and_, or_, text, func from werkzeug.datastructures import Headers -import json - -try: - import gdriveutils as gd -except ImportError: - pass -import random -from subproc_wrapper import process_open -import ub try: from urllib.parse import quote @@ -70,17 +60,23 @@ try: except ImportError: use_levenshtein = False -try: - from functools import reduce -except ImportError: - pass # We're not using Python 3 - try: from PIL import Image use_PIL = True except ImportError: use_PIL = False +from . import logger, config, global_WorkerThread, get_locale, db, ub, isoLanguages +from . import gdriveutils as gd +from .constants import STATIC_DIR as _STATIC_DIR +from .pagination import Pagination +from .subproc_wrapper import process_open +from .worker import STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS +from .worker import TASK_EMAIL, TASK_CONVERT, TASK_UPLOAD, TASK_CONVERT_ANY + + +log = logger.create() + def update_download(book_id, user_id): check = ub.session.query(ub.Downloads).filter(ub.Downloads.user_id == user_id).filter(ub.Downloads.book_id == @@ -96,7 +92,7 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == old_book_format).first() if not data: error_message = _(u"%(format)s format not found for book id: %(book)d", format=old_book_format, book=book_id) - app.logger.error("convert_book_format: " + error_message) + log.error("convert_book_format: %s", error_message) return error_message if config.config_use_google_drive: df = gd.getFileFromEbooksFolder(book.path, data.name + "." + old_book_format.lower()) @@ -190,7 +186,7 @@ def check_send_to_kindle(entry): 'text':_('Convert %(orig)s to %(format)s and send to Kindle',orig='Epub',format='Azw3')})''' return bookformats else: - app.logger.error(u'Cannot find book entry %d', entry.id) + log.error(u'Cannot find book entry %d', entry.id) return None @@ -275,8 +271,8 @@ def get_sorted_author(value): value2 = value[-1] + ", " + " ".join(value[:-1]) else: value2 = value - except Exception: - app.logger.error("Sorting author " + str(value) + "failed") + except Exception as ex: + log.error("Sorting author %s failed: %s", value, ex) value2 = value return value2 @@ -293,13 +289,12 @@ def delete_book_file(book, calibrepath, book_format=None): else: if os.path.isdir(path): if len(next(os.walk(path))[1]): - app.logger.error( - "Deleting book " + str(book.id) + " failed, path has subfolders: " + book.path) + log.error("Deleting book %s failed, path has subfolders: %s", book.id, book.path) return False shutil.rmtree(path, ignore_errors=True) return True else: - app.logger.error("Deleting book " + str(book.id) + " failed, book path not valid: " + book.path) + log.error("Deleting book %s failed, book path not valid: %s", book.id, book.path) return False @@ -322,7 +317,7 @@ def update_dir_structure_file(book_id, calibrepath, first_author): if not os.path.exists(new_title_path): os.renames(path, new_title_path) else: - app.logger.info("Copying title: " + path + " into existing: " + new_title_path) + log.info("Copying title: %s into existing: %s", path, new_title_path) for dir_name, __, file_list in os.walk(path): for file in file_list: os.renames(os.path.join(dir_name, file), @@ -330,8 +325,8 @@ def update_dir_structure_file(book_id, calibrepath, first_author): path = new_title_path localbook.path = localbook.path.split('/')[0] + '/' + new_titledir except OSError as ex: - app.logger.error("Rename title from: " + path + " to " + new_title_path + ": " + str(ex)) - app.logger.debug(ex, exc_info=True) + log.error("Rename title from: %s to %s: %s", path, new_title_path, ex) + log.debug(ex, exc_info=True) return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_title_path, error=str(ex)) if authordir != new_authordir: @@ -340,8 +335,8 @@ def update_dir_structure_file(book_id, calibrepath, first_author): os.renames(path, new_author_path) localbook.path = new_authordir + '/' + localbook.path.split('/')[1] except OSError as ex: - app.logger.error("Rename author from: " + path + " to " + new_author_path + ": " + str(ex)) - app.logger.debug(ex, exc_info=True) + log.error("Rename author from: %s to %s: %s", path, new_author_path, ex) + log.debug(ex, exc_info=True) return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_author_path, error=str(ex)) # Rename all files from old names to new names @@ -354,8 +349,8 @@ def update_dir_structure_file(book_id, calibrepath, first_author): os.path.join(path_name, new_name + '.' + file_format.format.lower())) file_format.name = new_name except OSError as ex: - app.logger.error("Rename file in path " + path + " to " + new_name + ": " + str(ex)) - app.logger.debug(ex, exc_info=True) + log.error("Rename file in path %s to %s: %s", path, new_name, ex) + log.debug(ex, exc_info=True) return _("Rename file in path '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_name, error=str(ex)) return False @@ -454,26 +449,25 @@ def get_book_cover(book_id): if config.config_use_google_drive: try: if not gd.is_gdrive_ready(): - return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg") + return send_from_directory(_STATIC_DIR, "generic_cover.jpg") path=gd.get_cover_via_gdrive(book.path) if path: return redirect(path) else: - app.logger.error(book.path + '/cover.jpg not found on Google Drive') - return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg") + log.error('%s/cover.jpg not found on Google Drive', book.path) + return send_from_directory(_STATIC_DIR, "generic_cover.jpg") except Exception as e: - app.logger.error("Error Message: " + e.message) - app.logger.exception(e) + log.exception(e) # traceback.print_exc() - return send_from_directory(os.path.join(os.path.dirname(__file__), "static"),"generic_cover.jpg") + return send_from_directory(_STATIC_DIR,"generic_cover.jpg") else: cover_file_path = os.path.join(config.config_calibre_dir, book.path) if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")): return send_from_directory(cover_file_path, "cover.jpg") else: - return send_from_directory(os.path.join(os.path.dirname(__file__), "static"),"generic_cover.jpg") + return send_from_directory(_STATIC_DIR,"generic_cover.jpg") else: - return send_from_directory(os.path.join(os.path.dirname(__file__), "static"),"generic_cover.jpg") + return send_from_directory(_STATIC_DIR,"generic_cover.jpg") # saves book cover from url @@ -493,15 +487,15 @@ def save_cover_from_filestorage(filepath, saved_filename, img): try: os.makedirs(filepath) except OSError: - app.logger.error(u"Failed to create path for cover") + log.error(u"Failed to create path for cover") return False try: img.save(os.path.join(filepath, saved_filename)) except OSError: - app.logger.error(u"Failed to store cover-file") + log.error(u"Failed to store cover-file") return False except IOError: - app.logger.error(u"Cover-file is not a valid image file") + log.error(u"Cover-file is not a valid image file") return False return True @@ -512,7 +506,7 @@ def save_cover(img, book_path): if use_PIL: if content_type not in ('image/jpeg', 'image/png', 'image/webp'): - app.logger.error("Only jpg/jpeg/png/webp files are supported as coverfile") + log.error("Only jpg/jpeg/png/webp files are supported as coverfile") return False # convert to jpg because calibre only supports jpg if content_type in ('image/png', 'image/webp'): @@ -526,7 +520,7 @@ def save_cover(img, book_path): img._content = tmp_bytesio.getvalue() else: if content_type not in ('image/jpeg'): - app.logger.error("Only jpg/jpeg files are supported as coverfile") + log.error("Only jpg/jpeg files are supported as coverfile") return False if ub.config.config_use_google_drive: @@ -534,7 +528,7 @@ def save_cover(img, book_path): if save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img) is True: gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), os.path.join(tmpDir, "uploaded_cover.jpg")) - app.logger.info("Cover is saved on Google Drive") + log.info("Cover is saved on Google Drive") return True else: return False @@ -547,7 +541,7 @@ def do_download_file(book, book_format, data, headers): if config.config_use_google_drive: startTime = time.time() df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format) - app.logger.debug(time.time() - startTime) + log.debug('%s', time.time() - startTime) if df: return gd.do_gdrive_download(df, headers) else: @@ -556,7 +550,7 @@ def do_download_file(book, book_format, data, headers): filename = os.path.join(config.config_calibre_dir, book.path) if not os.path.isfile(os.path.join(filename, data.name + "." + book_format)): # ToDo: improve error handling - app.logger.error('File not found: %s' % os.path.join(filename, data.name + "." + book_format)) + log.error('File not found: %s', os.path.join(filename, data.name + "." + book_format)) response = make_response(send_from_directory(filename, data.name + "." + book_format)) response.headers = headers return response @@ -581,7 +575,7 @@ def check_unrar(unrarLocation): version = value.group(1) except OSError as e: error = True - app.logger.exception(e) + log.exception(e) version =_(u'Error excecuting UnRar') else: version = _(u'Unrar binary file not found') @@ -724,12 +718,12 @@ def get_search_results(term): db.Books.authors.any(db.func.lower(db.Authors.name).ilike("%" + term + "%")) return db.session.query(db.Books).filter(common_filters()).filter( - db.or_(db.Books.tags.any(db.func.lower(db.Tags.name).ilike("%" + term + "%")), - db.Books.series.any(db.func.lower(db.Series.name).ilike("%" + term + "%")), - db.Books.authors.any(and_(*q)), - db.Books.publishers.any(db.func.lower(db.Publishers.name).ilike("%" + term + "%")), - db.func.lower(db.Books.title).ilike("%" + term + "%") - )).all() + or_(db.Books.tags.any(db.func.lower(db.Tags.name).ilike("%" + term + "%")), + db.Books.series.any(db.func.lower(db.Series.name).ilike("%" + term + "%")), + db.Books.authors.any(and_(*q)), + db.Books.publishers.any(db.func.lower(db.Publishers.name).ilike("%" + term + "%")), + db.func.lower(db.Books.title).ilike("%" + term + "%") + )).all() def get_unique_other_books(library_books, author_books): # Get all identifiers (ISBN, Goodreads, etc) and filter author's books by that list so we show fewer duplicates diff --git a/cps/isoLanguages.py b/cps/isoLanguages.py index 31ef341e..ab112270 100644 --- a/cps/isoLanguages.py +++ b/cps/isoLanguages.py @@ -17,6 +17,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, print_function, unicode_literals + + try: from iso639 import languages, __version__ get = languages.get diff --git a/cps/jinjia.py b/cps/jinjia.py index 480ae274..37f9ce30 100644 --- a/cps/jinjia.py +++ b/cps/jinjia.py @@ -23,15 +23,21 @@ # custom jinja filters -from flask import Blueprint, request, url_for +from __future__ import division, print_function, unicode_literals import datetime +import mimetypes import re -from . import mimetypes, app + from babel.dates import format_date +from flask import Blueprint, request, url_for from flask_babel import get_locale from flask_login import current_user +from . import logger + + jinjia = Blueprint('jinjia', __name__) +log = logger.create() # pagination links in jinja @@ -79,8 +85,7 @@ def formatdate_filter(val): formatdate = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S") return format_date(formatdate, format='medium', locale=get_locale()) except AttributeError as e: - app.logger.error('Babel error: %s, Current user locale: %s, Current User: %s' % - (e, current_user.locale, current_user.nickname)) + log.error('Babel error: %s, Current user locale: %s, Current User: %s', e, current_user.locale, current_user.nickname) return formatdate @jinjia.app_template_filter('formatdateinput') diff --git a/cps/logger.py b/cps/logger.py new file mode 100644 index 00000000..d94767b4 --- /dev/null +++ b/cps/logger.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2019 pwr +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals +import os +import inspect +import logging +from logging import Formatter, StreamHandler +from logging.handlers import RotatingFileHandler + +from .constants import BASE_DIR as _BASE_DIR + + +FORMATTER = Formatter("[%(asctime)s] %(levelname)5s {%(name)s:%(lineno)d} %(message)s") +DEFAULT_LOG_LEVEL = logging.INFO +DEFAULT_LOG_FILE = os.path.join(_BASE_DIR, "calibre-web.log") +LOG_TO_STDERR = '/dev/stderr' + + +logging.addLevelName(logging.WARNING, "WARN") +logging.addLevelName(logging.CRITICAL, "CRIT") + + +def get(name=None): + return logging.getLogger(name) + + +def create(): + parent_frame = inspect.stack(0)[1] + if hasattr(parent_frame, 'frame'): + parent_frame = parent_frame.frame + else: + parent_frame = parent_frame[0] + parent_module = inspect.getmodule(parent_frame) + return get(parent_module.__name__) + + +def is_debug_enabled(): + return logging.root.level <= logging.DEBUG + + +def get_level_name(level): + return logging.getLevelName(level) + + +def is_valid_logfile(file_path): + if not file_path: + return True + if os.path.isdir(file_path): + return False + log_dir = os.path.dirname(file_path) + return (not log_dir) or os.path.isdir(log_dir) + + +def setup(log_file, log_level=None): + if log_file: + if not os.path.dirname(log_file): + log_file = os.path.join(_BASE_DIR, log_file) + log_file = os.path.abspath(log_file) + else: + # log_file = LOG_TO_STDERR + log_file = DEFAULT_LOG_FILE + + # print ('%r -- %r' % (log_level, log_file)) + r = logging.root + r.setLevel(log_level or DEFAULT_LOG_LEVEL) + + previous_handler = r.handlers[0] if r.handlers else None + # print ('previous %r' % previous_handler) + + if previous_handler: + # if the log_file has not changed, don't create a new handler + if getattr(previous_handler, 'baseFilename', None) == log_file: + return + r.debug("logging to %s level %s", log_file, r.level) + + if log_file == LOG_TO_STDERR: + file_handler = StreamHandler() + file_handler.baseFilename = LOG_TO_STDERR + else: + try: + file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2) + except IOError: + if log_file == DEFAULT_LOG_FILE: + raise + file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=50000, backupCount=2) + file_handler.setFormatter(FORMATTER) + + for h in r.handlers: + r.removeHandler(h) + h.close() + r.addHandler(file_handler) + # print ('new handler %r' % file_handler) + + +# Enable logging of smtp lib debug output +class StderrLogger(object): + def __init__(self, name=None): + self.log = get(name or self.__class__.__name__) + self.buffer = '' + + def write(self, message): + try: + if message == '\n': + self.log.debug(self.buffer.replace('\n', '\\n')) + self.buffer = '' + else: + self.buffer += message + except: + self.logger.debug("Logging Error") diff --git a/cps/oauth.py b/cps/oauth.py index 960a3810..35362dbf 100644 --- a/cps/oauth.py +++ b/cps/oauth.py @@ -1,7 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from __future__ import division, print_function, unicode_literals from flask import session + + try: from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user from sqlalchemy.orm.exc import NoResultFound diff --git a/cps/oauth_bb.py b/cps/oauth_bb.py index fb4ba60c..3a48798a 100644 --- a/cps/oauth_bb.py +++ b/cps/oauth_bb.py @@ -21,30 +21,34 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -from flask_dance.contrib.github import make_github_blueprint, github -from flask_dance.contrib.google import make_google_blueprint, google -from flask_dance.consumer import oauth_authorized, oauth_error +from __future__ import division, print_function, unicode_literals +import json +from functools import wraps from oauth import OAuthBackend -from sqlalchemy.orm.exc import NoResultFound + from flask import session, request, make_response, abort -import json -from cps import config, app -import ub -from flask_login import login_user, current_user from flask import Blueprint, flash, redirect, url_for from flask_babel import gettext as _ -# from web import github_oauth_required -from functools import wraps -from web import login_required +from flask_dance.consumer import oauth_authorized, oauth_error +from flask_dance.contrib.github import make_github_blueprint, github +from flask_dance.contrib.google import make_google_blueprint, google +from flask_login import login_user, current_user +from sqlalchemy.orm.exc import NoResultFound + +from . import constants, logger, config, app, ub +from .web import login_required +# from .web import github_oauth_required + oauth_check = {} oauth = Blueprint('oauth', __name__) +log = logger.create() def github_oauth_required(f): @wraps(f) def inner(*args, **kwargs): - if config.config_login_type == ub.LOGIN_OAUTH_GITHUB: + if config.config_login_type == constants.LOGIN_OAUTH_GITHUB: return f(*args, **kwargs) if request.is_xhr: data = {'status': 'error', 'message': 'Not Found'} @@ -59,7 +63,7 @@ def github_oauth_required(f): def google_oauth_required(f): @wraps(f) def inner(*args, **kwargs): - if config.config_use_google_oauth == ub.LOGIN_OAUTH_GOOGLE: + if config.config_use_google_oauth == constants.LOGIN_OAUTH_GOOGLE: return f(*args, **kwargs) if request.is_xhr: data = {'status': 'error', 'message': 'Not Found'} @@ -101,7 +105,7 @@ def register_user_with_oauth(user=None): try: ub.session.commit() except Exception as e: - app.logger.exception(e) + log.exception(e) ub.session.rollback() @@ -133,9 +137,9 @@ if ub.oauth_support: google_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True) - if config.config_login_type == ub.LOGIN_OAUTH_GITHUB: + if config.config_login_type == constants.LOGIN_OAUTH_GITHUB: register_oauth_blueprint(github_blueprint, 'GitHub') - if config.config_login_type == ub.LOGIN_OAUTH_GOOGLE: + if config.config_login_type == constants.LOGIN_OAUTH_GOOGLE: register_oauth_blueprint(google_blueprint, 'Google') @@ -195,7 +199,7 @@ if ub.oauth_support: ub.session.add(oauth) ub.session.commit() except Exception as e: - app.logger.exception(e) + log.exception(e) ub.session.rollback() # Disable Flask-Dance's default behavior for saving the OAuth token @@ -221,7 +225,7 @@ if ub.oauth_support: ub.session.add(oauth) ub.session.commit() except Exception as e: - app.logger.exception(e) + log.exception(e) ub.session.rollback() return redirect(url_for('web.login')) #if config.config_public_reg: @@ -264,11 +268,11 @@ if ub.oauth_support: logout_oauth_user() flash(_(u"Unlink to %(oauth)s success.", oauth=oauth_check[provider]), category="success") except Exception as e: - app.logger.exception(e) + log.exception(e) ub.session.rollback() flash(_(u"Unlink to %(oauth)s failed.", oauth=oauth_check[provider]), category="error") except NoResultFound: - app.logger.warning("oauth %s for user %d not fount" % (provider, current_user.id)) + log.warning("oauth %s for user %d not fount", provider, current_user.id) flash(_(u"Not linked to %(oauth)s.", oauth=oauth_check[provider]), category="error") return redirect(url_for('web.profile')) diff --git a/cps/opds.py b/cps/opds.py index 86b24ab6..dd7755bb 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -21,22 +21,24 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# opds routing functions -from . import config, db -from flask import request, render_template, Response, g, make_response -from pagination import Pagination -from flask import Blueprint +from __future__ import division, print_function, unicode_literals +import sys import datetime -import ub -from flask_login import current_user from functools import wraps -from .web import login_required_if_no_ano, common_filters, get_search_results, render_read_books, download_required -from sqlalchemy.sql.expression import func, text + +from flask import Blueprint, request, render_template, Response, g, make_response +from flask_login import current_user +from sqlalchemy.sql.expression import func, text, or_, and_ from werkzeug.security import check_password_hash + +from . import logger, config, db, ub from .helper import fill_indexpage, get_download_link, get_book_cover -import sys +from .pagination import Pagination +from .web import common_filters, get_search_results, render_read_books, download_required + opds = Blueprint('opds', __name__) +log = logger.create() def requires_basic_auth_if_no_ano(f): @@ -231,10 +233,10 @@ def feed_shelf(book_id): if current_user.is_anonymous: shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == book_id).first() else: - shelf = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), - ub.Shelf.id == book_id), - ub.and_(ub.Shelf.is_public == 1, - ub.Shelf.id == book_id))).first() + shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id), + ub.Shelf.id == book_id), + and_(ub.Shelf.is_public == 1, + ub.Shelf.id == book_id))).first() result = list() # user is allowed to access shelf if shelf: diff --git a/cps/pagination.py b/cps/pagination.py index 50fbc4e5..0a138a64 100644 --- a/cps/pagination.py +++ b/cps/pagination.py @@ -21,6 +21,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, print_function, unicode_literals from math import ceil diff --git a/cps/redirect.py b/cps/redirect.py index 7b3981c4..324c4b20 100644 --- a/cps/redirect.py +++ b/cps/redirect.py @@ -28,10 +28,12 @@ # http://flask.pocoo.org/snippets/62/ +from __future__ import division, print_function, unicode_literals try: from urllib.parse import urlparse, urljoin except ImportError: from urlparse import urlparse, urljoin + from flask import request, url_for, redirect diff --git a/cps/reverseproxy.py b/cps/reverseproxy.py index 3b256cb4..25bbe77b 100644 --- a/cps/reverseproxy.py +++ b/cps/reverseproxy.py @@ -37,6 +37,8 @@ # # Inspired by http://flask.pocoo.org/snippets/35/ +from __future__ import division, print_function, unicode_literals + class ReverseProxied(object): """Wrap the application in this middleware and configure the diff --git a/cps/server.py b/cps/server.py index f104203a..3dd03257 100644 --- a/cps/server.py +++ b/cps/server.py @@ -17,12 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - -from socket import error as SocketError +from __future__ import division, print_function, unicode_literals import sys import os import signal -from . import config, global_WorkerThread +import socket try: from gevent.pywsgi import WSGIServer @@ -36,6 +35,11 @@ except ImportError: from tornado import version as tornadoVersion gevent_present = False +from . import logger, config, global_WorkerThread + + +log = logger.create() + class server: @@ -49,76 +53,77 @@ class server: def init_app(self, application): self.app = application + self.port = config.config_port + + self.ssl_args = None + certfile_path = config.get_config_certfile() + keyfile_path = config.get_config_keyfile() + if certfile_path and keyfile_path: + if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path): + self.ssl_args = {"certfile": certfile_path, + "keyfile": keyfile_path} + else: + log.warning('The specified paths for the ssl certificate file and/or key file seem to be broken. Ignoring ssl.') + log.warning('Cert path: %s', certfile_path) + log.warning('Key path: %s', keyfile_path) + + def _make_gevent_socket(self): + if os.name == 'nt': + return ('0.0.0.0', self.port) + + try: + s = WSGIServer.get_listener(('', self.port), family=socket.AF_INET6) + except socket.error as ex: + log.error('%s', ex) + log.warning('Unable to listen on \'\', trying on IPv4 only...') + s = WSGIServer.get_listener(('', self.port), family=socket.AF_INET) + log.debug("%r %r", s._sock, s._sock.getsockname()) + return s def start_gevent(self): - ssl_args = dict() + ssl_args = self.ssl_args or {} + log.info('Starting Gevent server') + try: - certfile_path = config.get_config_certfile() - keyfile_path = config.get_config_keyfile() - if certfile_path and keyfile_path: - if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path): - ssl_args = {"certfile": certfile_path, - "keyfile": keyfile_path} - else: - self.app.logger.info('The specified paths for the ssl certificate file and/or key file seem ' - 'to be broken. Ignoring ssl. Cert path: %s | Key path: ' - '%s' % (certfile_path, keyfile_path)) - if os.name == 'nt': - self.wsgiserver = WSGIServer(('0.0.0.0', config.config_port), self.app, spawn=Pool(), **ssl_args) - else: - self.wsgiserver = WSGIServer(('', config.config_port), self.app, spawn=Pool(), **ssl_args) + sock = self._make_gevent_socket() + self.wsgiserver = WSGIServer(sock, self.app, spawn=Pool(), **ssl_args) self.wsgiserver.serve_forever() - - except SocketError: - try: - self.app.logger.info('Unable to listen on \'\', trying on IPv4 only...') - self.wsgiserver = WSGIServer(('0.0.0.0', config.config_port), self.app, spawn=Pool(), **ssl_args) - self.wsgiserver.serve_forever() - except (OSError, SocketError) as e: - self.app.logger.info("Error starting server: %s" % e.strerror) - print("Error starting server: %s" % e.strerror) - global_WorkerThread.stop() - sys.exit(1) + except (OSError, socket.error) as e: + log.info("Error starting server: %s", e.strerror) + print("Error starting server: %s" % e.strerror) + global_WorkerThread.stop() + sys.exit(1) except Exception: - self.app.logger.info("Unknown error while starting gevent") + log.exception("Unknown error while starting gevent") + + def start_tornado(self): + log.info('Starting Tornado server') + + try: + # Max Buffersize set to 200MB + http_server = HTTPServer(WSGIContainer(self.app), + max_buffer_size = 209700000, + ssl_options=self.ssl_args) + http_server.listen(self.port) + self.wsgiserver=IOLoop.instance() + self.wsgiserver.start() + # wait for stop signal + self.wsgiserver.close(True) + except socket.error as err: + log.exception("Error starting tornado server") + print("Error starting server: %s" % err.strerror) + global_WorkerThread.stop() + sys.exit(1) def startServer(self): if gevent_present: - self.app.logger.info('Starting Gevent server') # leave subprocess out to allow forking for fetchers and processors self.start_gevent() else: - try: - ssl = None - self.app.logger.info('Starting Tornado server') - certfile_path = config.get_config_certfile() - keyfile_path = config.get_config_keyfile() - if certfile_path and keyfile_path: - if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path): - ssl = {"certfile": certfile_path, - "keyfile": keyfile_path} - else: - self.app.logger.info('The specified paths for the ssl certificate file and/or key file ' - 'seem to be broken. Ignoring ssl. Cert path: %s | Key ' - 'path: %s' % (certfile_path, keyfile_path)) - - # Max Buffersize set to 200MB - http_server = HTTPServer(WSGIContainer(self.app), - max_buffer_size = 209700000, - ssl_options=ssl) - http_server.listen(config.config_port) - self.wsgiserver=IOLoop.instance() - self.wsgiserver.start() - # wait for stop signal - self.wsgiserver.close(True) - except SocketError as e: - self.app.logger.info("Error starting server: %s" % e.strerror) - print("Error starting server: %s" % e.strerror) - global_WorkerThread.stop() - sys.exit(1) + self.start_tornado() if self.restart is True: - self.app.logger.info("Performing restart of Calibre-Web") + log.info("Performing restart of Calibre-Web") global_WorkerThread.stop() if os.name == 'nt': arguments = ["\"" + sys.executable + "\""] @@ -128,7 +133,7 @@ class server: else: os.execl(sys.executable, sys.executable, *sys.argv) else: - self.app.logger.info("Performing shutdown of Calibre-Web") + log.info("Performing shutdown of Calibre-Web") global_WorkerThread.stop() sys.exit(0) diff --git a/cps/shelf.py b/cps/shelf.py index 1a926b0e..392c05b1 100644 --- a/cps/shelf.py +++ b/cps/shelf.py @@ -21,28 +21,34 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, print_function, unicode_literals + from flask import Blueprint, request, flash, redirect, url_for -from . import ub, searched_ids, app, db from flask_babel import gettext as _ -from sqlalchemy.sql.expression import func, or_ from flask_login import login_required, current_user +from sqlalchemy.sql.expression import func, or_, and_ + +from . import logger, ub, searched_ids, db from .web import render_title_template + shelf = Blueprint('shelf', __name__) +log = logger.create() + @shelf.route("/shelf/add//") @login_required def add_to_shelf(shelf_id, book_id): shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() if shelf is None: - app.logger.info("Invalid shelf specified") + log.error("Invalid shelf specified: %s", shelf_id) if not request.is_xhr: flash(_(u"Invalid shelf specified"), category="error") return redirect(url_for('web.index')) return "Invalid shelf specified", 400 if not shelf.is_public and not shelf.user_id == int(current_user.id): - app.logger.info("Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name) + log.error("User %s not allowed to add a book to %s", current_user, shelf) if not request.is_xhr: flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name), category="error") @@ -50,7 +56,7 @@ def add_to_shelf(shelf_id, book_id): return "Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name, 403 if shelf.is_public and not current_user.role_edit_shelfs(): - app.logger.info("User is not allowed to edit public shelves") + log.info("User %s not allowed to edit public shelves", current_user) if not request.is_xhr: flash(_(u"You are not allowed to edit public shelves"), category="error") return redirect(url_for('web.index')) @@ -59,7 +65,7 @@ def add_to_shelf(shelf_id, book_id): book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id, ub.BookShelf.book_id == book_id).first() if book_in_shelf: - app.logger.info("Book is already part of the shelf: %s" % shelf.name) + log.error("Book %s is already part of %s", book_id, shelf) if not request.is_xhr: flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error") return redirect(url_for('web.index')) @@ -88,17 +94,17 @@ def add_to_shelf(shelf_id, book_id): def search_to_shelf(shelf_id): shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() if shelf is None: - app.logger.info("Invalid shelf specified") + log.error("Invalid shelf specified: %s", shelf_id) flash(_(u"Invalid shelf specified"), category="error") return redirect(url_for('web.index')) if not shelf.is_public and not shelf.user_id == int(current_user.id): - app.logger.info("You are not allowed to add a book to the the shelf: %s" % shelf.name) + log.error("User %s not allowed to add a book to %s", current_user, shelf) flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error") return redirect(url_for('web.index')) if shelf.is_public and not current_user.role_edit_shelfs(): - app.logger.info("User is not allowed to edit public shelves") + log.error("User %s not allowed to edit public shelves", current_user) flash(_(u"User is not allowed to edit public shelves"), category="error") return redirect(url_for('web.index')) @@ -116,7 +122,7 @@ def search_to_shelf(shelf_id): books_for_shelf = searched_ids[current_user.id] if not books_for_shelf: - app.logger.info("Books are already part of the shelf: %s" % shelf.name) + log.error("Books are already part of %s", shelf) flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error") return redirect(url_for('web.index')) @@ -142,7 +148,7 @@ def search_to_shelf(shelf_id): def remove_from_shelf(shelf_id, book_id): shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() if shelf is None: - app.logger.info("Invalid shelf specified") + log.error("Invalid shelf specified: %s", shelf_id) if not request.is_xhr: return redirect(url_for('web.index')) return "Invalid shelf specified", 400 @@ -161,7 +167,7 @@ def remove_from_shelf(shelf_id, book_id): ub.BookShelf.book_id == book_id).first() if book_shelf is None: - app.logger.info("Book already removed from shelf") + log.error("Book %s already removed from %s", book_id, shelf) if not request.is_xhr: return redirect(url_for('web.index')) return "Book already removed from shelf", 410 @@ -174,7 +180,7 @@ def remove_from_shelf(shelf_id, book_id): return redirect(request.environ["HTTP_REFERER"]) return "", 204 else: - app.logger.info("Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name) + log.error("User %s not allowed to remove a book from %s", current_user, shelf) if not request.is_xhr: flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name), category="error") @@ -248,15 +254,15 @@ def delete_shelf(shelf_id): else: if (not cur_shelf.is_public and cur_shelf.user_id == int(current_user.id)) \ or (cur_shelf.is_public and current_user.role_edit_shelfs()): - deleted = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), - ub.Shelf.id == shelf_id), - ub.and_(ub.Shelf.is_public == 1, - ub.Shelf.id == shelf_id))).delete() + deleted = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id), + ub.Shelf.id == shelf_id), + and_(ub.Shelf.is_public == 1, + ub.Shelf.id == shelf_id))).delete() if deleted: ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete() ub.session.commit() - app.logger.info(_(u"successfully deleted shelf %(name)s", name=cur_shelf.name, category="success")) + log.info("successfully deleted %s", cur_shelf) return redirect(url_for('web.index')) # @shelf.route("/shelfdown/") @@ -267,10 +273,10 @@ def show_shelf(type, shelf_id): if current_user.is_anonymous: shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first() else: - shelf = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), - ub.Shelf.id == shelf_id), - ub.and_(ub.Shelf.is_public == 1, - ub.Shelf.id == shelf_id))).first() + shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id), + ub.Shelf.id == shelf_id), + and_(ub.Shelf.is_public == 1, + ub.Shelf.id == shelf_id))).first() result = list() # user is allowed to access shelf if shelf: @@ -283,7 +289,7 @@ def show_shelf(type, shelf_id): if cur_book: result.append(cur_book) else: - app.logger.info('Not existing book %s in shelf %s deleted' % (book.book_id, shelf.id)) + log.info('Not existing book %s in %s deleted', book.book_id, shelf) ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete() ub.session.commit() return render_title_template(page, entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name), @@ -309,10 +315,10 @@ def order_shelf(shelf_id): if current_user.is_anonymous: shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first() else: - shelf = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), - ub.Shelf.id == shelf_id), - ub.and_(ub.Shelf.is_public == 1, - ub.Shelf.id == shelf_id))).first() + shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id), + ub.Shelf.id == shelf_id), + and_(ub.Shelf.is_public == 1, + ub.Shelf.id == shelf_id))).first() result = list() if shelf: books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \ diff --git a/cps/static/css/style.css b/cps/static/css/style.css index fc0d1390..b1edd2ec 100644 --- a/cps/static/css/style.css +++ b/cps/static/css/style.css @@ -1,3 +1,6 @@ + +.tooltip.bottom .tooltip-inner{font-size:13px;font-family:Open Sans Semibold,Helvetica Neue,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;padding:3px 10px;border-radius:4px;background-color:#fff;-webkit-box-shadow:0 4px 10px 0 rgba(0,0,0,.35);box-shadow:0 4px 10px 0 rgba(0,0,0,.35);opacity:1;white-space:nowrap;margin-top:-16px!important;line-height:1.71428571;color:#ddd} + @font-face { font-family: 'Grand Hotel'; font-style: normal; diff --git a/cps/subproc_wrapper.py b/cps/subproc_wrapper.py index d8826111..0371fd7e 100644 --- a/cps/subproc_wrapper.py +++ b/cps/subproc_wrapper.py @@ -16,9 +16,11 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import subprocess -import os + +from __future__ import division, print_function, unicode_literals import sys +import os +import subprocess def process_open(command, quotes=(), env=None, sout=subprocess.PIPE): diff --git a/cps/templates/index.html b/cps/templates/index.html index ed86c8c6..0faed5fd 100755 --- a/cps/templates/index.html +++ b/cps/templates/index.html @@ -54,16 +54,16 @@

{{_(title)}}

-
{% if entries[0] %} diff --git a/cps/ub.py b/cps/ub.py index 9253dafe..b8d44375 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -18,78 +18,35 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from sqlalchemy import * -from sqlalchemy import exc -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import * -from flask_login import AnonymousUserMixin +from __future__ import division, print_function, unicode_literals import sys import os -import logging -from werkzeug.security import generate_password_hash -import json import datetime +import json from binascii import hexlify -import cli + from flask import g from flask_babel import gettext as _ - +from flask_login import AnonymousUserMixin try: from flask_dance.consumer.backend.sqla import OAuthConsumerMixin oauth_support = True except ImportError: oauth_support = False +from sqlalchemy import create_engine, exc, exists +from sqlalchemy import Column, ForeignKey +from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime +from sqlalchemy.orm import relationship, sessionmaker +from sqlalchemy.ext.declarative import declarative_base +from werkzeug.security import generate_password_hash try: import ldap except ImportError: pass -ROLE_USER = 0 -ROLE_ADMIN = 1 -ROLE_DOWNLOAD = 2 -ROLE_UPLOAD = 4 -ROLE_EDIT = 8 -ROLE_PASSWD = 16 -ROLE_ANONYMOUS = 32 -ROLE_EDIT_SHELFS = 64 -ROLE_DELETE_BOOKS = 128 -ROLE_VIEWER = 256 - - -DETAIL_RANDOM = 1 -SIDEBAR_LANGUAGE = 2 -SIDEBAR_SERIES = 4 -SIDEBAR_CATEGORY = 8 -SIDEBAR_HOT = 16 -SIDEBAR_RANDOM = 32 -SIDEBAR_AUTHOR = 64 -SIDEBAR_BEST_RATED = 128 -SIDEBAR_READ_AND_UNREAD = 256 -SIDEBAR_RECENT = 512 -SIDEBAR_SORTED = 1024 -MATURE_CONTENT = 2048 -SIDEBAR_PUBLISHER = 4096 -SIDEBAR_RATING = 8192 -SIDEBAR_FORMAT = 16384 - -UPDATE_STABLE = 0 -AUTO_UPDATE_STABLE = 1 -UPDATE_NIGHTLY = 2 -AUTO_UPDATE_NIGHTLY = 4 - -LOGIN_STANDARD = 0 -LOGIN_LDAP = 1 -LOGIN_OAUTH_GITHUB = 2 -LOGIN_OAUTH_GOOGLE = 3 - -DEFAULT_PASS = "admin123" -try: - DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083)) -except ValueError: - print ('Environmentvariable CALIBRE_PORT is set to an invalid value: ' + - os.environ.get("CALIBRE_PORT", 8083) + ', faling back to default (8083)') - DEFAULT_PORT = 8083 +from . import constants, logger, cli + session = None @@ -109,47 +66,47 @@ def get_sidebar_config(kwargs=None): content = 'conf' in kwargs sidebar = list() sidebar.append({"glyph": "glyphicon-book", "text": _('Recently Added'), "link": 'web.index', "id": "new", - "visibility": SIDEBAR_RECENT, 'public': True, "page": "root", + "visibility": constants.SIDEBAR_RECENT, 'public': True, "page": "root", "show_text": _('Show recent books'), "config_show":True}) sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot", - "visibility": SIDEBAR_HOT, 'public': True, "page": "hot", "show_text": _('Show hot books'), + "visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot", "show_text": _('Show hot books'), "config_show":True}) sidebar.append( {"glyph": "glyphicon-star", "text": _('Best rated Books'), "link": 'web.books_list', "id": "rated", - "visibility": SIDEBAR_BEST_RATED, 'public': True, "page": "rated", + "visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated", "show_text": _('Show best rated books'), "config_show":True}) sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read", - "visibility": SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "read", + "visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "read", "show_text": _('Show read and unread'), "config_show": content}) sidebar.append( {"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread", - "visibility": SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread", + "visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread", "show_text": _('Show unread'), "config_show":False}) sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand", - "visibility": SIDEBAR_RANDOM, 'public': True, "page": "discover", + "visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover", "show_text": _('Show random books'), "config_show":True}) sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat", - "visibility": SIDEBAR_CATEGORY, 'public': True, "page": "category", + "visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category", "show_text": _('Show category selection'), "config_show":True}) sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie", - "visibility": SIDEBAR_SERIES, 'public': True, "page": "series", + "visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series", "show_text": _('Show series selection'), "config_show":True}) sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author", - "visibility": SIDEBAR_AUTHOR, 'public': True, "page": "author", + "visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author", "show_text": _('Show author selection'), "config_show":True}) sidebar.append( {"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher", - "visibility": SIDEBAR_PUBLISHER, 'public': True, "page": "publisher", + "visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher", "show_text": _('Show publisher selection'), "config_show":True}) sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang", - "visibility": SIDEBAR_LANGUAGE, 'public': (g.user.filter_language() == 'all'), + "visibility": constants.SIDEBAR_LANGUAGE, 'public': (g.user.filter_language() == 'all'), "page": "language", "show_text": _('Show language selection'), "config_show":True}) sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate", - "visibility": SIDEBAR_RATING, 'public': True, + "visibility": constants.SIDEBAR_RATING, 'public': True, "page": "rating", "show_text": _('Show ratings selection'), "config_show":True}) sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format", - "visibility": SIDEBAR_FORMAT, 'public': True, + "visibility": constants.SIDEBAR_FORMAT, 'public': True, "page": "format", "show_text": _('Show file formats selection'), "config_show":True}) return sidebar @@ -161,51 +118,35 @@ class UserBase: def is_authenticated(self): return True + def _has_role(self, role_flag): + return constants.has_flag(self.role, role_flag) + def role_admin(self): - if self.role is not None: - return True if self.role & ROLE_ADMIN == ROLE_ADMIN else False - else: - return False + return self._has_role(constants.ROLE_ADMIN) def role_download(self): - if self.role is not None: - return True if self.role & ROLE_DOWNLOAD == ROLE_DOWNLOAD else False - else: - return False + return self._has_role(constants.ROLE_DOWNLOAD) def role_upload(self): - return bool((self.role is not None)and(self.role & ROLE_UPLOAD == ROLE_UPLOAD)) + return self._has_role(constants.ROLE_UPLOAD) def role_edit(self): - if self.role is not None: - return True if self.role & ROLE_EDIT == ROLE_EDIT else False - else: - return False + return self._has_role(constants.ROLE_EDIT) def role_passwd(self): - if self.role is not None: - return True if self.role & ROLE_PASSWD == ROLE_PASSWD else False - else: - return False + return self._has_role(constants.ROLE_PASSWD) def role_anonymous(self): - if self.role is not None: - return True if self.role & ROLE_ANONYMOUS == ROLE_ANONYMOUS else False - else: - return False + return self._has_role(constants.ROLE_ANONYMOUS) def role_edit_shelfs(self): - if self.role is not None: - return True if self.role & ROLE_EDIT_SHELFS == ROLE_EDIT_SHELFS else False - else: - return False + return self._has_role(constants.ROLE_EDIT_SHELFS) def role_delete_books(self): - return bool((self.role is not None)and(self.role & ROLE_DELETE_BOOKS == ROLE_DELETE_BOOKS)) - + return self._has_role(constants.ROLE_DELETE_BOOKS) def role_viewer(self): - return bool((self.role is not None)and(self.role & ROLE_VIEWER == ROLE_VIEWER)) + return self._has_role(constants.ROLE_VIEWER) @property def is_active(self): @@ -222,10 +163,10 @@ class UserBase: return self.default_language def check_visibility(self, value): - return bool((self.sidebar_view is not None) and (self.sidebar_view & value == value)) + return constants.has_flag(self.sidebar_view, value) def show_detail_random(self): - return bool((self.sidebar_view is not None)and(self.sidebar_view & DETAIL_RANDOM == DETAIL_RANDOM)) + return self.check_visibility(constants.DETAIL_RANDOM) def __repr__(self): return '' % self.nickname @@ -246,7 +187,7 @@ class User(UserBase, Base): id = Column(Integer, primary_key=True) nickname = Column(String(64), unique=True) email = Column(String(120), unique=True, default="") - role = Column(SmallInteger, default=ROLE_USER) + role = Column(SmallInteger, default=constants.ROLE_USER) password = Column(String) kindle_mail = Column(String(120), default="") shelf = relationship('Shelf', backref='user', lazy='dynamic', order_by='Shelf.name') @@ -270,7 +211,7 @@ class Anonymous(AnonymousUserMixin, UserBase): self.loadSettings() def loadSettings(self): - data = session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() # type: User + data = session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() # type: User settings = session.query(Settings).first() self.nickname = data.nickname self.role = data.role @@ -308,7 +249,7 @@ class Shelf(Base): user_id = Column(Integer, ForeignKey('user.id')) def __repr__(self): - return '' % self.name + return '' % (self.id, self.name) # Baseclass representing Relationship between books and Shelfs in Calibre-Web in app.db (N:M) @@ -379,7 +320,7 @@ class Settings(Base): mail_password = Column(String) mail_from = Column(String) config_calibre_dir = Column(String) - config_port = Column(Integer, default=DEFAULT_PORT) + config_port = Column(Integer, default=constants.DEFAULT_PORT) config_certfile = Column(String) config_keyfile = Column(String) config_calibre_web_title = Column(String, default=u'Calibre-Web') @@ -388,7 +329,7 @@ class Settings(Base): config_authors_max = Column(Integer, default=0) config_read_column = Column(Integer, default=0) config_title_regex = Column(String, default=u'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+') - config_log_level = Column(SmallInteger, default=logging.INFO) + config_log_level = Column(SmallInteger, default=logger.DEFAULT_LOG_LEVEL) config_uploading = Column(SmallInteger, default=0) config_anonbrowse = Column(SmallInteger, default=0) config_public_reg = Column(SmallInteger, default=0) @@ -445,8 +386,6 @@ class RemoteAuthToken(Base): # Class holds all application specific settings in calibre-web class Config: def __init__(self): - self.config_main_dir = os.path.join(os.path.normpath(os.path.dirname( - os.path.realpath(__file__)) + os.sep + ".." + os.sep)) self.db_configured = None self.config_logfile = None self.loadSettings() @@ -497,19 +436,12 @@ class Config: # self.config_use_google_oauth = data.config_use_google_oauth self.config_google_oauth_client_id = data.config_google_oauth_client_id self.config_google_oauth_client_secret = data.config_google_oauth_client_secret - if data.config_mature_content_tags: - self.config_mature_content_tags = data.config_mature_content_tags - else: - self.config_mature_content_tags = u'' - if data.config_logfile: - self.config_logfile = data.config_logfile + self.config_mature_content_tags = data.config_mature_content_tags or u'' + self.config_logfile = data.config_logfile or u'' self.config_rarfile_location = data.config_rarfile_location self.config_theme = data.config_theme self.config_updatechannel = data.config_updatechannel - - @property - def get_main_dir(self): - return self.config_main_dir + logger.setup(self.config_logfile, self.config_log_level) @property def get_update_channel(self): @@ -533,72 +465,41 @@ class Config: else: return self.config_keyfile - def get_config_logfile(self): - if not self.config_logfile: - return os.path.join(self.get_main_dir, "calibre-web.log") - else: - if os.path.dirname(self.config_logfile): - return self.config_logfile - else: - return os.path.join(self.get_main_dir, self.config_logfile) + def _has_role(self, role_flag): + return constants.has_flag(self.config_default_role, role_flag) def role_admin(self): - if self.config_default_role is not None: - return True if self.config_default_role & ROLE_ADMIN == ROLE_ADMIN else False - else: - return False + return self._has_role(constants.ROLE_ADMIN) def role_download(self): - if self.config_default_role is not None: - return True if self.config_default_role & ROLE_DOWNLOAD == ROLE_DOWNLOAD else False - else: - return False + return self._has_role(constants.ROLE_DOWNLOAD) def role_viewer(self): - if self.config_default_role is not None: - return True if self.config_default_role & ROLE_VIEWER == ROLE_VIEWER else False - else: - return False + return self._has_role(constants.ROLE_VIEWER) def role_upload(self): - if self.config_default_role is not None: - return True if self.config_default_role & ROLE_UPLOAD == ROLE_UPLOAD else False - else: - return False + return self._has_role(constants.ROLE_UPLOAD) def role_edit(self): - if self.config_default_role is not None: - return True if self.config_default_role & ROLE_EDIT == ROLE_EDIT else False - else: - return False + return self._has_role(constants.ROLE_EDIT) def role_passwd(self): - if self.config_default_role is not None: - return True if self.config_default_role & ROLE_PASSWD == ROLE_PASSWD else False - else: - return False + return self._has_role(constants.ROLE_PASSWD) def role_edit_shelfs(self): - if self.config_default_role is not None: - return True if self.config_default_role & ROLE_EDIT_SHELFS == ROLE_EDIT_SHELFS else False - else: - return False + return self._has_role(constants.ROLE_EDIT_SHELFS) def role_delete_books(self): - return bool((self.config_default_role is not None) and - (self.config_default_role & ROLE_DELETE_BOOKS == ROLE_DELETE_BOOKS)) - - def show_detail_random(self): - return bool((self.config_default_show is not None) and - (self.config_default_show & DETAIL_RANDOM == DETAIL_RANDOM)) + return self._has_role(constants.ROLE_DELETE_BOOKS) def show_element_new_user(self, value): - return bool((self.config_default_show is not None) and - (self.config_default_show & value == value)) + return constants.has_flag(self.config_default_show, value) + + def show_detail_random(self): + return self.show_element_new_user(constants.DETAIL_RANDOM) def show_mature_content(self): - return bool((self.config_default_show is not None) and - (self.config_default_show & MATURE_CONTENT == MATURE_CONTENT)) + return self.show_element_new_user(constants.MATURE_CONTENT) def mature_content_tags(self): if sys.version_info > (3, 0): # Python3 str, Python2 unicode @@ -608,16 +509,7 @@ class Config: return list(map(lstrip, self.config_mature_content_tags.split(","))) def get_Log_Level(self): - ret_value = "" - if self.config_log_level == logging.INFO: - ret_value = 'INFO' - elif self.config_log_level == logging.DEBUG: - ret_value = 'DEBUG' - elif self.config_log_level == logging.WARNING: - ret_value = 'WARNING' - elif self.config_log_level == logging.ERROR: - ret_value = 'ERROR' - return ret_value + return logger.get_level_name(self.config_log_level) # Migrate database to current version, has to be updated after every database change. Currently migration from @@ -696,9 +588,9 @@ def migrate_Database(): conn.execute("UPDATE user SET 'sidebar_view' = (random_books* :side_random + language_books * :side_lang " "+ series_books * :side_series + category_books * :side_category + hot_books * " ":side_hot + :side_autor + :detail_random)" - ,{'side_random': SIDEBAR_RANDOM, 'side_lang': SIDEBAR_LANGUAGE, 'side_series': SIDEBAR_SERIES, - 'side_category': SIDEBAR_CATEGORY, 'side_hot': SIDEBAR_HOT, 'side_autor': SIDEBAR_AUTHOR, - 'detail_random': DETAIL_RANDOM}) + ,{'side_random': constants.SIDEBAR_RANDOM, 'side_lang': constants.SIDEBAR_LANGUAGE, 'side_series': constants.SIDEBAR_SERIES, + 'side_category': constants.SIDEBAR_CATEGORY, 'side_hot': constants.SIDEBAR_HOT, 'side_autor': constants.SIDEBAR_AUTHOR, + 'detail_random': constants.DETAIL_RANDOM}) session.commit() try: session.query(exists().where(User.mature_content)).scalar() @@ -706,7 +598,7 @@ def migrate_Database(): conn = engine.connect() conn.execute("ALTER TABLE user ADD column `mature_content` INTEGER DEFAULT 1") - if session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() is None: + if session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() is None: create_anonymous_user() try: session.query(exists().where(Settings.config_remote_login)).scalar() @@ -850,7 +742,7 @@ def create_anonymous_user(): user = User() user.nickname = "Guest" user.email = 'no@email' - user.role = ROLE_ANONYMOUS + user.role = constants.ROLE_ANONYMOUS user.password = '' session.add(user) @@ -864,13 +756,10 @@ def create_anonymous_user(): def create_admin_user(): user = User() user.nickname = "admin" - user.role = ROLE_USER + ROLE_ADMIN + ROLE_DOWNLOAD + ROLE_UPLOAD + ROLE_EDIT + ROLE_DELETE_BOOKS + ROLE_PASSWD +\ - ROLE_VIEWER - user.sidebar_view = DETAIL_RANDOM + SIDEBAR_LANGUAGE + SIDEBAR_SERIES + SIDEBAR_CATEGORY + SIDEBAR_HOT + \ - SIDEBAR_RANDOM + SIDEBAR_AUTHOR + SIDEBAR_BEST_RATED + SIDEBAR_READ_AND_UNREAD + SIDEBAR_RECENT + \ - SIDEBAR_SORTED + MATURE_CONTENT + SIDEBAR_PUBLISHER + SIDEBAR_RATING + SIDEBAR_FORMAT + user.role = constants.ADMIN_USER_ROLES + user.sidebar_view = constants.ADMIN_USER_SIDEBAR - user.password = generate_password_hash(DEFAULT_PASS) + user.password = generate_password_hash(constants.DEFAULT_PASSWORD) session.add(user) try: diff --git a/cps/updater.py b/cps/updater.py index d7ec63b0..3b679a51 100644 --- a/cps/updater.py +++ b/cps/updater.py @@ -17,22 +17,28 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - -from . import config, get_locale, Server, app -import threading -import zipfile +from __future__ import division, print_function, unicode_literals +import sys +import os +import datetime +import json import requests +import shutil +import threading import time +import zipfile from io import BytesIO -import os -import sys -import shutil -from ub import UPDATE_STABLE from tempfile import gettempdir -import datetime -import json -from flask_babel import gettext as _ + from babel.dates import format_datetime +from flask_babel import gettext as _ + +from . import constants, logger, config, get_locale, Server + + +log = logger.create() +_REPOSITORY_API_URL = 'https://api.github.com/repos/janeczku/calibre-web' + def is_sha1(sha1): @@ -53,13 +59,13 @@ class Updater(threading.Thread): self.updateIndex = None def get_current_version_info(self): - if config.get_update_channel == UPDATE_STABLE: + if config.get_update_channel == constants.UPDATE_STABLE: return self._stable_version_info() else: return self._nightly_version_info() def get_available_updates(self, request_method): - if config.get_update_channel == UPDATE_STABLE: + if config.get_update_channel == constants.UPDATE_STABLE: return self._stable_available_updates(request_method) else: return self._nightly_available_updates(request_method) @@ -67,45 +73,45 @@ class Updater(threading.Thread): def run(self): try: self.status = 1 - app.logger.debug(u'Download update file') + log.debug(u'Download update file') headers = {'Accept': 'application/vnd.github.v3+json'} r = requests.get(self._get_request_path(), stream=True, headers=headers) r.raise_for_status() self.status = 2 - app.logger.debug(u'Opening zipfile') + log.debug(u'Opening zipfile') z = zipfile.ZipFile(BytesIO(r.content)) self.status = 3 - app.logger.debug(u'Extracting zipfile') + log.debug(u'Extracting zipfile') tmp_dir = gettempdir() z.extractall(tmp_dir) foldername = os.path.join(tmp_dir, z.namelist()[0])[:-1] if not os.path.isdir(foldername): self.status = 11 - app.logger.info(u'Extracted contents of zipfile not found in temp folder') + log.info(u'Extracted contents of zipfile not found in temp folder') return self.status = 4 - app.logger.debug(u'Replacing files') - self.update_source(foldername, config.get_main_dir) + log.debug(u'Replacing files') + self.update_source(foldername, constants.BASE_DIR) self.status = 6 - app.logger.debug(u'Preparing restart of server') + log.debug(u'Preparing restart of server') time.sleep(2) Server.setRestartTyp(True) Server.stopServer() self.status = 7 time.sleep(2) except requests.exceptions.HTTPError as ex: - app.logger.info( u'HTTP Error' + ' ' + str(ex)) + log.info(u'HTTP Error %s', ex) self.status = 8 except requests.exceptions.ConnectionError: - app.logger.info(u'Connection error') + log.info(u'Connection error') self.status = 9 except requests.exceptions.Timeout: - app.logger.info(u'Timeout while establishing connection') + log.info(u'Timeout while establishing connection') self.status = 10 except requests.exceptions.RequestException: self.status = 11 - app.logger.info(u'General error') + log.info(u'General error') def get_update_status(self): return self.status @@ -153,14 +159,14 @@ class Updater(threading.Thread): if sys.platform == "win32" or sys.platform == "darwin": change_permissions = False else: - app.logger.debug('Update on OS-System : ' + sys.platform) + log.debug('Update on OS-System : %s', sys.platform) new_permissions = os.stat(root_dst_dir) # print new_permissions for src_dir, __, files in os.walk(root_src_dir): dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1) if not os.path.exists(dst_dir): os.makedirs(dst_dir) - app.logger.debug('Create-Dir: '+dst_dir) + log.debug('Create-Dir: %s', dst_dir) if change_permissions: # print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid)) os.chown(dst_dir, new_permissions.st_uid, new_permissions.st_gid) @@ -170,22 +176,22 @@ class Updater(threading.Thread): if os.path.exists(dst_file): if change_permissions: permission = os.stat(dst_file) - app.logger.debug('Remove file before copy: '+dst_file) + log.debug('Remove file before copy: %s', dst_file) os.remove(dst_file) else: if change_permissions: permission = new_permissions shutil.move(src_file, dst_dir) - app.logger.debug('Move File '+src_file+' to '+dst_dir) + log.debug('Move File %s to %s', src_file, dst_dir) if change_permissions: try: os.chown(dst_file, permission.st_uid, permission.st_gid) except (Exception) as e: # ex = sys.exc_info() old_permissions = os.stat(dst_file) - app.logger.debug('Fail change permissions of ' + str(dst_file) + '. Before: ' - + str(old_permissions.st_uid) + ':' + str(old_permissions.st_gid) + ' After: ' - + str(permission.st_uid) + ':' + str(permission.st_gid) + ' error: '+str(e)) + log.debug('Fail change permissions of %s. Before: %s:%s After %s:%s error: %s', + dst_file, old_permissions.st_uid, old_permissions.st_gid, + permission.st_uid, permission.st_gid, e) return def update_source(self, source, destination): @@ -219,15 +225,15 @@ class Updater(threading.Thread): for item in remove_items: item_path = os.path.join(destination, item[1:]) if os.path.isdir(item_path): - app.logger.debug("Delete dir " + item_path) + log.debug("Delete dir %s", item_path) shutil.rmtree(item_path, ignore_errors=True) else: try: - app.logger.debug("Delete file " + item_path) + log.debug("Delete file %s", item_path) # log_from_thread("Delete file " + item_path) os.remove(item_path) except Exception: - app.logger.debug("Could not remove:" + item_path) + log.debug("Could not remove: %s", item_path) shutil.rmtree(source, ignore_errors=True) @classmethod @@ -243,12 +249,12 @@ class Updater(threading.Thread): @classmethod def _stable_version_info(self): - return {'version': '0.6.4 Beta'} # Current version + return constants.STABLE_VERSION # Current version def _nightly_available_updates(self, request_method): tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone) if request_method == "GET": - repository_url = 'https://api.github.com/repos/janeczku/calibre-web' + repository_url = _REPOSITORY_API_URL status, commit = self._load_remote_data(repository_url +'/git/refs/heads/master') parents = [] if status['message'] != '': @@ -348,7 +354,7 @@ class Updater(threading.Thread): if request_method == "GET": parents = [] # repository_url = 'https://api.github.com/repos/flatpak/flatpak/releases' # test URL - repository_url = 'https://api.github.com/repos/janeczku/calibre-web/releases?per_page=100' + repository_url = _REPOSITORY_API_URL + '/releases?per_page=100' status, commit = self._load_remote_data(repository_url) if status['message'] != '': return json.dumps(status) @@ -434,10 +440,10 @@ class Updater(threading.Thread): return json.dumps(status) def _get_request_path(self): - if config.get_update_channel == UPDATE_STABLE: + if config.get_update_channel == constants.UPDATE_STABLE: return self.updateFile else: - return 'https://api.github.com/repos/janeczku/calibre-web/zipball/master' + return _REPOSITORY_API_URL + '/zipball/master' def _load_remote_data(self, repository_url): status = { diff --git a/cps/uploader.py b/cps/uploader.py index 235c215c..17816d0a 100644 --- a/cps/uploader.py +++ b/cps/uploader.py @@ -17,13 +17,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - -from tempfile import gettempdir -import hashlib +from __future__ import division, print_function, unicode_literals import os +import hashlib +from tempfile import gettempdir + from flask_babel import gettext as _ -import comic -from . import app + +from . import logger, comic +from .constants import BookMeta + + +log = logger.create() + try: from lxml.etree import LXML_VERSION as lxmlversion @@ -36,7 +42,7 @@ try: from wand.exceptions import PolicyError use_generic_pdf_cover = False except (ImportError, RuntimeError) as e: - app.logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e) + log.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e) use_generic_pdf_cover = True try: @@ -44,29 +50,29 @@ try: from PyPDF2 import __version__ as PyPdfVersion use_pdf_meta = True except ImportError as e: - app.logger.warning('cannot import PyPDF2, extracting pdf metadata will not work: %s', e) + log.warning('cannot import PyPDF2, extracting pdf metadata will not work: %s', e) use_pdf_meta = False try: - import epub + from . import epub use_epub_meta = True except ImportError as e: - app.logger.warning('cannot import epub, extracting epub metadata will not work: %s', e) + log.warning('cannot import epub, extracting epub metadata will not work: %s', e) use_epub_meta = False try: - import fb2 + from . import fb2 use_fb2_meta = True except ImportError as e: - app.logger.warning('cannot import fb2, extracting fb2 metadata will not work: %s', e) + log.warning('cannot import fb2, extracting fb2 metadata will not work: %s', e) use_fb2_meta = False try: from PIL import Image from PIL import __version__ as PILversion use_PIL = True -except ImportError: - app.logger.warning('cannot import Pillow, using png and webp images as cover will not work: %s', e) +except ImportError as e: + log.warning('cannot import Pillow, using png and webp images as cover will not work: %s', e) use_generic_pdf_cover = True use_PIL = False @@ -88,7 +94,7 @@ def process(tmp_file_path, original_file_name, original_file_extension): meta = comic.get_comic_info(tmp_file_path, original_file_name, original_file_extension) except Exception as ex: - app.logger.warning('cannot parse metadata, using default: %s', ex) + log.warning('cannot parse metadata, using default: %s', ex) if meta and meta.title.strip() and meta.author.strip(): return meta @@ -192,10 +198,10 @@ def pdf_preview(tmp_file_path, tmp_dir): img.save(filename=os.path.join(tmp_dir, cover_file_name)) return cover_file_name except PolicyError as ex: - app.logger.warning('Pdf extraction forbidden by Imagemagick policy: %s', ex) + log.warning('Pdf extraction forbidden by Imagemagick policy: %s', ex) return None except Exception as ex: - app.logger.warning('Cannot extract cover image, using default: %s', ex) + log.warning('Cannot extract cover image, using default: %s', ex) return None diff --git a/cps/web.py b/cps/web.py index f02c6006..5c2151f6 100644 --- a/cps/web.py +++ b/cps/web.py @@ -21,35 +21,39 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from . import mimetypes, global_WorkerThread, searched_ids, lm, babel, ub, config, get_locale, language_table, app, db -from .helper import common_filters, get_search_results, fill_indexpage, speaking_language, check_valid_domain, \ - order_authors, get_typeahead, render_task_status, json_serial, get_unique_other_books, get_cc_columns, \ - get_book_cover, get_download_link, send_mail, generate_random_password, send_registration_mail, \ - check_send_to_kindle, check_read_formats, lcase -from flask import render_template, request, redirect, send_from_directory, make_response, g, flash, abort, url_for -from flask_login import login_user, logout_user, login_required, current_user -from werkzeug.exceptions import default_exceptions -from werkzeug.security import generate_password_hash, check_password_hash -from werkzeug.datastructures import Headers -from redirect import redirect_back -from pagination import Pagination +from __future__ import division, print_function, unicode_literals +import os +import base64 +import datetime +import json +import mimetypes + from babel import Locale as LC from babel.dates import format_date from babel.core import UnknownLocaleError +from flask import Blueprint +from flask import render_template, request, redirect, send_from_directory, make_response, g, flash, abort, url_for from flask_babel import gettext as _ -from sqlalchemy.sql.expression import text, func, true, false, not_ +from flask_login import login_user, logout_user, login_required, current_user from sqlalchemy.exc import IntegrityError -import base64 -import os.path -import json -import datetime -import isoLanguages -from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download +from sqlalchemy.sql.expression import text, func, true, false, not_, and_ +from werkzeug.exceptions import default_exceptions +from werkzeug.datastructures import Headers +from werkzeug.security import generate_password_hash, check_password_hash +from . import constants, logger, isoLanguages +from . import global_WorkerThread, searched_ids, lm, babel, db, ub, config, get_locale, app, language_table +from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download +from .helper import common_filters, get_search_results, fill_indexpage, speaking_language, check_valid_domain, \ + order_authors, get_typeahead, render_task_status, json_serial, get_unique_other_books, get_cc_columns, \ + get_book_cover, get_download_link, send_mail, generate_random_password, send_registration_mail, \ + check_send_to_kindle, check_read_formats, lcase +from .pagination import Pagination +from .redirect import redirect_back feature_support = dict() try: - from oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status + from .oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status feature_support['oauth'] = True except ImportError: feature_support['oauth'] = False @@ -72,32 +76,17 @@ try: except ImportError: pass # We're not using Python 3 -try: - import rarfile - feature_support['rar'] = True -except ImportError: - feature_support['rar'] = False +# try: +# import rarfile +# feature_support['rar'] = True +# except ImportError: +# feature_support['rar'] = False try: from natsort import natsorted as sort except ImportError: sort = sorted # Just use regular sort then, may cause issues with badly named pages in cbz/cbr files -from flask import Blueprint - -# Global variables - -EXTENSIONS_AUDIO = {'mp3', 'm4a', 'm4b'} - -EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx', - 'fb2', 'html', 'rtf', 'odt', 'mp3', 'm4a', 'm4b'} - - -'''EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] + - (['rar','cbr'] if feature_support['rar'] else []))''' - - -# with app.app_context(): # custom error page def error_http(error): @@ -116,6 +105,7 @@ for ex in default_exceptions: web = Blueprint('web', __name__) +log = logger.create() # ################################### Login logic and rights management ############################################### @@ -238,7 +228,7 @@ def edit_required(f): # Returns the template for rendering and includes the instance name def render_title_template(*args, **kwargs): sidebar=ub.get_sidebar_config(kwargs) - return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, accept=EXTENSIONS_UPLOAD, + return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, accept=constants.EXTENSIONS_UPLOAD, *args, **kwargs) @@ -272,9 +262,9 @@ def get_email_status_json(): @login_required def bookmark(book_id, book_format): bookmark_key = request.form["bookmark"] - ub.session.query(ub.Bookmark).filter(ub.and_(ub.Bookmark.user_id == int(current_user.id), - ub.Bookmark.book_id == book_id, - ub.Bookmark.format == book_format)).delete() + ub.session.query(ub.Bookmark).filter(and_(ub.Bookmark.user_id == int(current_user.id), + ub.Bookmark.book_id == book_id, + ub.Bookmark.format == book_format)).delete() if not bookmark_key: ub.session.commit() return "", 204 @@ -292,8 +282,8 @@ def bookmark(book_id, book_format): @login_required def toggle_read(book_id): if not config.config_read_column: - book = ub.session.query(ub.ReadBook).filter(ub.and_(ub.ReadBook.user_id == int(current_user.id), - ub.ReadBook.book_id == book_id)).first() + book = ub.session.query(ub.ReadBook).filter(and_(ub.ReadBook.user_id == int(current_user.id), + ub.ReadBook.book_id == book_id)).first() if book: book.is_read = not book.is_read else: @@ -318,8 +308,7 @@ def toggle_read(book_id): db.session.add(new_cc) db.session.commit() except KeyError: - app.logger.error( - u"Custom Column No.%d is not exisiting in calibre database" % config.config_read_column) + log.error(u"Custom Column No.%d is not exisiting in calibre database", config.config_read_column) return "" ''' @@ -342,10 +331,10 @@ def get_comic_book(book_id, book_format, page): extract = lambda page: rf.read(names[page]) except: # rarfile not valid - app.logger.error('Unrar binary not found, or unable to decompress file ' + cbr_file) + log.error('Unrar binary not found, or unable to decompress file %s', cbr_file) return "", 204 else: - app.logger.info('Unrar is not supported please install python rarfile extension') + log.info('Unrar is not supported please install python rarfile extension') # no support means return nothing return "", 204 elif book_format in ("cbz", "zip"): @@ -357,7 +346,7 @@ def get_comic_book(book_id, book_format, page): names=sort(tf.getnames()) extract = lambda page: tf.extractfile(names[page]).read() else: - app.logger.error('unsupported comic format') + log.error('unsupported comic format') return "", 204 if sys.version_info.major >= 3: @@ -477,7 +466,7 @@ def books_list(data, sort, book_id, page): order = [db.Books.timestamp] if data == "rated": - if current_user.check_visibility(ub.SIDEBAR_BEST_RATED): + if current_user.check_visibility(constants.SIDEBAR_BEST_RATED): entries, random, pagination = fill_indexpage(page, db.Books, db.Books.ratings.any(db.Ratings.rating > 9), order) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, @@ -485,7 +474,7 @@ def books_list(data, sort, book_id, page): else: abort(404) elif data == "discover": - if current_user.check_visibility(ub.SIDEBAR_RANDOM): + if current_user.check_visibility(constants.SIDEBAR_RANDOM): entries, __, pagination = fill_indexpage(page, db.Books, True, [func.randomblob(2)]) pagination = Pagination(1, config.config_books_per_page, config.config_books_per_page) return render_title_template('discover.html', entries=entries, pagination=pagination, @@ -517,7 +506,7 @@ def books_list(data, sort, book_id, page): def render_hot_books(page): - if current_user.check_visibility(ub.SIDEBAR_HOT): + if current_user.check_visibility(constants.SIDEBAR_HOT): if current_user.show_detail_random(): random = db.session.query(db.Books).filter(common_filters()) \ .order_by(func.random()).limit(config.config_random_books) @@ -564,7 +553,7 @@ def render_author_books(page, book_id, order): other_books = get_unique_other_books(entries.all(), author_info.books) except Exception: # Skip goodreads, if site is down/inaccessible - app.logger.error('Goodreads website is down/inaccessible') + log.error('Goodreads website is down/inaccessible') return render_title_template('author.html', entries=entries, pagination=pagination, title=name, author=author_info, other_books=other_books, page="author") @@ -630,7 +619,7 @@ def render_category_books(page, book_id, order): @web.route("/author") @login_required_if_no_ano def author_list(): - if current_user.check_visibility(ub.SIDEBAR_AUTHOR): + if current_user.check_visibility(constants.SIDEBAR_AUTHOR): entries = db.session.query(db.Authors, func.count('books_authors_link.book').label('count'))\ .join(db.books_authors_link).join(db.Books).filter(common_filters())\ .group_by(text('books_authors_link.author')).order_by(db.Authors.sort).all() @@ -648,7 +637,7 @@ def author_list(): @web.route("/publisher") @login_required_if_no_ano def publisher_list(): - if current_user.check_visibility(ub.SIDEBAR_PUBLISHER): + if current_user.check_visibility(constants.SIDEBAR_PUBLISHER): entries = db.session.query(db.Publishers, func.count('books_publishers_link.book').label('count'))\ .join(db.books_publishers_link).join(db.Books).filter(common_filters())\ .group_by(text('books_publishers_link.publisher')).order_by(db.Publishers.sort).all() @@ -664,7 +653,7 @@ def publisher_list(): @web.route("/series") @login_required_if_no_ano def series_list(): - if current_user.check_visibility(ub.SIDEBAR_SERIES): + if current_user.check_visibility(constants.SIDEBAR_SERIES): entries = db.session.query(db.Series, func.count('books_series_link.book').label('count'))\ .join(db.books_series_link).join(db.Books).filter(common_filters())\ .group_by(text('books_series_link.series')).order_by(db.Series.sort).all() @@ -680,7 +669,7 @@ def series_list(): @web.route("/ratings") @login_required_if_no_ano def ratings_list(): - if current_user.check_visibility(ub.SIDEBAR_RATING): + if current_user.check_visibility(constants.SIDEBAR_RATING): entries = db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'), (db.Ratings.rating/2).label('name'))\ .join(db.books_ratings_link).join(db.Books).filter(common_filters())\ @@ -694,7 +683,7 @@ def ratings_list(): @web.route("/formats") @login_required_if_no_ano def formats_list(): - if current_user.check_visibility(ub.SIDEBAR_FORMAT): + if current_user.check_visibility(constants.SIDEBAR_FORMAT): entries = db.session.query(db.Data, func.count('data.book').label('count'),db.Data.format.label('format'))\ .join(db.Books).filter(common_filters())\ .group_by(db.Data.format).order_by(db.Data.format).all() @@ -707,7 +696,7 @@ def formats_list(): @web.route("/language") @login_required_if_no_ano def language_overview(): - if current_user.check_visibility(ub.SIDEBAR_LANGUAGE): + if current_user.check_visibility(constants.SIDEBAR_LANGUAGE): charlist = list() if current_user.filter_language() == u"all": languages = speaking_language() @@ -753,7 +742,7 @@ def language(name, page): @web.route("/category") @login_required_if_no_ano def category_list(): - if current_user.check_visibility(ub.SIDEBAR_CATEGORY): + if current_user.check_visibility(constants.SIDEBAR_CATEGORY): entries = db.session.query(db.Tags, func.count('books_tags_link.book').label('count'))\ .join(db.books_tags_link).join(db.Books).order_by(db.Tags.name).filter(common_filters())\ .group_by(text('books_tags_link.tag')).all() @@ -945,7 +934,7 @@ def render_read_books(page, are_read, as_xml=False, order=None): .filter(db.cc_classes[config.config_read_column].value is True).all() readBookIds = [x.book for x in readBooks] except KeyError: - app.logger.error(u"Custom Column No.%d is not existing in calibre database" % config.config_read_column) + log.error("Custom Column No.%d is not existing in calibre database", config.config_read_column) readBookIds = [] if are_read: @@ -988,7 +977,7 @@ def serve_book(book_id, book_format): book = db.session.query(db.Books).filter(db.Books.id == book_id).first() data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format.upper())\ .first() - app.logger.info('Serving book: %s', data.name) + log.info('Serving book: %s', data.name) if config.config_use_google_drive: headers = Headers() try: @@ -1058,7 +1047,7 @@ def register(): content.password = generate_password_hash(password) content.role = config.config_default_role content.sidebar_view = config.config_default_show - content.mature_content = bool(config.config_default_show & ub.MATURE_CONTENT) + content.mature_content = bool(config.config_default_show & constants.MATURE_CONTENT) try: ub.session.add(content) ub.session.commit() @@ -1071,8 +1060,7 @@ def register(): return render_title_template('register.html', title=_(u"register"), page="register") else: flash(_(u"Your e-mail is not allowed to register"), category="error") - app.logger.info('Registering failed for user "' + to_save['nickname'] + '" e-mail adress: ' + - to_save["email"]) + log.info('Registering failed for user "%s" e-mail adress: %s', to_save['nickname'], to_save["email"]) return render_title_template('register.html', title=_(u"register"), page="register") flash(_(u"Confirmation e-mail was send to your e-mail account."), category="success") return redirect(url_for('web.login')) @@ -1104,10 +1092,10 @@ def login(): return redirect_back(url_for("web.index")) except ldap.INVALID_CREDENTIALS: ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr) - app.logger.info('LDAP Login failed for user "' + form['username'] + '" IP-adress: ' + ipAdress) + log.info('LDAP Login failed for user "%s" IP-adress: %s', form['username'], ipAdress) flash(_(u"Wrong Username or Password"), category="error") except ldap.SERVER_DOWN: - app.logger.info('LDAP Login failed, LDAP Server down') + log.info('LDAP Login failed, LDAP Server down') flash(_(u"Could not login. LDAP server down, please contact your administrator"), category="error") else: if user and check_password_hash(user.password, form['password']) and user.nickname is not "Guest": @@ -1116,7 +1104,7 @@ def login(): return redirect_back(url_for("web.index")) else: ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr) - app.logger.info('Login failed for user "' + form['username'] + '" IP-adress: ' + ipAdress) + log.info('Login failed for user "%s" IP-adress: %s', form['username'], ipAdress) flash(_(u"Wrong Username or Password"), category="error") next_url = url_for('web.index') @@ -1263,7 +1251,7 @@ def profile(): val += int(key[5:]) current_user.sidebar_view = val if "Show_detail_random" in to_save: - current_user.sidebar_view += ub.DETAIL_RANDOM + current_user.sidebar_view += constants.DETAIL_RANDOM current_user.mature_content = "Show_mature_content" in to_save @@ -1297,9 +1285,9 @@ def read_book(book_id, book_format): # check if book has bookmark bookmark = None if current_user.is_authenticated: - bookmark = ub.session.query(ub.Bookmark).filter(ub.and_(ub.Bookmark.user_id == int(current_user.id), - ub.Bookmark.book_id == book_id, - ub.Bookmark.format == book_format.upper())).first() + bookmark = ub.session.query(ub.Bookmark).filter(and_(ub.Bookmark.user_id == int(current_user.id), + ub.Bookmark.book_id == book_id, + ub.Bookmark.format == book_format.upper())).first() if book_format.lower() == "epub": return render_title_template('read.html', bookid=book_id, title=_(u"Read a Book"), bookmark=bookmark) elif book_format.lower() == "pdf": @@ -1350,15 +1338,14 @@ def show_book(book_id): if not current_user.is_anonymous: if not config.config_read_column: matching_have_read_book = ub.session.query(ub.ReadBook).\ - filter(ub.and_(ub.ReadBook.user_id == int(current_user.id), ub.ReadBook.book_id == book_id)).all() + filter(and_(ub.ReadBook.user_id == int(current_user.id), ub.ReadBook.book_id == book_id)).all() have_read = len(matching_have_read_book) > 0 and matching_have_read_book[0].is_read else: try: matching_have_read_book = getattr(entries, 'custom_column_'+str(config.config_read_column)) have_read = len(matching_have_read_book) > 0 and matching_have_read_book[0].value except KeyError: - app.logger.error( - u"Custom Column No.%d is not exisiting in calibre database" % config.config_read_column) + log.error("Custom Column No.%d is not exisiting in calibre database", config.config_read_column) have_read = None else: @@ -1373,7 +1360,7 @@ def show_book(book_id): audioentries = [] for media_format in entries.data: - if media_format.format.lower() in EXTENSIONS_AUDIO: + if media_format.format.lower() in constants.EXTENSIONS_AUDIO: audioentries.append(media_format.format.lower()) return render_title_template('detail.html', entry=entries, audioentries=audioentries, cc=cc, diff --git a/cps/worker.py b/cps/worker.py index 8da30885..3d5f058a 100644 --- a/cps/worker.py +++ b/cps/worker.py @@ -17,21 +17,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import print_function -import smtplib -import threading -from datetime import datetime -import logging -import time -import socket +from __future__ import division, print_function, unicode_literals import sys import os -from email.generator import Generator -from . import config, db, app -from flask_babel import gettext as _ import re -from .gdriveutils import getFileFromEbooksFolder, updateGdriveCalibreFromLocal -from .subproc_wrapper import process_open +import smtplib +import socket +import time +import threading +from datetime import datetime try: from StringIO import StringIO @@ -47,6 +41,14 @@ except ImportError: from email import encoders from email.utils import formatdate from email.utils import make_msgid +from email.generator import Generator +from flask_babel import gettext as _ + +from . import logger, config, db, gdriveutils +from .subproc_wrapper import process_open + + +log = logger.create() chunksize = 8192 # task 'status' consts @@ -70,7 +72,7 @@ def get_attachment(bookpath, filename): """Get file as MIMEBase message""" calibrepath = config.config_calibre_dir if config.config_use_google_drive: - df = getFileFromEbooksFolder(bookpath, filename) + df = gdriveutils.getFileFromEbooksFolder(bookpath, filename) if df: datafile = os.path.join(calibrepath, bookpath, filename) if not os.path.exists(os.path.join(calibrepath, bookpath)): @@ -88,8 +90,8 @@ def get_attachment(bookpath, filename): data = file_.read() file_.close() except IOError as e: - app.logger.exception(e) # traceback.print_exc() - app.logger.error(u'The requested file could not be read. Maybe wrong permissions?') + log.exception(e) # traceback.print_exc() + log.error(u'The requested file could not be read. Maybe wrong permissions?') return None attachment = MIMEBase('application', 'octet-stream') @@ -114,7 +116,7 @@ class emailbase(): def send(self, strg): """Send `strg' to the server.""" - app.logger.debug('send:' + repr(strg[:300])) + log.debug('send: %r', strg[:300]) if hasattr(self, 'sock') and self.sock: try: if self.transferSize: @@ -139,7 +141,7 @@ class emailbase(): raise smtplib.SMTPServerDisconnected('please run connect() first') def _print_debug(self, *args): - app.logger.debug(args) + log.debug(args) def getTransferStatus(self): if self.transferSize: @@ -236,7 +238,7 @@ class WorkerThread(threading.Thread): filename = self._convert_ebook_format() if filename: if config.config_use_google_drive: - updateGdriveCalibreFromLocal() + gdriveutils.updateGdriveCalibreFromLocal() if curr_task == TASK_CONVERT: self.add_email(self.queue[self.current]['settings']['subject'], self.queue[self.current]['path'], filename, self.queue[self.current]['settings'], self.queue[self.current]['kindle'], @@ -254,14 +256,14 @@ class WorkerThread(threading.Thread): # if it does - mark the conversion task as complete and return a success # this will allow send to kindle workflow to continue to work if os.path.isfile(file_path + format_new_ext): - app.logger.info("Book id %d already converted to %s", bookid, format_new_ext) + log.info("Book id %d already converted to %s", bookid, format_new_ext) cur_book = db.session.query(db.Books).filter(db.Books.id == bookid).first() self.queue[self.current]['path'] = file_path self.queue[self.current]['title'] = cur_book.title self._handleSuccess() return file_path + format_new_ext else: - app.logger.info("Book id %d - target format of %s does not exist. Moving forward with convert.", bookid, format_new_ext) + log.info("Book id %d - target format of %s does not exist. Moving forward with convert.", bookid, format_new_ext) # check if converter-executable is existing if not os.path.exists(config.config_converterpath): @@ -317,13 +319,13 @@ class WorkerThread(threading.Thread): if conv_error: error_message = _(u"Kindlegen failed with Error %(error)s. Message: %(message)s", error=conv_error.group(1), message=conv_error.group(2).strip()) - app.logger.debug("convert_kindlegen: " + nextline) + log.debug("convert_kindlegen: %s", nextline) else: while p.poll() is None: nextline = p.stdout.readline() if os.name == 'nt' and sys.version_info < (3, 0): nextline = nextline.decode('windows-1252') - app.logger.debug(nextline.strip('\r\n')) + log.debug(nextline.strip('\r\n')) # parse progress string from calibre-converter progress = re.search("(\d+)%\s.*", nextline) if progress: @@ -353,7 +355,7 @@ class WorkerThread(threading.Thread): return file_path + format_new_ext else: error_message = format_new_ext.upper() + ' format not found on disk' - app.logger.info("ebook converter failed with error while converting book") + log.info("ebook converter failed with error while converting book") if not error_message: error_message = 'Ebook converter failed with unknown error' self._handleError(error_message) @@ -449,7 +451,7 @@ class WorkerThread(threading.Thread): # _print_debug function if sys.version_info < (3, 0): org_smtpstderr = smtplib.stderr - smtplib.stderr = StderrLogger() + smtplib.stderr = logger.StderrLogger('worker.smtp') if use_ssl == 2: self.asyncSMTP = email_SSL(obj['settings']["mail_server"], obj['settings']["mail_port"], timeout) @@ -457,9 +459,7 @@ class WorkerThread(threading.Thread): self.asyncSMTP = email(obj['settings']["mail_server"], obj['settings']["mail_port"], timeout) # link to logginglevel - if config.config_log_level != logging.DEBUG: - self.asyncSMTP.set_debuglevel(0) - else: + if logger.is_debug_enabled(): self.asyncSMTP.set_debuglevel(1) if use_ssl == 1: self.asyncSMTP.starttls() @@ -501,7 +501,7 @@ class WorkerThread(threading.Thread): return retVal def _handleError(self, error_message): - app.logger.error(error_message) + log.error(error_message) self.UIqueue[self.current]['stat'] = STAT_FAIL self.UIqueue[self.current]['progress'] = "100 %" self.UIqueue[self.current]['runtime'] = self._formatRuntime( @@ -513,22 +513,3 @@ class WorkerThread(threading.Thread): self.UIqueue[self.current]['progress'] = "100 %" self.UIqueue[self.current]['runtime'] = self._formatRuntime( datetime.now() - self.queue[self.current]['starttime']) - - -# Enable logging of smtp lib debug output -class StderrLogger(object): - - buffer = '' - - def __init__(self): - self.logger = app.logger - - def write(self, message): - try: - if message == '\n': - self.logger.debug(self.buffer.replace("\n","\\n")) - self.buffer = '' - else: - self.buffer += message - except: - self.logger.debug("Logging Error")