Merge remote-tracking branch 'constants/Develop-logging-cleanup' into Develop

This commit is contained in:
Ozzieisaacs 2019-06-08 06:52:41 +02:00
commit 50973ffb72
35 changed files with 962 additions and 832 deletions

10
cps.py
View File

@ -17,14 +17,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -20,29 +20,28 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
__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']

View File

@ -21,29 +21,30 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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,

View File

@ -21,29 +21,32 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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 "admin_role" in to_save:
content.role |= constants.ROLE_ADMIN
else:
content.role &= ~constants.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 "download_role" in to_save:
content.role |= constants.ROLE_DOWNLOAD
else:
content.role &= ~constants.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 "viewer_role" in to_save:
content.role |= constants.ROLE_VIEWER
else:
content.role &= ~constants.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 "upload_role" in to_save:
content.role |= constants.ROLE_UPLOAD
else:
content.role &= ~constants.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 "edit_role" in to_save:
content.role |= constants.ROLE_EDIT
else:
content.role &= ~constants.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 "delete_role" in to_save:
content.role |= constants.ROLE_DELETE_BOOKS
else:
content.role &= ~constants.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 "passwd_role" in to_save:
content.role |= constants.ROLE_PASSWD
else:
content.role &= ~constants.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
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')]
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

View File

@ -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, "")

View File

@ -18,9 +18,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -17,17 +17,21 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -17,14 +17,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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():

View File

@ -18,16 +18,20 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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'):

View File

@ -21,28 +21,25 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# 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 uuid import uuid4
from . import helper
from .helper import order_authors, common_filters
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
from uuid import uuid4
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 . 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
editbook = Blueprint('editbook', __name__)
EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt'}
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)

View File

@ -17,11 +17,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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'),

View File

@ -17,7 +17,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
from lxml import etree
from .constants import BookMeta

View File

@ -20,26 +20,31 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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 ''

View File

@ -17,23 +17,35 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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)

View File

@ -18,40 +18,30 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -17,6 +17,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
try:
from iso639 import languages, __version__
get = languages.get

View File

@ -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')

125
cps/logger.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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")

View File

@ -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

View File

@ -21,30 +21,34 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
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 oauth import OAuthBackend
from sqlalchemy.orm.exc import NoResultFound
from flask import session, request, make_response, abort
from __future__ import division, print_function, unicode_literals
import json
from cps import config, app
import ub
from flask_login import login_user, current_user
from functools import wraps
from oauth import OAuthBackend
from flask import session, request, make_response, abort
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'))

View File

@ -21,22 +21,24 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# 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
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 werkzeug.security import check_password_hash
from .helper import fill_indexpage, get_download_link, get_book_cover
from __future__ import division, print_function, unicode_literals
import sys
import datetime
from functools import wraps
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
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:

View File

@ -21,6 +21,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
from math import ceil

View File

@ -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

View File

@ -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

View File

@ -17,12 +17,11 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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()
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)
self.wsgiserver.serve_forever()
ssl_args = self.ssl_args or {}
log.info('Starting Gevent server')
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)
try:
sock = self._make_gevent_socket()
self.wsgiserver = WSGIServer(sock, self.app, spawn=Pool(), **ssl_args)
self.wsgiserver.serve_forever()
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)

View File

@ -21,28 +21,34 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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/<int:shelf_id>/<int:book_id>")
@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/<int:shelf_id>")
@ -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) \

View File

@ -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;

View File

@ -16,9 +16,11 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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):

View File

@ -54,16 +54,16 @@
<div class="discover load-more">
<h2 class="{{title}}">{{_(title)}}</h2>
<div class="filterheader hidden-xs hidden-sm">
<a id="new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='new')}}"><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a id="old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='old')}}"><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
<a data-toggle="tooltip" id="new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='new')}}"><span class="glyphicon glyphicon-sort-by-attributes"></span></a>
<a id="old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='old')}}"><span class="glyphicon glyphicon-sort-by-attributes-alt"></span></a>
<a id="asc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
<a id="desc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
<a id="pub_new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a id="pub_old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
</div>
<div class="btn-group character">
<a id="no_shelf" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='pubold')}}"><span class="glyphicon glyphicon-list"></span><b>{{_('Group by series')}}</b></a>
<div class="btn-group character">
<a id="no_shelf" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='pubold')}}"><span class="glyphicon glyphicon-list"></span> <b>{{_('Group by series')}}</b></a>
</div>
</div>
<div class="row">
{% if entries[0] %}

251
cps/ub.py
View File

@ -18,79 +18,36 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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
from . import constants, logger, cli
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
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 '<User %r>' % 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 '<Shelf %r>' % self.name
return '<Shelf %d:%r>' % (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:

View File

@ -17,22 +17,28 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from . import config, get_locale, Server, app
import threading
import zipfile
import requests
import time
from io import BytesIO
import os
from __future__ import division, print_function, unicode_literals
import sys
import shutil
from ub import UPDATE_STABLE
from tempfile import gettempdir
import os
import datetime
import json
from flask_babel import gettext as _
import requests
import shutil
import threading
import time
import zipfile
from io import BytesIO
from tempfile import gettempdir
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 = {

View File

@ -17,13 +17,19 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -21,35 +21,39 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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,

View File

@ -17,21 +17,15 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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")